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
yarnadd@corets/form
npminstall--save@corets/form
Seamless React integration is shipped in this package:
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.
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.
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!
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:
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.
import React, { useState } from"react"import { createForm } from"@corets/form"import { useForm } from"@corets/use-form"import { Memo } from"@corets/memo"constExample= () => { constform=useForm(() =>createForm({ field1:"foo", field2:"bar" })) const [someValue,setSomeValue] =useState("some state") return ( <form> <Memo deps={form.getDeps("field1")}> This section will only ever re-render when some of shared form properties change, like:`submitting`,`submitted` or `result`, or when one of the field related properties receives a change specific to this field, like:`errors`,`values`,`changedFields` or `dirtyFields`. </Memo> <Memo deps={form.getDeps(["field1","field2"])}> This section will change when one of the two fields receives a relevant change. </Memo> <Memo deps={form.getDeps(["field1","field2"], { errors:false })}> This block will NOT re-render if there is an error for one of the two fields.</Memo><Memo deps={[...form.getDeps(["field1","field2"]), someValue]}> Include an aditional, custom, value to the list of dependencies for a re-render. </Memo> </form> )}
You can read more about <Memo/> in the package docs:
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.
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:
Alter form behaviour by providing configuration overrides:
import { createForm } from"@corets/form"constform=createForm().configure({ // run schema sanitizers on form submit() and validate()? (default: true) sanitize:true,// validate on form submit()? (default: true) validate:true,// validate fields immediately after a change? (default: true) reactive:true,// delay invocation of listeners after a change (default: 10ms) debounce:10,})
Form.handler()
Specify a handler that is called whenever the form is submitted:
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.
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:
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:
You can choose not to persist any possible errors and just do a dry run:
consterrors=awaitform.validate({ persist:false })
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.
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.
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):
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 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).
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.getDeps("some.field", { // form config (default: true) config:true,// value of that specific field (default: true) value:true,// dirty fields (default: true) isDirty:true,// changed fields config (default: true) isChanged:true,// isSubmitting form status (default: true) isSubmitting:true,// isSubmitted form status (default: true) isSubmitted:true,// errors of that specific field (default: true) errors:true,// form result (default: true) result:true,})
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.
import { createForm } from"@corets/form"constform=createForm({ some: { nested: { field:"value" } } })constfields=form.getFields()fields.some.nested.field.get().setValue("new value")// same asform.setAt("some.nested.field","new value")// or evenform.setAt(fields.some.nested.field.key(),"new value")
Check out the package docs to learn more about the library used behind the scenes: