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!
The general idea of a rendering object can be observed in the mock used by the tests.
When writing your own renderer implementation, I would recommend forking the 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.
Implementing that basic todo app is a good functional test for your renderer, and ensures that all the basic functionality is at hand.
It's a function that returns an object with four methods:
module.exports = function makeRenderer(stateRouter) {
return {
render: function render(context, cb) {
const element = context.element
myArbitraryRenderFunction(element, function(renderedTemplateApi) {
cb(null, renderedTemplateApi)
})
},
reset: function reset(context, cb) {
const renderedTemplateApi = context.domApi
renderedTemplateApi.reset()
setTimeout(cb, 100)
},
destroy: function destroy(renderedTemplateApi, cb) {
renderedTemplateApi.teardown()
setTimeout(cb, 100)
},
getChildElement: function getChildElement(renderedTemplateApi, cb) {
setTimeout(function() {
cb(null, renderedTemplateApi.getChildElement('ui-view'))
}, 100)
},
}
}
You'll pass it to the state router like this:
var StateRouter = require('abstract-state-router')
var stateRouter = StateRouter(yourRendererFunction, 'body')
Your renderer function will be passed a single argument which is the state router object itself.
This function should return an object with four properties (all functions), which are described below.
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.
Is passed an object with four properties:
- 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. - element: The element where the template should be rendered. Returned by your renderer's
getChildElement
call. ASR doesn't care what this is - if you want to make people pass in actual element objects, or just selector strings, your call. - 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.
- parameters: the state parameters
function render(context, cb) {
var myHtml = myTemplateParser(context.template, context.content) // Compile template and content
$(context.element).html(myHtml) // Apply to the DOM
// domApi is a jquery object in these examples
// You should expose the interface provided by your dom manipulation library of choice
var domApi = $(context.element)
cb(null, domApi)
}
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...
Is passed whatever DOM/template manipulation object your render function returned.
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.
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.
function getChildElement(domApi, cb) {
// domApi is a jquery object in these examples
var child = domApi.children('ui-view').first()
cb(null, child)
}
Is passed an object with four properties:
- domApi: the object returned by your
render
function - content: the result of the user's
resolve
function - the same kind of thing passed torender
above - template: the template provided to the
stateRouter.addState
call, same as above - parameters: the state parameters
This function is similar in function to the render
function above, but with a difference - the template has already been rendered.
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.
Your reset implementation should
- wipe out all content/state in
domApi
- apply the new
content
to the samedomApi
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.
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.
function reset(context, cb) {
// Note the similarity to `render()`
var myHtml = myTemplateParser(context.template, context.content) // Compile template and content
// context.domApi is a jquery object in these examples
context.domApi.html(myHtml) // Apply to the DOM
cb(null)
}
Is passed the domApi
object returned by your render
function above.
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.
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.
function destroy(domApi, cb) {
// domApi is a jquery object in these examples
domApi.remove()
}
This is a matter of taste, but I think that the renderer should expose some basic functionality within all templates:
- easily linking to other states
- setting an
active
class on an element if a given state is active
You'll need to implement those in order to get the example todo app working. See the Ractive app
state template here 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.
You can implement these with functionality exposed by the state router:
- the stateRouter.stateIsActive function tells you whether or not a state is active
- the stateRouter.makePath function gives you a path you can link to
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
.
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.
Open an issue or ping me on Twitter and I'll be happy to help!