$30 off During Our Annual Pro Sale. View Details »

EmberConf 2015 – Ambitious UX for Ambitious Apps

EmberConf 2015 – Ambitious UX for Ambitious Apps

Presented at EmberConf 2015 by @sugarpirate_

Video: https://www.youtube.com/watch?v=TlU0m18Pr-Y

In the dark ages of web development, designing a beautiful user experience meant having to constantly fight with the DOM to get it to do what you want, when you want. With Ember, we no longer have to struggle with managing DOM state, and we are free to put the user experience first with reactive UI.

In this talk, we'll discuss the ways in which Ember makes it easy to build delightful and reactive user experiences, and how you can build reusable components that even non-technical designers can learn to use. Learn about the thoughtful touches and interactions you can add to an Ember app.

Links:

1. Computed property macro demo – http://emberjs.jsbin.com/vubaga/11/edit?js,output

2. Showerthoughts loading messages demo – http://emberjs.jsbin.com/lulaki/35/edit?js,output

3. Flash messages demo – http://emberjs.jsbin.com/ranewo/45/edit?js,output

4. Drag and drop demo – http://emberjs.jsbin.com/denep/18/edit?js,output

Lauren Tan

March 03, 2015
Tweet

More Decks by Lauren Tan

Other Decks in Programming

