Let's build the future of forms
with GraphQL 👩‍🚀 👨‍🚀

#whoami

Charly POLY - Senior Software Engineer at

 


🔗   Writing about Software Eng. on honest.engineering

       Writing about TypeScript and GraphQL on Medium

Forms on the web ecosystems

  • Subject tackled by most ecosystems
  • Form as a Service
    • TypeForm
    • Google Forms

What about SPA, JavaScript ecosystem forms?

Role of Forms in a Web Application

Forms handle the experience that user have with data

Developer Experience

User Experience

  • Be the most precise as possible
  • Make is easy and fast to fill complex informations
  • Immediate feedback
  • Be flexible, customisable
  • Avoid validation divergence with
    back-end
  • Handle state

In short, fast and smooth experience

Current state of JS forms building

Vue.js forms

redux-form

Formik

Benchmark: Vue.js forms

What it doesn’t solve:

  • almost everything is done by hand ✍️
    • components, fields, state
  • no Form state management
  • no easily reusable ☠️
  • non-standard validation format ☠️

What it solves:

  • smart binding
  • state related features ( lazy, numbers and debounce attributes  )
  • (validations using vee-validate)

Benchmark: redux-form

What it doesn’t solve:

  • lot of configuration ✍️
  • validators by hand ✍️
  • forms state in global state ☠️
  • Field by Field building workflow ✍️

What it solves:

  • Error management
  • Form state management
  • Built-in components and helpers

The best way to manage your form state in Redux.

Benchmark: Formik

What it doesn’t solve:

  • lot of configuration ✍️
  • validators by hand ✍️
  • non-standard validation format ☠️
  • Field by Field building workflow ✍️

What it solves:

  • Object-schema based validations with Yup
  • Form state management (not global)
  • "Functional" state management + render props pattern
  • Built-in components and helpers

Build forms in React, without the tears 😭

Benchmark results

Vue.js forms redux-form Formik
Manage state ✍️
Data validation ✍️ ✍️ / ✅
Theming ✍️ ✍️ ✍️
Error messages ✍️ ✍️ ✍️
Fields conf. ✍️ ✍️ ✍️

Developer Experience

Benchmark results

Vue.js forms redux-form Formik
Productivity ⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐
Performance ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐
Flexibility ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐
Components
Reusability
⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐
Validation
Isomorphism
nodejs-only nodejs-only nodejs-only

Architecture

Observation

  • No matters the solution, we have to duplicate:
    • fields list and definition
    • data validations

What about types?

Since forms are about data, why not build typed forms?

The JavaScript eco-system new era:

- TypeScript: typed JavaScript

- GraphQL: typed data exchange

Feedback: Dropping redux-form

configured
redux-form

class Form extends ModuleForm {
    fields: FieldsDefinitions = {
        id: 'none',
        email: 'none',
        picture_path: {
            type: 'image', transformations: 'h_200,w_200,r_max,c_fill'
        },
        first_name: 'string*',
        last_name: 'string*',
        username: 'string*',
        job_title: 'string',
        company_name: 'string',
        language: {
            type: 'select*',
            component: LanguageSelectView,
            valueProperty: 'code',
            values: supportedLanguages,
            moduleName: 'attachment' // TODO: fix.
        }
    };

    constructor() {
        super('UserForm', 'user');
    }
}

Solution: Mozilla react-jsonschema-form

A React component for building Web forms from JSON Schema.

Mozilla react-jsonschema-form

interface Todo {
    id: String;
    name: String;
    completed: Boolean;
    color: Color;
}
{
    $schema: 'http://json-schema.org/draft-06/schema#',
    properties: {
        Todo: {
            type: 'object',
            properties: {
                id: { type: 'string' },
                name: { type: 'string' },
                completed: { type: 'boolean' },
                color: { $ref: '#/definitions/Color' },
            },
            required: ['id', 'name']
        }
    }
}

What is JSON Schema?

  • "A Media Type for Describing JSON Documents"

Mozilla react-jsonschema-form

Mozilla react-jsonschema-form

  1. Provide a JSON Schema describing the data
  2. Provide a JSON Schema describing the UI
  3. Provide initial data

Form React component

Mozilla react-jsonschema-form

react-jsonschema-form
Productivity ⭐⭐
Performance ⭐⭐⭐⭐
Flexibility ⭐⭐
Components
Reusability
⭐⭐
Validation
Isomorphism
⭐⭐

Architecture

