Download - Complex Architectures in Ember
Hi. I’m Matthew.I build spiffy apps for clients in NYC
Thursday, September 19, 13
@mixonic
httP://madhatted.com
Thursday, September 19, 13
Let’s go single page
Thursday, September 19, 13
AMBITIOUS
Thursday, September 19, 13
COMPLEXThursday, September 19, 13
Start simple.
Thursday, September 19, 13
title
body
preview submit
preview
Thursday, September 19, 13
title
body
preview submit
preview
Thursday, September 19, 13
1 <div style="clear:both"> 2 <div style="float:left"> 3 {{input value=title}} 4 {{textarea value=body}} 5 </div> 6 <div style="float:left"> 7 {{post-preview markdown=body}} 8 </div> 9 </div>10 11 <div style="clear:both">12 <button style="float:left" {{action "preview"}}>Preview</button>13 <button style="float:left" {{action "submit"}}>Submit</button>14 </div>
Thursday, September 19, 13
Great! HTML.
Thursday, September 19, 13
HTML dictates layout.
Thursday, September 19, 13
HTML dictate layout.Templates
Thursday, September 19, 13
AND YOUR ARCHITECTURE
Thursday, September 19, 13
1 <div style="clear:both"> 2 <div style="float:left"> 3 {{input value=title}} 4 {{textarea value=body}} 5 </div> 6 <div style="float:left"> 7 {{post-preview markdown=body}} 8 </div> 9 </div>10 11 <div style="clear:both">12 <button style="float:left" {{action "preview"}}>Preview</button>13 <button style="float:left" {{action "submit"}}>Submit</button>14 </div>
ACTION DOESNT TALK TO COMPONENTS
Thursday, September 19, 13
1 <div style="clear:both"> 2 <div style="float:left"> 3 {{input value=title}} 4 {{textarea value=body}} 5 </div> 6 <div style="float:left"> 7 {{post-preview markdown=body}} 8 </div> 9 </div>10 11 <div style="clear:both">12 <button style="float:left" {{action "preview"}}>Preview</button>13 <button style="float:left" {{action "submit"}}>Submit</button>14 </div>
CHASM OF DOM
Thursday, September 19, 13
1 <div style="clear:both"> 2 <div style="float:left"> 3 {{input value=title}} 4 {{textarea value=body}} 5 </div> 6 <div style="float:left"> 7 {{post-preview markdown=body viewName="preview"}} 8 </div> 9 </div>10 11 <div style="clear:both">12 <button style="float:left" {{action "preview" target=view.preview}}>Preview</button>13 <button style="float:left" {{action "submit"}}>Submit</button>14 </div>
WORKAROUND 1
Thursday, September 19, 13
1 <div style="clear:both"> 2 <div style="float:left"> 3 {{input value=title}} 4 {{textarea value=body}} 5 </div> 6 <div style="float:left"> 7 {{view App.PostPreview markdownBinding=body viewName="preview"}} 8 </div> 9 </div>10 11 <div style="clear:both">12 <button style="float:left" {{action "preview" target=view.preview}}>Preview</button>13 <button style="float:left" {{action "submit"}}>Submit</button>14 </div>
WORKAROUND 2
Thursday, September 19, 13
1 <div style="clear:both"> 2 <div style="float:left"> 3 {{input value=title}} 4 {{textarea value=body}} 5 </div> 6 <div style="float:left"> 7 {{render "post_preview" body}} 8 </div> 9 </div>10 11 <div style="clear:both">12 <button style="float:left" {{action "preview"}}>Preview</button>13 <button style="float:left" {{action "submit"}}>Submit</button>14 </div>
1 App.ApplicationRoute = Ember.Route.extend({2 actions: {3 preview: function(){4 var controller = this.controllerFor('post_preview');5 controller.updatePreview();6 }7 }8 });
WORKAROUND 3
Thursday, September 19, 13
WORKAROUND 4
1 App.ApplicationRoute = Ember.Route.extend({ 2 renderTemplate: function(){ 3 this._super.apply(this, arguments); 4 this.render('post_preview', { 5 into: 'application', 6 outlet: 'preview', 7 controller: 'post_preview' 8 }); 9 },10 actions: {11 preview: function(){12 var app = this.controllerFor('application');13 var preview = this.controllerFor('post_preview');14 preview.set('markdown', app.get('body'));15 }16 }17 });18 19 App.PostPreviewController = Ember.Controller.extend();
1 <div style="clear:both"> 2 <div style="float:left"> 3 {{input value=title}} 4 {{textarea value=body}} 5 </div> 6 <div style="float:left"> 7 {{outlet "preview"}} 8 </div> 9 </div>10 11 <div style="clear:both">12 <button style="float:left" {{action "preview"}}>Preview</button>13 <button style="float:left" {{action "submit"}}>Submit</button>14 </div>
Thursday, September 19, 13
When we pick between these options...
Thursday, September 19, 13
We make design decisions.
Thursday, September 19, 13
• Re-usability as ui component• re-usability as action• If an action fires• Where the action is handled• The internals of preview
Thursday, September 19, 13
ARCHITECTURE
Thursday, September 19, 13
Architecture in Ember apps is dictated by routes and templates.
Thursday, September 19, 13
Routes and templates decide how actions propagate the controller/route tree, scope access
to dependencies, and are most subject to external constraints.
Thursday, September 19, 13
Routes and templates decide how actions propagate the controller/route tree, scope access
to dependencies, and are most subject to external constraints.
Thursday, September 19, 13
Routes and templates decide how actions propagate the controller/route tree, scope access
to dependencies, and are most subject to external constraints.
Thursday, September 19, 13
Routes and templates decide how actions propagate the controller/route tree, scope access
to dependencies, and are most subject to external constraints.
Thursday, September 19, 13
Routes and templates decide how actions propagate the controller/route tree, scope access
to dependencies, and are most subject to external constraints.
Thursday, September 19, 13
“Understanding actions in two easy steps”
Thursday, September 19, 13
#1: Bubbling
Thursday, September 19, 13
WARNING
THEse slides describe the behavior IN rc8 and beyond. behavior before the changes in rc8 is very similar to
what is described here, but not identical.
Thursday, September 19, 13
ACTION BUBBLING
1 var Actionable = Ember.Object.extend(Ember.ActionHandler);2 3 var firstTarget = Actionable.create();4 5 firstTarget.send("Wie Geht's");6 7 // Nothing!
Thursday, September 19, 13
ACTION BUBBLING
1 var Actionable = Ember.Object.extend(Ember.ActionHandler); 2 3 var firstTarget = Actionable.extend({ 4 actions: { 5 "Wie Geht's": function(){ 6 console.log("Danke gut"); 7 } 8 } 9 }).create();10 11 firstTarget.send("Wie Geht's");12 13 // Danke gut
Thursday, September 19, 13
ACTION BUBBLING
1 var Actionable = Ember.Object.extend(Ember.ActionHandler); 2 3 var secondTarget = Actionable.extend({ 4 actions: { 5 "Wie Geht's": function(){ 6 console.log("Und dir?"); 7 } 8 } 9 }).create();10 11 var firstTarget = Actionable.extend({12 target: secondTarget,13 actions: {14 "Wie Geht's": function(){15 console.log("Danke gut");16 }17 }18 }).create();19 20 firstTarget.send("Wie Geht's");21 22 // Danke gut
Thursday, September 19, 13
ACTION BUBBLING 1 var Actionable = Ember.Object.extend(Ember.ActionHandler); 2 3 var secondTarget = Actionable.extend({ 4 actions: { 5 "Wie Geht's": function(){ 6 console.log("Und dir?"); 7 } 8 } 9 }).create();10 11 var firstTarget = Actionable.extend({12 target: secondTarget,13 actions: {14 "Wie Geht's": function(){15 console.log("Danke gut");16 return true;17 }18 }19 }).create();20 21 firstTarget.send("Wie Geht's");22 23 // Danke gut24 // Und dir?
RETUrn true to continue bubbling the action
Thursday, September 19, 13
ACTION BUBBLING
1 var Actionable = Ember.Object.extend(Ember.ActionHandler); 2 3 var secondTarget = Actionable.extend({ 4 actions: { 5 "Wie Geht's": function(){ 6 console.log("Und dir?"); 7 } 8 } 9 }).create();10 11 var firstTarget = Actionable.extend({12 target: secondTarget,13 actions: {14 "Wie Geht's": null, // Or just don't define actions15 }16 }).create();17 18 firstTarget.send("Wie Geht's");19 20 // Und dir?
Thursday, September 19, 13
Controllers, Routes, Views, and Components handle actions.
Thursday, September 19, 13
Only Controllers and Routes have targets for bubbling.
Thursday, September 19, 13
#1: ROUTES ARE stateS. Kind of.
Thursday, September 19, 13
ROUTES ARE STATES 1 var moodManager = Ember.StateManager.create({ 2 initialState: 'good', 3 good: Ember.State.create({ 4 gut: function(){ 5 console.log('Ja'); 6 } 7 }), 8 bad: Ember.State.create({ 9 gut: function(){10 console.log('Nein');11 }12 }),13 quiet: Ember.State.create()14 });15 16 moodManager.send('gut');17 // Ja18 19 moodManager.transitionTo('bad');20 moodManager.send('gut');21 // Nein22 23 moodManager.transitionTo('quiet');24 moodManager.send('gut');25 // Uncaught Error: <Ember.StateManager:ember138> could not respond to event gut in state quiet.
Thursday, September 19, 13
ROUTES ARE STATES 1 App = Ember.Application.create(); 2 3 App.Router.map(function(){ 4 this.route('good'); 5 this.route('bad'); 6 this.route('quiet'); 7 }); 8 9 App.GoodRoute = Ember.Route.extend({10 actions: { gut: function(){ console.log('Ja'); } }11 });12 13 App.BadRoute = Ember.Route.extend({14 actions: { gut: function(){ console.log('Nein'); } }15 });16 17 App.QuietRoute = Ember.Route.extend();
1 {{! application.hbs }}2 <a {{action "gut"}}>Gut?</a>3 {{#link-to "good"}}Get happy{{/link-to}}4 {{#link-to "bad"}}Get sour{{/link-to}}5 {{#link-to "quiet"}}Get quiet{{/link-to}}
Thursday, September 19, 13
ROUTES ARE STATES 1 App = Ember.Application.create(); 2 3 App.Router.map(function(){ 4 this.route('good'); 5 this.route('bad'); 6 this.route('quiet'); 7 }); 8 9 App.GoodRoute = Ember.Route.extend({10 actions: { gut: function(){ console.log('Ja'); } }11 });12 13 App.BadRoute = Ember.Route.extend({14 actions: { gut: function(){ console.log('Nein'); } }15 });16 17 App.QuietRoute = Ember.Route.extend();
1 {{! application.hbs }}2 <a {{action "gut"}}>Gut?</a>3 {{#link-to "good"}}Get happy{{/link-to}}4 {{#link-to "bad"}}Get sour{{/link-to}}5 {{#link-to "quiet"}}Get quiet{{/link-to}}
ACTIOns always hit the leaf route, regardless
of where they fire from
Thursday, September 19, 13
ROUTES ARE STATES 1 App = Ember.Application.create(); 2 3 App.Router.map(function(){ 4 this.route('good'); 5 this.route('bad'); 6 this.route('quiet'); 7 }); 8 9 App.GoodRoute = Ember.Route.extend({10 actions: { gut: function(){ console.log('Ja'); } }11 });12 13 App.GoodController = Ember.Controller.extend({14 actions: { gut: function(){ console.log('ignored :-('); } }15 });
1 {{! application.hbs }}2 <a {{action "gut"}}>Gut?</a>3 {{#link-to "good"}}Get happy{{/link-to}}4 {{#link-to "bad"}}Get sour{{/link-to}}5 {{#link-to "quiet"}}Get quiet{{/link-to}}
BUT the TEMPLATE DECIDES WHICH CONTROLLERS SEE
THAT ACTION
Thursday, September 19, 13
ACTIONS ON CONTROLLERS
• couples template to controller• should not force use of NEEDs
ACTIONS ON routes
• have access to all controllers• handled from any template on a page
Thursday, September 19, 13
Choose where to put an action.
Thursday, September 19, 13
Routes and templates decide how actions propagate the controller/route tree, scope access
to dependencies, and are most subject to external constraints.
Thursday, September 19, 13
1 <div style="clear:both"> 2 <div style="float:left"> 3 {{input value=title}} 4 {{textarea value=body}} 5 </div> 6 <div style="float:left"> 7 {{post-preview markdown=body viewName="preview"}} 8 </div> 9 </div>10 11 <div style="clear:both">12 <button style="float:left" {{action "preview" target=view.preview}}>Preview</button>13 <button style="float:left" {{action "submit"}}>Submit</button>14 </div>
setting viewName causes a property named
“preview” to be added on the parentview of that
view with it’s own instance
that “preview” property can be set as a target
Thursday, September 19, 13
• Components access nothing• views access parentView, controller• controllers access A target (the route or a parent controller) AND OTHER CONTROLLERS via needs• routes access all controllers and models• CHEAT WITH REGISTER/INJECT
Thursday, September 19, 13
CHEAT WITH REGISTER/INJECT
1 App = Ember.Application.create(); 2 3 App.inject('route', 'session', 'controller:session'); 4 5 App.IndexRoute = Ember.Route.extend({ 6 beforeModel: function(){ 7 console.log( this.get('session.user.name') ); 8 } 9 });10 11 App.SessionController = Ember.Controller.extend({12 user: { name: 'Bob' }13 });
Thursday, September 19, 13
1 {{render "post"}}2 {{render "post" post}}3 {{component content=post}}4 {{view App.PostView contentBinding="post"}}5 {{template "post"}}
THIS
dictates part of your architecture
Thursday, September 19, 13
When confused about an app, look to the templates and the routes first.
Thursday, September 19, 13
“Controllers have lots of features!”
AKA
BUT PLEASE DON’t MAKE THEM CLEVER
Thursday, September 19, 13
“I AM SO CLEVER THAT SOMETIMES I DON’T
UNDERSTAND A SINGLE WORD OF WHAT I AM SAYING”
Oscar Wilde
Thursday, September 19, 13
Clever Controller 1
1 App.BooksController = Ember.ArrayController.extend({2 needs: ['library'],3 content: Em.computed.alias('controllers.library.books')4 });
Thursday, September 19, 13
Clever Controller 1
1 App.BooksController = Ember.ArrayController.extend({2 needs: ['library'],3 content: Em.computed.alias('controllers.library.books')4 });
assumption about library controller
Assumed to only be attached to a route (no item controller)
assumes books belong to a single library
Thursday, September 19, 13
LESS Clever Controller 1
1 App.BooksRoute = Ember.Route.extend({2 model: function(){3 this.modelFor('library').get('books')4 }5 });6 7 // The Ember controller provided by convention is sufficient.
Thursday, September 19, 13
1 <div> 2 <ul class="tabs"> 3 <li {{action "openSignIn"}}>Sign In</li> 4 <li {{action "openSignUp"}}>Sign Up</li> 5 <li {{action "openForgotPassword"}}>Forgot Password</li> 6 </ul> 7 {{#if isSignInOpen}}{{template "sign_in"}}{{/if}} 8 {{#if isSignUpOpen}}{{template "sign_up"}}{{/if}} 9 {{#if isForgotPasswordOpen}}{{template "forgot_password"}}{{/if}}10 </div>
Clever Controller 2 1 App.SessionController = Em.Controller.extend({ 2 isSignInOpen: false, 3 isSignUpOpen: false, 4 isForgotPasswordOpen: false, 5 6 actions: { 7 closeOptions: function(){ 8 this.setProperties({ 9 isSignInOpen: false,10 isSignUpOpen: false,11 isForgotPasswordOpen: false12 });13 },14 openSignIn: function(){ this.closeOptions(); this.set('isSignUpOpen', true); },15 openSignUp: function(){ this.closeOptions(); this.set('isSignUpOpen', true); },16 openForgotPassword: function(){ this.closeOptions(); this.set('isForgotPasswordOpen', true); }17 }18 });
Thursday, September 19, 13
1 <div> 2 <ul class="tabs"> 3 <li {{action "openSignIn"}}>Sign In</li> 4 <li {{action "openSignUp"}}>Sign Up</li> 5 <li {{action "openForgotPassword"}}>Forgot Password</li> 6 </ul> 7 {{#if isSignInOpen}}{{template "sign_in"}}{{/if}} 8 {{#if isSignUpOpen}}{{template "sign_up"}}{{/if}} 9 {{#if isForgotPasswordOpen}}{{template "forgot_password"}}{{/if}}10 </div>
Clever Controller 2 1 App.SessionController = Em.Controller.extend({ 2 isSignInOpen: false, 3 isSignUpOpen: false, 4 isForgotPasswordOpen: false, 5 6 actions: { 7 closeOptions: function(){ 8 this.setProperties({ 9 isSignInOpen: false,10 isSignUpOpen: false,11 isForgotPasswordOpen: false12 });13 },14 openSignIn: function(){ this.closeOptions(); this.set('isSignUpOpen', true); },15 openSignUp: function(){ this.closeOptions(); this.set('isSignUpOpen', true); },16 openForgotPassword: function(){ this.closeOptions(); this.set('isForgotPasswordOpen', true); }17 }18 });
NOT. VERY. DRY.
NEW PANELS MUST BE ADDED ON COnTROLLER
NEW PANELS MUST BE ADDED IN TEMPLATEThursday, September 19, 13
1 <div> 2 <ul class="tabs"> 3 <li {{action "openSignIn"}}>Sign In</li> 4 <li {{action "openSignUp"}}>Sign Up</li> 5 <li {{action "openForgotPassword"}}>Forgot Password</li> 6 </ul> 7 {{#if isSignInOpen}}{{template "sign_in"}}{{/if}} 8 {{#if isSignUpOpen}}{{template "sign_up"}}{{/if}} 9 {{#if isForgotPasswordOpen}}{{template "forgot_password"}}{{/if}}10 </div>
Clever Controller 2 1 App.SessionController = Em.Controller.extend({ 2 isSignInOpen: false, 3 isSignUpOpen: false, 4 isForgotPasswordOpen: false, 5 6 actions: { 7 closeOptions: function(){ 8 this.setProperties({ 9 isSignInOpen: false,10 isSignUpOpen: false,11 isForgotPasswordOpen: false12 });13 },14 openSignIn: function(){ this.closeOptions(); this.set('isSignUpOpen', true); },15 openSignUp: function(){ this.closeOptions(); this.set('isSignUpOpen', true); },16 openForgotPassword: function(){ this.closeOptions(); this.set('isForgotPasswordOpen', true); }17 }18 });
NAV & DISPLAYED PANEL TIGHTLY COUPLED
panels cannot be controlled from other scopes
Thursday, September 19, 13
LESS Clever Controller 2
1 <div>2 <ul class="tabs">3 <li {{bind-attr class="isSignInOpen:active"}}{{action "openPanel" "signIn"}}>Sign In</li>4 <li {{bind-attr class="isSignUpOpen:active"}}{{action "openPanel" "signUp"}}>Sign Up</li>5 <li {{bind-attr class="isForgotPasswordOpen:active"}}{{action "openPanel" "forgotPassword"}}>Forgot Password</li>6 </ul>7 {{outlet "panel"}}8 </div>
1 App.SessionRoute = Ember.Router.extend({ 2 setupController: function(){ 3 this._super.apply(this, arguments); 4 Em.run.once(this, function(){ 5 this.send('openPanel', 'signIn'); 6 }); 7 }, 8 actions: { 9 openPanel: function(panel){10 this.controller.set('openPanel', panel);11 this.render('panels/'+panel, {12 into: 'session',13 outlet: 'panel'14 });15 }16 }17 });18 19 App.SessionController = Ember.Controller.extend({20 isSignInOpen: Em.computed.equal('openPanel', 'signIn'),21 isSignUpOpen: Em.computed.equal('openPanel', 'signUp'),22 isForgotPasswordOpen: Em.computed.equal('openPanel', 'forgotPassword')23 });
Thursday, September 19, 13
1 {{#each controller}}2 {{name}}3 {{/each}}
Clever Controller 3
1 App = Ember.Application.create(); 2 3 App.IndexRoute = Ember.Route.extend({ 4 model: function(){ 5 return Ember.A([ 6 { name: 'Munster' }, 7 { name: 'Alpine Lace' }, 8 { name: 'Tome' } 9 ]);10 }11 });
Thursday, September 19, 13
1 {{#each controller}}2 {{name}}3 {{/each}}
Clever Controller 3
1 App = Ember.Application.create(); 2 3 App.IndexRoute = Ember.Route.extend({ 4 model: function(){ 5 return Ember.A([ 6 { name: 'Munster' }, 7 { name: 'Alpine Lace' }, 8 { name: 'Tome' } 9 ]);10 }11 });
APPle invasion! apple invasion! apple invasion!
Thursday, September 19, 13
1 {{#each controller}}2 {{name}}3 {{/each}}
Clever Controller 3 1 App = Ember.Application.create(); 2 3 App.IndexRoute = Ember.Route.extend({ 4 model: function(){ 5 return Ember.A([ 6 { name: 'Munster' }, 7 { name: 'Alpine Lace' }, 8 { name: 'Tome' } 9 ]);10 }11 });12 13 App.IndexController = Ember.ArrayController.extend({14 itemController: 'fruitInvasion'15 });16 17 App.FruitInvasionController = Ember.ObjectController.extend({18 name: function(){19 return 'Apple invasion!';20 }.property()21 });
APPle invasion! apple invasion! apple invasion!
Changes context in the template from outside the
template.
Thursday, September 19, 13
1 {{#each controller itemController='fruitInvasion'}}2 {{name}}3 {{/each}}
LESS Clever Controller 3
1 App = Ember.Application.create(); 2 3 App.IndexRoute = Ember.Route.extend({ 4 model: function(){ 5 return Ember.A([ 6 { name: 'Munster' }, 7 { name: 'Alpine Lace' }, 8 { name: 'Tome' } 9 ]);10 }11 });12 13 App.FruitInvasionController = Ember.ObjectController.extend({14 name: function(){15 return 'Apple invasion!';16 }.property()17 });
Thursday, September 19, 13
“TOO CLEVER IS DUMB”Ogden Nash
Thursday, September 19, 13
TL;DR
Thursday, September 19, 13
Don’t work so hard in controllers.
Thursday, September 19, 13
Look to routes and templates for your application architecture.
Thursday, September 19, 13
Thanks!
@mixonic
httP://madhatted.com
Thursday, September 19, 13