Danielle Adams / EmberConf 2018 🎉
slides.com/danielleadams/art-of-forms-ec18
@adamzdanielle
@danielleadams
Component Patterns
Data
Management
User Experience
Accessibility
{{!-- 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>
<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"}}
<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}}
{{!-- 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.
All data should be loaded once the application completes a transition into a route.Â
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);
});
}
});
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);
});
}
});
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);
});
}
});
Ember Models should reflect the back end that persists them.
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.
Data should be validated at the model or server level.
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);
Never use two-way binding.
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.
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.
(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"}}
(And make sure the browser is doing what is expected.)
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 ¯\_(ツ)_/¯
@adamzdanielle
@danielleadams
slides.com/danielleadams/art-of-forms-ec18