@shelacek/formica

Preact forms made easy!

Usage no npm install needed!

<script type="module">
  import shelacekFormica from 'https://cdn.skypack.dev/@shelacek/formica';
</script>

README

Formica

npm npm bundle size (minified + gzip)

Preact forms made easy!

Formica allows you to build fairly complex forms declaratively in JSX. Nested forms and arrays are supported out of the box. The library is trying to not re-invent the wheel so it combines native forms API with controlled inputs (native isn't that bad). Validation is handled by the browser's constraint validation. Also, Formica has no dependencies and has a small footprint!

If you encounter a bug, please create an issue.

Example

Minimal, but complete example can look like:

import { h, render, Component } from 'preact';
import { Form, FormGroup } from '@shelacek/formica';

export default class ProfileForm extends Component {
    state = { form: null };

    componentDidMount() {
        this.setState({
            form: /* await fetch(...).json() */ {
                name: 'John Doe',
                info: {
                   bio: 'Hello, I\'m John Doe and ...',
                   website: 'http://example.com'
                }
            }
        });
    }

    handleChange = form => { // choose your favorite binding method
        this.setState({ form });
    };

    handleSubmit = event => {
        if (event.target.checkValidity()) {
            /* fetch(..., { body: JSON.stringify(this.state.form) }) */
            console.log('submitted form:', this.state.form);
        }
    };

    render({}, { form }) {
        return (
            <Form value={form} onChange={this.handleChange} onSubmit={this.handleSubmit}>
                <label>
                   Name:
                   <input name="name" type="text" required />
                </label>
                <FormGroup name="info">
                <label>
                   Bio:
                   <textarea name="bio" maxLength="250" />
                </label>
                <label>
                   Website:
                   <input name="website" type="text" pattern="^(http://|https://)\S{1,63}quot; />
                </label>
                </FormGroup>
                <input type="submit" />
            </Form>
        );
    }
}

render(<ProfileForm />, document.body);

Edit formica-demo

For more complex example take a look at Caesar cipher demo.

All components are available on the top-level export:

import { Form, FormGroup, FormArray /* ... */ } from '@shelacek/formica';

How it works

Formica consist of <Form /> and controls <FormGroup />, <FormArray />, <FormControl /> and <FormAction />. Form control can be also native form input like <input> or <select>. <Form /> wrap native <form> element and expose form model to it's children.

<FormGroup /> represents the basic building block. It selects property from form model by it's name prop and connect this submodel to actual controls, like <input>, but also another <FormGroup />, <FormArray /> or <FormControl />.

<FormArray /> receive array and for every item render and connect its children. <FormArray /> can also receive the model object. In this case, it selects property by name prop like <FormGroup /> does.

<FormAction /> allow simple form model modifications, like adding/removing items of the array.

<FormControl /> is a helper component, that exposes disabled, touched and validity to enable displaying of validation message. If you don't need validation or simple :invalid CSS pseudo-class is enough, you can ignore <FormControl /> entirely.

Custom form controls

You can create custom controls. Formica map any vnode, that is enhanced with asFormControl Higher-Order Components.

Custom form controls receive these props:

Prop Type Description
name string ⎮ number (optional) Name of connected model property.
value any Value of connected model property.
onChange { (event: OnChangeEvent<any>): void } Value changes handler.

Please note: name prop can be also a numeric index in case, that the control is used directly as an array item field.

Prop's onChange event can proxy native form input event or implement it's minimal subset:

export type OnChangeEvent<T> = Event | {
    target: {
        name: string | number; // same as props.name
        value: T; // new mapped value
    }
};

Minimal example of custom control:

import { h } from 'preact';
import { asFormControl } from "@shelacek/formica";

const FormInput = asFormControl(function({ children, ...attrs }) {
    return (
        <label>
            {children}
            <input {...attrs} />
        </label>
    );
});

Edit formica-simple-custom-control

Add/remove form array items

Let's say we have the following model:

type FormModel = { contacts: { email: string; }[]; };

And appropriate form:

<Form value={form} onChange={this.handleChange} onSubmit={this.handleSubmit}>
    <FormArray name="contacts">
        <label>
            Email:
            <input name="email" type="email" required />
        </label>
        <FormAction>
            {({ remove }) => (<button type="button" onClick={remove}>Remove contact</button>)}
        </FormAction>
    </FormArray>
    <FormAction name="contacts" item={{ email: '' }}>
        {({ add }) => (<button type="button" onClick={add}>Add contact</button>)}
    </FormAction>
    <input type="submit" />
</Form>

Validation

Formica uses browser's native constraint validation, so you can use :invalid CSS pseudo-class to highlight invalid fields and event.target.checkValidity() method in onSubmit handler.

If you want display invalid fields after blur (Validate on blur or keypress?), you can wrap form field by <FormControl />, that apply CSS class .touched after the first blur.

Validation messages

You can use <FormControl /> also for displaying validation errors:

<FormControl name="age" id="name-field">
    {({ touched, validity }) => (
        <div>
            <label>Age</label>
            <input type="number" required min="15" />
            {touched && validity.valueMissing && <span>This field is required</span>}
            {touched && validity.rangeUnderflow && <span>Sorry, you must be at least 15 years old</span>}
        </div>
    )}
</FormControl>

API

Note: For full API of exported symbols, please see typings at https://bitbucket.org/shelacek/formica/src/master/src/types.d.ts.

Form component

Encapsulates <form> element and also wraps children into name-less <FormGroup />.

Properties

Prop Type Description
value any Form data (form model).
onChange { (value: any): void } Form data changes handler.
onSubmit { (event: Event): void } Form submit handler.

Any other passed properties will be added to <form> element.

FormGroup component

<FormGroup /> groups another form controls and connect them with the submodel.

Properties

Prop Type Description
disabled boolean Disable descendant controls.
name string Selected form model property to expose as submodel.

<FormGroup /> accepts standard vnodes or function as children. The function is called with an object contains props passed to <FormGroup />, like name or data-index, disabled and value. This can be useful if you need display index of array subform or some metadata carried with subform value.

FormArray component

<FormArray /> iterate descendant controls by received submodel's array.

Properties

Prop Type Description
keyedBy string Name of form model property to use as <FormArray />'s item key.
disabled boolean Disable descendant controls.
name string Selected form model property to iterate. Array is expected if not specified.

FormControl component

<FormControl /> wrap and connect native <input>, <select> or <textarea>. It can also associate label tag and expose some CSS classes and props for validation - namely disabled, touched and invalid.

Properties

Prop Type Description
id string Id mapped to control's id and label's for.
class¹ string Space-separated list of the classes.
disabled boolean Disable control.
name string Selected model property to connect with native input.

¹class and className are equivalent.

<FormControl /> accepts standard vnodes or function as children. The function is called with an object contains disabled, touched and validity. Argument validity is ValidityState object, others are booleans.

FormAction component

<FormAction /> can be used for simple form mutations and expose add and remove functions to children render prop. Any of these functions can be called directly from onClick events and the like.

Properties

Prop Type Description
name string Optional form submodel to attach.
item { (): void } | any Item value or function that return value to append.

<FormAction /> accepts function as children which can takes object with 2 functions:

  • add function add item to selected (or parent) submodel array, or object.
  • remove function remove self from selected (or parent) submodel array, or object. Prop name can specify key to remove.

This object also contains all props, that <FormAction /> received.

asFormControl HOC

Enhance any component with name, value and onChange props into form control.

It has only one argument: the wrapped component.

function asFormControl<P>(WrappedControl: ComponentFactory<P>): ComponentFactory<P>;

PRs are welcome!