DEV Community

loading...
Cover image for A new approach to have Dynamic Forms in Angular
Mynd.co

A new approach to have Dynamic Forms in Angular

Mateo Tibaquirá
Senior Angular Developer, Library Author, Open Source Advocate, Dependency Maintainer by hobby.
Updated on ・6 min read

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()
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!

Discussion (6)

Collapse
toulix profile image
Toulix • Edited

Hello!
I would like to know how to use this library with Boostrap and not with Angular Material.
Is there a way to do that? Or, if there is a useful resource for this, could you please point
me in the right direction?

Collapse
matheo profile image
Mateo Tibaquirá Author

Hi @toulix
we will need to add a new subpackage @myndpm/dyn-forms/ui-bootstrap with components in the Bootstrap way.
Do you want to work with me adding that to the library?

Please fill a new Issue with the Controls you need and we start to create them together, while we document the experience for future UI packages ;)
github.com/myndpm/open-source/issu...

Collapse
toulix profile image
Toulix

Hi Mateo,

I am sorry for my late response.
I am more than happy to work with you for this new package even though I don't have strong experience with Angular. And I want you to know that I am just a junior Angular developer, but I am enthusiastic about learning more and contribute with you. :)

Thread Thread
matheo profile image
Mateo Tibaquirá Author

Enthisiasm is good enough to start!
I invite you to join us on Discord: discord.gg/XxEqkvzeXg
and we can keep talking about the advances in this new ui-package.
I can build it and you add details to the controls ;)

Collapse
nachovazquez profile image
Nacho Vazquez • Edited

Nice library and nice article! Keep up the good work!

Collapse
coffeetea profile image
Vitalii Baziuk

Really like this approach and wanna to try it out asap! Modes is a nice bonus for this stuff :)

Forem Open with the Forem app