react-jsonschema-form
Manage state
Data validation ✍️
Theming
Error messages
Fields conf. ✍️

Developer Experience

<ApolloForm> 👩‍🚀 👨‍🚀

import * as React from 'react';
import gql from 'graphql-tag';
import { configure } from 'react-apollo-form';
import { client } from './apollo';
import { applicationFormTheme } from './core/forms/themes/application';


const jsonSchema = require('./core/apollo-form-json-schema.json');

export const ApplicationForm = configure<ApolloFormMutationNames>({
    // tslint:disable-next-line:no-any
    client: client as any,
    jsonSchema,
    theme: applicationFormTheme
});

<ApplicationForm
    config={{
        mutation: {
            name: 'create_todo',
            document: gql`mutation {...}`
        }
    }}
    data={{}}
/>

<ApolloForm> 👩‍🚀 👨‍🚀

GraphQL mutations
(fields + types)

JSON Schema
(validations)

+

<ApolloForm>

<ApolloForm> flexible API

  • Schema options
  • Theming options
  • Error messages
  • Callbacks
  • Render props
import * as React from 'react';
import gql from 'graphql-tag';
import { configure } from 'react-apollo-form';
import { client } from './apollo';
import { applicationFormTheme } from './core/forms/themes/application';


const jsonSchema = require('./core/apollo-form-json-schema.json');

export const ApplicationForm = configure<ApolloFormMutationNames>({
    // tslint:disable-next-line:no-any
    client: client as any,
    jsonSchema,
    theme: applicationFormTheme
});

<ApplicationForm
    config={
        /* schema options */
    }
    data={{
        /* data */
    }}
    ui={
        /* UI options */
    }
/>

<ApolloForm> flexible API

Schema options

  • "Mutation mode"
  • "Manual mode"
<ApplicationForm
    config={{
        mutation: {
            name: 'create_todo',
            document: gql`mutation {...}`
        }
    }}
    data={{}}
/>
<ApplicationForm
    title={'Todo Form'}
    config={{
        name: 'todo',
        schema: schema({
            todo: {
                name: types.type('string', { required: true }),
                completed: types.type('boolean')
            }
        }),
        saveData: data => {
            console.log('save !', data);
        }
    }}
    data={{}}
    ui={{}}
/>
<ApplicationForm
    config={{
        mutation: {
            name: 'create_todo',
            document: gql`mutation {...}`
        },
        ignoreFields: ['user.image'],
        updateFields: {
          'user.phone_number': { pattern: '(^[0-9\+]{5,}$)|(^[a-zA-Z0-9]{0}$)' }
        },
        requiredFields: ['user.email']
    }}
    data={{}}
/>

<ApolloForm> flexible API

Render props

export interface ApolloRenderProps {
    // renderers
    header: () => React.ReactNode; // render the form header (title by default)
    form: () => React.ReactNode; // render the inputs
    buttons: () => React.ReactNode; // render save and cancel buttons
    saveButton: () => React.ReactNode; // render save button
    cancelButton: () => React.ReactNode; // render cancel button
    // actions
    cancel: () => void; // trigger a cancel
    save: (args: any) => void; // trigger a save
    // state
    isDirty: boolean;
    isSaved: boolean;
    hasError: boolean;
    data: any;
}
<ApplicationForm
   /* props options */
>
    {
        form => (
            <div
                style={{
                    backgroundColor: '#FFF',
                    padding: '20px'
                }}>
                <h2>
                    My form!
                </h2>
                <br />
                {form.form()}
                {form.saveButton()}
            </div>
        )
    }
</ApplicationForm>

<ApolloForm> flexible API

Error messages

By default, <ApolloForm> do not display errors.

To enable it, you should provide some options to ui prop.

type ApolloFormUi = {
    // default: false, should we display errors list at the top ?
    showErrorsList?: boolean;
    // default: true, should we display errors at field level ?
    showErrorsInline?: boolean;
    // you can provide a custom component to display error list
    errorListComponent?: ErrorListComponent;
};

<ApolloForm> flexible API

Theming

  • Templates render Fields and Widgets
     
  • Renderers are <ApolloForm> specific:
    • <saveButton>
    • <cancelButton>
    • <header>
interface ApolloFormConfigureTheme {
    templates?: ApolloFormTheme['templates'];
    widgets?: ApolloFormTheme['widgets'];
    fields?: ApolloFormTheme['fields'];
    renderers?: Partial<ApolloFormTheme['renderers']>;
}

<ApolloForm> internals

