1) Routes
- Are triggered from URL, links in templates or
transitionTo
in code.
- Handle most application-level events:
- choose which data transfer to initiate
- react to “big” user actions, such as navigating in the application, saving data, …
- Decide which template to render
- All of this is done in a cascade. Typically you will nest routes, for instance suppose you’re editing some article, you’d probably have 3 nested routes:
- The root route
/
handles some global transitions and that’s pretty much it.
- Then a
/articles/
route would handle going from an article to another, loading the list of all articles’ metadata, perhaps refreshing it, or updating whatever metadata can be updated from the list or in bulk.
- Then the
/articles/42/
route would handle loading additional data required to edit an article. Perhaps related models, perhaps initialize a new article as needed…
- Remember actions bubble up in routes, so you can handle them at the level that makes sense.
2) Templates
All current routes load a template. The very same way routes are nested, their templates are nested too, with the inner route’s template being inserted in the {{outlet}}
placeholder. Missing templates are filled in automatically with a single {{outlet}}
directive so they do not break the nesting chain.
Templates get the data the route loaded, and display it in some way. Either directly creating HTML markup, or instantiating components and passing them relevant data. They also bind actions.
3) Components
They are instantiated by templates. They get their data from the template, and work in complete isolation from there. It makes it possible to structure the application’s behavior into reusable … well… components.
A component works like a mini-route with a template. See below for what I mean by this.
This is the basic structure. It has a major shortcoming though: all state is transient, since navigating to another route will clean up templates, destroy components and turn the current route inactive. The solution to this comes with…
4) Services
They are singletons. They live forever. And they hold data that should stay around as you navigate the application. Typical uses include:
- Data from the server (not mandatory, but putting it in a service allows caching). Ember-data does this.
- Keeping user drafts. So they are not lost when the user navigates away from the editing route.
- Data shared among components.
- User credentials, so you don’t have to ask him to enter them again every time they’re needed.
Putting it together
- Routes choose what data they need.
- They typically trigger some load operation from a service such as ember-data. Let’s go with that example…
- Ember-data fetches requested resource from the server, caches the result and passes it back to the route.
- Route now triggers rendering of its template, passing it the data.
- Template dispatches relevant bits of data to appropriate components.
- Components render the markup using their own template (which may contain sub-components, so this step is recursive).
This pattern is what we express as “data down”. It flows from the top application down to the tiniest components. Then, as the user works with the application,
- Components receive events from the browser (click, keyup, …).
- They interpret them into user intents, which we call “actions”. For instance, “save this article”
- They send those actions up.
- Actions that were bound by the template reach their template’s route (others are discarded).
- The action then bubbles through the route tree, until…
- A route takes appropriate action, such as telling ember-data to send the data to the server.
This pattern is what we express as “actions up”. Actions are generated from browser events, and progressively enriched with context as they go up, until they reach the relevant route.
More on components.
They are like a mini-route with a template. So they reproduce the workflow on a smaller scale. They can have their own data (internal state). Their template can instantiate components too, thereby creating a tree of components. This allows composition, and encapsulation of common behaviors.
They are very versatile, and experience has shown me that all components are not created equal. I typically split components into 3 main use categories:
- Canonical component. Data down, actions up, encloses some well-defined user interaction. For instance, a dropdown box. Fully isolated, droppable as-is in any other project. Think “custom html element”.
- Application-specific component. Combines some canonical components to make re-usable interactions specific to my application. For instance, a product editor component. Still data down, actions up, but encloses knowledge of how data items interact. Typically, it knows when to disable a field based on another field’s value.
- Clever component. Breaks out of the data down, actions up model. Will act upon events and trigger data loading, almost like a sub-application. Clearly documented as such. For instance, an image picker that supports opening a modal to browse through folders on the server, or a list widget that supports server-side pagination.
What about controllers?
Deprecated. They exist in between the route and the template. They used to hold transient state for the route’s template. Now the recommended way is to create a component for this purpose (it is perfectly acceptable that your route’s template contains nothing but a single component instantiation). Controllers will go away when route-able components hit. You should not bother with them, Ember will automatically create a simple pass-through controller for you at runtime.