Danielle Adams / EmberFest 2018
slides.com/danielleadams/art-of-forms-ef18
@adamzdanielle
@danielleadams
me, usually
me, jet-lagged
beginner
knows things
has opinions
expert
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
a system design principle that provides components that can be selected and assembled in various combinations to satisfy user requirements
<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}} />
<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>
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
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!
🎉
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
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);
});
}
});
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
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);
'ember-cp-validations'
📢
encapsulated validation
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?
{{!-- 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
(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" />
Email:
Do you want to receive newsletters?
they got it right!
@adamzdanielle
@danielleadams
slides.com/danielleadams/art-of-forms-ef18