121
votes

I use Underscore template. It is possible to attach a external file as template?

In Backbone View I have:

 textTemplate: _.template( $('#practice-text-template').html() ),

 initialize: function(){                                            
  this.words = new WordList;            
  this.index = 0;
  this.render();
 },

In my html is:

<script id="practice-text-template" type="text/template">
   <h3>something code</h3>
</script>

It works well. But I need external template. I try:

<script id="practice-text-template" type="text/template" src="templates/tmp.js">

or

textTemplate: _.template( $('#practice-text-template').load('templates/tmp.js') ),

or

$('#practice-text-template').load('templates/tmp.js', function(data){ this.textTemplate = _.template( data ) })

but it did not work.

12

12 Answers

51
votes

EDIT: This answer is old and outdated. I'd delete it, but it is the "accepted" answer. I'll inject my opinion instead.

I wouldn't advocate doing this anymore. Instead, I would separate all templates into individual HTML files. Some would suggest loading these asynchronously (Require.js or a template cache of sorts). That works well on small projects but on large projects with lots of templates, you find yourself making a ton of small async requests on page load which I really dislike. (ugh... ok, you can get around it with Require.js by pre-compiling your initial dependencies with r.js, but for templates, this still feels wrong to me)

I like using a grunt task (grunt-contrib-jst) to compile all of the HTML templates into a single templates.js file and include that. You get the best of all worlds IMO... templates live in a file, compilation of said templates happen at build time (not runtime), and you don't have one hundred tiny async requests when the page starts up.

Everything below is junk

For me, I prefer the simplicity of including a JS file with my template. So, I might create a file called view_template.js which includes the template as a variable:

app.templates.view = " \
    <h3>something code</h3> \
";

Then, it is as simple as including the script file like a normal one and then using it in your view:

template: _.template(app.templates.view)

Taking it a step further, I actually use coffeescript, so my code actually looks more like this and avoid the end-of-line escape characters:

app.templates.view = '''
    <h3>something code</h3>
'''

Using this approach avoids brining in require.js where it really isn't necessary.

107
votes

Here is a simple solution:

var rendered_html = render('mytemplate', {});

function render(tmpl_name, tmpl_data) {
    if ( !render.tmpl_cache ) { 
        render.tmpl_cache = {};
    }

    if ( ! render.tmpl_cache[tmpl_name] ) {
        var tmpl_dir = '/static/templates';
        var tmpl_url = tmpl_dir + '/' + tmpl_name + '.html';

        var tmpl_string;
        $.ajax({
            url: tmpl_url,
            method: 'GET',
            dataType: 'html', //** Must add 
            async: false,
            success: function(data) {
                tmpl_string = data;
            }
        });

        render.tmpl_cache[tmpl_name] = _.template(tmpl_string);
    }

    return render.tmpl_cache[tmpl_name](tmpl_data);
}

Using "async: false" here is not a bad way because in any case you must wait until template will be loaded.

So, "render" function

  1. allows you to store each template in separate html file in static dir
  2. is very lightweight
  3. compiles and caches templates
  4. abstracts template loading logic. For example, in future you can use preloaded and precompiled templates.
  5. is easy to use

[I am editing the answer instead of leaving a comment because I believe this to be important.]

if templates are not showing up in native app, and you see HIERARCHY_REQUEST_ERROR: DOM Exception 3, look at answer by Dave Robinson to What exactly can cause an "HIERARCHY_REQUEST_ERR: DOM Exception 3"-Error?.

Basically, you must add

dataType: 'html'

to the $.ajax request.

18
votes

This mixin allows you to render external template using Underscore in very simple way: _.templateFromUrl(url, [data], [settings]). Method API is almost the same as Underscore's _.template(). Caching included.

_.mixin({templateFromUrl: function (url, data, settings) {
    var templateHtml = "";
    this.cache = this.cache || {};

    if (this.cache[url]) {
        templateHtml = this.cache[url];
    } else {
        $.ajax({
            url: url,
            method: "GET",
            async: false,
            success: function(data) {
                templateHtml = data;
            }
        });

        this.cache[url] = templateHtml;
    }

    return _.template(templateHtml, data, settings);
}});

Usage:

var someHtml = _.templateFromUrl("http://example.com/template.html", {"var": "value"});
17
votes

I didn't want to use require.js for this simple task, so I used modified koorchik's solution.

function require_template(templateName, cb) {
    var template = $('#template_' + templateName);
    if (template.length === 0) {
        var tmpl_dir = './templates';
        var tmpl_url = tmpl_dir + '/' + templateName + '.tmpl';
        var tmpl_string = '';

        $.ajax({
            url: tmpl_url,
            method: 'GET',
            contentType: 'text',
            complete: function (data, text) {
                tmpl_string = data.responseText;
                $('head').append('<script id="template_' + templateName + '" type="text/template">' + tmpl_string + '<\/script>');
                if (typeof cb === 'function')
                    cb('tmpl_added');
            }
        });
    } else {
        callback('tmpl_already_exists');
    }
}

require_template('a', function(resp) {
    if (resp == 'tmpl_added' || 'tmpl_already_exists') {
        // init your template 'a' rendering
    }
});
require_template('b', function(resp) {
    if (resp == 'tmpl_added' || 'tmpl_already_exists') {
        // init your template 'b' rendering
    }
});

Why to append templates to document, rather than storing them in javascript object? Because in production version I would like to generate html file with all templates already included, so I won't need to make any additional ajax requests. And in the same time I won't need to make any refactoring in my code, as I use

this.template = _.template($('#template_name').html());

in my Backbone views.

16
votes

