compose all the things (wicked good ember 2015)
TRANSCRIPT
Compose all the thingsMike North
Wicked Good Ember 2015
@MichaelLNorth
modernwebui.org
Modern Web UI
advertising.yahoo.com
Yahoo Ads & Data
Hi
ember-resize
ember-orientation
ember-cpm
ember-cli-materialize…and more
@MichaelLNorth
Composability
(Mike working w/ a composable system)
//TODO
• The state of ember at yahoo • What’s composability, and why do we care? • 4 areas where you can compose today
• Style • CPMs • Components • Tests
@MichaelLNorth
Yahoo Ads & Data
• 14 Ember Apps • 68 Ember-focused developers • A “flagship” app that ’s huge (70K lines JS) • An internal collection of add ons
Ember @ Yahoo
@MichaelLNorth
@MichaelLNorth
Composability
• Recombinant self-contained pieces
• Built around established contracts and conventions
• Promotes reuse
What do I mean?
@MichaelLNorth
Why composability?
• Leverage existing code repeatedly • Build apps in a more expressive way (DSLs) • Opportunities for unforeseen uses!
@MichaelLNorth
Great places to start
Style
Tests
Computed Properties
Components
@MichaelLNorth
Great places to startStyle
Declarative CSS
A<div id=“myThing”> ...</div>
#myThing { float: left; color: white;}
B<div class=“pull-left white-text"> ...</div>
.pull-left { float: left;}
.white-text { color: white;}
@MichaelLNorth
Great places to startStyle
• Atomic CSS classes • Expressive HTML • Promotes consistency
Declarative CSS
<div class=“pull-left white-text"> ...</div>
.pull-left { float: left;}
.white-text { color: white;}
@MichaelLNorth
Great places to startStyle
• You may have classes and/or attributes for • Testing • Style • Behavior
Keep attributes & classes organized
<input class="first-name large-input” data-autoid="first-name" />
.large-input { font-size: 32px;}
style
fillIn(‘input[data-autoid="first-name"]','Mike');
testing
@MichaelLNorth
Great places to start
Style
Tests
Computed Properties
Components
@MichaelLNorth
Great places to startComputed Properties
How does a computed property work?
GET Has cached value?
Recalculate
No
Yes
XAllows
caching?
CacheX
XYes
No
ReturnX
@MichaelLNorth
Great places to startComputed Properties
How does a computed property work?
DEPENDENT CHANGED
Cache
X
I’ve changed!
ViewProperty obj.get(‘val’)
@MichaelLNorth
Great places to startComputed Properties
CPs can be thought of as filters
CPrgb
#ff1a99
get() set()
(sometimes)
CPr
b
g#ff1a99
@MichaelLNorth
Great places to startComputed Properties
• Ember.computed.*
Macros make this even easier
function product(prop, coeff) {return Ember.computed(prop, {get() {return this.get(prop) * coeff;
}});
}
@MichaelLNorth
Great places to startComputed Properties
totalAmount: sum( 'subtotal', 'tipAmount', 'taxAmount', product('discount', -1)),
Composable CPs can be mixed and matched
ember-cpm
@MichaelLNorth
Great places to startComputed Properties
Example
@MichaelLNorth
Great places to start
Style
Tests
Computed Properties
Components
@MichaelLNorth
Great places to startComponents
• Not source of truth for state ✔ • Promotes Reuse ✔ • Recombinant ?
Components are pretty close…
ember-cli-materialize
@MichaelLNorth
Great places to startComponents
Looking for this<div class="card blue-grey darken-1"> <div class="card-content white-text"> <div class="card-title"> Wicked Good Ember </div> This will go in the body of the card </div> <div class="card-action"> <a><span {{action “accept”}}>Accept</span>
</a> <a><span {{action “cancel”}}>Cancel</span>
</a> </div></div>
@MichaelLNorth
Great places to startComponents
One option - lowest common element<div class="card blue-grey darken-1"> <div class="card-content white-text"> <div class="card-title"> Wicked Good Ember </div> This will go in the body of the card </div> <div class="card-action"> <a><span {{action “accept”}}>Accept</span>
</a> <a><span {{action “cancel”}}>Cancel</span>
</a> </div></div>
{{#wge-card}} <div class="card-content white-text"> <div class="card-title"> Wicked Good Ember </div> This will go in the body of the card </div> <div class="card-action">
{{#wge-card-action}} <span {{action "accept"}}> Accept </span> {{/wge-card-action}} {{#wge-card-action}} <span {{action "cancel"}}> Cancel </span> {{/wge-card-action}}
</div>{{/wge-card}}
@MichaelLNorth
Great places to startComponents
One option - lowest common element<div class="card blue-grey darken-1"> <div class="card-content white-text"> <div class="card-title"> Wicked Good Ember </div> This will go in the body of the card </div> <div class="card-action"> <a><span {{action “accept”}}>Accept</span>
</a> <a><span {{action “cancel”}}>Cancel</span>
</a> </div></div>
{{#wge-card}} <div class="card-content white-text"> <div class="card-title"> Wicked Good Ember </div> This will go in the body of the card </div> <div class="card-action">
{{#wge-card-action}} <span {{action "accept"}}> Accept </span> {{/wge-card-action}} {{#wge-card-action}} <span {{action "cancel"}}> Cancel </span> {{/wge-card-action}}
</div>{{/wge-card}}
Not Useful
@MichaelLNorth
Great places to startComponents
Another option - parent does everything<div class="card blue-grey darken-1"> <div class="card-content white-text"> <div class="card-title"> Wicked Good Ember </div> This will go in the body of the card </div> <div class="card-action"> <a><span {{action “accept”}}>Accept</span>
</a> <a><span {{action “cancel”}}>Cancel</span>
</a> </div></div>
{{#wge-cardtitle="Wicked Good Ember"cardActions=myCardActions}}
This will go in the bodyof the card
{{/wge-card}}
@MichaelLNorth
Great places to startComponents
Another option - parent does everything<div class="card blue-grey darken-1"> <div class="card-content white-text"> <div class="card-title"> Wicked Good Ember </div> This will go in the body of the card </div> <div class="card-action"> <a><span {{action “accept”}}>Accept</span>
</a> <a><span {{action “cancel”}}>Cancel</span>
</a> </div></div>
{{#wge-cardtitle="Wicked Good Ember"cardActions=myCardActions}}
This will go in the bodyof the card
{{/wge-card}}Not Compo
sable
@MichaelLNorth
Great places to startComponents
The expressive option
{{#wge-card title="Wicked Good Ember"}} This will go in the body of the card {{#wge-card-action}} <span {{action "accept"}}>Accept</span> {{/wge-card-action}} {{#wge-card-action}} <span {{action "cancel"}}>Cancel</span> {{/wge-card-action}}{{/wge-card}}
@MichaelLNorth
Great places to start
{{#wge-card title="Wicked Good Ember"}}
This will go in the body of the card
{{#wge-card-action}} <span {{action “accept"}}> Accept
</span> {{/wge-card-action}} {{#wge-card-action}} <span {{action “cancel”}}>
Cancel</span>
{{/wge-card-action}}{{/wge-card}}
Components
Content projection - ruh roh<div class="card blue-grey darken-1"> <div class="card-content white-text"> <div class="card-title"> Wicked Good Ember </div> This will go in the body of the card </div> <div class="card-action"> <a><span {{action “accept”}}>Accept</span>
</a> <a><span {{action “cancel”}}>Cancel</span>
</a> </div></div>
4 distinct pieces of content
@MichaelLNorth
Great places to start
{{#wge-card title="Wicked Good Ember"}}
This will go in the body of the card
{{#wge-card-action}} <span {{action “accept"}}> Accept
</span> {{/wge-card-action}} {{#wge-card-action}} <span {{action “cancel”}}>
Cancel</span>
{{/wge-card-action}}{{/wge-card}}
Components
Content projection - ruh roh<div class="card blue-grey darken-1"> <div class="card-content white-text"> <div class="card-title"> Wicked Good Ember </div> This will go in the body of the card </div> <div class="card-action"> <a><span {{action “accept”}}>Accept</span>
</a> <a><span {{action “cancel”}}>Cancel</span>
</a> </div></div>
4 distinct pieces of content
Sub-components project into parent
{{yield}}
Simple property binding
@MichaelLNorth
Great places to startComponents
Content projection approach{{#wge-card
title="Wicked Good Ember"}} This will go in the body of the card
{{#wge-card-action}} <span {{action “accept"}}> Accept
</span> {{/wge-card-action}} {{#wge-card-action}} <span {{action “cancel”}}>
Cancel</span>
{{/wge-card-action}}{{/wge-card}}
• Child components won’t render directly
• Parent will handle rendering of children
• Register/unregister to parent
@MichaelLNorth
Great places to start
Childexport default Ember.Component.extend({
didInsertElement() { this.nearestWithProperty('_wgeCard') .registerWgeAction(this); },
willDestroyElement() { this.nearestWithProperty('_wgeCard') .unregisterWgeAction(this); },
render() {} // Don't render});
const { computed: {alias, empty} } = Ember;
export default Ember.Component.extend({ classNames: ['card'], _wgeCard: true, _cardActions: [],
// Needed to ensure context of // child component templates // is the controller parentController: alias('targetObject'),
registerWgeAction(component) { this.get('_cardActions') .addObject(component); }, unregisterWgeAction(component) { this.get('_cardActions') .removeObject(component); }});
Parent
Components
@MichaelLNorth
Great places to start
import Ember from 'ember';
const { computed: {alias, empty} } = Ember;
export default Ember.Component.extend({ classNames: ['card'], _wgeCard: true, _cardActions: [],
// Needed to ensure context of // child component templates // is the controller parentController: alias('targetObject'),
registerWgeAction(component) { this.get('_cardActions') .addObject(component); },
unregisterWgeAction(component) { this.get('_cardActions') .removeObject(component); }});
Parent
<div class="card-content white-text"> <div class="card-title">{{title}}</div> {{yield}}</div>
{{#if _cardActions.length}} <div class="card-action"> {{#each _cardActions as |cardAction|}} {{view Ember.View tagName='a' template=cardAction.template controller=parentController}} {{/each}} </div>{{/if}}
Components
Parent.hbs
@MichaelLNorth
Great places to startComponents
<div class="card-content white-text"> <div class="card-title">{{title}}</div> {{yield}}</div>
{{#if _cardActions.length}} <div class="card-action"> {{#each _cardActions as |cardAction|}} {{view Ember.View tagName='a' template=cardAction.template controller=parentController}} {{/each}} </div>{{/if}}
“Captured” template
Needed for actions & bindings
@MichaelLNorth
Great places to start
Style
Tests
Computed Properties
Components
@MichaelLNorth
Great places to startTests
A lot of tests are verbose and ugly
• Sensitivity to order and/or timing • Brittle selectors to interact with the DOM • 1 change —> break N tests
@MichaelLNorth
Great places to startTests
test(“authorized user should end up at account's search list page”, function(assert) { server.get(`${apiHost.url}/me`, json(200, me)); server.get(`${apiHost.url}/campaigns/:id`, json(200, campaign1)); server.get(`${apiHost.url}/seats/2`, json(200, seat2)); server.get(`${apiHost.url}/seats/1`, json(200, seat1)); server.get(`${apiHost.url}/account/:id`, json(200, account1)); server.get(`${apiHost.url}/breadcrumbs`, json(200, breadcrumbs.campaign));
visit('/app/account/1/campaigns');
andThen(function() { assert.equal(currentPath(), ‘app.account.campaign', 'Current url is search list page for campaign 1'); assert.equal(Ember.$('.top-navbar .brand-logo .active-title').text().trim(), campaign1.campaign.name, 'Campaign name header is on the page'); assert.equal(Ember.$('.resource-tiles-container .card').length, campaigns.campaigns.length, 'One row in the table per search'); assert.deepEqual(Ember.$('.resource-tiles-container .resource-tile:first-child .card .card-content-row .card-content-row-label').toArray().map(e => Ember.$(e).text()), ['Created', 'Updated'], 'Columns are correct'); assert.equal(Ember.$('.new-campaign-button').length, 1, 'New Campaign button is on the screen'); });});
A lot of tests are verbose and ugly
@MichaelLNorth
Great places to startTests
A wild PageObject appears
• Prime pretender • Access to controls • Specific asserts
fillInclick
currentURL
andThenvisit
triggerEvent
$().val()$
$().click()$().trigger()
setFirstName
clickResetButton setAge
openSettings
PageObject API
Ember Testing API
DOMSee:
@MichaelLNorth
Great places to startTests
Writing PageObjects
• Return this • To assert or not to assert? • Build PageObjects for components
ExampleThe recombinant part!
@MichaelLNorth
Great places to startSome final thoughts
Even more composability on the way!
• Add-ons • Ember.Service • Engines (TBD) • Components ( {{yield}}, block params,
etc…)
@MichaelLNorth
Conclusion
Style
Tests
Computed Properties
Components
truenorth/wge-examples