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...
- past EmberConf talks
- MDN web docs
- tweet about it like I did and someone from Ember Core will respond to you too
@adamzdanielle
@danielleadams
slides.com/danielleadams/art-of-forms-ec18
thank you!
Art of Forms
By Danielle Adams
Art of Forms
Slides for EmberConf 2018
- 2,704