Knockout.js 1.3 External Templates

On December 2, 2011, in JavaScript, by Jim Cowart

{ 02/04/2012 Update:  This is now available as a nuget package if you’re in the .NET world. }

Finally!  I updated my Knockout.js-External-Templates plugin to support the new template architecture in Knockout.js 1.3.  “So,” you ask, “why would I want to use this?”  If you’re a developer using Knockout.js, perhaps you’ve run into the inevitable code bloat as your templates multiply and begin to crowd your document?  If you’ve been using jQuery templates with Knockout.js, perhaps you’ve not only grown tired of keeping templates in SCRIPT elements, but you’ve also wanted to take advantage of real markup syntax highlighting in your favorite IDE (which should be WebStorm, by the way) – but alas, markup inside a SCRIPT element just can’t do that.  Fret no more!  This plugin will enable you to:

  • Separate your concerns by keeping templates in separate files, OR in the document, OR both (mix and match to your heart’s content).
  • Take advantage of syntax highlighting by storing your native or jQuery templates in their own HTML file.
  • Lazy load templates only as they are needed by your application.

Enough preamble already, let’s look at an example.

Here we have a simple page that displays a list of states.  Each state has a list of cities associated with it, and each city has an image and an accompanying list of statistics:

KoExternalTemplateExample App

Here’s a look at the JavaScript view model that contains the data bound to the view(s) on the page:

var viewModel = {
    states: [
        new State("Tennessee", "Southeast", [
            new City("Nashville", [
                new Statistic("Population", "749,935"),
                new Statistic("Mayor" ,"Karl Dean")
            ]),
            new City("Franklin", [
                new Statistic("Population", "62,487"),
                new Statistic("Mayor" ,"Ken Moore")
            ]),
            new City("Brentwood", [
                new Statistic("Population", "37,060"),
                new Statistic("Mayor" ,"Paul Webb")
            ]),
            new City("Murfreesboro", [
                new Statistic("Population", "108,755"),
                new Statistic("Mayor" ,"Tommy Bragg")
            ])
        ]),
        new State("Georgia", "Southeast", [
            new City("Atlanta", [
                new Statistic("Population", "3,500,000"),
                new Statistic("Mayor" ,"Kasim Reed")
            ]),
            new City("Snellville", [
                new Statistic("Population", "18,242"),
                new Statistic("Mayor" ,"Jerry Oberholtzer")
            ])
        ]),
        new State("Ohio", "Mid-West", [
            new City("Columbus", [
                new Statistic("Population", "1,100,000"),
                new Statistic("Mayor" ,"Michael B. Coleman")
            ])
        ]),
    ]
};

To drive this page, we have a state template, a city template and a statistics template, all of which are stored separately from the index.html page, here’s a screen capture of the project folder hierarchy:

File Structure

As you can see from above, the templates reside in the “templates” folder – relative to the index.html document.  At the top of the main.js file, we tell infuser where to go and look for templates with this line: infuser.defaults.templateUrl = “templates”;.  You can easily change the template look-up location by altering the templateUrl.

The state.html template:

<li class="state-container">
    <h3 data-bind="text: name"></h3>
    <div>
        <ul data-bind="template: { name: 'city', foreach: cities }"></ul>
    </div>
</li>

The city.html template:

<li class="city-container">
    <div>
        <img class="city-img" data-bind="attr: { src: img, alt: name }">
        <div class="city-facts">
            <em data-bind="text:name"></em>
            <div data-bind="template: { name: 'stats'}"></div>
        </div>
        <span style="clear:both;"></span>
    </div>
</li>

The stats.html template:

<ul data-bind="foreach: stats">
    <li class="stat-container" data-bind="ifnot: editing">
        <div class="stat stat-name" data-bind="text:name"></div>
        <div class="stat stat-value" data-bind="text:value, click: toggleEdit "></div>
    </li>

    <li class="stat-container" data-bind="if: editing">
        <span class="stat stat-name" data-bind="text:name"></span>
        <input class="stat stat-value"type="text" data-bind="value: value"><input type="button" value="save" data-bind="click: toggleEdit">
    </li>
