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.
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>°</sup></span>
<span class="weather-info temperature low">${lowTemperature}<sup>°</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:
script
element usually rules out development niceties like IDE syntax highlighting, intellisense and so on.RequireJS actually helps us with both issues.
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:
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:
paths
object are general dependencies (Kendo UI, jQuery and the Text loader plugin for RequireJS).templateLoader
in our app. We'll look at that one in a moment.script
element anymore.main.js
, we're requiring app.js
and calling init
to kick things off.Let's quickly look at the other relevant modules.
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(); } }; });
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>°</sup></span>
<span class="weather-info temperature low">${lowTemperature}<sup>°</sup></span>
</div>
<div class="position-center">
<span class="weather-icon ${image}"></span>
</div>
</div>
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 ); } }); });
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?
script
element.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.
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
You can download or clone the code example used in this blog post by grabbing it from github.
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.