New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

mstform

Package Overview
Dependencies
Maintainers
1
Versions
108
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

mstform

mobx-state-tree powered forms

  • 0.23.0
  • npm
  • Socket score

Version published
Maintainers
1
Created
Source

mstform README

mstform is a form library written for mobx-state-tree and React. It manages form state for you and lets you define validation rules. It understands about repeating sub-forms as well.

It doesn't put any requirements on your widgets. It works with any React controlled component.

Features

  • It knows about raw input (the value you type) and the converted input (the value you want). You may type a string but want a number, for instance. mstform converts this automatically.
  • It can drive any React controlled component. It normalizes input components so it can generate the right props for it -- whether it be a input type string, type checked or a custom component that works in terms of objects - mstform has you covered.
  • Integrates deeply with a mobx-state-tree (MST) model. You give it a model instance and it renders its contents. When you are ready to submit the form, you have a mobx-state-tree model again. You can modify the mobx-state-tree instance in code as well and the form is automatically updated.
  • Thanks to MST it's easy to convert form contents to JSON and back again.
  • It knows about types. If you use vscode for instance, your editor tells you if you do something wrong. This works even in plain Javascript if you enable ts-check.

Philosophy

Form libraries are tricky.

Web forms are an integral part of most web applications. This means that you need a lot of flexibility: you want to be able to mix form content with non-form content, use whichever React components you like for input (from plain HTML <input> to fancy UI component libraries), and style it the way you want.

Web forms are also everywhere. That's why we'd like to automate as much as possible and write as little form-specific code as possible.

But those two desires are in conflict with each other. It's tempting to start auto-generating forms. It makes writing forms really easy. Unfortunately many forms need special behavior, and it's difficult to capture this in an auto-generating form library.

This way a form library that automates too much can be in the way when you want to customize it to fit your application's requirements exactly. On the other hand if your form library that doesn't do enough means you end up writing a lot of custom code.

mstform balances simple usage with flexibility. It doesn't provide any React widgets, or in fact any React components at all. It doesn't auto-generate forms either. You write your own React components that render the form, and mstform automates the management of values and errors. It aims to make the form code that you do write look as straightforward as possible.

mstform is also built on a very powerful state management library: mobx-state-tree (MST). Both the library and the application programmer can use its features to make form construction more easy. As one example, MST makes it trivial to serialize form contents to JSON and restore it again.

A Simple Example

// @ts-check
import { observer } from "mobx-react";
import { types } from "mobx-state-tree";
import { Field, Form, FormState, converters } from "mstform";
import * as React from "react";
import { Component } from "react";

// we have a MST model with a string field foo
const M = types.model("M", {
    foo: types.string
});

// we expose this field in our form
const form = new Form(M, {
    foo: new Field(converters.string, {
        validators: [value => (value !== "correct" ? "Wrong" : false)]
    })
});

// we create an instance of the model
const o = M.create({ foo: "FOO" });

@observer
class Input extends Component {
    render() {
        const { type, field } = this.props;
        return <input type={type} {...field.inputProps} />;
    }
}

@observer
class InlineError extends Component {
    render() {
        const { children, field } = this.props;
        return (
            <div>
                {children}
                <span>{field.error}</span>
            </div>
        );
    }
}

@observer
export class MyForm extends Component {
    constructor(props) {
        super(props);
        // we create a form state for this model
        this.formState = form.state(o);
    }

    render() {
        // we get the foo field from the form
        const field = this.formState.field("foo");
        return (
            <InlineError field={field}>
                <Input type="text" field={field} />
            </InlineError>
        );
    }
}

What's going on in the example?

First we define a MST model M, which defines a string property foo. Next we define a form for that model. We pass the model definition as the first argument to the Form constructor.

A form needs to define fields for entries you want to expose in the form. Each Field needs a converter. The converter specifies how to turn a raw value as input by a user into a value you want to store in our MST model instance. Here we use converters.string, a simple string, which is also stored as a string. That's not a very fancy converter. A more complex converter would be converters.number, which converts the input string into a number, and does some validation to make sure that only numbers can be entered into the form.

The field definition also takes a number of options. The option we specify here is a validation function. You can provide additional validator functions in a list. If the validation function returns a string, that is the text of the validation error and the validation has failed -- the entered value is not stored in the underlying model instance. If the validation is successful, the function should return false, null or undefined; not returning any value is a successful validation.

