Telerik blogs

I've used RequireJS a lot over the last 2+ years, and as I've been learning Kendo UI, I was curious how well the two could work together. Burke Holland has already written a bit on this topic as well, but I wanted to find out if I could use the text loader plugin for RequireJS to load Kendo templates from external files. It turns out, you can. There's just a slight twist in doing so.

If you're not familiar with RequireJS, check out this post to get more familiar. Then read some of Burke's material. Once you feel good about those, this post will make sense! If you want to learn more about optimizing a RequireJS project, read this.

Templates The Normal Way

First thing's first - let's do a quick re-cap of how you'd handle templates in a "normal" Kendo UI application (not using RequireJS). Let's say we have a weather app that lists the forecast for the next few days (btw - if you check out the Kendo UI project template in Icenium, you'll see this example). Our markup would look like this:

<!DOCTYPE html>
<html>
<head>
<title>Weather Example</title>
<meta charset="utf-8"/>
<link href="kendo/styles/kendo.mobile.all.min.css" rel="stylesheet"/>
<link href="css/main.css" rel="stylesheet"/>
</head>
<body>
<!--Weather-->
<div id="weather"
data-role="view"
data-title="Weather"
data-model="app.weatherService.viewModel">

<div class="weather">
<p class="weather-title">20-Day Forecast</p>

<div class="separator">
<div class="dark"></div>
<div class="light"></div>
</div>

<ul class="forecast-list"
data-role="listview"
data-bind="source: weatherDataSource"
data-template="weather-forecast-template">
</ul>
</div>
</div>

<!--Weather forecast template-->
<script type="text/x-kendo-tmpl" id="weather-forecast-template">
<div>
<div class="position-left">
<span class="weather-info date">${day}</span>
</div>
<div class="position-right">
<span class="weather-info temperature high">${highTemperature}<sup>&deg;</sup></span>
<span class="weather-info temperature low">${lowTemperature}<sup>&deg;</sup></span>
</div>
<div class="position-center">
<span class="weather-icon ${image}"></span>
</div>
</div>
</script>

<!--Layout-->
<div data-role="layout" data-id="layout">
<!--Header-->
<div data-role="header">
<div data-role="navbar">
<span data-role="view-title"></span>
</div>
</div>
</div>
<script src="js/jquery.min.js"></script>
<script src="kendo/js/kendo.mobile.min.js"></script>
<script src="js/app.js"></script>
</body>
</html>

You'll notice in the above markup that we're adding a script element to the page with a type of text/x-kendo-tmpl. This is a common approach with many templating languages – using a script element with a custom type so that the browser doesn't attempt to parse and evaluate it as JavaScript.

The other important thing to catch is that the ul element containing our weather forecasts has the following attribute: data-template="weather-forecast-template". Is just so happens that our script element containing our weather template has an id of "weather-forecast-template". This is a very convenient way in which Kendo UI enables you to indicate which "item" template should be used in something like a listview.

There's nothing wrong with this approach - it works. But I do have two complaints:

  • Nesting markup inside a script element usually rules out development niceties like IDE syntax highlighting, intellisense and so on.
  • We can't re-use this template without copying and pasting it into another html file.

RequireJS actually helps us with both issues.

Templates via RequireJS + text Loader Plugin

While Kendo UI is wrapped in an asynchronous module definition, it doesn't declare its dependency on jQuery. In order to use it in a RequireJS project, we have to "shim" it (or you could use a loader plugin to ensure that jQuery is loaded first, but this shim approach accomplishes the same task). We'll add a main.js file to our app, which sets up our RequireJS configuration. It looks like this:

main.js

require.config({
    paths: {
        kendo            : "kendo/js/kendo.mobile.min",
        jquery           : "jquery/jquery.min",
        text             : "require/text",
        templateLoader   : "infrastructure/kendo-template-loader",
        weatherViewModel : "weather/WeatherViewModel",
        weatherTemplate  : "weather/forecast-template.html"
    },

    shim: {
        kendo: {
            deps: ['jquery'],
            exports: 'kendo'
        }
    }
});

require(['app'], function (app) {
    $(function () {
        app.init();
    });
}); 

Some things to notice about the above code:

  • The first three files in the paths object are general dependencies (Kendo UI, jQuery and the Text loader plugin for RequireJS).
  • We've created a module called "kendo-template-loader" and have effectively aliased it as templateLoader in our app. We'll look at that one in a moment.
  • We've moved our WeatherViewModel out to it's own module, and our forecast template is now in a separate html file, not in the index.html's script element anymore.
  • Our "app.js" is also in a separate module, and you can see at the end of main.js, we're requiring app.js and calling init to kick things off.

Let's quickly look at the other relevant modules.

app.js

Our app.js module is creating the application namespace that will eventually be attached to the window (since our declarative bindings in our markup expect it to be there). The main behavior is in the init method, where we create Kendo UI Mobile application and an instance of our WeatherViewModel. The module value itself is the object literal being returned.

define([
    'kendo',
    'weather/WeatherViewModel'
], function ( kendo, WeatherViewModel ) {
    var os = kendo.support.mobileOS;
    var statusBarStyle = os.ios && os.flatVersion >= 700 ? "black-translucent" : "black";
    return {
        kendoApp : null,
        weatherService : {
            viewModel: null
        },
        init: function() {
            this.kendoApp = new kendo.mobile.Application( document.body, { layout: "layout", statusBarStyle: statusBarStyle });
            this.weatherService.viewModel = new WeatherViewModel();
        }
    };
});

 

forecast-template.html

This is the same markup template we had nested inside a script tag in our "normal" example. Now it's in its own file.

 

<div>
<div class="position-left">
<span class="weather-info date">${day}</span>
</div>
<div class="position-right">
<span class="weather-info temperature high">${highTemperature}<sup>&deg;</sup></span>
<span class="weather-info temperature low">${lowTemperature}<sup>&deg;</sup></span>
</div>
<div class="position-center">
<span class="weather-icon ${image}"></span>
</div>
</div>

 

WeatherViewModel

The WeatherViewModel is almost identical to our "normal" example. The only change (aside from being wrapped in an asynchronous module definition), is that we added a call to templateLoader.ensureLoaded at the beginning of the init method. We pass the string ID of the template (this is used to populate the ID value of a script tag that will be created on the fly), and we pass the template text itself. Notice that in dependencies list for this module, our last dependency is "text!weatherTemplate". The "text!" part tells RequireJS to use the "text" loader plugin to load the file mapped to "weatherTemplate". Our main.js file has this mapped to "weather/forecast-template.html", so that file will be loaded like any other module, except that the module's value will be the text content of the file.

 

define([
    'kendo',
    'templateLoader',
    'text!weatherTemplate'
], function( kendo, templateLoader, weatherTemplate ){
    return kendo.data.ObservableObject.extend({
        weatherDataSource: null,

        init: function () {
            templateLoader.ensureLoaded("weather-forecast-template", weatherTemplate);
            kendo.data.ObservableObject.fn.init.apply( this, [] );

            var dataSource = new kendo.data.DataSource({
                transport: {
                    read: {
                        url: "data/weather.json",
                        dataType: "json"
                    }
                }
            });

            this.set( "weatherDataSource", dataSource );
        }
    });
});

 

kendo-template-loader

The "kendo-template-loader" is a lightweight (possibly naive) abstraction I added in order to load Kendo UI templates into the DOM asynchronously. The ensureLoaded method takes an ID and the template text as arguments. It then checks to see if that template has been loaded before or not. If it hasn't, it creates a script element, setting the ID & type attributes as well as the contents of the script element. Now this template will exist in the DOM when the Kendo UI framework resolves the "data-template" attribute value we set on our unordered list.

 

By keeping track of templates, you have the option to purge them from the DOM if necessary at a later point. In a real world scenario you might choose to load several templates up front and not just on-demand.

define([
    'jquery'
], function ($) {
    var templates = {};

    return {
        templates    : templates,
        ensureLoaded : function(tmplId, templateString) {
            if(!templates[tmplId]) {
                templates[tmplId] = $('').text(templateString).appendTo("body")[0];
            }
        }
    }
});

So - what have done?

  • We've moved our forecast template into it's own file, so that it's not only resuable (without the need for copy/paste) but it's also going to benefit from our IDE's syntax highlighting, auto-completion and other features.
  • Since we're using the "text" loader plugin for RequireJS, when our app requests our template, it will actually get a module instance where the return value is the template text.
  • We created an abstraction that takes a template, and adds it to the DOM inside a script element.
  • We've told our WeatherViewModel to use the template loader abstraction to load the template into the DOM as it initializes.

Is it odd to you that we're still putting the template in the DOM via a script element? Honestly, that part doesn't bother me as much as my view model needing to pass an ID that matches up to the data-template attribute value. Lucky for us, there's a way we can do this that doesn't require either.

Imperative Instead of Declarative

In the above example, we were still depending on Kendo UI's declarative bindings to map the correct script element to the ID value set as the template. But instead of setting the data-template value, we can set the template value of widget imperatively in JavaScript. If you <3 declarative bindings, you may not like this approach. However, if you want to skip the "add the script into the DOM" middle man and just pass the template text itself directly to the widget, you can change the WeatherViewModel to be this:

define([
    'kendo',
    'text!weatherTemplate'
], function( kendo, weatherTemplate ){
    return kendo.data.ObservableObject.extend({
        weatherDataSource: null,

        init: function (listView) {
            var self = this;

            listView.kendoMobileListView({
                template : kendo.template(weatherTemplate)
            });

            kendo.data.ObservableObject.fn.init.apply( self, [] );

            var dataSource = new kendo.data.DataSource({
                transport: {
                    read: {
                        url: "data/weather.json",
                        dataType: "json"
                    }
                }
            });

            self.set( "weatherDataSource", dataSource );
        }
    });
});

We've removed all mention of the "templateLoader" and instead, we pass a jQuery selector result in as the listView argument. We call kendoMobileListView and pass the template property in to set the template used for the items in the ListView. This also required a small change in our app.js file. When we create an instance of the WeatherViewModel, we need to pass in the listView:

// other app.js code
init: function() {
    this.kendoApp = new kendo.mobile.Application( document.body, { layout: "layout", statusBarStyle: statusBarStyle });
    this.weatherService.viewModel = new WeatherViewModel($("#forecast"));
}
// other app.js code

 

Wrapping Up

So there you have it - two ways to load templates using RequireJS and the Text loader plugin. As always, take the approach that makes the most sense/fits within your app's architecture. Adding an abstraction to manage loading the templates into the DOM, or directly to the widget instances isn't difficult. Interested in looking at some more advanced work along these lines? Check out some of the work Burke Holland did in the Chrome Cam app.

 

You can download or clone the code example used in this blog post by grabbing it from github.


About the Author

Jim Cowart

Jim Cowart is an architect, developer, open source author, and overall web/hybrid mobile development geek. He is an active speaker and writer, with a passion for elevating developer knowledge of patterns and helpful frameworks. 

Comments

Comments are disabled in preview mode.