GraphQL Schema

JSON-Schema
schema

TypeScript
Mutations enum

Build
tools

At
runtime

JSON & TS files
imported at runtime

introspection

Files
genenations

configure a FormFactory against
a schema

building a mutation
form using the
FormFactory

<ApolloForm> internals : Build tools

GraphQL Schema

JSON-Schema
schema

TypeScript
Mutations enum

introspection

- schema.json
- mutations.d.ts
- apollo-form-json-schema.json

react-apollo-form fetch-mutations <graphqlEndpoint> <outpurDir>

<ApolloForm> internals : Build tools

What is an introspection query?

<ApolloForm> internals : Build tools

graphql-2-json-schema package

type Todo {
    id: String!
    name: String!
    completed: Boolean
    color: Color
}

input TodoInputType {
    name: String!
    completed: Boolean
    color: Color
}

type Mutation {
    update_todo(id: String!, todo: TodoInputType!): Todo
    create_todo(todo: TodoInputType!): Todo
}
{
    $schema: 'http://json-schema.org/draft-06/schema#',
    properties: {
        Mutation: {
            type: 'object',
            properties: {
                update_todo: {
                    type: 'object',
                    properties: {
                        arguments: {
                            type: 'object',
                            properties: {
                                id: { type: 'string' },
                                todo: { $ref: '#/definitions/TodoInputType' }
                            },
                            required: ['id', 'todo']
                        },
                        return: {
                            $ref: '#/definitions/Todo'
                        }
                    },
                    required: []
                },
                // ...
            }
        },
    },
    definitions: {
        'Todo': {
            type: 'object',
            properties: {
                id: { type: 'string' },
                name: { type: 'string' },
                completed: { type: 'boolean' },
                color: { $ref: '#/definitions/Color' },
            },
            required: ['id', 'name']
        },
        'TodoInputType': {
            type: 'object',
            properties: {
                name: { type: 'string' },
                completed: { type: 'boolean' },
                color: { $ref: '#/definitions/Color' },
            },
            required: ['name']
        }
    }
}

<ApolloForm> internals : at runtime

JSON & TS files
imported at runtime

configure a FormFactory against
a schema

building a mutation
form using the
FormFactory

<ApolloForm> internals : at runtime

  1. import definitions files
  2. configure a Form Builder
     
  3. Instantiate a Form
import * as React from 'react';
import gql from 'graphql-tag';
import { configure } from 'react-apollo-form';
import { client } from './apollo';
import { applicationFormTheme } from './core/forms/themes/application';


const jsonSchema = require('./core/apollo-form-json-schema.json');

export const ApplicationForm = configure<ApolloFormMutationNames>({
    // tslint:disable-next-line:no-any
    client: client as any,
    jsonSchema,
    theme: applicationFormTheme
});

<ApplicationForm
    config={{
        mutation: {
            name: 'create_todo',
            document: gql`mutation {...}`
        }
    }}
    data={{}}
/>

<ApolloForm> internals : at runtime

<ApolloForm> internals : at runtime

Provide mutation name + options

Your application

<ApolloForm>

react-jsonschema-form

get json-schema for mutation +
transform UI props

handle rendering +
validations

<ApolloForm> benchmark

  • react-jsonschema-form provides:

    • Extendable, with the fields and widgets system.
    • State management and validation,
      handled with local state and local validation using ajv.
       
  • <ApolloForm> bring the last 2 key features:

    • API/Client Interoperability: generate the form JSON Schema from a mutation definition
    • Simplicity: provide some helpers to avoid the complexity of JSON Schema and enhance the extensibility of react-jsonschema-form.

Conclusion

Vue.js forms redux-form Formik react-jsonschema-form <ApolloForm>
Manage state ✍️
Data validation ✍️ ✍️
Theming ✍️ ✍️ ✍️
Error messages ✍️ ✍️ ✍️
Fields conf. ✍️ ✍️ ✍️ ✍️

Developer Experience

Conclusion

Vue.js forms redux-form Formik react-jsonschema-form <ApolloForm>
Productivity ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐
Performance ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐
Flexibility ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐
Components
Reusability
⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐
Validation
Isomorphism
nodejs-only nodejs-only nodejs-only ⭐⭐⭐⭐

Architecture

Thanks for listening!

Still in beta, looking for collaborators ➡️  react-apollo-form

Let's build the future of forms with GraphQL

By Charly Poly

Let's build the future of forms with GraphQL

  • 2,100