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.
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:
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.
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
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/
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.
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:
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']});
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']});
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 is code that can handle (or modify) the HTTP request in the server. Because Mojito middleware is based on the HTTP middleware Connect, the code must follow the Connect API. Also, because each piece of middleware is a Node.js module, it should use module.exports to create a function to handle incoming requests.
To use middleware, the path to its code must be listed in the middleware array in application.json. The path can be marked as relative to the application by prefixing it with ”./”.
[
{
"settings": [ "master" ],
"middleware": [
"./middleware/static.js"
],
"specs": {
"hello": {
"type": "HelloMojit"
}
}
}
]
We suggest that middleware be located in the directory {app_dir}/middleware/, but this is only a convention and not required. The name of the file is not important.
The simple example below of middleware intercepts an HTTP request and lowercases URLs containing the string “module_” before the URLs are received by the server.
module.exports = function (req, res, next) {
if (req.url.indexOf('module_') > -1) {
req.url = req.url.toLowerCase();
}
next();
};
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 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.
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
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/
In the sections below, we provide an example YUI module and then show how to require and use that YUI module from a controller.
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;
});
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('HelloMojit', 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']});
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.
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
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.
The following list may help clarify the meaning of commonly used terms in this section.
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
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
{app_dir}/addons/view-engines
{mojit_dir}/addons/view-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}.
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) {
...
}
...
Sends a rendered template as the first argument to the methods adapter.flush or adapter.done.
render(data, mojitType, tmpl, adapter, meta, more)
None
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.
The following example is of the EJS view 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
{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: []});
{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>