Top Banner
AMBITIOUS UX FOR AMBITIOUS APPS EMBERCONF 2015 Lauren Elizabeth Tan @sugarpirate_ @poteto
113

EmberConf 2015 – Ambitious UX for Ambitious Apps

Jul 14, 2015

Download

Technology

Welcome message from author
This document is posted to help you gain knowledge. Please leave a comment to let me know what you think about it! Share it to your friends and learn new things together.
Transcript
Page 1: EmberConf 2015 – Ambitious UX for Ambitious Apps

AMBITIOUS UX FOR AMBITIOUS APPS

EMBERCONF 2015

Lauren Elizabeth Tan ! @sugarpirate_ " @poteto

Page 2: EmberConf 2015 – Ambitious UX for Ambitious Apps

# DESIGN $ DEV

Lauren Elizabeth Tan Designer & Front End Developer

Page 3: EmberConf 2015 – Ambitious UX for Ambitious Apps
Page 4: EmberConf 2015 – Ambitious UX for Ambitious Apps

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

Page 5: EmberConf 2015 – Ambitious UX for Ambitious Apps

BUT I’M NOT A DESIGNER

Page 6: EmberConf 2015 – Ambitious UX for Ambitious Apps

“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.”

Page 7: EmberConf 2015 – Ambitious UX for Ambitious Apps

applyConcatenatedProperties() giveDescriptorSuper() beginPropertyChanges()

Page 8: EmberConf 2015 – Ambitious UX for Ambitious Apps
Page 9: EmberConf 2015 – Ambitious UX for Ambitious Apps
Page 10: EmberConf 2015 – Ambitious UX for Ambitious Apps
Page 11: EmberConf 2015 – Ambitious UX for Ambitious Apps
Page 12: EmberConf 2015 – Ambitious UX for Ambitious Apps
Page 13: EmberConf 2015 – Ambitious UX for Ambitious Apps

=

Page 14: EmberConf 2015 – Ambitious UX for Ambitious Apps
Page 15: EmberConf 2015 – Ambitious UX for Ambitious Apps

What is good design?

Page 16: EmberConf 2015 – Ambitious UX for Ambitious Apps

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

Page 17: EmberConf 2015 – Ambitious UX for Ambitious Apps

REACTIVE?

Page 18: EmberConf 2015 – Ambitious UX for Ambitious Apps
Page 19: EmberConf 2015 – Ambitious UX for Ambitious Apps

FLOW OF DATA &

MAINTAINING RELATIONSHIPS BETWEEN THAT DATA

Page 20: EmberConf 2015 – Ambitious UX for Ambitious Apps

var EmberObject = CoreObject.extend(Observable);

Page 21: EmberConf 2015 – Ambitious UX for Ambitious Apps

FUNCTIONAL REACTIVE PROGRAMMING?

Page 22: EmberConf 2015 – Ambitious UX for Ambitious Apps

FUNCTIONAL REACTIVE PROGRAMMING?

Page 23: EmberConf 2015 – Ambitious UX for Ambitious Apps

FUNCTIONAL REACTIVE PROGRAMMING?

Immutability Some side effects

Page 24: EmberConf 2015 – Ambitious UX for Ambitious Apps

EVENT STREAMS Things that consist of discrete events

Page 25: EmberConf 2015 – Ambitious UX for Ambitious Apps

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

Page 26: EmberConf 2015 – Ambitious UX for Ambitious Apps

PROPERTIES Things that change and have a current state

Page 27: EmberConf 2015 – Ambitious UX for Ambitious Apps

%(100, 250)

Page 28: EmberConf 2015 – Ambitious UX for Ambitious Apps

%(300, 200)

Page 29: EmberConf 2015 – Ambitious UX for Ambitious Apps

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

Page 30: EmberConf 2015 – Ambitious UX for Ambitious Apps

BACON.JS FRP library

Page 31: EmberConf 2015 – Ambitious UX for Ambitious Apps

!==

(obviously)

Page 32: EmberConf 2015 – Ambitious UX for Ambitious Apps

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

Page 33: EmberConf 2015 – Ambitious UX for Ambitious Apps

THE OBSERVER PATTERN Computed properties and observers

Page 34: EmberConf 2015 – Ambitious UX for Ambitious Apps

COMPUTED PROPERTIES Transforms properties, and keeps relationships in sync

Page 35: EmberConf 2015 – Ambitious UX for Ambitious Apps

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

Page 36: EmberConf 2015 – Ambitious UX for Ambitious Apps

COMPUTED PROPERTY MACROS Keeping things DRY

Page 37: EmberConf 2015 – Ambitious UX for Ambitious Apps

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); };

