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. 3.
  2. 4.

    DESIGN IS HOW IT WORKS GOOD DESIGN IS REACTIVE GOOD

    DESIGN IS PLAYFUL GOOD DESIGN IS INFORMATIVE GOOD DESIGN IS INTUITIVE
  3. 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.”
  4. 8.
  5. 9.
  6. 10.
  7. 11.
  8. 12.
  9. 13.

    =

  10. 14.
  11. 16.

    DESIGN IS HOW IT WORKS GOOD DESIGN IS REACTIVE GOOD

    DESIGN IS PLAYFUL GOOD DESIGN IS INFORMATIVE GOOD DESIGN IS INTUITIVE
  12. 17.
  13. 18.
  14. 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); };
  15. 39.

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

    [ 'title', 'firstName', 'middleName', 'lastName', 'suffix' ]) }); get(this, 'fullName'); // Mr Harvey Reginald Specter Esq.
  16. 42.

    DESIGN IS HOW IT WORKS GOOD DESIGN IS REACTIVE GOOD

    DESIGN IS PLAYFUL GOOD DESIGN IS INFORMATIVE GOOD DESIGN IS INTUITIVE
  17. 43.
  18. 44.
  19. 46.
  20. 48.

    If your shirt isn't tucked into your pants, then your

    pants are tucked into your shirt.
  21. 49.
  22. 50.

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

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

    Service ƈ Reddit API Index Route User Route Loading Route

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

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

    'all' ], topPeriod : 'day', subreddit : 'showerthoughts',
  26. 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(/* ... */); }); }
  27. 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'),
  28. 57.
  29. 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(); }
  30. 61.

    DESIGN IS HOW IT WORKS GOOD DESIGN IS REACTIVE GOOD

    DESIGN IS PLAYFUL GOOD DESIGN IS INFORMATIVE GOOD DESIGN IS INTUITIVE
  31. 65.
  32. 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); }
  33. 70.

    SERVICE: PRIVATE API _addToQueue(message, type, timeout) { let flashes =

    get(this, 'queue'); let flash = this._newFlashMessage(this, message, type, timeout); flashes.pushObject(flash); }
  34. 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 }); }
  35. 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
  36. 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')
  37. 75.

    FLASH MESSAGE: PRIVATE API _destroyMessage() { let queue = get(this,

    'queue'); if (queue) { queue.removeObject(this); } this.destroy(); }
  38. 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); } }
  39. 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 };
  40. 79.
  41. 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(); } });
  42. 82.
  43. 87.

    DESIGN IS HOW IT WORKS GOOD DESIGN IS REACTIVE GOOD

    DESIGN IS PLAYFUL GOOD DESIGN IS INFORMATIVE GOOD DESIGN IS INTUITIVE
  44. 93.
  45. 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'); } });
  46. 96.

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

    [ 'draggable' ], draggable : 'true', dragStart(event) { return event.dataTransfer.setData('text/data', get(this, 'content')); } });
  47. 98.
  48. 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>
  49. 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); } } }
  50. 102.
  51. 103.

    DESIGN IS HOW IT WORKS GOOD DESIGN IS REACTIVE GOOD

    DESIGN IS PLAYFUL GOOD DESIGN IS INFORMATIVE GOOD DESIGN IS INTUITIVE
  52. 104.

    DESIGN IS HOW IT WORKS GOOD DESIGN IS REACTIVE GOOD

    DESIGN IS PLAYFUL GOOD DESIGN IS INFORMATIVE GOOD DESIGN IS INTUITIVE
  53. 105.

    DESIGN IS HOW IT WORKS GOOD DESIGN IS REACTIVE GOOD

    DESIGN IS PLAYFUL GOOD DESIGN IS INFORMATIVE GOOD DESIGN IS INTUITIVE
  54. 106.

    DESIGN IS HOW IT WORKS GOOD DESIGN IS REACTIVE GOOD

    DESIGN IS PLAYFUL GOOD DESIGN IS INFORMATIVE GOOD DESIGN IS INTUITIVE
  55. 107.

    DESIGN IS HOW IT WORKS GOOD DESIGN IS REACTIVE GOOD

    DESIGN IS PLAYFUL GOOD DESIGN IS INFORMATIVE GOOD DESIGN IS INTUITIVE