Skip to content

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;

Released under the MIT License.

Released under the MIT License.