A new approach to have Dynamic Forms in Angular
Mateo Tibaquirá
Posted on April 5, 2021
TL;DR
Go to Stackblitz and witness the power of@myndpm/dyn-forms
, check its synthetic source code and join the GitHub Discussions to design the upcoming features based on our experiences with Angular Forms.
As in most companies, at Mynd we build forms, filters, tables and display views for different purposes. We handle a ton of entities and we have custom components in our Design System to satisfy our needs. In this complex scenario, avoid boilerplate is a must, and to speed up the development process and facilitate the implementation and maintenance of these views, we built some base libraries to abstract the requirements into configuration objects that enable us to easily modify a form, a filter, a table, without touching a view template (most of the times).
So the question is: can we implement a standard, flexible enough layer to do this job and be shared with the Angular Community?
A bit of History
This challenge has been addressed by many developers and companies in many ways, we even have an official documentation guide on this topic; some approaches ends up with a template processing different types of fields with a ngSwitch
, others vary on the entrypoint component depending on the desired UI framework, or their config objects are not standardized and uses different field names for the same task on different controls. They are not completely generic, typed and/or extensible.
The ideal scenario is to have a strictly typed and serializable configuration object, so we are able store it in the state or the database without problems, as well as the ability to share some recipes with the community for common use-cases without complex functions involved, just a JSON object; there are a lot of good ideas out there, and we're in the process of discussing the best possible solutions for each topic.
Technically speaking, the challenge is to translate a Config Object (JSON
) into a functional Form (FormGroup
) being able to build any required nested structure, composing Control (inputs, selects, etc) into Containers to group them and customize the layout (cards, panels, etc).
What's New?
@myndpm/dyn-forms
is not just a "dynamic" forms library providing you a finite set of controls, or limiting your creativity and possibilities in any way. This library aims to be a quite generic and lightweight layer on the top of Angular's Form Framework, allowing us to build, extend and maintain our forms from their metadata, giving us more time to focus our attention on the business-logic requirements, custom validations, etc.
Moreover, we keep the control of our model and the Angular Form, manipulating the supported methods of FormGroup
, FormArray
and FormControl
, giving the responsibility of building the form hierarchy and its presentation to the library, but patching and listening any valueChanges
as we are used to.
Creating a DynForm
All we need is to import DynFormsModule
to our NgModule
and also provide the DynControls
that we need in our form. As a demostrative implementation, we mocked DynFormsMaterialModule
at @myndpm/dyn-forms/ui-material
to enable you right now to see how it works with some basic components:
import {
DynFormsMaterialModule
} from '@myndpm/dyn-forms/ui-material';
@NgModule({
imports: [
DynFormsMaterialModule.forFeature()
This package also provides a typed createMatConfig
Factory Method that (hopefully) will facilitate the development experience while creating configuration objects, by supporting type-checks (with overloads for the different controls):
import { createMatConfig } from '@myndpm/dyn-forms/ui-material';
@Component(...) {
form = new FormGroup({});
mode = 'edit';
config = {
controls: [
createMatConfig('CARD', {
name: 'billing',
params: { title: 'Billing Address' },
controls: [
createMatConfig('INPUT', {
name: 'firstName',
validators: ['required'],
params: { label: 'First Name' },
}),
createMatConfig('INPUT', {
name: 'lastName',
validators: ['required'],
params: { label: 'Last Name' },
}),
createMatConfig('DIVIDER', {
params: { invisible: true },
}),
...
now you're ready to invoke the Dynamic Form in your template
<form [formGroup]="form">
<dyn-form
[config]="config"
[form]="form"
[mode]="mode"
></dyn-form>
<button type="button" (click)="mode = 'display'">
Switch to Display Mode
</button>
</div>
Where the magic happens
The main feature is the ability to plug-in new Dynamic Form Controls, provide customized ones for some particular requirements, or integrate third-party components into our forms, with ease!
For this matter, Angular's InjectionTokens
are the way to apply the Dependency Inversion Principle, so we do not rely on the controls provided by a single library anymore, but any NgModule
(like DynFormsMaterialModule
) can provide new controls via the DYN_CONTROL_TOKEN
by registering the component to be loaded dynamically (DynControl
) with an "ID" (INPUT
, RADIO
, SELECT
, etc).
From there the Dynamic Form Registry can let the Factory
know what component it should load for a given "ID"
@Injectable()
export class DynFormRegistry {
constructor(
@Inject(DYN_CONTROLS_TOKEN) controls: ControlProvider[]
)
it's super hard to name these kind of "id" and "type" fields, so trying to keep the context clear, the ControlProvider
interface consists of:
export interface InjectedControl {
control: DynControlType;
instance: DynInstanceType;
component: Type<AbstractDynControl>;
}
- the
control
identificator is the 'string' to reference the dynamic control from the Config Object - the
instance
is the type ofAbstractControl
that it will create in the form hierarchy (FormGroup
,FormArray
orFormControl
), and - the
component
which should extend any of the Dynamic Control classes (DynFormGroup
,DynFormArray
,DynFormControl
orDynFormContainer
) implementing the simple contract explained here.
Configuration Object Typing
You can define your Form with an array of controls
which can have some subcontrols
; with this nested structure you can build any hierarchy to satisfy your needs (like in the example). This configuration unit is specified by the DynBaseConfig
interface which follows a simple Tree structure:
export interface DynBaseConfig<TMode, TParams> {
name?: string;
controls?: DynBaseConfig<TMode>[];
modes?: DynControlModes<TMode>;
}
The form also supports different "modes". Modes are partial overrides that we can apply to the main Control Configuration depending on a particular situation. In the simple-form demo we show an example of this: a display
mode where we define a readonly: true
parameter to be passed to all the dynamic controls, and they react changing their layout or styles. These "modes" are just a custom string
, so the configuration is open to any kind of mode
that you'd like to define.
In the DynFormConfig
you can specify the global override for each mode:
const config: DynFormConfig<'edit'|'display'> = {
modes: {
display: {
params: { readonly: true }
and you can also override the configuration of a single control for a given a mode, like this RADIO
button being changed to an INPUT
control when we switch the form to display
mode:
createMatConfig('RADIO', {
name: 'account',
params: { label: 'Create Account', color: 'primary' },
modes: {
display: {
control: 'INPUT',
params: { color: 'accent' },
In this case, the control
will be overriden but the params
will be merged and we will have the original label in the display
mode.
Feedback WANTED
With this brief introduction to this powerful library, we hope that you join its design/development efforts by sharing your experience/ideas/point of view in the GitHub Discussions opened for the upcoming features, creating Pull Request extending or adding new Material/TaigaUI/any controls, or reporting Issues that you find.
There are some challenges to be addressed, like a standard way to handle the Validations and show the respective Error message; handle the visibility of a control depending on some conditions; these topics have opened discussions to collect ideas and figure out a solution.
We might write more articles explaining the internals to analyze and improve the chosen architecture.
Without further ado, enjoy it!
// PS. We are hiring!
Posted on April 5, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.