form icon indicating copy to clipboard operation
form copied to clipboard

feat(core): add field listeners

Open ha1fstack opened this issue 1 year ago • 6 comments

from discussion #709

Background

Sometimes we may want to attach our own side effects to the field when some event happends. For example, resetting another field if some field value has changed.

Attaching these 'listeners' inside the validator does not work as intended because validator does not guarantee if it was triggered by the exact event. (eg. all validators run when form submits)

It's of course possible to implement but it's not very clean and the concerns become scattered.

Proposal

This PR adds listeners api to the field for easy handling of forementioned use cases.

onChange and onHandleChange are differentiated because onHandleChange captures the 'manual' changes to the field. (eg. user input) while onChange also captures programmatic changes. (eg. setValue)

Thoughts

I'm not certain about the whole idea or implementation, open to suggestions.

ha1fstack avatar Jun 30 '24 21:06 ha1fstack

This feature will be useful +1. We are currently hacking around validators by it not practical

zaosoula avatar Jul 02 '24 20:07 zaosoula

This feature will be useful +1. We are currently hacking around validators by it not practical

@zaosoula Bit clumsy code, but in the meantime you can do this:

import { DeepKeys, DeepValue, FieldApi, functionalUpdate, Validator } from '@tanstack/form-core';

export function attachOnChangeToField<
  TParentData,
  TName extends DeepKeys<TParentData>,
  TFieldValidator extends Validator<DeepValue<TParentData, TName>, unknown> | undefined = undefined,
  TFormValidator extends Validator<TParentData, unknown> | undefined = undefined,
  TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>
>(
  field: FieldApi<TParentData, TName, TFieldValidator, TFormValidator, TData>,
  onChange: (props: {
    value: DeepValue<TParentData, TName>;
    fieldApi: FieldApi<TParentData, TName, TFieldValidator, TFormValidator, TData>;
  }) => void
) {
  const handleChange: typeof field.handleChange = updater => {
    onChange({
      value: functionalUpdate(updater, field.store.state.value),
      fieldApi: field,
    });
    field.handleChange(updater);
  };

  return {
    ...field,
    handleChange,
  } as typeof field;
}

export function AttachOnchangeToField<
  TParentData,
  TName extends DeepKeys<TParentData>,
  TFieldValidator extends Validator<DeepValue<TParentData, TName>, unknown> | undefined = undefined,
  TFormValidator extends Validator<TParentData, unknown> | undefined = undefined,
  TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>
>(props: {
  field: FieldApi<TParentData, TName, TFieldValidator, TFormValidator, TData>;
  onChange: (props: {
    value: DeepValue<TParentData, TName>;
    fieldApi: FieldApi<TParentData, TName, TFieldValidator, TFormValidator, TData>;
  }) => void;
  children: (field: FieldApi<TParentData, TName, TFieldValidator, TFormValidator, TData>) => JSX.Element;
}) {
  const { field, onChange } = props;
  const newField = attachOnChangeToField(field, onChange);

  return props.children(newField);
}
<form.Field name="name">
  {field => (
    <AttachOnchangeToField
      field={field}
      onChange={({ value, fieldApi }) => { ... }
    >
      {field => ...

ha1fstack avatar Jul 03 '24 01:07 ha1fstack

@zaosoula this would be helpful to understand a practical use case :) Can you share yours?

crutchcorn avatar Jul 03 '24 22:07 crutchcorn

@crutchcorn

Screenshot 2024-07-04 at 09 48 09

Here we are using onChange to dynamically set a checkbox to true if the selected value match a list

Screenshot 2024-07-04 at 09 49 11

Here we are using onChangeAsync to open a modal to create a new entry for a sub-relation

Each time we have to return null or an empty string as we do not want to show errors; using the validators property is also confusing for new developper reading our code as the functions above are clearly not validators

zaosoula avatar Jul 04 '24 08:07 zaosoula

This alone makes a lot of sense to me. Let's move forward with the idea. Reviewing the code now

crutchcorn avatar Jul 05 '24 07:07 crutchcorn

Any news on this feature ?

zaosoula avatar Sep 17 '24 14:09 zaosoula

@ha1fstack @crutchcorn If this task is stale, I would like to pick it up as this functionality is something we would really like.

For us, the use case as an example, is a payment form where if you select credit_card as a payment method you have a form field called creditCardBrands, which needs to be cleared when you change the payment method back to anything other than credit_card.

Personally, this is something that is not an infrequent use case, and having an api that's simpler than a hacky workaround utilising the validator linked-fields, is something that I feel is needed.

Let me know if I can take it up 🤟

harry-whorlow avatar Nov 21 '24 10:11 harry-whorlow

Completed in #1032

Balastrong avatar Nov 26 '24 18:11 Balastrong