Skip to content

Latest commit

 

History

History
132 lines (86 loc) · 6.5 KB

renderer.md

File metadata and controls

132 lines (86 loc) · 6.5 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 {
		async render(context) {
			const element = context.element
			const renderedTemplateApi = await myArbitraryRenderFunction(element)

			return renderedTemplateApi
		},
		async destroy(renderedTemplateApi) {
			await renderedTemplateApi.teardown()
		},
		async getChildElement(renderedTemplateApi) {
			return renderedTemplateApi.getChildElement('ui-view')
		},
	}
}

You'll pass it to the state router like this:

const StateRouter = require('abstract-state-router')

const 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) {
  const 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
  const domApi = $(context.element)

  return 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) {
  // domApi is a jquery object in these examples
  const child = domApi.children('ui-view').first()

  return child
}

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) {
  // 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!