Coder Social home page Coder Social logo

jaredlunde / form-atoms Goto Github PK

View Code? Open in Web Editor NEW
75.0 5.0 5.0 340 KB

Atomic form primitives for Jotai

Home Page: https://codesandbox.io/s/getting-started-with-form-atoms-v2-ddhgq2?file=/src/App.tsx

License: MIT License

Shell 0.09% TypeScript 99.91%
jotai react react-form jotai-form jotai-forms react-forms form-library react-form-library forms atomic-form react-hook-form react-hooks form-hooks

form-atoms's Introduction


form-atoms

Atomic form primitives for Jotai

npm i form-atoms jotai

Bundlephobia Types Code coverage Build status NPM Version MIT License


Features

  • Renders what changes and nothing else
  • Strongly typed allowing you to quickly iterate on durable code
  • Tiny (<3kB gzipped) but powerful API
  • Nested/array fields without parsing field names
  • Dynamic fields - you aren't stuck with your initial config
  • Controlled inputs because no, uncontrolled inputs are not preferrable
  • Ready for concurrent React - validation updates have a lower priority
  • Familiar API that is very similar to other form libraries
  • Async field-level validation with Zod support

Quick start

Check out the example on CodeSandbox ↗

import { fieldAtom, useInputField, formAtom, useForm } from "form-atoms";

const nameFormAtom = formAtom({
  name: {
    first: fieldAtom({ value: "" }),
    last: fieldAtom({ value: "" }),
  },
});

function Form() {
  const { fieldAtoms, submit } = useForm(nameFormAtom);
  return (
    <form
      onSubmit={submit((values) => {
        console.log(values);
      })}
    >
      <Field label="First name" atom={fieldAtoms.name.first} />
      <Field label="Last name" atom={fieldAtoms.name.last} />
    </form>
  );
}

function Field({ label, atom }) {
  const field = useInputField(atom);
  return (
    <label>
      <span>{label}</span>
      <input {...field.props} />
    </label>
  );
}

Concepts

Jotai was born to solve extra re-render issue in React. Extra re-render is a render process that produces the same UI result, with which users won't see any differences.

Like Jotai, this library was built to solve the extra re-render issue with React Forms. It takes a bottom-up approach using Jotai's atomic model. In practice that means that formAtom() derives its state from fieldAtom(). For example, validation occurs at the field-level rather than the form-level. Normally that would pose a problem for fields with validation that is dependent on other state or other fields, but using fieldAtom's validate function allows you to read the value of other atoms.

The form-atoms minimal API is written to be ergonomic and powerful. It feels like other form libraries (even better in my opinion). You don't lose anything by using it, but you gain a ton of performance and without footguns.

Table of contents

Field atoms Description
fieldAtom() An atom that represents a field in a form. It manages state for the field, including the name, value, errors, dirty, validation, and touched state.
useField() A hook that returns state and actions of a field atom from useFieldState, and useFieldActions.
useInputField() A hook that returns props, state, and actions of a field atom from useInputFieldProps, useFieldState, and useFieldActions.
useInputFieldProps() A hook that returns a set of props that can be destructured directly into an <input>, <select>, or <textarea> element.
useTextareaField() A hook that returns props, state, and actions of a field atom from useTextareaFieldProps, useFieldState, and useFieldActions.
useTextareaFieldProps() A hook that returns a set of props that can be destructured directly into a <textarea> element.
useSelectField() A hook that returns props, state, and actions of a field atom from useSelectFieldProps, useFieldState, and useFieldActions.
useSelectFieldProps() A hook that returns a set of props that can be destructured directly into a <select> element.
useFieldState() A hook that returns the state of a field atom. This includes the field's value, whether it has been touched, whether it is dirty, the validation status, and any errors.
useFieldActions() A hook that returns a set of actions that can be used to interact with the field atom state.
useFieldInitialValue() A hook that sets the initial value of a field atom. Initial values can only be set once per scope. Therefore, if the initial value used is changed during rerenders, it won't update the atom value.
useFieldValue() A hook that returns the value of a field atom.
useFieldErrors() A hook that returns the errors of a field atom.
Form atoms Description
formAtom() An atom that derives its state fields atoms and allows you to submit, validate, and reset your form.
useForm() A hook that returns an object that contains the fieldAtoms and actions to validate, submit, and reset the form.
useFormState() A hook that returns the primary state of the form atom including values, errors, submit and validation status, as well as the fieldAtoms. Note that this hook will cuase its parent component to re-render any time those states change, so it can be useful to use more targeted state hooks like useFormStatus.
useFormActions() A hook that returns a set of actions that can be used to update the state of the form atom. This includes updating fields, submitting, resetting, and validating the form.
useFormValues() A hook that returns the values of the form atom.
useFormErrors() A hook that returns the errors of the form atom.
useFormStatus() A hook that returns the submitStatus and validateStatus of the form atom.
useFormSubmit() A hook that returns a callback for handling form submission.
Components Description
<Form> A React component that renders form atoms and their fields in an isolated scope using a Jotai Provider.
<InputField> A React component that renders field atoms with initial values. This is useful for fields that are rendered as native HTML elements because the props can unpack directly into the underlying component.
<TextareaField> A React component that renders field atoms with initial values. This is useful for fields that are rendered as native HTML elements because the props can unpack directly into the underlying component.
<SelectField> A React component that renders field atoms with initial values. This is useful for fields that are rendered as native HTML elements because the props can unpack directly into the underlying component.
<Field> A React component that renders field atoms with initial values. This is useful for fields that aren't rendered as native HTML elements.
Utility Types Description
FormValues A utility type for inferring the value types of a form's nested field atoms.
FormErrors A utility type for inferring the error types of a form's nested field atoms.
Validator Description
form-atoms/zod A validator that can be used with the Zod library to validate fields.

Recipes

  1. How to validate on (blur, change, touch, submit)
  2. How to validate a field conditional to the state of another field
  3. How to validate a field asynchronously
  4. How to validate using a Zod schema
  5. How to create a nested fields
  6. How to create an array of fields

Projects using form-atoms

  • @form-atoms/field - Declarative & headless form fields build on top of Jotai & form-atoms

Field atoms

fieldAtom()

An atom that represents a field in a form. It manages state for the field, including the name, value, errors, dirty, validation, and touched state.

Arguments

Name Type Required? Description
config FieldAtomConfig<Value> Yes The initial state and configuration of the field.

FieldAtomConfig

