File: lib/client.js
/*
* Copyright (c) 2013, Yahoo! Inc. All rights reserved.
* Copyrights licensed under the New BSD License.
* See the accompanying LICENSE file for terms.
*/
/*jshint eqeqeq: false*/
/**
Provides a set of features
to control a YUI instance on the client side. This module will be
serialized and sent to the client side thru `res.expose()` and available
in the client side thru `window.app.yui`.
@module express-yui/lib/client
**/
var utils = require('./utils');
/**
Provides a set of methods to be serialized and sent to the client side to boot
the application in the browser.
@class client
@static
@uses utils
@extensionfor express-yui/lib/yui
*/
function bootstrap(method, args) {
var self = this,
d = document,
head = d.getElementsByTagName('head')[0],
// Some basic browser sniffing...
ie = /MSIE/.test(navigator.userAgent),
// it is just a queue of pending "use" statements until YUI gets ready
callback = [],
config = typeof YUI_config != "undefined" ? YUI_config : {};
function flush() {
var l = callback.length,
i;
if (!self.YUI && typeof YUI == "undefined") {
throw new Error("YUI was not injected correctly!");
}
self.YUI = self.YUI || YUI;
// callback on every pending use statement
for (i = 0; i < l; i++) {
callback.shift()();
}
}
function decrementRequestPending() {
self._pending--;
if (self._pending <= 0) {
setTimeout(flush, 0);
} else {
// in case there is any remaining script to be loaded
load();
}
}
function createScriptNode(src) {
var node = d.createElement('script');
// use async=false for ordered async?
// parallel-load-serial-execute http://wiki.whatwg.org/wiki/Dynamic_Script_Execution_Order
if (node.async) {
node.async = false;
}
if (ie) {
node.onreadystatechange = function () {
if (/loaded|complete/.test(this.readyState)) {
this.onreadystatechange = null;
decrementRequestPending();
}
};
} else {
node.onload = node.onerror = decrementRequestPending;
}
node.setAttribute('src', src);
return node;
}
function load() {
if (!config.seed) {
throw new Error('YUI_config.seed array is required.');
}
var seed = config.seed,
l = seed.length,
i, node;
if (!self._injected) {
// flagging the injection process to avoid duplicated entries
self._injected = true;
self._pending = seed.length;
}
// appending every file from seed collection into the header
for (i = 0; i < l; i++) {
node = createScriptNode(seed.shift());
head.appendChild(node);
// TODO: if original node.async is undefined, it means the order has
// to be preserved manually, in which case we should insert
// them one by one to guarantee serial execute
if (node.async !== false) {
break;
}
}
}
callback.push(function () {
var i;
if (!self._Y) {
// extend core (YUI_config.extendedCore)
self.YUI.Env.core.push.apply(self.YUI.Env.core, config.extendedCore || []);
// create unique instance which is accesible thru app.yui.use()
self._Y = self.YUI();
self.use = self._Y.use;
if (config.patches && config.patches.length) {
for (i = 0; i < config.patches.length; i += 1) {
config.patches[i](self._Y, self._Y.Env._loader);
}
}
}
// call use/require with arguments
self._Y[method].apply(self._Y, args);
});
// just in case YUI was injected manually in the page
self.YUI = self.YUI || (typeof YUI != "undefined" ? YUI : null);
if (!self.YUI && !self._injected) {
// attach seed and wait for it to finish to flush the callback
load();
} else if (self._pending <= 0) {
// everything is ready, just flush on the next tick
setTimeout(flush, 0);
} // else do nothing, the current pending job will take care of flushing callback
return this; // chaining
}
module.exports = {
/**
Attaches the seed into the head, then creates a YUI Instance and attaches
`modules` into it. This is equivalent to `YUI().use()` after getting the seed
ready. This method is a bootstrap implementation for the library, and the way
you use this in your templates, is by doing this:
<script>{{{state}}}</script>
<script>
app.yui.use('foo', 'bar', function (Y) {
// do something!
});
</script>
Where `state` defines the state of the express app, and `app.yui` defines
the helpers that `express-yui` brings into the client side.
@method use
@public
@chainable
**/
use: utils.minifyFunction(function () {
return this._bootstrap('use', [].slice.call(arguments));
}),
/**
Like `app.yui.use()` but instead delegates to `YUI().require()` after
getting the seed ready. The callback passed to `require()` receives two
arguments, the `Y` object like `use()`, but also `imports` which holds all
of the exports from the required modules.
<script>{{{state}}}</script>
<script>
app.yui.require('foo', 'bar', function (Y, imports) {
var Foo = imports['foo'],
namedExport = imports['bar'].baz;
});
</script>
@method require
@public
**/
require: utils.minifyFunction(function () {
this._bootstrap('require', [].slice.call(arguments));
}),
/**
Boots the application, rehydrate the app state and calls back to notify the
`ready` state of the app.
<script>{{{state}}}</script>
<script>
app.yui.ready(function (err) {
if (err) {
throw err;
}
app.yui.use('foo', 'bar', function (Y) {
// do something!
});
});
</script>
@method ready
@param {Function} callback when the app is ready. If an error occurr, the error object
will be passed as the first argument of the callback function.
@public
**/
ready: utils.minifyFunction(function (callback) {
this.use(function () {
callback();
});
}),
/**
The internal bootstraping implementation.
@method _bootstrap
@param {String} method The method to call on `Y`, either `"use"` or `"require"`.
@param {Any[]} args Array of `arguments` to apply to the `Y` `method`.
@private
*/
_bootstrap: utils.minifyFunction(bootstrap)
};