1 | # Create your own renderer
|
2 |
|
3 | abstract-state-router is cool because you can use it with any templating/dom library you like. To learn how to create a new rendering layer, read on!
|
4 |
|
5 | ## Where to start
|
6 |
|
7 | The general idea of a rendering object can be observed in [the mock used by the tests](https://github.com/TehShrike/abstract-state-router/blob/master/test/helpers/renderer-mock.js#L21).
|
8 |
|
9 | When writing your own renderer implementation, I would recommend forking the [state-router-example](https://github.com/TehShrike/state-router-example) and creating a new directory in the implementations folder for your new templating. Add your new dependencies to the package.json, and add a new build script.
|
10 |
|
11 | Implementing that basic todo app is a good functional test for your renderer, and ensures that all the basic functionality is at hand.
|
12 |
|
13 | ## What is a renderer really
|
14 |
|
15 | It's a function!
|
16 |
|
17 | You'll pass it to the state router like this:
|
18 |
|
19 | ```js
|
20 | var StateRouter = require('abstract-state-router')
|
21 |
|
22 | var stateRouter = StateRouter(yourRendererFunction, 'body')
|
23 | ```
|
24 |
|
25 | Your renderer function will be passed a single argument which is the state router object itself.
|
26 |
|
27 | This function should return an object with four properties (all functions), which are described below.
|
28 |
|
29 | All of the functions are asynchronous, and take an error-first callback function as the final argument - but if you're more of a promises type, you can return a promise instead, no problem.
|
30 |
|
31 | ### render
|
32 |
|
33 | Is passed an object with four properties:
|
34 |
|
35 | - **template**: comes from the original `stateRouter.addState` call when the state was created. The template to be rendered in the DOM. ASR doesn't care at all what this is, so it can be a string, some parsed template object, or anything else - whatever your templating library supports.
|
36 | - **element**: also comes from the original `stateRouter.addState` call. The element where the template should be rendered. Again, ASR doesn't care what this is - if you want to make people pass in actual element objects, or just selector strings, your call.
|
37 | - **content** generated by the resolve function provided by the user. If possible, you should apply this object to your DOM interface so that the data will be reflected in the template in the DOM immediately.
|
38 | - **parameters**: the state parameters
|
39 |
|
40 | ```js
|
41 | function render(context, cb) {
|
42 | var myHtml = myTemplateParser(context.template, context.content) // Compile template and content
|
43 | $(context.element).html(myHtml) // Apply to the DOM
|
44 |
|
45 | // domApi is a jquery object in these examples
|
46 | // You should expose the interface provided by your dom manipulation library of choice
|
47 | var domApi = $(context.element)
|
48 | cb(null, domApi)
|
49 | }
|
50 | ```
|
51 |
|
52 | Your render function should return in the promise/callback whatever object your chosen template library uses to represent an instantiated template. This is the object that is passed to the consumer's activate function as the `domApi` property, and is also passed in to...
|
53 |
|
54 | ### getChildElement
|
55 |
|
56 | Is passed whatever DOM/template manipulation object your render function returned.
|
57 |
|
58 | This `getChildElement` function must return the element in the DOM where a child state should be inserted. This element object, whatever it is, will be passed to the `render` function above.
|
59 |
|
60 | Convention in the renderers so far is to find a `<ui-view></ui-view>` element, in a nod to ui-router. This is totally up to you, though - whatever you choose to to use as child elements, make sure to document in your renderer's readme.
|
61 |
|
62 | ```js
|
63 | function getChildElement(domApi, cb) {
|
64 | // domApi is a jquery object in these examples
|
65 | var child = domApi.children('ui-view').first()
|
66 | cb(null, child)
|
67 | }
|
68 | ```
|
69 |
|
70 | ### reset
|
71 |
|
72 | Is passed an object with four properties:
|
73 |
|
74 | - **domApi**: the object returned by your `render` function
|
75 | - **content**: the result of the user's `resolve` function - the same kind of thing passed to `render` above
|
76 | - **template**: the template provided to the `stateRouter.addState` call, same as above
|
77 | - **parameters**: the state parameters
|
78 |
|
79 | This function is similar in function to the `render` function above, but with a difference - the template has already been rendered.
|
80 |
|
81 | This `reset` option exists so that if a state changes, but the template is the same as the old state, the whole thing doesn't need to be destroyed, and then all of the DOM elements re-created.
|
82 |
|
83 | Your reset implementation should
|
84 |
|
85 | - wipe out all content/state in `domApi`
|
86 | - apply the new `content` to the same `domApi`
|
87 |
|
88 | This function doesn't have to return anything, it's totally fine to call the callback or return a resolved promise without any value once the resetting is complete. The router will assume that the previous DOM API is still valid.
|
89 |
|
90 | If you do pass a truthy value to the callback or in the promise, the router will assume that the value is the new DOM API which should replace the previous one.
|
91 |
|
92 | ```js
|
93 | function reset(context, cb) {
|
94 | // Note the similarity to `render()`
|
95 | var myHtml = myTemplateParser(context.template, context.content) // Compile template and content
|
96 | // context.domApi is a jquery object in these examples
|
97 | context.domApi.html(myHtml) // Apply to the DOM
|
98 | cb(null)
|
99 | }
|
100 | ```
|
101 |
|
102 | ### destroy
|
103 |
|
104 | Is passed the `domApi` object returned by your `render` function above.
|
105 |
|
106 | Here, all you need to do is whatever it is that wipes out the contents in the DOM. If you need to emit any cleanup events or anything, this is the place to do it.
|
107 |
|
108 | **NOTE**: only clean up things having to do with the templating/DOM. You shouldn't be signalling to your code that it's cleanup time, the code should be watching for the `destroy` event to be emitted on the context object passed to the `activate` function.
|
109 |
|
110 | ```js
|
111 | function destroy(domApi, cb) {
|
112 | // domApi is a jquery object in these examples
|
113 | domApi.remove()
|
114 | }
|
115 | ```
|
116 |
|
117 | ## Other functionality you'll want in your renderer
|
118 |
|
119 | This is a matter of taste, but I think that the renderer should expose some basic functionality within all templates:
|
120 |
|
121 | - easily linking to other states
|
122 | - setting an `active` class on an element if a given state is active
|
123 |
|
124 | You'll need to implement those in order to get the example todo app working. See the Ractive `app` state template [here](https://github.com/TehShrike/state-router-example/blob/gh-pages/implementations/ractive/app/app.html#L5-L7) using the Ractive decorator exposed by ractive-state-router that sets an `active` class on the element when the given state name is active, and the `makePath` function that builds a url to a state given a state name and some properties.
|
125 |
|
126 | You can implement these with functionality exposed by the state router:
|
127 |
|
128 | - the [stateRouter.stateIsActive](https://github.com/TehShrike/abstract-state-router#staterouterstateisactivestatename-stateparameters) function tells you whether or not a state is active
|
129 | - the [stateRouter.makePath](https://github.com/TehShrike/abstract-state-router#stateroutermakepathstatename-stateparameters) function gives you a path you can link to
|
130 |
|
131 | ## Other minutia
|
132 |
|
133 | ### What should your new rendering module be named?
|
134 |
|
135 | I named the first renderer implementation `ractive-state-router`, and I regret that choice. I would recommend that your module follow in the pattern of `riot-state-renderer` and `virtualdom-state-renderer`, and go with `[templating library name]-state-renderer`.
|
136 |
|
137 | ### Don't forget
|
138 |
|
139 | Open a pull request with your changes to the `state-router-example`, I would really like for it to be an example of using the abstract-state-router with every single supported renderer.
|
140 |
|
141 | ### Any other questions?
|
142 |
|
143 | [Open an issue](https://github.com/TehShrike/abstract-state-router/issues) or [ping me on Twitter](https://twitter.com/TehShrike) and I'll be happy to help!
|