type FieldAtomConfig<Value> = {
  /**
   * Optionally provide a name for the field that will be added
   * to any attached `<input>`, `<select>`, or `<textarea>` elements
   */
  name?: string;
  /**
   * The initial value of the field
   */
  value: Value;
  /**
   * The initial touched state of the field
   */
  touched?: boolean;
  /**
   * Transform the value of the field each time `setValue` is
   * called and before validation
   */
  preprocess?: (value: Value) => Value;
  /**
   * A function that validates the value of the field any time
   * one of its atoms changes. It must either return an array of
   * string error messages or undefined. If it returns undefined,
   * the validation is "skipped" and the current errors in state
   * are retained.
   */
  validate?: (state: {
    /**
     * A Jotai getter that can read other atoms
     */
    get: Getter;
    /**
     * The current value of the field
     */
    value: Value;
    /**
     * The dirty state of the field
     */
    dirty: boolean;
    /**
     * The touched state of the field
     */
    touched: boolean;
    /**
     * The event that caused the validation. Either:
     *
     * - `"change"` - The value of the field has changed
     * - `"touch"` - The field has been touched
     * - `"blur"` - The field has been blurred
     * - `"submit"` - The form has been submitted
     * - `"user"` - A user/developer has triggered the validation
     */
    event: ValidateOn;
  }) => void | string[] | Promise<void | string[]>;
};

Returns

type FieldAtom<Value> = Atom<{
  /**
   * An atom containing the field's name
   */
  name: WritableAtom<
    string | undefined,
    [string | undefined | typeof RESET],
    void
  >;
  /**
   * An atom containing the field's value
   */
  value: WritableAtom<
    Value,
    [Value | typeof RESET | ((prev: Value) => Value)],
    void
  >;
  /**
   * An atom containing the field's touched status
   */
  touched: WritableAtom<
    boolean,
    [boolean | typeof RESET | ((prev: boolean) => boolean)],
    void
  >;
  /**
   * An atom containing the field's dirty status
   */
  dirty: Atom<boolean>;
  /**
   * A write-only atom for validating the field's value
   */
  validate: WritableAtom<null, [] | [ValidateOn], void>;
  /**
   * An atom containing the field's validation status
   */
  validateStatus: WritableAtom<ValidateStatus, [ValidateStatus], void>;
  /**
   * An atom containing the field's validation errors
   */
  errors: WritableAtom<
    string[],
    [string[] | ((value: string[]) => string[])],
    void
  >;
  /**
   * A write-only atom for resetting the field atoms to their
   * initial states.
   */
  reset: WritableAtom<null, [], void>;
  /**
   * An atom containing a reference to the `HTMLElement` the field
   * is bound to.
   */
  ref: WritableAtom<
    HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | null,
    [
      | HTMLInputElement
      | HTMLTextAreaElement
      | HTMLSelectElement
      | null
      | ((
          value:
            | HTMLInputElement
            | HTMLTextAreaElement
            | HTMLSelectElement
            | null
        ) => HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | null)
    ],
    void
  >;
}>;

useField()

A hook that returns state and actions of a field atom from useFieldState and useFieldActions.

Arguments

Name Type Required? Description
fieldAtom FieldAtom<Value> Yes The atom that stores the field's state
options UseFieldOptions<Value> No Provide an initialValue here in additon to options that are forwarded to the useAtom, useAtomValue, and useSetAtom hooks.

Returns

type UseFieldAtom<Value> = {
  /**
   * Actions for managing the state of the field
   */
  actions: UseFieldActions<Value>;
  /**
   * The current state of the field
   */
  state: UseFieldState<Value>;
};

useInputField()

A hook that returns props, state, and actions of a field atom from useInputFieldProps, useFieldState, and useFieldActions.

Arguments

Name Type Required? Description
fieldAtom FieldAtom<Value> Yes The atom that stores the field's state
options UseInputFieldOptions<Type, Value> No Provide an initialValue here in additon to options that are forwarded to the useAtom, useAtomValue, and useSetAtom hooks.

Returns

type UseInputField<
  Type extends React.HTMLInputTypeAttribute,
  Value extends InputFieldValueForType<Type> = InputFieldValueForType<Type>
> = {
  /**
   * `<input>` props for the field
   */
  props: UseInputFieldProps<Type>;
  /**
   * Actions for managing the state of the field
   */
  actions: UseFieldActions<Value>;
  /**
   * The current state of the field
   */
  state: UseFieldState<Value>;
};

useInputFieldProps()

A hook that returns a set of props that can be destructured directly into an <input> element.

Arguments

Name Type Required? Description
fieldAtom FieldAtom<Value> Yes The atom that stores the field's state
options UseInputFieldPropsOptions<Type> No A type field and other options that are forwarded to the useAtom, useAtomValue, and useSetAtom hooks

Returns

type UseInputFieldProps<Type extends React.HTMLInputTypeAttribute> = {
  /**
   * The name of the field if there is one
   */
  name: string | undefined;
  /**
   * The value of the field
   */
  value: Type extends DateType
    ? string
    : Type extends NumberType
    ? number | string
    : Type extends FileType
    ? undefined
    : string;
  /**
   * The type of the field
   *
   * @default "text"
   */
  type: Type;
  /**
   * A WAI-ARIA property that tells a screen reader whether the
   * field is invalid
   */
  "aria-invalid": boolean;
  /**
   * A React callback ref that is used to bind the field atom to
   * an `<input>` element so that it can be read and focused.
   */
  ref: React.RefCallback<HTMLInputElement>;
  onBlur(event: React.FormEvent<HTMLInputElement>): void;
  onChange(event: React.ChangeEvent<HTMLInputElement>): void;
};

useTextareaField()

A hook that returns props, state, and actions of a field atom from useTextareaFieldProps, useFieldState, and useFieldActions.

Arguments

Name Type Required? Description
fieldAtom FieldAtom<Value> Yes The atom that stores the field's state
options UseTextareaFieldOptions<Value> No Provide an initialValue here in additon to options that are forwarded to the useAtom, useAtomValue, and useSetAtom hooks.

Returns

type UseTextareaField<Value extends string> = {
  /**
   * `<input>` props for the field
   */
  props: UseTextareaFieldProps<Value>;
  /**
   * Actions for managing the state of the field
   */
  actions: UseFieldActions<Value>;
  /**
   * The current state of the field
   */
  state: UseFieldState<Value>;
};

useTextareaFieldProps()

A hook that returns a set of props that can be destructured directly into a <textarea> element.

Arguments

Name Type Required? Description
fieldAtom FieldAtom<Value> Yes The atom that stores the field's state
options UseTextareaFieldPropsOptions No A type field and other options that are forwarded to the useAtom, useAtomValue, and useSetAtom hooks

Returns

type UseTextareaFieldProps<Value extends string> = {
  /**
   * The name of the field if there is one
   */
  name: string | undefined;
  /**
   * The value of the field
   */
  value: Value;
  /**
   * A WAI-ARIA property that tells a screen reader whether the
   * field is invalid
   */
  "aria-invalid": boolean;
  /**
   * A React callback ref that is used to bind the field atom to
   * an `<input>` element so that it can be read and focused.
   */
  ref: React.RefCallback<HTMLTextAreaElement>;
  onBlur(event: React.FormEvent<HTMLTextAreaElement>): void;
  onChange(event: React.ChangeEvent<HTMLTextAreaElement>): void;
};

useSelectField()

A hook that returns props, state, and actions of a field atom from useSelectFieldProps, useFieldState, and useFieldActions.

Arguments

Name Type Required? Description
fieldAtom FieldAtom<Value> Yes The atom that stores the field's state
options UseSelectFieldOptions<Value, Multiple> No Provide an initialValue here in additon to options that are forwarded to the useAtom, useAtomValue, and useSetAtom hooks.

