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 <formkit-form>
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 <formkit-form>
component 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).
Demo / Live examples
https://politie.github.io/ngx-formkit
Installation
npm i @politie/ngx-formkit
This will install the FormKit library as a dependency in your project.
Import the FormKitModule
Add the FormKitModule
in your AppModule
and call the forRoot()
method. You can add a optional configuration object 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 { }
You can now use the <formkit-form>
component in your project.
Create a form
Add the following options to the component where you want to add a <formkit-form>
.
import { FormComponent, IFormGroup, FieldType, FormKitFormConfig } from '@politie/formkit';
import { ViewChild } from '@angular/core';
type UserForm = {
username: string;
}
export class MyComponent {
form = new FormGroup({}) as IFormGroup<UserForm>;
config: FormKitFormConfig<UserForm> = {
fields: {
username: {
type: FieldType.Text
}
}
};
@ViewChild('userForm', { static: true }) formComponent: FormComponent<UserForm>;
onSubmit() {
}
}
In your Component template, add the form by adding the formkit-form
selector. Give it the reference name you've assigned in the @ViewChild
, for this example: #userForm
. In the [form]
property, you add the empty FormGroup
, in the [config]
property, you add your FormKitFormConfig
definition.
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<formkit-form #myFormKitForm [form]="form" [config]="config"></formkit-form>
<button
type="button"
(click)="onSubmit()"
[disabled]="form.invalid">
Save
</button>
</form>
That's it! You can now dive in the extensive set of configuration options and properties to alter the behavior of the form and form fields. You can also check the Examples page to check out the different configurations.
Field
properties
Each field object should at least have a type
option set. The type property must be of type FieldType
which is a enum
type provided by the FormKitModule
.
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 to render. See Field Types for available field types. |
class | string[] | Add additional CSS classes which will be placed on the <formkit-form-field> in the HTML. |
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. |
data | {key: string: any} | If you need to have extra properties, add them here. |
header | { title?: string, description?: string, tooltip?: string } | Object with properties to add a title, description or tooltip above the field. |
footer | { description?: string } | Object with properties to add a description below the field (below the field messages). |
messages | false or FieldMessagesFunction<T> | A function that returns dynamic messages for this field. See Field messages for example usage. |
showMessagesIfControlIsUntouched | boolean | If you want to show messages, even if the control hasn't been touched, set this property to true (defaults to false). |
placeholder | string | Optional placeholder text for input fields (when the user sets focus on a field and the field value is empty, the placeholder is shown). |
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. You can't use this property inside a Array Field type. |
status | FieldStatusFunction<T> | A function that should return a object with boolean properties for hidden , disabled , required status. See Status for examples. |
validators | ValidatorFn[] | Optional array of Validator functions. |
value | any | Set a default value to the control. |
width | 1 - 12 | If you want to limit the with of the field, add the width property to your field. You can choose a value between 1 and 12 to span your field over the available grid columns. |
Settings for specific field types
CheckboxField (single)
Parameter | Type | Description |
---|
label* | string | Label for this checkbox |
description | string | If you want to render multiple checkboxes, use the options property. |
CheckboxField (multiple)
Parameter | Type | Description |
---|
value* | any[] | Set the default value for this field |
options* | Options[] | If you want to render multiple checkboxes, use the options property. |
PasswordField
Parameter | Type | Description |
---|
autofocus | boolean | Add focus to this field when rendering the form. |
RadioField & RadioButtonsField
Parameter | Type | Description |
---|
options | Options[] | List of options to render. |
RepeatableField
Parameter | Type | Description |
---|
delete | boolean | Render a delete button per field group? |
max | number | The maximum number of field groups that can be added to this repeatable field. Omit this property for unlimited additions. |
fields | {[key: string]: IField} | The field definitions per FieldGroup in this repeatable field. The keys should match child key names in the given field key for the repeatable field. |
SelectField
Parameter | Type | Description |
---|
autoSelectSingleOption | boolean | If only one option in the options list, should the select box select it? |
options | Options[] or Observable<Options[]> | List of options to render. |
emptyOptionsText | string | Text to show if there are no options available. |
TextField
Parameter | Type | Description |
---|
autofocus | boolean | Add focus to this field when rendering the form. |
TextareaField
Parameter | Type | Description |
---|
autofocus | boolean | Add focus to this field when rendering the form. |
ToggleField
Parameter | Type | Description |
---|
label | string | Label to render next to the toggle. |
Status
You can use the status
property on a field to alter the status of a field dynamically, based on form values or control
properties. The status
property asks for a function that returns a object with disabled
, hidden
and required
properties (all optional) that are of type boolean
. The function has a payload
parameter with the following properties:
type payload = {
control: AbstractControl | FormControl | FormArray | FormGroup,
errors: ValidationErrors,
values: FormValues<T>
}
Example
import { FormKitFormConfig, FieldType } from '@politie/ngx-formkit';
type UserForm = {
username: string;
toggle: boolean;
}
const config: FormKitFormConfig<UserForm> = {
fields: {
toggle: {
type: FieldType.Toggle,
value: false,
label: 'Toggle me to show/hide the username field'
},
username: {
type: FieldType.Text,
status: (payload) => ({
disabled: !payload.values.toggle,
hidden: payload.values.toggle
})
}
}
}
In this example, the username
field will be hidden
if the toggle
field value is truthy. You can use object destructuring as well, to only get the properties needed from the payload object.
import { FormKitFormConfig, FieldType } from '@politie/ngx-formkit';
type UserForm = {
username: string;
toggle: boolean;
}
const config: FormKitFormConfig<UserForm> = {
fields: {
toggle: {
type: FieldType.Toggle,
value: false,
label: 'Toggle me to show/hide the username field'
},
username: {
type: FieldType.Text,
status: ({ values }) => ({
disabled: !values.toggle,
required: values.toggle
})
}
}
}
If you want to make a field always required, it's better to use the validators
property with Angular built in Validators.required
method.
Field messages
To add messages to a field, add a function to the messages
property that returns an array with FieldMessage
type objects in it. The function has a payload parameter with the following properties:
type payload = {
control: AbstractControl | FormControl | FormArray | FormGroup,
errors: ValidationErrors,
values: FormValues<T>
}
Each message in the returned messages array should have at least a show
and text
property set. A type
property can be set to change the type of message: Information (default), Warning, Error.
Property | Type | Description |
---|
show* | boolean | Wether to show this message. |
type | FieldMessageType | Type of message (defaults to FieldMessageType.Information ) |
text* | string | The text for this message. |
Always show a message
import { FormKitFormConfig, FieldType } from '@politie/ngx-formkit';
type UserForm = {
username: string;
toggle: boolean;
}
const config: FormKitFormConfig<UserForm> = {
fields: {
username: {
type: FieldType.Text,
messages: (payload) => ([
{
show: true,
text: 'Hello world'
}
])
}
}
}
Use in combination with a Validator
import { FormKitFormConfig, FieldType } from '@politie/ngx-formkit';
import { Validators } from '@angular/forms';
type UserForm = {
username: string;
toggle: boolean;
}
const config: FormKitFormConfig<UserForm> = {
fields: {
username: {
type: FieldType.Text,
validators: [Validators.minLength(10)],
messages: ({control, errors, values}) => ([
{
show: Boolean(control.value && errors.minlength),
type: FieldType.Error,
text: `You've entered ${errors.minlength.actualLength} / ${errors.minlength.requiredLength} characters.`
}
])
}
}
}
Force message visibility
By default, messages will only show when the control has the touched
property set to true. This property is set when the user has interacted with a control and focus on the control is lost. If you want to show the message even if the control hasn't been touched yet, use the showMessagesIfControlIsUntouched
on the field configuration.
Disable all messages
If you want to disable all messages (including default messages) for a field, simply pass messages: false
to the field configuration:
import { FormKitFormConfig, FieldType } from '@politie/ngx-formkit';
import { Validators } from '@angular/forms';
type UserForm = {
username: string;
toggle: boolean;
}
const config: FormKitFormConfig<UserForm> = {
fields: {
username: {
type: FieldType.Text,
validators: [Validators.minLength(10)],
messages: false
}
}
}
FormComponent
@Input()
properties
Example:
<formkit-form [config]="config" [form]="form"></formkit-form>
Property | Type | Description | Default |
---|
form* | FormGroup | The FormGroup to use for this form. | |
config* | FormKitFormConfig<T> | The configuration for this form. The configuration object for a form has the following properties: fields , where you add all fields to render for this form and transforms . Read more about Transforming field values. | |
readonly | boolean | Render the form in readonly mode | false |
*required
Transforming field values
Sometimes, you need to transform the value of a input based on the value(s) of other input(s). You can do this on a per-form basis with FormKit. In your config
add the transforms
property to start. This property takes a function that receives the current values
of the form as a parameter. The values
will be typed as your provided Type in the config (in the example below: UserForm
). The function should return a object with the keys of the fields you want to transform.
import { FormKitFormConfig, FieldType } from '@politie/ngx-formkit';
import { Validators } from '@angular/forms';
type UserForm = {
username: string;
toggle: boolean;
}
const config: FormKitFormConfig<UserForm> = {
transforms: values => ({
...(values.toggle && { username: 'default' })
}),
fields: {
toggle: {
type: FieldType.Toggle,
value: false,
label: 'Toggle to change username to default'
},
username: {
type: FieldType.Text
}
}
}
The above example uses a shorthand to spread the object into the returned object. You can also use a different approach, as long as the key you want to transform returns either any value (including null
or ''
) or undefined
. If you return undefined
for a key, the transform will be ignored, and the current value of the field will be untouched. In the example above, if the toggle is set to false
, the username
field value isn't touched.
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). |
Use any of these properties in ngAfterViewInit
lifecycle hook of your host component to prevent undefined
properties.
FormComponent
methods
Example:
this.myFormKitForm.patch(...);
Method | Payload | Description | Returns |
---|
patch | values | Update the form values with the provided values . The values should be a object with { name: value } properties. This method is useful if you have fields set with the resetFormOnChange property, to bypass reset behaviour if you call patchValue on the FormGroup directly. | void |
triggerUpdateChecks | values (optional) | Trigger the update checks manually. Normally, this is done automatically on each change in the form. void | |
reset | | Resets the form values to the initial values. | void |
triggerUpdateChecks | values (optional) | Trigger the update checks manually. Normally, this is done automatically on each change in the form. void | |
Important: If you want to use properties or methods on the FormComponent
ViewChild
, you should trigger them after Angular AfterViewInit
checks are done, to prevent issues with input properties or methods that aren't defined yet. So, for example:
ngAfterViewInit() {
this.myFormKitForm.patch(...);
}
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' } |
messages | object | Object with default messages for common validator errors | {...} |
components | {...} | Object with [FieldType.<name>] properties to globally override components per Field type | default set, see Field Types |
*= required
Content slots
The <formkit-form>
offers several content slots for you to project content in.
Example:
<div formDescription>
Content will be placed in the `formDescription` content slot.
</div>
The available slots are:
formDescription
Placed above the form fields.formDescriptionReadOnly
Visible if the readonly
attribute is set to true
.footer
Placed below the form fields.submitClickedFormInvalid
Visible if the submit button is clicked and the formComponent onSubmitClick()
method is called and if the form is invalid
. You can use this area for a form wide error message.
Styling
You can bring your own CSS to style <formkit-form>
instances. Out of the box, the inputs use no styling at all, to be styled by Tailwind or something similar.
To get the most out of the form, you can use the following starting point for the CSS:
.formkit-form > * + * {
margin-top: 2rem
}
.formkit-fields {
--column-span: 12;
display: grid;
grid-template-columns: repeat(12, 1fr);
grid-gap: 1rem;
}
.formkit-field {
grid-column: auto / span var(--column-span);
}
Use Field Components without FormKitForm
By default, FormKit renders all fields automatically for you when you use <formkit-form>
. If you want to have more control, you can use components from the FormKit
library without using a <formkit-form>
as well. You can use a specific field (like <formkit-checkbox-field>
) without any logic like state and messages. If you want to use a field with messages and status, you need to render a <formkit-form-field>
. This field will automatically render the right component based on the field type.
In the example below, we add a <formkit-checkbox-field>
to a FormGroup and to our template without using the formkit-form
. This way, we can reuse the <formkit-*-field>
components without all form logic provided by a <formkit-form>
.
<div class="my-component">
<form [formGroup]="formGroup">
<input type="text" [formControl]="formGroup.get('input')">
<formkit-checkbox-field [control]="control" [field]="field"></formkit-checkbox-field>
</form>
</div>
In the class:
import { FieldType, ICheckboxField } from '@politie/formkit';
import { FormControl } from '@angular/forms';
type MyFormType = {
input: string;
checkbox: boolean;
}
export class MyComponent {
field: ICheckboxField<MyFormType, any, any> = {
type: FieldType.Checkbox,
label: 'This is a label'
}
control = new FormControl(false);
formGroup = new FormGroup({
input: new FormControl(''),
checkbox: this.control
});
}
Now we can use this configuration to render a formkit-checkbox-field
component inside our template.
Please note that you can't use transforms
without using a <formkit-form>
.
Module configuration
You can add a optional configuration object to the FormKitModule
when importing. This object has a few optional properties you can set:
Debounce time
Add a updateDebounceTime
property in the object to edit the amount of milliseconds it takes before FormKit calls the update logic per field. Defaults to 200
.
Default messages
You can edit default messages for common Validators
errors if you import the FormKit
Module. Update the messages
object in the forRoot()
payload object:
@NgModule({
imports: [
FormKitModule.forRoot({
messages: {
required: 'This field is required',
min: error => `Value should be at least ${error.min}.`
}
})
]
})
export class AppModule { }
You can also use the provided FORMKIT_DEFAULT_MESSAGES_NL
object for a Dutch translated set of default messages.
import { FORMKIT_DEFAULT_MESSAGES_NL } from '@politie/ngx-formkit';
@NgModule({
imports: [
FormKitModule.forRoot({
messages: FORMKIT_DEFAULT_MESSAGES_NL
})
]
})
export class AppModule { }
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
import { FormKitFormConfig, FieldType } from '@politie/ngx-formkit';
import { Validators } from '@angular/forms';
type UserForm = {
username: string;
toggle: boolean;
}
const config: FormKitFormConfig<UserForm> = {
fields: {
username: {
type: FieldType.TextField,
component: CustomTextInputComponent
}
}
}
This will make sure that field myField
uses the CustomTextInputComponent
.
Creating your custom component
You can extend the FieldBaseComponent
. 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 { FieldBaseComponent, ISingleField, 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 FieldBaseComponent implements OnInit {
@Input() control!: FormControl | FormArray | FormGroup;
@Input() field!: ISingleField<any>;
@Input() name!: string;
@Input() form!: FormGroup;
ngOnInit() {}
}
Make sure that you add your component to the entryComponents
in your AppModule.
Field Types
The following field types are available (you can add your own):
- Checkbox Field
- Date Field
- Email Field
- Hidden Field
- Number Field
- Password Field
- Radiobuttons Field
- Radio Field
- Select Field (with multiple option)
- Text Field
- Textarea Field
- Toggle Field
Validators
FormKit ships with a few extra Validators:
arrayMinChecked(min: number
)
Useful for checkbox lists (checks array length in the control
value).
arrayMaxChecked(max: number
)
Useful for checkbox lists (checks array length in the control
value).
These validators can be imported by importing the FormKitValidators
class and calling one of the static methods:
Example:
import { FieldType, FormKitValidators, FormKitFormConfig } from '@politie/ngx-formkit';
const config: FormKitFormConfig<Type> = {
fields: {
list: {
type: FieldType.Checkbox,
options: [
{
id: 1,
label: 'My label'
},
{
id: 2,
label: 'My second label'
}
]
},
validators: [FormKitValidators.arrayMinChecked(1)]
}
}
Note about using strictTemplates
Since all published libraries must use the View Engine compiler, something is off with the recognition of a generic type provided in a @ViewChild which turns the [config]
attribute into a error if you provide FormKitFormConfig<YourType>
.
Until this is fixed, we recommend you set the strictTemplates
under angulerCompilerOptions
in tsconfig.json
to false.
If no strictTemplates
property is set in your tsconfig.json
, you already should be good to go, since the property defaults to false
.
Building / Developing this library
Run npm run build
to build the entire project. The build artifacts will be stored in the dist/
directory. You can then cd
into the dist/formkit
to push this library to the NPM registry.
Running unit tests
Run npm run test:lib
to execute the unit tests with Jest.
Running ESLint
Run npm run eslint
. If you'd like to autofix the problems, use npm run eslint:fix
.
Running E2E tests
Run npm run e2e
to run headless. Run npm run e2e:watch
to run in browser and watch for file changes.