</ul>

The main index.html file is very lean, holding only the script & css includes, plus the initial placeholder for the starting template:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
        "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
    <title>koExternalTemplateEngine Example</title>
    <link rel="stylesheet" href="css/style.css">
    <script type="text/javascript" src="js/jquery-1.5.2.js"></script>
    <script type="text/javascript" src="js/knockout-latest.debug.js"></script>
    <script type="text/javascript" src="js/koExternalTemplateEngine_all.js"></script>
    <script type="text/javascript" src="js/main.js"></script>
</head>
<body>
    <ul class="state-list" data-bind="template: { name: 'state', foreach: states }"></ul>
</body>
</html>

When you call ko.applyBindings(viewModel), Knockout will parse the template binding on line 13 (UL element) in the index.html file and ask the template engine to get the template content for the “state” template. The KoExternalTemplateEngine will check the DOM first (if you included the template in the page), and if it doesn’t find it locally, it tells infuser to pull the template down from the external endpoint you configured via the infuser.defaults options. (It’s worth noting that the KoExternalTemplateEngine will handle anonymous templates just like the native engine if the template being retrieved isn’t a DOM template or an external one.) As the “state” template is evaluated, the same steps will occur when Knockout asks for the “city” template, and again when it comes across the template binding to the “stats” template.

Although this is a very simple example, I’ve included a click binding that will swap the “view” template for a statistic out with an “edit” version if you click on one of the statistic values.  This is to demonstrate that all the normal “native KO template” functionality behaves as you would expect.

KoExternalTemplateEngine Example App 2

The KoExternalTemplateEngine also supports jQuery templates (special thanks to Ryan Niemeyer for fixing the last two bugs I had with jquery-tmpl support).  Since it takes a dependency on infuser, you may want to look at the configuration options available in infuser (it will give you an idea of how you can control where templates are pulled from, etc.).

The sample app from this blog post is included as part of the KoExternalTemplateEngine repository on github (it’s in the example/native2 folder).

