Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IxInput does not fire onValueChange event until onBlur #1593

Open
2 tasks done
charlesp-siemens opened this issue Dec 3, 2024 · 1 comment
Open
2 tasks done

IxInput does not fire onValueChange event until onBlur #1593

charlesp-siemens opened this issue Dec 3, 2024 · 1 comment
Labels
type: bug Something isn't working

Comments

@charlesp-siemens
Copy link

Prerequisites

  • I have read the Contributing Guidelines.
  • I have not leaked any internal/restricted information like screenshots, videos, code snippets, links etc.

What happened?

I am using an <IxInput> component inside a <form>, with React Hook Form set to mode: 'all' and reValidateMode: 'onChange'.

Despite this, the validation is not triggered onChange, and instead the onValueChange event of the input is only fired when focus is lost from the element (onBlur). As per the documentation, this should not be the case.

For sanity's sake, If I replace the IxInput with a regular html input, the onChange event fires as soon as I type, and the validation is triggered immediately. It's not possible for me to use a native input element because I'd then lose all the styling and validation functionality encapsulated in the IxInput component (which looks great and otherwise works well).

I greatly suspect that the IxInput components themselves are swallowing all onValueChange events, and only firing them onBlur, always.

Note: For the 'Code to produce this issue.' section of this bug report, I've copied the code exactly from the iX documentation on form validation here. When running this code, the validation does not run onChange - only onBlur. It should validate onChange, as the React Hook Form specifies the mode as 'all' and reValidateMode as 'onChange'.

What type of frontend framework are you seeing the problem on?

React

Which version of iX do you use?

2.6.0

Code to produce this issue.

