صناعة زر متعدد الأستخدامات (Tailwind CSS, React)
Mohanad Alrwaihy
June 23, 2023
134
0
يستغرق بناء المكونات من الصفر الكثير من الوقت ولهذا السبب من المهم أن يكون لديك طريقة منظمة لإنشاء المكونات لأنها قد تصبح معقدة ويصعب الحفاظ عليها بمرور الوقت.
4 دقائق للقراءة
سأناقش اليوم كيف يمكننا إنشاء زر (Button) ذو مظهر رائع مع TailwindCSS مع العديد من الخيارات المتوفرة في الزر لأضافته في تطبيقك.
نظرة عامة
نظرة عامة لكيفية شكل الزر النهائي وجميع الخيارات المتوفرة 👇
المتطلبات الضرورية
مكتبة Class Variance Authority
تقوم هذه المكتبة بإنشاء متغيرات لأي مكون من خلال توفير القابلية لأضافة خيارات متعددة في العنصر مثل نوع المتغير والحجم والشكل وأي نوع آخر من المعلومات المستخدمة لإنشاء متغير فريد.
تثبيت
POWERSHELLnpm install class-variance-authorityأضافة Tailwind Merge
هذه الأضافة توفر فنكشن مهم يساعدنا في دمع كلاسات TailwindCSS مع بعضها بدون اي تعارض في التصميم
تثبيت
POWERSHELLnpm install tailwind-mergeالخطوات
الزر الأساسي (Basic Button)
لنبدأ أولاً بإنشاء Button أساسي وإعطائه الprops types الصحيحة حتى نتمكن فقط من إرسال السمات المضمنة في عنصر ال HTML Button 👇
components/ui/Button.tsxTSXtype ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement>
export const Button = ({ children, ...rest }: ButtonProps) => {
return <button {...rest}>{children}</button>
}إضافة عنصر الButton وأستخدامه 👇
App.tsxTSXimport { Button } from './components/ui/button'
export default function App(){
return (
<Button>Primary</Button>
)
}إضافة forwardRef
استخدم forwardRef للسماح لمكون Button بكشف الDOM Node إلى المكون الأصلي باستخدام ref 👇
components/ui/Button.tsxTSXimport { forwardRef } from 'react'
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ children, ...rest }, ref) => {
return (
<button ref={ref} {...rest}>
{children}
</button>
)
}
)الآن يمكننا استخدام الهوك useRef وتمرير المرجع إلى مكون Button إذا أردنا 👇
App.tsxTSXimport { Button } from './components/ui/button'
import { useEffect, useRef } from 'react'
export default function App(){
const buttonRef = useRef<HTMLButtonElement | null>(null)
useEffect(() => {
if (buttonRef.current) {
buttonRef.current?.focus()
}
}, [])
return (
<Button ref={buttonRef}>Primary</Button>
)
}إضافة علامة تحميل & ايقونة
يمكن استخدام الأزرار أحيانًا في النماذج أو لجلب البيانات ونحتاج إلى إخبار المستخدم إذا كان عليه الانتظار لشيء ما. يمكننا إضافة علامة تحميل داخل الزر الذي يمكننا إظهاره عند ارسال الLoading Prop بقيمة True الى الButton ويمكننا أيضا أضافة ايقونة بجانب النص بنفس الطريقة 👇
components/ui/Button.tsxTSXtype ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
loading?: boolean
icon?: React.ReactNode
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ children, loading, icon, ...rest }, ref) => {
return (
<button ref={ref} {...rest}>
{loading && <ButtonLoader />}
<span
className={`inline-flex items-center justify-center gap-2 transition-opacity ${
loading ? 'opacity-0' : 'opacity-100'
}`}
>
{children}
</span>
</button>
)
}
)
function ButtonLoader() {
return (
<div
className='absolute inline-flex h-5 w-5 animate-spin items-center rounded-full border-[3px] border-current border-t-transparent text-center leading-6 text-gray-200'
role='status'
aria-label='loading'
>
<span className='sr-only'>Loading...</span>
</div>
)
}لا تنس إستخراج الProps الخاصة بالLoading والIcon وإضافتها إلى ButtonProps كحقول اختيارية.
يمكنك أن ترى أنني أضفت span داخل الزر لعرض الChildren المرسلة داخل الزر وايضا اضفت شروط للتصميم في حالة كان الLoading بقيمة True عندها سوف اخفي الChildren وأعرض فقط علامة التحميل وهذه الطريقة تمنع من أن يحدث إي تغير في حجم الButton.
App.tsxTSX<Button
ref={buttonRef}
icon={<HeartIcon/>}
loading={true}
className='bg-purple-600 rounded-xl p-2 flex flex-col items-center justify-center'
>
Sign In
</Button>
function HeartIcon() {
return (
<svg
fill='none'
stroke='currentColor'
strokeWidth='1.5'
viewBox='0 0 24 24'
xmlns='http://www.w3.org/2000/svg'
aria-hidden='true'
<path
strokeLinecap='round'
strokeLinejoin='round'
d='M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607- 2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z'
> </path>
</svg>
)
}إضافة Class Variance Authority (CVA)
لنبدأ بإنشاء الزر الحقيقي بجميع الخيارات!
نحن بحاجة إلى استيراد cva و VariantProps من class-variance-authority👇
POWERSHELLimport { cva, type VariantProps } from 'class-variance-authority'إنشاء متغير الbuttonVariance 👇
components/ui/Button.tsxTSXconst buttonVariants = cva(
// Basic Styles
'',
{
// Type of variants
variants: {
// Button colors variant
variant: {
primary: '',
secondary: '',
destructive: '',
success: '',
ghost: '',
outline: '',
link: '',
},
// Button size variants
size: {
sm: '',
default: '',
lg: '',
},
// Outline variants
outline: {
default: '',
outline: '',
},
},
// The default variant if not specified.
defaultVariants: { variant: 'primary', size: 'default' },
// Styles applied when two variants or more are met.
compoundVariants: [
{
variant: 'primary',
outline: 'outline',
class: '',
},
],
}
)
هذا متغير طويل ولكنه واضح ومباشر للغاية ويمكنك بسهولة فهم مكان إضافة الخيارات وكيفية إضافة متغيرات وشروط جديدة!
أضف الآن VariantProps إلى ButtonProps 👇
TSXtype ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> &
VariantProps<typeof buttonVariants> & {
loading?: boolean
icon?: React.ReactNode
}
الآن دعنا نستخرج المتغيرات التي أنشأناها والتي هي variant, size, outline من الbutton props 👇
TSXexport const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ children, loading, icon, variant, size, outline, ...rest }, ref) => {
return (
<button ref={ref} {...rest}>
...
</button>
)
}
)إضافة كلاسات مع دمج Tailwind (twMerge)
لدينا كل شيء تم تعيينه الآن ونحتاج إلى إضافة الكلاسات ودمجها بإستخدام twMerge 👇
components/ui/Button.tsxTSXimport { twMerge } from 'tailwind-merge'
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ children, className, loading, icon, variant, size, outline, ...rest }, ref) => {
return (
<button ref={ref}
className={twMerge(buttonVariants({ variant, size, outline, className }))}
{...rest}
>
...
</button>
)
}
)قم بإستخراج className من الButton props ثم باستخدام twMerge سنقوم بدمج جميع خيارات الvariant props التي تم تمريرها معًا وإضافة className في النهاية لإضافة فئات إضافية إلى الزر أو تجاوزها.
الخطوة الوحيدة التي لدينا هي إضافة الكلاسات نفسها إلى متغير buttonVariants 👇
components/ui/Button.tsxTSXconst buttonVariants = cva(
// Basic Styles
'relative inline-flex items-center justify-center cursor-pointer rounded-xl tracking-wide shadow hover:shadow-md shadow-white/20 disabled:shadow disabled:cursor-not-allowed outline-none focus-visible:ring-2 ring-offset-4 ring-offset-zinc-900 focus:scale-[0.95] transition border-2 border-white/20',
{
// Type of variants
variants: {
// Button colors variant
variant: {
primary:
'bg-purple-600 hover:bg-purple-700 disabled:bg-purple-700/60 text-white ring-purple-600',
secondary:
'bg-gray-600 hover:bg-gray-700 disabled:bg-gray-700/60 text-white ring-gray-600',
destructive:
'bg-pink-800 hover:bg-pink-900 disabled:bg-pink-800/60 text-white ring-pink-800',
success:
'bg-emerald-600 hover:bg-emerald-700 disabled:bg-emerald-600/60 text-white ring-emerald-600',
ghost:
'bg-transparent shadow-none border-none hover:bg-gray-600 ring-gray-600',
outline: 'bg-transparent shadow-none hover:bg-gray-700 ring-gray-700',
link: 'bg-transparent shadow-none border-none text-purple-600',
},
// Button size variants
size: {
sm: 'py-0.5 px-2 text-xs md:text-sm font-bold',
default: 'py-2 px-4 text-sm md:text-base font-medium',
lg: 'py-3.5 px-6 md:text-lg font-medium',
},
// Outline variants
outline: {
default: '',
outline: 'bg-transparent',
},
},
// The default variant if not specified.
defaultVariants: { variant: 'primary', size: 'default' },
// Styles applied when two variants or more are met.
compoundVariants: [
{
variant: 'primary',
outline: 'outline',
class: 'text-purple-600 border-purple-600 hover:text-white',
},
{
variant: 'secondary',
outline: 'outline',
class: 'text-gray-200 border-gray-600 hover:text-white',
},
{
variant: 'destructive',
outline: 'outline',
class: 'text-pink-600 border-pink-600 hover:text-white',
},
{
variant: 'success',
outline: 'outline',
class: 'text-emerald-600 border-emerald-600 hover:text-white',
},
],
}
)
أمثلة
من السهل جدًا اختيار نوع الزر وحجمه ويكون لديه حدود أو لا، كما يمكننا إضافة كلاسات الأزرار إلى المكونات الأخرى هنا بعض الأمثلة.
أختيار نوع الزر
هذه هي الطريقة التي يمكننا استخدامها للأختيار بين المتغيرات المختلفة 👇
TSX// Choose between variants
<Button>Primary</Button> // Default variant (primary)
<Button variant='primary'>Primary</Button>
<Button variant='secondary'>Secondary</Button>
<Button variant='destructive'>Destructive</Button>
<Button variant='success'>Success</Button>
<Button variant='ghost'>Ghost</Button>
<Button variant='outline'>Outline</Button>
<Button variant='link'>Link</Button>أختر الحجم
الاختيار بين الأحجام واضح أيضًا 👇
JSX<Button>Default Size</Button> // Default Size (default)
<Button size='sm'>Small</Button>
<Button size='default'>Default</Button>
<Button size='lg'>Large</Button>أختر إضافة حدود
ستؤدي إضافة الحواف إلى جعل خلفية الزر شفافة وتغيير ألوان الحدود والنص 👇
TSX<Button>No Outline</Button> // Default Outline (default)
<Button outline='default'>No Outline</Button>
<Button outline='outlien'>Outline</Button>أستخدام مع HTML Tags مختلفة
يمكن تصدير متغير buttonVariants واستخدامه مع أي علامة html لتطبيق كلاسات الزر عليه ولكن علينا تصديره أولاً 👇
TSX// components/ui/Button.tsx
export {Button, buttonVariants}والآن يمكننا استخدام buttonVariants لإضافة الكلاسات إلى a على سبيل المثال 👇
TSX<a
href='/'
className={buttonVariants({ variant: 'primary' })}
>
A Tag + Primary Button Style
</a>الخاتمة
هذا كل ما لدي آمل أن تكون قد تعلمت شيئًا جديدًا اليوم ويمكنك تطبيقه في مشاريعك حيث يمكن أن تكون طريقة إنشاء المتغيرات مفيدة جدًا في إنشاء أنواع أخرى من المكونات ليس فقط الأزرار لأن Class Variance Authority (CVA) يمكن استخدامها أيضًا في إنشاء محتوى نصي مع خيارات مختلفة لأن CVA يمكن النظر إليه على أنها مجرد طريقة أنيقة لإدارة الStrings