Mastering the

Art of Forms

Danielle Adams / EmberConf 2018 🎉

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

I'm Danielle

@adamzdanielle

@danielleadams

Mastering the

Art of Forms

Component Patterns

Data

Management

User Experience

Accessibility

Component

Patterns

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

<form>
    <label for="first-name">
        First Name:
    </label>
    {{input id='first-name' type='text' required=true value=human.firstName}}

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

    <p>Are you feeling lucky?</p>

    <label for="am-feeling-lucky">
        Yes
    </label>
    {{input type='radio' id='am-feeling-lucky' name='feeling-lucky' value='yes'}}

    <label for="not-feeling-lucky">
        No
    </label>
    {{input type='radio' id='not-feeling-lucky' name='feeling-lucky' value='no'}}

    <label for="lucky-number">
        Lucky Number:
    </label>
    {{input id='lucky-number' type='number'required=false value=human.luckyNumber
        disabled=isFeelingLucky}}

    <button type="submit">
        Submit
    </button>
</form>

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=true value=human.firstName}}

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

{{input/text-field id='last-name' required=true value=model.lastName}}

becomes

<label for="first-name">
    First Name:
</label>
{{input id='first-name' type='text' required=true value=human.firstName}}

<label for="last-name">
    Last Name:
</label>
{{input id='last-name' type='text' required=true value=human.lastName}}
{{!-- templates/components/input/text-field.hbs --}}

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

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>Are you feeling lucky?</p>

<label for="am-feeling-lucky">
    Yes
</label>
{{input type='radio' id='am-feeling-lucky' name='feeling-lucky' value='yes'}}

<label for="not-feeling-lucky">
    No
</label>
{{input type='radio' id='not-feeling-lucky' name='feeling-lucky' value='no'}}

<label for="lucky-number">
    Lucky Number:
</label>
{{input id='lucky-number' type='number'required=false value=human.luckyNumber
    disabled=isFeelingLucky}}
{{#form-group/lucky-number model=human as |formGroup|}}
    {{formGroup.questionText}}
    {{formGroup.radioButtonGroup}}
    {{formGroup.luckyNumberInput}}
{{/form-group}}

becomes

<p>Are you feeling lucky?</p>

<label for="am-feeling-lucky">
    Yes
</label>
{{input type='radio' id='am-feeling-lucky' name='feeling-lucky' value='yes'}}

<label for="not-feeling-lucky">
    No
</label>
{{input type='radio' id='not-feeling-lucky' name='feeling-lucky' value='no'}}

<label for="lucky-number">
    Lucky Number:
</label>
{{input id='lucky-number' type='number'required=false value=human.luckyNumber
    disabled=isFeelingLucky}}
{{!-- templates/components/form-group/lucky-number.hbs --}}

{{yield
    (hash
        questionText=(t 'radio-button-group.feeling-lucky')
        radioButtonGroup=(component 'radio-button-group
            options=radioOptions)
        luckyNumberInput=(component 'input/number-value'
            id='input.lucky-number'
            value=model.luckyNumber)
    )
}}

yields some text

yields specified component

component:sign-up-form

component:input

component:input

component:input-group

component:input

component:input

component:submit

first name

last name

yes/no

lucky number

becomes

<p>Are you feeling lucky?</p>

<label for="am-feeling-lucky">
    Yes
</label>
{{input type='radio' id='am-feeling-lucky' name='feeling-lucky' value='yes'}}

<label for="not-feeling-lucky">
    No
</label>
{{input type='radio' id='not-feeling-lucky' name='feeling-lucky' value='no'}}

<label for="lucky-number">
    Lucky Number:
</label>
{{input id='lucky-number' type='number'required=false value=human.luckyNumber
    disabled=isFeelingLucky}}
{{#form-group/lucky-number model=human as |formGroup|}}
    {{formGroup.questionText}}
    {{formGroup.radioButtonGroup}}
    {{input/text-field id='last-name' required=true value=model.lastName}}
    {{formGroup.luckyNumberInput}}
{{/form-group}}

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.
{{!-- templates/components/sign-up.hbs --}}

<form>
    {{input/text-field id='first-name' required=true value=human.firstName}}

    {{input/text-field id='last-name' required=true value=human.lastName}}
    
    {{#form-group/lucky-number model=human as |formGroup|}}
        {{formGroup.questionText}}

        {{formGroup.radioButtonGroup}}

        {{formGroup.luckyNumberInput}}
    {{/form-group}}

    <button type="submit">
        Submit
    </button>
</form>

Components should fit together like a game of Tetris.

 

Plan ahead how a component fits into the larger application. Components may not cleanly "stack", but do what you can to make them work together.

Data Management

True or False:

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

FALSE

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

State (optional):

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

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

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);
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(),
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('errorHandler').send(error);
            else {
                this.getDataTask().perform();
            }
        }
    }).restartable(),

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

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.

True or False:

Ember Models should reflect the back end that persists them.

NOT ALWAYS

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

<ember-model:sign-up-form>

<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.

True or False:

Data should be validated at the model or server level.

NOPE

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

True or False:

Never use two-way binding.

HAHA WAT

Two-way binding is not the enemy!

Is the element an input?

You can probably use two-way binding.

YES

<ember-model:sign-up-form>

<input />

<input />

<input />

State?

When handling data from upstream, try "read-only" binding to ensure the original data is not updated until persisted.

Validations?

Debugging?

model:sign-up-form

<form></form>

API call

model:human

unloaded

Simplify until it makes sense.

 

Be thoughtful in code designs and patterns. There are rarely "always" cases that hold up.

Except Observers.

Web Accessibility

User Experience &

Accessibility is the practice of making your websites usable by as many people as possible - we traditionally think of this as being about people with disabilities, but really it also covers other groups such as those using mobile devices, or those with slow network connections.

Things to Remember

Label everything

(Or use ARIA.)

ARIA - Accessible Rich Internet Applications - is a set of attributes that define ways to make Web content and Web applications more accessible to people with disabilities.

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

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

Indicate required fields

(And make sure the browser is doing what is expected.)

Use logical tabbing

????

Use thorough success and error messaging

Idempotence (US:  EYE-dəm-POH-təns) is the property of certain operations in mathematics and computer science that they can be applied multiple times without changing the result beyond the initial application.

Wikipedia ¯\_(ツ)_/¯

There's so much more...

@adamzdanielle

@danielleadams

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

thank you!