20160908 Aesthetic-Driven Development

41
Aesthetics-Driven Development: Cool Features for Ergonomic Software Design Boston Ember.js September 8, 2016 Stephen Vance 1

Transcript of 20160908 Aesthetic-Driven Development

Aesthetics-Driven Development: Cool Features for Ergonomic

Software DesignBoston 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 NavbarFull 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>

NavbarHeader

Toggle

Content

Nav

Brand

4

Navbar StructureNavbar

Header

Toggle

Content

5

Navbar StructureNavbar

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 Up6

Applying DDAU*Navbar

Header

Toggle

Content

1. Toggles State

2. Passes State

*Data Down, Actions Up6

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

PlumbingMysteriousMysterious

MysteriousPlumbing

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

PlumbingMysteriousMysterious

MysteriousPlumbing

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 Templatebs-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 Stylebs-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 Stylebs-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 Stylebs-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 Stylebs-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

[email protected] @StephenRVance

srvance on GitHub and LinkedIn

25