Form

Build statically typed forms with ease. A refreshing alternative to existing form libraries.

Source code is hosted at GitHub

This is an opinionated form library for any JavaScript environment, with focus on React and React Native. The main difference between this project and other form libraries out there, is the ability to define a form outside of your presentation layer and just map it inside a component later on. A form built this way will encapsulate all the necessary logic to handle validation, submission, error handling and so on. This leads to clean separation of concerns, easier testing, composability and reusability.

One of the coolest features of this library is the fully static form fields access, anything that can be caught at compile time does not have to be tested explicitly.

  • Works everywhere, optimised for React and React Native

  • Strong typings from tail to toe, for joyful developer experience

  • Can be tested separately from the UI

  • Treat forms as a service and reduce complexity inside components

  • John Wick kind of crazy powerful validation options

  • Fully static field access, forget your string based keys

  • Many goodies like, status indicators, dirty fields, etc.

  • Tested by an army of killer coding ninja monkeys with a test coverage of 100%

  • Tons of other features and very easy customisation options

yarn add @corets/form

Seamless React integration is shipped in this package:

useForm

Ready to use form bindings for React, to get you started, can be found here:

useFormBinder

Comes with built in support for the schema package for delightful validation logic:

Schema

Static fields access is powered by this library:

Accessor

Quick start

Use this example as a high level overview of what this library has to offer and whether it suits your personal preference. Here we are going to create a very basic form that has some validation logic and dispatches an HTTP request to a remote endpoint, for processing.

First, let's define our types:

Next, define a method that is going to call the API:

Now we can build the form logic:

Now let's build the actual form:

In this example we are using the vanilla form binder shipped through the @corets/use-form-binder package, read more about form binders in the next section.

Static fields

One of the unique features of this library is it's static field access. Normally you bind / get / set your form data using dynamic, string based keys, which breaks your compile time safety. This significantly increases the maintenance cost of forms in the future, since you always have to be extremely careful when changing any of the form fields or the form object structure.

There is a better way to do this! Below is a side by side comparison of a form using dynamic and static field access. Keep in mind that you have full IDE / autocomplete support when accessing form fields through the static facade.

Have a look at the Form.getFields() documentation for more details.

You can read more about static accessors in the package docs:

Accessor

Component binders

This library was not designed for any specific framework. You should be able to use it everywhere where you can run JavaScript. This is why it does not ship any logic on how to connect to the UI out of the box. The @corets/use-form-binder package provides bindings for vanilla HTML elements, to get you started.

Given the nature of modern frontend development, projects use various component libraries and more often than not you have to write a custom input component. Obviously it is not possible to write one binder to rule them all. Therefore the most pragmatic approach is to create dedicated binders for various use cases. The good thing is that it is super easy to write your own binder!

Take a look at how binders are implemented in the @corets/use-form-binder package to get an idea.

Here is an example of a very basic, vanilla text field binder:

That's all there is about it! The best thing is, no matter what component you are going to use, you can always make it work! ™ 😎

Now let's use that binder in our form:

Another way to create a binder is using the Form.getFields() method powered by the @corets/accessor library. You no longer need to use dynamic, string based keys to map the form fields, you can use a statically typed facade instead:

Now let's use that binder in our form:

Read more about static fields here.

Optimisation

You can reduce the number of re-renders in big forms, by wrapping your UI blocks into the <Memo/> component from the @corets/memo package. Most likely you will never need this, but if you do, you are covered.

You can read more about <Memo/> in the package docs:

<Memo />

Testing

When writing unit tests, make sure that you set the debounce config property to 0. This disables throttling of the state changes and allows you to write tests in a synchronous manner.

createForm()

Create a new form instance:

Create a new form without the factory function:

Create a form instance with a specific type:

Specify form result type:

createFormFromSchema()

Create a new form instance based on a schema definition from the @corets/schema package. This is a convenient helper that allows you to avoid unnecessary boilerplate code when defining initial values for a form. Instead of creating an object, that adheres to the specific form type, with the initial values, you can define those initial values inside the schema definition itself.

Here is a side by side comparison of the two different approaches:

You should always use createFormFromSchema instead of of createForm where possible.

You can read more about schemas in the package docs:

Schema

Form.configure()

Alter form behaviour by providing configuration overrides:

Form.handler()

Specify a handler that is called whenever the form is submitted:

Result returned from a handler is also returned from the Form.submit() method and is available through the Form.getResult() method later on.

Form.validator()

Specify a validation function that is called before the form is submitted or after a form field has been changed, depends on your form config:

If you happen to return validation errors for the whole form, but the validator has been invoked after a field change, errors relating to fields that are not yet changed, will be omitted. Validator will be called in addition to the schema that you might have configured, possible errors will be merged.

Form.schema()

Configure a validation schema for your form:

Schemas are a delightful and powerful way to write your validation logic. They are feature loaded, very flexible and customisable. If you happen to provide a schema and a validator function, both will be invoked and the resulting errors will be merged.

Sanitizers that have been added to the schema will be called before any kind of validation happens and will alter the form values if necessary. This applies to the complete validation cycle and not to the reactive one (where validation is triggered only for the changed field).

