-
Notifications
You must be signed in to change notification settings - Fork 2
Form
API / Form<TValidationError> class
Represents a form for which both fields and sections can be configured. Form sections are forms themselves making this a tree structure where fields represent leaves and sections are parent nodes.
Extends Validatable<TValidationError>.
class Form<TValidationError = string>
extends Validatable<TValidationError>
Source reference: src/forms/Form.ts:351
.
-
TValidationError - The concrete type for representing validation errors (strings, enums, numbers etc.).
Default value:
string
.
Any change within the tree can be propagated to the root as, for instance, the validity of a node is determined by its own valid state plus the valid states of its descendants. If a field is invalid, the entire form is invalid, if a section is invalid, the entire form is invalid.
More flags and properties can be added to forms through inheritance, the model provides the bare minimum that would be required for any
application. For instance, an isTouched
flag can be added to fields, however this does not need to be propagated to the root. On the
other hand, a hasChanged
feature can be added on fields, but this should propagate to the root in order to know if the form itself has
changes.
The design is mean to be extensible with the idea that each application would make its own customizations through inheritance (e.g.:
declaring a MyAppForm
and a MyAppFormModel
). While it can be a bit redundant if no extra features are added,
but it prepares the code in case any flag will be added in the future, the effort is small and covers for any scenario in the future.
The validation error defaults to string
, however this can be anything, enums, numbers, specific strings and so on. This is to enable
type-safety at the validation level and not force a particular approach. In most cases, a string
will do, but if there is need for
something else, the option is available as well.
The library makes a distinction between form definition and form configuration.
Form definition relates to the structure of the form, what fields and what type of value they handle along side any sections or collections of editable items the form can handle.
Large forms can be broken down into sections to group relevant fields together, in essence, it is still one form, however the object model allows for an easier navigation and understanding of the form structure.
Form configuration relates to form validation and field locking. In more complex scenarios, an edit form may have different validation rules depending on the underlying entity state.
For instnace, when placing orders in an online shop, the respective order goes through a number of states and some fields are editable dependent on that.
The configuration does not change the state, the form still looks more or less the same, but the way fields behave is different. Some fields become required or have different validation rules while other can become locked and are no longer editable.
Forms have a hierarchical structure comprising of fields and sections which are forms themselves. This allows for both simple and complex form definitions through the same model.
Any form contrains a collection of fields and a collection of sections, however propagation has an additional level of sections collections. Any form is a parent node while fields are leaves in the tree structure with propagation generally going bottom-up.
-
Form<TValidationError>
- root or parent node-
FormField<TValue, TValidationError>
- leaf nodes, any changes to a field are propagated to the parent node, aForm<TValidationError>
. -
FormCollection<TForm, TValidationError>
- a collection ofForm<TValidationError>
instances, any change to a form collection is propagated to the parent node, aForm<TValidationError>
.
-
Any changes to a Form<TValidationError>
is propagated to the FormCollection<TForm, TValidationError>
to which it was added. With this, extensions and validation can be added at any level,
from fields, to forms and form collections themselves.
For simple cases, defining a form is done by extending a Form<TValidationError>
and
adding fields using Form.withFields
.
In case of large forms it can be beneficial to group fields into sections,
which are just different Form<TValidationError>
composing a larger one. This can be
done using Form.withSections
.
For more complex cases where there are collections of forms where items can
be added and removed, and each item has its own set of editable fields, a
FormCollection<TForm, TValidationError>
must be used to allow for items to be added and
removed. To conrol the interface for mutating the collection consider
extending ReadOnlyFormCollection<TForm, TValidationError> instead.
To add your own form collections to a form use Form.withSectionsCollection
as this will perform the same operation as Form.withSections
only that
you have control over the underlying form collection. Any changes to the
collection are reflected on the form as well.
All fields and sections that are added with any of the mentioned methods are
available through the Form.fields
and Form.sections
properties.
Validation is one of the best examples for change propagation and is offered out of the box. Whenever a field becomes invalid, the entire form becomes invalid.
This applies to form sections as well, whenever a section collection is invalid, the form (parent node) becomes invalid, and finally, when a form becomes invalid, the form collection it was added to also becomes invalid.
With this, the propagation can be seen clearly as validity is determined completely by the status of each component of the entire form, from all levels. Any change in one of the nodes goes all the way up to the root node making it very easy to check if the entire form is valid or not, and later on checking which sections or fields are invalid.
Multiple validators can be added and upon any change that is notified by the target invokes them until the first validator returns an error message. E.g.: if a field is required and has 2nd validator for checking the length of the content, the 2nd validator will only be invoked when the 1st one passes, when the field has an actual value.
This allows for granular validation messages as well as reusing them across
IValidatable<TValidationError>
objects.
For more complex cases when the validity of one field is dependent on the value of another field, such as the start date/end date pair, then validation triggers can be configured so that when either field changes the validators are invoked. This is similar in a way to how dependencies work on a ReactJS hook.
All form components have a validation
property where configuraiton can be
made, check Form.validation
for more information.
-
constructor - Initializes a new instance of the
Form<TValidationError>
class.
-
readonly
fields - Gets the fields defined within the form instance. -
override
isInvalid - A flag indicating whether the object is invalid. -
override
isValid - A flag indicating whether the object is valid. -
readonly
sections - Gets the sections defined within the form instance. -
readonly
sectionsCollections - Gets the sections collections defined within the form instance. -
readonly
validation - Gets the validation configuration for the form. Fields have their own individual validation config as well. -
inherited
error - Gets or sets the error message when the object is invalid. -
inherited
propertiesChanged - An event that is raised when one or more properties may have changed.
- reset - Resets the form, contained fields and sections to their initial configuration.
-
protected
onFieldChanged - Invoked when a field's properies change, this is a plugin method through which notification propagation can be made with ease. -
protected
onSectionChanged - Invoked when a section's properies change, this is a plugin method through which notification propagation can be made with ease. -
protected
onSectionsCollectionChanged - Invoked when a section's properies change, this is a plugin method through which notification propagation can be made with ease. -
protected
onShouldTriggerValidation - Invoked when the current instance's properties change, this is a plugin method to help reduce validations when changes do not -
protected
withFields - Adds the provided fields to the form, returns an observable collection containing them. -
protected
withSections - Adds the provided sections to the form, returns an observable collection containing them. -
protected
withSectionsCollection - Adds the provided sections collection to the form, this is similar to theForm.withSections
method,
- ViewModel
To define a form with all related fields we need to extend from the base class and then declare the fields it contains and add them to the form.
We can do this in one expression, we assign each individual field to properties to easily access them later on.
export class MyForm extends Form {
public constructor() {
super();
this.withFields(
this.name = new FormField<string>({
name: 'name',
initialValue: '',
validators: [field => field.value === '' ? 'Required' : null]
}),
this.description = new FormField<string>({
name: 'description',
initialValue: ''
})
);
}
public readonly name: FormField<string>;
public readonly description: FormField<string>;
}
All forms are view models as well, we can create an instance and watch it
for changes using the useViewModel
hook.
function MyFormComponent(): JSX.Element {
const myForm = useViewModel(MyForm);
return (
<>
<input
value={myForm.name.value}
onChange={event => myForm.name.value = event.target.value} />
<input
value={myForm.description.value}
onChange={event => myForm.description.value = event.target.value} />
</>
)
}
Ideally, input binding is done through a specific component that handles a specific type of input (text, numbers dates etc.) with appropriate memoization.
For more information about input binding see FormField<TValue, TValidationError>
.
One of the big features of this library is extensibility, especially when it comes to forms.
It is very easy to extend a FormField<TValue, TValidationError>
, however that is not always
enough. Some extensions apply to the form as a whole, changes at the field
level propagate to the form that contains them.
One such example is adding a has changes feature which can be used as a navigation guard on edit pages. If there are no changes, then there is no need to prompt the user.
Whether a form has changed is determined at the field level, if there is at least one field that changed then we can say that the form indeed has changes.
First, we will define a custom FormField<TValue, TValidationError>
to define this flag.
class MyCustomFormField<TValue> extends FormField<TValue> {
public get value(): TValue {
return super.value;
}
public set value(value: TValue) {
super.value = value;
this.notifyPropertiesChanged('hasChanged');
}
public get hasChanged(): boolean {
return this.initialValue !== this.value;
}
}
We can further improve on the code above, even extend the field config to ask for an equality comparer in case we are dealing with complex types, but for this example it will do.
We need to define a custom form as well which will take into account the
newly defined flag on our custom field. Whenever we are notified that
hasChanged
may have changed (no pun intended), we will propagate this
on the form itself.
class MyCustomForm extends Form {
public readonly fields: IReadOnlyObservableCollection<MyCustomFormField<unknown>>;
protected withFields(
...fields: readonly MyCustomFormField<any>[]
): IObservableCollection<MyCustomFormField<any>> {
return super.withFields.apply(this, arguments);
}
public get hasChanges(): boolean {
return this.fields.some(field => field.hasChanged);
}
protected onFieldChanged(
field: MyCustomFormField<unknown>,
changedProperties: readonly (keyof MyCustomFormField<unknown>)[]
): void {
if (changedProperties.includes('hasChanged'))
this.notifyPropertiesChanged('hasChanges');
}
}
This example only covers field extension propagation, forms can contain
sub-forms, or sections, which they too can propagate changes. For more
information about this check Form.sections
.
Sections are part of complex forms and more than enough applications will not even need to cover for this, however keep in mind that the library provides easy extension throughout the form definition including complex scenarios like lists or tables of editable items.
Generally, when validating a field or perform validation on any type of
IValidatable<TValidationError>
object the general way to represent an error
is through a string
.
Validation errors can be represented in several ways such as number
s
or error codes, enum
s or even literal types
which are more or less predefined strings.
Some offer more freedom as to how errors are represented, while others are more restrictive on one hand, but on the other provide type checking and intellisense support.
By default, validation errors are represented using string
s, however this
can be changed through the TValidationError
generic parameter.
The snippet below illustrates using a literal type
for validation results.
type ValidationError = 'Required' | 'GreaterThanZero';
class AppForm extends Form<ValidationError> {
}
This is all, additionally an AppFormField
can be defined as well to bind
the generic parameter at the field level as well.
Defining a specific form is similar as in the first example, inherit from
the newly defined AppForm
class and define the fields.
class SpecificAppForm extends AppForm {
constructor() {
super();
this.withFields(
this.description = new FormField<string, ValidationError>({
name: 'description',
initialValue: ''
}),
this.count = new FormField<number, ValidationError>({
name: 'count',
initialValue: 0
})
)
}
public readonly description: FormField<string, ValidationError>;
public readonly count: FormField<number, ValidationError>;
}
Finally, we get to configuring the form and adding validators. We can do
this when we instantiate the fields as well. Each validator is bound to
the specific ValidationResult
type. Attempting to return something else
would result in a compilation error.
const form = new SpecificAppForm();
form.description
.validation
.add(field => !field.value ? 'Required' : null);
form.count
.validation
.add(field => field.value <= 0 ? 'GreaterThanZero' : null);
Overview
Motivation
Guides and Tutorials - Getting Started
Releases
CodeSandbox
API
Events
IEvent
IEventHandler
EventDispatcher
ViewModels
INotifyPropertiesChanged
ViewModel
Forms
Form
IFormFieldConfig
FormField
ReadOnlyFormCollection
FormCollection
IConfigurableFormCollection
FormSetupCallback
Validation
IValidator
ValidatorCallback
IObjectValidator
IValidatable
Validation / Triggers
WellKnownValidationTrigger
ValidationTrigger
Observable Collection
ReadOnlyObservableCollection
ObservableCollection
INotifyCollectionChanged
CollectionChangeOperation
INotifyCollectionReordered
CollectionReorderOperation
Observable Map
ReadOnlyObservableMap
ObservableMap
INotifyMapChanged
MapChangeOperation
Observable Set
ReadOnlyObservableSet
ObservableSet
INotifySetChanged
SetChangeOperation
Dependency Handling
IDependencyResolver
IDependencyContainer
DependencyContainer
useDependency
useViewModelDependency
useDependencyResolver
React Hooks
useViewModel
useViewModelMemo
useObservableCollection
useObservableMap
useObservableSet