Awesome Forms with Solidjs
Tags: solidjs, rxjs, forms, githunt
Published: 2021-04-27 18:56:15

_Editors note 6/7/22: I overhauled this article in response to library improvements._

I recently started falling in love with [Solidjs](https://www.solidjs.com/), a javascript library that looks like React but is significantly faster and, dare I say, has a notably better API. Unlike React, Solidjs component functions are invoked only once when the component is initialized and then never again.

I decided to take advantage of Solidjs' strengths, and build a 3.6kb min zipped library to aid with user input forms: [solid-forms](https://github.com/jorroll/solid-forms). Let's dive in and see what we can do (note, if you want an introduction to Solidjs, [start here](https://dev.to/ryansolid/introducing-the-solidjs-ui-library-4mck)).

Let's create a simple `TextField` component in typescript.

```tsx
import { IFormControl, createFormControl } from 'solid-forms';

export const TextField: Component<{
  control: IFormControl<string>,
  label: string,
  placeholder?: string,
}> = (props) => {
  return (
    <label>
      <span class='input-label'>{props.label}</span>

      <input
        type="text"
        value={props.control.value}
        oninput={(e) => {
          props.control.markDirty(true);
          props.control.setValue(e.currentTarget.value || null);
        }}
        onblur={() => props.control.markTouched(true)}
        placeholder={props.placeholder}
      />
    </label>
  );
};
```

This component tracks whether it has been `touched` by a user (notice `onblur` callback) and whether it has been changed by a user (`oninput`). When a user changes the value, we mark the control as `dirty` to track that the value has been changed by the user. We also have the ability to set a label on the input as well as a placeholder. Pretty straightforward stuff.

But text field's are _rarely_ used in isolation. We want to build a component to collect some address information. This will involve asking for a `Street`, `City`, `State`, and `Postcode`. Lets use our `TextField` component to create our `AddressForm`.

```tsx
import { withControl, createFormGroup, createFormControl } from 'solid-forms';

const controlFactory = () => 
    createFormGroup({
      street: createFormControl<string | null>(null),
      city: createFormControl<string | null>(null),
      state: createFormControl<string | null>(null),
      zip: createFormControl<string | null>(null),
    });

export const AddressForm = withControl({
  controlFactory,
  component: (props) => {
    const controls = () => props.control.controls;

    return (
      <fieldset classList={{
        "is-valid": props.control.isValid,
        "is-invalid": !props.control.isValid,
        "is-touched": props.control.isTouched,
        "is-untouched": !props.control.isTouched,
        "is-dirty": props.control.isDirty,
        "is-clean": !props.control.isDirty,
      }}>
        <TextField label="Street" control={controls().street} />
        <TextField label="City" control={controls().city} />
        <TextField label="State" control={controls().state} />
        <TextField label="Postcode" control={controls().zip} />
      </fieldset>
    );
  },
});
```

Note that the address form is wrapped with `withControl()`, a higher order component. This streamlines the process of creating reusable form components.

We want our `AddressForm` to use a `FormGroup` control rather than the default `FormControl` so we provide a `controlFactory` function which initializes the control. 

```tsx
const controlFactory = () => 
    createFormGroup({
      street: createFormControl<string | null>(null),
      city: createFormControl<string | null>(null),
      state: createFormControl<string | null>(null),
      zip: createFormControl<string | null>(null),
    });

export const AddressForm = withControl({
  controlFactory,
  component: (props) => {
    // continued...
```

All we needed to do to connect our AddressForm control to the `TextField's` control was to use the `control={/* ... */}` property to specify which FormControl on the parent should be connected with the child TextField.

```tsx
const controls = () => props.control.controls;
// ...
<TextField label="Street" control={controls().street} />
<TextField label="City" control={controls().city} />
```

We also set the component up to apply css classes based on if the `AddressForm` is valid/invalid, edited/unedit, and touched/untouched.

Say we want to hook our AddressForm component into a larger form. That's also easy!

```tsx
// factory for initializing the `MyLargerForm` `FormGroup`
const controlFactory = () => 
    createFormGroup({
      firstName: TextField.control(),
      address: AddressForm.control(),
    });

// the form component itself
export const MyLargerForm = withControl({
  controlFactory,
  component: (props) => {
    const controls = () => props.control.controls;

    // because we can
    const lastNameControl = createFormControl<string | null>(null);
  
    return (
      <form>
        <fieldset>
          <TextField label="First name" control={controls().firstName} />
          <TextField label="Last name" control={lastNameControl} />
        </fieldset>

        <AddressForm control={controls().address} />
      </form>
    );
  },
});
```

And, with just a few steps, we have a very powerful, very composible set of form components. As changes happen to the `TextField` components, those changes flow upwards and automatically update the parent `FormGroup` components.

You may have noticed that we did something new in the `controlFactory` function.

```ts
const controlFactory = () => 
    createFormGroup({
      firstName: TextField.control(),
      address: AddressForm.control(),
    });
```

We could have defined this controlFactory function the same way we defined the others (i.e. using `createFormControl()` and `createFormGroup()`), but `withControl()` helpfully adds the control's `controlFactory` function to a special `control` property on a component created using `withControl()`. This DRYs up our code and allows easily creating FormControl's for any components you create `withControl()`.

So what if we want to listen to control changes in our `MyLargerForm` component? Well that's easy. For example, to listen to when any part of the form is touched, we can simply observe the `isTouched` property inside a Solidjs effect.

```tsx
createEffect(() => {
  if (!props.control.isTouched) return;

  // do stuff...
});
```

To listen to when the "firstName" control, specifically, is touched

```tsx
createEffect(() => {
  if (!props.control.controls.firstName.isTouched) return;

  // do stuff...
});
```

Here's a more complex, advanced example: if we want to listen for value changes, debounce the rate of changes, perform validation, and mark the `control` as pending while we wait for validation to complete, we can do the following. Note, when we set errors on the `firstName` control, that will result in the "First name" `TextField`  being marked as invalid (score!).

```tsx
import { myCustomValidationService } from './my-validation-service';

export const MyLargerForm = withControl({
  // ...hiding the controlFactory boilerplate...
  component: (props) => {
    const control = () => props.control;
    const controls = () => control().controls;

    // This is a simplified example. In reality, you'd want
    // to debounce user input and only run async
    // validation when it made sense rather than on every
    // change. There are multiple ways to accomplish this
    // but none of them are specific to Solid Forms.
    createEffect(async () => {
      const firstName = controls().firstName;

      firstName.markPending(true);

      const response = await myCustomValidationService(
        firstName.value
      );

      if (response.errors) {
        firstName.setErrors({ validationFailed: true });
      } else {
        firstName.setErrors(null);
      }

      firstName.markPending(false);
    });

    const onsubmit (e) => {
      e.preventDefault();
      if (control().isPending || !control().isValid) return;

      // do stuff...
    };

    return (
      <form onsubmit={onsubmit}>
        <fieldset>
          <TextField label="First name" control={controls().firstName} />
          <TextField label="Last name" control={controls().lastName} />
        </fieldset>

        <AddressForm control={controls().address} />
      </form>
    );
  },
});
```

This is really just scratching the surface of what you can do with `solid-forms`. [Check out the repo to read the documentation and learn more](https://github.com/jorroll/solid-forms).

## Check out the repo

- [https://github.com/jorroll/solid-forms](https://github.com/jorroll/solid-forms)