Dynamic forms with NgRx

Juan Stoppa

Software Engineer - Wealth Dynamix

@juanstoppa

My journey with forms

@juanstoppa

"Digital Transformation"

@juanstoppa

What does it mean for the user?

Web Forms

Paper forms

@juanstoppa

Agenda

  • Define the requirements
  • Propose a solution
  • Show a few demos
  • Conclusion

@juanstoppa

The requirements

  • Dynamic validation
  • Extensible with custom components
  • Support multiple layouts
  • Design system agnostic

@juanstoppa

Proposed solution

NgRx

Dynamic Reactive forms

Dynamic component loader

CSS grid layout

@juanstoppa

Template vs Model      

  <form nameForm="ngForm">
    <input type="text" 
      [(ngModel)]="model.firstName">
    </input>
    <input type="text" 
      [(ngModel)]="model.lastName">
    </input>
  </form>
  <form [formGroup]="model">
    <input type="text" 
      formControlName="firstName">
    </input>
    <input type="text" 
      formControlName="lastName">
    </input>
  </form>

Template-driven

Model-driven (reactive form)


  model = new FormGroup({
    firstName: new FormControl(''),
    lastName:  new FormControl('')
  });

  model = {
    firstName: '',
    lastName: ''
  };

@juanstoppa

Dynamic reactive forms

@juanstoppa

Dynamic reactive forms

let questions = QuestionBase<any>[] = [ 
    new TextboxQuestion(), 
    new DropdownQuestion()
];

let group: any = {};

questions.forEach(question => {
  group[question.key] = 
    question.required ? new FormControl(question.value || '', Validators.required)
                      : new FormControl(question.value || '');
});

let form = new FormGroup(group);

<form [formGroup]="form">
  <div *ngFor="let question of questions">
    <app-question [question]="question" [form]="form"></app-question>
  </div>
</form>

@juanstoppa

Dynamic reactive forms

let questions: QuestionBase < any > [] = [
    new TextboxQuestion({
        key: 'firstName',
        label: 'First name',
        value: 'Bombasto',
        required: true,
        order: 1
    }),
    new TextboxQuestion({
        key: 'emailAddress',
        label: 'Email',
        type: 'email',
        order: 2
    }),
    new DropdownQuestion({
        key: 'brave',
        label: 'Bravery Rating',
        options: [
            { key: 'solid', value: 'Solid' },
            { key: 'great', value: 'Great' },
            { key: 'good',  value: 'Good'  },
            { key: 'unproven', value: 'Unproven' }],
        order: 3
    })];

@juanstoppa

Dynamic reactive forms

export class QuestionBase<T> {
  id: string;
  value: T;
  key: string;
  label: string;
  order: number;
  controlType: string;
  rules: FormRules;
  position: GridPosition;
}

@juanstoppa

Demo

@juanstoppa

Dynamic Validation

@juanstoppa

Ngrx

Ngrx store

Questions

Template

2

Form

Groups

Components

3

4

Initial state is loaded

Form Groups recalculated with new state

Data

1

Dispatch [form] load action

@juanstoppa

Ngrx

Ngrx store

Data

Form

Components

Validation rules

let questions: QuestionBase < any > [] = [
    new TextboxQuestion({
       key: 'firstName',
        label: 'First name',
        value: 'Bombasto',
        required: true,
        order: 1
    }),
    new TextboxQuestion({
        key: 'emailAddress',
        label: 'Email',
        type: 'email',
        order: 2,
        rules: {
            required: {},
            hidden: {},
            readonly: {
                condition: "!firstName"
            }
        }
    }),
    new DropdownQuestion({
        key: 'brave',
        label: 'Bravery Rating',
        options: [
            { key: 'solid', value: 'Solid' },
            { key: 'great', value: 'Great' },
            { key: 'good',  value: 'Good'  },
            { key: 'unproven', value: 'Unproven' }],
        order: 3
    })];

@juanstoppa

Extensible components

@juanstoppa

Component loader

 

Container

Component

 

 

Presentational

Component

 

