#ng2 Dynamic Forms
ng2 Dynamic Forms is a rapid form development library based on the official Angular 2 dynamic forms cookbook. It simplifies all the time-consuming work of implementing reactive Angular 2 forms by building upon a layer of maintainable object models.
It also provides a flexible system of dynamic UI components with out of the box support for Bootstrap, Foundation, Material 2 and more.
See what's possible by exploring the live demo!
##Table of Contents
- Getting Started
- Running the Example
- Basic Usage
- Form UI Modules and Components
- Model Bindings and Control References
- Form Groups
- Form Arrays
- Form Layouts
- Disabling and Enabling Form Controls
- Related Form Controls
- Form JSON Export and Import
- Validation Messaging
- Form Autocomplete
- Appendix
1. Install the core package:
npm install @ng2-dynamic-forms/core --save
2. Choose your UI library (e.g. Bootstrap) and install the appropriate package:
npm install @ng2-dynamic-forms/ui-bootstrap --save
3. When using SystemJS, update your configuration to import the corresponding UMD bundles:
System.config({
paths: {
"npm:": "node_modules/"
},
map: {
// ...all the rest (Angular 2, RxJS, etc.)
"@ng2-dynamic-forms/core": "npm:@ng2-dynamic-forms/core/bundles/core.umd.js",
"@ng2-dynamic-forms/ui-bootstrap": "npm:@ng2-dynamic-forms/ui-bootstrap/bundles/ui-bootstrap.umd.js",
}
});
1. Clone the Git repository:
git clone https://github.com/udos86/ng2-dynamic-forms.git
2. Install the npm dependencies:
npm install
3. Transpile the TypeScript files:
npm run tsc
4. Run the sample application:
npm start
1. Import the ng2 Dynamic Forms core NgModule
and a corresponding UI NgModule
:
import {NgModule} from "@angular/core";
import {BrowserModule} from "@angular/platform-browser";
import {ReactiveFormsModule} from "@angular/forms";
import {DynamicFormsCoreModule} from "@ng2-dynamic-forms/core";
import {DynamicFormsBootstrapUIModule} from "@ng2-dynamic-forms/ui-bootstrap";
// ..all remaining component and routing imports
@NgModule({
imports: [
DynamicFormsCoreModule.forRoot(),
DynamicFormsBootstrapUIModule,
BrowserModule,
ReactiveFormsModule
// ...all remaining imports
],
declarations: [AppComponent, MyDynamicFormComponent],
bootstrap: [AppComponent]
})
export class AppModule {}
2. Define your dynamic form model as Array<DynamicFormControlModel>
:
import {
DynamicFormControlModel
DynamicCheckboxModel,
DynamicInputModel,
DynamicRadioGroupModel
} from "@ng2-dynamic-forms/core";
export const MY_DYNAMIC_FORM_MODEL: Array<DynamicFormControlModel> = [
new DynamicInputModel({
id: "exampleInput",
label: "Example Input",
maxLength: 42,
placeholder: "example input",
}),
new DynamicRadioGroupModel<string>({
id: "exampleRadioGroup",
label: "Example Radio Group",
options: [
{
label: "Option 1",
value: "option-1",
},
{
label: "Option 2",
value: "option-2"
},
{
label: "Option 3",
value: "option-3"
}
],
value: "option-3"
}),
new DynamicCheckboxModel({
id: "exampleCheckbox",
label: "I do agree"
})
];
3. Create a FormGroup
via DynamicFormService
:
import {MY_DYNAMIC_FORM_MODEL} from "./my-dynamic-form.model";
import {DynamicFormControlModel, DynamicFormService} from "@ng2-dynamic-forms/core";
export class MyDynamicFormComponent implements OnInit {
myDynamicFormModel: Array<DynamicFormControlModel> = MY_DYNAMIC_FORM_MODEL;
myForm: FormGroup;
constructor(private dynamicFormService: DynamicFormService) {}
ngOnInit() {
this.myForm = this.dynamicFormService.createFormGroup(this.myDynamicFormModel);
}
}
4. Add the DynamicFormControlComponent
to your template
and bind it's FormGroup
and DynamicFormControlModel
:
<form [formGroup]="myForm">
<dynamic-form-bootstrap-control *ngFor="let controlModel of myDynamicFormModel"
[controlGroup]="myForm"
[model]="controlModel"></dynamic-form-bootstrap-control>
</form>
ng2 Dynamic Forms is built to provide solid yet unobtrusive support for a variety of common UI libraries:
- Basic (pure HTML5)
- Bootstrap
- Foundation
- Material 2
- PrimeNG
- Semantic UI (planned)
- Kendo UI (planned)
You can instantly plug in your favorite form controls by installing the appropriate package and it's peer dependencies:
npm install @ng2-dynamic-forms/ui-<library-name> --save
Right afterwards just import the corresponding UI NgModule
:
@NgModule({
imports: [
DynamicFormsCoreModule.forRoot(),
DynamicFormsBootstrapUIModule,
BrowserModule,
ReactiveFormsModule
],
// ...all remaining definitions
})
export class AppModule {}
Every UI NgModule
declares a DynamicFormControlComponent
that can easily be added to
your component template
:
<form [formGroup]="myForm">
<dynamic-form-bootstrap-control *ngFor="let controlModel of myDynamicFormModel"
[controlGroup]="myForm"
[model]="controlModel"></dynamic-form-bootstrap-control>
</form>
Also don't forget to refer the library stylesheet:
<link href="./node_modules/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet"/>
Due to Angular 2 Material still being in alpha full support for all major form controls cannot be provided at the moment. See the following compatibility table:
Checkbox | Checkbox Group | Input | Radio Group | Select | Textarea | Switch | |
---|---|---|---|---|---|---|---|
ui-basic | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
ui-bootstrap | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
ui-foundation | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
ui-material | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ | ✓ |
ui-primeng | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
One of the benefits of using ng2 Dynamic Forms is that interacting with your form programmatically becomes pretty easy.
Since a DynamicFormControlModel
is bound directly to a DOM
element via Angular 2 core mechanisms,
changing one of it's properties will immediately trigger a UI update.
Well, almost...
ng2 Dynamic Forms relies on the Angular 2 ReactiveFormsModule
. Therefore the value
property is not two-way-bound via [(ngModel)]
under the hood.
So what if we actually want to update the value of an arbitrary form control at runtime?
At first we need to get a reference to it's DynamicFormControlModel
representation. This can easily be achieved either by
a simple index-based array lookup or through the findById
method of DynamicFormService
:
this.myInputModel = this.myDynamicFormModel[2];
this.myInputModel = <DynamicInputModel> this.dynamicFormService.findById("myInput", this.myDynamicFormModel);
We now have access to the valueUpdates
Rx.Subject
to push new values via next()
:
this.myInputModel.valueUpdates.next("my new value");
At any time we also can safely read the most recent user input from the value
property:
let currentValue = this.myInputModel.value;
In order to improve clarity it's often considered good practice to group forms into several logical fieldset
sections.
Luckily ng2 Dynamic Forms supports nesting of form groups out of the box!
1. Just create a DynamicFormGroupModel
within your Array<DynamicFormControlModel>
and add it's models to the group
array:
export const MY_DYNAMIC_FORM_MODEL: Array<DynamicFormControlModel> = [
new DynamicFormGroupModel({
id: "basicFormGroup1",
legend: "Form Group 1",
group: [
new DynamicInputModel({
id: "basicGroupInput1-1",
label: "Example Group Input 1-1",
value: "Test 1-1"
}),
new DynamicInputModel({
id: "basicGroupInput1-2",
label: "Example Group Input 1-2",
value: "Test 1-2"
})
]
}),
new DynamicFormGroupModel({
id: "basicFormGroup2",
legend: "Form Group 2",
group: [
new DynamicInputModel({
id: "basicGroupInput2-1",
label: "Example Group Input 2-1",
value: "Test 2-1"
}),
new DynamicInputModel({
id: "basicGroupInput2-2",
label: "Example Group Input 2-2",
value: "Test 2-2"
})
]
})
];
2. Create a FormGroup
and apply a DynamicFormControlComponent
:
ngOnInit() {
this.myForm = this.dynamicFormService.createFormGroup(this.myDynamicFormModel);
}
<form [formGroup]="myForm">
<dynamic-form-bootstrap-control *ngFor="let controlModel of myDynamicFormModel"
[controlGroup]="myForm"
[model]="controlModel"></dynamic-form-bootstrap-control>
</form>
Sometimes forms need to allow the user to dynamically add multiple items of the same kind to it, e.g. addresses, products and so on. Particularly for this reason Angular 2 provides so called Form Arrays.
Although it's a bit more tricky, ng2 Dynamic Forms is capable of managing such nested form structures!
1. Add a DynamicFormArrayModel
to your form model:
export const MY_DYNAMIC_FORM_MODEL: Array<DynamicFormControlModel> = [
new DynamicFormArrayModel({
id: "myFormArrayModel"
})
];
2. Add the createGroup
property to the DynamicFormArrayModel
and assign a function to it which returns
the structure of a single form array item:
new DynamicFormArrayModel({
id: "myFormArray",
initialCount: 5,
createGroup: () => {
return [
new DynamicInputModel({
id: "formArrayInput",
label: "Form Array Input"
})
];
}
})
3. As usual, create a FormGroup
via DynamicFormService
and bind it to your component template:
this.myForm = this.dynamicFormService.createFormGroup(this.myDynamicFormModel);
<form [formGroup]="myForm">
<dynamic-form-basic-control *ngFor="let controlModel of myDynamicFormModel"
[controlGroup]="myForm"
[model]="controlModel"></dynamic-form-basic-control>
<button type="button" (click)="addItem()">Add item</button>
<button type="button" (click)="clear()">Remove all items</button>
</form>
4. You can now easily modify your form array with DynamicFormService
:
ngOnInit() {
this.myArrayControl = <FormArray> this.myForm.get("myFormArray");
this.myArrayModel = <DynamicFormArrayModel> this.dynamicFormService.findById("myFormArray", myDynamicFormModel);
}
addItem() {
this.dynamicFormService.addFormArrayGroup(this.myArrayControl, this.myArrayModel);
}
clear() {
this.dynamicFormService.clearFormArray(this.myArrayControl, this.myArrayModel);
}
Alright, works like a charm!
But wait a minute... what if we want to append, let's say, a remove <button>
for each array group?
No Problemo! Particularly for this case you can add a <template>
and declare some custom content that is rendered equally for all array groups:
<form [formGroup]="myForm">
<dynamic-form-basic-control *ngFor="let controlModel of myDynamicFormModel"
[controlGroup]="myForm"
[model]="controlModel">
<template let-context="context" let-index="index">
<button type="button" (click)="removeItem(context, index)">Remove Item</button>
<button type="button" (click)="insertItem(context, index + 1)">Add Item</button>
</template>
</dynamic-form-basic-control>
</form>
Whenever a <template>
is set for a DynamicFormArrayModel
, NgTemplateOutletContext
is internally bound to
the associated DynamicFormArrayGroup
.
That means you can access the group index and it's context DynamicFormArrayModel
by declaring some local template variable let-context="context" let-index="index"
.
This is extremely useful when you'd like to add a remove or insert function:
removeItem(context: DynamicFormArrayModel, index: number) {
this.dynamicFormService.removeFormArrayGroup(index, this.myArrayControl, context);
}
insertItem(context: DynamicFormArrayModel, index: number) {
this.dynamicFormService.insertFormArrayGroup(index, this.myArrayControl, context);
}
When using a ng2 Dynamic Forms UI package, e.g. ui-bootstrap
, all essential form classes of the underlying CSS library
(like form-group
or form-control
) are automatically put in place for you in the template of the corresponding DynamicFormControlComponent
.
Apart from that, ng2 Dynamic Forms does not make any further presumptions about optional CSS classes and leaves advanced layouting all up to you. That's solid yet unobtrusive.
So let's say we want to implement a beautifully aligned Bootstrap horizonal form...
At first we have to append the mandatory Bootstrap CSS class form-horizontal
to the <form>
element in our template:
<form class="form-horizontal" [formGroup]="myForm">
<dynamic-form-bootstrap-control *ngFor="let controlModel of myDynamicFormModel"
[controlGroup]="myForm"
[model]="controlModel"></dynamic-form-bootstrap-control>
</form>
Now we need to position the <label>
and the form-control
using the Bootstrap grid system. But since all the template logic for the form controls is capsuled in the scope of the DynamicFormBootstrapComponent
we cannot directly attach those necessary CSS classes to markup.
Don't worry!
By providing the cls
and it's nested grid
and element
configuration objects, ng2 Dynamic Forms allows us to optionally define additional CSS classes for every DynamicFormControlModel
, which are then intelligently appended within the DynamicFormControlComponent
template.
We can just pass it as a second constructor parameter of every DynamicFormControlModel
, i.e. separation of model and style information remains intact:
new DynamicInputModel(
{
// ... all model configuration properties
},
{
element: {
label: "control-label"
},
grid: {
control: "col-sm-9",
label: "col-sm-3"
}
}
)
Since RC.6 to date, Angular 2 does not allow any dynamic bindings of the disabled
attribute in reactive forms.
That means changing the corresponding disabled
property of some DynamicFormControlModel
at runtime won't have any effect.
But similar to updating values ng2 Dynamic Forms helps you out here
by providing a Rx.Subject
disabledUpdates
that can be used to programmatically switch the activation state of any form control:
this.myInputModel.disabledUpdates.next(true);
In many complex forms the activation state of a certain form control depends directly on the value
or status
of some other form control.
So let's pretend we need to have our textarea myTextArea
disabled as soon as the third option of our select menu mySelect
is chosen.
Manually implementing such a requirement would be time-consuming and only lead to undesired boilerplate code.
Using ng2 Dynamic Forms however, you can easily define relations between form controls by declaration:
new DynamicTextAreaModel(
{
id: "myTextArea",
label: "My Textarea",
relation: [
{
action: "DISABLE",
when: [
{
id: "mySelect",
value: "option-3"
}
]
}
]
}
The relation
property may seem a bit oversized at first sight, but that way it allows the flexible declaration of even multi-related form controls.
So what if the activation state of myTextArea
should actually depend on another control myRadioGroup
as well?
Just add a second entry to the when
array and define how both relations should logically be connected via connective
:
new DynamicTextAreaModel(
{
id: "myTextArea",
label: "My Textarea",
relation: [
{
action: "DISABLE",
connective: "AND",
when: [
{
id: "mySelect",
value: "option-3"
},
{
id: "myRadioGroup",
value: "option-4"
}
]
}
]
}
)
Sooner or later you likely want to persist your dynamic form model in order to restore it at some point in the future.
That's why all DynamicFormControlModel
s have been preprared to export properly to JSON:
storeForm() {
let json: string = JSON.stringify(this.myDynamicFormModel);
// ...store JSON in localStorage or transfer to server
}
Since all DynamicFormControlModel
s in ng2 Dynamic Forms rely on prototypical inheritance and thus aren't just plain JavaScript objects literals,
recreating a form from JSON unfortunately becomes more complex.
The good news is, that DynamicFormService
offers the function fromJSON()
to make things short and easy:
restoreForm() {
let json: string;
// ...load JSON from localStorage or server
let parsedJSON: Array<Object> = JSON.parse(json);
this.myDynamicFormModel = this.dynamicFormService.fromJSON(parsedJSON);
}
Delivering meaningful validation information to the user is an essential part of good form design. Yet HTML5 already comes up with some native functionality you very likely want to use Angular 2 mechanisms to gain much more control over validation logic and it's corresponding message output.
ng2 Dynamic Forms was intentionally developed without any kind of obtrusive validation message system since this would be off the original subject and result in a library too opinionated.
As with form layouting, implementing validation messages should be entirely up to you, following the recommended approach below:
1. Create your own custom validation message component and make it accept a FormControl
input:
import {Component, Input} from "@angular/core";
import {FormControl} from "@angular/forms";
@Component({
moduleId: module.id,
selector: "my-validation-message",
templateUrl: "./my-validation-message.html"
})
export class MyValidationMessage {
@Input() control: FormControl;
constructor () {}
}
2. Create a template file for your custom validation component and implement it's logic based on the control
property:
<span *ngIf="control && control.hasError('required') && control.touched">Field is required</span>
3. Define some Validators
for your DynamicFormControlModel
:
new DynamicInputModel({
id: "exampleInput",
label: "Example Input",
placeholder: "example input",
validators: [Validators.required]
})
4. Add your validation component aside from the DynamicFormControlComponent
in your form component template
and bind the internal FormControl
reference via local template variables:
<form [formGroup]="myForm">
<div *ngFor="let controlModel of myDynamicFormModel">
<dynamic-form-basic-control [controlGroup]="myForm"
[model]="controlModel" #componentRef></dynamic-form-basic-control>
<my-validation-message [control]="componentRef.control"></my-validation-message>
</div>
</form>
Adding automatic completion can be key factor to good user experience (especially on mobile devices) and should always be considered when designing forms. That's why ng2 Dynamic Forms keeps you covered here, as well!
Following HTML5 standard behavior, the autocomplete
attribute is always bound to on
for any DynamicFormTextInputControl
form element by default.
Nevertheless you can completely disable this feature by explicitly setting the corresponding model property to off
:
import {AUTOCOMPLETE_OFF} from "@ng2-dynamic-forms/core";
let model = new DynamicInputModel({
autoComplete: AUTOCOMPLETE_OFF
//...all remaining properties
});
Further on ng2 Dynamic Forms embraces the brand new HTML5
autofill detail tokens by providing
AUTOFILL_<TOKEN_NAME|FIELD_NAME>
string constants and DynamicFormAutoFillService
to help you putting together a valid expression:
Note: Jason Grigsby - "Autofill: What web devs should know, but don’t"
import {
DynamicFormAutoFillService,
AUTOFILL_TOKEN_BILLING,
AUTOFILL_FIELD_NAME,
AUTOCOMPLETE_ON
} from "@ng2-dynamic-forms/core";
export class MyAutoFillExample {
constructor(private autoFillService: DynamicFormAutoFillService) {
let expression = `${AUTOFILL_TOKEN_BILLING} ${AUTOFILL_FIELD_NAME}`;
let model = new DynamicInputModel({
autoComplete: autoFillService.validate(expression) ? expression : AUTOCOMPLETE_ON
//...all remaining properties
});
}
}
Besides you can make user input more comfortable, providing HTML5 datalists
by setting the list
property of DynamicInputControlModel
:
new DynamicInputModel({
id: "basicInput",
label: "Example Input",
list: ["One", "Two", "Three", "Four", "Five"]
})
- Logo design made by oscarana