Returns

type UseSelectField<
  Value extends string,
  Multiple extends Readonly<boolean> = false
> = {
  /**
   * `<input>` props for the field
   */
  props: UseSelectFieldProps<Value, Multiple>;
  /**
   * Actions for managing the state of the field
   */
  actions: UseFieldActions<Multiple extends true ? Value[] : Value>;
  /**
   * The current state of the field
   */
  state: UseFieldState<Multiple extends true ? Value[] : Value>;
};

useSelectFieldProps()

A hook that returns a set of props that can be destructured directly into a <select> element.

Arguments

Name Type Required? Description
fieldAtom FieldAtom<Value> Yes The atom that stores the field's state
options UseSelectFieldPropsOptions<Multiple> No A type field and other options that are forwarded to the useAtom, useAtomValue, and useSetAtom hooks

Returns

type UseSelectFieldProps<
  Value extends string,
  Multiple extends Readonly<boolean> = false
> = {
  /**
   * The name of the field if there is one
   */
  name: string | undefined;
  /**
   * The value of the field
   */
  value: Multiple extends true ? Value[] : Value;
  /**
   * Whether the field is a multiple select
   */
  multiple?: Multiple;
  /**
   * A WAI-ARIA property that tells a screen reader whether the
   * field is invalid
   */
  "aria-invalid": boolean;
  /**
   * A React callback ref that is used to bind the field atom to
   * an `<input>` element so that it can be read and focused.
   */
  ref: React.RefCallback<HTMLSelectElement>;
  onBlur(event: React.FormEvent<HTMLSelectElement>): void;
  onChange(event: React.ChangeEvent<HTMLSelectElement>): void;
};

useFieldState()

A hook that returns the state of a field atom. This includes the field's value, whether it has been touched, whether it is dirty, the validation status, and any errors.

Arguments

Name Type Required? Description
fieldAtom FieldAtom<Value> Yes The atom that stores the field's state
options UseAtomOptions No Options that are forwarded to the useAtom, useAtomValue, and useSetAtom hooks

Returns

type UseFieldState<Value> = {
  /**
   * The value of the field
   */
  value: ExtractAtomValue<ExtractAtomValue<FieldAtom<Value>>["value"]>;
  /**
   * The touched state of the field
   */
  touched: ExtractAtomValue<ExtractAtomValue<FieldAtom<Value>>["touched"]>;
  /**
   * The dirty state of the field. A field is "dirty" if it's value has
   * been changed.
   */
  dirty: ExtractAtomValue<ExtractAtomValue<FieldAtom<Value>>["dirty"]>;
  /**
   * The validation status of the field
   */
  validateStatus: ExtractAtomValue<
    ExtractAtomValue<FieldAtom<Value>>["validateStatus"]
  >;
  /**
   * The error state of the field
   */
  errors: ExtractAtomValue<ExtractAtomValue<FieldAtom<Value>>["errors"]>;
};

useFieldActions()

A hook that returns a set of actions that can be used to interact with the field atom state.

Arguments

Name Type Required? Description
fieldAtom FieldAtom<Value> Yes The atom that stores the field's state
options UseAtomOptions No Options that are forwarded to the useAtom, useAtomValue, and useSetAtom hooks

Returns

type UseFieldActions<Value> = {
  /**
   * A function that validates the field's value with a `"user"` validation
   * event.
   */
  validate(): void;
  /**
   * A function for changing the value of a field. This will trigger a `"change"`
   * validation event.
   *
   * @param {Value} value - The new value of the field
   */
  setValue(
    value: ExtractAtomArgs<ExtractAtomValue<FieldAtom<Value>>["value"]>[0]
  ): void;
  /**
   * A function for changing the touched state of a field. This will trigger a
   * `"touch"` validation event.
   *
   * @param {boolean} touched - The new touched state of the field
   */
  setTouched(
    touched: ExtractAtomArgs<ExtractAtomValue<FieldAtom<Value>>["touched"]>[0]
  ): void;
  /**
   * A function for changing the error state of a field
   *
   * @param {string[]} errors - The new error state of the field
   */
  setErrors(
    errors: ExtractAtomArgs<ExtractAtomValue<FieldAtom<Value>>["errors"]>[0]
  ): void;
  /**
   * Focuses the field atom's `<input>`, `<select>`, or `<textarea>` element
   * if there is one bound to it.
   */
  focus(): void;
  /**
   * Resets the field atom to its initial state.
   */
  reset(): void;
};

useFieldInitialValue()

A hook that sets the initial value of a field atom. Initial values can only be set once per scope. Therefore, if the initial value used is changed during rerenders, it won't update the atom value.

Arguments

Name Type Required? Description
fieldAtom FieldAtom<Value> Yes The atom that stores the field's state
initialValue Value No The initial value to set the atom to. If this is undefined, no initial value will be set.
options UseAtomOptions No Options that are forwarded to the useAtom, useAtomValue, and useSetAtom hooks

useFieldValue()

A hook that returns the value of a field atom.

Arguments

Name Type Required? Description
fieldAtom FieldAtom<Value> Yes The atom that stores the field's state
options UseAtomOptions No Options that are forwarded to the useAtom, useAtomValue, and useSetAtom hooks

Returns

type UseFieldValue<Value> = Value;

useFieldErrors()

A hook that returns the errors of a field atom.

Arguments

Name Type Required? Description
fieldAtom FieldAtom<Value> Yes The atom that stores the field's state
options UseAtomOptions No Options that are forwarded to the useAtom, useAtomValue, and useSetAtom hooks

Returns

type UseFieldErrors<Value> = UseFieldState<Value>["errors"];

Form atoms

formAtom()

An atom that derives its state fields atoms and allows you to submit, validate, and reset your form.

Arguments

Name Type Required? Description
fields FormFields Yes An object containing field atoms to be included in the form. Field atoms can be deeply nested in objects and arrays.

FormFields

type FormFields = {
  [key: string | number]:
    | FieldAtom<any>
    | FormFields
    | FormFields[]
    | FieldAtom<any>[];
};

Returns

type FormAtom<Fields extends FormFields> = Atom<{
  /**
   * An atom containing an object of nested field atoms
   */
  fields: WritableAtom<
    Fields,
    Fields | typeof RESET | ((prev: Fields) => Fields),
    void
  >;
  /**
   * An read-only atom that derives the form's values from
   * its nested field atoms.
   */
  values: Atom<FormFieldValues<Fields>>;
  /**
   * An read-only atom that derives the form's errors from
   * its nested field atoms.
   */
  errors: Atom<FormFieldErrors<Fields>>;
  /**
   * A read-only atom that returns `true` if any of the fields in
   * the form are dirty.
   */
  dirty: Atom<boolean>;
  /**
   * A read-only atom derives the touched state of its nested field atoms.
   */
  touchedFields: Atom<TouchedFields<Fields>>;
  /**
   * A write-only atom that resets the form's nested field atoms
   */
  reset: WritableAtom<null, void>;
  /**
   * A write-only atom that validates the form's nested field atoms
   */
  validate: WritableAtom<null, void | ValidateOn>;
  /**
   * A read-only atom that derives the form's validation status
   */
  validateStatus: Atom<ValidateStatus>;
  /**
   * A write-only atom for submitting the form
   */
  submit: WritableAtom<
    null,
    (values: FormFieldValues<Fields>) => void | Promise<void>
  >;
  /**
   * A read-only atom that reads the number of times the form has
   * been submitted
   */
  submitCount: Atom<number>;
  /**
   * An atom that contains the form's submission status
   */
  submitStatus: WritableAtom<SubmitStatus, SubmitStatus>;
}>;

