DEV Community

Cover image for A new approach to have Dynamic Forms in Angular
Mateo Tibaquirá for This is Angular

Posted on • Updated on • Originally published at dev.to

A new approach to have Dynamic Forms in Angular

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 valueChange 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()
Enter fullscreen mode Exit fullscreen mode

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 },
        }),
        ...
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

and voilá!
simple-form demo at Stackblitz

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[]
  )
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode
  1. the control identificator is the 'string' to reference the dynamic control from the Config Object
  2. the instance is the type of AbstractControl that it will create in the form hierarchy (FormGroup, FormArray or FormControl), and
  3. the component which should extend any of the Dynamic Control classes (DynFormGroup, DynFormArray, DynFormControl or DynFormContainer) 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>;
}
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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' },
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (3)

Collapse
 
monfernape profile image
Usman Khalil

Holy S###. I love this stuff. API is clean.

Collapse
 
matheo profile image
Mateo Tibaquirá

I'm glad you like it
we've flatten the config a bit to simplify the "modes" config, I will update the related code in the next few days :)

Collapse
 
monfernape profile image
Usman Khalil

I started working on a client project 3 months ago that required dynamically created form. I had to go through all the troubles and created one myself. Had I known earlier that such library already existed, I would've picked it without any hesitation. Mad respect.