Kudu is a micro MVC framework centered around AMD modules and Ractive templates.
kudu provides a router for mapping URLs to controllers. Controllers are essentially AMD modules with a well defined life-cycle consisting of an "initialization" phase, "rendering" phase and finally a "remove" phase. AMD modules can partake in these phases by implementing the appropriate method such as "onInit", "onRender", "onRemove" etc.
Each controller has an associated View and Model. The View is a Ractive instance that binds to the Model. The Model is a plain javascript object generally fetched as data from the server.
Ractive is a ViewModel implementation which binds an HTML template to a data object. Ractive uses mustache based html templates and binds them to Javascript data objects.
For example, given an html template, index.html:
<!doctype html>
<html lang='en-GB'>
<head>
<meta charset='utf-8'>
<title>Ractive test</title>
</head>
<body>
<h1>Ractive test</h1>
<!--
1. This is the element we'll render our Ractive to.
-->
<div id='container'></div>
<!--
2. You can load a template in many ways. For convenience, we'll include it in
a script tag so that we don't need to mess around with AJAX or multiline strings.
Note that we've set the type attribute to 'text/ractive' - though it can be
just about anything except 'text/javascript'
-->
<script id='template' type='text/ractive'>
<p>Hello, {{name}}!</p>
</script>
and this script:
var ractive = new Ractive({
// The `el` option can be a node, an ID, or a CSS selector.
el: '#container',
// We could pass in a string, but for the sake of convenience
// we're passing the ID of the <script> tag above.
template: '#template',
// Here, we're passing in some initial data
data: { name: 'world' }
});
Running this in a browser will replace the {{name}} mustache with the name variable, world. Changing the name variable will also update the template eg. In Ractive this can be done with:
ractive.set('name', 'Steve');
and the template will change from Hello world to Hello Steve.
Checkout the Ractive site for comprehensive documentation.
Ractive is a library, not a framework, it does not ship with a router, or specify how to structure your code. Ractive is basically a way to 'componentize' your html pages, without worrying about navigating between views.
This is where Kudu fits in. Kudu provides the 'C' in MVC. Kudu also provides a router and a way to navigate between views.
A basic setup kudu, mapping URLs to Controllers.
setup.js
// Import some controllers
var homeCtrl = require("homeCtrl");
var personCtrl = require("personCtrl");
// Specify the routes
var routes = {
home: {path: 'home', ctrl: homeCtrl}
person: {path: 'person', ctrl: personCtrl}
};
// Initialize kudu with the given routes and a target id (#someId) where the views will
// be rendered to
kudu.init({
target: "#container",
routes: routes
});
Below is a basic index.html template where our single page app (SPA) views will be rendered to.
_index.html
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Basic Kudu Demo</title>
<link rel="stylesheet" type="text/css" href="css/site.css" />
</head>
<body>
<!-- View will be rendered to the div with id=container -->
<div id="container"></div>
</body>
<script data-main="js/app/config/config" src="js/lib/require.js"></script>
</html>
The Home Controller is shown next. Controllers must implement an onInit method that returns a Ractive instance. In the createView function below we create a Ractive instance and pass in the hello variable.
homeCtrl.js
define(function (require) {
var template = require("rvc!./home"); // Import the home template
function home() {
var that = {};
// Implement the required onInit method that returns a Ractive ViewModel
that.onInit = function (options) {
var view = createView();
return view;
};
function createView() {
var view = new template({
data: { // We pass a data object with the hello variable to the Ractive template
hello: "Hello World!"
}
});
return view;
}
return that;
}
return home;
});
The home template contains a mustache, {{hello}}, which prints out the hello variable we passed to the Ractive instance above.
_home.html
<div class="content">
<div class="row">
<div class="col-md-12">
<h1>{{hello}}</h1>
</div>
</div>
</div>
Create a web project with the following structure:
web
src
index.jsp
css
js
app
lib
require.js
ractive.js
kudu
kudu files
config.js
The kudu module is a facade for the framework and most interaction with the framework is handled by the kudu module.
There is one kudu instance per application.
A new kudu instance is initialized by passing in a "target", an element ID where views will be rendered, and a "routes" object which specifies which URLs map to which Controllers.
Here is an example setup.js file showing how to setup and create a kudu instance:
var kudu = require("kudu");
// Import some controllers
var homeCtrl = require("homeCtrl");
var personCtrl = require("personCtrl");
var notFoundCtrl = require("notFoundCtrl");
// Specify the routes
var routes = {
home: {path: 'home', ctrl: homeCtrl}
person: {path: 'person', ctrl: personCtrl}
notFound: {path: '*', ctrl: notFound}
};
// Initialize kudu with the given routes and a target id (#someId) where the views will
// be rendered to
kudu.init({
target: "#container",
routes: routes
});
Kudu.init() accepts the following options:
options = {
target: // a CSS id selector specifying the DOM node where Views will be
// rendered to, eg. "#container",
routes: // an object mapping URLs to Controller modules,
defaultRoute: // the default route to load if no path is specified eg.
// http://host/ instead of http://host/#home
unknownRouteResolver: // a function that is called if none of the registered
// routes matches the URL
intro: // a function for performing animations when showing the View
outro: // a function for performing animations when removing the View
fx: // Specify weather effects and animations should be enabled or not, default
// is false
viewFactory: // Provides a hook for creating views other than Ractive
// instances. See ViewFactory section below
debug: // specify debug mode, true or false. Default is true
};
An optional function passed a kudu instance that is called if none of the registered routes matches the URL. This allows users to inject custom logic to handle this situation.
The function must return a promise which resolves to a route. If the promise is rejected the "notFound" route will be used.
A ViewFactory provides global hooks for creating, rendering and unrendering views from the DOM.
A custom ViewFactory must provide three functions:
var CustomFactory = {
createView: function(options) {
return promise;
}
renderView: function(options) {
return promise;
}
unrenderView: function(options) {
return promise;
}
}
The above function accepts options consisting of the following:
var options = {
args: // an object that was passed to a new view from the current view
ctrl: // the controller to create
mvc: // the current view/controller instance
route: // the route object to create a new view for
routeParams: // the URL parameters
target: // the CSS selector where the view must be rendered to
viewOrPromise: // an object that was returned from the controller onInit() function,
// either a view or a promise that resolves to a view
};
createView must return a promise that resolves to the new view instance. This view instance will be passed to the renderView function
renderView must return a promise that resolves once the view has been rendered to the DOM.
unrenderView must return a promise that resolves once the view has been removed from the DOM.
These functions provides global hooks for performing animation when showing and hiding views. These functions will be invoked whenever a view is rendered and unrendered. Note: you can also provide fune grained animations on a per view basis by providing enter/leave functions in the route object.
intro is called after the view is rendered to the DOM. Here you can provide animations on the view eg. fade the view in
outro is called before the view is removed from the DOM. Here you can provide animations on the view eg. fade the view out
The functions have the following format:
var intro = function (options, done) {
}
options contain the following value:
var options: {
target: // the CSS selector where the view was rendered to,
};
The done argument is a function to be called once the animation is finished to let kudu know the view is complete.
Kudu includes a router that maps url paths to controllers.
The application routes are specified as an object with key/value pairs where each key is the name of the route and the value is the route itself, which consists of a url path and controller.
For example:
var routes = {
home: {path: '/home', ctrl: customer},
customer: {path: '/customer', ctrl: customer},
notFound: {path: '*', ctrl: notFound} // if none of the routes match the url,
// the route defined as, '*', will match and it's controller
// instantiated.
};
// Pass the routes to kudu
kudu.init({
target: "#container",
routes: routes;
});
The following route mappings are supported:
-
Segment parameters are specified as a colon with a name eg: /person/:id The following url will match this route: /person/1
-
Query parameters are specified as ampersand separated values after the questionmark eg: /person?id&name. The following url will match this route: /person?id=1&name=bob
-
Wildcards are specified as an asterisk eg: /view/*/person. The following url will match this route: /view/anything/person
Routes consist of the following options:
{
path: // this is the url path to match
ctrl: // if the path matches a url, this controller will be instantiated,
// alternatively specify the 'moduleId' option for lazy loading of the
moduleId: // controller if the path matches a url, the controller with this ID
// will be instantiated, alternatively specify the 'ctrl' option for
enter: // eager loading of the controller a function for manually adding the
// view to the DOM and to perform custom intro animations. By default
leave: // kudu insert views into the default target a function for manually
// removing the view from the DOM and to perform custom outro
// animations. By default kudu remove views from the default target
}
New routes can also be added to router through router.addRoute().
var router = require("kudu/router/router");
router.addRoute(
path: "/path", {
ctrl: HomeCtrl
});
When navigating between views, Kudu will remove the current view from the DOM and then add the new view to the DOM. If kudu is created with the fx option set to true, Kudu will animate the transition between views, by fading out the current view, remove it from the DOM, add the new view to the DOM, and finally fading in the new view.
You can customize this behaviour through a custom ViewFactory implementation and intro, outro functions that is passed to the new Kudu instance.
For finer grained control over the creation of views, you can provide an enter function on a per route basis. When providing an enter function, Kudu will delegate the rendering and animation of the view to that function.
The enter function can return a Promise instance in order to perform animations on the view. Kudu will wait until the promise resolves before continuing with other work.
Example enter function:
var route: {
path: "/home",
ctrl: HomeController,
enter: function(options) {
var d = $.deferred();
options.view.render(options.target); // Append view to DOM
$(options.target).slidDown(function() { // Use jQuery to slide the view down
d.resolve(); // Resolve the promise to notify Kudu that the view is complete
});
return d.promise();
}
}
Enter accepts the following options:
var options = {
ctrl: // the new controller instance
prevCtrl: // the previous controller instance
view: // the new view instance
prevView: // the previous view instance
route: // the new route instance
prevRoute: // the previous route instance
target: // the default DOM target as passed to the Kudu instance
};
Similar to the enter function _ leave provides finer grained control to remove the view from the DOM and animate it.
When providing a leave function, Kudu will delegate the unrendering and animation of the view to that function.
The leave function can return a Promise instance in order to perform animations on the view. Kudu will wait until the promise resolves before continuing with other work.
Example leave function:
var route: {
path: "/home",
ctrl: HomeController,
leave: function(options) {
var d = $.deferred();
$(options.target).slidUp(function() { // Use jQuery to slide the view up
options.view.unrender(options.target); // Remove view from the DOM
d.resolve(); // Resolve the promise to notify Kudu that the view is complete
});
return d.promise();
}
}
Leave accepts the following options:
var options = {
ctrl: // the new controller instance
prevCtrl: // the previous controller instance
view: // the new view instance
prevView: // the previous view instance
route: // the new route instance
prevRoute: // the previous route instance
target: // the default DOM target as passed to the Kudu instance
};
Controllers are AMD modules that must return an object which implement an onInit function. onInit must return a Ractive View instance or a Promise which resolves to a Ractive View. (If you want to implement views in an alternative technology to Ractive, eg. normal HTML, you can specify a custom ViewFactory to handle different types of views).
Example controller:
define(function (require) {
var template = require("rvc!./home");
function homeCtrl() {
var that = {}; // The object we will return from our module
that.onInit = function(options) {
var data = {hello: "Hello World"};
var view = new template( { data: data } );
return view;
}
return that;
}
return homeCtrl;
});
In the home controller above, we return an object that contains an onInit method. onInit receives an options object and must return the view.
In Kudu, views are Ractive instances, consisting of an HTML template and data. Ractive binds the HTML template and data to form the view.
onInit must return a Ractive instance (the view) or a promise which resolves to a Ractive instance.
The Ractive HTML template is imported as an AMD module through the "rvc" plugin. This plugin transforms an HTML Ractive template by compiling it to a Ractive function, ready to be instantiated.
Controllers in kudu must implement an onInit method. onInit must return a Ractive view instance or function (kudu will instantiate it if needed) or a promise which resolves to a Ractive view instance or function.
The following options are passed to the onInit method:
options = {
ajaxTracker: // provides a means of registering ajax calls in the controller. Ajax
// calls tracked this way will automatically abort when the view is
// removed. ajaxTracker also provides a way to listen to ajax lifecycle
// events such as ajax.start / ajax.stop etc.
routeParams: // all URL parameters (including segment parameters and query parameters)
// are passed to the controller through the routeParams object.
args: // arguments passed to the controller from another controller. args can
// only be passed to a view when called from a controller, not when
// navigating via the URL hash
}
Controllers can optionally implement an onRemove method. This method controls whether the view can be removed or not. onRemove must return either true or false or a promise that resolves to true or false.
If onRemove returns true, the view will be removed. If false, the request will be cancelled and the view will not be removed. This is useful in situations where a view wants to stop the user from navigating away until changes in a form has been saved, for example.
The following options are passed to the onRemove method:
options = {
ajaxTracker: // provides a means of registering ajax calls in the controller. Ajax
// calls tracked this way will automatically abort when the view is
// removed. ajaxTracker also provides a way to listen to ajax lifecycle
// events such as ajax.start / ajax.stop etc.
routeParams: // all URL parameters (including segment parameters and query parameters)
// are passed to the controller through the routeParams object.
args: // arguments passed to the controller from another controller. args can
// only be passed to a view when called from a controller, not when
// navigating via the URL hash
view: // a reference to the view that is going to be removed
}
You can subscribe to global events fired by kudu as follows:
var kudu = require("kudu/kudu");
$(kudu).on('viewInit', function (e, options) {
// called whenever a view has been initialized
});
The following global events exist:
viewBeforeInit : called before the controller.onInit method is called
viewInit : called after the controller.onInit method is called
viewRender : called after the controller's Ractive view has been added to the
DOM
viewComplete : called after the controller's Ractive view has been rendered and
completed
any transitions
viewBeforeUnrender : called before view is removed from the dom. this event only
occurs
if the Controller.onRemove method returns true
viewUnrender : called after the controller's Ractive view has been removed
from the DOM
viewFail : called when a view failed to create
The following options are passed to the events:
options = {
prevCtrl : // previous controller which is being removed
newCtrl : // new controller being added
isMainCtrl : // (experimental) true if the new controller replaces the main view eg.
// the target
// specified in kudu initialization is replaced. If false
// it means the new controller is a sub view on another controller
ctrlOptions : // all the options used for the new controller
eventName : // name of the event which fired
error : // optionally specifies the error (an array of error messages)
// which led to the event being triggered
}
The following events exist on a controller:
onInit : the initialization event which must be implemented by each controller
onRender : called after the view has been added to the DOM
onComplete : called after the view has been added to the DOM AND once all transitions
has completed.
onRemove : called before removing the controller
onUnrender : called after the view has been removed from the DOM
define(function (require) {
var template = require("rvc!./home");
function homeCtrl() {
var that = {};
that.onInit = function(options) {
// View must be returned from onInit
var view = new template();
return view;
}
that.onRender = function(options) {
// View has been added to the DOM
}
that.onComplete = function(options) {
// view has been added to the DOM and transitions completed
}
return that;
}
return homeCtrl;
});