UNPKG

mojito

Version:

Mojito provides an architecture, components and tools for developers to build complex web applications faster.

454 lines (392 loc) 15.8 kB
/* * Copyright (c) 2011-2013, Yahoo! Inc. All rights reserved. * Copyrights licensed under the New BSD License. * See the accompanying LICENSE file for terms. */ /*jslint anon:true, sloppy:true, nomen:true*/ /*global YUI*/ /** * Client side Mojito runtime * @module MojitoClient */ YUI.add('mojito-mojit-proxy', function(Y, NAME) { var MJT_EVT_PRFX = 'mojit:'; /** * The object that is given to each mojit binder to be used to interact with * other mojits and the mojito framework. * @class MojitProxy */ function MojitProxy(opts) { // "private" this._action = opts.action; this._binder = opts.binder; this._base = opts.base; this._node = opts.node; this._element = opts.element; this._viewId = opts.viewId; this._instanceId = opts.instanceId; this._client = opts.client; this._store = opts.store; /** * The mojit type * @property type * @type {String} */ this.type = opts.type; /** * The mojit configuration for this binder * @property config * @type {Object} */ this.config = opts.config; /** * The mojit data model, for getting and setting data that is shared by * a single mojit controller, binder, and/or template. * Use this.data.get() and this.data.set(). See also * http://yuilibrary.com/yui/docs/api/classes/Model.html * and http://developer.yahoo.com/cocktails/mojito/docs/topics/mojito_data.html#sharing-data * @property data * @type {Model.Vanilla} */ this.data = new Y.Model(opts.data || {}); /** * Returns the value of the specified attribute. * @method data.get * @param {string} name Attribute name. * @return {Any} Attribute value, or `undefined` if the attribute doesn't * exist. */ /** * Sets the value of a single attribute. * @method data.set * @param {string} name Attribute name. * @param {any} value Value to set. * @param {Object} [options] Data to be mixed into the event facade * of the `change` event(s) for these attributes. * @param {Boolean} [options.silent=false] If `true`, no `change` * event will be fired. * @chainable */ /** * The page data model, or getting and setting data that is shared * between mojits on a page. * Use this.pageData.get() and this.pageData.set(). See also * http://yuilibrary.com/yui/docs/api/classes/Model.html * and http://developer.yahoo.com/cocktails/mojito/docs/topics/mojito_data.html#sharing-data * @property pageData * @type {Model.Vanilla} */ this.pageData = opts.pageData; /** * Returns the value of the specified attribute. * @method pageData.get * @param {string} name Attribute name. * @return {Any} Attribute value, or `undefined` if the attribute doesn't * exist. */ /** * Sets the value of a single attribute. * @method pageData.set * @param {string} name Attribute name. * @param {any} value Value to set. * @param {Object} [options] Data to be mixed into the event facade * of the `change` event(s) for these attributes. * @param {Boolean} [options.silent=false] If `true`, no `change` * event will be fired. * @chainable */ /** * The context used to generate this page * @property context * @type {Object} */ this.context = opts.context; /** * The config of a child which this mojit is proxying. * @property proxied * @optional * @type {Object} */ this.proxied = opts.proxied; } MojitProxy.prototype = { /** * Used by mojit binders to broadcast a message between mojits. * @method broadcast * @param {String} name event name. * @param {Object} payload the payload for the event. * @param {Object} options currently only used to specify target for * broadcast. For example, to target only one child mojit for * broadcast, use: * {target: {slot: 'slot name', viewId: 'DOM view id'}}. */ broadcast: function(name, payload, options) { this._client.doBroadcast(MJT_EVT_PRFX + name, this._viewId, payload, options); }, /** * Allows mojit binders to register to listen to other mojit events * @method listen * @param {String} name event name. * @param {Function} callback called when an event is broadcast with * the event data. */ listen: function(name, callback) { this._client.doListen(MJT_EVT_PRFX + name, this._viewId, function(evt) { // prevent mojits from listening to their own events if (evt.source !== this.id) { callback(evt); } }); }, /** * The opposite of the "listen" function. Deletes all callback functions * from the listener queue associated with this binder and event type. * If event name is not specified, all callbacks associated with this * binder are deleted. * @method unlisten * @param {String} [optional] name event name. */ unlisten: function(name) { var eventName = name ? MJT_EVT_PRFX + name : null; this._client.doUnlisten(this._viewId, eventName); }, /** * This method renders the "data" provided into the "View" specified. * The "view" must be the name of one of the files in the current * Mojits "views" folder. Returns via the callback. * @method render * @param {Object} data The data to render. * @param {String} view The view name to use for rendering. * @param {function(err,str)} cb The callback function. */ render: function(data, view, cb) { // this.pageData will be accessible in views thru `page` // but only if the mojit is not overruling it by setting it // thru mp.data.set('page', {}) or by mp.render({ page: {} }, ...) this._client.doRender(this, Y.merge({ page: this.pageData.toJSON() }, this.data.toJSON(), data), view, cb); }, /** * Used by the mojit binders to invoke actions on themselves within * Mojito. * The <em>options</em> parameter is optional and may contain: * <dl> * <dt>params</dt><dd>&lt;object&gt; must be broken out explicitly: * <dl> * <dt>route</dt><dd>&lt;object&gt; Map of key/value pairs.</dd> * <dt>url</dt><dd>&lt;object&gt; Map of key/value pairs.</dd> * <dt>body</dt><dd>&lt;object&gt; Map of key/value pairs.</dd> * <dt>file</dt><dd>&lt;object&gt; Map of key/value pairs.</dd> * </dl></dd> * <dt>rpc</dt><dd>&lt;boolean&gt; Means that we are immediately * sending the request to the server to answer the invocation.</dd> * </dl> * @method invoke * @param {String} action name of the action to invoke. * @param {Object} options see above. * @param {Function} cb function to be called on completion. */ invoke: function(action, options, cb) { var callback, command, instance; options = options || {}; // If there are no options use it as the callback if ('function' === typeof options) { callback = options; options = {}; } else { callback = cb; } // If we don't have a callback set an empty one if ('function' !== typeof callback) { // TODO: this can be a constant function...not created on each // invoke call. callback = function() {}; } // Make sure we have a "params" key in our "options" object options.params = options.params || {}; // This is the "partial instance" which isn't an "Object instance" // :). At least one of base or type must be defined. // TODO: we don't check base vs. type here... instance = { base: this._base, type: this.type, instanceId: this._instanceId, config: Y.mojito.util.copy(this.config), data: this.data // passing the hydrated data model to be re-used }; // Create the command we will use to call executeAction() with command = { instance: instance, action: action, params: { // Explicitly map the params to there keys route: options.params.route || {}, // NOTE the defaulting here to drive from URL if no explicit // params are provided. // TODO: we should have an explicit override option here and // merge... url: options.params.url || this.getFromUrl(), body: options.params.body || {}, file: options.params.file || {} }, // NOTE this isn't a standard command option. // TODO: not really "proper" to be part of command object, // should really be somewhere else...but where? rpc: options.rpc || false }; if (options.tunnelUrl) { command._tunnelUrl = options.tunnelUrl; } this._client.executeAction(command, this.getId(), callback); }, /** * Refreshes the current DOM view for this binder without recreating the * binder instance. Will call the binder's onRefreshView() function when * complete with the new Y.Node and HTMLElement objects. * @method refreshView * @param {Object} opts same as the options for invoke(). * @param {Function} cb Called after replacement and onRefreshView have * been called, sends data/meta. */ refreshView: function(opts, cb) { opts = opts || {}; if (Y.Lang.isFunction(opts)) { cb = opts; opts = {}; } this._client.refreshMojitView(this, opts, cb); }, /** * Gets URL parameters * @method getFromUrl * @param {String} key The name of the parameter required. * @return {String|Object} param value, or all params if no key * specified. */ getFromUrl: function(key) { if (!this.query) { this.query = Y.QueryString.parse( window.location.href.split('?').pop() ); } if (key) { return this.query[key]; } return this.query; }, /* * Returns the DOM Node ID for the current binder * @method getId * @return {String} YUI GUID */ getId: function() { return this._viewId; }, /** * Helper function to gather up details about a mojit's children from * the Mojito Client. * @method getChildren * @return {Object} slot &lt;String&gt;-->child information &lt;Object&gt;. */ getChildren: function() { return this._client._mojits[this.getId()].children; }, /** * Clears out a child's view, calling the appropriate life cycle * functions, then destroy's its binder and dereferences it. Will also * dereference the child from this mojit's children. * @method destroyChild * @param {String} id Either the slot key of the child, or the DOM * view id of the child. * @param {Boolean} retainNode if true, the binder's node will remain in * the dom. */ destroyChild: function(id, retainNode) { var slot, doomed, // viewid/dom id children = this.getChildren(), child = children[id]; if (child) { doomed = child.viewId; } else { //child key could be a random YUI Id for (slot in children) { if (children.hasOwnProperty(slot) && children[slot].viewId === id) { doomed = id; break; } } } if (!doomed) { throw new Error("Cannot destroy a child mojit with id '" + id + "'. Are you sure this is your child?"); } this._client.destroyMojitProxy(doomed, retainNode); if (child) { if (this.config.children) { this.config.children[id] = undefined; } children[id] = undefined; } }, /** * Destroys all children. (Calls destroyChild() for each child.) * @method destroyChildren * @param {Boolean} retainNode if true, the binder's node will remain in * the dom. */ destroyChildren: function(retainNode) { var children = this.getChildren(), child; for (child in children) { if (children.hasOwnProperty(child)) { this.destroyChild(child, retainNode); } } }, /** * Allows a binder to destroy itself and be removed from Mojito client * runtime entirely. * @method destroySelf * @param {Boolean} retainNode if true, the binder's node will remain in * the dom. */ destroySelf: function(retainNode) { this._client.destroyMojitProxy(this.getId(), retainNode); }, _destroy: function(retainNode) { var binder = this._binder; Y.log('destroying binder ' + this._viewId, 'debug', NAME); this.destroyChildren(retainNode); if (Y.Lang.isFunction(binder.destroy)) { // this will de-register all this binder's callbacks from any // listener queues binder.destroy.call(binder); } if (!retainNode) { this._node.remove(true); } this._client.doUnlisten(this._viewId); }, _pause: function() { var binder = this._binder; if (Y.Lang.isFunction(binder.onPause)) { binder.onPause(); } }, _resume: function() { var binder = this._binder; if (Y.Lang.isFunction(binder.onResume)) { binder.onResume(); } } }; Y.namespace('mojito').MojitProxy = MojitProxy; }, '0.1.0', {requires: [ 'mojito', 'mojito-util', 'querystring', 'model' ]});