Mastering the

Art of Forms

Danielle Adams / EmberFest 2018 

slides.com/danielleadams/art-of-forms-ef18

I'm Danielle

@adamzdanielle

@danielleadams

me, usually

me, jet-lagged

beginner

knows things

has opinions

expert

Mastering the

Art of Forms

  1. Composable Elements
  2. Manageable & Performant Data
  3. Accessibility

Email:

Do you want to receive newsletters?

<form>
  <div class="row padding-5">
    <div class="col-md-6">
      <label for="first-name">
          First Name:
      </label>
    </div>
  </div>
  <div class="row padding-5">
    <div class="col-md-6">
      <input id="first-name" type="text" required value={{human.firstName}} />
    </div>
  </div>
  <div class="row padding-5">
    <div class="col-md-6">
      <label for="last-name">
          Last Name:
      </label>
    </div>
  </div>
  <div class="row padding-5">
    <div class="col-md-6">
      <input id="last-name" type="text" required value={{human.lastName}} />
    </div>
  </div>
  <div class="row padding-5">
    <div class="col-md-6">
      <p class="h3">Do you want to receive newsletters?</p>
    </div>
  </div>
  <div class="row padding-5">
    <div class="col-md-6">
      <label for="subscribe" class="m-left-4">
          Yes
      </label>
      {{radio-button id='subscribe' value=true groupValue=human.subscribed changed=(action (mut human.subscribed))}}
      <label for="subscribe" class="m-left-4">
          No
      </label>
      {{radio-button id='subscribe' value=false groupValue=human.subscribed changed=(action (mut human.subscribed))}}
    </div>
  </div>
  <div class="row padding-5">
    <div class="col-md-6">
      <label for="email">
          Email:
      </label>
    </div>
  </div>
  <div class="row padding-5">
    <div class="col-md-6">
      <input id="email" type="text" value={{human.email}} />
    </div>
  </div>
  <div class="row padding-5">
    <div class="col-md-6">
      <button class="btn btn-default" type="submit">
          Submit
      </button>
    </div>
  </div>
</form>
<MyFormComponent
    @options={{options}}
    @model={{user}}
    @requireName={{true}} />
<MyFormComponent
    @options={{options}}
    @options2={{options2}}
    @options3={{options3}}
    @model={{user}}
    @catsName={{human.cat.name}}
    @catsBirthday={{human.cat.birthday}}
    @auntsBirthday={{human.auntsBirthday}}
    @didClick={{didClick}}
    @requireEmail{{false}}
    @hideOptions2={{true}}
    @zodiacSign={{or human.sign human.sister.sign}}
    @requireName={{true}} />
<form>
  <div class="row padding-5">
    <div class="col-md-6">
      <label for="first-name">
          First Name:
      </label>
    </div>
  </div>
  <div class="row padding-5">
    <div class="col-md-6">
      <input id="first-name" type="text" required value={{human.firstName}} />
    </div>
  </div>
  <div class="row padding-5">
    <div class="col-md-6">
      <label for="last-name">
          Last Name:
      </label>
    </div>
  </div>
  <div class="row padding-5">
    <div class="col-md-6">
      <input id="last-name" type="text" required value={{human.lastName}} />
    </div>
  </div>
  <div class="row padding-5">
    <div class="col-md-6">
      <p class="h3">Do you want to receive newsletters?</p>
    </div>
  </div>
  <div class="row padding-5">
    <div class="col-md-6">
      <label for="subscribe" class="m-left-4">
          Yes
      </label>
      {{radio-button id='subscribe' value=true groupValue=human.subscribed changed=(action (mut human.subscribed))}}
      <label for="subscribe" class="m-left-4">
          No
      </label>
      {{radio-button id='subscribe' value=false groupValue=human.subscribed changed=(action (mut human.subscribed))}}
    </div>
  </div>
  <div class="row padding-5">
    <div class="col-md-6">
      <label for="email">
          Email:
      </label>
    </div>
  </div>
  <div class="row padding-5">
    <div class="col-md-6">
      <input id="email" type="text" value={{human.email}} />
    </div>
  </div>
  <div class="row padding-5">
    <div class="col-md-6">
      <button class="btn btn-default" type="submit">
          Submit
      </button>
    </div>
  </div>
</form>

62 Lines

to 8 Lines

Composability

a system design principle that provides components that can be selected and assembled in various combinations to satisfy user requirements

Design Questions

  • What field type is this?
  • Is this a required field?
  • Is it dependent on another component?
  • Does it supply functionality, UI elements or both?
<label for="first-name">
  First Name:
</label>
<input id="first-name" type="text" required value={{human.firstName}} />