useForm()

A hook that returns an object that contains the fieldAtoms and actions to validate, submit, and reset the form.

Arguments

Name Type Required? Description
formAtom FormAtom<Fields> Yes The atom that stores the form's state
options UseAtomOptions No Options that are forwarded to the useAtom, useAtomValue, and useSetAtom hooks

Returns

type UseFormAtom<Fields extends FormFields> = {
  /**
   * An object containing the values of a form's nested field atoms
   */
  fieldAtoms: Fields;
  /**
   * A function for handling form submissions.
   *
   * @param handleSubmit - A function that is called with the form's values
   *   when the form is submitted
   */
  submit(
    handleSubmit: (values: FormFieldValues<Fields>) => void | Promise<void>
  ): (e?: React.FormEvent<HTMLFormElement>) => void;
  /**
   * A function that validates the form's nested field atoms with a
   * `"user"` validation event.
   */
  validate(): void;
  /**
   * A function that resets the form's nested field atoms to their
   * initial states.
   */
  reset(): void;
};

useFormState()

A hook that returns the primary state of the form atom including values, errors, submit and validation status, as well as the fieldAtoms. Note that this hook will cuase its parent component to re-render any time those states change, so it can be useful to use more targeted state hooks like useFormStatus.

Arguments

Name Type Required? Description
formAtom FormAtom<Fields> Yes The atom that stores the form's state
options UseAtomOptions No Options that are forwarded to the useAtom, useAtomValue, and useSetAtom hooks

Returns

type UseFormState<Fields extends FormFields> = {
  /**
   * An object containing the form's nested field atoms
   */
  fieldAtoms: Fields;
  /**
   * An object containing the values of a form's nested field atoms
   */
  values: FormFieldValues<Fields>;
  /**
   * An object containing the errors of a form's nested field atoms
   */
  errors: FormFieldErrors<Fields>;
  /**
   * `true` if any of the fields in the form are dirty.
   */
  dirty: boolean;
  /**
   * An object containing the touched state of the form's nested field atoms.
   */
  touchedFields: TouchedFields<Fields>;
  /**
   * The number of times a form has been submitted
   */
  submitCount: number;
  /**
   * The validation status of the form
   */
  validateStatus: ValidateStatus;
  /**
   * The submission status of the form
   */
  submitStatus: SubmitStatus;
};

useFormActions()

A hook that returns a set of actions that can be used to update the state of the form atom. This includes updating fields, submitting, resetting, and validating the form.

Arguments

Name Type Required? Description
formAtom FormAtom<Fields> Yes The atom that stores the form's state
options UseAtomOptions No Options that are forwarded to the useAtom, useAtomValue, and useSetAtom hooks

Returns

type UseFormActions<Fields extends FormFields> = {
  /**
   * A function for adding/removing fields from the form.
   *
   * @param fields - An object containing the form's nested field atoms or
   *   a callback that receives the current fields and returns the next
   *   fields.
   */
  updateFields(
    fields: ExtractAtomArgs<ExtractAtomValue<FormAtom<Fields>>["fields"]>[0]
  ): void;
  /**
   * A function for handling form submissions.
   *
   * @param handleSubmit - A function that is called with the form's values
   *   when the form is submitted
   */
  submit(
    handleSubmit: (values: FormFieldValues<Fields>) => void | Promise<void>
  ): (e?: React.FormEvent<HTMLFormElement>) => void;
  /**
   * A function that validates the form's nested field atoms with a
   * `"user"` validation event.
   */
  validate(): void;
  /**
   * A function that resets the form's nested field atoms to their
   * initial states.
   */
  reset(): void;
};

useFormValues()

A hook that returns the values of the form atom.

Arguments

Name Type Required? Description
formAtom FormAtom<Fields> Yes The atom that stores the form's state
options UseAtomOptions No Options that are forwarded to the useAtom, useAtomValue, and useSetAtom hooks

Returns

type UseFormValues<Fields extends FormFields> = FormFieldValues<Fields>;

useFormErrors()

A hook that returns the errors of the form atom.

Arguments

Name Type Required? Description
formAtom FormAtom<Fields> Yes The atom that stores the form's state
options UseAtomOptions No Options that are forwarded to the useAtom, useAtomValue, and useSetAtom hooks

Returns

type UseFormErrors<Fields extends FormFields> = FormFieldErrors<Fields>;

useFormStatus()

A hook that returns the submitStatus and validateStatus of the form atom.

Arguments

Name Type Required? Description
formAtom FormAtom<Fields> Yes The atom that stores the form's state
options UseAtomOptions No Options that are forwarded to the useAtom, useAtomValue, and useSetAtom hooks

Returns

type UseFormStatus = {
  /**
   * The validation status of the form
   */
  validateStatus: ValidateStatus;
  /**
   * The submission status of the form
   */
  submitStatus: SubmitStatus;
};

useFormSubmit()

A hook that returns a callback for handling form submission.

Arguments

Name Type Required? Description
formAtom FormAtom<Fields> Yes The atom that stores the form's state
options UseAtomOptions No Options that are forwarded to the useAtom, useAtomValue, and useSetAtom hooks

Returns

type UseFormSubmit<Fields extends FormFields> = {
  (values: (value: FormFieldValues<Fields>) => void | Promise<void>): (
    e?: React.FormEvent<HTMLFormElement>
  ) => void;
};

Components

<Form>

A React component that renders form atoms and their fields in an isolated scope using a Jotai Provider.

Props

Name Type Required? Description
atom FormAtom<FormFields> Yes A form atom
store AtomStore No A Jotai store
component React.ComponentType<{state: UseFormState<Value>; actions: UseFormActions<Value>;}> No A React component to render as the input field
render (state: UseFormState<Value>, actions: UseFormActions<Value>) => JSX.Element No A render prop

<InputField>

A React component that renders field atoms with initial values. This is most useful for fields that are rendered as native HTML elements because the props can unpack directly into the underlying component.

Props

Name Type Required? Description
atom FieldAtom<Value> Yes A field atom
initialValue Value No The initial value of the field
type Type No The type of the field. Defaults to "text".
store AtomStore No A Jotai store
component React.ComponentType<{state: UseFieldState<Value>; actions: UseFieldActions<Value>;}> No A React component to render as the input field
render (state: UseFieldState<Value>, actions: UseFieldActions<Value>) => JSX.Element No A render prop

<TextareaField>

A React component that renders field atoms with initial values. This is most useful for fields that are rendered as native HTML elements because the props can unpack directly into the underlying component.

