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?

@juanstoppa

Web Forms

Paper forms

@juanstoppa

Agenda

  • Define the requirements
  • Define a solution
  • Show a few demos
  • Q&A

@juanstoppa

The requirements

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

@juanstoppa

Proposed solution

NgRx

Dynamic Reactive forms

Dynamic component loader

CSS grid

@juanstoppa

Dynamic reactive forms

@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

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

Component loader

 

Container

Component

 

 

Representational

Component

 

@Input

@Ouput

const componentFactory = this.fr.resolveComponentFactory(component);

const ref = viewContainerRef.createComponent(componentFactory);

// @Input
(<QuestionComponent>ref.instance).input = this.component;

// @Output
this.formControl.controls[this.component.id].valuesChanges.subscribe(...

@juanstoppa

Component loader

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);

@juanstoppa

CSS Grid

.grid-container {
  display: grid;
  grid-template-areas:
    'header header'
    'left   right'
    'bottom bottom';
}

.header { grid-area: header; }
.left   { grid-area: left; }
.right  { grid-area: right; }
.across { grid-area: bottom; }
<div class="grid-container">
  <div class="header">Header</div>
  <div class="left">Field 1</div>  
  <div class="right">Field 2</div>
  <div class="bottom">Field 3</div>
</div>

@juanstoppa

What about a real life example?

@juanstoppa

Level up the form

Component

Component

Section

Page

Page

Layout

Section

Form Metadata

Form Metadata

@juanstoppa

Level up the form

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

Conclusion

Ngrx as one source of truth

Reactive forms FTW

Dynamic component loader for extending

CSS grid for custom layouts

Thank you!

Juan Stoppa

@juanstoppa

References

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

Dynamic forms with NgRx - Angular London Meetup

By Juan Stoppa

Dynamic forms with NgRx - Angular London Meetup

  • 1,080