Page 38: EmberConf 2015 – Ambitious UX for Ambitious Apps

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

Page 39: EmberConf 2015 – Ambitious UX for Ambitious Apps

import joinWith from '...';

export default Ember.Object.extend({ fullName: joinWith(' ', [ 'title', 'firstName', 'middleName', 'lastName', 'suffix' ]) });

get(this, 'fullName'); // Mr Harvey Reginald Specter Esq.

Page 40: EmberConf 2015 – Ambitious UX for Ambitious Apps

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

Page 41: EmberConf 2015 – Ambitious UX for Ambitious Apps

OBSERVERS Synchronously invoked when dependent

properties change

Page 42: EmberConf 2015 – Ambitious UX for Ambitious Apps

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

Page 43: EmberConf 2015 – Ambitious UX for Ambitious Apps
Page 44: EmberConf 2015 – Ambitious UX for Ambitious Apps
Page 45: EmberConf 2015 – Ambitious UX for Ambitious Apps

&http://youtu.be/OK34L4-qaDQ

Page 46: EmberConf 2015 – Ambitious UX for Ambitious Apps

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

what either of those things are.

Page 47: EmberConf 2015 – Ambitious UX for Ambitious Apps

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

Nazi.

Page 48: EmberConf 2015 – Ambitious UX for Ambitious Apps

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

into your shirt.

Page 49: EmberConf 2015 – Ambitious UX for Ambitious Apps

&+

Page 50: EmberConf 2015 – Ambitious UX for Ambitious Apps

' /index

( /users

) route:user

model() { this.store.find('user') }

// returns Promise

GET "https://foo.com/v1/api/users"

+ /loading

, /error

resolve()

reject()

Page 51: EmberConf 2015 – Ambitious UX for Ambitious Apps

- Service

& Reddit API

' Index Route

. User Route

+ Loading Route

Fetch top posts

/ Component

Fetch user records resolve()

Get random message

Display shower thought

Page 52: EmberConf 2015 – Ambitious UX for Ambitious Apps

- SERVICE

Page 53: EmberConf 2015 – Ambitious UX for Ambitious Apps

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

Page 54: EmberConf 2015 – Ambitious UX for Ambitious Apps

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

Page 55: EmberConf 2015 – Ambitious UX for Ambitious Apps

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(/* ... */); }); }

Page 56: EmberConf 2015 – Ambitious UX for Ambitious Apps

_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'),

Page 57: EmberConf 2015 – Ambitious UX for Ambitious Apps

- COMPONENT

Page 58: EmberConf 2015 – Ambitious UX for Ambitious Apps

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

Page 59: EmberConf 2015 – Ambitious UX for Ambitious Apps

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

Page 60: EmberConf 2015 – Ambitious UX for Ambitious Apps

DEMOhttp://emberjs.jsbin.com/lulaki/35/edit?output

Page 61: EmberConf 2015 – Ambitious UX for Ambitious Apps

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

Page 62: EmberConf 2015 – Ambitious UX for Ambitious Apps

VISIBILITY OF SYSTEM STATUSJakob Nielsen — 10 Heuristics for User Interface Design

Page 63: EmberConf 2015 – Ambitious UX for Ambitious Apps

0 FLASH MESSAGES

1 Is it time for snacks yet?

Page 64: EmberConf 2015 – Ambitious UX for Ambitious Apps

0 Service

' Routes

2 Controllers

3 Message Component 3 Message Component

Page 65: EmberConf 2015 – Ambitious UX for Ambitious Apps

0 SERVICE

Page 66: EmberConf 2015 – Ambitious UX for Ambitious Apps

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();

Page 67: EmberConf 2015 – Ambitious UX for Ambitious Apps

SERVICE: PROPS

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

Page 68: EmberConf 2015 – Ambitious UX for Ambitious Apps

SERVICE: PUBLIC APIsuccess(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); }

Page 69: EmberConf 2015 – Ambitious UX for Ambitious Apps

SERVICE: PUBLIC API

clearMessages() { let flashes = get(this, 'queue');

flashes.clear(); }

Page 70: EmberConf 2015 – Ambitious UX for Ambitious Apps

SERVICE: PRIVATE API

_addToQueue(message, type, timeout) { let flashes = get(this, 'queue'); let flash = this._newFlashMessage(this, message, type, timeout);

flashes.pushObject(flash); }

Page 71: EmberConf 2015 – Ambitious UX for Ambitious Apps

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 }); }

Page 72: EmberConf 2015 – Ambitious UX for Ambitious Apps

3 FLASH MESSAGE

Page 73: EmberConf 2015 – Ambitious UX for Ambitious Apps

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

Page 74: EmberConf 2015 – Ambitious UX for Ambitious Apps

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')