Props

Name Type Required? Description
atom FieldAtom<Value> Yes A field atom
initialValue Value No The initial value of the field
store AtomStore No A Jotai store
component React.ComponentType<{state: UseFieldState<Value>; actions: UseFieldActions<Value>;}> No A React component to render as the input field
render (state: UseFieldState<Value>, actions: UseFieldActions<Value>) => JSX.Element No A render prop

<SelectField>

A React component that renders field atoms with initial values. This is most useful for fields that are rendered as native HTML elements because the props can unpack directly into the underlying component.

Props

Name Type Required? Description
atom FieldAtom<Value> Yes A field atom
initialValue Value No The initial value of the field
multiple boolean No Is this a multi-select field?
store AtomStore No A Jotai store
component React.ComponentType<{state: UseFieldState<Value>; actions: UseFieldActions<Value>;}> No A React component to render as the input field
render (state: UseFieldState<Value>, actions: UseFieldActions<Value>) => JSX.Element No A render prop

<Field>

A React component that renders field atoms with initial values. This is most useful for fields that aren't rendered as native HTML elements.

Props

Name Type Required? Description
atom FieldAtom<Value> Yes A field atom
initialValue Value No The initial value of the field
store AtomStore No A Jotai store
component React.ComponentType<{state: UseFieldState<Value>; actions: UseFieldActions<Value>;}> No A React component to render as the field
render (state: UseFieldState<Value>, actions: UseFieldActions<Value>) => JSX.Element No A render prop

Utilities

walkFields()

A function that walks through an object containing nested field atoms and calls a visitor function for each atom it finds.

Arguments

Name Type Required? Description
fields FormFields Yes An object containing nested field atoms
visitor (field: FieldAtom<any>, path: string[]) => void | false Yes A function that will be called for each field atom. You can exit early by returning false from the function.

Returns

void

Utility types

FormValues

A utility type for inferring the value types of a form's nested field atoms.

const nameForm = formAtom({
  name: fieldAtom({ value: "" }),
});

type NameFormValues = FormValues<typeof nameForm>;

FormErrors

A utility type for inferring the error types of a form's nested field atoms.

const nameForm = formAtom({
  name: fieldAtom({ value: "" }),
});

type NameFormErrors = FormErrors<typeof nameForm>;

form-atoms/zod

zodValidate()

Validate your field atoms with Zod schemas. This function validates on every "user" and "submit" event, in addition to other events you specify.

Check out an example on CodeSandbox

import { z } from "zod";
import { formAtom, fieldAtom } from "form-atoms";
import { zodValidate } from "form-atoms/zod";

const schema = z.object({
  name: z.string().min(3),
});

const nameForm = formAtom({
  name: fieldAtom({
    validate: zodValidate(schema.shape.name, {
      on: "submit",
      when: "dirty",
    }),
  }),
});

Arguments

Name Type Required? Description
schema ((get: Getter) => z.Schema) | z.Schema Yes A Zod schema or a function that returns a Zod schema
config ZodValidateConfig No Configuration options

ZodValidateConfig

type ZodValidateConfig = {
  /**
   * The event or events that triggers validation.
   */
  on?: ZodValidateOn | ZodValidateOn[];
  /**
   * Validate if the field is:
   * - `touched`
   * - `dirty`
   */
  when?: "touched" | "dirty" | ("touched" | "dirty")[];
  /**
   * Format the error message returned by the validator.
   *
   * @param error - A ZodError object
   */
  formatError?: (error: ZodError) => string[];
};

Wait, it's all atoms?

Wait it's all atoms? Always has been.


LICENSE

MIT

form-atoms's People

Contributors

jaredlunde avatar miroslavpetrik avatar schiller-manuel avatar semantic-release-bot avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

form-atoms's Issues

How to set the initial values of a form?

I want to populate a form with values I get dynamically (from a http request, etc).

Is the only option using <Field> or can I pass values through a <Provider>-like component

EDIT: Ended up doing something like

const fooAtom = fieldAtom({ value: 0 });

const fields = {
  foo: fooAtom,
}
type FormFields = typeof fields;

const myFormAtom = formAtom(fields);

interface FormProps {
  initialValues: {
    [Field in keyof FormFields]: any,
  }
}

const Form = ({ initialValues }) => {
  useFieldAtomInitialValue(fooAtom, initialValues.foo);
  return <form>...</form>
}

`FormAtomValues` should accept `FormAtom` instead of `FormAtomFields`

Describe the bug
The naming of FormAtomValues indicates that given form atom, it will return the form values.
The problem is it accepts form fields, not form object thus making it confusing.

To Reproduce

const userForm = formAtom({
  first: fieldAtom({
    value: "",
  }),
  last: fieldAtom({
    value: "",
  }),
});

type Values = FormAtomValues<typeof userForm> // ERR!
// The helper type in reality accepts `FormAtomFields`
// and thus should be named `FormAtomFieldsValues`


// Currently custom helper is needed!
type FormValues<Form extends FormAtom<any>> = Form extends FormAtom<
  infer Fields
>
  ? FormAtomValues<Fields>
  : never;

type Values = FormValues<typeof userForm>

Expected behavior

  1. the call to FormAtomValues should work when given FormAtom.
  2. The existent FormAtomValues can be renamed to FormFieldsValues or FieldsValues or FieldAtomsValues ...

Why this makes sense

  1. Clear definition of form & fields
    1. fields is object containing the fieldAtoms
    2. form is return value of formAtom which accepts the fields
  2. Simple forms can be constructed without having a separate variable for the fields. Currently I often move the argument back and forth which is not good.
  3. This would be consistent with the hook useFormAtomValues which takes formAtom as argument, not form fields.

Additional context

  • docs must be updated

Bug - the `useField` reads value before initialization

Describe the bug
the first render with useField() does not use the value supplied in the initialValue option.

To Reproduce

  const field = fieldAtom<string>({ value: "" });

    const FirstValueProp = () => {
      const {
        state: { value },
      } = useField(field, { initialValue: "test" });
      const [firstValueProp] = useState(() => value);

      return <input value={firstValueProp} placeholder={value} />;
    };

    render(<FirstValueProp />);

the input placeholder will have the value "test", while the input value will have value "" from the first render.

Expected behavior
the value and placeholder should have the same value.

Screenshots
image

Additional context

  • This is problematic, when the value is forwarded to some uncontrolled components, which will be incorrectly initialized.

onReset callback

I'd like to react to a form reset by supplying e.g. an onReset callback that is executed after a form was reset.
Is this already possible?

My concrete use case is an atom that holds display state of the form, but is not part of the form state itself. I'd like to be able to set this atom upon form reset. I did not put into the form state since modifying just the form display state should not cause the form state to be dirty.

Permit array as root form fields

Currently the fields passed to a formAtom() must be an object.

type FormFields = {
    [key: string | number]: FieldAtom<any> | FormFields | FormFields[] | FieldAtom<any>[];
};

It could be generalized, to accept also array:

type FormArrayFields = FormFields[] | FieldAtom<any>[];
type FormFields = {
    [key: string | number]: FieldAtom<any> | FormFields | FormArrayFields;
} | FormArrayFields;

