الحقل
التسمية والتلميح والخطأ حول أي عنصر تحكم في النموذج، مربوطة به بالمعرّفات في HTML الخادم قبل تشغيل أي سكربت.
split - الحجم لم يُقَس بعد
الحقل
split - الحجم لم يُقَس بعدالتسمية والتلميح والخطأ حول أي عنصر تحكم في النموذج، مربوطة به بالمعرّفات في HTML الخادم قبل تشغيل أي سكربت.
الشيفرة
field.tsx
import { Input } from 'topclassui/input'
import { Field } from 'topclassui/field'
/** The label points at the input, and the input is named by it before any script runs. */
export default function Demo() {
return (
<div className="grid max-w-sm">
<Field name="email" label="Email">
{(control) => <Input {...control} type="email" autoComplete="email" fullWidth />}
</Field>
</div>
)
}
لا يوجد مكوّن Label: التسمية هي هذه الخاصية، والابن ينشر كائن التحكم على عنصر التحكم.
أو استورده من الحزمة
topclassui/field
import { Field } from 'topclassui/field'ثبّت مرة واحدة:npm i topclassui@beta zod
أمثلة
- في ملف خادم استخدم
Field؛ وفي ملف عميل يعرضFieldLiveنفس HTML ويُظهر الخطأ الذي أعاده أقربFormActionلاسمهname. - الابن دالة،
(control) => <Input {...control} />. كائن التحكم هوidوnameوrequiredوسمات ARIA الثلاث، وكل عنصر من عناصر التحكم الأحد عشر يأخذه كما هو. - المعرّفات تأتي من
idأوuseId()، لا منnameأبدًا: قد يتشارك حقلان الاسم نفسه. مرّرidحين ترتبط صفحة بالحقل. errorرسالة واحدة أو المصفوفة التي يعطيهاfieldErrorsفي Zod؛ ومع رسالة يصبح العنصر غير صالح ما لم يقلinvalidغير ذلك.inlineيضع عنصر التحكم أولًا والتسمية بجانبه، لمربع الاختيار أو المفتاح.
basic
basic.tsx
import { Input } from 'topclassui/input'
import { Field } from 'topclassui/field'
/** The label points at the input, and the input is named by it before any script runs. */
export default function Demo() {
return (
<div className="grid max-w-sm">
<Field name="email" label="Email">
{(control) => <Input {...control} type="email" autoComplete="email" fullWidth />}
</Field>
</div>
)
}
hint-error
Letters and numbers only
That name is taken
Shown on your profile
Too short
Add at least one sentence
Optional
hint-error.tsx
import { Input } from 'topclassui/input'
import { Textarea } from 'topclassui/textarea'
import { Field } from 'topclassui/field'
/** A hint, one error, and the array Zod's fieldErrors gives: the control reads the hint and then the errors. */
export default function Demo() {
return (
<div className="grid max-w-sm gap-4">
<Field name="username" label="Username" hint="Letters and numbers only" error="That name is taken">
{(control) => <Input {...control} defaultValue="omar" autoComplete="username" fullWidth />}
</Field>
<Field name="bio" label="About you" hint="Shown on your profile" error={['Too short', 'Add at least one sentence']}>
{(control) => <Textarea {...control} defaultValue="Hi" fullWidth />}
</Field>
<Field name="website" label="Website" hint="Optional">
{(control) => <Input {...control} type="url" fullWidth />}
</Field>
</div>
)
}
required
We never share it
required.tsx
import { Checkbox } from 'topclassui/checkbox'
import { Input } from 'topclassui/input'
import { Field } from 'topclassui/field'
/** The asterisk is only seen; the control says required itself: a native attribute, or aria-required on a Checkbox. */
export default function Demo() {
return (
<div className="grid max-w-sm gap-4">
<Field name="email" label="Email" hint="We never share it" required>
{(control) => <Input {...control} type="email" autoComplete="email" fullWidth />}
</Field>
<Field name="terms" label="I accept the terms" required inline>
{(control) => <Checkbox {...control} />}
</Field>
</div>
)
}
inline
About once a month
Turn it on to get the summary
inline.tsx
import { Checkbox } from 'topclassui/checkbox'
import { Switch } from 'topclassui/switch'
import { Field } from 'topclassui/field'
/** inline: the box first and the label beside it, the hint and the error under the label. A click on a label toggles its box. */
export default function Demo() {
return (
<div className="grid max-w-sm gap-4">
<Field name="news" label="Send me product news" hint="About once a month" inline>
{(control) => <Checkbox {...control} />}
</Field>
<Field name="digest" label="Weekly digest" error="Turn it on to get the summary" inline>
{(control) => <Switch {...control} />}
</Field>
</div>
)
}
group
You can change it later
group.tsx
import { RadioGroupItem, RadioGroupRoot } from 'topclassui/radio-group'
import { Field } from 'topclassui/field'
/** A group no `<label for>` can name: the control object's aria-labelledby names it, and its hint describes it. */
export default function Demo() {
return (
<div className="grid max-w-sm">
<Field name="plan" label="Plan" hint="You can change it later" required>
{(control) => (
<RadioGroupRoot {...control} defaultValue="pro">
<RadioGroupItem value="free">Free</RadioGroupItem>
<RadioGroupItem value="pro">Pro</RadioGroupItem>
<RadioGroupItem value="team">Team</RadioGroupItem>
</RadioGroupRoot>
)}
</Field>
</div>
)
}
controls
controls.tsx
import { Checkbox } from 'topclassui/checkbox'
import { Combobox } from 'topclassui/combobox'
import { Input } from 'topclassui/input'
import { NumberField } from 'topclassui/number-field'
import { OTPField } from 'topclassui/otp-field'
import { RadioGroupItem, RadioGroupRoot } from 'topclassui/radio-group'
import { SegmentedControlItem, SegmentedControlRoot } from 'topclassui/segmented-control'
import { Select } from 'topclassui/select'
import { Slider } from 'topclassui/slider'
import { Switch } from 'topclassui/switch'
import { Textarea } from 'topclassui/textarea'
import { Field } from 'topclassui/field'
const COUNTRIES = [{ value: 'jo', label: 'Jordan' }, { value: 'eg', label: 'Egypt' }, { value: 'ma', label: 'Morocco' }]
/** The eleven controls, each spreading one control object as it is: each is named by the label, described by the hint
* and then the error, and invalid, in the server HTML and once hydrated. */
export default function Demo() {
return (
<form className="grid max-w-sm gap-5">
<Field name="email" label="Email" hint="Input hint" error="Input error" required>
{(control) => <Input {...control} type="email" fullWidth />}
</Field>
<Field name="bio" label="Bio" hint="Textarea hint" error="Textarea error">
{(control) => <Textarea {...control} fullWidth />}
</Field>
<Field name="terms" label="Terms" hint="Checkbox hint" error="Checkbox error" required inline>
{(control) => <Checkbox {...control} />}
</Field>
<Field name="digest" label="Digest" hint="Switch hint" error="Switch error" inline>
{(control) => <Switch {...control} />}
</Field>
<Field name="plan" label="Plan" hint="RadioGroup hint" error="RadioGroup error" required>
{(control) => (
<RadioGroupRoot {...control} orientation="horizontal">
<RadioGroupItem value="free">Free</RadioGroupItem>
<RadioGroupItem value="pro">Pro</RadioGroupItem>
</RadioGroupRoot>
)}
</Field>
<Field name="view" label="View" hint="SegmentedControl hint" error="SegmentedControl error">
{(control) => (
<SegmentedControlRoot {...control} defaultValue="board">
<SegmentedControlItem value="board">Board</SegmentedControlItem>
<SegmentedControlItem value="table">Table</SegmentedControlItem>
</SegmentedControlRoot>
)}
</Field>
<Field name="volume" label="Volume" hint="Slider hint" error="Slider error">
{(control) => <Slider {...control} defaultValue={40} />}
</Field>
<Field name="guests" label="Guests" hint="NumberField hint" error="NumberField error">
{(control) => <NumberField {...control} defaultValue={2} min={1} max={8} />}
</Field>
<Field name="code" label="Code" hint="OTPField hint" error="OTPField error">
{(control) => <OTPField {...control} length={4} />}
</Field>
<Field name="country" label="Country" hint="Select hint" error="Select error" required>
{(control) => <Select {...control} items={COUNTRIES} defaultValue="jo" />}
</Field>
<Field name="city" label="City" hint="Combobox hint" error="Combobox error">
{(control) => <Combobox {...control} items={COUNTRIES} />}
</Field>
</form>
)
}
form
form.tsx
import { Button } from 'topclassui/button'
import { Checkbox } from 'topclassui/checkbox'
import { Input } from 'topclassui/input'
import { Textarea } from 'topclassui/textarea'
import { Field } from 'topclassui/field'
/** A plain form: Field adds nothing it sends, the controls do, before any script runs and after. */
export default function Demo() {
return (
<form className="grid max-w-sm gap-4">
<Field name="email" label="Email" hint="We never share it" required>
{(control) => <Input {...control} type="email" defaultValue="omar@example.com" autoComplete="email" fullWidth />}
</Field>
<Field name="message" label="Message">
{(control) => <Textarea {...control} defaultValue="Hello" fullWidth />}
</Field>
<Field name="terms" label="I accept the terms" inline>
{(control) => <Checkbox {...control} defaultChecked />}
</Field>
<div className="flex flex-wrap gap-2">
<Button type="submit">Send</Button>
<Button type="reset" variant="outline">Clear</Button>
</div>
</form>
)
}
live
Shown to other players
Pick the size you ordered
live.tsx
'use client'
import { Input } from 'topclassui/input'
import { Select } from 'topclassui/select'
import { FieldLive } from 'topclassui/field'
const SIZES = [{ value: 's', label: 'Small' }, { value: 'm', label: 'Medium' }, { value: 'l', label: 'Large' }]
/** FieldLive in a client file, outside a form: the same server HTML as Field, and an error of its own stays until the caller drops it. */
export default function Demo() {
return (
<div className="grid max-w-sm gap-4">
<FieldLive name="nickname" label="Nickname" hint="Shown to other players">
{(control) => <Input {...control} autoComplete="nickname" fullWidth />}
</FieldLive>
<FieldLive name="size" label="Size" error="Pick the size you ordered" required>
{(control) => <Select {...control} items={SIZES} defaultValue="m" />}
</FieldLive>
</div>
)
}
long-content
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
long-content.tsx
import { Checkbox } from 'topclassui/checkbox'
import { Input } from 'topclassui/input'
import { Field } from 'topclassui/field'
const WORD = 'x'.repeat(300)
const SENTENCE = 'A label long enough to wrap onto several lines on any phone, '.repeat(5)
/** Hostile text: a 300-character word in the label, the hint and the error, and a long sentence beside a box. It wraps; nothing scrolls. */
export default function Demo() {
return (
<div className="grid max-w-sm gap-4">
<Field name="word" label={WORD} hint={WORD} error={WORD} required>
{(control) => <Input {...control} fullWidth />}
</Field>
<Field name="sentence" label={SENTENCE} hint={WORD} inline>
{(control) => <Checkbox {...control} />}
</Field>
</div>
)
}
بالعربية، من اليمين إلى اليسار
لن نشاركه مع أحد
هذا البريد مستخدم
اقرأها قبل المتابعة
rtl.tsx
import { Checkbox } from 'topclassui/checkbox'
import { Input } from 'topclassui/input'
import { TopClassProvider } from 'topclassui/provider'
import { Field } from 'topclassui/field'
/** Arabic, inside TopClassProvider as the root layout would mount it: the asterisk follows the label, and an inline box sits at the right. */
export default function Demo() {
return (
<TopClassProvider direction="rtl" locale="ar">
<div dir="rtl" lang="ar" className="grid max-w-sm gap-4">
<Field name="email" label="البريد الإلكتروني" hint="لن نشاركه مع أحد" error="هذا البريد مستخدم" required>
{(control) => <Input {...control} type="email" defaultValue="omar@example.com" fullWidth />}
</Field>
<Field name="terms" label="أوافق على الشروط" hint="اقرأها قبل المتابعة" inline>
{(control) => <Checkbox {...control} />}
</Field>
</div>
</TopClassProvider>
)
}
الخصائص
لوحة المفاتيح وإمكانية الوصول
- التسمية تسمّي عنصر التحكم عبر
<label for>وaria-labelledby؛ وaria-describedbyيقرأ التلميح ثم الخطأ، وما يُعرض منهما فقط. - النجمة مرئية فقط:
requiredيصل إلى عنصر التحكم، فيعلن ذلك بنفسه عبرrequiredأوaria-required. - الخطأ ليس منطقة حيّة. يصل مع الصفحة، وبعد FormAction ينتقل التركيز إلى أول عنصر غير صالح.
من اليمين إلى اليسار
تبدأ التسمية والتلميح والخطأ من اليمين؛ والنجمة تتبع التسمية، ومربع الاختيار في الوضع inline يقع على اليمين.
الخادم والمتصفح
يُعرض على الخادم، والجزء التفاعلي مكوّن متصفح لا يُرسل إلا عند استخدامه.
الملفات: index.tsx, client.tsx.
على الشارة: split - الحجم لم يُقَس بعد.