@Input

@Ouput

 

NgRx Store

 

 

Dispatch

Selector

Dynamic Component

Loader

@juanstoppa

export class DynamicFormQuestionComponent implements OnInit {
  @Input() question: QuestionBase<any>;
  @Input() form: FormGroup;
  @ViewChild('content', { read: ViewContainerRef, static: true}) 
    content: ViewContainerRef;
  
  constructor(
    private cfr: ComponentFactoryResolver,
    private vcr: ViewContainerRef
  ) {}

  ngOnInit(): void {
    const factories = Array.from(this.cfr['_factories'].keys());
    const type = <Type<Component>>factories.
        find((x: any) => x.componentName === this.question.controlType);
   
    const component = this.vcr.
        createComponent(this.cfr.resolveComponentFactory(type));
      
    (<any>component).instance.question = this.question;
    (<any>component).instance.form = this.form;

    this.content.insert(component.hostView);

Component loader

@juanstoppa

@NgModule({
  imports: [BrowserModule, ReactiveFormsModule],
  declarations: [AppComponent, 
    TextboxQuestionComponent, 
    TextareaQuestionComponent, 
    DropdownQuestionComponent
  ],
  entryComponents: [
    TextboxQuestionComponent, 
    TextareaQuestionComponent,
    DropdownQuestionComponent 
  ],
  bootstrap: [AppComponent]
})
export class AppModule {
  constructor() {
  }
}

Component loader

@juanstoppa

Support multiple layouts

@juanstoppa

CSS Grid

.grid-container {
  display: grid;
  grid-template-areas:
    'topleft    topright'
    'middle     middle'
    'bottomleft bottomright';
  grid-template-columns: 1fr 1fr;
  grid-template-rows: 1fr 1fr 1fr;
}

.topleft    { grid-area: topleft; }
.topright   { grid-area: topright; }
.middle     { grid-area: middle; }
.bottomleft { grid-area: bottomleft; }
<div class="grid-container">
  <div class="topleft">Q1</div>
  <div class="toright">Q2</div>  
  <div class="middle">Q3</div>  
  <div class="bottomleft">Q4</div>
  <div class="bottomright">Q5</div>
</div>

@juanstoppa

CSS Grid directive

<ng-template 
    gdConfig 
    [gdConfigOf]="template" 
    let-templateitem let-i="index">
    <div app-dynamic-container 
        [ngStyle]="templateitem.style" 
        [questions]="questions"
        [form]="form">
    </div>
</ng-template>

@juanstoppa

What about a real life example?

@juanstoppa

Level up the form

Component

Component

Section

Page

Page

Layout

Section

Form Metadata

Form Metadata

@juanstoppa

Form Group structure

Component

Component

Section

Page

Page

Layout

Section

FormControl

FormControl

FormGroup

FormGroup

FormGroup

FormGroup

FormGroup

Store Service

Component

Form Metadata

 RF FormGroup

Store Service dictates validation rules with new state

FormGroup applies validation rules from new state

@juanstoppa

App architecture

Questions library

Angular App

Question 1

Question 2

Question n

Form library

NgRx

Form Service

CSS Directive

Design System

@juanstoppa

FormQL

  • @formql/core
  • @formql/editor
  • MIT License

github.com/formql/formql

@juanstoppa

Dynamic forms

Advantages

  • Keeps your code DRY
  • Enforces standard patterns across large teams
  • Keeps your application modular

Disadvantages

  • Deal with JSON config
  • Initial rendering might be slow on complex dynamic validation

@juanstoppa

Conclusion

When to use dynamic forms

When NOT to use dynamic forms

  • Simple forms
  • Forms that will hardly ever change
  • Complex/large forms that change very often
  • Multiple forms with similar functionality
  • Forms are changed by large teams

@juanstoppa

Thank you!

Juan Stoppa

@juanstoppa

References

NgRx docs - https://ngrx.io/docs

Dynamic forms with NgRx - Angular Denver

By Juan Stoppa

Dynamic forms with NgRx - Angular Denver

  • 1,361