This could enable the submit value to be passed directly to some endpoints. For example, when configuring contact persons, I can submit the form data directly, as my endpoint expects array payload.

Currently, best we can do is to have the contact persons under some key in the form, which requires us to unwrap the data on submit:

const contactsForm = formAtom({persons: [...]}) // must use arbitrary key just to fulfil the interface

const { submit } = useForm(contactsForm)
// unnecessary unwrapping required
<form onSubmit={submit(({ persons }) => saveContactPersons(persons))}>

This could be:

const contactsForm = formAtom([...]) // array form fields

const { submit } = useForm(contactsForm)

<form onSubmit={submit(saveContactPersons)}>

Debug label completion

I've noticed that there are some atoms without labels, so I would make these improvements to debugging:

  1. the baseValueAtom is internal in fieldAtom
    I would mark it as debugPrivate = true

  2. the field itself (fieldAtom return value) is unlabelled
    This can be easily labelled field/${config.name ?? field}, The value displays the structure of the nested atoms, which is good

  3. the formAtom itself is unlabelled
    Like 2. I would label if form/${formAtom}, here we don't have a config.name like in fields, so the key will be useful to distinguish multiple mounted form atoms.

  4. for the internal atoms of formAtoms, we are missing scoping by the atom key
    Currently we have form/values, form/fields labels, but these can be scoped by atom key form/${formAtom}/value to again separate multiple mounted forms.

  5. The labeling of field atoms should be field/{config.name}/${atomName} instead of the current field/${atomName}/${config.name}
    This plays better with the devtools, where you can filter by prefix, so entergin field/username will filter only the internals of the username fieldAtom.

Boolean field atom does not fit `useFieldAtom` type.

Having atom with boolean value will display type error with the useFieldAtom

As a reproduction demo I have simple form with 2 checkbox inputs:
https://codesandbox.io/s/form-atoms-boolean-checkbox-field-9nv7jo?file=/src/components/checkbox-field.tsx:660-719

Expected behavior
There should be no type error.

I believe this was partly fixed for the <Field /> component in
#11.

Open question:
There is the InputField which is still constrained to the string | number | string[] and perhaps could be extended to allow boolean, as checkboxes are regular inputs of type checkbox. I believe that would be misleading, as the onChange handler for the input should be changed as well, as it uses event.target.checked instead of event.target.value https://codesandbox.io/s/form-atoms-boolean-checkbox-field-9nv7jo?file=/src/components/input-field.tsx:691-766.
So my question is whats the direction with the InputField? Would it be feasible in the future to have its props properly typed with discriminant types for various inputs e.g. of type file and others?

Direct DOM mutation for fields

Is your feature request related to a problem? Please describe.

React hook form has shown that direct DOM manipulation works in place of the classic react controlled component pattern.

Describe the solution you'd like

Improve hooks and examples to support this pattern.

Describe alternatives you've considered

React controlled components, or a wrapper around field atom.

Additional context

Great library, well typed and documented! Happy to contribute PRs.

v2

  • Use the Jotai v2 API
    • Replace scope with store and context
    • await get()
    • initialValues={[[countAtom, 1]]} is replaced by createStore() and store.set()
    • Maybe do something with abort signals
  • Create a useInputFieldAtom() hook, remove props from useFieldAtom()
    #18 (comment)
  • Maybe add form-atoms/zod validator
    #17 (comment)
    • For consideration: #23
  • FormValues and FormErrors should infer fields from form instead of using fields as the generic
    #28
  • Fix incorrect dirty value when initialValue is used
  • Fix reset doesn't set value to the value in useFieldInitialValue
    #33
  • Fix #26
  • Update CodeSandbox examples
  • Use tsup for build step
  • Use vitest for tests
  • Pin package versions in devDependencies
  • Fix GitHub build badge
  • Remove .tsbuildinfo and types directory
  • Add prettier to eslint config instead of running them separately

Number values cannot be used safely with `<InputField />` nor `useInputField`

Hello

  1. The <InputField /> accepts number as generic FieldAtom value
  2. so we can initialize our fieldAtom with number and pass it in safely
  3. When user changes the input, the handler reads event.target.value and sets it as value thus number becomes a string.
  4. This is error (also expected in code, so should be addressed), as it defies purpose of typescript

To Reproduce
codesandbox demo

Expected behavior
Alternatives:
a) The value should stay of type number when user changes the input & submits.
b) the <InputField /> should not accept non-string fieldAtoms

Possible solution
The generic type should be narrowed, so it excludes number. Thus we avoid this pitfall.
or
Extend the InputField hook/component to accept an event value getter.

For number we could read event.target.valueAsNumber like here

There is no reason to keep it the way it is. If it would to stay, then the InputField generic type could be extended to accept also boolean, as that would work with <input type="checkbox /> and would be broken the same way. So it does not matter if we have 1 or 2 broken things as they are equally evil.

I have modified version of the useFieldHook so the event is parametrized in order to support various input types
e.g. read event.target.checked for checkbox of event.target.files for file input etc.

We could ship something solid, maybe even for all the well known input types? Lets discuss, the expect-error indicates a knot which should be unfolded so the lib is pure.

Export `validateAtoms` on `formAtom` similarly to `_validateCallback` on `fieldAtom`

I have a listAtom where I need to validate nested formAtoms
https://github.com/form-atoms/field/blob/0b1e474c84bce88d89495a615c4eda23d7bfbb94/src/atoms/list-atom/listAtom.ts#L261

So far I've copied the validateAtoms function, as to verify that it will work.
https://github.com/form-atoms/field/blob/0b1e474c84bce88d89495a615c4eda23d7bfbb94/src/atoms/list-atom/listAtom.ts#L313.

Since the code is redundant, it's better to expose the function as a property of the formAtom so I can call formAtom._validateAtoms() instead.

Examples: Validation Option Valibot along with zod

Is your feature request related to a problem? Please describe.
As per this blog and intention of this library to the smallest, Can we try to use valibot instead of zod.

This is purely to reduce the bundle size and keeping smallest chunk of peer dependencies to this library.

Describe the solution you'd like
I am looking if there is need of some work from this library so that its supported out of the box and looking for examples

Describe alternatives you've considered
A clear and concise description of any alternative solutions or features you've considered.

Additional context
Similar work was done jotai labs too Reference

When input is `required` the `_validateCount` increases while the validate function is not being called

Have regular input with native required prop. When this input is empty, browser traps the submit event and displays the native popup window requiring user to fill this input.

Currently the _validateCount increases with every submit while the validate function is not being called.

Here is codesandbox demo with two inputs.

Steps:

  1. open console
  2. submit the form
  3. observe that nothing is logged, the input has popup "please fill out this form"
  4. submit the form again
  5. in console it logs {name: "first", validateStatus: "valid", _validateCount: 1}

For me the increasing _validateCount is a problem, as I use it in conjunction with the aria-invalid prop.
When a field is required, I set the aria-invalid to _validateCount > 0 && validateStatus === "invalid"

