صناعة زر متعدد الأستخدامات (Tailwind CSS, React)

Mohanad Alrwaihy

June 23, 2023

134

0

يستغرق بناء المكونات من الصفر الكثير من الوقت ولهذا السبب من المهم أن يكون لديك طريقة منظمة لإنشاء المكونات لأنها قد تصبح معقدة ويصعب الحفاظ عليها بمرور الوقت.

4 دقائق للقراءة


سأناقش اليوم كيف يمكننا إنشاء زر (Button) ذو مظهر رائع مع TailwindCSS مع العديد من الخيارات المتوفرة في الزر لأضافته في تطبيقك.

نظرة عامة

نظرة عامة لكيفية شكل الزر النهائي وجميع الخيارات المتوفرة 👇

المتطلبات الضرورية

مكتبة Class Variance Authority

تقوم هذه المكتبة بإنشاء متغيرات لأي مكون من خلال توفير القابلية لأضافة خيارات متعددة في العنصر مثل نوع المتغير والحجم والشكل وأي نوع آخر من المعلومات المستخدمة لإنشاء متغير فريد.

تثبيت

POWERSHELL
npm install class-variance-authority

أضافة Tailwind Merge

هذه الأضافة توفر فنكشن مهم يساعدنا في دمع كلاسات TailwindCSS مع بعضها بدون اي تعارض في التصميم

تثبيت

POWERSHELL
npm install tailwind-merge

الخطوات

الزر الأساسي (Basic Button)

لنبدأ أولاً بإنشاء Button أساسي وإعطائه الprops types الصحيحة حتى نتمكن فقط من إرسال السمات المضمنة في عنصر ال HTML Button 👇

components/ui/Button.tsxTSX
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement>

export const Button = ({ children, ...rest }: ButtonProps) => {
  return <button {...rest}>{children}</button>
}

إضافة عنصر الButton وأستخدامه 👇

App.tsxTSX
import { Button } from './components/ui/button'

export default function App(){
	return (
		<Button>Primary</Button>
	)
}

إضافة forwardRef

استخدم forwardRef للسماح لمكون Button بكشف الDOM Node إلى المكون الأصلي باستخدام ref 👇

components/ui/Button.tsxTSX
import { forwardRef } from 'react'

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ children, ...rest }, ref) => {
    return (
      <button ref={ref} {...rest}>
        {children}
      </button>
    )
  }
)

الآن يمكننا استخدام الهوك useRef وتمرير المرجع إلى مكون Button إذا أردنا 👇

App.tsxTSX
import { 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.tsxTSX
type 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👇

POWERSHELL
import { cva, type VariantProps } from 'class-variance-authority'

إنشاء متغير الbuttonVariance 👇

components/ui/Button.tsxTSX
const 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 👇

TSX
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> &
  VariantProps<typeof buttonVariants> & {
    loading?: boolean
    icon?: React.ReactNode
  }

الآن دعنا نستخرج المتغيرات التي أنشأناها والتي هي variant, size, outline من الbutton props 👇

TSX
export 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.tsxTSX
import { 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.tsxTSX
const 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