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
Dynamic Form example https://github.com/jstoppa/dynamic-forms
References
CSS Grid template - https://developer.mozilla.org/en-US/docs/Web/CSS/grid-template-areas
NgRx docs - https://ngrx.io/docs
Dynamic forms with NgRx - Angular Denver
By Juan Stoppa
Dynamic forms with NgRx - Angular Denver
- 1,361