Since the "valid" is initial value of the _validateStatus atom, my inputs get the aria-invalid="false" prop which colors inputs in green when you submit twice

I understand that the atoms prefixed with _ are internals, and likely should not be used for this aria-invalid behavior.

I think the solution here is not to stop the validateCount from increasing, but instead have a new validatedCount atom which will indicate that the field did validate in the past. Since the validate function did not run, it will stay 0, so I won't see the false-positive value of aria-invalid=false.

Also this would solve one other issue for me - the _validateCount is not being reset when the form state is reset (which is ok, since it's internal thing) so having the validatedCount would be a public API.

This started like a bug initially, but since I don't have other solution in mind consider this a feature request.

Use atom id instead of 'unnamed-field' in debugLabels

Each atom has unique id which can be obtained as atom.toString().
We can use this in the debug labels instead of unnamed-field.

This way, in the devtools when there are 2 fieldAtoms, we can group all the atoms belonging to a field by filtering the atomId, which is better than having bunch of 'unnamed' labels mixed all together.

Allow `Value` to be any type instead of the union `string | number | string[]`

Is your feature request related to a problem? Please describe.
I have some fields whose value is an ID to an object but I store another value from the object to show the user. So I'd like to do something like:

user: fieldAtom(value: {
  id: null,
  name: "",
})

This way, I can update the values atomically.

Describe the solution you'd like
I would like to be able to add any value to a fieldAtom and be able to use them with components such as Field.

Describe alternatives you've considered
Use two fields.

Additional context
...

Validate by schema (`zod` or other)

Is your feature request related to a problem? Please describe.

The atoms can be validated by passing validate function into the config, but this does not scale well, as we don't want to write code for each field atom separately.

import { z } from "zod";

export const productSchema = z.object({
  name: z.string().min(3),
  // rest of the fields
});

const nameAtom = fieldAtom({
    value: "",
    validate: ({ value }) => {
      try {
        productSchema.pick({ name: true }).parse({ name: value });
        return [];
      } catch (err) {
        if (err instanceof z.ZodError) {
          return err.issues.map((issue) => issue.message);
        }
      }
    },
  }),

Ultimately, the library should accept a schema against which the atoms would automatically validate. This is common practice e.g. in react-hook-form.

Describe the solution you'd like

Solution would be to provide a integration with one or more popular schema validation libraries:

import { validateBySchema } from "form-atoms/zod";

const nameAtom = fieldAtom({
    value: "",
    validate: validateBySchema(productSchema, "name"),
  }),

// definition of validation helper
export const validateBySchema =
  <V, T extends ZodRawShape>(
    schema: ZodObject<T>,
    fieldName: keyof T
  ): FieldAtomConfig<V>["validate"] =>
  ({ value }) => {
    try {
      // TODO: generic type help needed
      // @ts-ignore
      schema.pick({ [fieldName]: true }).parse({ [fieldName]: value });
      return [];
    } catch (err) {
      console.info({ err });
      if (err instanceof ZodError) {
        return err.issues.map((issue) => issue.message);
      }
    }
  };

This reduces the boilerplate code as we can easily validate other fields:

export const productSchema = z.object({
  name: z.string().min(3),
  description: z.string().min(20),
});

const descriptionAtom = fieldAtom({
  value: "",
  validate: validateBySchema(productSchema, "description"),
}),

Describe alternatives you've considered

Alternative would be to have the validation not on the individual atoms, but on the formAtom which would just accept the schema object and validate each atom respectively.

Additional context

The same issue has discussion in the jotai-form project:
jotaijs/jotai-form#2

What's the reason for limiting setting the initial value only once?

The docs say about useFieldInitialValue:

Initial values can only be set once per scope. Therefore, if the initial value used is changed during rerenders, it won't update the atom value.

But where does this limitation come from?
Why can't the initial value be updated as follows?

function useFieldInitialValue<Value>(
  fieldAtom: FieldAtom<Value>,
  initialValue?: Value,
  options?: UseAtomOptions,
): UseFieldInitialValue {
  const field = useAtomValue(fieldAtom, options);
  const store = useStore(options);

  useEffect(() => {
    if (initialValue === undefined) {
      return;
    }
    if (store.get(field._initialValue) === initialValue) {
      return;
    }
    if (!store.get(field.dirty)) {
      store.set(field.value, initialValue);
    }
    store.set(field._initialValue, initialValue);
  }, [store, field._initialValue, field.value, field.dirty, initialValue]);
}

`FormAtomValues` hints wrong type to the developer

Describe the bug
The return type of FormAtomValues should contain only primitive language types, while it still mentions FieldAtom generics.
The type itself is correct, but the developer experience could be better.

Case 1: array of field atoms FieldAtom<T>[]

const imageFields = {
  images: [
    fieldAtom({
      value: { id: "1", url: "https://" },
    }),
    fieldAtom({
      value: { id: "2", url: "https://" },
    }),
  ],
};

// when hovered/inspected
type ImageValuesACTUAL = FormAtomValues<typeof imageFields>;

// ISSUE1: the [x: number] is ugly way to say there is array of items
// ISSUE2: FieldAtom is obfuscating the actual type
// ISSUE3: there is recursive call to FormAtomValues
type ImageValuesACTUAL = {
  images: FormAtomValues<{
    [x: number]: FieldAtom<{
      id: string;
      url: string;
    }>;
  }>;
};

// FIX1: the array has intuitive syntax []
// FIX2: we have plain values, not boxes of FieldAtom
// FIX3: no recursive call
type ImageValuesEXPECTED = {
  images: {
    id: string;
    url: string;
  }[];
};

// Fix of https://github.com/jaredLunde/form-atoms/blob/main/src/index.tsx#L1208
type _FormAtomValues<Fields extends FormAtomFields> = {
  [Key in keyof Fields]: Fields[Key] extends FieldAtom<infer Value>
    ? Value
    : Fields[Key] extends FormAtomFields
    ? _FormAtomValues<Fields[Key]>
    : Fields[Key] extends FieldAtom<infer Value>[] // FIX2: here go deeper into array of FieldAtom; no need to recurr
    ? Value[]
    : Fields[Key] extends FormAtomFields[] // FIX1: fixes the [x: number] for Case 2 below
    ? _FormAtomValues<FormAtomFields>
    : never;
};

Case 2: array of 'fields' {[string]: FieldAtom<any>}[]

const addressesFields = {
  addresses: [
    {
      city: fieldAtom({ value: "Stockholm" }),
      street: fieldAtom({ value: "Carl Gustav Street" }),
    },
    {
      city: fieldAtom({ value: "Bratislava" }),
      street: fieldAtom({ value: "Kosicka" }),
    },
  ],
};

type AddressValuesACTUAL = FormAtomValues<typeof addressesFields>;

// when hovered 
type AddressValuesACTUAL = {
    addresses: FormAtomValues<{
        [x: number]: {
            city: FieldAtom<string>;
            street: FieldAtom<string>;
        };
    }>;
}

type AddressValuesEXPECTED = {
    addresses: {
        city: string;
        street: string;
    }[];
}

// Now the fix is a bit trickier - the arrays does not contain leaf nodes, they have objects, so we must recurr,
// but recurr would keep the `FormAtomValues` call indication in the result, and obfuscate it
// SO WE CAN recurr by duplicating the code.
// This does not cover all cases, e.g. deeply nested form definitions would be still 'bad', but certain level, e.g. depth 2
// could be supported to cover most user needs:
type _FormAtomValues<Fields extends FormAtomFields> = {
  [Key in keyof Fields]: Fields[Key] extends FieldAtom<infer Value>
    ? Value
    : Fields[Key] extends FormAtomFields
    ? _FormAtomValues<Fields[Key]>
    : Fields[Key] extends FieldAtom<infer Value>[]
    ? Value[]
    : Fields[Key] extends (infer NestedFields)[]
    ? NestedFields extends FormAtomFields
      ? {
          [K2 in keyof NestedFields]: NestedFields[K2] extends FieldAtom<
            infer Value
          >
            ? Value
            : NestedFields[K2] extends FormAtomFields
            ? _FormAtomValues<NestedFields[K2]>
            : NestedFields[K2] extends any[]
            ? _FormAtomValues<NestedFields[K2][number]>
            : never;
        }[]
      : never
    : never;
};

Additional context

Pass setter to the `Validate` callback.

Is your feature request related to a problem? Please describe.
I have a special listAtom which has each list item wrapped as an internal formAtom. When validating a form with the listAtom, I need to also validate the internal forms. For that, the set(form.validate, event) must be called.

Describe the solution you'd like
Simply extend the Validate callback to also get the set jotai function.

Describe alternatives you've considered
None

Hydration with `useFieldInitialValue` in Next.js renders twice - first empty, then filled input.

I believe that currently the useFieldInitialValue is not optimized for SSR, as it fails to render a filled input on the first paint.
I suspect that the useEffect might be the cause.
So far I've switched to useHydrateAtoms from jotai/utils which fixed the issue. form-atoms/field@9264c90#diff-57ff5d92fdd79e6888455c22013fd0924199b51e87f1be3075f7c8b9cc848c47R5

The hydrateAtoms will set the atoms only once, while the useFieldInitialValue was recently updated to permit multiple updates. #52

The multiple updates I believe are also vital for Next.js. My use case is that after a form is saved, I call router.refresh(), where the router is the useRouter() from nextjs/navigation. This successfully re-initialized my form with the changes from #52, but it fails to work (expected) with the useHydrateAtoms.

Potential solutions

  • use the useHydrateAtoms in the useFieldInitialValue
  • update useFieldInitialValue to set the first value outside of the useEffect?

Beware that the useHydrateAtoms might not be straighforward - with my linked implementation for useHydrateField I'm experiencing HTML content mismatch, as multiple listAtom items result to multiple DOM nodes. So after the router.refresh() when item in a list was added/removed, the FE jotai store is stalled (since useHydrateAtoms is permited only once) and the BE renders different amount of items, so a mismatched HTML. Here I can clear the jotai store, but likely that won't help as the internal hydrateMap preventing the duplicate hydration is not resetable from outside... https://github.com/pmndrs/jotai/blob/2d760920f5ebea05abaa8e072cee578fa125ca9b/src/react/utils/useHydrateAtoms.ts#L22C7-L22C18
So maybe even that could be made resetable in the jotai lib, but that would require manual user cache invalidation ,e.g. just after the router.refresh() call in my case.

Add `clear` action to support controlling `input[type='file']`

In react the file input cannot be controlled. This means, that when we reset a form, the input will still look as filled. (it will display the filename of last picked file).

Users must clear the input manually via the ref. We already have ref in the fieldAtom which is good.

I've created a reusable hook which provides clear() action which uses the ref. This is similar to the existing focus() method.

Lastly, I have clearInputEffect. Which essentially does what React could do - it waits for the empty value to be set on the field (e.g. when we call reset()) and then clears the input. The empty value is not important in this case.

This clear effect hook is not only usage for the clear action. Other is having clear buttons next to the input, to enable users to quickly clear the input content. This can be useful for any input, e.g. date, password...

I believe this clearing would make this library more complete, as we aim here to have all the inputs controlled.

The automated release is failing 🚨

🚨 The automated release from the main branch failed. 🚨

I recommend you give this issue a high priority, so other packages depending on you can benefit from your bug fixes and new features again.

You can find below the list of errors reported by semantic-release. Each one of them has to be resolved in order to automatically publish your package. I’m sure you can fix this 💪.

Errors are usually caused by a misconfiguration or an authentication problem. With each error reported below you will find explanation and guidance to help you to resolve it.

Once all the errors are resolved, semantic-release will release your package the next time you push a commit to the main branch. You can also manually restart the failed CI job that runs semantic-release.

If you are not sure how to resolve this, here are some links that can help you:

If those don’t help, or if this issue is reporting something you think isn’t right, you can always ask the humans behind semantic-release.


No npm token specified.

An npm token must be created and set in the NPM_TOKEN environment variable on your CI environment.

Please make sure to create an npm token and to set it in the NPM_TOKEN environment variable on your CI environment. The token must allow to publish to the registry https://registry.npmjs.org/.


Good luck with your project ✨

Your semantic-release bot 📦🚀

Field & Form atoms should be of `PrimitiveAtom` type

Describe the bug
Form atom is defined as Atom<...>.
The same is true for FieldAtom

in practice, jotai atom(initialValue) call returns a PrimitiveAtom which has a write method.

To Reproduce
try setting a field or form atom's value:

const f = fieldAtom()

// type err, f has no write method
set(f, get(f)); 

Expected behavior
The write via set should work.

Screenshots
image

  • Version: 3.2.4

Additional context
Other atoms could use the PrimitiveAtom in type definitions. e.g.

Submitting form after removing last item from array fields returns empty object

Maybe just undefined behavior, but from consistency standpoint I think the following can be improved:

To Reproduce

  1. open array fields example
  2. delete the initial hobby by clicking the gray button
  3. click submit blue button
  4. {} appears in the alert window

Expected behavior
I would expect { hobbies: [] } as the submit value

The empty object is also inconsistent with the inferred type:

FormAtomValues<{
    hobbies: {
        name: FieldAtom<string>;
    }[];
}>

// { hobbies: {name: string}[] }

Additional context
I consider the issue to be in the valuesAtom where the walk over the fields happens.
Technically the hobbies array is not a field, so it's skipped.

I noticed that the library supports two kinds of arrays in the form fields object:
FormAtomFields[] | FieldAtom<any>[]

And that basically all the field atoms are eventually leafs of the tree of type FormAtomFields.
Eventually I will need to define some validation rules for the field array, e.g. to have minimum 1 hobbies or max 3.

It could be related to how the fix for above will be solved. Maybe there could be support for a type
FieldAtom<FormAtomFields[] | FieldAtom<any>[]> or something similar, but that would likely require more work, as the field atom could be in positions where it's not a leaf of the tree...

Maybe it even works now, I will try this case soon :)

Outdated lockfile?

When I install project with pnpn it creates a lockfile with new version:
image

Likely we want to update the project lockfile?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.