<label for="last-name">
  Last Name:
</label>
<input id="last-name" type="text" required value={{human.lastName}} />
<FormTextInput @idValue="first-name" @required={{true}} @value={{human.firstName}}/>

<FormTextInput @idValue="last-name" @required={{true}} @value={{human.lastName}}/>

becomes

{{!-- templates/components/form-text-input.hbs --}}

<label for={{idValue}}>
    {{t (concat 'input-label.' idValue)}}
</label>
<input
    id={{idValue}}
    type="text"
    {{if required "required"}}
    value={{value}} />

Benefits

  • Standardizes APIs of the form elements in the scope of the application and among the code's components.
  • Standardizes the UI of the various form elements across the application.
<p class="h3">Do you want to receive newsletters?</p>
<label for="subscribe" class="m-left-4">
  Yes
</label>
{{radio-button id="subscribe" value=true
    groupValue=human.subscribed changed=(action (mut human.subscribed))}}
<label for="subscribe" class="m-left-4">
  No
</label>
{{radio-button id="subscribe" value=false
    groupValue=human.subscribed changed=(action (mut human.subscribed))}}
<label for="email">
  Email:
</label>
<input id="email" type="text" value={{human.email}} />
<FormSubscribeGroup @model={{human}} as |Group|}}
    <Group.titleQuestion />
    <Group.radioButtonGroup />
    <Group.emailInput />
</FormSubscribeGroup>

becomes

Email:

Do you want to receive newsletters?

{{!-- templates/components/form-subscribe-group.hbs --}}

{{yield
    (hash
        questionText=(t 'radio-button-group.ask-to-subscribe')
        radioButtonGroup=(component 'radio-button-group'
            value=model.subscribe
            options=radioOptions)
        emailInput=(component 'form-email-input'
            idValue='email'
            required=model.subscribe
            value=model.email)
    )
}}
questionText=(t 'radio-button-group.ask-to-subscribe')

renders text

emailInput=(component 'form-email-input')

renders component

<FormSubscribeGroup @model={{human}} as |Group|}}
    <Group.titleQuestion />
    <Group.radioButtonGroup />
    <Group.emailInput />
</FormSubscribeGroup>
<FormSubscribeGroup @model={{human}} as |Group|}}
    <Group.titleQuestion class="margin-10" />
    <Group.radioButtonGroup class="margin-10" />
    <Group.emailInput class="margin-10" />
</FormSubscribeGroup>
<FormSubscribeGroup @model={{human}} as |Group|}}
    <Group.titleQuestion class="margin-10" />
    <FormTextInput @idValue="pet-name"
        @value={{human.petName}} />
    <Group.radioButtonGroup class="margin-10" />
    <Group.emailInput class="margin-10" />
</FormSubscribeGroup>
<FormSubscribeGroup @model={{human}} as |Group|}}
    <Group.titleQuestion class="margin-10" />
    <FormTextInput @idValue="pet-name"
        @value={{human.petName}} />
    <Group.radioButtonGroup @options={{parentOpts}}
        class="margin-10" />
    <Group.emailInput class="margin-10" />
</FormSubscribeGroup>
  • add classes
  • embed other components
  • overwrite properties

component:sign-up-form

component:input

component:input

component:input-group

component:input

component:input

component:submit

first name

yes/no

email

last name

component:sign-up-form

component:input

component:input

component:input-group

component:input

component:input

component:submit

first name

yes/no

email

last name

Functional components

UI components

Design Questions

  • What field type is this?
  • Is this a required field?
  • Is it dependent on another component?
  • Does it supply functionality, UI elements or both?

Benefits

  • Standardizes APIs of the form elements in the scope of the application and among the code's components.
  • Standardizes the UI of the various form elements across the application.
  • Form groupings are reusable without being bound to their initial layout or attributes.
  • Way easier to test.

Email:

Do you want to receive newsletters?

{{!-- templates/components/sign-up-form.hbs --}}

<FormTextInput @idValue="first-name" @required={{true}} @value={{human.firstName}}/>

<FormTextInput @idValue="last-name" @required={{true}} @value={{human.lastName}}/>

<FormSubscribeGroup @model={{human}} as |Group|}}
    <Group.titleQuestion />
    <Group.radioButtonGroup />
    <Group.emailInput />
</FormSubscribeGroup>

<SubmitButton @text={{t "submit"}} @submitAction={{action 'submitForm'}} />

8 Lines!

🎉

  1. Composable Elements
  2. Manageable & Performant Data
  3. Accessibility

All data should be loaded once the application completes a transition into a route.

NOPE

Data that isn't required by the view should not hold up a page from rendering.

State (optional):

Email:

Is it essential when the user lands on the page?

