Time Estimate: 20 minutes
Difficulty Level: Advanced
This example shows how to bind events to a mojit, configure code to run on the client, and make AJAX calls to the YQL Web service. The application listens for events and then makes AJAX calls to YQL to get Flickr photo information.
The following topics will be covered:
- configuring the application to run on the client
- getting Flickr data from the model with YQL
- binding events through the
mojitProxy
object - making AJAX calls to YQL from the binder
You will need to get a Flickr API key to run this example.
Mojito lets you configure applications to run on either the server or client side. This example uses binders that are deployed to the client, so we need to configure Mojito to deploy the application to the client, where it will be executed by the browser.
To configure Mojito to run on the client, you simply set the "deploy"
property to true
in application.json
as seen below.
[
{
"settings": [ "master" ],
"specs": {
"frame": {
"type": "HTMLFrameMojit",
"config": {
"deploy": true,
"child": {
"type": "PagerMojit"
}
}
}
}
}
]
In the mojit model, the YUI YQL Query Utility
is used to get Flickr photo information. To access the utility in your model,
specify 'yql'
in the requires
array as seen in the code snippet below:
YUI.add('PagerMojitModel', function(Y, NAME) {
...
/* Code for PagerMojitModel */
...
}, '0.0.1', {requires: ['yql']});
This code example uses the flickr.photos.search
table to get information
for photos that have a title, description, or tags containing a string. For
example, the YQL statement below returns Flickr photo information for those
photos that have a title, description, or tags containing the string "Manhattan".
Copy the query below into the YQL Console,
replace {your_flickr_api_key}
with your own Flickr API key, and then click TEST
to see the returned XML response.
select * from flickr.photos.search where text="Manhattan" and api_key="{your_flickr_api_key}"
The returned response contains photo information in the photo
element. You extract the
farm
, server
, id
, and secret
attributes from each photo element to create
the photo URI as seen here:
http://farm + {farm} + static.flickr.com/ + {server} + / + {id} + _ + {secret} + .jpg
In the model.server.js
of PagerMojit
shown below, the YQL
function uses the YQL
statement above to get photo data, then parses the returned response to create the photo
URIs. The model then wraps the photo information in an object and stores those objects in
the images
array that is sent to the controller through the callback
function.
YUI.add('PagerMojitModel', function(Y, NAME) {
/**
* The PagerMojitModel module.
* @module PagerMojitModel
*/
/**
* Constructor for the Model class.
* @class Model
* @constructor
**/
Y.namespace('mojito.models')[NAME] = {
init: function(config) {
this.config = config;
},
getData: function(query, start, count, callback) {
var q = null;
// Get Flickr API key: http://www.flickr.com/services/api/keys/apply/
var API_KEY = "{your_flickr_api_key}";
start = parseInt(start) || 0;
count = parseInt(count) || 10;
q = 'select * from flickr.photos.search(' + start + ',' + count + ') where text="%' + query + '%" and api_key="' + API_KEY + '"';
Y.YQL(q, function(rawData) {
if (!rawData.query.results) {
callback([]);
return;
}
var rawImages = rawData.query.results.photo, rawImage = null,images = [], image = null, i = 0;
for (; i<rawImages.length; i++) {
rawImage = rawImages[i];
image = {
title: rawImage.title,
location: 'http://farm' + rawImage.farm + '.static.flickr.com/' + rawImage.server + '/' + rawImage.id + '_' + rawImage.secret + '.jpg',
farm: rawImage.farm,
server: rawImage.server,
image_id: rawImage.id,
secret: rawImage.secret
};
if (!image.title) {
image.title = "Generic Title: " + query;
}
images.push(image);
}
callback(images);
});
}
};
}, '0.0.1', {requires: [ 'yql']});
For a more detailed explanation about how to use YQL in your Mojito application, see Calling YQL from a Mojit. For more information about YQL, see the YQL Guide.
This section will discuss the basics of binding events in Mojito and then look at the binder used in this code example.
A mojit may have zero, one, or many binders within the binders
directory. Each binder
will be deployed to the browser along with the rest of the mojit code, where the client-side
Mojito runtime will call it appropriately. On the client, the binder has a proxy
object (mojitProxy
) for interacting with the mojit it represents as well as with other
mojits on the page. Methods can be called from the mojitProxy
object that allow
binders to listen for and fire events.
The binder consists of a constructor, an initializer, and a bind function. The following
describes each component and indicates when the mojitProxy
object can be used.
- constructor - creates the namespace for your binder that wraps the initialization code and binder.
- initializer - is passed the
mojitProxy
where it can be stored and used to listen and fire events with other binders. ThemojitProxy
is the only gateway back into the Mojito framework for your binder. - bind - is a function that is passed a
Y.Node
instance that wraps the DOM node representing this mojit instance. The DOM event handlers for capturing user interactions should be attached in this function.
The skeleton of the binders/index.js
file below illustrates the basic structure of the
binder. For more information, see Mojito Binders.
YUI.add('AwesomeMojitBinder', function(Y, NAME) {
// Binder constructor
Y.namespace('mojito.binders')[NAME] = {
init: function(mojitProxy) {
this.mojitProxy = mojitProxy;
},
// The bind function
bind: function(node) {
var thatNode = node;
}
};
Y.mojito.registerEventBinder('AwesomeMojit', Binder);
}, '0.0.1', {requires: ['mojito']});
This code example uses the binder PageMojitBinder
to perform the following:
- attach
onClick
handlers toprev
andnext
links - invoke the
index
method of the controller through themojitProxy
object - create an overlay with Flickr photo information received from YQL
The binders/index.js
for this code example is long and fairly involved, so we will
dissect and analyze the code. Let's begin by looking at the bind
function of
index.js
, which allows mojits to attach DOM event handlers.
In this code snippet of binders/index.js
, the bind
function contains the nested
updateDOM
function that updates node content and attaches event handlers. Using the
mojitProxy
object, the nested flipper
function calls the index
function of the
controller. The callback updateDOM
is passed to index
to update the content.
...
bind: function(node) {
var thatNode = node;
// Define the action when user click on prev/next.
var flipper = function(event) {
var target = event.target;
// Get the link to the page.
var page = parsePage(target.get('href'));
var updateDOM = function(markup) {
thatNode.set('innerHTML', markup);
thatNode.all('#nav a').on('click', flipper, this);
thatNode.all('#master ul li a').on('mouseover', showOverlay, this);
thatNode.all('#master ul li a').on('mouseout', showOverlay, this);
};
this.mojitProxy.invoke('index',
{
params: {page: page},
}, updateDOM
);
};
...
The event handler for mouseovers and mouseouts are handled by the showOverlay
function,
which creates the overlay containing photo information. In the code snippet below,
showOverlay
makes an AJAX call to YQL to get photo data that is placed in an unordered
list for the overlay.
...
bind: function(node) {
...
var showOverlay = function(event) {
var target = event.target;
var href = target.get('href');
var imageId = parseImageId(href);
if (target.hasClass('overlayed')) {
target.removeClass('overlayed');
thatNode.one('#display').setContent('');
} else {
Y.log('HREF: ' + href);
Y.log('IMAGE ID: ' + imageId);
target.addClass('overlayed');
// Query for the image metadata
var query = 'select * from flickr.photos.info where photo_id="' + imageId + '" and api_key="' + {your_flickr_api_key} + '"';
thatNode.one('#display').setContent('Loading ...');
Y.YQL(query, function(raw) {
if (!raw.query.results.photo) {
Y.log('No results found for photoId: ' + imageId);
return;
}
var props = raw.query.results.photo;
var snippet = '<ul style="list-style-type: square;">';
for (var key in props) {
if (typeof(props[key]) == 'object') {
continue;
}
snippet += '<li>' + key + ': ' + props[key] + '</li>';
}
snippet += '</ul>';
thatNode.one('#display').setContent(snippet);
});
}
};
...
}
...
Thus far, we've looked at the event handlers, but not the actual binding of the handlers
to nodes. At the end of the bind
function, you'll see three important lines
(shown below) that bind the flipper
and showOutlay
functions to handle click and
mouseover events.
...
bind: function(node) {
...
// Bind all the image links to showOverlay
thatNode.all('#master ul li a').on('mouseover', showOverlay, this);
thatNode.all('#master ul li a').on('mouseout', showOverlay, this);
// Bind the prev + next links to flipper
thatNode.all('#nav a').on('click', flipper, this);
}
...
After a little analysis, the full binders/index.js
below should be easier to
understand. The binder attaches event handlers to nodes, invokes a function in the
controller, and updates the content in the template. The binder also has a couple of
helper functions for parsing and requires the IO and YQL modules, which are specified in
the requires
array.
YUI.add('PagerMojitBinder', function(Y, NAME) {
var API_KEY = '{your_flickr_api_key}';
function parseImageId(link) {
var matches = link.match(/com\/(\d+)\/(\d+)_([0-9a-z]+)\.jpg$/);
return matches[2];
}
function parsePage(link) {
var matches = link.match(/page=(\d+)/);
return matches[1];
}
/**
* The PagerMojitBinder module.
* @module PagerMojitBinder
*/
/**
* Constructor for the Binder class.
*
* @param mojitProxy {Object} The proxy to allow
* the binder to interact with its owning mojit.
* @class Binder
* @constructor
*/
Y.namespace('mojito.binders')[NAME] = {
/**
* Binder initialization method, invoked
* after all binders on the page have
* been constructed.
*/
init: function(mojitProxy) {
this.mojitProxy = mojitProxy;
},
/**
* The binder method, invoked to allow the mojit
* to attach DOM event handlers.
* @param node {Node} The DOM node to which this
* mojit is attached.
*/
bind: function(node) {
var thatNode = node;
Y.log('NODE: ' + Y.dump(this.node));
// define the action when user click on prev/next
var flipper = function(event) {
var target = event.target;
// get the link to the page
var page = parsePage(target.get('href'));
Y.log('PAGE: ' + page);
var updateDOM = function(markup) {
thatNode.set('innerHTML', markup);
thatNode.all('#nav a').on('click', flipper, this);
thatNode.all('#master ul li a').on('mouseover', showOverlay, this);
thatNode.all('#master ul li a').on('mouseout', showOverlay, this);
};
this.mojitProxy.invoke('index',
{
params: {page: page}
}, updateDOM
);
};
var showOverlay = function(event) {
var target = event.target;
var href = target.get('href');
var imageId = parseImageId(href);
if (target.hasClass('overlayed')) {
target.removeClass('overlayed');
thatNode.one('#display').setContent('');
} else {
Y.log('HREF: ' + href);
Y.log('IMAGE ID: ' + imageId);
target.addClass('overlayed');
// Query for the image metadata
var query = 'select * from flickr.photos.info where photo_id="' + imageId + '" and api_key="' + API_KEY + '"';
thatNode.one('#display').setContent('Loading ...');
Y.YQL(query, function(raw) {
if (!raw.query.results.photo) {
Y.log('No results found for photoId: ' + imageId);
return;
}
var props = raw.query.results.photo;
var snippet = '<ul style="list-style-type: square;">';
for (var key in props) {
if (typeof(props[key]) == 'object') {
continue;
}
snippet += '<li>' + key + ': ' + props[key] + '</li>';
}
snippet += '</ul>';
thatNode.one('#display').setContent(snippet);
});
}
};
// Bind all the image links to showOverlay
thatNode.all('#master ul li a').on('mouseover', showOverlay, this);
thatNode.all('#master ul li a').on('mouseout', showOverlay, this);
// Bind the prev + next links to flipper
thatNode.all('#nav a').on('click', flipper, this);
}
};
}, '0.0.1', {requires: ['yql', 'io', 'dump']});
The paging for this code example relies on the application configuration to set route paths and the controller to create links to access previous and next pages.
The routes.json
file below configures two route paths for HTTP GET calls made on the
root path. The perpage
configuration, however, requires a query string with the
page
parameter, which is used for paging. The page
parameter has the value
:page
, which is a variable that is assigned a value by the controller that we're
going to look shortly.
[
{
"settings": ["master"],
"root": {
"verbs": ["get"],
"path": "/",
"call": "frame.index"
},
"perpage": {
"verbs": ["get"],
"path": "/?page=:page",
"call": "frame.index"
}
}
]
The controller for PagerMojit
performs several functions:
- uses the
Params
addon to get thepage
parameter from the query string - calculates the index of the first photo on the page
- calls the
getData
function in the model to get photo data - creates URLs for the next and prev links
The Params addon allows you to access variables
from the query string parameters, the POST request bodies, or the routing systems URLs.
In this code example, you use the getFromMerged
method, which merges the parameters
from the query string, POST request body, and the routing system URLs to give you access
to all of the parameters. In the code snippet taken from controller.server.js
below,
the getFromMerged
method is used to get the value for the page
parameter and then
calculate the index of the first photo to display:
...
index: function(actionContext) {
var page = actionContext.params.getFromMerged('page');
var start;
page = parseInt(page) || 1;
if ((!page) || (page<1)) {
page = 1;
}
// Page param is 1 based, but the model is 0 based
start = (page - 1) * PAGE_SIZE;
...
}
...
To get the photo data, the controller depends on the model to call YQL to query the
Flickr API. Using actionContext.models.{model_name}
lets you get a reference to the
model. In this example controller, the model of the PagerMojit
is accessed through
actionContext.models.PageMojit
, allowing you to call getData
and get the returned
data from YQL in the callback function.
...
index: function(actionContext) {
...
var model = actionContext.models.PagerMojit;
// Data is an array of images
model.getData('mojito', start, PAGE_SIZE, function(data) {
Y.log('DATA: ' + Y.dump(data));
var theData = {
data: data, // images
hasLink: false,
prev: {
title: "prev" // opportunity to localize
},
next: {
link: createLink(actionContext, {page: page+1}),
title: "next"
},
query: 'mojito'
};
if (page > 1) {
theData.prev.link = createLink(actionContext, {page: page-1});
theData.hasLink = true;
}
actionContext.done(theData);
});
}
...
};
...
The URLs for the prev and next links are created by passing the mojit instance,
the method, and the query string parameters to the make
method from the Url
addon.
The code snippet below creates the query string parameters with the
YUI QueryString module.
If the query string created by Y.QueryString.stringify
is "page=2" ,
actionContext.url.make
would return the URL {domain_name}:8666/?page=2
.
...
function createLink(actionContext, params) {
var mergedParams = Y.mojito.util.copy(actionContext.params.getFromMerged());
for (var k in params) {
mergedParams[k] = params[k];
}
return actionContext.url.make('frame', 'index', Y.QueryString.stringify(mergedParams));
}
...
Stitching the above code snippets together, we have the controller.server.js
below.
The index
function relies on the model for data and the createLink
function to
create URLs for the next and prev links.
YUI.add('PagerMojit', function(Y, NAME) {
/**
* The PagerMojit module.
* @module PagerMojit */
var PAGE_SIZE = 10;
/**
* Constructor for the Controller class.
* @class Controller
* @constructor
*/
Y.namespace('mojito.controllers')[NAME] = {
index: function(actionContext) {
var page = actionContext.params.getFromMerged('page');
var start;
page = parseInt(page) || 1;
if ((!page) || (page<1)) {
page = 1;
}
// Page param is 1 based, but the model is 0 based
start = (page - 1) * PAGE_SIZE;
var model = actionContext.models.PagerMojit;
// Data is an array of images
model.getData('mojito', start, PAGE_SIZE, function(data) {
Y.log('DATA: ' + Y.dump(data));
var theData = {
data: data, // images
hasLink: false,
prev: {
title: "prev" // opportunity to localize
},
next: {
link: createLink(actionContext, {page: page+1}),
title: "next"
},
query: 'mojito'
};
if (page > 1) {
theData.prev.link = createLink(actionContext, {page: page-1});
theData.hasLink = true;
}
actionContext.done(theData);
});
}
};
// generate the link to the next page based on:
// - mojit id
// - action
// - params
function createLink(actionContext, params) {
var mergedParams = Y.mojito.util.copy(actionContext.params.getFromMerged());
for (var k in params) {
mergedParams[k] = params[k];
}
return actionContext.url.make('frame', 'index', Y.QueryString.stringify(mergedParams));
}
}, '0.0.1', {requires: ['dump', 'mojito-url-addon', 'mojito-params-addon']});
To set up and run binding_events
:
Create your application.
$ mojito create app binding_events
Change to the application directory.
Create your mojit.
$ mojito create mojit PagerMojit
To configure you application to run on the client and use
HTMLFrameMojit
, replace the code inapplication.json
with the following:[ { "settings": [ "master" ], "specs": { "frame": { "type": "HTMLFrameMojit", "config": { "deploy": true, "child": { "type": "PagerMojit" } } } } } ]
To configure routing to call the
index
action from the instance of theHTMLFrameMojit
, replace the code inroutes.json
with the following:[ { "settings": ["master"], "root": { "verbs": ["get"], "path": "/", "call": "frame.index" }, "perpage": { "verbs": ["get"], "path": "/?page=:page", "call": "frame.index" } } ]
Change to
mojits/PageMojit
.To have the controller get data from the model and create links for paging, replace the code in
controller.server.js
with the following:YUI.add('PagerMojit', function(Y, NAME) { var PAGE_SIZE = 10; /** * Constructor for the Controller class. * @class Controller * @constructor */ Y.namespace('mojito.controllers')[NAME] = { index: function(actionContext) { var page = actionContext.params.getFromMerged('page'); var start; page = parseInt(page) || 1; if ((!page) || (page<1)) { page = 1; } // Page param is 1 based, but the model is 0 based start = (page - 1) * PAGE_SIZE; var model = actionContext.models.PagerMojit; // Data is an array of images model.getData('mojito', start, PAGE_SIZE, function(data) { Y.log('DATA: ' + Y.dump(data)); var theData = { data: data, // images hasLink: false, prev: { title: "prev" // opportunity to localize }, next: { link: createLink(actionContext, {page: page+1}), title: "next" }, query: 'mojito' }; if (page > 1) { theData.prev.link = createLink(actionContext, {page: page-1}); theData.hasLink = true; } actionContext.done(theData); }); } }; // Generate the link to the next page based on: // - mojit id // - action // - params function createLink(actionContext, params) { var mergedParams = Y.mojito.util.copy(actionContext.params.getFromMerged()); for (var k in params) { mergedParams[k] = params[k]; } return actionContext.url.make('frame', 'index', Y.QueryString.stringify(mergedParams)); } }, '0.0.1', {requires: ['dump', 'mojito-url-addon', 'mojito-params-addon']});
To get Flickr photo information using YQL, create the file
models/model.server.js
with the code below. Be sure to replace the'{your_flickr_api_key}'
with your own Flickr API key.YUI.add('PagerMojitModel', function(Y, NAME) { var API_KEY = '{your_flickr_api_key}'; /** * The PagerMojitModel module. * @module PagerMojitModel */ /** * Constructor for the Model class. * @class Model * @constructor */ Y.namespace('mojito.models')[NAME] = { getData: function(query, start, count, callback) { var q = null; // Get Flickr API key: http://www.flickr.com/services/api/keys/apply/ var API_KEY = "{your_api_key}"; start = parseInt(start) || 0; count = parseInt(count) || 10; q = 'select * from flickr.photos.search(' + start + ',' + count + ') where text="%' + query + '%" and api_key="' + API_KEY+ '"'; Y.YQL(q, function(rawData) { if (!rawData.query.results) { callback([]); return; } var rawImages = rawData.query.results.photo, rawImage = null,images = [], image = null, i = 0; for (; i<rawImages.length; i++) { rawImage = rawImages[i]; image = { title: rawImage.title, location: 'http://farm' + rawImage.farm + '.static.flickr.com/' + rawImage.server + '/' + rawImage.id + '_' + rawImage.secret + '.jpg', farm: rawImage.farm, server: rawImage.server, image_id: rawImage.id, secret: rawImage.secret }; if (!image.title) { image.title = "Generic Title: " + query; } images.push(image); } callback(images); }); } }; }, '0.0.1', {requires: ['yql']});
To create the binder for click events and invoke the
index
function of the controller, replace the code inbinders/index.js
with the code below. Again, Be sure to replace the'{your_flickr_api_key}'
with your own Flickr API key.YUI.add('PagerMojitBinder', function(Y, NAME) { var API_KEY = '{your_flickr_api_key}'; function parseImageId(link) { var matches = link.match(/com\/(\d+)\/(\d+)_([0-9a-z]+)\.jpg$/); return matches[2]; } function parsePage(link) { var matches = link.match(/page=(\d+)/); return matches[1]; } /** * The PagerMojitBinder module. * @module PagerMojitBinder */ /** * Constructor for the Binder class. * * @param mojitProxy {Object} The proxy to allow * the binder to interact with its owning mojit. * @class Binder * @constructor */ Y.namespace('mojito.binders')[NAME] = { /** * Binder initialization method, invoked * after all binders on the page have * been constructed. */ init: function(mojitProxy) { this.mojitProxy = mojitProxy; }, /** * The binder method, invoked to allow the mojit * to attach DOM event handlers. * @param node {Node} The DOM node to which this * mojit is attached. */ bind: function(node) { var thatNode = node; Y.log('NODE: ' + Y.dump(this.node)); // define the action when user click on prev/next var flipper = function(event) { var target = event.target; // get the link to the page var page = parsePage(target.get('href')); Y.log('PAGE: ' + page); var updateDOM = function(markup) { thatNode.set('innerHTML', markup); thatNode.all('#nav a').on('click', flipper, this); thatNode.all('#master ul li a').on('mouseover', showOverlay, this); thatNode.all('#master ul li a').on('mouseout', showOverlay, this); }; this.mojitProxy.invoke('index', { params: {page: page} }, updateDOM ); }; var showOverlay = function(event) { var target = event.target; var href = target.get('href'); var imageId = parseImageId(href); if (target.hasClass('overlayed')) { target.removeClass('overlayed'); thatNode.one('#display').setContent(''); } else { Y.log('HREF: ' + href); Y.log('IMAGE ID: ' + imageId); target.addClass('overlayed'); // Query for the image metadata var query = 'select * from flickr.photos.info where photo_id="' + imageId + '" and api_key="' + API_KEY + '"'; thatNode.one('#display').setContent('Loading ...'); Y.YQL(query, function(raw) { if (!raw.query.results.photo) { Y.log('No results found for photoId: ' + imageId); return; } var props = raw.query.results.photo; var snippet = '<ul style="list-style-type: square;">'; for (var key in props) { if (typeof(props[key]) == 'object') { continue; } snippet += '<li>' + key + ': ' + props[key] + '</li>'; } snippet += '</ul>'; thatNode.one('#display').setContent(snippet); }); } }; // Bind all the image links to showOverlay thatNode.all('#master ul li a').on('mouseover', showOverlay, this); thatNode.all('#master ul li a').on('mouseout', showOverlay, this); // Bind the prev + next links to flipper thatNode.all('#nav a').on('click', flipper, this); } }; }, '0.0.1', {requires: ['yql', 'io', 'dump']});
To display links to photos and associated photo data in the rendered template, replace the code in
views/index.hb.html
with the following:<div id="{{mojit_view_id}}" class="mojit" style="position: relative; width: 960px"> <h3>Query Term: {{query}}</h3> <div id="nav" style="clear: both;"> {{#hasLink}} {{#prev}} <a href="{{{link}}}">{{title}}</a> {{/prev}} {{/hasLink}} {{^hasLink}} {{#prev}}{{title}}{{/prev}} {{/hasLink}} {{#next}} <a href="{{{link}}}">{{title}}</a> {{/next}} </div> <div id="master" style="width: 30%; float: left;"> <ul> {{#data}} <li><a href="{{location}}" data-id="{{image_id}}">{{title}}</a></li> {{/data}} </ul> </div> <div style="width: 50%; float: right"> <!-- load image here dynamically --> <div id="display" style="margin: 0 auto;"> </div> </div> </div>
From the application directory, run the server.
$ mojito start
To view your application, go to the URL: