Backbone.Inline.Template
Quick intro – Why? – Setup – Framework integration – Selecting templates – Cache
Limitations – Alternative – Build and test
It seems that quite a few people would like to see fully-formed templates in Backbone: templates which contain the root element of the template – the el
, in Backbone lingo – and not just the inner HTML of that element. It's been a popular feature request for Backbone, and for frameworks built on it.
Backbone.Inline.Template is that feature.
In a nutshell
Assume you want to create a view with an el
of <p id="foo" class="bar">
. But you don't want to set up the el
in your Javascript code, with Backbone's tagName
, id
and className
, as you'd normally have to do. Instead, the el
should be defined inside the template itself, with HTML markup.
Classic Backbone
Here's how:
Note the data attribute data-el-definition="inline"
. That's how you tell your application that the el
is defined inline in the template. (You can change how such templates are marked.)
When you create a view with that template, its el
will be the element you expect: <p>
. It is already set up and available when initialize()
is called, just like any other Backbone el
.
That's it, really. For your own processing and rendering, proceed as you like. However, you can boost performance and avoid accessing the DOM for retrieving the template. Fetch the template from the cache of Backbone.Inline.Template instead. E.g.
// Tell Backbone.Inline.Template which template compiler function you use.BackboneInlineTemplatecustomcompiler = _template; // Pull the template content from the cache.var BaseView = BackboneView;
For more on the cache, see below.
Frameworks like Marionette
Now, let's assume that you are using a Backbone framework which takes care of template loading and rendering, but doesn't support el
inlining. One such framework is Marionette. How do you undercut the framework magic and make it process your template format?
With a single statement. This one:
BackboneInlineTemplateupdateTemplateSource = true;
Now Backbone.Inline.Template sets up your el
as it did before, but it also modifies the original template in place. By the time your framework kicks in, the HTML defining the el
is no longer part of the template content. Instead, the framework sees a template it can understand: just the part which provides the inner HTML of the el
.
For which Backbone frameworks does that work? Presumably for most. Backbone.Inline.Template does its job at a very early stage during the life cycle of a view. So the chances are excellent that your framework of choice picks up the changes which Backbone.Inline.Template has made to the templates.
You'll find the details on framework integration and the updateTemplateSource
option below.
And finally...
Please have a brief look at the fine print and limitations, too.
Why use it?
There are several reasons for wanting to see the el
of a view inside a template.
Markup should stay out of Javascript.
The principle itself doesn't really need explaining, it is a basic tenet of clean design. Using Backbone view properties like tagName
, className
etc is in direct violation of that principle.
However, there is more than one way to achieve a separation of markup and Javascript code. If that separation is all you want from fully self-contained templates, consider using Backbone.Declarative.Views instead, which does the job very well. In fact, Backbone.Inline.Template is just a plug-in for it, adding an additional layer of template wrangling.
A definite plus of Backbone.Declarative.Views is that it works with zero configuration in virtually any Backbone application. Inlining the el
might be more intuitive with Backbone.Inline.Template, and some might favour it for reasons of style. But Backbone frameworks typically require the templates to be modified in place, which adds a bit of complexity. If you can do without, why not do it. In any event, you have a choice.
The el
must be inline to make templates work on the server.
If you render your HTML on the server and slap on a Backbone application as a client-side progressive enhancement, you probably want to use the exact same templates on both ends. And your server-side app might require you to define the template root element, the el
, inline.
Backbone.Inline.Template is your new best friend in that case, and it saves you from the worries which burden other approaches.
The el
itself, and not just its content, depends on template variables.
That, too, is a legitimate requirement. But in that case, sadly, Backbone.Inline.Template is not for you. Have a look at its limitations to see why. Alternatives exist, though at a cost.
Dependencies and setup
Backbone.Inline.Template depends on Backbone.Declarative.Views and the Backbone stack: Backbone, Underscore, and jQuery or one of its replacements. Include backbone.inline.template.js after that lot.
If you use other components which extend Backbone.View, load these components after Backbone.Inline.Template.
Backbone.Inline.Template augments the Backbone.View base type, so its functionality is available in every view throughout your code.
When loaded as a module (e.g. AMD, Node), Backbone.Inline.Template does not export a meaningful value. It solely lives in the Backbone namespace.
With Marionette
Load backbone.inline.template.js after Marionette. Do it in this order: Marionette, Backbone.Declarative.Views, Backbone.Inline.Template.
If you use AMD, please be aware that Marionette is not declared as a dependency in the AMD build of Backbone.Inline.Template, nor in Backbone.Declarative.Views. Declare it yourself by adding the following shim to your config:
requirejs;
Note that the shim makes Marionette a dependency of Backbone.Declarative.Views, not of Backbone.Inline.Template.
Download, Bower, npm
The stable version of Backbone.Inline.Template is available in the dist
directory (dev, prod). If you use Bower, fetch the files with bower install backbone.inline.template
. With npm, it is npm install backbone.inline.template
.
updateTemplateSource
Framework integration: Backbone leaves it up to you how you deal with your templates. Frameworks built on top of Backbone, however, usually have specific expectations about what is in the template, and what isn't. The el
of the view usually isn't.
So if your templates contain the el
of the view initially, it has to be gone by the time the framework processes the template. Backbone.Inline.Template can sort that out for you. Set the configuration option
BackboneInlineTemplateupdateTemplateSource = true;
and your original templates are modified. Once Backbone.Inline.Template is done with them, they just contain the inner HTML of the el
.
You also need that option if you are working with pre-existing code which expects "normal" templates (ie, templates without the el
inside).
How does it work?
Let's get the magic out of this process and look at an example. Suppose your original template looks like this:
As soon as you instantiate a view with that template, Backbone.Inline.Template transforms the template into:
The transformed version is presented to the framework when it processes the template. As you can see, the el
tag is no longer part of the template content – just the inner HTML of the el
is left.
Meanwhile, the el
has morphed into a set of data attributes on the template element (data-tag-name
etc). Backbone frameworks happily ignore the data attributes. However, they are picked up by Backbone.Declarative.Views, the engine behind Backbone.Inline.Template, whenever you instantiate a new view with the template. The el
of the view is set up accordingly.
updateTemplateSource
Limitations of The updateTemplateSource
option is global. If you use it, the transformation applies to all templates which are marked for processing by Backbone.Inline.Template. You can't transform templates on a case-by-case basis.
As you have seen, the transformed template is written back to the original template element (<script>
or <template>
). So an actual template element needs to be present in the DOM. With updateTemplateSource
, you can't use a raw HTML string as a template (you'll get an error if you do). Provide a node.
Selecting templates for processing
By default, Backbone.Inline.Template does not act on each and every template. A template is recognized as having an inline el
only if a specific data attribute is set on the template element: data-el-definition: "inline"
. All other templates are left alone.
If that doesn't suit your needs, you can change the way templates are selected for processing. Do it by overriding
BackboneInlineTemplatehasInlineEl
with a custom function.
hasInlineEl
Providing a function for Your hasInlineEl
function is called with a single argument: the template, as a jQuery object. The function can examine the template and must return a boolean.
For instance, assume you want templates to be handled by Backbone.Inline.Template if they have an attribute of type="text/x-inline-template"
. That can be achieved with
BackboneInlineTemplate { return $template === "text/x-inline-template";};
In order to treat all templates as having an inline el
, the function just has to return true:
BackboneInlineTemplate { return true; };
A word of caution: Please don't define a custom hasInlineEl
just for using another data attribute. If you want a data attribute to select your templates, use the one which is provided out of the box (data-el-definition
).
Why? Because the jQuery methods for data attributes would interfere with the handling of your custom attribute. These methods are buggy and inconsistent across jQuery versions. Backbone.Inline.Templates takes care of the issues for the data attributes which are built in, but custom ones would not be covered by that.
Cache
Backbone.Inline.Template is a plug-in for Backbone.Declarative.Views, which does all the real work. Unsurprisingly, both share the same template cache. Backbone.Inline.Template just provides a number of aliases to access that cache.
In the documentation of Backbone.Declarative.Views, you'll find more about the cache itself.
Used with Backbone.Inline.Template, the cache captures the state of the template after the el
has been extracted. So when a template has been processed by Backbone.Inline.Template and you query the template cache,
- the
html
property of the cached object refers to the HTML inside theel
, not the HTML of the entire template (which would have included the inlineel
) - the
compiled
property, likewise, hands you the template function for the HTML inside theel
, not the entire template. (You need to have defined a compiler function for that, of course).
The following aliases are available for the cache of Backbone.Declarative.Views.
-
Tell Backbone.Inline.Template which template compiler function to use:
// E.g. using _.template as your compiler:BackboneInlineTemplatecustomcompiler = _template;// Alias ofBackboneDeclarativeViewscustomcompiler = _template; -
Define a custom template loader:
BackboneInlineTemplatecustom { /*...*/ };// Alias ofBackboneDeclarativeViewscustom { /*...*/ }; -
Retrieve a cached template in the context of a view:
{cachedTemplate = thisinlineTemplate;// Alias ofcachedTemplate = thisdeclarativeViews;} -
Retrieve a cached template independently of a view, by selector:
cachedTemplate = BackboneInlineTemplate;// Alias ofcachedTemplate = BackboneDeclarativeViews; -
Clear a cached template in the context of a view:
someViewinlineTemplate;// Alias ofsomeViewdeclarativeViews; -
Clear a cached template independently of a view, by selector:
BackboneInlineTemplate;// Alias ofBackboneDeclarativeViews; -
BackboneInlineTemplate;// Alias ofBackboneDeclarativeViews;
Fine Print and Limitations
The idea behind Backbone.Inline.Template is that it should "just work" with a minimum of intervention. But there are things to keep in mind, and some use cases are beyond its scope.
Just one top-level tag inside the template
The top-level HTML element tag inside a template defines the el
of the view, and there can just be one such el
. You must abide by that rule.
Multiple top-level HTML elements inside a template will confuse the hell out of the template loader and lead to unexpected results, such as an empty template, invalid and disfigured HTML, or an error while creating the view.
So don't do this:
However, you can add top-level HTML comments to the template. They are simply ignored.
el
itself
Don't use template variables in the Setting up the el
of a view with template variables is a valid use case, but unfortunately, Backbone.Inline.Template doesn't support it.
Backbone creates the el
as soon as the view is instantiated, even before initialize()
is called (and for a reason). The el
already exists before any processing of template data can take place. The data may not even be available that early. In any event, it can't be used for the el
.
So don't do this:
If you need to support template variables in the el
itself, you have to go down a different route and swap out the el
on render, when the data is there.
Backbone.Inline.Template can't help you with it and is not useful in that scenario. You can find an alternative approach below, and additional inspiration e.g. on Stack Overflow. But be aware of the pitfalls.
el
in place when rendering
Leave the It may be quite obvious, but maybe warrants at least a mention: The el
is already in place when you render, so don't throw it away needlessly. Nothing breaks if you do (well, mostly), but you take a performance hit and have to face all sorts of event binding woes for no reason.
So you want to keep the el
. What does that imply? You have defined the el
inside the template. If you compile the template in its original form, the el
element shows up in the result. You have to remove the el
element from the template and only inject the remainder into the existing el
of the view.
There is no need to transform the template yourself, though. Backbone.Inline.Template can remove the el
for you and redefine the template with what is left. Use the updateTemplateSource
option for that – you may have to turn it on anyway if your framework expects it.
Or better still, don't even do that, don't (re-)compile the template yourself. There is a much easier and more efficient option: Pull the compiled template from the built-in cache instead. There, the duplicate el
tag is already removed for you.
el
attributes
Boolean Boolean attributes of an el
, like hidden
, are fully supported. That includes the short notation with just the attribute name, without a value assignment (<p hidden>
).
How to use template literals, instead of a selector
For your templates, you don't always have to use a template element in the DOM, chosen with a selector. Instead, you can pass in a complete template as a string. But how do you mark it for processing by Backbone.Inline.Template? Obviously, you can't set a data attribute on a string.
You have two options. For one, you can simply make sure that all templates are processed by Backbone.Inline.Template. That includes template strings. A custom hasInlineEl
function does the trick.
Alternatively, you can add a special HTML comment to your template string which selects it for processing:
<!-- data-el-definition="inline" -->
The comment can be placed anywhere in your template. It is governed by the same rules as special comments in Backbone.Declarative.Views.
You can't use the special comment if you have defined a custom hasInlineEl
function. Only the default data attribute is recognized in the comment (data-el-definition="inline"
).
Please be aware that you cannot process template strings with Backbone.Inline.Template if you also configure it to update the template source. The updateTemplateSource
setting must be off.
setElement()
Alternative approach: Backbone.Inline.Template and its cousin, Backbone.Declarative.Views, do their respective jobs in an unobtrusive way which is compatible with pretty much everything. But there are limits to what they can do. Once once you hit these constraints, you need to find alternatives.
A well-known approach makes use of setElement()
, which is native to Backbone views, when rendering a view.
The pattern
With setElement()
, you throw away the existing el
which is provided by Backbone, and replace it with your own when you render the view. The basic pattern looks like this:
{ var $existingEl = this$el // Fill in the template vars and return the HTML of the view, // INCLUDING the outer `el`. It doesn't matter here how you // get that done. The evaluated template is a string of HTML. evaluatedTemplate = this $replacementEl = ; // Make the view reference the new node as its `el` this; // Replace the existing `el` node in the DOM with the new one $existingEl; // ... other stuff }
The pros
The setElement()
pattern is unbeatable in one respect: you can use template variables to set up the el
of a view.
For instance, assume you need an individual id
for each el
. For some reason, you can't set the id
in your Javascript code with the id
property of the view. The id
has to be a template variable, along the lines of
To incorporate the template data, you have to recreate the el
when the data is ready. The pattern above allows you to do it.
That's not exactly a long list of advantages, but for that one use case, setElement()
is the way to go.
The cons
Relying on setElement()
has a lot of drawbacks. None of these matter if your use case forces you to recreate the el
(see the pros). But there is a strong case against using setElement()
if you have a choice.
The challenges broadly fall into two categories: compatibility and implementation detail. Compatibility, both with existing and future code, is probably the trickier of the two.
-
Defining the
el
in the template may be convenient and even necessary, but it breaks with Backbone conventions. For the view code you write yourself, that won't be much of an issue. But as soon as you pull third-party Backbone extensions like Marionette into a project, templates of that kind no longer work. You probably have to rewrite the render method of your would-be framework, at the very least, to adapt it.Backbone.Inline.Template does its processing very early during the life cycle of a view. That's why it is able to present a cleaned-up, conventional template to Backbone frameworks. With
setElement()
, however, you don't have that option. It forces you to keep the original template around until render time. -
Backbone is not very prescriptive about how it is to be used. It gives developers a lot of flexibility, and expects them to figure out their own way of handling views in particular. As a result, there are many different approaches and implementations.
Replacing a view's
el
withsetElement()
, on the other hand, requires very specific steps at a very specific time, and may clash with the approach chosen in the application you work on. Expect problems when integrating it into legacy code.
The other group of issues is about pitfalls when rendering views with setElement()
. The devil here is in the details. These issues are solvable, but require attention (and additional code).
-
setElement()
rebinds events set up with theevents
hash of the view, but not events set up in any other way. You have to remember taking care of them. -
setElement()
screws up events in nested views. Fortunately, a good pattern exists to deal with it. It is important to be aware of the issue, though. -
Recreating the
el
and moving the delegated events around has a performance cost which matters as the number of render calls goes up (e.g. large lists).
That's quite a few things to consider, and the list is not even complete. Long discussions have taken place about defining the el
inline, inside a template. The problems associated with re-assigning the el
during render have come up repeatedly in these discussions. The Backbone team decided against this approach back in 2012. For Marionette, the debate is still going on – it has already lasted for years.
Build process and tests
If you'd like to fix, customize or otherwise improve the project: here are your tools.
Setup
npm and Bower set up the environment for you.
- The only thing you've got to have on your machine is Node.js. Download the installer here.
- Open a command prompt in the project directory.
- Run
npm install
. (Creates the environment.) - Run
bower install
. (Fetches the dependencies of the script.)
Your test and build environment is ready now. If you want to test against specific versions of Backbone, edit bower.json
first.
Running tests, creating a new build
Considerations for testing
To run the tests on remote clients (e.g. mobile devices), start a web server with grunt interactive
and visit http://[your-host-ip]:9400/web-mocha/
with the client browser. Running the tests in a browser like this is slow, so it might make sense to disable the power-save/sleep/auto-lock timeout on mobile devices. Use grunt test
(see below) for faster local testing.
Tool chain and commands
The test tool chain: Grunt (task runner), Karma (test runner), Mocha (test framework), Chai (assertion library), Sinon (mocking framework). The good news: you don't need to worry about any of this.
A handful of commands manage everything for you:
- Run the tests in a terminal with
grunt test
. - Run the tests in a browser interactively, live-reloading the page when the source or the tests change:
grunt interactive
. - If the live reload bothers you, you can also run the tests in a browser without it:
grunt webtest
. - Run the linter only with
grunt lint
orgrunt hint
. (The linter is part ofgrunt test
as well.) - Build the dist files (also running tests and linter) with
grunt build
, or justgrunt
. - Build continuously on every save with
grunt ci
. - Change the version number throughout the project with
grunt setver --to=1.2.3
. Or just increment the revision withgrunt setver --inc
. (Remember to rebuild the project withgrunt
afterwards.) grunt getver
will quickly tell you which version you are at.
Finally, if need be, you can set up a quick demo page to play with the code. First, edit the files in the demo
directory. Then display demo/index.html
, live-reloading your changes to the code or the page, with grunt demo
. Libraries needed for the demo/playground should go into the Bower dev dependencies, in the project-wide bower.json
, or else be managed by the dedicated bower.json
in the demo directory.
The grunt interactive
and grunt demo
commands spin up a web server, opening up the whole project to access via http. So please be aware of the security implications. You can restrict that access to localhost in Gruntfile.js
if you just use browsers on your machine.
Changing the tool chain configuration
In case anything about the test and build process needs to be changed, have a look at the following config files:
karma.conf.js
(changes to dependencies, additional test frameworks)Gruntfile.js
(changes to the whole process)web-mocha/_index.html
(changes to dependencies, additional test frameworks)
New test files in the spec
directory are picked up automatically, no need to edit the configuration for that.
Release notes
1.0.1
- Fixed template source failing to update if all
el
properties are defined in a Backbone view - Updated Backbone.Declarative.Views dependency to 3.1
1.0.0
- Removed the separate AMD/Node builds in
dist/amd
. Module systems and browser globals are now supported by the same file,dist/backbone.inline.template.js
(or.min.js
) - Simplified AMD shim for using Marionette
- Version is exposed in
Backbone.InlineTemplate.version
v0.3.0
- Completed test suite
- Fixed handling of
el
attributes with empty string values, are no longer dropped - Updated Backbone.Declarative.Views dependency to 3.x
v0.2.1
- Added matcher tests
- Fixed matcher handling of multi-line
el
tags - Fixed matcher handling of omitted slash in self-closing
el
tags - Fixed matcher handling of top-level HTML comments which contain tags
v0.2.0
- Removed
removeInlineElMarker()
- Renamed
updateOriginalTemplates
option toupdateTemplateSource
v0.1.0
- Initial public release
License
MIT.
Copyright (c) 2016 Michael Heim.
Code in the data provider test helper: (c) 2014 Box, Inc., Apache 2.0 license. See file.