We define a simple InlineError component that can display error text. It takes a field and displays its error (which may be empty). Your UI component library probably has a nicer component that helps to display errors -- Ant Design for instance has Form.Item.

We also define a simple Input component that is our input; it takes a field prop too and uses this to set the required input props (value and onChange).

We then define a MyForm component that actually displays the form. To display a form we need to initialize its form state. We create the form state with form.state and store it on this in the constructor.

The form state maintains form-related state, such as errors to display. It also can take state specific configuration (for instance how to save the form), but in this case we don't supply any.

The form state needs a MST instance; this is the MST instance that you modify with the form. When the user changes the form and it passes validation, the MST instance is directly updated.

Here the MST instance is o from the global scope. but typically the object to edit in the form comes in as a prop, and we can access it here.

Then in render we retrieve the field accessor for the foo field. This has everything we need: error to show the current error, and inputProps for the input component, so we pass the field to InlineError and Input.

I've enabled ts-check on top. If you're using vscode you can see it reflect the correct types -- it knows raw is a string, for instance. This can help to catch errors.

RepeatingForm

Many forms have a sub-form that repeats itself. The MST model could look like this:

const Animal = types.model("Animal", {
    name: types.string,
    size: types.string
});

const Zoo = types.model("Zoo", {
    animals: types.array(Animal)
});

Here we want a form that lets you add and remove animals:

import { RepeatingForm } from "mstform";

const form = new Form(Zoo, {
    animals: new RepeatingForm({
        name: new Field(converters.string),
        size: new Field(converters.string)
    })
});

We can now use it in our render method:

// this represents all the sub forms
const animalForms = state.repeatingForm("animals");

const entries = o.animals.map((animal, index) => {
    // get the sub-form we want
    const animalForm = animalForms.index(index);
    // and get the fields as usual
    const name = animalForm.field("name");
    const size = animalForm.field("size");
    return (
        <div>
            <InlineError field={name}>
                <Input type="text" field={name} />
            </InlineError>
            <InlineError field={size}>
                <Input type="text" field={size} />
            </InlineError>
        </div>
    );
});

return <div>{entries}</div>;

SubForm

Some MST models have a sub-object. When you render the form for such a model, you want to be able to include fields for this sub-object. The MST model could look like this:

const Animal = types.model("Animal", {
    name: types.string,
    size: types.string
});

const House = types.model("House", {
    description: types.string,
    pet: Animal
});

We want a form that includes information about the pet:

import { SubForm } from "mstform";

const form = new Form(Zoo, {
    description: new Field(converters.string),
    pet: new SubForm({
        name: new Field(converters.string),
        size: new Field(converters.string)
    })
});

We can now mix fields from the main form with those from the sub-form in our render method:

const description = state.field("description");
const name = state.subForm("pet").field("name");

Supported converters

A converter specifies how to convert a raw value as it is entered in the form to the converted value as it's stored in the MST instance, and back again. A converter also specifies the empty form of the raw value (such as an empty string), which is used by add forms. It also specifies which controlled props to generate by default for React. Conversion may fail, in which case the converter generates a validation error.

Converters from raw string value

The input raw value is a string. The converted value may be a string or some other object:

  • converters.string: value is a string.

  • converters.number: value is a number.

  • converters.integer: value is an integer.

  • converters.decimal({maxWholeDigits:x, decimalPlaces:y, allowNegative:z}): value is a string (not a number) that contains a decimal number with a maximum maxWholeDigits (default 10) before the period and a maximum of decimalPlaces (default 2) after the period. With allowNegative (boolean, default true) you can specify if negatives are allowed

Boolean

converters.boolean: raw value is a boolean, value is also a boolean. The default raw value in add forms is false.

Arrays

converters.stringArray: raw value is an array of strings. value is an observable array of strings. Note that this is for using arrays that are treated a value -- a list of which checkboxes are selected, for instance. When you want the user to be able to add items to the array, using RepeatingForm instead.

Models

converters.model(Model): does not do any conversion (model instance goes in, model instance comes out), but allows you to specify that a MST model comes in as a raw value and is the value. Typescript will be happy. This can be used to support an input component such as a drop-down selection that generate a reference to an object. This fits MST's types.reference.

Maybe

converters.maybe(converter): This works on converters that convert raw string values as well as converters that deal with MST nodes.

When you wrap it around any converter that takes a raw string value, the empty value (such as the empty string) is accepted and converted into null. This allows you to model empty values.

