Closed5
柔軟にタグを指定できる Button.tsx コンポーネントに型をつけたい
欲しい仕様
- as で HTML タグを指定できる。デフォルトは
button
にしたい。また onClick などHTMLが持つ attributes は props で毎回指定したくない。
<Button className="mt-2" as="button" onClick={onClick}>
ボタンテキスト
</Button>
<Button className="mt-2" as="a" onClick={onClick}>
ボタンテキスト
</Button>
- ただし、as で指定できる HTML タグを a | button のみ制限したい
// as にエラーついて欲しい
<Button className="mt-2" as="div" onClick={onClick}>
ボタンテキスト
</Button>
- as のHTMLタグで使用できない attributes はエラーになって欲しい
// href にエラー欲しい
<Button className="mt-2" href="https://zenn.dev" as="button" onClick={onClick}>
ボタンテキスト
</Button>
// href はエラー無し
<Button className="mt-2" href="https://zenn.dev" as="a" onClick={onClick}>
ボタンテキスト
</Button>
- できれば、as に "next/Link" も nextjs の component も指定したい
<Button className="mt-2" href="https://zenn.dev" as="Link" onClick={onClick}>
ボタンテキスト
</Button>
ここまで button.tsx 書いたけど、Tag の部分でエラー出ちゃう。
誰かぁ〜。わかんないよ〜。教えてん
import classnames from 'classnames'
import { ReactNode } from 'react'
type Tags = 'button' | 'a'
type Props<T extends Tags> = JSX.IntrinsicElements[T] & {
as?: T
children?: ReactNode
className?: string
}
export const Button = <T extends Tags = 'button'>({ as, children, className, ...props }: Props<T>) => {
const Tag = as || 'button'
return (
<Tag className={classnames(`btn`, className)} {...props}>
{children}
</Tag>
)
}
以下、TS plauground
createElement を使うとエラーでなくて、 1-3 までの仕様は実装できてる(気がする)が、なんで?
import classnames from 'classnames'
import { createElement, ReactNode } from 'react'
type Tags = 'button' | 'a'
type Props<T extends Tags> = JSX.IntrinsicElements[T] & {
as?: T
children?: ReactNode
className?: string
}
export const Button = <T extends Tags = 'button'>({ as, children, className, ...props }: Props<T>) => {
return createElement(
as || 'button',
{
...props,
class: classnames(`btn`, className),
},
children
)
}
next/link の Link に as という attributes あるやん..
as を tag に変更
できたかも?!!!
Link タグで囲うのを、単にゴリ押して分岐させてるけど、もっと cool な書き方ないんかな?
button.tsx
import classnames from 'classnames'
import Link, { LinkProps } from 'next/link'
import { createElement, ReactNode } from 'react'
type Tags = 'button' | 'a'
type Element<T extends Tags | 'Link'> = T extends Tags
? JSX.IntrinsicElements[T]
: LinkProps & JSX.IntrinsicElements['button']
type Props<T extends Tags | 'Link'> = Element<T> & {
tag?: T | 'Link'
children?: ReactNode
className?: string
}
export const Button = <T extends Tags | 'Link'>({ tag, size, color, children, className, ...props }: Props<T>) => {
const attrs = {
...props,
class: classnames(`btn`, className),
}
const tagName = tag === 'Link' ? 'a' : tag || 'button'
return tag === 'Link' ? (
<Link href="/" {...props}>
{createElement(tagName, attrs, children)}
</Link>
) : (
createElement(tagName, attrs, children)
)
}
このスクラップは2022/05/01にクローズされました