Page 75: EmberConf 2015 – Ambitious UX for Ambitious Apps

FLASH MESSAGE: PRIVATE API

_destroyMessage() { let queue = get(this, 'queue');

if (queue) { queue.removeObject(this); }

this.destroy(); }

Page 76: EmberConf 2015 – Ambitious UX for Ambitious Apps

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); } }

Page 77: EmberConf 2015 – Ambitious UX for Ambitious Apps

4 DEPENDENCY INJECTION

Page 78: EmberConf 2015 – Ambitious UX for Ambitious Apps

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 };

Page 79: EmberConf 2015 – Ambitious UX for Ambitious Apps

/ COMPONENT

Page 80: EmberConf 2015 – Ambitious UX for Ambitious Apps

COMPONENT: TEMPLATE

{{#if template}} {{yield}} {{else}} {{flash.message}} {{/if}}

Page 81: EmberConf 2015 – Ambitious UX for Ambitious Apps

COMPONENT: PUBLIC APIexport 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(); } });

Page 82: EmberConf 2015 – Ambitious UX for Ambitious Apps

5 USAGE

Page 83: EmberConf 2015 – Ambitious UX for Ambitious Apps

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

Page 84: EmberConf 2015 – Ambitious UX for Ambitious Apps

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

Page 85: EmberConf 2015 – Ambitious UX for Ambitious Apps

DEMOhttp://emberjs.jsbin.com/ranewo/46/edit?js,output

Page 86: EmberConf 2015 – Ambitious UX for Ambitious Apps

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

Page 87: EmberConf 2015 – Ambitious UX for Ambitious Apps

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

Page 88: EmberConf 2015 – Ambitious UX for Ambitious Apps

6 DRAG AND DROP

Skip

Page 89: EmberConf 2015 – Ambitious UX for Ambitious Apps

7 Draggable Dropzone

8 Draggable Item

8 Draggable Item

8 Draggable Item 2 Controller

sendAction()

' Route

Page 90: EmberConf 2015 – Ambitious UX for Ambitious Apps

COMPONENT/VIEW EVENTS

http://emberjs.com/api/classes/Ember.View.html#toc_event-names

Page 91: EmberConf 2015 – Ambitious UX for Ambitious Apps

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

Page 92: EmberConf 2015 – Ambitious UX for Ambitious Apps

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

Page 93: EmberConf 2015 – Ambitious UX for Ambitious Apps

7 DROPZONE

Page 94: EmberConf 2015 – Ambitious UX for Ambitious Apps

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'); } });

Page 95: EmberConf 2015 – Ambitious UX for Ambitious Apps

8 DRAGGABLE ITEM

Page 96: EmberConf 2015 – Ambitious UX for Ambitious Apps

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

Page 97: EmberConf 2015 – Ambitious UX for Ambitious Apps

{{ yield }}

Page 98: EmberConf 2015 – Ambitious UX for Ambitious Apps
Page 99: EmberConf 2015 – Ambitious UX for Ambitious Apps

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

Page 100: EmberConf 2015 – Ambitious UX for Ambitious Apps

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); } } }

Page 101: EmberConf 2015 – Ambitious UX for Ambitious Apps

DEMOhttp://emberjs.jsbin.com/denep/18/edit?js,output

Page 102: EmberConf 2015 – Ambitious UX for Ambitious Apps

TL;DR

Page 103: EmberConf 2015 – Ambitious UX for Ambitious Apps

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

Page 104: EmberConf 2015 – Ambitious UX for Ambitious Apps

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

Page 105: EmberConf 2015 – Ambitious UX for Ambitious Apps

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

Page 106: EmberConf 2015 – Ambitious UX for Ambitious Apps

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

Page 107: EmberConf 2015 – Ambitious UX for Ambitious Apps

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

Page 108: EmberConf 2015 – Ambitious UX for Ambitious Apps

AMBITIOUS UX FOR AMBITIOUS APPS

EMBERCONF 2015

Lauren Elizabeth Tan ! @sugarpirate_ " @poteto

Page 109: EmberConf 2015 – Ambitious UX for Ambitious Apps

Makes ambitious UX easy (and fun!)

Page 110: EmberConf 2015 – Ambitious UX for Ambitious Apps

#Design is how it works

Page 111: EmberConf 2015 – Ambitious UX for Ambitious Apps

!Follow @sugarpirate_

Page 112: EmberConf 2015 – Ambitious UX for Ambitious Apps

bit.ly/sugarpirate9

Page 113: EmberConf 2015 – Ambitious UX for Ambitious Apps

:Thank you!

Lauren Elizabeth Tan ! @sugarpirate_ " @poteto