Tagged with:  
  • Pingback: Knockout.js 1.3 External Templates « If & Else

  • http://www.Marisic.Net dotnetchris

    I think you left out an extremely important illustration on this blog post, and that’s where exactly your templates are, and what they look where they are.

    I assume they have to be in main.js? But i’m not sure.

    • http://ifandelse.com Jim Cowart

      Hi Chris – sorry if anything was unclear. I tend to get long-winded in my posts, so I was trying to keep it brief. :-) If you look at the example app on github (https://github.com/ifandelse/Knockout.js-External-Template-Engine/tree/master/example/native2), you’ll notice a templates folder. The templates are in their own individual files inside that folder. At the top of the “main.js” file is this line:
      infuser.config.templateUrl = “templates”;
      This tells infuser (the lib this plugin uses to fetch templates) to look in a “templates” directory relative to the current page. (Infuser.config also gives you options for prefix and suffix naming conventions.)

      Thanks for your feedback – I’ll try and update the post today to help clarify where the templates reside. In the meantime, if you browse (or pull down) the sample project, it will probably make a lot of sense…

      • Thomas Mckearney

        Adding a tiny image w/ you directory tree would be helpful. Are they HTML files? Is the content you’re showing the entire contents of the file? Not enough info here

        • http://ifandelse.com Jim Cowart

          I’ve included links to the github repo in the post that make all of the source available, so you can see exactly what is in each file, and the folder structure, etc. In addition, I included a screen shot of this example app’s folder hierarchy (expanded to show files) in the post (it’s approx in the middle). Thanks for the feedback!

  • Pingback: The Morning Brew - Chris Alcock » The Morning Brew #995

  • Anonymous

    Do you have an example of how to integrate with a different template engine, such as the new jsRender stuff?

    • http://ifandelse.com Jim Cowart

      I don’t currently have a template engine supporting jsRender. It’s on my list to look into (and it’s possible someone else has already written one). I might revisit that once my current series on JavaScript pub/sub is done.

  • http://www.mohundro.com/blog/ David Mohundro

    Looks good – I have been doing something relatively similar with Razor partial views, but this looks a lot cleaner.

    Forgive me if you’ve mentioned this, but would it support multiple directories? For example, if I’ve abstracted a viewModel into a separate JS module and am applying bindings to a subset of the HTML (via applyBindings(vm, $(‘#some-div’)[0]), I might have those templates in a different directory as well. Hope my question makes sense.

    Thanks for the post!

    • http://ifandelse.com Jim Cowart

      Hi! That’s a great question. It currently allows only one directory *per page* (i.e. – the templateUrl setting is used for all requests from the current page). You could technically change it on an as-needed basis (which is ugly, I know), but I’m planning on looking into allowing you to pass the templateUrl as one of the values in the template binding (another dev asked me if I could look into that, and I think it sounds like a good idea).

      (btw – sorry for taking so long to get back to you, I’ve been out of town…)

    • http://ifandelse.com Jim Cowart

      David, I wanted to post an update to let you know that this template engine plugin now supports multiple directories per page – you can override the templateUrl (and other options) in the template binding itself if you are using native KO templates.

      • http://www.mohundro.com/blog/ David Mohundro

        Awesome, thanks for letting me know – I’ll try to check it out!

  • Tom Shadle

    Is it possible to use external templates offline (local file system, no web server)? I get “The template view could not be loaded. HTTP Status code: 404″ when I open the native example.

    Thanks.

    • http://ifandelse.com Jim Cowart

      Unfortunately you will run into an Access-Control-Allow-Origin issue, since the Origin will be null. This goes back to browser security, so it’s beyond the scope of anything the plugin could address. However, you could check out the answer to this question on stackoverflow and see if starting Chrome with the switch to allow accessing of local files can help you out: http://stackoverflow.com/questions/4208530/xmlhttprequest-origin-null-is-not-allowed-access-control-access-allow-for-file.

      If you’re on Linux/Mac OS X, though, I’d just grab nvm (https://github.com/creationix/nvm), pull down the latest version of node, and the run “node nodetesthost.js” at the root of the repository and it will host all the examples (under “/example”). If you’re on Windows, I’d set up a virtual directory in IIS and point it to the root of the repository, and the examples will get hosted under “/example”. Thanks!

  • http://www.facebook.com/chris.a.keyser Chris Keyser

    hi Jim – thanks for the the framework, it’s very useful. I am find debugging to be a pain since the templates are being cached. I can’t find a good way to override this behavior – and its time consuming to close and reopen the browser to get the templates released from cache. Is there a better way, or a way to force the templates not to cache that I can apply during development?

    Thanks.

    • http://ifandelse.com Jim Cowart

      Chris – sorry for the debugging woes. You should be able to disable caching, though, since infuser is effectively passing options down to $.ajax(). I’m going from memory here, so bear with me, but you can try putting this in where you’re configuring infuser: infuser.defaults.ajax.cache = false; The infuser.defaults.ajax options get passed on through to the ajax call….so give that a try and see if it helps. Keep me posted!

      • http://www.facebook.com/chris.a.keyser Chris Keyser

        I had given this a try Jim, but it looks like the template is being cached in a collection, templates (template = infuser.store.getTemplate(templateOptions.ajax.url); I’m guessing this ajax option is only telling jquery not to serve it back from browser cache.

        Not sure what I was doing wrong before, but it seems now to be dumping the template from the cache when I force refresh the page w/o closing the browser. Thanks for your help.

        • http://ifandelse.com Jim Cowart

          Chris – glad it’s working for you now. FWIW – you can also call the infuser.store.purge() method to clear the in-memory cache. Worst case – the “nuke it from orbit” approach – you can implement a “dummy” version of the hashStorage object that doesn’t actually store the template in the storeTemplate() method…..forcing it to always retrieve. (infuser.store holds the reference to whatever storage piece is being used). Anyways, hopefully things keep going smoothly now.

  • Michael Wulf

    Did you find a good solution for integrating Knockout.js tags/libs into WebStorm?