Form
These examples use react-hook-form@6
library.
Login form
tsx
import React from 'react';
import { FormProvider, useForm, useFormContext, RegisterOptions } from 'react-hook-form';
import { Flex } from '@semcore/flex-box';
import Tooltip from '@semcore/tooltip';
import Input from '@semcore/input';
import Button from '@semcore/button';
import { Text } from '@semcore/typography';
type FormValues = {
email: string;
password: string;
};
const defaultValues = { email: '', password: '' };
type FormControlProps = {
name: keyof FormValues;
type: string;
options: RegisterOptions;
autocomplete: string;
};
const FormControl = ({ name, type, options, autocomplete }: FormControlProps) => {
const {
register,
trigger,
formState: { isSubmitted, errors },
} = useFormContext();
const [active, setActive] = React.useState<boolean>(false);
const error = errors[name];
const hasError = () => {
if (error?.type === 'required' && !isSubmitted) {
return false;
}
return Boolean(error);
};
const invalid = (): boolean => {
return hasError();
};
const showErrorTooltip = (): boolean => {
return invalid() && active;
};
const { onChange, ...restField } = register(name, {
...options,
onBlur: () => setActive(false),
});
const field = {
onChange: (_v: string, e: React.SyntheticEvent) => {
// important: keep call order, otherwise validation breaks
onChange(e);
hasError() && trigger();
},
...restField,
};
return (
<Tooltip placement='top' interaction={'none'} animationsDisabled>
<Tooltip.Popper visible={showErrorTooltip()} id={`form-${name}-error`} theme='warning'>
{showErrorTooltip() && (error?.message as any)}
</Tooltip.Popper>
<Input w='100%' mb={4} size='l' state={hasError() ? 'invalid' : 'normal'} controlsLength={1}>
<Tooltip.Trigger
tag={Input.Value}
{...field}
id={name}
type={type}
onFocus={() => setActive(true)}
autoComplete={autocomplete}
aria-invalid={hasError()}
aria-describedby={hasError() ? `form-${name}-error` : undefined}
__excludeProps={['aria-haspopup']}
/>
</Input>
</Tooltip>
);
};
const Demo = () => {
const methods = useForm<FormValues>({
mode: 'onBlur',
defaultValues,
});
const { handleSubmit, reset } = methods;
const onSubmit = (data: FormValues) => {
reset(defaultValues, { keepIsSubmitted: false, keepTouched: false });
alert(JSON.stringify(data));
};
return (
<FormProvider {...methods}>
<Flex tag='form' noValidate onSubmit={handleSubmit(onSubmit)} direction='column' gap={2}>
<Text size={300} tag='label' htmlFor='email'>
Email
</Text>
<FormControl
name='email'
type='email'
autocomplete='email'
options={{
validate: {
required: (v: string) => Boolean(v) || 'Email is required',
email: (v: string) => {
if (!v) {
return true;
}
return /.+@.+\..+/i.test(v) || 'Email is not valid';
},
},
}}
/>
<Text size={300} tag='label' htmlFor='password'>
Password
</Text>
<FormControl
name='password'
type='password'
autocomplete='current-password'
options={{
required: 'Password is required',
minLength: {
value: 8,
message: 'Password must have at least 8 characters',
},
}}
/>
<Button type='submit' use='primary' theme='success' size='l' w='100%' mt={2}>
Log in
</Button>
</Flex>
</FormProvider>
);
};
export default Demo;
InputTags and Select
tsx
import React from 'react';
import { useForm, Controller } from 'react-hook-form';
import { Flex } from '@semcore/flex-box';
import { Text } from '@semcore/typography';
import Select from '@semcore/select';
import Counter from '@semcore/counter';
import Tooltip from '@semcore/tooltip';
import InputTags from '@semcore/input-tags/';
import Button from '@semcore/button';
const Demo = () => {
const defaultValues = {
period: 'Weekly',
day_week: 'Monday',
emails: ['first@react.hook.form', 'second@react.hook.form'],
};
const {
handleSubmit,
getValues,
setValue,
control,
setError,
clearErrors,
formState: { errors },
watch,
} = useForm({
defaultValues,
});
const [valueTag, setValueTag] = React.useState('');
const [isFocused, setIsFocused] = React.useState(false);
const onSubmit = (data: typeof defaultValues) => {
alert(JSON.stringify(data));
};
const isEmailValid = (val: string) => /.+@.+\..+/i.test(val);
const handleAppendTags = (newTags: string[]) => {
const tags = getValues('emails');
if (newTags.some((tag) => !isEmailValid(tag))) {
setError('emails', { message: "Email isn't valid" });
return;
}
if (tags.length + newTags.length > 5) {
setError('emails', { message: 'Max emails is 5' });
return;
}
setValue('emails', [...tags, ...newTags]);
setValueTag('');
};
const handleInputBlur = (e: React.FocusEvent<HTMLInputElement>) => {
if (e.target.value && !isEmailValid(e.target.value)) {
setError('emails', { message: "Email isn't valid" });
}
setIsFocused(false);
};
const handleInputChange = (value: string) => {
setValueTag(value);
if (!value || isEmailValid(value)) {
clearErrors();
}
};
const handleRemoveTag = () => {
const tags = getValues('emails');
if (tags.length === 0) return;
setValue('emails', tags.slice(0, -1));
setValueTag(`${tags.slice(-1)[0]} ${valueTag}`);
};
const handleCloseTag = (e: React.MouseEvent<HTMLDivElement>) => {
const tags = getValues('emails');
const { dataset } = e.currentTarget;
setValue(
'emails',
tags.filter((_tag, idx) => idx !== Number(dataset.id)),
);
};
const periods = ['Daily', 'Weekly'].map((value) => ({ value, children: value }));
const daysWeek = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'].map((value) => ({
value,
children: value,
}));
const emailInvalid = Boolean(errors.emails);
const showError = isFocused && emailInvalid;
return (
<Flex tag='form' onSubmit={handleSubmit(onSubmit)} direction='column' alignItems='flex-start'>
<Text size={300} tag='label' mb={2} htmlFor='period'>
Email frequency
</Text>
<Flex mb={6} gap={4}>
<Controller
render={({ field }) => <Select size='l' id='period' options={periods} {...field} />}
control={control}
name='period'
/>
{watch('period') === 'Weekly' && (
<Controller
render={({ field }) => (
<Select size='l' aria-label='Day' options={daysWeek} {...field} />
)}
control={control}
name='day_week'
/>
)}
</Flex>
<Controller
render={({ field: { value: tags = [] } }) => (
<>
<Flex>
<Text size={300} tag='label' mb={2} htmlFor='emails'>
Emails
</Text>
<Counter
ml={1}
size='xl'
theme={tags.length < 5 ? '' : 'warning'}
>{`${tags.length}/5`}</Counter>
</Flex>
<Tooltip
interaction='none'
placement='bottom'
theme='warning'
w='100%'
animationsDisabled
visible={showError}
>
<Tooltip.Trigger
tag={InputTags}
size='l'
state={emailInvalid ? 'invalid' : 'normal'}
onAppend={handleAppendTags}
onRemove={handleRemoveTag}
>
{tags.map((tag, idx) => (
<InputTags.Tag key={idx}>
<InputTags.Tag.Text>{tag}</InputTags.Tag.Text>
<InputTags.Tag.Close data-id={idx} onClick={handleCloseTag} />
</InputTags.Tag>
))}
<InputTags.Value
id='emails'
name='email'
type='email'
autoComplete='email'
value={valueTag}
onChange={handleInputChange}
onBlur={handleInputBlur}
onFocus={() => setIsFocused(true)}
aria-invalid={emailInvalid}
aria-describedby={showError ? 'form-emails-error' : undefined}
__excludeProps={['aria-haspopup']}
/>
</Tooltip.Trigger>
<Tooltip.Popper id='form-emails-error'>
{String((errors['emails'] as any)?.message)}
</Tooltip.Popper>
</Tooltip>
</>
)}
control={control}
name='emails'
/>
<Button mt={8} type='submit' use='primary' theme='success' size='l' wMin={120}>
Save
</Button>
</Flex>
);
};
export default Demo;
DatePicker and TimePicker
tsx
import React from 'react';
import { useForm, Controller } from 'react-hook-form';
import { Flex } from '@semcore/flex-box';
import { Text } from '@semcore/typography';
import { DatePicker } from '@semcore/date-picker';
import TimePicker from '@semcore/time-picker';
import Checkbox from '@semcore/checkbox';
import Button from '@semcore/button';
type FormValues = {
start_date?: string;
start_time?: string;
due_date?: string;
due_time?: string;
};
const defaultValues: FormValues = {
start_date: '',
start_time: '',
due_date: '',
due_time: '',
};
const Demo = () => {
const [period, setPeriod] = React.useState(false);
const { handleSubmit, control, reset } = useForm<FormValues>({
defaultValues,
});
const onSubmit = (data: FormValues) => {
alert(JSON.stringify(data));
};
const onReset = () => {
reset(defaultValues);
};
const onPreventDefault = (e: React.MouseEvent<HTMLDivElement>) => {
e.preventDefault();
};
return (
<Flex
tag='form'
onSubmit={handleSubmit(onSubmit)}
direction='column'
alignItems='flex-start'
gap={6}
>
<Flex gap={4}>
<Flex direction='column' gap={2}>
<Text size={300} tag='label' htmlFor='startDate'>
Start date
</Text>
<Controller
render={({ field: { value, onChange } }) => (
<DatePicker value={value} onChange={onChange}>
<DatePicker.Trigger id='startDate' size='l' />
<DatePicker.Popper />
</DatePicker>
)}
control={control}
name='start_date'
/>
</Flex>
<Flex direction='column' gap={2}>
<Text size={300} tag='label' htmlFor='startTime'>
Time
</Text>
<Controller
render={({ field: { value, onChange } }) => (
<TimePicker id='startTime' size='l' is12Hour value={value} onChange={onChange}>
<TimePicker.Hours />
<TimePicker.Separator />
<TimePicker.Minutes />
<TimePicker.Format onClick={onPreventDefault} />
</TimePicker>
)}
control={control}
name='start_time'
/>
</Flex>
</Flex>
<Checkbox size='l'>
<Checkbox.Value onChange={setPeriod} />
<Checkbox.Text>Period</Checkbox.Text>
</Checkbox>
{period && (
<Flex gap={4}>
<Flex direction='column' gap={2}>
<Text size={300} tag='label' htmlFor='dueDate'>
Due date
</Text>
<Controller
render={({ field: { value, onChange } }) => (
<DatePicker value={value} onChange={onChange}>
<DatePicker.Trigger id='dueDate' size='l' />
<DatePicker.Popper />
</DatePicker>
)}
control={control}
name='due_date'
/>
</Flex>
<Flex direction='column' gap={2}>
<Text size={300} tag='label' htmlFor='dueTime'>
Time
</Text>
<Controller
render={({ field: { value, onChange } }) => (
<TimePicker id='dueTime' size='l' is12Hour value={value} onChange={onChange}>
<TimePicker.Hours />
<TimePicker.Separator />
<TimePicker.Minutes />
<TimePicker.Format onClick={onPreventDefault} />
</TimePicker>
)}
control={control}
name='due_time'
/>
</Flex>
</Flex>
)}
<Flex gap={3} mt={2}>
<Button type='submit' use='primary' theme='success' size='l'>
Create
</Button>
<Button size='l' onClick={onReset}>
Cancel
</Button>
</Flex>
</Flex>
);
};
export default Demo;
Radio and Select
tsx
import React from 'react';
import { useForm, Controller } from 'react-hook-form';
import { Flex } from '@semcore/flex-box';
import { Text } from '@semcore/typography';
import Radio, { RadioGroup } from '@semcore/radio';
import Select from '@semcore/select';
import Button from '@semcore/button';
import { ScreenReaderOnly } from '@semcore/flex-box';
type FormValues = {
export?: string;
};
const defaultValues: FormValues = {
export: 'all',
};
const Demo = () => {
const [selectedFirst, setSelectedFirst] = React.useState(100);
const [message, setMessage] = React.useState('');
const { handleSubmit, control, reset, getValues } = useForm<FormValues>({
defaultValues,
});
const optionsFirst = [100, 500].map((value) => ({ value, children: value }));
const onSubmit = (data: FormValues) => {
if (data.export === 'first') {
data.export = `first ${selectedFirst}`;
}
alert(JSON.stringify(data));
};
React.useEffect(() => {
const timer = setTimeout(() => setMessage(''), 500);
return () => clearTimeout(timer);
}, [message]);
const onChangeSelect = (value: number) => {
setSelectedFirst(value);
if (getValues('export') !== 'first') {
setMessage(`Selection changed to First ${value} rows`);
reset({ export: 'first' });
}
};
return (
<Flex
tag='form'
onSubmit={handleSubmit(onSubmit)}
direction='column'
alignItems='flex-start'
gap={4}
>
<ScreenReaderOnly role='status' aria-live='polite'>
{message}
</ScreenReaderOnly>
<Text size={300} id='radio-group-label' tag='label'>
Export data
</Text>
<Controller
render={({ field }) => (
<RadioGroup {...field} size='l' gap={3} aria-labelledby='radio-group-label'>
<Radio mb={2} value='all' label='All' />
<Radio>
<Radio.Value value='selected' />
<Radio.Text>
Selected <Text use='secondary'>(3)</Text>
</Radio.Text>
</Radio>
<Radio style={{ alignItems: 'center' }}>
<Radio.Value value='first' />
<Radio.Text>First</Radio.Text>
<Select
size='l'
ml={2}
options={optionsFirst}
onChange={onChangeSelect}
defaultValue={100}
aria-label='Rows'
/>
</Radio>
</RadioGroup>
)}
control={control}
name='export'
/>
<Button type='submit' use='primary' theme='info' size='l' mt={2}>
Export
</Button>
</Flex>
);
};
export default Demo;