Hyperbone View
Please note that this module is no longer being maintained.
(Having said that I've just updated it to stop using the deprecated replaceWholeText() functionality deprecated in Firefox and soon everythign else.. )
It has been replaced with HalogenJS View.
Installing
$ npm install --save hyperbone-view
Running tests
Once:
$ npm install -g grunt-cli browserify
Once after cloning repo
$ npm install
Running tests
$ npm test
tl;dr
Push style template system for Hyperbone (and probably Backbone) models, allowing strict model/view separation.
You get 'if', 'if-not', 'hb-trigger', 'hb-click-toggle', 'hb-with' and 'hb-bind' as the only custom attributes you need to learn See paper on this subject.
Features
- Logicless moustache-eseque templates for attributes and innertext.
- Define your own custom helpers to do advanced string processing
- Hypermedia extensions: Automatically insert href attributes for recognised rels.
- 'hb-trigger' custom attribute to trigger Hyperbone events on a model
- 'if' custom attribute to conditionally display elements.
- 'hb-bind' custom attribute to link an input to a model attribute
- 'hb-with' to change scope of a template and render out collections (partials, in effect)
- API for adding additional attributes for when you HAVE to touch the DOM.
Example
HTML in your page:
Hello, {{name}} Enter your name: {{strip(description)}} Some link A link to myself {{flavour}}
JSON HAL document on the server
Presume that we've loaded this JSON into a HyperboneModel instance..
var HyperboneView = HyperboneView; // we want to register a helper called 'strip'. This will be available to all Views in the system. // now we can create our view instance. // the model... model : myHypermediaDocument // our view root el : '#some-view'; // set our editing flag to true so that we can see our htmlmyHypermediaDocument; // bind to a hyperbone event that'll trigger when the user// clicks on the particular link.myHypermediaDocument
As soon as the initial processing is done, our DOM has been transformed.
Some things to note:
- The collection of flavours has been expanded
- Each flavour has automatically had its own href added to the link because of the rel='self'
- Our link to rel='some-rel' has had its href added as well.
- Our 'strip' helper has removed the markdown from 'description'.
Hello, Enter your name: This is very exciting Some link A link to myself Chickenesque Spicy Beef substitute Curry. Just Curry.
If you happen to do this in your code....
myHypermediaDocument
Then the page automatically updates to...
And if you happen to click on A link to myself
, the Hyperbone event fires, updates the model and that results in..
A link to myself
And if you type something into the the 'Enter your name box'
Hello, something
And if you do
myHypermediaDocument;
Then the element gets hidden.
Installation
Install with component(1):
$ component install green-mesa/hyperbone-view
Hyperbone View has a number of dependencies which are installed at the same time. These are:
- Underscore
- component/dom
- Parts of Backbone
Note that unlike Backbone View this does not have a dependency on jQuery. It does use a tiny standalone dom manipulation component called Dom instead.
Module API
require('hyperbone-view').registerHelper(name, fn)
Register a helper function for use inside templates. It becomes globally available to all views.
Example:
; model: name : "squirrel" el : ;
The template calls the helper...
Hello {{shout(name)}}
Which produces
Hello SQUIRREL
require('hyperbone-view').registerAttributeHandler(name, fn)
Register a custom attribute handler for extending the capabilities of View. More on this below.
require('hyperbone-view').HyperboneView
Your reference to the HyperboneView prototype.
var HyperboneView = HyperboneView; model : model el : el { // i get called after it's initialised for the first time. };
or
;
HyperboneView Instance API
.on( event, callback )
HyperboneView instances are Backbone event emitters. There are three events emitted currently: initialised
, updated
and delegate-fired
.
The callbacks are passed a dom object, which is the view HTML and the model. For updated and delegate fired, information about what has changed is also added.
The philosphy behind these events is that they're useful for running integration tests, keeping a track on your application's state directly.
view
view
view
.create( dom, hyperboneModel )
If you want to postpone the view initialising, you can manually triggered this by invoking HyperboneView without a model and el and then calling .create(). Pass it either a CSS selector or a dom
List object along with the model and this then binds the model to the view.
.addDelegate(obj | name, fn)
If you're using the .create() method, you can manually set up actual DOM event delegates, although this... probably isn't wise.
delegates : { // do something here. Scope is the model. } model : model el: el
is equivilant to
Hyperbone HTML Attributes
Hyperbone attributes can be added to the HTML, and allow for additional functionality not provided in the logicless attribute/innerText templates.
if="attribute"
Given the truthiness of the model attribute, it will conditionally display the element.
{{organisation}}
This is as complex as the logic gets. How do I do an 'else' or an 'or' or an 'and' I hear you cry. Anything more complex than this is a job for code. It's what code is good at. The philosophy is that you do your difficult logic stuff in your code.
hb-with="attribute"
Changes the scope for the innerHTML to the selected model or collection. In effect the nested elements become a partial.
This HTML...
{{greeting}}
... is equivilant to
{{nested-model.greeting}}
... except when you use hb-with
for a model you create a subview and any change events that fire show only the sub-view and the sub-model.
Slightly more useful than this is the ability to iterate through collections with hb-with
{{name}}
... this then automatically clones the li tag for every model inside the collection.
hb-trigger="hyperbone-event"
On clicking an element with the hb-trigger attribute, a subscribeable hyperbone event is fired. The handler is passed three parameters - the originating model, the name of the signal and a function to cancel any default DOM events.
This solves a particular problem of being able to access individual models within collections without doing horrible things to the DOM.
A futher example:
Our model contains...
filters : name : "Filter one" active : true name : "Filter two" active : false
And our view makes a new li for each filter. The scope of each li is the individual model in the collection.
{{name}}
Which means when that li is clicked, the 'filters-changed' event fires on the 'filters' object (in backbone style that's filters-changed:filters
), and the first parameter is the individual filter.
model
hb-click-toggle="model-attribute"
The most common use case for hb-trigger
is actually just toggling a flag on or off, so this custom attribute automates this for you.
Edit Hello {{Name}} View Enter your name:
That's really all there is to it. You can, of course, bind to the change event and do somethign else...
app
hb-bind
This attribute allows two-way binding to form inputs to allow an easy way to let your users interact with your model.
Default Dark Light
When used with a model..
theme : "default"
...results in the class on the body tag being automatically updated when the user changes the select. Etc.
Adding your own custom attributes
Because Hyperbone View enforces a strict separation of model and view, your applications shouldn't be touching the DOM at all. However, sometimes, you do in fact need to touch the DOM. When you do, the idea is that you use your own custom attributes. Luckily Hyperbone View exposes an API for this.
require('hyperbone-view').registerAttributeHandler( attributeName, fn )
require('hyperbone-view').use( attributeHandlers : { attributeName : fn })
fn
is called when HyperboneView finds an element with your attribute. When called, it is passed the element, the value of the attribute as arguments and a 'cancel' function. The scope is the instance of HyperboneView itself, meaning you can use this.model and this.el (this may not be true forever)
The cancel function should be called if you do not wish the View to continue processing the node (i.e, recurse into the childNodes etc).
Here's a non-disruptive non-cancelled example. We want a link to switch between .on
and .off
whenever it's clicked..
// create a modelvar model = status : "";// register an attribute handler;// create a view model: model el : html;
A disruptive 'cancelling' example: Creating a new instance of HyperboneView with a different model to process the element and all its children.
This is the parent Hypermedia document. Note that it contains a rel some-rel
which points to /some-other-document
.
And this is the JSON for /some-other-document
Our HTML. We want to manually embed /some-other-document
into our page. We don't use the href, only the rel.
{{greeting}}{{greeting}}
Now we add our custom attribute handler...
// add attribute handler; // create a viewmodel : someModel el : myElement ;
WHich should, after everything's loaded, result in..
Welcome to the magic world of HypermediaWoooo!
As these two examples should demonstrate, using the custom attribute handler API is fairly powerful, largely unopinionated... and very very easy to abuse.
Logicless Template rules
It looks like moustache templating but it's not. It supports referencing model attributes, calling custom helpers (which are passed the referenced model attribute) and... if you really really must... you can just send in arbitrary javascript so long as it's inside a call to a custom helper.
Built ins:
{{property}}
automatically becomes model.get("property"){{get(property)}}
for when you absolutely want everyone to know there's some backbone happening{{url()}}
gets the _links.self.href{{rel('some-rel')}}
gets a specific rel{{expresion(1 + 2 + model.get('current-value'))}}
- expression helper lets you add arbitrary javascript. Note the use of model.get to access data in the model is required in this situation.
Custom helpers:
{{myHelper(property)}}
passes model.get('property') to your custom handler
Won't work:
{{1 + 2}}
License
MIT