It can also be wrapped around a model converter, in which case it now accepts empty. This is handy when you have a types.maybe(types.reference()) in MST.

Object

converters.object: this accept any object as raw value and returns it, including null. Prefer converters.model if you can. Warning: the default raw value is null and using this with basic data types (string, boolean, number and such) won't make the type checker happy as they don't accept "null". Use more specific converters instead.

Controlled props

A controlled component is a React component that displays a value and defines an onChange handler that is called when the value is changed by the user. The component itself does not manage its value; this is done externally. mstform is a library that helps you control these components for you.

Controlled components receive subtly different props:

  • input type string has a value prop and an onChange with an event. It gets the updated value from event.target.value.

  • input type checkbox has a checked prop and an onChange that receives event.target.checked with the updated value.

  • There are also higher level widgets where value and onChange are symmetrical. A date picker widget for instance could have a JS Date as value and onChange directly returns a new Date instance.

mstform offers a controlled hook. It takes a function that given the field accessor returns the right props for control. This can be used to ensure that accessor.inputProps contains the right information for your particular controlled component.

There are three controlled implementations built in:

  • controlled.value - value and onChange processes event.target.value.

  • controlled.checked - checked and onChange processes event.target.checked.

  • controlled.object - value represents some object and onChange gets a new object as an argument. Symmetrical value and onChange.

By default the converter determines which is used. If you use the string converter or a derivative, controlled.value is used, and if you use the boolean converter by default the controlled.checked is used. For anything else the default is controlled.object.

You can always override controlled in the field configuration. For example:

import { observer } from "mobx-react";
import { types } from "mobx-state-tree";
import { Field, Form, FormState, converters, controlled } from "mstform";
import * as React from "react";
import { Component } from "react";

// we have a MST model with a string field foo
const M = types.model("M", {
    foo: types.string
});

// we expose this field in our form
const form = new Form(M, {
    foo: new Field(converters.string, {
        controlled: controlled.string
    })
});

For backward compatibility with earlier versions of mstform, mstform also supports fromEvent and getRaw in the field options. fromEvent is a flag that indicates we want to pull the raw value to validate and convert from the event.target.value. getRaw is a function that given the arguments to onChange turns them into the updated raw value.

Add Mode

So far we've described how to use mstform with edit forms -- we display what's in a MST instance and allow the user to edit it. There's another use case where you want to create a new MST instance however: an add form.

Consider this MST model:

const M = types.model("Foo", {
    nr: types.number
});

How do we create an add form for it? The add form needs an MST instance so that it can store the user-entered values. But this model requires you to create an instance with a value for nr.

Let's do that and use an arbitrary number for nr. We could have picked any number but 0 is probably the most clear, so we use that:

const node = M.create({ nr: 0 });

Let's look at the form definition:

const form = new Form(M, {
    nr: new Field(converters.number)
});

If we create a normal edit form for this node, we would see the raw value "0" in the input widget the form. That's not what we want to do in an add form; we want to display an empty input widget (raw value "", the empty string). We can accomplish this by setting the form in add mode when we create it:

const state = form.state(node, { addMode: true });

This way the form is shown correctly, with the empty values. How does it know what empty values to display in an add form? The converter actually specifies this -- converters.number for instance knows that the empty value is the empty string.

Add mode for repeating forms

Consider a repeating sub-form. Adding a new entry to a sub-form is much like having an add form. Each time you add a new entry, the new sub-form should be in add mode, even in edit forms. mstform automatically takes care of this if you use the .push and .insert methods on the repeating form accessor, or if you manipulate the underlying model directly. Existing records are shown in edit mode, unless the whole form is in add mode.

Saving

When we create the form state, we can pass it some options. One is a function that explains how to save the MST instance, for instance by sending JSON to a backend:

this.formState = form.state(o, {
    save: async node => {
        // we call the real save function that actually knows
        // how to save the form.
        return node.save();
    }
});

The save function should return null if the save succeeded and there are no server validation errors. It can also returns a special errors object in case saving failed -- we discuss this in a bit.

Then when you implement a form submit button, you should call state.save():

@observer
export class MyForm extends Component {
    constructor(props) {
        super(props);
        // we create a form state for this model
        this.formState = form.state(o, {
            save: async node => {
                return node.save();
            }
        });
    }

    handleSave = async () => {
        const success = await this.formState.save();
        if (success) {
            // success
        } else {
            // failure
        }
    };