You can read more about schemas in the package docs:

Schema

Form.validate()

Invoking the validate method will trigger the validation process where values get sanitized, schema and validator get called, resulting errors get merged, and so on:

Errors object will look something like this:

You can access errors anytime later:

You can choose to validate and sanitize only fields that have been changed:

You can choose to disable sanitization of values:

You can choose not to persist any possible errors and just do a dry run:

Form.submit()

Submitting a form will invoke the validator, schema and handler, depending on the configuration. Whatever is returned from the handler will be returned from the submit method itself and is also stored on the form object for later access.

You can access result anytime later:

Access validation errors:

Submit without validation:

Submit without sanitization:

Validate and sanitize only those form fields that have been changed:

Form.listen()

Subscribe to any changes on the form object:

Set a custom debounce interval for your listener:

Disable debounce for your listener:

Form.get()

Get all form values:

Form.getAt()

Get form values at a specific path:

Form.set()

Replace all form values with the new ones:

This method overrides all form values and does not track any dirty or changed fields. Use the Form.setAt() method instead, if you want to track dirty and changed fields.

Form.setAt()

Replace form values at a specific path:

Form.put()

Add some data to the form by doing a merge:

Unlike Form.set(), this method does not override all the form data, but will rather merge it instead. This method does not track any dirty or changed fields. Use Form.setAt() method, if you want to track dirty and changed fields.

Form.clear()

Reset form values, errors, status indicators, etc. This will reset form values to the initial values (the one that you've passed into the createForm() function):

You might want to call this after a successful form submission.

You can replace initial values while clearing the form:

Form.getErrors()

Get all form errors:

Form.getErrorsAt()

Get all errors at a specific path:

Form.setErrors()

Replace all errors:

Form.setErrorsAt()

Replace all errors at a specific path:

Form.addErrors()

Add some new errors:

Form.addErrorsAt()

Add some new errors at a specific path:

Form.hasErrors()

Check if there are any errors:

Form.hasErrorsAt()

Check if there are any errors at a specific path:

Form.clearErrors()

Clear all errors:

Form.clearErrorsAt()

Clear all errors at a specific path:

Form.isDirty()

Check if any of the form fields are dirty (have been written to):

Works only for fields that have been set using the Form.setAt() method. Methods like Form.set() or Form.put() do not track this kind of changes.

Form.isDirtyAt()

Check if a specific field is dirty:

Form.getDirty()

Get a list of all dirty fields:

Form.setDirtyAt()

Tag some fields as dirty:

Form.clearDirty()

Clear all dirty fields:

Form.clearDirtyAt()

Clear a specific dirty field:

Form.isChanged()

Check if any of the form fields have been changed:

Contrary to the Form.isDirty() method, for a field to be tracked as changed, its new value has to be different from its initial value provided during the form creation. This only works for fields that have been changed using the Form.setAt() method. Methods like Form.set() and Form.put() do not track this kind of changes.

Form.isChangedAt()

Check if a specific field has been changed:

Form.getChanged()

Get all fields that have been changed:

Form.setChangedAt()

Track some fields as changed:

Form.clearChanged()

Clear all changed fields:

Form.clearChangedAt()

Clear a specific changed field:

Form.getResult()

Get the result that has been returned from the handler function during the last form submission:

Form handler result is also returned directly from the Form.submit() method after invocation.

Form.setResult()

Manually replace form result:

Form.clearResult()

Clear form result:

Form.isSubmitting()

Check if form is currently being submitted:

Form enters this state whenever you invoke the Form.submit() method.

Form.setIsSubmitting()

Manually change form status:

Usually you don't have to do this manually.

Form.isSubmitted()

Check if form has been successfully submitted at least once:

Form enters this state after a successful form submission (one that did not cause any validation errors nor did throw an exception from the form handler).

Form.setIsSubmitted()

Manually change form status:

Usually you don't have to do this manually.

Form.getDeps()

Returns a list of dependencies for any specific field. This is useful whenever you need to calculate whether an input field should be re-rendered or not, for example inside useMemo(), useCallback() or even a useEffect().

You can customise what kind of form state should be considered a dependency:

Form.getFields()

Returns an accessor object that can be used to get a handle for an individual form field. Each form field is represented by an instance of FormField. The main purpose of this approach is to be able to statically access fields, without having to rely on dynamic string based keys.

Check out the package docs to learn more about the library used behind the scenes:

Accessor

FormField.getValue()

Get field value:

FormField.setValue()

Set field value:

FormField.getKey()

Get absolute field key:

FormField.getErrors()

Get field errors:

FormField.setErrors()

Override field errors:

FormField.hasErrors()

Check if field has any errors:

FormField.addErrors()

Add some field errors:

FormField.clearErrors()

Clear field errors:

FormField.isDirty()

Check if field is dirty:

FormField.setDirty()

Tag field as dirty:

FormField.clearDirty()

Clear dirty status of this field:

FormField.isChanged()

Check if field is changed:

FormField.setChanged

Tag field as changed:

FormField.clearChanged()

Clear changed status of this field:

FormField.getForm()

Get the Form instance attached to this field:

Last updated

Was this helpful?