Skip to content

Latest commit

 

History

History
170 lines (117 loc) · 8.22 KB

renderer.md

File metadata and controls

170 lines (117 loc) · 8.22 KB

Create your own renderer

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!

Where to start

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.

What is a renderer really

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.

render

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...

getChildElement

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)
}

reset

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 to render 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 same domApi

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)
}

destroy

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()
}

Other functionality you'll want in your renderer

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:

Other minutia

What should your new rendering module be named?

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.

Don't forget

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.

Any other questions?

Open an issue or ping me on Twitter and I'll be happy to help!