ngx-FormKit
FormKit is an Angular Library built to make form handling in Angular a breeze. It allows you to create strongly typed forms in your code, without the hassle of working with templates. FormKit provides a FormComponent
component that you can use to display the form and respond to events. It provides methods to call FormKit logic from within your host component, by using the FormComponent
as a @ViewChild reference.
Since the FormKit library is built on top of Angular Reactive Forms, you can use all the built in Validators as well as custom validator functions. FormKit has dynamic and conditional callback functions per defined Field
, like:
- Conditional transformation of field values (based on the values of other fields in the form)
- Conditional visibility (based on the values of other fields in the form)
- Conditional required property (based on the values other fields in the form).
Installation
npm i @politie/formkit
This will install the FormKit library as a dependency in your project. You can now use the FormComponent
component in your project.
Building blocks
FormComponent
Add this component to your host component. This component renders a <formkit-form>
component and FormFieldComponent
components for every defined field in your FormKitFields
list.
FormFieldComponent
This component is rendered for each field in your form configuration. The component takes care of all logic after an update is done to the model (the form values). It will hook into the events$
stream of the FormComponent
to handle these updates and will call all logic in your field configuration, from state management (required, disabled, hidden, loading) to hooks (onInit).
FormKitForm<T>
This type is used to create objects for FormKit
. In a FormKitFields
list, you define the fields that the form should render.
Import the FormKitModule
Add the FormKitModule
in your AppModule
and call the forRoot()
method. You can add configuration as a parameter. When you have multiple modules, you can omit the forRoot()
method in modules other than AppModule
.
@NgModule({
imports: [
FormKitModule.forRoot()
]
})
export class AppModule { }
Setup
Add the following options to the component where you want to add a <formkit-form>
.
import { FormComponent, IFormGroup, FormKitFields } from '@politie/formkit';
import { ViewChild } from '@angular/core';
export class MyComponent {
form = new FormGroup({}) as IFormGroup<Type>;
fields: FormKitFields<Type> = {
...
};
@ViewChild('myFormKitForm', { static: true }) formComponent: FormComponent<Type>;
onSubmit() {
}
}
In the template, add the form by adding the formkit-form
selector. Give it the reference name you've assigned in the @ViewChild
, for this example: #myFormKitForm
.
The Form component has a @Output() EventEmitter
set for the ngSubmit
event which you can use to call your own method in the host component. In the [form]
property, you add a FormKitForm
configuration.
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<formkit-form #myFormKitForm [fields]="fields" [form]="formGroup">
<button
type="button"
(click)="onSubmit()"
[disabled]="form.invalid">
Save
</button>
</formkit-form>
</form>
Create a form
To create a form, you'll need to create a FormKitFields
list and a FormGroup
. The FormKitFields
type expects an object with your field definitions.
import { FormKitFields, FormComponent } from '@politie/formkit';
import { OnInit, ViewChild } from '@angular/core';
import { FormControl } from '@angular/forms';
type MyType = {
username: string;
}
export class MyComponent implements OnInit {
formGroup = new FormGroup({});
@ViewChild('form', { static: true }) form: FormComponent<Type>;
fields: FormKitFields<MyType> = {
username: {
type: FieldType.Text,
control: () => new FormControl()
}
}
}
The above example will render with one field of type TextField
, bind to the username
name.
Field
properties
Each field object should at least have a type
option set:
The object name should match a key in the provided Type definition. Following the example above, you can use username
as name for a field object.
Below is a rundown of each option per field object.
Settings for all fields
Parameter | Type | Description |
---|
type* | FieldType | The type of field. See Field Types for available field types. |
control* | () => FormControl(value?, [Validator]?) | A function that returns a FormControl . You can add a default value as the first parameter, Validator functions as optional second parameter. If you use multiple validator functions, add them as an array. |
component | any | If you'd like to render a custom component for this field, add the class here. See See 'Custom components' for example usage. |
description | string | Description to display above the field |
disabled | boolean / ((values: T) => boolean) | Should the field be disabled based on values of other fields. See 'Disable fields' for example usage. |
hooks | {} | Object with hook definitions for this field. See 'Hooks' for example usage. |
hidden | boolean / ((values: T) => boolean) | Should the field be hidden based on values of other fields. See 'Hide fields' for example usage. |
label | string | Label for this field |
messages | FieldMessage[] | Messages for this field. See 'Field messages' for example usage. |
required | boolean / ((values: T) => boolean) | Should the field be required based on values of other fields. See 'Required fields' for example usage. |
resetFormOnChange | false | If true, all fields in the entire form (except this field) will reset to their default values on change of this field. After the change, one round of afterUpdateValues is run, to trigger transform and conditional hooks. Use with caution, since multiple usages of this property in one form may lead to MAX_CALL_STACK_SIZE_EXCEEDED errors. |
title | string | Title to display above the field |
tooltip | string | Tooltip to display above the field |
transform | (values: T) => T[K] / undefined | Transform the value of this field based on the values of other fields. Takes a function that has the current values as a parameter and should return the type of value given by the generic type in the FormKitForm for this field name. See 'Transform field values' for example usage. |
width | 1/2 , 1/3 , 2/3 | If you want to limit the with of the field, add the width property to your field. |
Hooks
You can use hooks to hook into the lifecycle of a field
. You can use the following hooks:
To use a hook, define a hooks
property in the field object and add a function to the hook you want to use:
const fields: FormKitFields<Type> = {
myField: {
type: FieldType.Text,
hooks: {
onInit: (payload): void => {
}
}
}
}
The payload
object has the following properties:
Property | Type | description |
---|
control | `FormControl | FormArray |
errors | `ValidationErrors | null` |
values | Values<T> | The current values of the root form. This is an object with { fieldname: value } notation. |
The function shouldn't return anything (void
).
Hide fields
With conditional hiding, you can hide a field based on the value (or values) of other fields.
const fields: FormKitFields<Type> = {
myField: {
type: FieldType.Text,
hidden: values => values.configuration !== 'local'
}
}
In this example, the myField
field will be hidden when the configuration
field value
isn't equal to local
.
Required fields
With conditional required, you'll make the field required
based on the value
(or values
) of other fields.
const fields: FormKitFields<Type> = {
myField: {
type: FieldType.Text,
required: values => values.configuration !== 'local'
}
}
In this example, the myField
field will be required when the configuration
field value
isn't equal to local
.
If you want to make a field that's always required, use the validators
property with Angular's built in Validators.required
method.
Disable fields
You can disable fields based on the values of other fields.
const fields: FormKitFields<Type> = {
myField: {
type: FieldType.Text,
disabled: values => values.configuration !== 'local'
}
}
In this example, the myField
field will be disabled when the configuration
field value
isn't equal to local
.
Field messages
To add messages, create an array with FieldMessage
type objects in it. Each message has the following properties:
Property | Type | Description |
---|
show | boolean or (payload : FieldMessageFunctionPayload<T>) => boolean | Choose when to show your message. |
type | FieldMessageType | Choose a type of message (defaults to FieldMessageType.Information |
text | string or (payload : FieldMessageFunctionPayload<T>) => string | Add your text. The function should return a string. |
Always show a message
const fields: FormKitFields<Type> = {
myField: {
type: FieldType.Text,
messages: [
{
show: true,
text: 'Hello world'
}
]
}
}
Use in combination with a Validator
const fields: FormKitFields<Type> = {
myField: {
type: FieldType.Text,
messages: [
{
show: ({ control, errors }) => (control.value && errors.minlength),
text: ({errors}) => {
return `Input length: ${errors.minlength.actualLength} / ${errors.minlength.requiredLength} characters.`
}
}
]
}
}
FormComponent
@Input()
properties
Example:
<formkit-form [fields]="fields"></formkit-form>
Property | Type | Description | Default |
---|
form* | FormGroup | The FormGroup to use for this form. | |
fields* | FormFields<T> | The fields configuration for this form | |
readonly | boolean | Render the form in readonly mode | false |
autoCreate | boolean | Should the form call the create() method automatically? If you set this property to false , you must call the create() method yourself. This is useful if you need to wait for e.g. API calls to complete before rendering the form. | true |
*required
FormComponent
properties
Example:
this.myFormKitForm.value$.subscribe();
Property | Type | Description |
---|
events$ | Subject<FormEvent> | Subject for hooking into the FormComponent lifecycle |
value$ | FormValues<T> | Observable that emits each time the form is updated (and after update checks per field are completed). |
created | boolean | Will be true if the create() method is called (automatically or manually). |
FormComponent
methods
Example:
this.myFormKitForm.setValues(...);
Method | Payload | Description | Returns |
---|
create | | If you set the autoCreate property in <formkit-form> to false, you have to call this method yourself. | |
setValues | values | Update the form values with the provided values . The values should be a object with { name: value } properties | void |
transformValues | TransformValues<T> | Get a object with the current form values and transform them. See 'Transforming form values' for example usage. | T |
Transform form values
Use the transformValues
method to transform form values, for example to match a certain (backend) schema.
Considering the following values in the form:
{
username: 'user',
id: 1,
groups: [
'group-1',
'group-2'
],
isAdmin: true,
preferences: {
email: true
}
}
Let's say the API / backend implementation has a updateUser
endpoint, where you shouldn't send the isAdmin
property. We can use transformValues:
const values = myFormKitForm.transformValues({
omit: ['isAdmin']
});
values
now contains each property of the original values except isAdmin
. If we need to transform a key, we can do it like this:
const values = myFormKitForm.transformValues({
transform: values => ([{
from: 'preferences',
to: { 'emailPreference': values.preferences?.email }
}])
})
or
const values = myFormKitForm.transformValues({
transform: values => ([{
from: 'preferences',
to: 'updatePreferences'
}])
})
The transform property in the transformValues
payload takes a function that receives the current form values. It should return an array with transforms. The to
property can take either a string
or object
. If you pass a string
, the value set in the property from
is copied to the key you define in to
. If you set a object
, you define a new key / value pair. You can use the values
to look up a specific value in the form values.
Available options for the forRoot()
method
In the forRoot()
, you can add a configuration object with the following properties:
Property | Type | Description | Default |
---|
text | object | Object with properties to override strings used in the form templates | { loading: 'loading' } |
components | {...} | Object with [FieldType.<name>] properties to globally override components per Field type | default set, see Field Types |
*= required
Custom components
By default, the FormKit uses a set of prebuilt components to build your form. You can add your own custom components. You can choose between updating all fields that match a certain FieldType
to render your custom component by customising the default set. If you need to update a specific field without changing other fields with the same FieldType
, you can customise a specific field.
Customising the default set
Add the components
property to the forRoot()
payload object and add components that you want to override per FieldType
:
@NgModule({
imports: [
FormKitModule.forRoot({
components: {
[FieldType.Text]: CustomTextInputComponent
}
})
]
})
export class AppModule { }
This will update all fields that use the FieldType.Text
type as field type
property to use the CustomTextInputComponent
.
Customising a component for a specific field
const formConfig: FormKitFields<Type> = {
myField: {
type: FieldType.TextField,
control: () => new FormControl(),
component: CustomTextInputComponent.
}
}
This will make sure that field myField
uses the CustomTextInputComponent
.
Creating your custom component
You can extend the CustomFieldComponent
. This component has all @Input()
properties set (you can override the types).
You must add a HTML element that has the [formGroup]
attribute that will be filled with the formGroup
@Input()
property. The [formControlName]
property must be mapped to the name
@Input()
property in order to get your custom component to work.
import { CustomFieldComponent, ISingleFieldConfig, FormKitForm } from '@politie/formkit';
@Component({
selector: 'app-custom-text-input-component',
template: `<div [formGroup]="formGroup"><input type="text" [formControlName]="name"></div>`,
})
export class CustomTextInputComponent extends CustomFieldComponent implements OnInit {
@Input() control!: FormControl | FormArray | FormGroup;
@Input() events$!: Subject<FormEvent>;
@Input() field!: ISingleFieldConfig<any>;
@Input() name!: string;
@Input() formGroup!: FormGroup;
ngOnInit() {}
}
Make sure that you add your component to the entryComponents
in the module.
Field Types
The following field types are available (you can add your own):
- Checkbox Field
- Hidden Field
- Password Field
- Radio Field
- Radiobuttons Field
- Select
- Text Field
- Textarea Field