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

C8fccffc013096c4b465b50c284a5208?s=128

Lauren Tan

March 03, 2015
Tweet

Transcript

  1. AMBITIOUS UX FOR AMBITIOUS APPS EMBERCONF 2015 Lauren Elizabeth Tan

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

  3. None
  4. DESIGN IS HOW IT WORKS GOOD DESIGN IS REACTIVE GOOD

    DESIGN IS PLAYFUL GOOD DESIGN IS INFORMATIVE GOOD DESIGN IS INTUITIVE
  5. BUT I’M NOT A DESIGNER

  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.”
  7. applyConcatenatedProperties() giveDescriptorSuper() beginPropertyChanges()

  8. None
  9. None
  10. None
  11. None
  12. None
  13. =

  14. None
  15. What is good design?

  16. DESIGN IS HOW IT WORKS GOOD DESIGN IS REACTIVE GOOD

    DESIGN IS PLAYFUL GOOD DESIGN IS INFORMATIVE GOOD DESIGN IS INTUITIVE
  17. REACTIVE?

  18. None
  19. FLOW OF DATA & MAINTAINING RELATIONSHIPS BETWEEN THAT DATA

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

  21. FUNCTIONAL REACTIVE PROGRAMMING?

  22. FUNCTIONAL REACTIVE PROGRAMMING?

  23. FUNCTIONAL REACTIVE PROGRAMMING? Immutability Some side effects

  24. EVENT STREAMS Things that consist of discrete events

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

  26. PROPERTIES Things that change and have a current state

  27. (100, 250)

  28. (300, 200)

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

  30. BACON.JS FRP library

  31. !== (obviously)

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

  33. THE OBSERVER PATTERN Computed properties and observers

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

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

    'firstName')} ${get(this, 'lastName')}`; }) });
  36. COMPUTED PROPERTY MACROS Keeping things DRY

  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); };
  38. DEMO http://emberjs.jsbin.com/vubaga/12/edit?js,output

  39. import joinWith from '...'; export default Ember.Object.extend({ fullName: joinWith(' ',

    [ 'title', 'firstName', 'middleName', 'lastName', 'suffix' ]) }); get(this, 'fullName'); // Mr Harvey Reginald Specter Esq.
  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

  41. OBSERVERS Synchronously invoked when dependent properties change

  42. DESIGN IS HOW IT WORKS GOOD DESIGN IS REACTIVE GOOD

    DESIGN IS PLAYFUL GOOD DESIGN IS INFORMATIVE GOOD DESIGN IS INTUITIVE
  43. None
  44. None
  45. ƈ http://youtu.be/OK34L4-qaDQ

  46. Waterboarding at Guantanamo Bay sounds super rad if you don't

    know what either of those things are.
  47. The person who would proof read Hitler's speeches was a

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

    pants are tucked into your shirt.
  49. ƈ +

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

     GET "https://foo.com/v1/api/users" /loading /error resolve() reject()
  51. Service ƈ Reddit API Index Route User Route Loading Route

    Fetch top posts Component Fetch user records resolve() Get random message Display shower thought
  52. SERVICE

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

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

    'all' ], topPeriod : 'day', subreddit : 'showerthoughts',
  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(/* ... */); }); }
  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'),
  57. COMPONENT

  58. export default Ember.Component.extend({ service : inject.service('shower-thoughts'), randomMsg : computedSample('service.messages'), loadingText

    : 'Loading', classNames : [ 'loadingMessage' ] });
  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(); }
  60. DEMO http://emberjs.jsbin.com/lulaki/35/edit?output

  61. DESIGN IS HOW IT WORKS GOOD DESIGN IS REACTIVE GOOD

    DESIGN IS PLAYFUL GOOD DESIGN IS INFORMATIVE GOOD DESIGN IS INTUITIVE
  62. VISIBILITY OF SYSTEM STATUS Jakob Nielsen — 10 Heuristics for

    User Interface Design
  63. FLASH MESSAGES Is it time for snacks yet?

  64. Service Routes Controllers Message Component Message Component

  65. SERVICE

  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();
  67. SERVICE: PROPS queue : Ember.A([]), isEmpty : computed.equal('queue.length', 0), defaultTimeout

    : 2000
  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); }
  69. SERVICE: PUBLIC API clearMessages() { let flashes = get(this, 'queue');

    flashes.clear(); }
  70. SERVICE: PRIVATE API _addToQueue(message, type, timeout) { let flashes =

    get(this, 'queue'); let flash = this._newFlashMessage(this, message, type, timeout); flashes.pushObject(flash); }
  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 }); }
  72. FLASH MESSAGE

  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
  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')
  75. FLASH MESSAGE: PRIVATE API _destroyMessage() { let queue = get(this,

    'queue'); if (queue) { queue.removeObject(this); } this.destroy(); }
  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); } }
  77. Lj DEPENDENCY INJECTION

  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 };
  79. COMPONENT

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

  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(); } });
  82. USAGE

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

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

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

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

  87. DESIGN IS HOW IT WORKS GOOD DESIGN IS REACTIVE GOOD

    DESIGN IS PLAYFUL GOOD DESIGN IS INFORMATIVE GOOD DESIGN IS INTUITIVE
  88. DRAG AND DROP Skip

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

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

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

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

  93. DROPZONE

  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'); } });
  95. DRAGGABLE ITEM

  96. export default Ember.Component.extend({ classNames : [ 'draggableItem' ], attributeBindings :

    [ 'draggable' ], draggable : 'true', dragStart(event) { return event.dataTransfer.setData('text/data', get(this, 'content')); } });
  97. {{ yield }}

  98. None
  99. <div class="selectedUsers"> {{#draggable-dropzone dropped="addUser"}} <ul class="selected-users-list"> {{#each selectedUsers as |user|}}

    <li>{{user.fullName}}</li> {{/each}} </ul> {{/draggable-dropzone}} </div> <div class="availableUsers"> {{#each users as |user|}} {{#draggable-item content=user.id}} <span>{{user.fullName}}</span> {{/draggable-item}} {{/each}} </div>
  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); } } }
  101. DEMO http://emberjs.jsbin.com/denep/18/edit?js,output

  102. TL;DR

  103. DESIGN IS HOW IT WORKS GOOD DESIGN IS REACTIVE GOOD

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

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

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

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

    DESIGN IS PLAYFUL GOOD DESIGN IS INFORMATIVE GOOD DESIGN IS INTUITIVE
  108. AMBITIOUS UX FOR AMBITIOUS APPS EMBERCONF 2015 Lauren Elizabeth Tan

    @sugarpirate_ @poteto
  109. Makes ambitious UX easy (and fun!)

  110. Design is how it works

  111. Follow @sugarpirate_

  112. bit.ly/sugarpirate

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