This might be slightly off topic, but you could use Grunt (http://gruntjs.com/) - which runs on node.js (http://nodejs.org/, available for all major platforms) to run tasks from the command line. There are a bunch of plugins for this tool, like a template compiler, https://npmjs.org/package/grunt-contrib-jst. See documentation on GitHub, https://github.com/gruntjs/grunt-contrib-jst. (You will also need to understand how to run node package manager, https://npmjs.org/. Don't worry, it's incredibly easy and versatile. )

You can then keep all your templates in separate html files, run the tool to precompile them all using underscore (which I believe is a dependency for the JST plugin, but don't worry, node package manager will auto install dependencies for you).

This compiles all your templates to one script, say

templates.js

Loading the script will set a global - "JST" by default - which is an array of functions, and can be accessed like so:

JST['templates/listView.html']()

which would be similar to

_.template( $('#selector-to-your-script-template'))

if you put the content of that script tag in (templates/)listView.html

However, the real kicker is this: Grunt comes with this task called 'watch', which will basically monitor changes to files that you have defined in your local grunt.js file (which is basically a config file for your Grunt project, in javascript). If you have grunt start this task for you, by typing:

grunt watch

from the command line, Grunt will monitor all changes you make to the files and auto-execute all tasks that you have setup for it in that grunt.js file if it detects changes - like the jst task described above. Edit and then save your files, and all your templates recompile into one js file, even if they are spread out over a number of directories and subdirectories.

Similar tasks can be configured for linting your javascript, running tests, concatenating and minifying / uglifying your script files. And all can be tied to the watch task so changes to your files will automatically trigger a new 'build' of your project.

It takes some time to set things up and understand how to configure the grunt.js file, but it well, well worth the time invested, and I don't think you will ever go back to a pre-grunt way of working

15
votes

I think this is what might help you. Everything in the solution revolves around require.js library which is a JavaScript file and module loader.

The tutorial at the link above shows very nicely how a backbone project could be organized. A sample implementation is also provided. Hope this helps.

4
votes

I got interested on javascript templating and now I'm taking the first steps with backbone. This is what i came up with and seems to work pretty well.

window.App = {

    get : function(url) {
        var data = "<h1> failed to load url : " + url + "</h1>";
        $.ajax({
            async: false,
            url: url,
            success: function(response) {
                data = response;
            }
        });
        return data;
    }
}

App.ChromeView = Backbone.View.extend({
    template: _.template( App.get("tpl/chrome.html") ),
    render: function () {
        $(this.el).html(this.template());
        return this;
    },
});

App.chromeView = new App.ChromeView({ el : document.body });
App.chromeView.render();
4
votes

I had to set the data type to "text" to make it work for me:

get : function(url) {
    var data = "<h1> failed to load url : " + url + "</h1>";
    $.ajax({
        async: false,
        dataType: "text",
        url: url,
        success: function(response) {
            data = response;
        }
    });
    return data;
}
2
votes

I found a solution that works for me with using jQuery.

I add the underscore template code, with jQuery.load() method, to the main html file.

Once it's there, I'm using it for generating the templates. All need to happen synchronously!

The concept is:

I have a underscore map template code:

<!-- MAP TEMPLATE-->
<script type="text/template" id="game-map-template">
    <% _.each(rc, function(rowItem, index){ %>
      <ul class="map-row" data-row="<%- index %>">
        <li class="map-col <%- colItem.areaType ? 'active-area' : '' %>"></li>
        ...
</script>

And I put that code in a file called map-template.html

After that I create a a wrapper for the template files.

<div id="templatesPool"></div>

Then I include that file in my main html file like so.

In head:

<!-- Template Loader -->
<script> 
    $(function(){
      $("#templatesPool").append($('<div>').load("map-template.html")); 
    });
</script> 

Cheers.

1
votes

I know this question is really old but it came up as the first result on a google search for underscore ajax templates.

I was tired of not finding a good solution for this so I created my own:

https://github.com/ziad-saab/underscore-async-templates

In addition to loading underscore templates using AJAX, it adds <% include %> functionality. I hope it can be useful to someone.

0
votes

I was a bit uneasy forcing jQuery to function synchronously, so I modified the previous synchronous example using promises. It's pretty much the same, but runs asynchronously. I'm using hbs templates in this example:

var asyncRenderHbs= function(template_name, template_data) {
    if (!asyncRenderHbs.template_cache) { 
        asyncRenderHbs.template_cache= {};
    }

    var promise= undefined;

    if (!asyncRenderHbs.template_cache[template_name]) {
        promise= new Promise(function(resolve, reject) {
            var template_url= '/templates/' + template_name;
            $.ajax({
                url: template_url,
                method: 'GET',
                success: function(data) {
                    asyncRenderHbs.template_cache[template_name]= Handlebars.compile(data);
                    resolve(asyncRenderHbs.template_cache[template_name](template_data));
                },
                error: function(err, message) {
                    reject(err);
                }           
            });
        });
    } else {
        promise= Promise.resolve(asyncRenderHbs.template_cache[template_name](template_data));
    }

    return promise;
};

Then to use the rendered html:

asyncRenderHbs('some_template.hbs', context)
    .then(function(html) {
        applicationMain.append(html);
        // Do other stuff here after html is rendered...
    })
    .catch(function(err) {
        // Handle errors
    });

NOTE: As discussed by others, it would be preferable to compile all templates into a single templates.js file and load that in the beginning rather than have many small synchronous AJAX calls to get templates when the webpage loads.

0
votes

Forward warning - Here be dragons:

I mention the approach shown below simply to help those struggling to make ASP.NET stacks (and similar frameworks) work harmoniously with the ecosystem of js-libs. It goes without saying that this is not a generic solution. Having said that ...

/endforwardwarning

If you are using ASP.NET you can externalize your templates simply by placing them inside one or more partial views of their own. Aka inside your .cshtml:

  @Html.Partial("path/to/template")

Inside your template.cshtml:

   // this is razorview and thusly if you ever need to use the @ character in here  
   // you will have to either escape it as @@ or use the html codepoint which is &#64
   // http://stackoverflow.com/questions/3626250/escape-character-in-razor-view-engine
   <script type="text/x-template" id="someId">
        <span class="foo"><%= name %></span>
   </script>

And now you can use the template like usual:

  _.template($("#someId").html())({ name: "Foobar" });

Hope this elusively-obvious approach helps someone save an hour's worth of head-scratching.