We recently announced that Kendo UI Mobile widgets have been decoupled from the Kendo UI Mobile framework so that they can be used anywhere and with other JavaScript frameworks. Kendo UI Mobile as originally intended to be used specifically for building hybrid mobile (PhoneGap) applications. In fact, it was one of the very first frameworks designed specifically with the hybrid application in mind. We've had over two years to tweak, refine and perfect Kendo UI Mobile. Given the announcement about the decoupling, you might be wondering - when should I use the mobile application framework, and when should I just use the mobile widgets?
Great question!
A good rule of thumb is that if you are building a hybrid (Cordova/PhoneGap) application, you want to use the Mobile Application Framework. If you are doing mobile web development, you will want to use the decoupled mobile widgets with a CSS and JavaScript framework of your choice (i.e. The Kendo UI SPA framework). Even though both mobile web and hybrid apps are built with HTML,
The development is done entirely in the front end. There are no URLs to post back to; no ASP.NET MVC or PHP to query your database with. Hybrid apps expect that you will be providing data via a service which is probably written
At least in-so-much as the user knows or expects. How you built this installed app is irrelevant to them. How it works is paramount. Users have
The same features that users expect from mobile apps become incredibly obnoxious on the web. Those loud sweeping animations are awkward and gaudy in a
author: Adam Kliczek / Wikipedia, licence: CC-BY-SA-3.0
There is no need for those transitions on the web. User's have a completely different set of expectations. Just walk normally man!
Web applications can be structured with any framework - be it Kendo UI SPA, Backbone, Angular or another. Applications that use the Kendo UI Mobile framework are different in that they cannot be used with other frameworks since the mobile application framework controls everything in the application from top to bottom in order to guarantee that "native" experience.
Without
We'll be creating a Todo List application with a sliding drawer of categories. You can run this application on iOS using the free AppBuilder Companion App. Just scan the QR code below.
Or you can just clone the project on GitHub. I've also embedded the application
Since we are building a hybrid application, I'm going to be using AppBuilder and the new CLI to create a new empty project.
> appbuilder create kendo-mobile-todo --template blank
This creates a new "blank" hybrid application in-so-much as it doesn't contain any libraries for you to work with for building your application. It does contain some boilerplate code that we want to get rid of, so delete the
In order to build this application, we need to install the required dependencies - not the least of which is Kendo UI Mobile.
I use Bower to get RequireJS, the RequireJS Text
> bower install requirejs > bower install requirejs-text > bower install jquery-tiny-pubsub
Open the index.html
file and delete everything. Replace it with the following markup.
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta name="format-detection" content="telephone=no" /> <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height, target-densitydpi=device-dpi" /> <title>Todos</title> <link rel="stylesheet" href="kendo/styles/kendo.mobile.all.min.css"> <link rel="stylesheet" href="styles/main.css"> </head> <body> <script src="cordova.js"></script> <script src="bower_components/jquery/jquery.min.js"></script> <script src="bower_components/jquery-tiny-pubsub/dist/ba-tiny-pubsub.js"></script> <script src="kendo/js/kendo.mobile.min.js"></script> <script src="bower_components/requirejs/require.js" data-main="app/main.js"></script> </body> </html>
You will see that Require is the last script file being loaded and it's data-main
is set app/main.js
// configure the path to the text plugin since it is not in the same directory // as require.js require.config({ paths: { 'text': '../bower_components/requirejs-text/text' } }); define([ 'app' ], function (app) { // if we are running on device, listen for cordova deviceready event if (kendo.mobileOs) { document.addEventListener('deviceready', function () { // initialize application app.init(); // hide the native spash screen navigator.splashscreen.hide(); }, false); } else { // we are running on the web (prolly debug) so just show the app app.init(); } });
The main file is listening for that all important
Now add an app.js file to the app directory. Place the following code in the app.js file:
define([ 'views/todos/todos', 'views/categories/categories', 'views/newTodo/newTodo', 'views/newCategory/newCategory' ], function () { // create a global container object var APP = window.APP = window.APP || {}; var init = function () { // initialize the application APP.instance = new kendo.mobile.Application(document.body, { skin: 'flat' }); }; return { init: init }; });
The app file brings in all of the views which are separated out into "modules" if you will. Each view has a .js file and an
Create the following directory structure under the app folder.
Kendo UI Mobile will require some items to be accessible from the global namespace. Each "view" - be it a regular Kendo UI Mobile View, Drawer, ModalView, PopOver or other visual container needs to be able to access
Create a new file in the views folder called view.js. Place the following code inside:
define([], function () { var APP = window.APP = window.APP || {}; var View = kendo.Class.extend({ init: function (name, template, model, events) { // append the template to the DOM this.html = $(template).appendTo(document.body); // expose the model and events off the global scope APP[name] = { model: model || {}, events: events || {} }; } }); return View; });
We use Kendo's nifty ability to create classes with an init
constructor and then pass in the name that will be exposed off the global APP object along with the HTML template, the model and an object holding any events we want to be able to access from the HTML. Now that the plumbing is laid,
Create a new file under the app/views/todos folder called todos.js and place the following code within:
define([ 'views/view', 'text!views/todos/todos.html' ], function (View, html) { var view, navbar, category; var todos = new kendo.data.DataSource({ data: [ { title: 'Talk to corporate', category: 'Work' }, { title: 'Promote synergy', category: 'Work' }, { title: 'Eat a bagel', category: 'Personal' }, { title: 'Eat some chicken strips', category: 'Personal' } ] }); var model = kendo.observable({ todos: todos }); var events = { init: function (e) { // store a reference to the navbar component in this view navbar = e.view.header.find('.km-navbar').data('kendoMobileNavBar'); }, afterShow: function (e) { // pull the current category off the parameters object category = e.view.params.category || 'Work'; // filter the data source against the current category todos.filter({ field: 'category', operator: 'eq', value: category }); // update the navbar title navbar.title(category); } }; // create a new view view = new View('todos', html, model, events); // subscribe to the /newTodo/add message $.subscribe('/newTodo/add', function (e, text) { todos.add({ title: text, category: category }); }); });
In this file a collection of todos
is defined. In this example it's static, but you would most likely wire your datasource up to a remote service. The model object is pretty simple since it just exposes our todos
collection. The events object is interesting since it uses a very important strategy that will be critical to your Kendo UI Mobile development. The $.subscribe
method there at the end is part of our tiny PubSub library and that function will be called whenever the /newTodo/add
event is fired. We'll get to that when we build the newTodo view.
The Kendo UI Mobile Application object is very tightly coupled with the idea of declarative initialization. That means that you can't actually get a reference to things like the view itself or many of it's components as they do not exist until the application object processes them. When is that? It's technically when the init
event is fired by the application object. However, there is an easier and more localized way to get the references you need and that's by using an init
event in the view. When the view init
method fires, you can get references to all of the Kendo UI Mobile widgets that it contains. In this example, I am storing a reference to the NavBar widget so that I can set it's title in the afterShow
method.
Add a new file to the app/views/todos folder called todos.html. Add the following markup to that file:
<div data-role="view" id="todos" data-model="APP.todos.model" data-init="APP.todos.events.init" data-after-show="APP.todos.events.afterShow"> <header data-role="header"> <div data-role="navbar"> <a data-role="button" data-rel="drawer" href="#categories" data-icon="drawer-button" data-align="left"></a> <span data-role="view-title"></span> <a data-role="button" data-rel="modalview" href="#newTodo" data-align="right" data-icon="compose"></a> </div> </header> <ul data-role="listview" data-bind="source: todos" data-template="todos-template"></ul> </div> <script type="text/x-kendo-template" id="todos-template"> #: title # </script>
This file is loaded by the RequireJS text plugin when text!views/todos/todos.html
is placed in the define
method at the top of the todos.js file. You can examine the markup to see where events are being bound and what items within the view are bound to the object declared at the view level in data-model
.
Let's add in the Drawer component which will contain the list of categories that we want to filter on. Add a new file to the app/views/categories folder called categories.js. Place the following code inside:
define([ 'views/view', 'text!views/categories/categories.html' ], function (View, html) { var categories = new kendo.data.DataSource({ data: [ { name: 'Work' }, { name: 'Personal' }, { name: 'Other' } ] }); var model = { categories: categories }; // create a new view var view = new View('categories', html, model); // subscribe to the add event from newCategory $.subscribe('/newCategory/add', function (e, text) { categories.add({ name: text }); }); });
There are no new surprises here, so add the corresponding template file in app/views/categories called categories.html. Give it the following markup:
<div data-role="drawer" id="categories" style="width: 270px" data-model="APP.categories.model"> <div class="km-group-title">Categories</div> <ul data-role="listview" data-bind="source: categories" data-template="categories-template"></ul> <a data-role="button" data-icon="compose" class="full" data-rel="modalview" href="#newCategory">New</a> </div> <script type="text/x-kendo-template" id="categories-template"> <a href="todos?category=#: name #">#: name #</a> </script>
Notice from the markup that this is not a Kendo UI Mobile View, but rather a Drawer. As I mentioned before, all of these layout containers derive from the Kendo UI Mobile view so they all have events and will take a model that you can bind to. That means we can work will all of these container objects in much the same way.
So that we can add new todo items to the todo list, create a new file in app/views/ called newTodo.js which will be the backing code for a Kendo UI Mobile ModalView in the same folder called newTodo.html
define([ 'views/view', 'text!views/newTodo/newTodo.html' ], function (View, html) { var view, modalView; var model = kendo.observable({ text: null, add: function (e) { $.publish('/newTodo/add', [ this.get('text') ]); modalView.close(); }, close: function (e) { modalView.close(); } }); var events = { init: function (e) { modalView = e.sender; } }; view = new View('newTodo', html, model, events); });
<div data-role="modalview" id="newTodo" data-model="APP.newTodo.model" data-init="APP.newTodo.events.init" style="display: none" data-width="80%"> <div data-role="header"> <div data-role="navbar"> <span data-role="view-title">New Todo</span> <a data-role="button" data-bind="click: close" data-align="right" data-icon="close"></a> </div> </div> <ul data-role="listview" data-style="inset"> <li> Todo: <textarea name="newTodo" id="newTodo" data-bind="value: text"></textarea> </li> </ul> <a data-bind="click: add" data-role="button" class="full">Add</a> </div>
Note that the ModalView has a set width of 80%. If you don't set the width with data-width
, the Modal will show up at the bottom of the screen on the mobile device and you will have to scroll down to see it. For a list of other Kendo UI Mobile Tips, check out Jeff Valore's excellent article on the subject.
By now, you are hopefully getting used to the pattern we are following here and this is starting to all look very familiar. Lastly you just need to add a folder called app/views/newCategory with newCategory.js and ____newCategory.html__ files.
define([ 'views/view', 'text!views/newCategory/newCategory.html' ], function (View, html) { var view, modalView; var model = kendo.observable({ text: null, close: function (e) { modalView.close(); }, add: function (e) { $.publish('/newCategory/add', [ this.get('text') ]); modalView.close(); } }); var events = { init: function (e) { modalView = e.sender; } }; return new View('newCategory', html, model, events); });
<div data-role="modalview" id="newCategory" data-model="APP.newCategory.model" data-init="APP.newCategory.events.init" data-width="80%"> <div data-role="header"> <div data-role="navbar"> <span data-role="view-title">New Category</span> <a data-role="button" data-bind="click: close" data-align="right" data-icon="close"></a> </div> </div> <ul data-role="listview" data-style="inset"> <li> Name: <input type="text" data-bind="value: text" > </li> </ul> <a data-bind="click: add" data-role="button" class="full">Add</a> </div>
The last item we need to touch on are the $.publish
and $.subscribe
methods. When you separate your code into modules like we have done here, the modules will need to communicate with each other. In this example, the newTodo and newCategory views need to be able to send the input from the user back to the todo and category views so the items can be added to the DataSource in those views. It's tempting to expose a method on those views - something like addTodo
on the todo.js file and then put it in the define
block on the newTodo module. This creates a tight coupling between those two modules and also forces you to have inconsistent API's between your different modules. In order to avoid this, we use the PubSub pattern which allows us to publish a message from any module and then respond to that message from any other module while passing data with the message.
PubSub is really a simple concept. You can expand on it with very powerful libraries such as Postal, but I find that for most of my needs, the basic PubSub model works just fine. In this application, I'm using the jQuery Tiny PubSub library from Ben Alman. Here is how it works: in the todod.js file we subscribe to a /newTodo/add message. That's just a string. We could have called it anything, but we typically format messages like URLs because URLs are really quite nice to work with.
// subscribe to the /newTodo/add message $.subscribe('/newTodo/add', function (e, text) { todos.add({ title: text, category: category }); });
The first parameter is always the jQuery event, and any parameters after that are objects sent along with the message. The /newTodo/add message is published from the newTodo.js file on the click
event in the model.
$.publish('/newTodo/add', [ this.get('text') ]);
Lastly lets add in a some simple styles to add a few extra icons and tweak our buttons and inputs just a little bit so that they take up all of the available space in the sidebar and modals. Add a new stylesheet to your app and put the following styles inside. I've called mine main.css and put it in the styles folder at the application root.
.full { display: block; margin: .6em .8em; font-size: 1.2em; text-align: center; } textarea { resize: none; } /* Custom Icons */ .km-close:after, .km-close:before { content: "\e038"; } .km-drawer-button:after, .km-drawer-button:before { content: "\E077"; }
Since I'm using AppBuilder, I can package all of this up now and test it in the companion application.
> appbuilder build ios --companion
I can also just deploy it straight to my device if it's connected.
> appbuilder deploy ios
That's the name of the game when it comes to structuring your Kendo UI Mobile hybrid applications. Keep your functionality as modular as possible. To recap:
The guidelines will help keep you on the right path to a mobile application that now only looks amazing thanks to Kendo UI, but is also structured so that you can grow it and maintain it for a long time to come.
Burke Holland is a web developer living in Nashville, TN and was the Director of Developer Relations at Progress. He enjoys working with and meeting developers who are building mobile apps with jQuery / HTML5 and loves to hack on social API's. Burke worked for Progress as a Developer Advocate focusing on Kendo UI.