    render() {
        // we get the foo field from the form
        const field = this.formState.field("foo");
        return (
            <div>
                ... render the form itself
                <button onClick={this.handleSave}>Save</button>
            </div>
        );
    }
}

state.save() does the following:

  • Makes sure the form is completely valid before it's submitted to the server, otherwise displays client-side validation errors.

  • Uses your supplied save function do to the actual saving.

  • Processes any additional validation errors returned by the server.

  • Returns true if saving succeeded, and false if not due to validation errors.

If you don't specify your own save you can still call state.save(), but it gives you a warning on the console that no actual saving could take place. This is handy during development when you haven't wired up your backend logic yet.

Save errors

When you save form content to some backend it may result in additional validation errors that are generated there. It is easier to detect some errors on the backend. It's a good idea to do server-side validation in any case, and it can be useful to reuse those errors in the frontend.

As we said above, if your save function doesn't return null it should return a custom object that contains server validation errors.

This can contain a description of the error, for instance:

{
    myError: "We cannot accept this data";
}

You can access these errors (so you can render them to the end user):

state.additionalError("myError");

Or you can get a list of all of them with state.additionalErrors().

You can also specify errors for particular fields, by naming the error key the same as the name of the field. So, if you have an MST model like this:

const M = types.model("M", {
    name: types.string()
});

And you want to display a specific backend-generated error for name, the error structure returned by save() needs to be:

{
    name: "Could not be matched in the database";
}

Every Field can have an error entry. This also works for repeating forms; if you have a repeating structure entries and there is an error in name of the second entry, the error structure should look like this:

{
    entries: [{}, { name: "We couldn't handle this" }];
}

Additionally you can also assign errors to a field that are managed outside of mstform:

this.formState = form.state(o, {
    getError: accessor => (accessor.path === "/name" ? "Is wrong" : undefined)
});

The error property of the field will contain the "Is wrong" error message if the field does not return undefined with the getError function. If a field contains both an internally generated error message and one that is generated via getError, the internally generated message trumps the one returned by the getError hook.

Other accessors in mstform - SubForm, RepeatingForm and Form - also use this error hook, allowing you to set errors on the complete form - or any accessor within it. Indexed entries within repeating forms can also be set with an error. If, for example, we want to raise an error when a RepeatingForm is empty, we can raise an error on the repeating form accessor like this

this.formState = form.state(o, {
    getError: accessor =>
        accessor instanceof RepeatingFormAccessor && accessor.length === 0
            ? "The repeating form must contain at least one form"
            : undefined
});

To help implement the error (and warning) hooks, you can use the resolveMessage function. This takes a messages structure and a path as argument and returns either a message string or undefined. The messages structure is an object that has the same structure as the form: if there's an error message for field "foo" then there's a message structure:

{
    foo: "Error message";
}

Sub forms are represented by sub objects in the messages structure, repeating forms by arrays with object entries. Besides this you can also hook up an error message to objects with the special key __error__:

{
    "__message__": "Object specific error message"
}

This way you can associate messages with FormState, SubFormAccessor and RepeatingFormIndexedAccessor when you use . You can also associate a message with RepeatingFormAccessor, i.e. the array itself, with __message__<name>:

{
    "__message__foo": "Array specific error message"
}

Controlling validation messages

By default, mstform displays inline validation errors as soon as you make a mistake. This may not be desirable. You can turn it off by passing another option:

this.formState = form.state(o, {
    validation: {
        beforeSave: "no"
    }
});

Now inline validation only occurs after you save the first time, not before.

It's also possible to turn off inline validation altogether:

this.formState = form.state(o, {
    validation: {
        beforeSave: "no",
        afterSave: "no"
    }
});

In this case the user only sees updated validation errors once they press the button that triggers state.save() and no errors are generated when the user is filling in the form.

required fields

You can control which fields are required by setting the required flag in the field definition:

const form = new Form(M, {
    nr: new Field(converters.number, { required: true })
});

This causes the field accessor's required property to be true, which you can use during form rendering. It also causes it to be a validation error if the field isn't filled in. You can control the required error message by setting requiredError:

const form = new Form(M, {
    nr: new Field(converters.number, {
        required: true,
        requiredError: "This is required!"
    })
});

Dynamic disabled, hidden, required and readOnly fields

mstform has hooks that let you calculate hidden, disabled, required and readOnly state based on the field accessor. Here is a small example that makes the foo field disabled. This uses the JSON Path functionality of mstform to determine whether a field is disabled, but any operation can be implemented here. You could for instance retrieve information about which fields are disabled dynamically from the backend before you display the form.