import { yupResolver } from '@hookform/resolvers/yup';
import { iconBezierCurve, iconLocation } from '@siemens/ix-icons/icons';
import {
  IxButton,
  IxCheckbox,
  IxCheckboxGroup,
  IxCustomField,
  IxDateInput,
  IxIcon,
  IxIconButton,
  IxLayoutAuto,
  IxNumberInput,
  IxRadio,
  IxRadioGroup,
  IxSelect,
  IxSelectItem,
  IxInput,
  IxTextarea,
  IxTypography,
} from '@siemens/ix-react';
import clsx from 'clsx';
import { useLayoutEffect, useRef, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import * as yup from 'yup';

const validationSchema = yup.object({
  name: yup.string().required('Name is required'),
  'last-name': yup.string(),
  address: yup.string(),
  thresholdLimitA: yup
    .number()
    .max(5, 'The threshold must be equal or lesser than 5'),
  thresholdLimitB: yup.number(),
  begin: yup.string(),
  end: yup
    .string()
    .test('valid-date', '2024/05/05 is not allowed to pick', (value) => {
      return value !== '2024/05/05';
    }),
  comment: yup.string(),
  agreed: yup.boolean().oneOf([true], 'You must agree to continue'),
  'booking-option': yup.string(),
  'travel-option': yup.string(),
  'room-size': yup.number(),
  email: yup.string(),
  pin: yup.string(),
  'confirm-pin': yup.string().oneOf([yup.ref('pin')], 'PIN does not match'),
  upload: yup.string(),
  'upload-path': yup.string().required('You need to upload a file'),
});

export default function FormValidation() {
  const uploadRef = useRef<HTMLInputElement>(null);

  const [showWarning, setShowWarning] = useState(true);

  const {
    register,
    handleSubmit,
    control,
    formState: { errors },
    trigger,
    setValue,
  } = useForm({
    mode: 'all',
    reValidateMode: 'onChange',
    defaultValues: {
      name: 'John',
      'last-name': 'Muster',
      address: 'John Street 14',
      thresholdLimitA: 6,
      thresholdLimitB: 7,
      begin: '2024/05/05',
      end: '2024/05/05',
      comment: 'Some info',
      agreed: false,
      'booking-option': '2',
      'travel-option': '3',
      'room-size': 100,
      email: '',
      pin: '',
      'confirm-pin': '',
      upload: '',
      'upload-path': '',
    },
    resolver: yupResolver(validationSchema),
  });

  const onSubmit = (data: any) => {
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="form-validation-example">
      <IxLayoutAuto>
        <IxInput
          label="Name"
          {...register('name')}
          className={clsx({ 'ix-invalid': errors.name })}
          invalidText={errors.name?.message}
          required
        />
        <IxInput label="Last Name" {...register('last-name')} />
        <IxInput label="Address" data-colspan="2" {...register('address')}>
          <IxIcon slot="start" name={iconLocation} size="16"></IxIcon>
        </IxInput>

        <IxRadioGroup label="Booking option">
          {Array.from({ length: 3 }).map((_, option) => (
            <Controller
              key={`Option${option}`}
              control={control}
              name="booking-option"
              render={({ field }) => (
                <IxRadio
                  label={`Option ${option}`}
                  value={`${option}`}
                  checked={field.value === `${option}`}
                  onCheckedChange={() => field.onChange(`${option}`)}
                ></IxRadio>
              )}
            />
          ))}
        </IxRadioGroup>

        <IxNumberInput
          label="Preferred room size"
          className="ix-info"
          infoText="You can adjust the room size"
          {...register('room-size')}
        >
          <IxIcon slot="start" name={iconBezierCurve} size="16"></IxIcon>
          <IxTypography slot="end" color="weak" className='padding-right'>
            m<sup>2</sup>
          </IxTypography>
        </IxNumberInput>

        <IxSelect
          label="Travel option"
          data-colspan="2"
          {...register('travel-option')}
        >
          <IxSelectItem value="1" label="Option 1"></IxSelectItem>
          <IxSelectItem value="2" label="Option 2"></IxSelectItem>
          <IxSelectItem value="3" label="Option 3"></IxSelectItem>
        </IxSelect>

        <IxNumberInput
          label="Threshold limit A"
          data-colspan="1"
          helperText="Max threshold is 5"
          {...register('thresholdLimitA', { required: false, max: '5' })}
          className={clsx({ 'ix-invalid': errors.thresholdLimitA })}
          invalidText={errors.thresholdLimitA?.message}
        ></IxNumberInput>

        <IxNumberInput
          label="Threshold limit B"
          data-colspan="1"
          showStepperButtons
          {...register('thresholdLimitB')}
          className={clsx({
            'ix-warning': showWarning,
          })}
          warningText={'A threshold greater than 5 is not recommended'}
          onValueChange={({ detail }) => {
            setShowWarning(detail > 5);
          }}
        ></IxNumberInput>

        <IxDateInput
          label="Begin"
          i18nErrorDateUnparsable="Please enter a valid date"
          {...register('begin')}
        ></IxDateInput>
        <IxDateInput
          label="End"
          {...register('end')}
          invalidText={errors.end?.message}
          className={clsx({
            'ix-invalid': errors.end,
          })}
        ></IxDateInput>

        <IxTextarea
          maxLength={100}
          label="Comment"
          data-colspan="2"
          textareaHeight="10rem"
          helperText="Let us know if you have any special requests or comments. We will do our best to accommodate you."
          {...register('comment')}
        ></IxTextarea>

        <IxInput
          type="email"
          label="Email"
          {...register('email')}
        ></IxInput>

        {}
        <IxCustomField label="Upload" invalidText="You need to upload a file">
          <IxInput
            type="text"
            onClick={() => uploadRef.current?.click()}
            readonly
            style={{ width: '100%' }}
            {...register('upload-path')}
            className={clsx({ 'ix-invalid': errors['upload-path'] })}
          ></IxInput>
          <input
            ref={uploadRef}
            type="file"
            style={{ display: 'none' }}
            onChange={(file) => {
              setValue('upload-path', file.target.value);
            }}
            name="upload"
          />
          <IxIconButton
            outline
            variant="primary"
            icon="star"
            onClick={() => uploadRef.current?.click()}
          ></IxIconButton>
        </IxCustomField>

        <IxInput
          type="password"
          label="PIN"
          helperText="Only numbers between 1 and 4 is allowed"
          allowedCharactersPattern="[1-4]"
          maxLength={4}
          {...register('pin')}
        ></IxInput>
        <IxInput
          type="password"
          label="PIN"
          helperText="Confirm password"
          allowedCharactersPattern="[1-4]"
          maxLength={4}
          {...register('confirm-pin')}
          className={clsx({ 'ix-invalid': errors['confirm-pin'] })}
          invalidText={errors['confirm-pin']?.message}
        ></IxInput>

        <Controller
          control={control}
          name="agreed"
          render={({ field }) => (
            <IxCheckboxGroup invalidText={errors.agreed?.message}>
              <IxCheckbox
                label="I agree everything"
                data-colspan="2"
                name={field.name}
                disabled={field.disabled}
                checked={field.value}
                onCheckedChange={(evt) => setValue('agreed', evt.detail)}
                className={clsx({ 'ix-invalid': errors.agreed })}
              ></IxCheckbox>
            </IxCheckboxGroup>
          )}
        />

        <IxButton type="submit" data-colspan="1">
          Submit
        </IxButton>
      </IxLayoutAuto>
    </form>
  );
}
@charlesp-siemens charlesp-siemens added the triage We discuss this topic in our internal weekly label Dec 3, 2024
@danielleroux
Copy link
Collaborator

danielleroux commented Dec 3, 2024

This is currently a limitation of using react-hook-form together with none react components (ix components are native web components).

You find more information of the react-form-hook page also there is a solution using controller component. (This will work after the fix which is part of #1595).

This library embraces uncontrolled components and native HTML inputs. However, it's hard to avoid working with external controlled components such as React-Select, AntD and MUI. To make this simple, we provide a wrapper component, Controller, to streamline the integration process while still giving you the freedom to use a custom register.

        <Controller
          name="name"
          control={control}
          rules={{ required: true }}
          render={({ field }) => (
            <IxInput
              label="Name"
              {...field}
              className={clsx({ 'ix-invalid': errors.name })}
              invalidText={errors.name?.message}
              onValueChange={({ detail }) => field.onChange(detail)} // Will work after fix
              onInput={({ target }) =>
                field.onChange((target as HTMLInputElement).value) // Works because of using native input event
              }
            />
          )}
        />

Here are more details:

image

HTMLInputElement extends HTMLElement provides some basic events change and input and here is the core problem of the issue.

The change event is triggered after loses focus:

When the element loses focus after its value was changed: for elements where the user's interaction is typing rather than selection, such as a <textarea> or the text, search, url, tel, email, or password types of the element.

The input event is triggered by each value change of the element.

@danielleroux danielleroux added type: bug Something isn't working and removed triage We discuss this topic in our internal weekly labels Dec 3, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants