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
- Composable Elements
- Manageable & Performant Data
- 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
last name
component:sign-up-form
component:input
component:input
component:input-group
component:input
component:input
component:submit
first name
yes/no
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!
🎉
- Composable Elements
- Manageable & Performant Data
- 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?
- Composable Elements
- Manageable & Performant Data
- Accessibility
-
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
-
Indicate required fields
-
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" />
-
Indicate required fields
-
Label everything
-
Enable Logical Tabbing
Email:
Do you want to receive newsletters?
-
Indicate required fields
-
Label everything
-
Enable logical tabbing
-
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,754