Awesome Forms with Solidjs

I recently started falling in love with Solidjs, 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 ini…

I recently started falling in love with Solidjs, 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 9kb min zipped library to aid with user input forms: rx-controls-solid. Let’s dive in and see what we can do (note, if you want an introduction to Solidjs, start here).

Let’s create a simple TextField component in typescript.

import { withControl, FormControl } from 'rx-controls-solid';

export const TextField = withControl((props) => {
  // prop.control is static for the lifetime of the component
  const control = props.control as FormControl<string | null>;

  return (
    <label>
      <span class='input-label'>{props.label}</span>

      <input
        type="text"
        value={control.value}
        oninput={(e) => {
          control.markDirty(true);
          control.setValue(e.currentTarget.value || null);
        }}
        onblur={() => 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. 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.

import { withControl, FormGroup, FormControl } from 'rx-controls-solid';
import { toSignal } from './utils';

const controlFactory = () => 
    new FormGroup({
      street: new FormControl<string | null>(null),
      city: new FormControl<string | null>(null),
      state: new FormControl<string | null>(null),
      zip: new FormControl<string | null>(null),
    });

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

    const isControlValid = toSignal(control.observe('valid'));
    const isControlTouched = toSignal(control.observe('touched'));
    const isControlDirty = toSignal(control.observe('dirty'));

    return (
      <fieldset classList={{
        "is-valid": isControlValid(),
        "is-invalid": !isControlValid(),
        "is-touched": isControlTouched(),
        "is-untouched": !isControlTouched(),
        "is-dirty": isControlDirty(),
        "is-clean": !isControlDirty(),
      }}>
        <TextField label="Street" controlName="street" />
        <TextField label="City" controlName="city" />
        <TextField label="State" controlName="state" />
        <TextField label="Postcode" controlName="zip" />
      </fieldset>
    );
  },
});

Note that the address form, itself, is also wrapped withControl(). This allows the AddressForm to also be used as a form component in a larger parent form.

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

const controlFactory = () => 
    new FormGroup({
      street: new FormControl<string | null>(null),
      city: new FormControl<string | null>(null),
      state: new FormControl<string | null>(null),
      zip: new FormControl<string | null>(null),
    });

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

    const isControlValid = toSignal(control.observe('valid'));
    // continued...

All we needed to do to connect our AddressForm control to the TextField's control was to use the controlName="street" property to specify which FormControl on the parent should be connected with the child TextField.

<TextField label="Street" controlName="street" />
<TextField label="City" controlName="city" />

We also set the component up to apply css classes based on if the AddressForm is valid/invalid, edited/unedit, and touched/untouched. There’s actually a helper function to make applying css classes really easy, but for the sake of education I didn’t use it for this example.

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

export const MyLargerForm = withControl({
  controlFactory: () => 
    new FormGroup({
      firstName: new FormControl<string | null>(null),
      address: new FormGroup({
        street: new FormControl<string | null>(null),
        city: new FormControl<string | null>(null),
        state: new FormControl<string | null>(null),
        zip: new FormControl<string | null>(null),        
      }),
    }),
  component: (props) => {
    const control = props.control;

    // because we can
    const lastNameControl = new FormControl<string | null>(null);

    return (
      <form>
        <fieldset>
          <TextField label="First name" controlName="firstName" />
          <TextField label="Last name" control={lastNameControl} />
        </fieldset>

        <AddressForm controlName="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.

We can easily listen to any of these changes and respond to them via the parent.

For example, to listen to when any part of the form is touched, we can simply subscribe to touched property state/changes.

control.observe('touched').subscribe(v => {/* ... */})

To listen to when the “firstName” control, specifically, is touched

// this is similar to control.controls.firstName.touched
control.observe('controls', 'firstName', 'touched')
// or
control.get('firstName').observe('touched')

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!).

import { interval } from 'rxjs';
import { switchMap, tap } from 'rxjs/operators';
import { myCustomValidationService } from './my-validation-service';

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

    const sub = control.observe('value', 'firstName').pipe(
      tap(() => firstName.markPending(true)),
      switchMap(v => interval(500).pipe(
        switchMap(() => myCustomValidationService(v)),
        tap(() => firstName.markPending(false)),
      )),
    ).subscribe(result => {
      if (result.errors) {
        firstName.setErrors({ validationFailed: true });
      } else {
        firstName.setErrors(null);
      }
    });

    const onsubmit (e) => {
      e.preventDefault();
      if (control.pending || control.invalid) return;

      // do stuff...
    };

    onCleanup(() => sub.unsubscribe());

    return (
      <form onsubmit={onsubmit}>
        <fieldset>
          <TextField label="First name" controlName="firstName" />
          <TextField label="Last name" control={lastNameControl} />
        </fieldset>

        <AddressForm controlName="address" />
      </form>
    );
  },
});

This is really just scratching the surface of what you can do with rx-controls-solid. I don’t have much in the way of documentation at this point, but you can play around with the library using this codesandbox.



Check out the repo


Print Share Comment Cite Upload Translate
APA
John Carroll | Sciencx (2024-03-28T13:05:04+00:00) » Awesome Forms with Solidjs. Retrieved from https://www.scien.cx/2021/04/27/awesome-forms-with-solidjs/.
MLA
" » Awesome Forms with Solidjs." John Carroll | Sciencx - Tuesday April 27, 2021, https://www.scien.cx/2021/04/27/awesome-forms-with-solidjs/
HARVARD
John Carroll | Sciencx Tuesday April 27, 2021 » Awesome Forms with Solidjs., viewed 2024-03-28T13:05:04+00:00,<https://www.scien.cx/2021/04/27/awesome-forms-with-solidjs/>
VANCOUVER
John Carroll | Sciencx - » Awesome Forms with Solidjs. [Internet]. [Accessed 2024-03-28T13:05:04+00:00]. Available from: https://www.scien.cx/2021/04/27/awesome-forms-with-solidjs/
CHICAGO
" » Awesome Forms with Solidjs." John Carroll | Sciencx - Accessed 2024-03-28T13:05:04+00:00. https://www.scien.cx/2021/04/27/awesome-forms-with-solidjs/
IEEE
" » Awesome Forms with Solidjs." John Carroll | Sciencx [Online]. Available: https://www.scien.cx/2021/04/27/awesome-forms-with-solidjs/. [Accessed: 2024-03-28T13:05:04+00:00]
rf:citation
» Awesome Forms with Solidjs | John Carroll | Sciencx | https://www.scien.cx/2021/04/27/awesome-forms-with-solidjs/ | 2024-03-28T13:05:04+00:00
https://github.com/addpipe/simple-recorderjs-demo