const state = form.state(o, {
    isDisabled: accessor => accessor.path === "/foo"
});

To implement hidden behavior, pass in an isHidden function. You can also determine whether a repeating form is disabled from add and remove using isRepeatingFormDisabled. It's up to you to use this information to render the add and remove buttons with the disabled status, however.

To implement readOnly behavior, pass in an isReadOnly function.

To implement required behavior, pass in an isRequired function. This does not only affect the required property on fieldAccessor, but also makes the field require the field just as if you used the required flag in the field definition. The required flag in the field definition always makes something required, no wonder what isRequired says.

isDisabled returning true makes the disabled prop true in accessor.inputProps. If isReadOnly is true, the readOnly flag is added to accessor.inputProps; otherwise it's absent, but it's up to you to ensure your React input widgets support a readOnly prop (HTML input does). There is no such behavior for hidden or required; use accessor.hidden and ``accessor.required` in your rendering code to determine whether a field wants to be hidden or required.

Warnings

mstform has a hook which allows you to include warning messages in its accessors. Warnings are similar to errors, but don't make the form invalid. The idea is that you can show warnings for certain fields in your form as a notification to the user.

const state = form.state(o, {
    getWarning: accessor =>
        accessor.raw < 0 ? "This value is negative" : undefined
});

To implement warnings, pass a getWarning function. It is up to you to decide how and when you which to show these warnings in the UI. To check if the form contains any warnings, you can use

state.isWarningFree; // true or false

Extra validation

Sometimes the information needed to validate the form cannot be known in advance at form definition time. Instead, the form can be displayed multiple times in the application, each time with different validation requirements. mstform has a hook that lets you define additional validation behavior on the form level.

const state = form.state(o, {
    extraValidation: (accessor, value) => {
        if (accessor.path === "/foo") {
            return value === "Wrong" ? "Wrong!" : false;
        }
    }
});

Note that you have to use the second value argument to get the value to use to validate, as accessor.value still has the old value.

Derived values

The value of some fields depends on the value of other fields; you can express this relationship in a MST view. In some forms you want to automatically calculate such a derived value but still allow the user to override it explicitly. Then if the input to the calculation changes, the value is updated again.

You express such derived values with mstform:

const M = types
    .model("M", {
        calculated: types.number,
        a: types.number,
        b: types.number
    })
    .views(self => ({
        sum() {
            return self.a + self.b;
        }
    }));

const form = new Form(M, {
    calculated: new Field(converters.number, {
        derived: node => node.sum()
    }),
    a: new Field(converters.number),
    b: new Field(converters.number)
});

calculated starts out with the value based on the sum of a and b. The user can modify calculated directly. When the user modifies a or b, the derived value changes again to the result of the derived function.

When you access a repeating form, the node passed into the derived function is the sub-node that the repeating form represents, so the derived value is determined within that context.

Note that derived calculations occur if you actually access the field to use it in a form; it doesn't work for fields that are never used.

Change hook

When you change one field it's sometimes useful to have some side effect, for instance to change the value of another field. You can do so with the change hook:

const M = types
    .model("M", {
        a: types.number,
        b: types.number
    })
    .actions(self => ({
        setB(value: number) {
            self.b = value;
        }
    }));

const form = new Form(M, {
    a: new Field(converters.number, {
        change: (node, value) => {
            node.setB(value);
        }
    }),
    b: new Field(converters.number)
});

We have defined an action that lets us modify b, which is represented by the field b. We implement a change hook to call that action whenever a is changed. This only happens if a passes validation -- changes to a that result in an error message don't result in an execution of the change hook.

Focus and blur hooks

You may want to react to field focus or blur events. You can do this with a custom onFocus or onBlur event handler on the input element, but in some cases you want to react generically to all focus/blur events in a form. You can pass a special hooks to the form state options for this:

const state = form.state(o, {
    focus: (ev, accessor) => {},
    blur: (ev, accessor) => {}
});

The hook receives the event and the focused field accessor. You can use the accessor to get the field name (accessor.name), value (accessor.value), etc. When you define the hook, inputProps on the field accessor contains an onFocus/onBlur handler, so if you use that with the field it is there automatically.

Tips

  • Don't name your form state this.state on a React component as this has a special meaning to React and can lead to odd bugs.

Keywords

FAQs

Package last updated on 11 Oct 2018

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc