emberconf 2015 – ambitious ux for ambitious apps

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

Upload: lauren-elizabeth-tan

Post on 14-Jul-2015

181 views

Category:

Technology


2 download

TRANSCRIPT

AMBITIOUS UX FOR AMBITIOUS APPS

EMBERCONF 2015

Lauren Elizabeth Tan ! @sugarpirate_ " @poteto

# DESIGN $ DEV

Lauren Elizabeth Tan Designer & Front End Developer

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

BUT I’M NOT A DESIGNER

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

applyConcatenatedProperties() giveDescriptorSuper() beginPropertyChanges()

=

What is good design?

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

REACTIVE?

FLOW OF DATA &

MAINTAINING RELATIONSHIPS BETWEEN THAT DATA

var EmberObject = CoreObject.extend(Observable);

FUNCTIONAL REACTIVE PROGRAMMING?

FUNCTIONAL REACTIVE PROGRAMMING?

FUNCTIONAL REACTIVE PROGRAMMING?

Immutability Some side effects

EVENT STREAMS Things that consist of discrete events

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

PROPERTIES Things that change and have a current state

%(100, 250)

%(300, 200)

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

BACON.JS FRP library

!==

(obviously)

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

THE OBSERVER PATTERN Computed properties and observers

COMPUTED PROPERTIES Transforms properties, and keeps relationships in sync

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

COMPUTED PROPERTY MACROS Keeping things DRY

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

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

import joinWith from '...';

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

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

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

OBSERVERS Synchronously invoked when dependent

properties change

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

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

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

what either of those things are.

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

Nazi.

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

into your shirt.

&+

' /index

( /users

) route:user

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

// returns Promise

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

+ /loading

, /error

resolve()

reject()

- Service

& Reddit API

' Index Route

. User Route

+ Loading Route

Fetch top posts

/ Component

Fetch user records resolve()

Get random message

Display shower thought

- SERVICE

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

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

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

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

- COMPONENT

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

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

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

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

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

0 FLASH MESSAGES

1 Is it time for snacks yet?

0 Service

' Routes

2 Controllers

3 Message Component 3 Message Component

0 SERVICE

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

SERVICE: PROPS

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

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

SERVICE: PUBLIC API

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

flashes.clear(); }

SERVICE: PRIVATE API

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

flashes.pushObject(flash); }

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

3 FLASH MESSAGE

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

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

FLASH MESSAGE: PRIVATE API

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

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

this.destroy(); }

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

4 DEPENDENCY INJECTION

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

/ COMPONENT

COMPONENT: TEMPLATE

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

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

5 USAGE

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

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

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

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

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

6 DRAG AND DROP

Skip

7 Draggable Dropzone

8 Draggable Item

8 Draggable Item

8 Draggable Item 2 Controller

sendAction()

' Route

COMPONENT/VIEW EVENTS

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

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

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

7 DROPZONE

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

8 DRAGGABLE ITEM

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

{{ yield }}

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

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

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

TL;DR

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

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

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

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

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

AMBITIOUS UX FOR AMBITIOUS APPS

EMBERCONF 2015

Lauren Elizabeth Tan ! @sugarpirate_ " @poteto

Makes ambitious UX easy (and fun!)

#Design is how it works

!Follow @sugarpirate_

bit.ly/sugarpirate9

:Thank you!

Lauren Elizabeth Tan ! @sugarpirate_ " @poteto