Aesthetics-Driven Development:
Cool Features for Ergonomic
Software Design
Boston Ember.js
September 8, 2016
Stephen Vance
1
Motivation
• Using Bootstrap
• Relying on responsive behavior of navbars
• Some of the behavior relies on JavaScript
• Wanted more Ember-y approach
• ember-bootstrap didn’t support it yet
2
The Navbar
Full Rendering
Responsive Rendering
3
Bootstrap Navbar
<nav class="navbar navbar-default" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class=“navbar-toggle"
data-toggle="collapse" data-target=".navbar-mwpc-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
{{#link-to "index" class="navbar-brand"}}{{siteBrand.brand}}{{/link-to}}
</div>
<div class="collapse navbar-collapse navbar-mwpc-collapse">
<ul class="nav navbar-nav">
<li class="dropdown">
<a href="#" class=“dropdown-toggle"
data-toggle="dropdown" role="button" aria-expanded="false">
Service Directory<span class="caret"></span>
</a>
<ul class="dropdown-menu" role="menu">
{{#each-in serviceCategories.categories as | category info |}}
<li>
{{link-to info.displayName "service" category}}
</li>
{{/each-in}}
</ul>
</li>
<li>
{{#link-to "resources"}}Local Resources{{/link-to}}
</li>
4
Bootstrap Navbar
<nav class="navbar navbar-default" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class=“navbar-toggle"
data-toggle="collapse" data-target=".navbar-mwpc-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
{{#link-to "index" class="navbar-brand"}}{{siteBrand.brand}}{{/link-to}}
</div>
<div class="collapse navbar-collapse navbar-mwpc-collapse">
<ul class="nav navbar-nav">
<li class="dropdown">
<a href="#" class=“dropdown-toggle"
data-toggle="dropdown" role="button" aria-expanded="false">
Service Directory<span class="caret"></span>
</a>
<ul class="dropdown-menu" role="menu">
{{#each-in serviceCategories.categories as | category info |}}
<li>
{{link-to info.displayName "service" category}}
</li>
{{/each-in}}
</ul>
</li>
<li>
{{#link-to "resources"}}Local Resources{{/link-to}}
</li>
Navbar
Header
Toggle
Content
Nav
Brand
4
Navbar Structure
Navbar
Header
Toggle
Content
5
Navbar Structure
Navbar
Header
Toggle
Content
Toggles
5
Applying DDAU*
Navbar
Header
Toggle
Content
*Data Down, Actions Up
6
Applying DDAU*
Navbar
Header
Toggle
Content
1. Toggles State
*Data Down, Actions Up
6
Applying DDAU*
Navbar
Header
Toggle
Content
1. Toggles State
2. Passes State
*Data Down, Actions Up
6
Design Concerns
• Peer components shouldn’t reference each other
• Navbar state should be within the component
• Outside would require users to define it
• Nearest common parent
• Support multiple navbars in a page
• Strive for clean DSL
• Minimize exposed plumbing
• Principle of Least Astonishment
7
Implementation Concerns
• Component block form
• Nature of problem doesn’t require inline
• Component isolation makes it harder for related
components to cooperate transparently
8
First Cut 😳
{{#bs-navbar}}
{{#bs-navbar-header}}
{{!-- TODO: Create bs-navbar-toggle? --}}
{{#bs-button toggle=true active=expanded action=toggle class="navbar-toggle collapsed"}}
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
{{/bs-button}}
{{!-- TODO
{{#bs-navbar-brand}}Brand{{/bs-navbar-brand}}
--}}
<a class="navbar-brand" href="#">Brand</a>
{{/bs-navbar-header}}
{{!-- TODO: Create bs-navbar-content instead of using bs-collapse --}}
{{#bs-collapse collapse=collapsed
class=(if expanded "collapse navbar-collapse in" "collapse navbar-collapse")}}
{{#bs-nav type=type.id justified=justified stacked=stacked navbar=true}}
{{#bs-nav-item}}{{#link-to "alert"}}Alert{{/link-to}}{{/bs-nav-item}}
{{#bs-nav-item}}{{#link-to "button"}}Buttons{{/link-to}}{{/bs-nav-item}}
{{#bs-nav-item}}{{#link-to "dropdown"}}Dropdown{{/link-to}}{{/bs-nav-item}}
{{#bs-nav-item}}{{#link-to "forms"}}Forms{{/link-to}}{{/bs-nav-item}}
{{#bs-nav-item}}{{#link-to "accordion"}}Accordion{{/link-to}}{{/bs-nav-item}}
{{#bs-nav-item}}{{#link-to "collapse"}}Collapse{{/link-to}}{{/bs-nav-item}}
{{#bs-nav-item}}{{#link-to "modal"}}Modals{{/link-to}}{{/bs-nav-item}}
{{#bs-nav-item}}{{#link-to "progress"}}Progress bars{{/link-to}}{{/bs-nav-item}}
{{#bs-nav-item}}{{#link-to "navs"}}Navs{{/link-to}}{{/bs-nav-item}}
{{#bs-nav-item}}{{#link-to "navbars"}}Navbars{{/link-to}}{{/bs-nav-item}}
{{/bs-nav}}
{{/bs-collapse}}
{{/bs-navbar}}
9
First Cut 😳
{{#bs-navbar}}
{{#bs-navbar-header}}
{{!-- TODO: Create bs-navbar-toggle? --}}
{{#bs-button toggle=true active=expanded action=toggle class="navbar-toggle collapsed"}}
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
{{/bs-button}}
{{!-- TODO
{{#bs-navbar-brand}}Brand{{/bs-navbar-brand}}
--}}
<a class="navbar-brand" href="#">Brand</a>
{{/bs-navbar-header}}
{{!-- TODO: Create bs-navbar-content instead of using bs-collapse --}}
{{#bs-collapse collapse=collapsed
class=(if expanded "collapse navbar-collapse in" "collapse navbar-collapse")}}
{{#bs-nav type=type.id justified=justified stacked=stacked navbar=true}}
{{#bs-nav-item}}{{#link-to "alert"}}Alert{{/link-to}}{{/bs-nav-item}}
{{#bs-nav-item}}{{#link-to "button"}}Buttons{{/link-to}}{{/bs-nav-item}}
{{#bs-nav-item}}{{#link-to "dropdown"}}Dropdown{{/link-to}}{{/bs-nav-item}}
{{#bs-nav-item}}{{#link-to "forms"}}Forms{{/link-to}}{{/bs-nav-item}}
{{#bs-nav-item}}{{#link-to "accordion"}}Accordion{{/link-to}}{{/bs-nav-item}}
{{#bs-nav-item}}{{#link-to "collapse"}}Collapse{{/link-to}}{{/bs-nav-item}}
{{#bs-nav-item}}{{#link-to "modal"}}Modals{{/link-to}}{{/bs-nav-item}}
{{#bs-nav-item}}{{#link-to "progress"}}Progress bars{{/link-to}}{{/bs-nav-item}}
{{#bs-nav-item}}{{#link-to "navs"}}Navs{{/link-to}}{{/bs-nav-item}}
{{#bs-nav-item}}{{#link-to "navbars"}}Navbars{{/link-to}}{{/bs-nav-item}}
{{/bs-nav}}
{{/bs-collapse}}
{{/bs-navbar}}
Plumbing
Plumbing
Plumbing
Plumbing
9
First Cut 😳
{{#bs-navbar}}
{{#bs-navbar-header}}
{{!-- TODO: Create bs-navbar-toggle? --}}
{{#bs-button toggle=true active=expanded action=toggle class="navbar-toggle collapsed"}}
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
{{/bs-button}}
{{!-- TODO
{{#bs-navbar-brand}}Brand{{/bs-navbar-brand}}
--}}
<a class="navbar-brand" href="#">Brand</a>
{{/bs-navbar-header}}
{{!-- TODO: Create bs-navbar-content instead of using bs-collapse --}}
{{#bs-collapse collapse=collapsed
class=(if expanded "collapse navbar-collapse in" "collapse navbar-collapse")}}
{{#bs-nav type=type.id justified=justified stacked=stacked navbar=true}}
{{#bs-nav-item}}{{#link-to "alert"}}Alert{{/link-to}}{{/bs-nav-item}}
{{#bs-nav-item}}{{#link-to "button"}}Buttons{{/link-to}}{{/bs-nav-item}}
{{#bs-nav-item}}{{#link-to "dropdown"}}Dropdown{{/link-to}}{{/bs-nav-item}}
{{#bs-nav-item}}{{#link-to "forms"}}Forms{{/link-to}}{{/bs-nav-item}}
{{#bs-nav-item}}{{#link-to "accordion"}}Accordion{{/link-to}}{{/bs-nav-item}}
{{#bs-nav-item}}{{#link-to "collapse"}}Collapse{{/link-to}}{{/bs-nav-item}}
{{#bs-nav-item}}{{#link-to "modal"}}Modals{{/link-to}}{{/bs-nav-item}}
{{#bs-nav-item}}{{#link-to "progress"}}Progress bars{{/link-to}}{{/bs-nav-item}}
{{#bs-nav-item}}{{#link-to "navs"}}Navs{{/link-to}}{{/bs-nav-item}}
{{#bs-nav-item}}{{#link-to "navbars"}}Navbars{{/link-to}}{{/bs-nav-item}}
{{/bs-nav}}
{{/bs-collapse}}
{{/bs-navbar}}
Plumbing
Plumbing
MysteriousMysterious
Mysterious
Plumbing
Plumbing
9
First Cut 😳
{{#bs-navbar}}
{{#bs-navbar-header}}
{{!-- TODO: Create bs-navbar-toggle? --}}
{{#bs-button toggle=true active=expanded action=toggle class="navbar-toggle collapsed"}}
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
{{/bs-button}}
{{!-- TODO
{{#bs-navbar-brand}}Brand{{/bs-navbar-brand}}
--}}
<a class="navbar-brand" href="#">Brand</a>
{{/bs-navbar-header}}
{{!-- TODO: Create bs-navbar-content instead of using bs-collapse --}}
{{#bs-collapse collapse=collapsed
class=(if expanded "collapse navbar-collapse in" "collapse navbar-collapse")}}
{{#bs-nav type=type.id justified=justified stacked=stacked navbar=true}}
{{#bs-nav-item}}{{#link-to "alert"}}Alert{{/link-to}}{{/bs-nav-item}}
{{#bs-nav-item}}{{#link-to "button"}}Buttons{{/link-to}}{{/bs-nav-item}}
{{#bs-nav-item}}{{#link-to "dropdown"}}Dropdown{{/link-to}}{{/bs-nav-item}}
{{#bs-nav-item}}{{#link-to "forms"}}Forms{{/link-to}}{{/bs-nav-item}}
{{#bs-nav-item}}{{#link-to "accordion"}}Accordion{{/link-to}}{{/bs-nav-item}}
{{#bs-nav-item}}{{#link-to "collapse"}}Collapse{{/link-to}}{{/bs-nav-item}}
{{#bs-nav-item}}{{#link-to "modal"}}Modals{{/link-to}}{{/bs-nav-item}}
{{#bs-nav-item}}{{#link-to "progress"}}Progress bars{{/link-to}}{{/bs-nav-item}}
{{#bs-nav-item}}{{#link-to "navs"}}Navs{{/link-to}}{{/bs-nav-item}}
{{#bs-nav-item}}{{#link-to "navbars"}}Navbars{{/link-to}}{{/bs-nav-item}}
{{/bs-nav}}
{{/bs-collapse}}
{{/bs-navbar}}
Plumbing
Plumbing
MysteriousMysterious
Mysterious
Plumbing
Plumbing
Typo
9
Issues
• Didn’t really work
• Explicit class manipulation circumvented
transitions
• Properties weren’t where I thought they were
10
First Really Working Version
{{#bs-navbar as |toggleNavbar navbarCollapsed|}}
{{#bs-navbar-header}}
{{#bs-navbar-toggle action=toggleNavbar}}
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
{{/bs-navbar-toggle}}
<a class="navbar-brand" href="#">Brand</a>
{{/bs-navbar-header}}
{{#bs-collapse collapsed=navbarCollapsed
class=“navbar-collapse"}}
...
bs-navbar.hbs
{{yield (action 'toggleNavbar') navbarCollapse}}
11
First Really Working Version
{{#bs-navbar as |toggleNavbar navbarCollapsed|}}
{{#bs-navbar-header}}
{{#bs-navbar-toggle action=toggleNavbar}}
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
{{/bs-navbar-toggle}}
<a class="navbar-brand" href="#">Brand</a>
{{/bs-navbar-header}}
{{#bs-collapse collapsed=navbarCollapsed
class=“navbar-collapse"}}
...
Plumbing
Plumbing
Plumbing
bs-navbar.hbs
{{yield (action 'toggleNavbar') navbarCollapse}}
11
Component Visibility
• A component’s properties are only visible in its
template
• Component block params are visible in the
template that is yielded to
• The same is true for action function references
12
Action Calling
13
Action Calling
{{bs-navbar-toggle action="toggleNavbar"}}
By default it calls the action on the component then passes it to the
controller if unhandled
13
Action Calling
{{bs-navbar-toggle action="toggleNavbar"}}
By default it calls the action on the component then passes it to the
controller if unhandled
{{bs-navbar-toggle action=toggleNavbar}}
Without quotes, it can call an action function passed to it from a higher
level, but …
13
Action Calling
{{bs-navbar-toggle action="toggleNavbar"}}
By default it calls the action on the component then passes it to the
controller if unhandled
{{bs-navbar-toggle action=toggleNavbar}}
Without quotes, it can call an action function passed to it from a higher
level, but …
{{#bs-navbar as | toggleNavbar | }}
It must be exposed as a component block param or …
13
Action Calling
{{bs-navbar-toggle action="toggleNavbar"}}
By default it calls the action on the component then passes it to the
controller if unhandled
{{bs-navbar-toggle action=toggleNavbar}}
Without quotes, it can call an action function passed to it from a higher
level, but …
{{#bs-navbar as | toggleNavbar | }}
It must be exposed as a component block param or …
{{yield toggleNavbar=(action "toggleNavbar")}}
yielded through the template
13
Action Calling
{{bs-navbar-toggle action="toggleNavbar"}}
By default it calls the action on the component then passes it to the
controller if unhandled
{{bs-navbar-toggle action=toggleNavbar}}
Without quotes, it can call an action function passed to it from a higher
level, but …
{{#bs-navbar as | toggleNavbar | }}
It must be exposed as a component block param or …
{{yield toggleNavbar=(action "toggleNavbar")}}
yielded through the template
Prefer Closure Actions!
13
Improving Ergonomics
{{#bs-navbar as |navbar|}}
{{#bs-navbar-header}}
{{#bs-navbar-toggle action=navbar.toggle}}
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
{{/bs-navbar-toggle}}
<a class="navbar-brand" href="#">Brand</a>
{{/bs-navbar-header}}
{{#bs-collapse collapsed=navbar.collapsed
class="navbar-collapse"}}
...
bs-navbar.hbs{{yield (hash
collapsed=navbarCollapsed
toggle=(action 'toggleNavbar'))}}
14
Can We Do Better?
• Why do these things need to be exposed at all?
• And while we’re at it, is there a better way to show
that the various components are really more closely
related?
15
Enter Contextual Components
{{#bs-navbar as |navbar|}}
{{#navbar.header}}
{{#navbar.toggle}}
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
{{/navbar.toggle}}
{{#navbar.brand}}Brand{{/navbar.brand}}
{{/navbar.header}}
{{#navbar.content}}
{{#navbar.nav}}
...
16
The Template
bs-navbar.hbs{{yield (hash collapsed=collapsed
header=(component 'bs-navbar-header')
toggle=(component ‘bs-navbar-toggle'
action=(action 'toggleNavbar'))
brand=(component 'bs-navbar-brand')
content=(component ‘bs-navbar-content'
collapsed=collapsed)
nav=(component 'bs-navbar-nav')
)
}}
17
Testing Contextual Components
• Integration testing
• Do you test all of the components through the parent?
• Do you just test the presence of the contextual
components? Easily done by referencing them.
• How far should you go to verify the currying is done
properly?
• To what extent should you test the aggregate
behavior?
• Do you need an acceptance test?
18
Victory!
😀
19
Victory!
But at a cost
This is an addon requiring compatibility to 1.13
Contextual components and hash were added in 2.3
😀😞
19
Final Form, But How?
{{#bs-navbar}}

<div class="navbar-header">

{{#bs-navbar-toggle}}

<span class="sr-only">Toggle navigation</span>

<span class="icon-bar"></span>

<span class="icon-bar"></span>

<span class="icon-bar"></span>

{{/bs-navbar-toggle}}

<a class="navbar-brand" href="#">Brand</a>

</div>

{{#bs-navbar-content}}

{{#bs-navbar-nav}}
...
20
Black Magic, Toggle Style
bs-navbar-toggle.jsimport Ember from 'ember';
import BsButtonComponent from 'ember-bootstrap/components/bs-button';
import NavbarComponent from 'ember-bootstrap/components/bs-navbar';
export default BsButtonComponent.extend({
...
targetObject: Ember.computed(function() {
return this.nearestOfType(NavbarComponent);
}),
action: 'toggleNavbar',
actions: {
toggleNavbar() {
this.sendAction();
}
}
});
21
Black Magic, Toggle Style
bs-navbar-toggle.jsimport Ember from 'ember';
import BsButtonComponent from 'ember-bootstrap/components/bs-button';
import NavbarComponent from 'ember-bootstrap/components/bs-navbar';
export default BsButtonComponent.extend({
...
targetObject: Ember.computed(function() {
return this.nearestOfType(NavbarComponent);
}),
action: 'toggleNavbar',
actions: {
toggleNavbar() {
this.sendAction();
}
}
});
21
Private!
Black Magic, Content Style
bs-navbar-content.jsimport Ember from 'ember';
import BsCollapseComponent from 'ember-bootstrap/components/bs-collapse';
import NavbarComponent from 'ember-bootstrap/components/bs-navbar';
export default BsCollapseComponent.extend({
navbar: Ember.computed(function() {
return this.nearestOfType(NavbarComponent);
}),
collapsed: Ember.computed.reads('navbar.collapsed')
});
22
Black Magic, Content Style
bs-navbar-content.jsimport Ember from 'ember';
import BsCollapseComponent from 'ember-bootstrap/components/bs-collapse';
import NavbarComponent from 'ember-bootstrap/components/bs-navbar';
export default BsCollapseComponent.extend({
navbar: Ember.computed(function() {
return this.nearestOfType(NavbarComponent);
}),
collapsed: Ember.computed.reads('navbar.collapsed')
});
22
Private!
Wrapping Up
• Think about how it will be used and how
newcomers will perceive it
• Shoot for elegance, aesthetics, and ergonomics
• Use the latest cool features in the service of design
and ergonomics, not for their own sake
• Remember not everyone’s on the cutting edge
23
Resources
• Ember Guides for Components
• https://guides.emberjs.com/v2.7.0/components/passing-properties-to-a-component/
• https://guides.emberjs.com/v2.7.0/components/wrapping-content-in-a-component/
• https://guides.emberjs.com/v2.7.0/components/block-params/
• Eric Kelly’s (@HeroicEric) Boston Ember.js Talk
• https://speakerdeck.com/heroiceric/contextual-components
• https://youtu.be/Au3rHHuEZNI?t=1h6m42s
• Some Twiddles
• Component Property Scope: https://ember-twiddle.com/
332c58bba5a2d0ac8874dd834e28ac06
• Action Calling: https://ember-twiddle.com/c9ba29e3c2f98937d4d0c8e493261a78
24
Contact Me
Stephen Vance
http://www.vance.com
steve@vance.com
@StephenRVance
srvance on GitHub and LinkedIn
25

20160908 Aesthetic-Driven Development

  • 1.
    Aesthetics-Driven Development: Cool Featuresfor Ergonomic Software Design Boston Ember.js September 8, 2016 Stephen Vance 1
  • 2.
    Motivation • Using Bootstrap •Relying on responsive behavior of navbars • Some of the behavior relies on JavaScript • Wanted more Ember-y approach • ember-bootstrap didn’t support it yet 2
  • 3.
  • 4.
    Bootstrap Navbar <nav class="navbarnavbar-default" role="navigation"> <div class="container-fluid"> <div class="navbar-header"> <button type="button" class=“navbar-toggle" data-toggle="collapse" data-target=".navbar-mwpc-collapse"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> {{#link-to "index" class="navbar-brand"}}{{siteBrand.brand}}{{/link-to}} </div> <div class="collapse navbar-collapse navbar-mwpc-collapse"> <ul class="nav navbar-nav"> <li class="dropdown"> <a href="#" class=“dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"> Service Directory<span class="caret"></span> </a> <ul class="dropdown-menu" role="menu"> {{#each-in serviceCategories.categories as | category info |}} <li> {{link-to info.displayName "service" category}} </li> {{/each-in}} </ul> </li> <li> {{#link-to "resources"}}Local Resources{{/link-to}} </li> 4
  • 5.
    Bootstrap Navbar <nav class="navbarnavbar-default" role="navigation"> <div class="container-fluid"> <div class="navbar-header"> <button type="button" class=“navbar-toggle" data-toggle="collapse" data-target=".navbar-mwpc-collapse"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> {{#link-to "index" class="navbar-brand"}}{{siteBrand.brand}}{{/link-to}} </div> <div class="collapse navbar-collapse navbar-mwpc-collapse"> <ul class="nav navbar-nav"> <li class="dropdown"> <a href="#" class=“dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"> Service Directory<span class="caret"></span> </a> <ul class="dropdown-menu" role="menu"> {{#each-in serviceCategories.categories as | category info |}} <li> {{link-to info.displayName "service" category}} </li> {{/each-in}} </ul> </li> <li> {{#link-to "resources"}}Local Resources{{/link-to}} </li> Navbar Header Toggle Content Nav Brand 4
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
    Applying DDAU* Navbar Header Toggle Content 1. TogglesState 2. Passes State *Data Down, Actions Up 6
  • 11.
    Design Concerns • Peercomponents shouldn’t reference each other • Navbar state should be within the component • Outside would require users to define it • Nearest common parent • Support multiple navbars in a page • Strive for clean DSL • Minimize exposed plumbing • Principle of Least Astonishment 7
  • 12.
    Implementation Concerns • Componentblock form • Nature of problem doesn’t require inline • Component isolation makes it harder for related components to cooperate transparently 8
  • 13.
    First Cut 😳 {{#bs-navbar}} {{#bs-navbar-header}} {{!--TODO: Create bs-navbar-toggle? --}} {{#bs-button toggle=true active=expanded action=toggle class="navbar-toggle collapsed"}} <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> {{/bs-button}} {{!-- TODO {{#bs-navbar-brand}}Brand{{/bs-navbar-brand}} --}} <a class="navbar-brand" href="#">Brand</a> {{/bs-navbar-header}} {{!-- TODO: Create bs-navbar-content instead of using bs-collapse --}} {{#bs-collapse collapse=collapsed class=(if expanded "collapse navbar-collapse in" "collapse navbar-collapse")}} {{#bs-nav type=type.id justified=justified stacked=stacked navbar=true}} {{#bs-nav-item}}{{#link-to "alert"}}Alert{{/link-to}}{{/bs-nav-item}} {{#bs-nav-item}}{{#link-to "button"}}Buttons{{/link-to}}{{/bs-nav-item}} {{#bs-nav-item}}{{#link-to "dropdown"}}Dropdown{{/link-to}}{{/bs-nav-item}} {{#bs-nav-item}}{{#link-to "forms"}}Forms{{/link-to}}{{/bs-nav-item}} {{#bs-nav-item}}{{#link-to "accordion"}}Accordion{{/link-to}}{{/bs-nav-item}} {{#bs-nav-item}}{{#link-to "collapse"}}Collapse{{/link-to}}{{/bs-nav-item}} {{#bs-nav-item}}{{#link-to "modal"}}Modals{{/link-to}}{{/bs-nav-item}} {{#bs-nav-item}}{{#link-to "progress"}}Progress bars{{/link-to}}{{/bs-nav-item}} {{#bs-nav-item}}{{#link-to "navs"}}Navs{{/link-to}}{{/bs-nav-item}} {{#bs-nav-item}}{{#link-to "navbars"}}Navbars{{/link-to}}{{/bs-nav-item}} {{/bs-nav}} {{/bs-collapse}} {{/bs-navbar}} 9
  • 14.
    First Cut 😳 {{#bs-navbar}} {{#bs-navbar-header}} {{!--TODO: Create bs-navbar-toggle? --}} {{#bs-button toggle=true active=expanded action=toggle class="navbar-toggle collapsed"}} <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> {{/bs-button}} {{!-- TODO {{#bs-navbar-brand}}Brand{{/bs-navbar-brand}} --}} <a class="navbar-brand" href="#">Brand</a> {{/bs-navbar-header}} {{!-- TODO: Create bs-navbar-content instead of using bs-collapse --}} {{#bs-collapse collapse=collapsed class=(if expanded "collapse navbar-collapse in" "collapse navbar-collapse")}} {{#bs-nav type=type.id justified=justified stacked=stacked navbar=true}} {{#bs-nav-item}}{{#link-to "alert"}}Alert{{/link-to}}{{/bs-nav-item}} {{#bs-nav-item}}{{#link-to "button"}}Buttons{{/link-to}}{{/bs-nav-item}} {{#bs-nav-item}}{{#link-to "dropdown"}}Dropdown{{/link-to}}{{/bs-nav-item}} {{#bs-nav-item}}{{#link-to "forms"}}Forms{{/link-to}}{{/bs-nav-item}} {{#bs-nav-item}}{{#link-to "accordion"}}Accordion{{/link-to}}{{/bs-nav-item}} {{#bs-nav-item}}{{#link-to "collapse"}}Collapse{{/link-to}}{{/bs-nav-item}} {{#bs-nav-item}}{{#link-to "modal"}}Modals{{/link-to}}{{/bs-nav-item}} {{#bs-nav-item}}{{#link-to "progress"}}Progress bars{{/link-to}}{{/bs-nav-item}} {{#bs-nav-item}}{{#link-to "navs"}}Navs{{/link-to}}{{/bs-nav-item}} {{#bs-nav-item}}{{#link-to "navbars"}}Navbars{{/link-to}}{{/bs-nav-item}} {{/bs-nav}} {{/bs-collapse}} {{/bs-navbar}} Plumbing Plumbing Plumbing Plumbing 9
  • 15.
    First Cut 😳 {{#bs-navbar}} {{#bs-navbar-header}} {{!--TODO: Create bs-navbar-toggle? --}} {{#bs-button toggle=true active=expanded action=toggle class="navbar-toggle collapsed"}} <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> {{/bs-button}} {{!-- TODO {{#bs-navbar-brand}}Brand{{/bs-navbar-brand}} --}} <a class="navbar-brand" href="#">Brand</a> {{/bs-navbar-header}} {{!-- TODO: Create bs-navbar-content instead of using bs-collapse --}} {{#bs-collapse collapse=collapsed class=(if expanded "collapse navbar-collapse in" "collapse navbar-collapse")}} {{#bs-nav type=type.id justified=justified stacked=stacked navbar=true}} {{#bs-nav-item}}{{#link-to "alert"}}Alert{{/link-to}}{{/bs-nav-item}} {{#bs-nav-item}}{{#link-to "button"}}Buttons{{/link-to}}{{/bs-nav-item}} {{#bs-nav-item}}{{#link-to "dropdown"}}Dropdown{{/link-to}}{{/bs-nav-item}} {{#bs-nav-item}}{{#link-to "forms"}}Forms{{/link-to}}{{/bs-nav-item}} {{#bs-nav-item}}{{#link-to "accordion"}}Accordion{{/link-to}}{{/bs-nav-item}} {{#bs-nav-item}}{{#link-to "collapse"}}Collapse{{/link-to}}{{/bs-nav-item}} {{#bs-nav-item}}{{#link-to "modal"}}Modals{{/link-to}}{{/bs-nav-item}} {{#bs-nav-item}}{{#link-to "progress"}}Progress bars{{/link-to}}{{/bs-nav-item}} {{#bs-nav-item}}{{#link-to "navs"}}Navs{{/link-to}}{{/bs-nav-item}} {{#bs-nav-item}}{{#link-to "navbars"}}Navbars{{/link-to}}{{/bs-nav-item}} {{/bs-nav}} {{/bs-collapse}} {{/bs-navbar}} Plumbing Plumbing MysteriousMysterious Mysterious Plumbing Plumbing 9
  • 16.
    First Cut 😳 {{#bs-navbar}} {{#bs-navbar-header}} {{!--TODO: Create bs-navbar-toggle? --}} {{#bs-button toggle=true active=expanded action=toggle class="navbar-toggle collapsed"}} <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> {{/bs-button}} {{!-- TODO {{#bs-navbar-brand}}Brand{{/bs-navbar-brand}} --}} <a class="navbar-brand" href="#">Brand</a> {{/bs-navbar-header}} {{!-- TODO: Create bs-navbar-content instead of using bs-collapse --}} {{#bs-collapse collapse=collapsed class=(if expanded "collapse navbar-collapse in" "collapse navbar-collapse")}} {{#bs-nav type=type.id justified=justified stacked=stacked navbar=true}} {{#bs-nav-item}}{{#link-to "alert"}}Alert{{/link-to}}{{/bs-nav-item}} {{#bs-nav-item}}{{#link-to "button"}}Buttons{{/link-to}}{{/bs-nav-item}} {{#bs-nav-item}}{{#link-to "dropdown"}}Dropdown{{/link-to}}{{/bs-nav-item}} {{#bs-nav-item}}{{#link-to "forms"}}Forms{{/link-to}}{{/bs-nav-item}} {{#bs-nav-item}}{{#link-to "accordion"}}Accordion{{/link-to}}{{/bs-nav-item}} {{#bs-nav-item}}{{#link-to "collapse"}}Collapse{{/link-to}}{{/bs-nav-item}} {{#bs-nav-item}}{{#link-to "modal"}}Modals{{/link-to}}{{/bs-nav-item}} {{#bs-nav-item}}{{#link-to "progress"}}Progress bars{{/link-to}}{{/bs-nav-item}} {{#bs-nav-item}}{{#link-to "navs"}}Navs{{/link-to}}{{/bs-nav-item}} {{#bs-nav-item}}{{#link-to "navbars"}}Navbars{{/link-to}}{{/bs-nav-item}} {{/bs-nav}} {{/bs-collapse}} {{/bs-navbar}} Plumbing Plumbing MysteriousMysterious Mysterious Plumbing Plumbing Typo 9
  • 17.
    Issues • Didn’t reallywork • Explicit class manipulation circumvented transitions • Properties weren’t where I thought they were 10
  • 18.
    First Really WorkingVersion {{#bs-navbar as |toggleNavbar navbarCollapsed|}} {{#bs-navbar-header}} {{#bs-navbar-toggle action=toggleNavbar}} <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> {{/bs-navbar-toggle}} <a class="navbar-brand" href="#">Brand</a> {{/bs-navbar-header}} {{#bs-collapse collapsed=navbarCollapsed class=“navbar-collapse"}} ... bs-navbar.hbs {{yield (action 'toggleNavbar') navbarCollapse}} 11
  • 19.
    First Really WorkingVersion {{#bs-navbar as |toggleNavbar navbarCollapsed|}} {{#bs-navbar-header}} {{#bs-navbar-toggle action=toggleNavbar}} <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> {{/bs-navbar-toggle}} <a class="navbar-brand" href="#">Brand</a> {{/bs-navbar-header}} {{#bs-collapse collapsed=navbarCollapsed class=“navbar-collapse"}} ... Plumbing Plumbing Plumbing bs-navbar.hbs {{yield (action 'toggleNavbar') navbarCollapse}} 11
  • 20.
    Component Visibility • Acomponent’s properties are only visible in its template • Component block params are visible in the template that is yielded to • The same is true for action function references 12
  • 21.
  • 22.
    Action Calling {{bs-navbar-toggle action="toggleNavbar"}} Bydefault it calls the action on the component then passes it to the controller if unhandled 13
  • 23.
    Action Calling {{bs-navbar-toggle action="toggleNavbar"}} Bydefault it calls the action on the component then passes it to the controller if unhandled {{bs-navbar-toggle action=toggleNavbar}} Without quotes, it can call an action function passed to it from a higher level, but … 13
  • 24.
    Action Calling {{bs-navbar-toggle action="toggleNavbar"}} Bydefault it calls the action on the component then passes it to the controller if unhandled {{bs-navbar-toggle action=toggleNavbar}} Without quotes, it can call an action function passed to it from a higher level, but … {{#bs-navbar as | toggleNavbar | }} It must be exposed as a component block param or … 13
  • 25.
    Action Calling {{bs-navbar-toggle action="toggleNavbar"}} Bydefault it calls the action on the component then passes it to the controller if unhandled {{bs-navbar-toggle action=toggleNavbar}} Without quotes, it can call an action function passed to it from a higher level, but … {{#bs-navbar as | toggleNavbar | }} It must be exposed as a component block param or … {{yield toggleNavbar=(action "toggleNavbar")}} yielded through the template 13
  • 26.
    Action Calling {{bs-navbar-toggle action="toggleNavbar"}} Bydefault it calls the action on the component then passes it to the controller if unhandled {{bs-navbar-toggle action=toggleNavbar}} Without quotes, it can call an action function passed to it from a higher level, but … {{#bs-navbar as | toggleNavbar | }} It must be exposed as a component block param or … {{yield toggleNavbar=(action "toggleNavbar")}} yielded through the template Prefer Closure Actions! 13
  • 27.
    Improving Ergonomics {{#bs-navbar as|navbar|}} {{#bs-navbar-header}} {{#bs-navbar-toggle action=navbar.toggle}} <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> {{/bs-navbar-toggle}} <a class="navbar-brand" href="#">Brand</a> {{/bs-navbar-header}} {{#bs-collapse collapsed=navbar.collapsed class="navbar-collapse"}} ... bs-navbar.hbs{{yield (hash collapsed=navbarCollapsed toggle=(action 'toggleNavbar'))}} 14
  • 28.
    Can We DoBetter? • Why do these things need to be exposed at all? • And while we’re at it, is there a better way to show that the various components are really more closely related? 15
  • 29.
    Enter Contextual Components {{#bs-navbaras |navbar|}} {{#navbar.header}} {{#navbar.toggle}} <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> {{/navbar.toggle}} {{#navbar.brand}}Brand{{/navbar.brand}} {{/navbar.header}} {{#navbar.content}} {{#navbar.nav}} ... 16
  • 30.
    The Template bs-navbar.hbs{{yield (hashcollapsed=collapsed header=(component 'bs-navbar-header') toggle=(component ‘bs-navbar-toggle' action=(action 'toggleNavbar')) brand=(component 'bs-navbar-brand') content=(component ‘bs-navbar-content' collapsed=collapsed) nav=(component 'bs-navbar-nav') ) }} 17
  • 31.
    Testing Contextual Components •Integration testing • Do you test all of the components through the parent? • Do you just test the presence of the contextual components? Easily done by referencing them. • How far should you go to verify the currying is done properly? • To what extent should you test the aggregate behavior? • Do you need an acceptance test? 18
  • 32.
  • 33.
    Victory! But at acost This is an addon requiring compatibility to 1.13 Contextual components and hash were added in 2.3 😀😞 19
  • 34.
    Final Form, ButHow? {{#bs-navbar}}
 <div class="navbar-header">
 {{#bs-navbar-toggle}}
 <span class="sr-only">Toggle navigation</span>
 <span class="icon-bar"></span>
 <span class="icon-bar"></span>
 <span class="icon-bar"></span>
 {{/bs-navbar-toggle}}
 <a class="navbar-brand" href="#">Brand</a>
 </div>
 {{#bs-navbar-content}}
 {{#bs-navbar-nav}} ... 20
  • 35.
    Black Magic, ToggleStyle bs-navbar-toggle.jsimport Ember from 'ember'; import BsButtonComponent from 'ember-bootstrap/components/bs-button'; import NavbarComponent from 'ember-bootstrap/components/bs-navbar'; export default BsButtonComponent.extend({ ... targetObject: Ember.computed(function() { return this.nearestOfType(NavbarComponent); }), action: 'toggleNavbar', actions: { toggleNavbar() { this.sendAction(); } } }); 21
  • 36.
    Black Magic, ToggleStyle bs-navbar-toggle.jsimport Ember from 'ember'; import BsButtonComponent from 'ember-bootstrap/components/bs-button'; import NavbarComponent from 'ember-bootstrap/components/bs-navbar'; export default BsButtonComponent.extend({ ... targetObject: Ember.computed(function() { return this.nearestOfType(NavbarComponent); }), action: 'toggleNavbar', actions: { toggleNavbar() { this.sendAction(); } } }); 21 Private!
  • 37.
    Black Magic, ContentStyle bs-navbar-content.jsimport Ember from 'ember'; import BsCollapseComponent from 'ember-bootstrap/components/bs-collapse'; import NavbarComponent from 'ember-bootstrap/components/bs-navbar'; export default BsCollapseComponent.extend({ navbar: Ember.computed(function() { return this.nearestOfType(NavbarComponent); }), collapsed: Ember.computed.reads('navbar.collapsed') }); 22
  • 38.
    Black Magic, ContentStyle bs-navbar-content.jsimport Ember from 'ember'; import BsCollapseComponent from 'ember-bootstrap/components/bs-collapse'; import NavbarComponent from 'ember-bootstrap/components/bs-navbar'; export default BsCollapseComponent.extend({ navbar: Ember.computed(function() { return this.nearestOfType(NavbarComponent); }), collapsed: Ember.computed.reads('navbar.collapsed') }); 22 Private!
  • 39.
    Wrapping Up • Thinkabout how it will be used and how newcomers will perceive it • Shoot for elegance, aesthetics, and ergonomics • Use the latest cool features in the service of design and ergonomics, not for their own sake • Remember not everyone’s on the cutting edge 23
  • 40.
    Resources • Ember Guidesfor Components • https://guides.emberjs.com/v2.7.0/components/passing-properties-to-a-component/ • https://guides.emberjs.com/v2.7.0/components/wrapping-content-in-a-component/ • https://guides.emberjs.com/v2.7.0/components/block-params/ • Eric Kelly’s (@HeroicEric) Boston Ember.js Talk • https://speakerdeck.com/heroiceric/contextual-components • https://youtu.be/Au3rHHuEZNI?t=1h6m42s • Some Twiddles • Component Property Scope: https://ember-twiddle.com/ 332c58bba5a2d0ac8874dd834e28ac06 • Action Calling: https://ember-twiddle.com/c9ba29e3c2f98937d4d0c8e493261a78 24
  • 41.