Dynamic reactive forms with GraphQL

Juan Stoppa

Software Engineer - Wealth Dynamix

@juanstoppa

Building 

Forms

@juanstoppa

The challenge

  • Capturing large amount of information
  • Complex Validation rules
  • Vague requirements
  • Creativity vs Best Practice

@juanstoppa

The solution

Ngrx

FormQL

Reactive forms

Dynamic component loader

GraphQL

@juanstoppa

@juanstoppa

Component

Component

Section

Page

Page

Layout

Section

The form representation

Form Metadata

Form Metadata

The FormGroup

@juanstoppa

Component

Component

Section

Page

Page

Layout

Section

FormControl

FormControl

FormGroup

FormGroup

FormGroup

FormGroup

FormGroup

Ngrx

Component

Form Metadata

 RF FormGroup

Store dictates validation rules with new state

FormGroup applies validation rules from new state

@juanstoppa

{
  formStore: {
    form: {
      layoutComponentName: 'FormLayoutDefaultComponent',
      pages: [
        {
          sections: [
            {
              components: [
                {
                  schema: 'contact.firstName',
                  key: 'firstName',
                  label: 'First name',
                  componentName: 'TextboxReactiveComponent',
                  type: 'textbox',
                  order: 1,
                  position: {
                    column: '4.1',
                    order: 0
                  },
                  componentId: 'f3ba55e9-20b3-db67-2099-22a9108bcd47',
                  value: 'Joe'
                },
                {
                  schema: 'contact.firstName',
                  key: 'lastName',
                  label: 'Last Name',
                  componentName: 'TextboxReactiveComponent',
                  type: 'textbox',
                  order: 1,
                  position: {
                    column: '4.2',
                    order: 0
                  },
                  componentId: '0af1e87f-09fe-e6e0-80ca-f1d512b889ec',
                  value: 'Joe'
                }
              ],
              structure: '4-4-4',
              position: {
                column: '12.1',
                order: 0
              },
              sectionId: '5d3fcbe3-a029-ca5e-4791-9666155fff0f',
              headerStyle: {
                'font-size': '1.2rem',
                'padding-bottom': '.5rem',
                'border-bottom': '2px solid #000',
                'margin-bottom': '10px'
              },
              sectionName: 'Header 1'
            },
            {
              components: [
                {
                  schema: 'contact.lastName',
                  key: 'lastName',
                  label: 'Last Name',
                  componentName: 'TextboxReactiveComponent',
                  type: 'textbox',
                  order: 1,
                  position: {
                    column: '12',
                    order: 0
                  },
                  componentId: 'b31ce3ce-a329-fc4b-15e9-208eeece91be',
                  value: 'Black'
                }
              ],
              structure: '12',
              position: {
                column: '6.1',
                order: 0
              },
              sectionId: '098ba7f6-94e3-00ee-23c3-ff803cb68400',
              headerStyle: {
                'font-size': '1.2rem',
                'padding-bottom': '.5rem',
                'border-bottom': '2px solid #000',
                'margin-bottom': '10px'
              },
              sectionName: 'Header 2'
            },
            {
              components: [
                {
                  schema: 'contact.mobile',
                  key: 'firstName',
                  label: 'Mobile',
                  componentName: 'TextboxReactiveComponent',
                  type: 'textbox',
                  order: 1,
                  position: {
                    column: '3.1',
                    order: 0
                  },
                  componentId: 'bb22abb9-0fd0-fa7b-ff0a-50e2c2031970',
                  value: '076666666666',
                  conditions: {}
                },
                {
                  schema: 'contact.email',
                  key: 'firstName',
                  label: 'Email2',
                  componentName: 'TextboxReactiveComponent',
                  type: 'textbox',
                  order: 1,
                  position: {
                    column: '6',
                    order: 0
                  },
                  componentId: '776bd62b-b83a-9b67-43b9-03cf7daa2dcb',
                  value: 'joe.back@nomyemail.com'
                }
              ],
              structure: '6-3-3',
              position: {
                column: '6.2',
                order: 0
              },
              sectionId: '81e907b9-b6e5-fe27-d9fe-aaec2de37541',
              headerStyle: {
                'font-size': '1.2rem',
                'padding-bottom': '.5rem',
                'border-bottom': '2px solid #000',
                'margin-bottom': '10px'
              },
              sectionName: 'Header 3'
            },
            {
              components: [
                {
                  schema: 'contact.firstName',
                  key: 'lastName',
                  label: 'First Name',
                  componentName: 'TextboxReactiveComponent',
                  type: 'textbox',
                  order: 1,
                  position: {
                    column: '12',
                    order: 0
                  },
                  componentId: '9ae93aff-abb9-ca20-c907-73302e92f94b',
                  conditions: {},
                  value: 'Joe'
                }
              ],
              structure: '12',
              position: {
                column: '12.2',
                order: 0
              },
              sectionId: '8d1156fe-484d-7cce-4c38-8c97e022d3e2',
              headerStyle: {
                'font-size': '1.2rem',
                'padding-bottom': '.5rem',
                'border-bottom': '2px solid #000',
                'margin-bottom': '10px'
              },
              sectionName: 'Header 4'
            }
          ],
          structure: '12/6-6/12',
          pageId: 'f83c2ca3-1259-aa95-e817-61321a04713d'
        }
      ]
    }
}

How can we easily change the form definition?

FormQL has a Form Editor!

@juanstoppa

Loading the form

Ngrx store

Data

Form

2

Form

Groups

Components

3

4

Initial state is loaded in the store using side effects

Form Groups recalculated with new state

Components

GraphQL Server

1

Dispatch [form] load action

FormQL

@juanstoppa

Changing the state

Ngrx store

Data

Form

1

2

3

Keystroke detected 

dispatch component update action

Form, Data & Components

state is recalculated

Form Groups

recalculated with new state

Components

Form

Groups

Components

4

FormQL

@juanstoppa

The app modes

View Mode

Edit Mode

Live Edit Mode

Editing Data

Editing Form Metadata

Editing Data

& Form Metadata

@juanstoppa

Extending FormQL

App architecture

FormQL

Angular App

Layouts

Sections

Pages

Components

Validators

NGRX Store

GraphQL Service

Custom components

Custom layouts

Custom components

Custom Validators

Loaded using dynamic component loader

@juanstoppa

@juanstoppa

Custom component

export class MyCustomComponent implements ControlValueAccessor {
  static componentName = 'MyCustomComponent';
  
  static validators = [
    <ComponentValidator> {
      name: "CustomValidator",
      validator: CustomValidator,
      key: "customValidator"
    }
  ];
  
  @Input() field: FormComponent<any>;
  @Input() reactiveFormGroup: FormGroup;

Validators to apply

to this field

Component Name to support prod builds

@juanstoppa

What's next

Single store (Apollo Client)

Feedback

Angular Material CDK

RESTful api support

@juanstoppa

Conclusion

Use what works best for you and your team!

Ngrx as one source of truth

Reactive forms FTW

Dynamic component loader for extending

GraphQL to cherry pick your data

Thank You!

@juanstoppa

@formql_io

Made with Slides.com