Extending Mojito¶
Introduction¶
The Mojito framework lets you add features and extend functionality through addons, libraries, and middleware. This chapter discusses how to create extensions and where to place files in the Mojito framework.
Addons¶
In addition to the Action Context addons that Mojito provides, you can create your own addons to add functionality to controller actions.
Addons allows you to do the following:
- wrap third-party Node.js libraries and YUI libraries
- inspect the content of the ActionContext object
- call methods on the ActionContext object, which can be references through this.get('host')
Creating New Addons¶
An addon is simply a JavaScript file that contains a YUI module. You can create addons at the application and mojit level. Application-level addons are available to all mojits in the application, whereas, mojit-level addons are only available to its mojit.
Naming Convention¶
The name of an addon should have the following syntax where {addon_namespace} is the namespace of the addon and {affinity} is server, common, or client.
{addon_namespace}.{affinity}.js
Location of Addons¶
Application-level addons should be placed in the following directory:
{app_dir}/addons/ac/
Mojit-level addons should be placed in the following directory:
{mojit_dir}/addons/ac/
Namespace of Addon¶
To access addon methods and properties, you use the Action Context object (i.e., ac) and the addon namespace. For example, the namespace of the addon addons/ac/foo.common.js would be foo. Thus, to call the method save of the foo addon, you would use ac.foo.save.
See Using Your Addon for an example of how to call methods from the addon namespace.
Writing the Addon¶
The ActionContext object and ActionContext addons simulate the YUI Base and YUI Plugin infrastructure, so creating a new addon is similar to creating a new YUI Plugin.
The addon must do the following:
- register a plugin name, which is the string passed to YUI.add
- constructor with a prototype property
- assign the constructor to a namespace of Y.mojito.addons.ac, so Mojito can access your addon
- require the ‘mojito’ module, otherwise the namespace Y.mojito.addons.ac might not be available (unless you use Y.namespace()).
Optional: requires array to include other modules. The code snippet below shows the skeleton of an addon with the registered plugin name ('addon-ac-cheese'), the constructor (CheeseAcAddon) with its prototype property, and requires the mojito module.
// Register the plugin name (must be unique)
YUI.add('addon-ac-cheese', function(Y, NAME) {
// Constructor for addon
function CheeseAcAddon(command, adapter, ac) {
// The "command" is the Mojito internal details
// for the dispatch of the mojit instance.
// The "adapter" is the output adapter, containing
// the "done()", "error()", etc, methods.
// The "ac" is the ActionContext object to which
// this addon is about to be attached.
}
// Use the prototype property to add
// methods and other properties
CheeseAcAddon.prototype = {
// Put methods and properties here
}
// Assign the constructor of the addon to a
// namespace of Y.mojito.addons.ac
Y.mojito.addons.ac.cheese = CheeseAcAddon;
// Optional: 'requires' array could include other
// YUI modules if needed.
}, '0.0.1', {requires: ['mojito']});
Example Addon¶
In the example addon cheese.common.js below, the YUI.add method registers the addon-ac-cheese plugin. The addon namespace is cheese, and the addon has the one method cheesify, which is added through the prototype property.
YUI.add('addon-ac-cheese', function(Y, NAME) {
function CheeseAcAddon(command, adapter, ac) {
// The "command" is the Mojito internal details
// for the dispatch of the mojit instance.
// The "adapter" is the output adapter, containing
// the "done()", "error()", etc, methods.
// The "ac" is the ActionContext object to which
// this addon is about to be attached.
}
CheeseAcAddon.prototype = {
// The "namespace" is where in the ActionContext
// the user can find this addon. The namespace
// must be the same as the first part of the addon file.
// Thus, this addon file must be named 'cheese'.{affinity}.js'
namespace: 'cheese',
cheesify: function(obj) {
var n;
if (Y.Lang.isString(obj)) {
return 'cheesy ' + obj;
}
for (n in obj) {
if (obj.hasOwnProperty(n)) {
obj[n] = this.cheesify(obj[n]);
}
}
return obj;
}
};
// If this addon depends on another, that can
// be specified here. Circular dependencies are not
// supported or automatically detected,
// so please be careful.
CheeseAcAddon.dependsOn = ['http'];
Y.mojito.addons.ac.cheese = CheeseAcAddon;
}, '0.0.1', {requires: ['mojito']});
Using Your Addon¶
The addon in Example Addon registered the plugin addon-ac-cheese and made its constructor available through the namespace cheese. This namespace must match the first part of the addon file name, so the addon file name is cheese.common.js.
Addons are not automatically added to the ActionContext, so to access an addon, your controller needs to add the YUI plugin name to the requires array. The YUI plugin name is the string passed to YUI.add in the addon. To invoke the addon methods, you use the Action Context object (ac) with the addon namespace: ac.cheese.{addon_method}
YUI.add('foo', function(Y, NAME) {
Y.namespace('mojito.controllers')[NAME] = {
index: function(ac) {
// Use the namespace defined by the
// addon file name ('cheese') with
// the addon function 'cheesify'
var cheesy = ac.cheese.cheesify({
food: "nachos",
things: "jokes"
});
}
};
// To use your addon, add 'addon-ac-cheese' to your
// 'requires' array.
}, '0.0.1', {requires: [ 'mojito', 'addon-ac-cheese']});
Middleware¶
Introduction¶
Middleware is code that can handle (or modify) the HTTP request in the server. As of Mojito v0.9, you no longer configure the middleware directory in application.json and instead use app.js to include middleware, by either directly writing the middleware code into the file or including it.
Because Mojito middleware is based on the HTTP middleware Connect, the code must follow the Connect API. Next, we’ll cover the available Mojito middleware and show examples how to include the Mojito middleware, select specific Mojito middleware modules, and write your write your own custom middleware.
Configuring Middleware¶
Mojito ships with a default list of middleware to facilitate the use of the Mojito dispatcher. These middleware are responsible for augmenting the request object for dispatcher to function, or just simply giving access to the RPC tunnel or static assets. Here is the default list in order:
[
'mojito-handler-static',
'mojito-parser-body',
'mojito-parser-cookies',
'mojito-contextualizer',
'mojito-handler-tunnel'
]
Mojito also provides a sugar method to use them in your Express application through the method app.use, just like any traditional Connect middleware. This is the recommended way of using the default Mojito middleware:
var express = require('express'),
libmojito = require('mojito'),
app;
app = express();
libmojito.extend(app);
app.use(libmojito.middleware());
By calling libmojito.middleware, your Express application will execute the default Mojito middleware in the correct order.
Example¶
If the default list of mojito middleware does not fit your needs, you can individually select middleware to provide a custom order and a custom organization. The following is an example of the mix-in between the default Mojito middleware and application specific middleware:
var express = require('express'),
libmojito = require('mojito'),
app;
app = express();
libmojito.extend(app);
app.use(function (req, res, next) {
// inline middleware to intercept an HTTP request and lowercases
// URLs containing the string "module_"
if (req.url.indexOf('module_') > -1) {
req.url = req.url.toLowerCase();
}
next();
});
app.use(libmojito.middleware['mojito-handler-static']());
app.use(libmojito.middleware['mojito-parser-body']());
app.use(libmojito.middleware['mojito-parser-cookies']());
app.use(libmojito.middleware['mojito-contextualizer']());
app.use(customContextualizerMiddleware());
app.use(libmojito.middleware['mojito-handler-tunnel']());
app.use(anotherCustomMiddleware());
Libraries¶
Mojito allows you to use YUI libraries, external libraries, or customized libraries. To use any library in Mojito, you need to specify the module in either the requires array in the controller for YUI libraries or by using the require method for Node.js modules.
YUI Library¶
YUI libraries can be made available at the application or the mojit level. Each file can only have one YUI.add statement. Other components, such as controllers, models, etc., needing the library should specify the YUI module name in the requires array.
File Naming Convention¶
The file name of a YUI module should have the following syntax where {yui_mod_name} is a unique YUI module name defined by the user and {affinity} is server, common, or client.
{yui_mod_name}.{affinity}.js
Location of YUI Modules¶
Application-level YUI modules should be placed in the following directory:
{app_dir}/yui_modules/
Mojit-level YUI modules should be placed in the following directory:
{mojit_dir}/yui_modules/
Using the YUI Module¶
In the sections below, we provide an example YUI module and then show how to require and use that YUI module from a controller.
hello-uid Module¶
In the code example below, the create_id function becomes the constructor for the UID namespace. This will let you create an instance, and the prototype object then allows you to access the method log from that instance.
{mojit_dir}/yui_modules/hello-uid.server.js
YUI.add('hello-uid', function(Y){
function create_id(){
var uid = Math.floor(Math.random()*10000000);
this.uid = uid;
}
create_id.prototype = {
log: function(user_name){
Y.log(user_name + "'s UID is " + '['+this.uid+']');
}
};
Y.namespace('mojito').UID = create_id;
});
Using the hello-uid Module¶
In the example mojit controller below, the YUI module hello-uid is loaded because the module is in the requires array. The log method can then be called from an instance (i.e., uid) of the module.
{mojit_dir}/controller.server.js
YUI.add('hello', function(Y, NAME) {
Y.namespace('mojito.controllers')[NAME] = {
index: function(ac) {
var user_name = ac.params.getFromMerged("name") || "User";
var uid = new Y.mojito.UID();
uid.log(user_name);
ac.done('Hello World!');
}
};
}, '0.0.1', {requires: ['hello-uid']});
Other Libraries¶
Non-YUI libraries can also be used at either the application or mojit level. Because Node.js and not Mojito will read the contents of the library files, you need to use the function require to include the library. Mojito will only confirm that the files exist.
Location of Non-YUI Libraries¶
Application-level libraries should be placed in the following directory:
{app_dir}/libs/
Mojit-level libraries should be placed in the following directory:
{mojit_dir}/libs
View Engines¶
Overview¶
A view engine is the piece of code that takes the data returned by a controller and applies it to a view. This is most often done by interpreting the view as a template. View engines in Mojito can be at either the application or mojit level. Application-level view engines are available to all mojits.
The view engine consists of an addon that we will refer to as the view engine addon to differentiate it from other addons. The view engine addon can include code that renders templates or use a rendering engine, such as `Embedded JavaScript (EJS) http://embeddedjs.com/>`_, to render templates. In the latter case, the view engine addon acts as an interface between the Mojito framework and the rendering engine.
In the following sections, we will be discussing how to create a view engine addon that relies on a rendering engine, not how to write code that renders templates.
Terminology¶
The following list may help clarify the meaning of commonly used terms in this section.
- view engine - The code used to apply data to a view. In Mojito, the view engine consists of a view engine addon.
- view engine addon - The Mojito addon that compiles and renders templates. The addon typically relies on a rendering engine to compile and render templates, but may include code to do the compilation and rendering.
- rendering engine - The rendering engine is typically off-the-shelf technology, such as Dust, Jade, or EJS, that renders the template into markup for an HTML page.
- template - The template file (chosen by the controller) that contains tags and HTML that is rendered into markup for an HTML page.
General Steps for Creating View Engines¶
- Use npm to install the rendering engine into your Mojito application or copy it into a directory such as {app_dir}/libs.
- Create a view engine addon that references the rendering engine with a require statement and meets the requirements of the view engine addon.
- Create templates using the templates for the rendering engine and place them in {mojit_dir}/views.
File Naming Conventions¶
View Engine Addon¶
The name of the addon should have the following syntax where {view_engine_name} is the view engine and {affinity} is server, common, or client.
{view_engine_name}.{affinity}.js
Template¶
The name of the template should have the following syntax where {view_engine_name} should be the same as the {view_engine_name} in the file name of the view engine addon.
{action}.{view_engine_name}.html
File Locations¶
Application-Level View Engine Addons¶
{app_dir}/addons/view-engines
Mojit-Level View Engine Addons¶
{mojit_dir}/addons/view-engines
Rendering Engines¶
Mojito does not require rendering engines to be in a specific location. The recommended practice is to use npm to install rendering engines into the node_modules directory or copy the rendering engine into the libs directory as shown below:
{app_dir}/node_modules/{rendering_engine}
{app_dir}/libs/{rendering_engine}
{mojit_dir}/libs/{rendering_engine}}
Note
If you are using mojit-level view engine addons, the rendering engine should be at the mojit level as well, such as {mojit_dir}/libs/{rendering_engine}.
Requirements of the View Engine Addon¶
The view engine addon must have the following:
a YUI.add statement to register the addon. For example:
YUI.add('addons-viewengine-ejs', function(Y, NAME) { // The addon name 'addons-viewengine-ejs' is registered by YUI.add }, '0.1.0', {requires: []});
an object that is assigned to Y.mojito.addons.viewEngines.{view_engine_name} as seen below:
... function EjsAdapter(viewId) { this.viewId = viewId; } ... Y.namespace('mojito.addons.viewEngines').ejs = EjsAdapter; ...
a prototype of the object has the following two methods render and compiler as shown below:
... EjsAdapter.prototype = { render: function(data, mojitType, tmpl, adapter, meta, more) { ... }, compiler: function(tmpl) { ... } ...
Methods for the View Engine Addon¶
render¶
Description¶
Sends a rendered template as the first argument to the methods adapter.flush or adapter.done.
Signature¶
render(data, mojitType, tmpl, adapter, meta, more)
Parameters¶
- data (Object) - the data to render.
- mojitType (String) - the mojit whose view is being rendered.
- tmpl - (String) - path to template to render.
- adapter (Object) - the output adapter to use.
- meta (Object) - the metadata that should be passed as the second argument to adapter.flush or adapter.done
- more (Boolean) - if true, the addon should call the method adapter.flush, and if false, call the method adapter.done.
Return¶
None
View Engine Addon and Its View¶
A naming convention associates a view engine and its templates. For example, the view engine {mojit_dir}/addons/view-engines/big_engine.server.js will be used to render the template {mojit_dir}/views/foo.big_engine.html. Having two templates that only differ by the view engine will cause an error because Mojito will not be able to decide which view engine to use (which to prioritize above the other) to render the template.
Example¶
Embedded JavaScript (EJS)¶
The following example is of the EJS view engine.
EJS Rendering Engine¶
You install ejs locally with npm so that the EJS rendering engine is installed in the node_modules directory as seen below:
{app_dir}/node_modules
└── ejs
├── History.md
├── Makefile
├── Readme.md
├── benchmark.js
├── ejs.js
├── ejs.min.js
├── examples
├── index.js
├── lib
├── package.json
├── support
└── test
View Engine Addon¶
{app_dir}/addons/view-engines/ejs.server.js
YUI.add('addons-viewengine-ejs', function(Y, NAME) {
var ejs = require('ejs'),
fs = require('fs');
function EjsAdapter(viewId) {
this.viewId = viewId;
}
EjsAdapter.prototype = {
render: function(data, mojitType, tmpl, adapter, meta, more) {
var me = this,
handleRender = function(output) {
output.addListener('data', function(c) {
adapter.flush(c, meta);
});
output.addListener('end', function() {
if (!more) {
adapter.done('', meta);
}
});
};
Y.log('Rendering template "' + tmpl + '"', 'mojito', NAME);
var result = ejs.render(this.compiler(tmpl),data);
adapter.done(result,meta);
},
compiler: function(tmpl) {
return fs.readFileSync(tmpl, 'utf8');
}
};
Y.namespace('mojito.addons.viewEngines').ejs = EjsAdapter;
}, '0.1.0', {requires: []});
Template¶
{app_dir}/mojits/{mojit_name}/views/foo.ejs.html
<h2> <%= title %></h2>
<div id=<%= mojit_view_id %>>
<h3><%= ul.title %></h3>
<ul>
<% for(var i=0;i<view_engines.length;i++){ %>
<li><%= view_engines[i] %></li>
<% } %>
</ul>
</div>