Transcript

  1. AMBITIOUS UX FOR
    AMBITIOUS APPS
    EMBERCONF 2015
    Lauren Elizabeth Tan
    @sugarpirate_ @poteto

    View Slide

  2. DESIGN DEV
    Lauren Elizabeth Tan
    Designer & Front End Developer

    View Slide

  3. View Slide

  4. DESIGN IS HOW IT WORKS
    GOOD DESIGN IS REACTIVE
    GOOD DESIGN IS PLAYFUL
    GOOD DESIGN IS INFORMATIVE
    GOOD DESIGN IS INTUITIVE

    View Slide

  5. BUT I’M NOT A DESIGNER

    View Slide

  6. “Most people make the mistake of thinking
    design is what it looks like. That’s not what we
    think design is. It’s not just what it looks like
    and feels like. Design is how it works.”

    View Slide

  7. applyConcatenatedProperties()
    giveDescriptorSuper()
    beginPropertyChanges()

    View Slide

  8. View Slide

  9. View Slide

  10. View Slide

  11. View Slide

  12. View Slide

  13. =

    View Slide

  14. View Slide

  15. What is good design?

    View Slide

  16. DESIGN IS HOW IT WORKS
    GOOD DESIGN IS REACTIVE
    GOOD DESIGN IS PLAYFUL
    GOOD DESIGN IS INFORMATIVE
    GOOD DESIGN IS INTUITIVE

    View Slide

  17. REACTIVE?

    View Slide

  18. View Slide

  19. FLOW OF DATA
    &
    MAINTAINING RELATIONSHIPS
    BETWEEN THAT DATA

    View Slide

  20. var EmberObject =
    CoreObject.extend(Observable);

    View Slide

  21. FUNCTIONAL REACTIVE
    PROGRAMMING?

    View Slide

  22. FUNCTIONAL REACTIVE
    PROGRAMMING?

    View Slide

  23. FUNCTIONAL REACTIVE PROGRAMMING?
    Immutability
    Some side effects

    View Slide

  24. EVENT STREAMS
    Things that consist of discrete events

    View Slide

  25. .asEventStream('click')
    https://gist.github.com/staltz/868e7e9bc2a7b8c1f754

    View Slide

  26. PROPERTIES
    Things that change and have a current state

    View Slide

  27. (100, 250)

    View Slide

  28. (300, 200)

    View Slide

  29. Array.prototype#map
    Array.prototype#filter
    Array.prototype#reduce
    Array.prototype#concat

    View Slide

  30. BACON.JS
    FRP library

    View Slide

  31. !==
    (obviously)

    View Slide

  32. Ember.observer
    Ember.computed
    Ember.Observable
    Ember.Evented
    Ember.on

    View Slide

  33. THE OBSERVER PATTERN
    Computed properties and observers

    View Slide

  34. COMPUTED PROPERTIES
    Transforms properties, and keeps relationships in sync

    View Slide

  35. export default Ember.Object.extend({
    fullName: computed('firstName', 'lastName', function() {
    return `${get(this, 'firstName')} ${get(this, 'lastName')}`;
    })
    });

    View Slide

  36. COMPUTED PROPERTY MACROS
    Keeping things DRY

    View Slide

  37. export default function(separator, dependentKeys) {
    let computedFunc = computed(function() {
    let values = dependentKeys.map((dependentKey) => {
    return getWithDefault(this, dependentKey, '');
    });
    return values.join(separator);
    });
    return computedFunc.property.apply(computedFunc, dependentKeys);
    };

    View Slide

  38. DEMO
    http://emberjs.jsbin.com/vubaga/12/edit?js,output

    View Slide

  39. import joinWith from '...';
    export default Ember.Object.extend({
    fullName: joinWith(' ', [
    'title',
    'firstName',
    'middleName',
    'lastName',
    'suffix'
    ])
    });
    get(this, 'fullName');
    // Mr Harvey Reginald Specter Esq.

    View Slide

  40. Ember.computed.{map,mapBy}
    Ember.computed.{filter,filterBy}
    Ember.computed.sort
    Ember.computed.intersect
    Ember.computed.setDiff
    Ember.computed.uniq
    Ember.computed.readTheAPIDocs
    http://emberjs.com/api/#method_computed

    View Slide

  41. OBSERVERS
    Synchronously invoked when dependent
    properties change

    View Slide

  42. DESIGN IS HOW IT WORKS
    GOOD DESIGN IS REACTIVE
    GOOD DESIGN IS PLAYFUL
    GOOD DESIGN IS INFORMATIVE
    GOOD DESIGN IS INTUITIVE

    View Slide

  43. View Slide

  44. View Slide

  45. ƈ
    http://youtu.be/OK34L4-qaDQ

    View Slide

  46. Waterboarding at Guantanamo Bay
    sounds super rad if you don't know
    what either of those things are.

    View Slide

  47. The person who would proof read
    Hitler's speeches was a grammar
    Nazi.

    View Slide

  48. If your shirt isn't tucked into your
    pants, then your pants are tucked
    into your shirt.

    View Slide

  49. ƈ
    +

    View Slide

  50. /index /users
    route:user
    model()
    { this.store.find('user') }
    // returns Promise

    GET "https://foo.com/v1/api/users"
    /loading
    /error
    resolve()
    reject()

    View Slide

  51. Service
    ƈ
    Reddit API
    Index Route User Route
    Loading Route
    Fetch top posts
    Component
    Fetch user records resolve()
    Get random message
    Display shower thought

    View Slide

  52. SERVICE

    View Slide

  53. Ember.Service.extend({ ... });

    View Slide

  54. messages : Ember.A([]),
    topPeriods : [ 'day', 'week', 'month', 'year', 'all' ],
    topPeriod : 'day',
    subreddit : 'showerthoughts',

    View Slide

  55. getPostsBy(subreddit, period) {
    let url = `//www.reddit.com/r/${subreddit}/top.json?sort=top&t=${period}`;
    return new RSVP.Promise((resolve, reject) => {
    getJSON(url)
    .then((res) => {
    let titles = res.data.children.mapBy('data.title');
    resolve(titles);
    }).catch(/* ... */);
    });
    }

    View Slide

  56. _handleTopPeriodChange: observer('subreddit', 'topPeriod', function() {
    let subreddit = get(this, 'subreddit');
    let topPeriod = get(this, 'topPeriod');
    run.once(this, () => {
    this.getPostsBy(subreddit, topPeriod)
    .then((posts) => {
    set(this, 'messages', posts);
    });
    });
    }).on('init'),

    View Slide

  57. COMPONENT

    View Slide

  58. export default Ember.Component.extend({
    service : inject.service('shower-thoughts'),
    randomMsg : computedSample('service.messages'),
    loadingText : 'Loading',
    classNames : [ 'loadingMessage' ]
    });

    View Slide

  59. export default function(dependentKey) {
    return computed(`${dependentKey}.@each`, () => {
    let items = getWithDefault(this, dependentKey, Ember.A([]));
    let randomItem = items[Math.floor(Math.random() * items.get('length'))];
    return randomItem || '';
    }).volatile().readOnly();
    }

    View Slide

  60. DEMO
    http://emberjs.jsbin.com/lulaki/35/edit?output

    View Slide

  61. DESIGN IS HOW IT WORKS
    GOOD DESIGN IS REACTIVE
    GOOD DESIGN IS PLAYFUL
    GOOD DESIGN IS INFORMATIVE
    GOOD DESIGN IS INTUITIVE

    View Slide

  62. VISIBILITY OF SYSTEM STATUS
    Jakob Nielsen — 10 Heuristics for User Interface Design

    View Slide

  63. FLASH MESSAGES
    Is it time for snacks yet?

    View Slide

  64. Service
    Routes Controllers
    Message Component Message Component

    View Slide

  65. SERVICE

    View Slide

  66. Ember.get(this, 'flashes').success('Success!', 2000);
    Ember.get(this, 'flashes').warning('...');
    Ember.get(this, 'flashes').info('...');
    Ember.get(this, 'flashes').danger('...');
    Ember.get(this, 'flashes').addMessage('Custom message', 'myCustomType', 3000)
    Ember.get(this, 'flashes').clearMessages();

    View Slide

  67. SERVICE: PROPS
    queue : Ember.A([]),
    isEmpty : computed.equal('queue.length', 0),
    defaultTimeout : 2000

    View Slide

  68. SERVICE: PUBLIC API
    success(message, timeout=get(this, 'defaultTimeout')) {
    return this._addToQueue(message, 'success', timeout);
    },
    info(/* ... */) {
    return ...;
    },
    warning(/* ... */) {
    return ...;
    },
    danger(/* ... */) {
    return ...;
    },
    addMessage(message, type='default', timeout=get(this, 'defaultTimeout')) {
    return this._addToQueue(message, type, timeout);
    }

    View Slide

  69. SERVICE: PUBLIC API
    clearMessages() {
    let flashes = get(this, 'queue');
    flashes.clear();
    }

    View Slide

  70. SERVICE: PRIVATE API
    _addToQueue(message, type, timeout) {
    let flashes = get(this, 'queue');
    let flash = this._newFlashMessage(this, message, type, timeout);
    flashes.pushObject(flash);
    }

    View Slide

  71. SERVICE: PRIVATE API
    _newFlashMessage(service, message, type='info', timeout=get(this, 'defaultTimeout')) {
    Ember.assert('Must pass a valid flash service', service);
    Ember.assert('Must pass a valid flash message', message);
    return FlashMessage.create({
    type : type,
    message : message,
    timeout : timeout,
    flashService : service
    });
    }

    View Slide

  72. FLASH MESSAGE

    View Slide

  73. FLASH MESSAGE: PROPS
    isSuccess : computed.equal('type', 'success'),
    isInfo : computed.equal('type', 'info'),
    isWarning : computed.equal('type', 'warning'),
    isDanger : computed.equal('type', 'danger'),
    defaultTimeout : computed.alias('flashService.defaultTimeout'),
    queue : computed.alias('flashService.queue'),
    timer : null

    View Slide

  74. FLASH MESSAGE: LIFECYCLE HOOK
    _destroyLater() {
    let defaultTimeout = get(this, 'defaultTimeout');
    let timeout = getWithDefault(this, 'timeout', defaultTimeout);
    let destroyTimer = run.later(this, '_destroyMessage', timeout);
    set(this, 'timer', destroyTimer);
    }.on('init')

    View Slide

  75. FLASH MESSAGE: PRIVATE API
    _destroyMessage() {
    let queue = get(this, 'queue');
    if (queue) {
    queue.removeObject(this);
    }
    this.destroy();
    }

    View Slide

  76. FLASH MESSAGE: PUBLIC API & OVERRIDE
    destroyMessage() {
    this._destroyMessage();
    },
    willDestroy() {
    this._super();
    let timer = get(this, 'timer');
    if (timer) {
    run.cancel(timer);
    set(this, 'timer', null);
    }
    }

    View Slide

  77. Lj DEPENDENCY INJECTION

    View Slide

  78. import FlashMessagesService from '...';
    export function initialize(_container, application) {
    application.register('service:flash-messages', FlashMessagesService, { singleton: true });
    application.inject('controller', 'flashes', 'service:flash-messages');
    application.inject('route', 'flashes', 'service:flash-messages');
    }
    export default {
    name: 'flash-messages-service',
    initialize: initialize
    };

    View Slide

  79. COMPONENT

    View Slide

  80. COMPONENT: TEMPLATE
    {{#if template}}
    {{yield}}
    {{else}}
    {{flash.message}}
    {{/if}}

    View Slide

  81. COMPONENT: PUBLIC API
    export default Ember.Component.extend({
    classNames: [ 'alert', 'flashMessage' ],
    classNameBindings: [ 'alertType' ],
    alertType: computed('flash.type', function() {
    let flashType = get(this, 'flash.type');
    return `alert-${flashType}`;
    }),
    click() {
    let flash = get(this, 'flash');
    flash.destroyMessage();
    }
    });

    View Slide

  82. USAGE

    View Slide

  83. {{#each flashes.queue as |flash|}}
    {{flash-message flash=flash}}
    {{/each}}

    View Slide

  84. {{#each flashes.queue as |flash|}}
    {{#flash-message flash=flash}}
    {{flash.type}}
    {{flash.message}}
    {{/flash-message}}
    {{/each}}

    View Slide

  85. DEMO
    http://emberjs.jsbin.com/ranewo/46/edit?js,output

    View Slide

  86. $ ember install:addon ember-cli-flash
    $ npm install --save-dev ember-cli-flash

    View Slide

  87. DESIGN IS HOW IT WORKS
    GOOD DESIGN IS REACTIVE
    GOOD DESIGN IS PLAYFUL
    GOOD DESIGN IS INFORMATIVE
    GOOD DESIGN IS INTUITIVE

    View Slide

  88. DRAG AND DROP
    Skip

    View Slide

  89. Draggable Dropzone
    Draggable Item
    Draggable Item
    Draggable Item
    Controller
    sendAction()
    Route

    View Slide

  90. COMPONENT/VIEW EVENTS
    http://emberjs.com/api/classes/Ember.View.html#toc_event-names

    View Slide

  91. https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/
    Drag_operations#draggableattribute

    View Slide

  92. https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer#getData.28.29

    View Slide

  93. DROPZONE

    View Slide

  94. export default Ember.Component.extend({
    classNames : [ 'draggableDropzone' ],
    classNameBindings : [ 'dragClass' ],
    dragClass : 'deactivated',
    dragLeave(event) {
    event.preventDefault();
    set(this, 'dragClass', 'deactivated');
    },
    dragOver(event) {
    event.preventDefault();
    set(this, 'dragClass', 'activated');
    },
    drop(event) {
    let data = event.dataTransfer.getData('text/data');
    this.sendAction('dropped', data);
    set(this, 'dragClass', 'deactivated');
    }
    });

    View Slide

  95. DRAGGABLE ITEM

    View Slide

  96. export default Ember.Component.extend({
    classNames : [ 'draggableItem' ],
    attributeBindings : [ 'draggable' ],
    draggable : 'true',
    dragStart(event) {
    return event.dataTransfer.setData('text/data', get(this, 'content'));
    }
    });

    View Slide

  97. {{ yield }}

    View Slide

  98. View Slide


  99. {{#draggable-dropzone dropped="addUser"}}

    {{#each selectedUsers as |user|}}
    {{user.fullName}}
    {{/each}}

    {{/draggable-dropzone}}


    {{#each users as |user|}}
    {{#draggable-item content=user.id}}
    {{user.fullName}}
    {{/draggable-item}}
    {{/each}}

    View Slide

  100. actions: {
    addUser(userId) {
    let selectedUsers = get(this, 'selectedUsers');
    let user = get(this, 'model').findBy('id', parseInt(userId));
    if (!selectedUsers.contains(user)) {
    return selectedUsers.pushObject(user);
    }
    }
    }

    View Slide

  101. DEMO
    http://emberjs.jsbin.com/denep/18/edit?js,output

    View Slide

  102. TL;DR

    View Slide

  103. DESIGN IS HOW IT WORKS
    GOOD DESIGN IS REACTIVE
    GOOD DESIGN IS PLAYFUL
    GOOD DESIGN IS INFORMATIVE
    GOOD DESIGN IS INTUITIVE

    View Slide

  104. DESIGN IS HOW IT WORKS
    GOOD DESIGN IS REACTIVE
    GOOD DESIGN IS PLAYFUL
    GOOD DESIGN IS INFORMATIVE
    GOOD DESIGN IS INTUITIVE

    View Slide

  105. DESIGN IS HOW IT WORKS
    GOOD DESIGN IS REACTIVE
    GOOD DESIGN IS PLAYFUL
    GOOD DESIGN IS INFORMATIVE
    GOOD DESIGN IS INTUITIVE

    View Slide

  106. DESIGN IS HOW IT WORKS
    GOOD DESIGN IS REACTIVE
    GOOD DESIGN IS PLAYFUL
    GOOD DESIGN IS INFORMATIVE
    GOOD DESIGN IS INTUITIVE

    View Slide

  107. DESIGN IS HOW IT WORKS
    GOOD DESIGN IS REACTIVE
    GOOD DESIGN IS PLAYFUL
    GOOD DESIGN IS INFORMATIVE
    GOOD DESIGN IS INTUITIVE

    View Slide

  108. AMBITIOUS UX FOR
    AMBITIOUS APPS
    EMBERCONF 2015
    Lauren Elizabeth Tan
    @sugarpirate_ @poteto

    View Slide

  109. Makes ambitious UX easy (and fun!)

    View Slide

  110. Design is how it works

    View Slide

  111. Follow @sugarpirate_

    View Slide

  112. bit.ly/sugarpirate

    View Slide

  113. Thank you!
    Lauren Elizabeth Tan
    @sugarpirate_ @poteto

    View Slide