Is it important in the scope of the route?

Is it a concern of a component?

The component can be responsible for fetching data.

NO

NO

YES

Load the data from the component

export default Component.extend({
    willRender() {
        this.get('store').findAll('state').then((states) => {
            this.set('options', states);
        }).catch((error) => {
            this.get('errorHandlerService').send(error);
        });
    }
});
willRender() {
this.set('options', states);
this.get('errorHandlerService').send(error);

encapsulated "state"

import { task } from 'ember-concurrency';

export default Component.extend({
    willRender() {
        this.get('getDataTask').perform();
        this._super(...arguments);
    },

    getDataTask: task(function * (fetchData) {
        try {
            fetchData();
        } catch(e) {
            this.incrementProperty('errorCount')

            if (this.get('errorCount') > 5) {
                this.get('errorHandlerService').send(error);
            else {
                this.getDataTask().perform();
            }
        }
    }).restartable(),

    fetchData() {
        return this.get('store').findAll('state').then((states) => {
            this.set('options', states);
        });
    }
});

Handle retries from the component

willRender() {
    this.get('getDataTask').perform();
    this._super(...arguments);
}
    this.incrementProperty('errorCount')

    if (this.get('errorCount') > 5) {
        this.get('errorHandlerService').send(error);
    } else {
        this.getDataTask().perform();
    }
}).restartable(),
'ember-concurrency'

📢

encapsulated error handling

Benefits

  • User is able to interact with the application sooner.
  • Relative data is handled within the scope of the component, rendering it more useful across the application without needing a more complicated API.
  • Easy to test.

Data should always be validated at the model or server level.

HOW ABOUT NO

Data can be validated anywhere!

Is the validation specific to the input type?

Is the error messaging generic enough to reuse?

It can be validated at the component level.

YES

YES

import { buildValidations, validator } from 'ember-cp-validations';

const Validations = buildValidations({
    value: validator('presence', true)
});

export default Component.extend(Validation);

Validate data from the component level

'ember-cp-validations'

📢

encapsulated validation

Ember Models should always reflect the back end that persists them.

NAH

The Data Store can be used to represent client-side state, so use that to the application's advantage.

<ember-model:sign-up>

<input />

<input />

<input />

// models/sign-up.js

import Model from 'ember-data/model';
import attr from 'ember-data/attr';

export default Model.extend({
    email: attr('email'),
    firstName: attr('string'),
    lastName: attr('string'),
    subscribed: attr('boolean', {
        defaultValue: false
    })
});
{{!-- templates/components/sign-up-form.hbs --}}

<FormTextInput @idValue="first-name"
    @required={{true}} @value={{signUp.firstName}}/>

<FormTextInput @idValue="last-name"
    @required={{true}} @value={{signUp.lastName}}/>

<FormTextInput @idValue="email" @value={{signUp.email}}/>

<ember-model:sign-up>

<input />

<input />

<input />

The form is simplified to a single model and can take advantage of Ember Data's Model states.

This could otherwise be known as a façade pattern.

model:sign-up

<form></form>

API call

model:human

unloaded

// components/sign-up-form.js

import Component from '@ember/component';

export default Component.extend({
    actions: {
        saveSignUpForm() {
            const signUp = this.signUp;

            signUp.save().then(() => {
                signUp.unloadRecord();
            });
        }
    }
});

Email:

Do you want to receive newsletters?

Email:

Do you want to receive newsletters?

Email:

Do you want to receive newsletters?

Email:

Do you want to receive newsletters?

  1. Composable Elements
  2. Manageable & Performant Data
  3. Accessibility
  1. Indicate required fields

{{!-- templates/components/input-group.hbs --}}

<label for={{idValue}}>
    {{t 'input-label'}}
</label>
<input
    id={{idValue}}
    {{if required "required"}}
    type="text" />

The browser will take care of the rest

  1. Indicate required fields

  2. Label everything

(Or use ARIA tags accordingly.)

{{!-- templates/components/input-group.hbs --}}

<label for={{idValue}}>
    {{t 'input-label'}}
</label>
<input
    id={{idValue}}
    {{if required "required"}}
    type="text" />
  1. Indicate required fields

  2. Label everything

  3. Enable Logical Tabbing

Email:

Do you want to receive newsletters?

  1. Indicate required fields

  2. Label everything

  3. Enable logical tabbing

  4. Use clear success and error messaging

they got it right!

Closing Thoughts

@adamzdanielle

@danielleadams

slides.com/danielleadams/art-of-forms-ef18

thank you!

Mastering the Art of Forms

By Danielle Adams

Mastering the Art of Forms

Slides for EmberFest 2018

  • 1,623