Beruflich Dokumente
Kultur Dokumente
trek.github.com
1/19
19/01/13
vrc=ti; a hs ti.nid) hsubn(; ty{ r ti.eeetpue) hs_lmn.as(, ti.eeetrmvEetitnr"ro" ti.tigrro) hs_lmn.eoevnLsee(err, hs_rgeErr, ti.eeetrmvEetitnr"ne" ti.tigrn) hs_lmn.eoevnLsee(edd, hs_rgeEd, ti.eeetrmvEetitnr"apa" ti.tigred) hs_lmn.eoevnLsee(cnly, hs_rgeRay, ti.eeetrmvEetitnr"oddeaaa,ti.oLaeMt hs_lmn.eoevnLsee(laemtdt" hs_noddea _ec(,fnto (){ .ahb ucin a c_lmn.eoevnLsee(,c_uberflnEet .eeetrmvEetitnra .bblPoiigvn) }, ) _ec(,fnto (){ .aha ucin a c_lmn.eoevnLsee(,c_oEet .eeetrmvEetitnra .lgvn) }, ) ti.eeet=nl, hs_lmn ul ti.rge(dsry) hstigr"eto" }cth(){ ac d } }
If this doesn't look familiar, you're in for a world of hurt when you try to parlay your Backbone skills into something Rdio-sized. Backbone is the best solution, hands down, for apps where the user comes to a page, interacts with the application for a short time before moving on, letting the views and the models get thrown away. Beyond that, it requires increasing diligence and expertise on your part. So, here's my pitch: I want you to learn Ember. Not instead of Backbone or Angular but in addition to them. There's a lot of writing comparing the three, but once you become familiar with them you'll see it's a totally nonsensical comparison. Although their output is the same (i.e. a "web app") they just don't belong in the same category. I apologize for the long preamble, but we're exploring some concepts and I want to be sure you're willing to allow that their strangeness are not due to the poor architecture of Ember but to your unfamiliarity with them. If you're willing to learn no matter how funky weird things appear at first, read on. If you're looking to troll then just skim on: you'll find lots of things to highlight when you create a troll twitter account with a single post maligning a tool you've never bothered to explore.
trek.github.com
2/19
19/01/13
This line creates a new instance of E b r A p i a i n which does two handy things: me.plcto provides one location for all your objects so we avoid polluting the global namespace. creates one listener for each user event (e.g. 'click') and controls event delegation.
Views in Ember are responsible for: determining the structure of a section of the application's rendered HTML. responding to delegated user events. In the above view we will change structure of the page only through the view's template, which will render as the contents of the view's tag, but we could also provide a different tag name, id, css class, or other HTML attributes for the rendered element. Your application must have an A p i a i n i w property. An instance of this class will plctoVe be created for you and inserted into the application's view hierarchy as the root view. ApApiainotolr=EbrCnrle.xed) p.plctoCnrle me.otolretn(;
Every view has a rendering context. This is the object where Handlebars templates will look for properties. So, if your template looks like this: {nm} {ae}
and its rendering context has a n m property, you'll see the value outputted. If there is ae no property, you'll see nothing. A single instance of A p i a i n o t o l r will be created for you and automatically plctoCnrle set as the rendering context of the A p i a i n i w. This is obvious but bears plctoVe mentioning: your application must have an A p i a i n o t o l r property. If it plctoCnrle lacked one, the application's root view would have no rendering context and would be pretty useless except for displaying static content. Ember enforces the presence of this property by throwing an error if it's missing. ApRue =EbrRue.xed{ p.otr me.otretn( ro:EbrRueetn( ot me.ot.xed{ idx EbrRueetn( ne: me.ot.xed{
trek.github.com
3/19
19/01/13
A R u e in Ember behaves significantly different than you probably suspect if you otr have experience with other javascript libraries using the 'router' label. Ember's R u e otr class is a subclass of its more general purpose S a e a a e Most browser-routers t t M n g r. are just copying the routing pattern from familiar server technologies. But HTTP is specifically a stateless protocol and the techniques for routing on the server are missing important abilities when translated into the stateful environment of browser application development. Your application's router is responsible for tracking the state of your application and affecting the application's view hierarchy in response to state change. It is also responsible for serializing this state into a single string the URL and for later deserializing the string into a usable application state. Rather than being a central organizing technique, URLs are just a useful side effect of state change. States are the central feature of an Ember application. Yes, property observations and automatic view updates are handy, but if that's all Ember offered it would be only a fraction as useful for serious and robust development. .. . ro:EbrRueetn( ot me.ot.xed{ idx EbrRueetn( ne: me.ot.xed{ rue '' ot: / } ) } ) .. .
Your router must have these two routes. The first, r o really just acts as a container o t, for all subsequent routes. You can think of it as the route set, rather than a proper route itself. The second i d x, can be called whatever you like. The key feature is that it has a ne r u e property of ' '. When your application loads, Ember will look through its ot / internal route maps to find one that matches the url in the browser. If you enter the application at the url ' ' your Router will automatically transition into this state. / Finally your application starts the routing process, sets up the necessary internal structure based on configuration we've done earlier, and inserts an instance of your A p i a i n i w (with an instance of A p i a i n o t o l r as its rending plctoVe plctoCnrle context) into the page.
Building Up An Application
From here, we can start building up an application by adding states to our router, navigable elements in our templates to allow a user to begin manipulating states, and views that are inserted in response to these state changes. We'll create a tiny application that lets you see information about committers to the main Ember repository. Let's start that process by adding some markup and an o t e into our currently empty ult a p i a i n template: plcto <cittp=tx/-adeas dt-epaenm=apiain> srp ye"etxhnlbr" aatmlt-ae"plcto"
trek.github.com
4/19
19/01/13
An o t e helper defines sections of a template where we will change specific portions ult of the view hierarchy in response to state change. Any template (not just the root one) can have any number of outlets (if you give them names). This lets you express really nuanced view hierarchies with minimal effort. Next, add a view for all contributors and a matching controller and template: ApAlotiuosotolr=EbrAryotolretn(; p.lCnrbtrCnrle me.raCnrle.xed) ApAlotiuosiw=EbrVe.xed{ p.lCnrbtrVe me.iwetn( tmltNm:'otiuos epaeae cnrbtr' }; )
/ i yu pg bd o ha: / n or ae oy r ed <cittp=tx/-adeas dt-epaenm=cnrbtr" srp ye"etxhnlbr" aatmlt-ae"otiuos> {#ahpro i cnrle} {ec esn n otolr} {pro.oi} {esnlgn} {/ah} {ec} <srp> /cit
Within the state that matches the index path ( ' '), implement a c n e t u l t / oncOtes method. It takes a single argument that will be your application's router. Within that method get the single instance of our A p i a i n o t o l r class and connect its plctoCnrle outlet with the c n e t u l t method: oncOte idx EbrRueetn( ne: me.ot.xed{ rue '' ot: /, cnetult:fnto(otr{ oncOtes ucinrue) rue.e(apiainotolr)cnetult'lCnrbtr' otrgt'plctoCnrle'.oncOte(alotiuos } } )
Give your application a reload. You won't see much yet, but this will let you catch any console errors now. Let me assuage your obvious fears right now: Yes, this is a lot of code. Yes, it seems weirdly complex. Yes, you could accomplish this same trivial task in Backbone or Angular with far less code. Ember isn't targeting applications with this minimal level of sophistication so it seems foolishly verbose when starting out. That said, this is the single central pattern to an application. Once you master it, you'll be cranking out applications like a pro. Ember applications start out with a complexity rating of 4/10 but never get much higher than 6/10, regardless of how sophisticated your application becomes. Backbone starts out at 1/10 but complexity grows linearly. This is a natural side effect of the types of applications the two frameworks were specifically created for. Let's unpack our new code, in reverse: idx EbrRueetn( ne: me.ot.xed{ rue '' ot: /,
trek.github.com
5/19
19/01/13
When your application is loaded at the url ' ', Ember will automatically transition the / application into the state I've called i d x. c n e t u l t is called on this state. It ne oncOtes acts as a callback for us to connect sections of our view hierarchy (designated with { o t e } to specific views based on the state. In this case I want to connect the { u l t }) { o t e } in our application template with markup for all our contributors so I access {ult} the application's single shared instance of A p i a i n o t o l r and call plctoCnrle c n e t u l t on it with ' l C n r b t r ' as an argument. oncOte alotiuos When our application is started, a single shared instance of each controller is created for us. Because you'll most likely access this instance from the router, it's placed as a property of the router with a name that matches the controller's classname but converted to lower-camel style: A p i a i n o t o l r's single instance is stored as plctoCnrle a p i a i n o t o l r. plctoCnrle Controllers have the ability to connect outlets in the views they control. In the above example, I'm calling c n e t u l t with ' l C n r b t r ' as an argument. This oncOte alotiuos will create an instance of A l o t i u o s i w for us, set the shared instance of lCnrbtrVe A l o t i u o s o t o l r as the view's default rendering context, and insert it lCnrbtrCnrle into our view hierarchy at the point where { o t e } appears in the application {ult} template. The second argument, which I've hard coded as an array of two object literals, is set as the c n e t of the controller instance. (Those who fear this kind of "magic" are otn free to read the documentation for Controllers to see the full, maximally verbose and explicit arguments you can pass). ApAlotiuosotolr=EbrAryotolretn(; p.lCnrbtrCnrle me.raCnrle.xed) ApAlotiuosiw=EbrVe.xed{ p.lCnrbtrVe me.iwetn( tmltNm:'otiuos epaeae cnrbtr' }; )
The A l o t i u o s o t o l r is a subclass of Ember's A r y o t o l r lCnrbtrCnrle raCnrle class. A r y o t o l rs act as containers for array-like objects in Ember and simply raCnrle proxy undefined properties or methods to the underlying c n e t array. otn In our template, the each call ( { e c p r o i c n r l e } is passed along to { a h e s n n o t o l r }) the c n e t of our ArrayController which I've hard-coded as an array of two object otn literals with a single property each. <cittp=tx/-adeas dt-epaenm=cnrbtr" srp ye"etxhnlbr" aatmlt-ae"otiuos> {#ahpro i cnrle} {ec esn n otolr} {pro.oi} {esnlgn} {/ah} {ec} <srp> /cit
trek.github.com
6/19
19/01/13
wrappers around $ a a ( that follow a Rails-style-REST pattern really merit the .jx) "persistence layer" label. And many other frameworks are starting to realize this too as their thin $ a a ( delegation is being fleshed out to handle the real and difficult .jx) problems of reliably synchronizing data between two environments when there are few structural standards to rely on. The real, valid criticism is that nobody who knows Ember has offered much guidance for how to handle data loading within an Ember application. Ember, as I'm trying to convince you, has valuable patterns you've never used before and it's totally unfair to maintain this assertion while simultaneously expecting you to know how to combine these patterns with data loading solutions you've previously used. The best advice I can offer is: always be returning. Ember relies on the immediate availability of data objects even if the underlying content of those objects is still loading. This is almost certainly different than asynchronous patterns for data loading you've used before. Let's step through how this works, one concern at a time. In our application so far, I've punted on data and just hard coded something into our index state: idx EbrRueetn( ne: me.ot.xed{ rue '' ot: /, cnetult:fnto(otr{ oncOtes ucinrue) rue.e(apiainotolr)cnetult'lCnrbtr' otrgt'plctoCnrle'.oncOte(alotiuos } } )
I'd much prefer to delegate that data loading out to a proper object. Let's call to a as yet unimplemented C n r b t r class and a f n method on that class: otiuo id idx EbrRueetn( ne: me.ot.xed{ rue '' ot: /, cnetult:fnto(otr{ oncOtes ucinrue) rue.e(apiainotolr)cnetult'lCnrbtr' otrgt'plctoCnrle'.oncOte(alotiuos } } )
And now implement this object: ApCnrbtr=EbrOjc.xed) p.otiuo me.betetn(; ApCnrbtrroeCas{ p.otiuo.epnls( fn:fnto({ id ucin)} }; )
This creates a new class and reopens that class to add a class (sometimes called 'static') method: If you reload your application you'll see that nothing renders now. This is because we've set the c n e t of our A l o t i u o s o t o l r to undefined which is the otn lCnrbtrCnrle default return value of our new f n method. Let's apply some $ a a to the id .jx method:
trek.github.com
7/19
19/01/13
ApCnrbtrroeCas{ p.otiuo.epnls( fn:fnto({ id ucin) $aa( .jx{ ul 'tp:/p.ihbcmrpsebrsebrj/otiuos r: hts/aigtu.o/eo/mej/me.scnrbtr' dtTp:'sn' aaye jop, sces fnto(epne{ ucs: ucinrsos) rtr rsos.aa eun epnedt; } } ) } }; )
Reload your application and you'll see there is still no change because, although we request data, f n still has no return value. It's here that people usually code id themselves into a corner trying to get their previous experience with ajax to fit into Ember patterns, give up, and post a StackOverflow question. There are a few solutions to this problem but the easiest for us now is to just make sure we're returning an array:
ApCnrbtrroeCas{ p.otiuo.epnls( alotiuos [, lCnrbtr: ] fn:fnto({ id ucin) $aa( .jx{ ul 'tp:/p.ihbcmrpsebrsebrj/otiuos r: hts/aigtu.o/eo/mej/me.scnrbtr' dtTp:'sn' aaye jop, cnet ti, otx: hs sces fnto(epne{ ucs: ucinrsos) rsos.aafrahfnto(otiuo) epnedt.oEc(ucincnrbtr{ ti.lCnrbtr.dOjc(p.otiuo.raecnrbt hsalotiuosadbetApCnrbtrcet(otiuo } ti) , hs } } ) rtr ti.lCnrbtr; eun hsalotiuos } }; )
I've changed f n to immediately return an array ( t i . l C n r b t r which id h s a l o t i u o s) starts out empty. This will become the c n e t of our controller, which is the default otn rendering context for the view. When the view first renders it will loop over the empty array and insert nothing into the page. When the ajax call is successful we loop through the response from the server turning each chunk of JSON into an instance of our C n r b t r class, and add it to the array. Ember's property notification system will otiuo trigger a view re-render for just the affected sections of the page. Because Ember has a good property observation system we can handle the asynchronicity from multiple points within the application structure where it's most appropriate rather than being forced to handle it at the communication layer. If you reload the application you'll see an empty page before the view updates when the data is loaded. If we were writing a slightly more complex application, we could use a library by the core team called Ember Data that would help with functionality like binding loading state to view display. It has far more ambitious goals than we'll need for demonstration: stateful data synchronization, property encoding and decoding, identity mapping, transactional communication, and more.
trek.github.com
8/19
19/01/13
ApRue =EbrRue.xed{ p.otr me.otretn( ro:EbrRueetn( ot me.ot.xed{ idx EbrRueetn( ne: me.ot.xed{ rue '' ot: /, cnetult:fnto(otr{ oncOtes ucinrue) rue.e(apiainotolr)cnetult'lCnrbtr otrgt'plctoCnrle'.oncOte(alotiuos } } ) } ) }; )
We'll add a sibling state to index for viewing just a single contributor. I'm also going to rename 'index' to the more descriptive state name of 'contributors':
ApRue =EbrRue.xed{ p.otr me.otretn( ro:EbrRueetn( ot me.ot.xed{ cnrbtr:EbrRueetn( otiuos me.ot.xed{ rue '' ot: /, cnetult:fnto(otr{ oncOtes ucinrue) rue.e(apiainotolr)cnetult'lCnrbtr otrgt'plctoCnrle'.oncOte(alotiuos } }, )
aotiuo:EbrRueetn( Cnrbtr me.ot.xed{ rue ':ihbsrae, ot: /gtuUeNm' cnetult:fnto(otr cnet{ oncOtes ucinrue, otx) rue.e(apiainotolr)cnetult'nCnrbtr otrgt'plctoCnrle'.oncOte(oeotiuo' } } ) } ) }; )
Examining this new state in isolation: aotiuo:EbrRueetn( Cnrbtr me.ot.xed{ rue ':ihbsrae, ot: /gtuUeNm' cnetult:fnto(otr cnet{ oncOtes ucinrue, otx) rue.e(apiainotolr)cnetult'nCnrbtr otrgt'plctoCnrle'.oncOte(oeotiuo' } } )
I've supplied a r u e property of ' : i h b s r a e which we'll use later to ot / g t u U e N m ', serialize and deserialize this state. I've implemented the c n e t u l t method with oncOtes two arguments: one to represent the entire router and one, called c n e t, which will otx help answer the question "which contributor" later on. Inside c n e t u l t I've oncOtes accessed the shared instance of A p i a i n o t o l r and used it to connect the plctoCnrle outlet in its view (an instance of A p i a i n i w) to a pairing of plctoVe O e o t i u o V e O e o t i u o C n r l e which are unimplemented. n C n r b t r i w/ n C n r b t r o t o l r,
trek.github.com
9/19
19/01/13
Next, we'll update the application template to include a way for users to change the application's state from 'contributors' to 'aContributor' through interaction. Currently our template just loops and prints the l g n property of each contributor: oi <cittp=tx/-adeas dt-epaenm=cnrbtr" srp ye"etxhnlbr" aatmlt-ae"otiuos> {#ahpro i cnrle} {ec esn n otolr} {pro.oi} {esnlgn} {/ah} {ec} <srp> /cit
We're going to encase that login in an < > tag that includes a call to the { a t o } a {cin} helper: <cittp=tx/-adeas dt-epaenm=cnrbtr" srp ye"etxhnlbr" aatmlt-ae"otiuos> {#ahpro i cnrle} {ec esn n otolr} < {ato soCnrbtrpro}>{pro.oi} <a a {cin hwotiuo esn} {esnlgn} /> {/ah} {ec} <srp> /cit
The { a t o } helper goes within the opening tag of an element (here, the < >) {cin} a and takes two arguments. The first, s o C n r b t r, is the action we'd like to send to hwotiuo the current state of the application and the second, p r o will become the c n e t e s n, otx argument passed through various callbacks in the application's router. If you reload the application now you'll see that our logins have become links. With your console enabled, click any of the links. You'll see a warning that your application's router 'could not respond to event showContributor in state root.contributors'. Add this transition action to the 'contributors' state. I like to put my actions between route property definition and route API callbacks like c n e t u l t o n c O t e s: cnrbtr:EbrRueetn( otiuos me.ot.xed{ rue '' ot: /, soCnrbtr EbrRuetastoT(aotiuo', hwotiuo: me.ot.rniino'Cnrbtr) cnetult:fnto(otr{ oncOtes ucinrue) rue.e(apiainotolr)cnetult'lCnrbtr' otrgt'plctoCnrle'.oncOte(alotiuos } } )
The new action is written for us by the static method t a s t o T on the rniino E b r R u e class. You can write your transitions yourself (they're just functions), but me.ot E b r R u e t a s t o T saves you trouble of hand-writing a lot of similar looking me.ot.rniino functions. Pop back to the browser, reload the application, and try to transition again. This time, you'll be warned that we're missing our O e o t i u o V e class. The transition has nCnrbtriw occurred and we've reached the c n e t u l t callback on the 'aContributor' state, oncOtes but cannot properly connect our outlet yet without the missing view class. Implement this class and matching controller and template: ApOeotiuoVe =EbrVe.xed{ p.nCnrbtriw me.iwetn(
trek.github.com
10/19
19/01/13
/ i yu HM dcmn / n or TL ouet <cittp=tx/-adeas dt-epaenm=acnrbtr> srp ye"etxhnlbr" aatmlt-ae"-otiuo" {lgn}-{cnrbtos}cnrbtost Ebrj {oi} {otiuin} otiuin o me.s <srp> /cit
I've made O e o t i u o C n r l e an instance of E b r O j c C n r l e nCnrbtrotolr m e . b e t o t o l r. O j c C n r l e is like A r y o t o l r a tiny wrapper for objects that will betotolr raCnrle just proxy property and method access to its underlying c n e t property but for otn single objects instead of collections. If you reload the application and try to transition you should have more success. It might be handy to enable logging on your router to get a better feel for what is happening on transitions: ApRue =EbrRue.xed{ p.otr me.otretn( ealLgig tu, nbeogn: re / ohrpoete adderir / te rpris de ale } )
Let's unpack what's going on when we click that link. Ember has registered a listener on the < > element for you (so, no, this is nothing like going back to ye olde onclick days) a that will call the matching action name ( s o C n r b t r) on the t r e property hwotiuo agt of the view. It just so happens that the default target for any view is the application's router. The router will delegate this action name to the current state. If the action is present on the state, it will be called with the object you provided as the second argument to { a t o } as a context argument. If it's not present, the router will walk up through {cin} the state tree towards r o looking for a matching action name. ot Since our state does have a matching name, s o C n r b t r E b r R u e t a s t o T ( a o t i u o ' it's called. h w o t i u o : m e . o t . r n i i n o ' C n r b t r ), This function transitions the router to the name state ('aContributor') and calls its c n e t u l t callback with the router as the first argument and the context from oncOtes the { a t o } helper as the second argument: {cin} cnetult:fnto(otr cnet{ oncOtes ucinrue, otx) rue.e(apiainotolr)cnetult'nCnrbtr, otrgt'plctoCnrle'.oncOte(oeotiuo' }
Within this method, we access the single shared instance of A p i a i n o t o l r plctoCnrle and connect the outlet in its view (an instance of A p i a i n i w) by inserting an plctoVe instance of O e o t i u o V e with the single shared instance of nCnrbtriw O e o t i u o C n r l e as its default rendering context. nCnrbtrotolr The c n e t property of this controller is set to the passed c n e t argument. Since otn otx O e o t i u o C n r l e is a descendant of O j c C n r l e property nCnrbtrotolr b e t o t o l r, access in the view will proxy through the controller to this c n e t. otn
trek.github.com
11/19
19/01/13
The view renders and we see our updated view hierarchy in the browser.
The return value from a custom s r a i e method must be an object literal with keys eilz that match any dynamic slugs in the supplied r u e. The value for these keys will be ot placed in the url. Browse back to the root state of your application (i.e. go back to '/'), reload the application, and navigate back to the 'aContributor' state for any contributor. The url should update properly. Unfortunately if you reload the application at this particular state you'll see the URL updates to '#/undefined' again. When we load an Ember application at a particular url it will attempt to match and transition into a state with matching r u e pattern and call the state's ot c n e t u l t and s r a i e callbacks. When we reload at '#/kselden', for oncOtes eilz example, The router matches the state with the r u e pattern of '/:githubUserName', ot transitions into it, then calls c n e t u l t with the router as the first argument and oncOtes a second argument of ... no context at all. Finally, s r a i e is called, also with an eilz u d f n d context, and the property g t u U e N m is accessed on u d f n d neie ihbsrae neie and the URL is updated to '#/undefined'. Entering the application at a particular URL doesn't give our application access to previous loaded data so to fully load the matching state, we need to re-access this data. States have a callback d s r a i e for doing just this. There's a default implementation, but we eeilz can implement our own custom one as well:
trek.github.com
12/19
19/01/13
aotiuo:EbrRueetn( Cnrbtr me.ot.xed{ rue ':ihbsrae, ot: /gtuUeNm' cnetult:fnto(ot,cnet{ oncOtes ucinrue otx) rue.e(apiainotolr)cnetult'nCnrbtr otrgt'plctoCnrle'.oncOte(oeotiuo' } , sraie fnto(otr cnet{ eilz: ucinrue, otx) rtr {ihbsrae cnetgt'oi'} eun gtuUeNm: otx.e(lgn) } , dsraie fnto(otr ulaas{ eeilz: ucinrue, rPrm) rtr ApCnrbtrfnOeulaasgtuUeNm) eun p.otiuo.idn(rPrm.ihbsrae; } } )
Above, I've mocked out what I want this deserialization interface to look like. I'll call A p C n r b t r f n O e with the section of our url represented by p.otiuo.idn g t u U e N m and return this object. The return value of d s r a i e becomes ihbsrae eeilz the c n e t passed to c n e t u l t so I must immediately return an object that otx o n c O t e s, will get populated with data later. Let's add A p C n r b t r f n O e to allow for p.otiuo.idn passing a Github user name. Github allows us to access a user at '/users/a name', but this isn't within the context of a particular repository, so we won't have access to this users contribution count, which is part of the data we need. To see a particular users in the context of a repository we'll need to load them all and locally find the one we're looking for. This isn't exactly ideal, but unless you control development of both client and server it's typical. fnOe fnto(srae{ idn: ucinuenm) vrcnrbtr=ApCnrbtrcet( a otiuo p.otiuo.rae{ lgn uenm oi: srae }; ) $aa( .jx{ ul 'tp:/p.ihbcmrpsebrsebrj/otiuos r: hts/aigtu.o/eo/mej/me.scnrbtr' dtTp:'sn' aaye jop, cnet cnrbtr otx: otiuo, sces fnto(epne{ ucs: ucinrsos) ti.ePoete(epnedt.idrpry'oi' uenm hsstrprisrsos.aafnPoet(lgn, srae } } ) rtr cnrbtr eun otiuo; }
The order of execution for this method is: create a new C n r b t r object with otiuo l g n set immediately to the known value passed in as u e n m from within the oi srae 'aContributor' states's deserialize method. Then we set up some ajax and immediately return the C n r b t r instance. When the ajax completes we find just the contributor otiuo we're interested in by looking for the first result with a matching username (using f n P o e t and update the returned C n r b t r instance's properties with i d r p r y) otiuo s t r p r i s, which will trigger a view update of any sections bound to properties ePoete whose values have changed.
Repeat
That's an Ember application. States, transitioning between them, and loading data when you need it. You can build up surprisingly sophisticated and robust UIs by repeating this
trek.github.com
13/19
19/01/13
process until you're happy. Let's repeat this by adding a "back to all contributors" navigation to our template for a single contributor: Right now the template is pretty simple: <cittp=tx/-adeas dt-epaenm=acnrbtr> srp ye"etxhnlbr" aatmlt-ae"-otiuo" {lgn}-{cnrbtos}cnrbtost Ebrj {oi} {otiuin} otiuin o me.s <srp> /cit
Let's add an element with an action to transition back to the 'contributors' state: <cittp=tx/-adeas dt-epaenm=acnrbtr> srp ye"etxhnlbr" aatmlt-ae"-otiuo" <i> dv < {ato soAlotiuos}AlCnrbtr<a a {cin hwlCnrbtr}>l otiuos/> <dv /i> {lgn}-{cnrbtos}cnrbtost Ebrj {oi} {otiuin} otiuin o me.s <srp> /cit
Add this action to the 'aContributor' state: aotiuo:EbrRueetn( Cnrbtr me.ot.xed{ rue ':ihbsrae, ot: /gtuUeNm' soAlotiuos EbrRuetastoT(cnrbtr' hwlCnrbtr: me.ot.rniino'otiuos)
Done. Nested states are possible and encouraged as well. They come with only one caveat: you must end the transition between states on state that is a 'leaf' (i.e. has no child states of its own). As you convert states into more complex sets of nested states, either remember to directly transition to one of the child states or set an i i i l t t property. ntaSae Let's convert our simple 'aContributor' state into a more complex object with two child states. The parent 'aContributor' we'll use for fetching a contributor and displaying her name and number of commits. Then we'll provide two nested states: one 'details' for viewing additional details about the contributor and a second 'repos' for showing a list of their repositories. For reference, the 'aContributor' state looks like this: aotiuo:EbrRueetn( Cnrbtr me.ot.xed{ rue ':ihbsrae, ot: /gtuUeNm' cnetult:fnto(otr cnet{ oncOtes ucinrue, otx) rue.e(apiainotolr)cnetult'nCnrbtr otrgt'plctoCnrle'.oncOte(oeotiuo' } , sraie fnto(otr cnet{ eilz: ucinrue, otx) rtr {ihbsrae cnetgt'oi'} eun gtuUeNm: otx.e(lgn) } , dsraie fnto(otr ulaas{ eeilz: ucinrue, rPrm) rtr ApCnrbtrfnOeulaasgtuUeNm) eun p.otiuo.idn(rPrm.ihbsrae; } , } )
trek.github.com
14/19
19/01/13
And we'll change it to this: aotiuo:EbrRueetn( Cnrbtr me.ot.xed{ rue ':ihbsrae, ot: /gtuUeNm' cnetult:fnto(otr cnet{ oncOtes ucinrue, otx) rue.e(apiainotolr)cnetult'nCnrbtr otrgt'plctoCnrle'.oncOte(oeotiuo' } , sraie fnto(otr cnet{ eilz: ucinrue, otx) rtr {ihbsrae cnetgt'oi'} eun gtuUeNm: otx.e(lgn) } , dsraie fnto(otr ulaas{ eeilz: ucinrue, rPrm) rtr ApCnrbtrfnOeulaasgtuUeNm) eun p.otiuo.idn(rPrm.ihbsrae; } ,
/ cidsae / hl tts iiiltt:'eal' ntaSae dtis, dtis EbrRueetn( eal: me.ot.xed{ rue '' ot: /, cnetult:fnto(otr{ oncOtes ucinrue) rue.e(oeotiuoCnrle'.oncOte(dtis) otrgt'nCnrbtrotolr)cnetult'eal'; } }, ) rps EbrRueetn( eo: me.ot.xed{ rue 'rps, ot: /eo' cnetult:fnto(otr{ oncOtes ucinrue) rue.e(oeotiuoCnrle'.oncOte(rps) otrgt'nCnrbtrotolr)cnetult'eo'; } } ) } )
Examining each state in isolation: iiiltt:'eal' ntaSae dtis, dtis EbrRueetn( eal: me.ot.xed{ rue '' ot: /, cnetult:fnto(otr{ oncOtes ucinrue) rue.e(oeotiuoCnrle'.oncOte(dtis) otrgt'nCnrbtrotolr)cnetult'eal'; } } )
When we transition into 'aContributor', its callbacks ( c n e t u l t o n c O t e s, s r a i e, eilz optionally d s r a i e if we're transitioning during application load) are called. This eeilz means the { o t e } in our application template is filled with an instance of {ult} O e o t i u o V e with the shared instance of O e o t i u o C n r l e nCnrbtriw nCnrbtrotolr used as its default rendering context. The c n e t argument is passed from the otx { a t o s o C n r b t r c n r b t r }, through the transition, and into this {cin hwotiuo otiuo} callback. We then pass it along as the second argument to c n e t u l t and it oncOte becomes the c n e t property of the shared O e o t i u o C n r l e otn nCnrbtrotolr instance. Then, because we have i i i l t t defined the router immediately transitions into ntaSae the state 'aContributor.details' and calls its c n e t u l t callback: oncOtes cnetult:fnto(otr{ oncOtes ucinrue) rue.e(oeotiuoCnrle'.oncOte(dtis) otrgt'nCnrbtrotolr)cnetult'eal'; }
trek.github.com
15/19
19/01/13
In this callback we're connecting an { o t e } that we'll place inside the template for {ult} a contributor (yes, outlets can be nested inside other outlets as deeply as you'd like to). Go ahead and change <cittp=tx/-adeas dt-epaenm=acnrbtr> srp ye"etxhnlbr" aatmlt-ae"-otiuo" <i> dv < {ato soAlotiuos}AlCnrbtr<a a {cin hwlCnrbtr}>l otiuos/> <dv /i> {lgn}-{cnrbtos}cnrbtost Ebrj {oi} {otiuin} otiuin o me.s <srp> /cit
to <cittp=tx/-adeas dt-epaenm=acnrbtr> srp ye"etxhnlbr" aatmlt-ae"-otiuo" <i> dv < {ato soAlotiuos}AlCnrbtr<a a {cin hwlCnrbtr}>l otiuos/> <dv /i> {lgn}-{cnrbtos}cnrbtost Ebrj {oi} {otiuin} otiuin o me.s <i> dv {ote} {ult} <dv /i> <srp> /cit
and add D t i s i w and template. You can skip creating a D t i s o t o l r. In ealVe ealCnrle the absence of a controller with a matching name, Ember will just use the rendering context of the parent template, which in our case is the shared instance of O e o t i u o C n r l e with its c n e t property already set to the nCnrbtrotolr otn contributor we're interested in: ApDtisiw=EbrVe.xed{ p.ealVe me.iwetn( tmltNm:'otiuo-eal' epaeae cnrbtrdtis } )
<cittp=tx/-adeas dt-epaenm=cnrbtrdtis> srp ye"etxhnlbr" aatmlt-ae"otiuo-eal" <>{mi}<p p{eal}/> <>{i}<p p{bo}/> <srp> /cit
Reload the app and navigate to the 'aContributor.details' state by clicking on a Github username. If you have e a l L g i g on for your router you'll see we've successfully nbeogn transitioned into the state but are missing the e a l and b o data. Unfortunately, mi i these properties are not part of the contributor data that comes from Github. We'll need to trigger a call to Github when we enter this state to fetch additional details. Let's stub out a call to this in the c n e t u l t for 'aContributor.details': oncOtes cnetult:fnto(otr{ oncOtes ucinrue) rue.e(oeotiuoCnrle.otn'.odoeeal(; otrgt'nCnrbtrotolrcnet)laMrDtis) rue.e(oeotiuoCnrle'.oncOte(dtis) otrgt'nCnrbtrotolr)cnetult'eal'; }
trek.github.com
16/19
19/01/13
ApCnrbtr=EbrOjc.xed{ p.otiuo me.betetn( laMrDtis fnto({ odoeeal: ucin) $aa( .jx{ ul 'tp:/p.ihbcmues%'ftti.e(lgn), r: hts/aigtu.o/sr/@.m(hsgt'oi') cnet ti, otx: hs dtTp:'sn' aaye jop, sces fnto(epne{ ucs: ucinrsos) ti.ePoete(epnedt) hsstrprisrsos.aa; } } ) } }; )
Now when we enter this state we'll trigger a call to load more data from Github, immediately render the view, and the view will automatically update when additional properties eventually have values. You can now reload the application and transition to this state again to see the updated view. What about transitioning between our 'aContributor.details' and 'aContributor.repos' state? This should begin to look boringly familiar soon. Update our view to provide some navigational elements. Currently it looks like this: <cittp=tx/-adeas dt-epaenm=acnrbtr> srp ye"etxhnlbr" aatmlt-ae"-otiuo" <i> dv < {ato soAlotiuos}AlCnrbtr<a a {cin hwlCnrbtr}>l otiuos/> <dv /i> {lgn}-{cnrbtos}cnrbtost Ebrj {oi} {otiuin} otiuin o me.s <i> dv {ote} {ult} <dv /i> <srp> /cit
And after we've added two actions: <cittp=tx/-adeas dt-epaenm=acnrbtr> srp ye"etxhnlbr" aatmlt-ae"-otiuo" <i> dv < {ato soAlotiuos}AlCnrbtr<a a {cin hwlCnrbtr}>l otiuos/> <dv /i> {lgn}-{cnrbtos}cnrbtost Ebrj {oi} {otiuin} otiuin o me.s <l u> <i< {ato soDtis}Dtis/>/i l>a {cin hweal}>eal<a<l> <i< {ato soRps}Rps/>/i l>a {cin hweo}>eo<a<l> <u> /l <i> dv {ote} {ult} <dv /i> <srp> /cit
Create the new transitions. I've placed them both within the 'aContributor' state itself: aotiuo:EbrRueetn( Cnrbtr me.ot.xed{ soDtis EbrRuetastoT(dtis) hweal: me.ot.rniino'eal', soRps EbrRuetastoT(rps) hweo: me.ot.rniino'eo'
trek.github.com
17/19
19/01/13
} )
Now we can toggle between the two states. The a o t i u o . e o state will throw Cnrbtrrps an error because we're missing R p s i w, which Ember is attempting to find because eoVe of our c n e t u l t call on router's o e o t i u o C n r l e oncOte n C n r b t r o t o l r: cnetult:fnto(otr{ oncOtes ucinrue) rue.e(oeotiuoCnrle'.oncOte(rps) otrgt'nCnrbtrotolr)cnetult'eo' }
Add the view and a template, again skipping the R p s o t o l r which will use the eoCnrle shared O e o t i u o C n r l e instance as the rendering context for this view: nCnrbtrotolr ApRpsiw=EbrVe.xed{ p.eoVe me.iwetn( tmltNm:'eo' epaeae rps } )
<cittp=tx/-adeas dt-epaenm=rps> srp ye"etxhnlbr" aatmlt-ae"eo" {#ahrp i rps} {ec eo n eo} {rp.ae} {eonm} {/ah} {ec} <srp> /cit
For the view I've just looped through the r p s property of this view's rendering eo context, the shared O e o t i u o C n r l e n C n r b t r o t o l r. O e o t i u o C n r l e is nCnrbtrotolr an subclass of O j c C n r l e so this r p s lookup is proxied along to the b e t o t o l r, eo controller's c n e t property. The c n e t is an instance of A p C n r b t r otn otn p.otiuo we've passed along through the { a t o } transition, and c n e t u l t { c i n }, oncOtes callback. Reload the application, navigate back to this state, and you'll see a sad dearth of repos. As with 'aContributor.details' we need to request the appropriate data to display. Update the c n e t u l t of 'aContributor.repos' to include a stubbed method for fetching oncOtes repos: cnetult:fnto(otr{ oncOtes ucinrue) rue.e(oeotiuoCnrle.otn'.odeo(; otrgt'nCnrbtrotolrcnet)laRps) rue.e(oeotiuoCnrle'.oncOte(rps) otrgt'nCnrbtrotolr)cnetult'eo'; }
ApCnrbtr=EbrOjc.xed{ p.otiuo me.betetn( laRps fnto({ odeo: ucin) $aa( .jx{ ul 'tp:/p.ihbcmues%/eo'ftti.e(lgn r: hts/aigtu.o/sr/@rps.m(hsgt'oi' cnet ti, otx: hs dtTp:'sn' aaye jop, sces fnto(epne{ ucs: ucinrsos) ti.e(rps,epnedt) hsst'eo'rsos.aa; } }; ) } ,
trek.github.com
18/19
19/01/13
Like our data retrieval in 'aContributor.details' we now transition into the 'aContributor.repos' state, trigger data retrieval and immediately update our views. Once the data loading is complete, the view should automatically update to reflect the new value of our r p s property. eo Reader @sly7_7 put together a jsFiddle of the completed example.
Copy-editing and proofing graciously provided by @frodsan, @patrickbaselier, and @edimoldovan. Remaining foolish errors or omissions are mine.
trek.github.com
19/19