diff --git a/README.md b/README.md index 1b121e8..9450704 100644 --- a/README.md +++ b/README.md @@ -98,4 +98,4 @@ Licensed under the MIT license. ## Credits and collaboration -The lead developer of FruitMachine is [Wilson Page](http://github.com/wilsonpage) at FT Labs. All open source code released by FT Labs is licenced under the MIT licence. We welcome comments, feedback and suggestions. Please feel free to raise an issue or pull request. Enjoy... +The lead developer of FruitMachine is [Wilson Page](http://github.com/wilsonpage) at FT Labs. All open source code released by FT Labs is licenced under the MIT licence. We welcome comments, feedback and suggestions. Please feel free to raise an issue or pull request. Enjoy... \ No newline at end of file diff --git a/build/fruitmachine.js b/build/fruitmachine.js index d84e893..7ff3c5f 100644 --- a/build/fruitmachine.js +++ b/build/fruitmachine.js @@ -140,75 +140,7 @@ module.exports = function(options) { return events(fm); }; -},{"./define":4,"./module":5,"utils":6,"event":7}],6:[function(require,module,exports){ - -/*jshint browser:true, node:true*/ - -'use strict'; - -exports.bind = function(method, context) { - return function() { return method.apply(context, arguments); }; -}; - -exports.isArray = function(arg) { - return arg instanceof Array; -}, - -exports.mixin = function(original, source) { - for (var key in source) original[key] = source[key]; - return original; -}, - -exports.byId = function(id, el) { - if (el) return el.querySelector('#' + id); -}, - -/** - * Inserts an item into an array. - * Has the option to state an index. - * - * @param {*} item - * @param {Array} array - * @param {Number} index - * @return void - */ -exports.insert = function(item, array, index) { - if (typeof index !== 'undefined') { - array.splice(index, 0, item); - } else { - array.push(item); - } -}, - -exports.toNode = function(html) { - var el = document.createElement('div'); - el.innerHTML = html; - return el.removeChild(el.firstElementChild); -}, - -// Determine if we have a DOM -// in the current environment. -exports.hasDom = function() { - return typeof document !== 'undefined'; -}; - -var i = 0; -exports.uniqueId = function(prefix) { - return (prefix || 'id') + ((++i) * Math.round(Math.random() * 100000)); -}; - -exports.keys = function(object) { - var keys = []; - for (var key in object) keys.push(key); - return keys; -}; - -exports.isPlainObject = function(ob) { - if (!ob) return false; - var c = (ob.constructor || '').toString(); - return !!~c.indexOf('Object'); -}; -},{}],7:[function(require,module,exports){ +},{"./define":4,"./module":5,"utils":6,"event":7}],7:[function(require,module,exports){ /** * Event @@ -322,6 +254,74 @@ function mixin(a, b) { for (var key in b) a[key] = b[key]; return a; } +},{}],6:[function(require,module,exports){ + +/*jshint browser:true, node:true*/ + +'use strict'; + +exports.bind = function(method, context) { + return function() { return method.apply(context, arguments); }; +}; + +exports.isArray = function(arg) { + return arg instanceof Array; +}, + +exports.mixin = function(original, source) { + for (var key in source) original[key] = source[key]; + return original; +}, + +exports.byId = function(id, el) { + if (el) return el.querySelector('#' + id); +}, + +/** + * Inserts an item into an array. + * Has the option to state an index. + * + * @param {*} item + * @param {Array} array + * @param {Number} index + * @return void + */ +exports.insert = function(item, array, index) { + if (typeof index !== 'undefined') { + array.splice(index, 0, item); + } else { + array.push(item); + } +}, + +exports.toNode = function(html) { + var el = document.createElement('div'); + el.innerHTML = html; + return el.removeChild(el.firstElementChild); +}, + +// Determine if we have a DOM +// in the current environment. +exports.hasDom = function() { + return typeof document !== 'undefined'; +}; + +var i = 0; +exports.uniqueId = function(prefix) { + return (prefix || 'id') + ((++i) * Math.round(Math.random() * 100000)); +}; + +exports.keys = function(object) { + var keys = []; + for (var key in object) keys.push(key); + return keys; +}; + +exports.isPlainObject = function(ob) { + if (!ob) return false; + var c = (ob.constructor || '').toString(); + return !!~c.indexOf('Object'); +}; },{}],8:[function(require,module,exports){ 'use strict'; @@ -547,8 +547,8 @@ module.exports = function(fm) { this._id = options.id || util.uniqueId(); this._fmid = options.fmid || util.uniqueId('fmid'); this.tag = options.tag || this.tag || 'div'; - this.classes = this.classes || options.classes || []; - this.helpers = this.helpers || options.helpers || []; + this.classes = options.classes || this.classes || []; + this.helpers = options.helpers || this.helpers || []; this.template = this._setTemplate(options.template || this.template); this.slot = options.slot; @@ -1303,36 +1303,56 @@ module.exports = function(fm) { }; /** - * Returns a JSON represention of + * @deprecated + */ + proto.toJSON = function(options) { + return this.serialize(options); + }; + + /** + * Returns a serialized represention of * a FruitMachine Module. This can * be generated serverside and - * passed into new FruitMachine(json) + * passed into new FruitMachine(serialized) * to inflate serverside rendered * views. * + * Options: + * + * - `inflatable` Whether the returned object should retain references to DOM ids for use with client-side inflation of views + * + * @param {Object} options * @return {Object} * @api public */ - proto.toJSON = function() { - var json = {}; - json.children = []; + proto.serialize = function(options) { + var serialized = {}; + + // Shallow clone the options + options = mixin({ + inflatable: true + }, options); + + serialized.children = []; // Recurse this.each(function(child) { - json.children.push(child.toJSON()); + serialized.children.push(child.serialize()); }); - json.id = this.id(); - json.fmid = this._fmid; - json.module = this.module(); - json.model = this.model.toJSON(); - json.slot = this.slot; + serialized.id = this.id(); + serialized.module = this.module(); + serialized.model = this.model.toJSON(); + serialized.slot = this.slot; + + if (options.inflatable) serialized.fmid = this._fmid; // Fire a hook to allow third - // parties to alter the json output - this.fireStatic('tojson', json); + // parties to alter the output + this.fireStatic('tojson', serialized); // @deprecated + this.fireStatic('serialize', serialized); - return json; + return serialized; }; // Events diff --git a/build/fruitmachine.min.js b/build/fruitmachine.min.js index f9c5703..9b0d695 100644 --- a/build/fruitmachine.min.js +++ b/build/fruitmachine.min.js @@ -1 +1 @@ -(function(t){if("function"==typeof bootstrap)bootstrap("fruitmachine",t);else if("object"==typeof exports)module.exports=t();else if("function"==typeof define&&define.amd)define(t);else if("undefined"!=typeof ses){if(!ses.ok())return;ses.makeFruitmachine=t}else"undefined"!=typeof window?window.fruitmachine=t():global.fruitmachine=t()})(function(){return function(t,e,i){function n(i,s){if(!e[i]){if(!t[i]){var o="function"==typeof require&&require;if(!s&&o)return o(i,!0);if(r)return r(i,!0);throw Error("Cannot find module '"+i+"'")}var h=e[i]={exports:{}};t[i][0].call(h.exports,function(e){var r=t[i][1][e];return n(r?r:e)},h,h.exports)}return e[i].exports}for(var r="function"==typeof require&&require,s=0;i.length>s;s++)n(i[s]);return n}({1:[function(t,e){"use strict";var i=t("./fruitmachine"),n=t("model");e.exports=i({Model:n})},{"./fruitmachine":2,model:3}],4:[function(t,e){"use strict";e.exports=function(t){return function(e){var i="object"==typeof e?t.Module.extend(e):e,n=i.prototype,r=n.name||n._module;return r&&(t.modules[r]=i),i}}},{}],2:[function(t,e){"use strict";var i=t("./module"),n=t("./define"),r=t("utils"),s=t("event");e.exports=function(t){function o(t){var e=o.modules[t.module];return e?new e(t):void 0}return o.create=e.exports,o.Model=t.Model,o.Events=s,o.Module=i(o),o.define=n(o),o.util=r,o.modules={},o.config={templateIterator:"children",templateInstance:"child"},s(o)}},{"./define":4,"./module":5,utils:6,event:7}],6:[function(t,e,i){"use strict";i.bind=function(t,e){return function(){return t.apply(e,arguments)}},i.isArray=function(t){return t instanceof Array},i.mixin=function(t,e){for(var i in e)t[i]=e[i];return t},i.byId=function(t,e){return e?e.querySelector("#"+t):void 0},i.insert=function(t,e,i){i!==void 0?e.splice(i,0,t):e.push(t)},i.toNode=function(t){var e=document.createElement("div");return e.innerHTML=t,e.removeChild(e.firstElementChild)},i.hasDom=function(){return"undefined"!=typeof document};var n=0;i.uniqueId=function(t){return(t||"id")+ ++n*Math.round(1e5*Math.random())},i.keys=function(t){var e=[];for(var i in t)e.push(i);return e},i.isPlainObject=function(t){if(!t)return!1;var e=""+(t.constructor||"");return!!~e.indexOf("Object")}},{}],7:[function(t,e){function i(t){return this instanceof i?t?n(t,r):void 0:new i(t)}function n(t,e){for(var i in e)t[i]=e[i];return t}var r=i.prototype;e.exports=i,r.on=function(t,e){return this._cbs=this._cbs||{},(this._cbs[t]||(this._cbs[t]=[])).unshift(e),this},r.off=function(t,e){if(this._cbs=this._cbs||{},!t)return this._cbs={};if(!e)return delete this._cbs[t];for(var i,n=this._cbs[t]||[];n&&~(i=n.indexOf(e));)n.splice(i,1);return this},r.fire=function(t){this._cbs=this._cbs||{};var e=t.name||t,i=t.ctx||this,n=this._cbs[e];if(n)for(var r=[].slice.call(arguments,1),s=n.length;s--;)n[s].apply(i,r);return this}},{}],8:[function(t,e){"use strict";var i={}.hasOwnProperty;e.exports=function(t){for(var e,n,r=arguments,s=r.length,o=0;s>++o;){e=r[o];for(n in e)i.call(e,n)&&(t[n]=e[n])}return t}},{}],3:[function(t,e){"use strict";function i(t){this._data=r({},t)}var n=t("event"),r=t("mixin");e.exports=i;var s=i.prototype;s.get=function(t){return t?this._data[t]:this._data},s.set=function(t,e){if("string"==typeof t&&e!==void 0&&(this._data[t]=e,this.fire("change:"+t,e)),"object"==typeof t){r(this._data,t);for(var i in t)this.fire("change:"+i,t[i])}return this.fire("change"),this},s.clear=function(){return this._data={},this.fire("change"),this},s.destroy=function(){for(var t in this._data)this._data[t]=null;delete this._data,this.fire("destroy")},s.toJSON=function(){return r({},this._data)},n(s)},{mixin:8,event:7}],5:[function(t,e){"use strict";var i=t("utils"),n=t("./events"),r=t("extend"),s=i.mixin;e.exports=function(t){function e(t){t=s({},t),this._configure(t),this._add(t.children),this.initialize&&this.initialize(t),this.fireStatic("initialize",t)}var o=e.prototype;return o._configure=function(e){this._id=e.id||i.uniqueId(),this._fmid=e.fmid||i.uniqueId("fmid"),this.tag=e.tag||this.tag||"div",this.classes=this.classes||e.classes||[],this.helpers=this.helpers||e.helpers||[],this.template=this._setTemplate(e.template||this.template),this.slot=e.slot,this.children=[],this._ids={},this._modules={},this.slots={};var n=e.model||e.data||{};this.model=i.isPlainObject(n)?new this.Model(n):n,this.helpers.forEach(this.attachHelper,this),e.fmid&&(t.fire("inflation",this,e),this.fireStatic("inflation",e))},o._add=function(t){if(t){var e,n=i.isArray(t);for(var r in t)e=t[r],n||(e.slot=r),this.add(e)}},o.attachHelper=function(t){t&&t(this)},o._setTemplate=function(t){return t&&t.render?i.bind(t.render,t):t},o.add=function(n,r){if(!n)return this;var s=r&&r.at,o=r&&r.inject,h="object"==typeof r?r.slot:r;n.parent&&n.remove({fromDOM:!1}),h=n.slot=h||n.slot;var u=this.slots[h];return u&&u.remove({fromDOM:!1}),n instanceof e||(n=t(n)),i.insert(n,this.children,s),this._addLookup(n),o&&this._injectEl(n.el,r),this},o.remove=function(t,i){if(1===arguments.length&&!t)return this;if(t instanceof e)return t.remove(i||{}),this;var n,r=t||{},s=r.fromDOM!==!1,o=this.parent,h=this.el,u=h&&h.parentNode;return s&&u&&u.removeChild(h),o&&(n=o.children.indexOf(this),o.children.splice(n,1),o._removeLookup(this)),this},o._addLookup=function(t){var e=t.module();(this._modules[e]=this._modules[e]||[]).push(t),this._ids[t.id()]=t,t.slot&&(this.slots[t.slot]=t),t.parent=this},o._removeLookup=function(t){var e=t.module(),i=this._modules[e].indexOf(t);this._modules[e].splice(i,1),delete this._ids[t._id],delete this.slots[t.slot],delete t.parent},o._injectEl=function(t,e){var i=e&&e.at,n=this.el;t&&n&&(i!==void 0?n.insertBefore(t,n.children[i]):n.appendChild(t))},o.id=function(t){if(!arguments.length)return this._id;var e=this._ids[t];return e?e:this.each(function(e){return e.id(t)})},o.module=function(t){if(!arguments.length)return this._module||this.name;var e=this._modules[t];return e?e[0]:this.each(function(e){return e.module(t)})},o.modules=function(t){var e=this._modules[t]||[];return this.each(function(i){e=e.concat(i.modules(t))}),e},o.each=function(t){for(var e,i=this.children.length,n=0;i>n;n++)if(e=t(this.children[n]))return e},o.toHTML=function(){var e,i,n={};return n[t.config.templateIterator]=[],this.each(function(r){i={},e=r.toHTML(),n[r.slot||r.id()]=e,i[t.config.templateInstance]=e,n.children.push(s(i,r.model.toJSON()))}),e=this.template?this.template(s(n,this.model.toJSON())):"",this._wrapHTML(e)},o._wrapHTML=function(t){return"<"+this.tag+' class="'+this.module()+" "+this.classes.join(" ")+'" id="'+this._fmid+'">'+t+""},o.render=function(){var t=this.toHTML(),e=i.toNode(t);return this._setEl(e),this._fetchEls(this.el),this.fireStatic("render"),this},o.setup=function(t){var e=t&&t.shallow;return this._getEl()?(this.isSetup&&this.teardown({shallow:!0}),this.fireStatic("before setup"),this._setup&&this._setup(),this.fireStatic("setup"),this.isSetup=!0,e||this.each(function(t){t.setup()}),this):this},o.teardown=function(t){var e=t&&t.shallow;return e||this.each(function(t){t.teardown()}),this.isSetup&&(this.fireStatic("before teardown"),this._teardown&&this._teardown(),this.fireStatic("teardown"),this.isSetup=!1),this},o.destroy=function(t){t=t||{};for(var e=t.remove!==!1,i=this.children.length;i--;)this.children[i].destroy({remove:!1});return this.destroyed?this:(e&&this.remove(t),this.teardown({shallow:!0}),this.fireStatic("before destroy"),this._destroy&&this._destroy(),this.fireStatic("destroy"),this.off(),this.destroyed=!0,this.el=this.model=this.parent=this._modules=this._ids=this._id=null,void 0)},o.empty=function(){for(var t=this.children.length;t--;)this.children[t].destroy();return this},o._fetchEls=function(t){t&&this.each(function(e){e.el=i.byId(e._fmid,t),e._fetchEls(e.el||t)})},o._getEl=function(){return i.hasDom()?this.el=this.el||document.getElementById(this._fmid):void 0},o._setEl=function(t){var e=this.el,i=e&&e.parentNode;return i&&i.replaceChild(t,e),this.el=t,this},o.inject=function(t){return t&&(t.innerHTML="",this.appendTo(t),this.fireStatic("inject")),this},o.appendTo=function(t){return this.el&&t&&t.appendChild&&(t.appendChild(this.el),this.fireStatic("appendto")),this},o.toJSON=function(){var t={};return t.children=[],this.each(function(e){t.children.push(e.toJSON())}),t.id=this.id(),t.fmid=this._fmid,t.module=this.module(),t.model=this.model.toJSON(),t.slot=this.slot,this.fireStatic("tojson",t),t},o.on=n.on,o.off=n.off,o.fire=n.fire,o.fireStatic=n.fireStatic,e.extend=r(i.keys(o)),o.Model=t.Model,e}},{"./events":9,utils:6,extend:10}],9:[function(t,e,i){function n(t,e,i){t&&i.propagate&&(t.event=i,r.prototype.fire.apply(t,e),n(t.parent,e,i))}var r=t("event");i.on=function(t,e,i){return 2===arguments.length&&(i=e,e=null),e?r.prototype.on.call(this,t,function(){this.event.target.module()===e&&i.apply(this,arguments)}):r.prototype.on.call(this,t,i),this},i.fire=function(){var t=this.event,e={target:this,propagate:!0,stopPropagation:function(){this.propagate=!1}};return n(this,arguments,e),t?this.event=t:delete this.event,this},i.fireStatic=r.prototype.fire,i.off=r.prototype.off},{event:7}],10:[function(t,e){"use strict";function i(t,e){for(var i in e)e.hasOwnProperty(i)&&~t.indexOf(i)&&(e["_"+i]=e[i],delete e[i])}var n=t("utils").mixin;e.exports=function(t){return function(e){function r(){this.constructor=o}var s=this,o=function(){return s.apply(this,arguments)};return n(o,s),t&&i(t,e),r.prototype=s.prototype,o.prototype=new r,n(o.prototype,e),o.__super__=s.prototype,o}}},{utils:6}]},{},[1])(1)}); \ No newline at end of file +(function(t){if("function"==typeof bootstrap)bootstrap("fruitmachine",t);else if("object"==typeof exports)module.exports=t();else if("function"==typeof define&&define.amd)define(t);else if("undefined"!=typeof ses){if(!ses.ok())return;ses.makeFruitmachine=t}else"undefined"!=typeof window?window.fruitmachine=t():global.fruitmachine=t()})(function(){return function(t,e,i){function n(i,s){if(!e[i]){if(!t[i]){var o="function"==typeof require&&require;if(!s&&o)return o(i,!0);if(r)return r(i,!0);throw Error("Cannot find module '"+i+"'")}var h=e[i]={exports:{}};t[i][0].call(h.exports,function(e){var r=t[i][1][e];return n(r?r:e)},h,h.exports)}return e[i].exports}for(var r="function"==typeof require&&require,s=0;i.length>s;s++)n(i[s]);return n}({1:[function(t,e){"use strict";var i=t("./fruitmachine"),n=t("model");e.exports=i({Model:n})},{"./fruitmachine":2,model:3}],4:[function(t,e){"use strict";e.exports=function(t){return function(e){var i="object"==typeof e?t.Module.extend(e):e,n=i.prototype,r=n.name||n._module;return r&&(t.modules[r]=i),i}}},{}],2:[function(t,e){"use strict";var i=t("./module"),n=t("./define"),r=t("utils"),s=t("event");e.exports=function(t){function o(t){var e=o.modules[t.module];return e?new e(t):void 0}return o.create=e.exports,o.Model=t.Model,o.Events=s,o.Module=i(o),o.define=n(o),o.util=r,o.modules={},o.config={templateIterator:"children",templateInstance:"child"},s(o)}},{"./define":4,"./module":5,utils:6,event:7}],7:[function(t,e){function i(t){return this instanceof i?t?n(t,r):void 0:new i(t)}function n(t,e){for(var i in e)t[i]=e[i];return t}var r=i.prototype;e.exports=i,r.on=function(t,e){return this._cbs=this._cbs||{},(this._cbs[t]||(this._cbs[t]=[])).unshift(e),this},r.off=function(t,e){if(this._cbs=this._cbs||{},!t)return this._cbs={};if(!e)return delete this._cbs[t];for(var i,n=this._cbs[t]||[];n&&~(i=n.indexOf(e));)n.splice(i,1);return this},r.fire=function(t){this._cbs=this._cbs||{};var e=t.name||t,i=t.ctx||this,n=this._cbs[e];if(n)for(var r=[].slice.call(arguments,1),s=n.length;s--;)n[s].apply(i,r);return this}},{}],6:[function(t,e,i){"use strict";i.bind=function(t,e){return function(){return t.apply(e,arguments)}},i.isArray=function(t){return t instanceof Array},i.mixin=function(t,e){for(var i in e)t[i]=e[i];return t},i.byId=function(t,e){return e?e.querySelector("#"+t):void 0},i.insert=function(t,e,i){i!==void 0?e.splice(i,0,t):e.push(t)},i.toNode=function(t){var e=document.createElement("div");return e.innerHTML=t,e.removeChild(e.firstElementChild)},i.hasDom=function(){return"undefined"!=typeof document};var n=0;i.uniqueId=function(t){return(t||"id")+ ++n*Math.round(1e5*Math.random())},i.keys=function(t){var e=[];for(var i in t)e.push(i);return e},i.isPlainObject=function(t){if(!t)return!1;var e=""+(t.constructor||"");return!!~e.indexOf("Object")}},{}],8:[function(t,e){"use strict";var i={}.hasOwnProperty;e.exports=function(t){for(var e,n,r=arguments,s=r.length,o=0;s>++o;){e=r[o];for(n in e)i.call(e,n)&&(t[n]=e[n])}return t}},{}],3:[function(t,e){"use strict";function i(t){this._data=r({},t)}var n=t("event"),r=t("mixin");e.exports=i;var s=i.prototype;s.get=function(t){return t?this._data[t]:this._data},s.set=function(t,e){if("string"==typeof t&&e!==void 0&&(this._data[t]=e,this.fire("change:"+t,e)),"object"==typeof t){r(this._data,t);for(var i in t)this.fire("change:"+i,t[i])}return this.fire("change"),this},s.clear=function(){return this._data={},this.fire("change"),this},s.destroy=function(){for(var t in this._data)this._data[t]=null;delete this._data,this.fire("destroy")},s.toJSON=function(){return r({},this._data)},n(s)},{mixin:8,event:7}],5:[function(t,e){"use strict";var i=t("utils"),n=t("./events"),r=t("extend"),s=i.mixin;e.exports=function(t){function e(t){t=s({},t),this._configure(t),this._add(t.children),this.initialize&&this.initialize(t),this.fireStatic("initialize",t)}var o=e.prototype;return o._configure=function(e){this._id=e.id||i.uniqueId(),this._fmid=e.fmid||i.uniqueId("fmid"),this.tag=e.tag||this.tag||"div",this.classes=e.classes||this.classes||[],this.helpers=e.helpers||this.helpers||[],this.template=this._setTemplate(e.template||this.template),this.slot=e.slot,this.children=[],this._ids={},this._modules={},this.slots={};var n=e.model||e.data||{};this.model=i.isPlainObject(n)?new this.Model(n):n,this.helpers.forEach(this.attachHelper,this),e.fmid&&(t.fire("inflation",this,e),this.fireStatic("inflation",e))},o._add=function(t){if(t){var e,n=i.isArray(t);for(var r in t)e=t[r],n||(e.slot=r),this.add(e)}},o.attachHelper=function(t){t&&t(this)},o._setTemplate=function(t){return t&&t.render?i.bind(t.render,t):t},o.add=function(n,r){if(!n)return this;var s=r&&r.at,o=r&&r.inject,h="object"==typeof r?r.slot:r;n.parent&&n.remove({fromDOM:!1}),h=n.slot=h||n.slot;var u=this.slots[h];return u&&u.remove({fromDOM:!1}),n instanceof e||(n=t(n)),i.insert(n,this.children,s),this._addLookup(n),o&&this._injectEl(n.el,r),this},o.remove=function(t,i){if(1===arguments.length&&!t)return this;if(t instanceof e)return t.remove(i||{}),this;var n,r=t||{},s=r.fromDOM!==!1,o=this.parent,h=this.el,u=h&&h.parentNode;return s&&u&&u.removeChild(h),o&&(n=o.children.indexOf(this),o.children.splice(n,1),o._removeLookup(this)),this},o._addLookup=function(t){var e=t.module();(this._modules[e]=this._modules[e]||[]).push(t),this._ids[t.id()]=t,t.slot&&(this.slots[t.slot]=t),t.parent=this},o._removeLookup=function(t){var e=t.module(),i=this._modules[e].indexOf(t);this._modules[e].splice(i,1),delete this._ids[t._id],delete this.slots[t.slot],delete t.parent},o._injectEl=function(t,e){var i=e&&e.at,n=this.el;t&&n&&(i!==void 0?n.insertBefore(t,n.children[i]):n.appendChild(t))},o.id=function(t){if(!arguments.length)return this._id;var e=this._ids[t];return e?e:this.each(function(e){return e.id(t)})},o.module=function(t){if(!arguments.length)return this._module||this.name;var e=this._modules[t];return e?e[0]:this.each(function(e){return e.module(t)})},o.modules=function(t){var e=this._modules[t]||[];return this.each(function(i){e=e.concat(i.modules(t))}),e},o.each=function(t){for(var e,i=this.children.length,n=0;i>n;n++)if(e=t(this.children[n]))return e},o.toHTML=function(){var e,i,n={};return n[t.config.templateIterator]=[],this.each(function(r){i={},e=r.toHTML(),n[r.slot||r.id()]=e,i[t.config.templateInstance]=e,n.children.push(s(i,r.model.toJSON()))}),e=this.template?this.template(s(n,this.model.toJSON())):"",this._wrapHTML(e)},o._wrapHTML=function(t){return"<"+this.tag+' class="'+this.module()+" "+this.classes.join(" ")+'" id="'+this._fmid+'">'+t+""},o.render=function(){var t=this.toHTML(),e=i.toNode(t);return this._setEl(e),this._fetchEls(this.el),this.fireStatic("render"),this},o.setup=function(t){var e=t&&t.shallow;return this._getEl()?(this.isSetup&&this.teardown({shallow:!0}),this.fireStatic("before setup"),this._setup&&this._setup(),this.fireStatic("setup"),this.isSetup=!0,e||this.each(function(t){t.setup()}),this):this},o.teardown=function(t){var e=t&&t.shallow;return e||this.each(function(t){t.teardown()}),this.isSetup&&(this.fireStatic("before teardown"),this._teardown&&this._teardown(),this.fireStatic("teardown"),this.isSetup=!1),this},o.destroy=function(t){t=t||{};for(var e=t.remove!==!1,i=this.children.length;i--;)this.children[i].destroy({remove:!1});return this.destroyed?this:(e&&this.remove(t),this.teardown({shallow:!0}),this.fireStatic("before destroy"),this._destroy&&this._destroy(),this.fireStatic("destroy"),this.off(),this.destroyed=!0,this.el=this.model=this.parent=this._modules=this._ids=this._id=null,void 0)},o.empty=function(){for(var t=this.children.length;t--;)this.children[t].destroy();return this},o._fetchEls=function(t){t&&this.each(function(e){e.el=i.byId(e._fmid,t),e._fetchEls(e.el||t)})},o._getEl=function(){return i.hasDom()?this.el=this.el||document.getElementById(this._fmid):void 0},o._setEl=function(t){var e=this.el,i=e&&e.parentNode;return i&&i.replaceChild(t,e),this.el=t,this},o.inject=function(t){return t&&(t.innerHTML="",this.appendTo(t),this.fireStatic("inject")),this},o.appendTo=function(t){return this.el&&t&&t.appendChild&&(t.appendChild(this.el),this.fireStatic("appendto")),this},o.toJSON=function(t){return this.serialize(t)},o.serialize=function(t){var e={};return t=s({inflatable:!0},t),e.children=[],this.each(function(t){e.children.push(t.serialize())}),e.id=this.id(),e.module=this.module(),e.model=this.model.toJSON(),e.slot=this.slot,t.inflatable&&(e.fmid=this._fmid),this.fireStatic("tojson",e),this.fireStatic("serialize",e),e},o.on=n.on,o.off=n.off,o.fire=n.fire,o.fireStatic=n.fireStatic,e.extend=r(i.keys(o)),o.Model=t.Model,e}},{"./events":9,utils:6,extend:10}],9:[function(t,e,i){function n(t,e,i){t&&i.propagate&&(t.event=i,r.prototype.fire.apply(t,e),n(t.parent,e,i))}var r=t("event");i.on=function(t,e,i){return 2===arguments.length&&(i=e,e=null),e?r.prototype.on.call(this,t,function(){this.event.target.module()===e&&i.apply(this,arguments)}):r.prototype.on.call(this,t,i),this},i.fire=function(){var t=this.event,e={target:this,propagate:!0,stopPropagation:function(){this.propagate=!1}};return n(this,arguments,e),t?this.event=t:delete this.event,this},i.fireStatic=r.prototype.fire,i.off=r.prototype.off},{event:7}],10:[function(t,e){"use strict";function i(t,e){for(var i in e)e.hasOwnProperty(i)&&~t.indexOf(i)&&(e["_"+i]=e[i],delete e[i])}var n=t("utils").mixin;e.exports=function(t){return function(e){function r(){this.constructor=o}var s=this,o=function(){return s.apply(this,arguments)};return n(o,s),t&&i(t,e),r.prototype=s.prototype,o.prototype=new r,n(o.prototype,e),o.__super__=s.prototype,o}}},{utils:6}]},{},[1])(1)}); \ No newline at end of file diff --git a/docs/api.md b/docs/api.md index b281f63..b607da1 100644 --- a/docs/api.md +++ b/docs/api.md @@ -3,7 +3,7 @@ ### fruitmachine.define() Defines a module. -\nOptions: +Options: - `name {String}` the name of the module - `tag {String}` the tagName to use for the root element @@ -18,7 +18,7 @@ Defines a module. ### Module#Module Module constructor -\nOptions: +Options: - `id {String}` a unique id to query by - `model {Object|Model}` the data with which to associate this module @@ -31,7 +31,7 @@ Module constructor ### Module#add() Adds a child view(s) to another Module. -\nOptions: +Options: - `at` The child index at which to insert - `inject` Injects the child's view element into the parent's @@ -43,7 +43,7 @@ Removes a child view from its current Module contexts and also from the DOM unless otherwise stated. -\nOptions: +Options: - `fromDOM` Whether the element should be removed from the DOM (default `true`) @@ -65,7 +65,7 @@ otherwise stated. Returns a decendent module by id, or if called with no arguments, returns this view's id. -\n*Example:* +*Example:* myModule.id(); //=> 'my_view_id' @@ -79,7 +79,7 @@ Returns the first descendent Module with the passed module type. If called with no arguments the Module's own module type is returned. -\n*Example:* +*Example:* // Assuming 'myModule' has 3 descendent // views with the module type 'apple' @@ -93,7 +93,7 @@ Returns a list of descendent Modules that match the module type given (Similar to Element.querySelectorAll();). -\n*Example:* +*Example:* // Assuming 'myModule' has 3 descendent // views with the module type 'apple' @@ -106,7 +106,7 @@ Element.querySelectorAll();). Calls the passed function for each of the view's children. -\n*Example:* +*Example:* myModule.each(function(child) { // Do stuff with each child view... @@ -119,7 +119,7 @@ any descendent views returning an html string. All data in the views model is made accessible to the template. -\nChild views are printed into the +Child views are printed into the parent template by `id`. Alternatively children can be iterated over a a list and printed with `{{{child}}}}`. @@ -140,13 +140,13 @@ and printed with `{{{child}}}}`. Renders the view and replaces the `view.el` with a freshly rendered node. -\nFires a `render` event on the view. +Fires a `render` event on the view. ### Module#setup() Sets up a view and all descendent views. -\nSetup will be aborted if no `view.el` +Setup will be aborted if no `view.el` is found. If a view is already setup, teardown is run first to prevent a view being setup twice. @@ -161,7 +161,7 @@ Options: Tearsdown a view and all descendent views that have been setup. -\nYour custom `teardown` method is +Your custom `teardown` method is called and a `teardown` event is fired. Options: @@ -174,7 +174,7 @@ Completely destroys a view. This means a view is torn down, removed from it's current layout context and removed from the DOM. -\nYour custom `destroy` method is +Your custom `destroy` method is called and a `destroy` event is fired. NOTE: `.remove()` is only run on the view @@ -187,7 +187,7 @@ Options: ### Module#empty() Destroys all children. -\nIs this needed? +Is this needed? ### Module#inject() @@ -199,14 +199,17 @@ and appends the view into it. Appends the view element into the destination element. -### Module#toJSON() +### Module#serialize() -Returns a JSON represention of +Returns a serialized represention of a FruitMachine Module. This can be generated serverside and -passed into new FruitMachine(json) +passed into new FruitMachine(serialized) to inflate serverside rendered views. +Options: + + - `inflatable` Whether the returned object should retain references to DOM ids for use with client-side inflation of views ### Module#on() diff --git a/docs/server-side-rendering.md b/docs/server-side-rendering.md index 6a4dd26..3e24e6f 100644 --- a/docs/server-side-rendering.md +++ b/docs/server-side-rendering.md @@ -25,9 +25,8 @@ var Apple = require('./apple'); app.get('/', function(req, res) { var apple = new Apple(); var html = apple.toHTML(); - var json = apple.toJSON(); - - json = JSON.stringify(json); + var serialized = apple.serialize(); + var json = JSON.stringify(serialized); // Imagine this response is also // wrapped in usual document boilerplate diff --git a/docs/templates/api.hogan b/docs/templates/api.hogan index b05b706..cac7587 100644 --- a/docs/templates/api.hogan +++ b/docs/templates/api.hogan @@ -5,6 +5,6 @@ {{{description.summary}}} {{#description.body}} -\n{{{description.body}}} +{{{description.body}}} {{/description.body}} {{/jsdoc}} \ No newline at end of file diff --git a/docs/templates/readme.hogan b/docs/templates/readme.hogan index 95f82a5..b5460eb 100644 --- a/docs/templates/readme.hogan +++ b/docs/templates/readme.hogan @@ -1,4 +1,4 @@ -# {{pkg.title}} [![Build Status](https://travis-ci.org/ftlabs/fruitmachine.png?branch=master)](https://travis-ci.org/ftlabs/fruitmachine) +# {{pkg.title}} [![Build Status](https://travis-ci.org/ftlabs/fruitmachine.png?branch=master)](https://travis-ci.org/ftlabs/fruitmachine) [![Coverage Status](https://coveralls.io/repos/ftlabs/fruitmachine/badge.png?branch=master)](https://coveralls.io/r/ftlabs/fruitmachine?branch=master) [![Dependency Status](https://gemnasium.com/ftlabs/fruitmachine.png)](https://gemnasium.com/ftlabs/fruitmachine) {{pkg.description}} diff --git a/examples/express/.sass-cache/02edf621e302928b0497cc899071d282b21c69ae/style.scssc b/examples/express/.sass-cache/02edf621e302928b0497cc899071d282b21c69ae/style.scssc deleted file mode 100644 index 06da364..0000000 Binary files a/examples/express/.sass-cache/02edf621e302928b0497cc899071d282b21c69ae/style.scssc and /dev/null differ diff --git a/examples/express/.sass-cache/34148d82a00c750b7f5bb319d56d454f5d3a0706/main.scssc b/examples/express/.sass-cache/34148d82a00c750b7f5bb319d56d454f5d3a0706/main.scssc deleted file mode 100644 index d4c0722..0000000 Binary files a/examples/express/.sass-cache/34148d82a00c750b7f5bb319d56d454f5d3a0706/main.scssc and /dev/null differ diff --git a/examples/express/.sass-cache/c17ef6a51d16627d3a39ba3670d20574256733ff/style.scssc b/examples/express/.sass-cache/c17ef6a51d16627d3a39ba3670d20574256733ff/style.scssc deleted file mode 100644 index 06da364..0000000 Binary files a/examples/express/.sass-cache/c17ef6a51d16627d3a39ba3670d20574256733ff/style.scssc and /dev/null differ diff --git a/examples/express/.sass-cache/e311ee56d011e27e22137ec9eba8360ee5a6567e/main.scssc b/examples/express/.sass-cache/e311ee56d011e27e22137ec9eba8360ee5a6567e/main.scssc deleted file mode 100644 index d4c0722..0000000 Binary files a/examples/express/.sass-cache/e311ee56d011e27e22137ec9eba8360ee5a6567e/main.scssc and /dev/null differ diff --git a/examples/express/.sass-cache/f489b5958955e5b353f8ff421f5e0e803b363f01/style.scssc b/examples/express/.sass-cache/f489b5958955e5b353f8ff421f5e0e803b363f01/style.scssc deleted file mode 100644 index 06da364..0000000 Binary files a/examples/express/.sass-cache/f489b5958955e5b353f8ff421f5e0e803b363f01/style.scssc and /dev/null differ diff --git a/examples/express/build/build.js b/examples/express/build/build.js index ca9a7c2..d5e5e62 100644 --- a/examples/express/build/build.js +++ b/examples/express/build/build.js @@ -1,13 +1,246 @@ -;(function(e,t,n){function i(n,s){if(!t[n]){if(!e[n]){var o=typeof require=="function"&&require;if(!s&&o)return o(n,!0);if(r)return r(n,!0);throw new Error("Cannot find module '"+n+"'")}var u=t[n]={exports:{}};e[n][0](function(t){var r=e[n][1][t];return i(r?r:t)},u,u.exports)}return t[n].exports}var r=typeof require=="function"&&require;for(var s=0;s");_.b("\n" + i);_.b("
");_.b(_.t(_.f("slot_1",c,p,0)));_.b("
");_.b("\n" + i);_.b("
");_.b("\n" + i);_.b("
");_.b(_.t(_.f("slot_2",c,p,0)));_.b("
");_.b("\n" + i);_.b("
");_.b(_.t(_.f("slot_3",c,p,0)));_.b("
");_.b("\n" + i);_.b("
");_.b("\n" + i);_.b("");return _.fl();;}); +},{}],4:[function(require,module,exports){ + +/** + * Module Dependencies + */ + +var fm = require('../../../../lib/'); +var template = require('./template'); + +/** + * Exports + */ + +module.exports = fm.define({ + module: 'apple', + template: template +}); +},{"../../../../lib/":21,"./template":5}],5:[function(require,module,exports){ +module.exports = new Hogan(function(c,p,i){var _=this;_.b(i=i||"");_.b("
");_.b("\n" + i);_.b("
");_.b(_.v(_.f("title",c,p,0)));_.b("
");_.b("\n" + i);_.b("
");return _.fl();;}); +},{}],6:[function(require,module,exports){ + +/** + * Module Dependencies + */ + +var fm = require('../../../../lib/'); +var template = require('./template'); + +/** + * Exports + */ + +module.exports = fm.define({ + module: 'banana', + template: template +}); +},{"../../../../lib/":21,"./template":7}],7:[function(require,module,exports){ +module.exports = new Hogan(function(c,p,i){var _=this;_.b(i=i||"");_.b("
Module Banana
");return _.fl();;}); +},{}],8:[function(require,module,exports){ + +/** + * Module Dependencies + */ + +var fm = require('../../../../lib/'); +var template = require('./template'); + +/** + * Exports + */ + +module.exports = fm.define({ + module: 'orange', + template: template +}); +},{"../../../../lib/":21,"./template":9}],9:[function(require,module,exports){ +module.exports = new Hogan(function(c,p,i){var _=this;_.b(i=i||"");_.b("
Module Orange
");return _.fl();;}); +},{}],10:[function(require,module,exports){ +var content = document.querySelector('.js-app_content'); +var View = require('./view'); + +var database = { + title: 'This is the About page' +}; + +module.exports = function() { + app.view = View(database); + + app.view + .render() + .inject(content); +}; +},{"./view":11}],11:[function(require,module,exports){ +var fruitmachine = require('../../../../lib/'); + +// Require these views so that +// fruitmachine registers them +var LayoutA = require('../layout-a'); +var ModuleApple = require('../module-apple'); +var ModuleOrange = require('../module-orange'); +var ModuleBanana = require('../module-banana'); + +module.exports = function(data) { + var layout = { + module: 'layout-a', + children: [ + { + id: 'slot_1', + module: 'apple', + model: { + title: data.title + } + }, + { + id: 'slot_2', + module: 'banana' + }, + { + id: 'slot_3', + module: 'orange' + } + ] + }; + + return fruitmachine(layout); +}; +},{"../../../../lib/":21,"../layout-a":2,"../module-apple":4,"../module-banana":6,"../module-orange":8}],12:[function(require,module,exports){ +var content = document.querySelector('.js-app_content'); +var View = require('./view'); + +var database = { + title: 'This is the Home page' +}; + +module.exports = function() { + app.view = View(database); + + app.view + .render() + .inject(content); +}; +},{"./view":13}],13:[function(require,module,exports){ +var fruitmachine = require('../../../../lib/'); + +// Require these views so that +// fruitmachine registers them +var LayoutA = require('../layout-a'); +var ModuleApple = require('../module-apple'); +var ModuleOrange = require('../module-orange'); +var ModuleBanana = require('../module-banana'); + +module.exports = function(data) { + var layout = { + module: 'layout-a', + children: [ + { + id: 'slot_1', + module: 'apple', + model: { + title: data.title + } + }, + { + id: 'slot_2', + module: 'orange' + }, + { + id: 'slot_3', + module: 'banana' + } + ] + }; + + return fruitmachine(layout); +}; +},{"../../../../lib/":21,"../layout-a":2,"../module-apple":4,"../module-banana":6,"../module-orange":8}],14:[function(require,module,exports){ +var content = document.querySelector('.js-app_content'); +var View = require('./view'); + +var database = { + title: 'This is the Links page' +}; + +module.exports = function() { + app.view = View(database); + + app.view + .render() + .inject(content); +}; +},{"./view":15}],15:[function(require,module,exports){ +var fruitmachine = require('../../../../lib/'); + +// Require these views so that +// fruitmachine registers them +var LayoutA = require('../layout-a'); +var ModuleApple = require('../module-apple'); +var ModuleOrange = require('../module-orange'); +var ModuleBanana = require('../module-banana'); + +module.exports = function(data) { + var layout = { + module: 'layout-a', + children: [ + { + id: 'slot_1', + module: 'orange' + }, + { + id: 'slot_2', + module: 'apple', + model: { + title: data.title + } + }, + { + id: 'slot_3', + module: 'banana' + } + ] + }; + + return fruitmachine(layout); +}; +},{"../../../../lib/":21,"../layout-a":2,"../module-apple":4,"../module-banana":6,"../module-orange":8}],16:[function(require,module,exports){ +var page = require('page'); +var home = require('../page-home/client'); +var about = require('../page-about/client'); +var links = require('../page-links/client'); + +page('/', home); +page('/about', about); +page('/links', links); + +page({ dispatch: false }); +},{"../page-about/client":10,"../page-home/client":12,"../page-links/client":14,"page":18}],17:[function(require,module,exports){ /* * Copyright 2011 Twitter, Inc. * Licensed under the Apache License, Version 2.0 (the "License"); @@ -250,224 +483,27 @@ var Hogan = {}; })(typeof exports !== 'undefined' ? exports : Hogan); -},{}],2:[function(require,module,exports){ +},{}],18:[function(require,module,exports){ -/*jslint browser:true, node:true*/ +;(function(){ -/** - * FruitMachine Singleton - * - * Renders layouts/modules from a basic layout definition. - * If views require custom interactions devs can extend - * the basic functionality. - * - * @version 0.3.3 - * @copyright The Financial Times Limited [All Rights Reserved] - * @author Wilson Page - */ + /** + * Perform initial dispatch. + */ -'use strict'; + var dispatch = true; -/** - * Module Dependencies - */ + /** + * Base path. + */ -var fruitMachine = require('./fruitmachine'); -var Model = require('model'); + var base = ''; -/** - * Exports - */ + /** + * Running flag. + */ -module.exports = fruitMachine({ Model: Model }); -},{"./fruitmachine":5,"model":6}],7:[function(require,module,exports){ - -/*jslint browser:true, node:true, laxbreak:true*/ - -'use strict'; - -module.exports = function(fm) { - - /** - * Defines a module. - * - * Options: - * - * - `name {String}` the name of the module - * - `tag {String}` the tagName to use for the root element - * - `classes {Array}` a list of classes to add to the root element - * - `template {Function}` the template function to use when rendering - * - `helpers {Array}` a lsit of helpers to apply to the module - * - `initialize {Function}` custom logic to run when module instance created - * - `setup {Function}` custom logic to run when `.setup()` is called (directly or indirectly) - * - `teardown {Function}` custom logic to unbind/undo anything setup introduced (called on `.destroy()` and sometimes on `.setup()` to avoid double binding events) - * - `destroy {Function}` logic to permanently destroy all references - * - * @param {Object|View} props - * @return {View} - * @public true - */ - return function(props) { - var Module = ('object' === typeof props) - ? fm.Module.extend(props) - : props; - - // Allow modules to be named - // via 'name:' or 'module:' - var proto = Module.prototype; - var name = proto.name || proto._module; - - // Store the module by module type - // so that module can be referred to - // by just a string in layout definitions - if (name) fm.modules[name] = Module; - - return Module; - }; -}; - -},{}],8:[function(require,module,exports){ -var content = document.querySelector('.js-app_content'); -var View = require('./view'); - -var database = { - title: 'This is the Home page' -}; - -module.exports = function() { - app.view = View(database); - - app.view - .render() - .inject(content); -}; -},{"./view":9}],10:[function(require,module,exports){ -var content = document.querySelector('.js-app_content'); -var View = require('./view'); - -var database = { - title: 'This is the About page' -}; - -module.exports = function() { - app.view = View(database); - - app.view - .render() - .inject(content); -}; -},{"./view":11}],12:[function(require,module,exports){ -var content = document.querySelector('.js-app_content'); -var View = require('./view'); - -var database = { - title: 'This is the Links page' -}; - -module.exports = function() { - app.view = View(database); - - app.view - .render() - .inject(content); -}; -},{"./view":13}],3:[function(require,module,exports){ -var page = require('page'); -var home = require('../page-home/client'); -var about = require('../page-about/client'); -var links = require('../page-links/client'); - -page('/', home); -page('/about', about); -page('/links', links); - -page({ dispatch: false }); -},{"../page-home/client":8,"../page-about/client":10,"../page-links/client":12,"page":14}],5:[function(require,module,exports){ - -/*jslint browser:true, node:true*/ - -/** - * FruitMachine - * - * Renders layouts/modules from a basic layout definition. - * If views require custom interactions devs can extend - * the basic functionality. - * - * @version 0.3.3 - * @copyright The Financial Times Limited [All Rights Reserved] - * @author Wilson Page - */ - -'use strict'; - -/** - * Module Dependencies - */ - -var mod = require('./module'); -var define = require('./define'); -var utils = require('utils'); -var events = require('event'); - -/** - * Creates a fruitmachine - * - * Options: - * - * - `Model` A model constructor to use (must have `.toJSON()`) - * - * @param {Object} options - */ -module.exports = function(options) { - - /** - * Shortcut method for - * creating lazy views. - * - * @param {Object} options - * @return {View} - */ - function fm(options) { - var Module = fm.modules[options.module]; - if (Module) return new Module(options); - } - - fm.create = module.exports; - fm.Model = options.Model; - fm.Events = events; - fm.Module = mod(fm); - fm.define = define(fm); - fm.util = utils; - fm.modules = {}; - fm.config = { - templateIterator: 'children', - templateInstance: 'child' - }; - - // Mixin events and return - return events(fm); -}; -},{"./define":7,"./module":15,"utils":16,"event":17}],14:[function(require,module,exports){ - -;(function(){ - - /** - * Perform initial dispatch. - */ - - var dispatch = true; - - /** - * Base path. - */ - - var base = ''; - - /** - * Running flag. - */ - - var running; + var running; /** * Register `path` with callback `fn()`, @@ -545,7 +581,8 @@ module.exports = function(options) { if (false !== options.popstate) window.addEventListener('popstate', onpopstate, false); if (false !== options.click) window.addEventListener('click', onclick, false); if (!dispatch) return; - page.replace(location.pathname + location.search, null, true, dispatch); + var url = location.pathname + location.search + location.hash; + page.replace(url, null, true, dispatch); }; /** @@ -624,7 +661,8 @@ module.exports = function(options) { */ function unhandled(ctx) { - if (window.location.pathname + window.location.search == ctx.canonicalPath) return; + var current = window.location.pathname + window.location.search; + if (current == ctx.canonicalPath) return; page.stop(); ctx.unhandled = true; window.location = ctx.canonicalPath; @@ -642,14 +680,24 @@ module.exports = function(options) { function Context(path, state) { if ('/' == path[0] && 0 != path.indexOf(base)) path = base + path; var i = path.indexOf('?'); + this.canonicalPath = path; this.path = path.replace(base, '') || '/'; + this.title = document.title; this.state = state || {}; this.state.path = path; this.querystring = ~i ? path.slice(i + 1) : ''; this.pathname = ~i ? path.slice(0, i) : path; this.params = []; + + // fragment + this.hash = ''; + if (!~this.path.indexOf('#')) return; + var parts = this.path.split('#'); + this.path = parts[0]; + this.hash = parts[1] || ''; + this.querystring = this.querystring.split('#')[0]; } /** @@ -722,7 +770,7 @@ module.exports = function(options) { return function(ctx, next){ if (self.match(ctx.path, ctx.params)) return fn(ctx, next); next(); - } + }; }; /** @@ -798,7 +846,7 @@ module.exports = function(options) { .replace(/([\/.])/g, '\\$1') .replace(/\*/g, '(.*)'); return new RegExp('^' + path + '$', sensitive ? '' : 'i'); - }; + } /** * Handle "populate" events. @@ -825,19 +873,22 @@ module.exports = function(options) { while (el && 'A' != el.nodeName) el = el.parentNode; if (!el || 'A' != el.nodeName) return; - // ensure non-hash - var href = el.href; - var path = el.pathname + el.search; - if (el.hash || '#' == el.getAttribute('href')) return; + // ensure non-hash for the same path + var link = el.getAttribute('href'); + if (el.pathname == location.pathname && (el.hash || '#' == link)) return; // check target if (el.target) return; // x-origin - if (!sameOrigin(href)) return; + if (!sameOrigin(el.href)) return; + + // rebuild path + var path = el.pathname + el.search + (el.hash || ''); // same page - var orig = path; + var orig = path + el.hash; + path = path.replace(base, ''); if (base && orig == path) return; @@ -878,446 +929,234 @@ module.exports = function(options) { })(); -},{}],16:[function(require,module,exports){ +},{}],19:[function(require,module,exports){ -/*jshint browser:true, node:true*/ +/*jslint browser:true, node:true, laxbreak:true*/ 'use strict'; -exports.bind = function(method, context) { - return function() { return method.apply(context, arguments); }; -}; +module.exports = function(fm) { -exports.isArray = function(arg) { - return arg instanceof Array; -}, + /** + * Defines a module. + * + * Options: + * + * - `name {String}` the name of the module + * - `tag {String}` the tagName to use for the root element + * - `classes {Array}` a list of classes to add to the root element + * - `template {Function}` the template function to use when rendering + * - `helpers {Array}` a list of helpers to apply to the module + * - `initialize {Function}` custom logic to run when module instance created + * - `setup {Function}` custom logic to run when `.setup()` is called (directly or indirectly) + * - `teardown {Function}` custom logic to unbind/undo anything setup introduced (called on `.destroy()` and sometimes on `.setup()` to avoid double binding events) + * - `destroy {Function}` logic to permanently destroy all references + * + * @param {Object|View} props + * @return {View} + * @public true + */ + return function(props) { + var Module = ('object' === typeof props) + ? fm.Module.extend(props) + : props; -exports.mixin = function(original, source) { - for (var key in source) original[key] = source[key]; - return original; -}, + // Allow modules to be named + // via 'name:' or 'module:' + var proto = Module.prototype; + var name = proto.name || proto._module; -exports.byId = function(id, el) { - if (el) return el.querySelector('#' + id); -}, + // Store the module by module type + // so that module can be referred to + // by just a string in layout definitions + if (name) fm.modules[name] = Module; + + return Module; + }; +}; + +},{}],20:[function(require,module,exports){ +/*jslint browser:true, node:true*/ /** - * Inserts an item into an array. - * Has the option to state an index. + * FruitMachine * - * @param {*} item - * @param {Array} array - * @param {Number} index - * @return void - */ -exports.insert = function(item, array, index) { - if (typeof index !== 'undefined') { - array.splice(index, 0, item); - } else { - array.push(item); - } -}, - -exports.toNode = function(html) { - var el = document.createElement('div'); - el.innerHTML = html; - return el.removeChild(el.firstElementChild); -}, - -// Determine if we have a DOM -// in the current environment. -exports.hasDom = function() { - return typeof document !== 'undefined'; -}; - -var i = 0; -exports.uniqueId = function(prefix, suffix) { - prefix = prefix || 'id'; - suffix = suffix || 'a'; - return [prefix, (++i) * Math.round(Math.random() * 100000), suffix].join('-'); -}; - -exports.keys = function(object) { - var keys = []; - for (var key in object) keys.push(key); - return keys; -}; - -exports.isPlainObject = function(ob) { - if (!ob) return false; - var c = (ob.constructor || '').toString(); - return !!~c.indexOf('Object'); -}; -},{}],17:[function(require,module,exports){ - -/** - * Event - * - * A super lightweight - * event emitter library. - * - * @version 0.1.4 - * @author Wilson Page + * Renders layouts/modules from a basic layout definition. + * If views require custom interactions devs can extend + * the basic functionality. + * + * @version 0.3.3 + * @copyright The Financial Times Limited [All Rights Reserved] + * @author Wilson Page */ -/** - * Locals - */ - -var proto = Event.prototype; +'use strict'; /** - * Expose `Event` + * Module Dependencies */ -module.exports = Event; +var mod = require('./module'); +var define = require('./define'); +var utils = require('utils'); +var events = require('event'); /** - * Creates a new event emitter - * instance, or if passed an - * object, mixes the event logic - * into it. + * Creates a fruitmachine * - * @param {Object} obj - * @return {Object} - */ -function Event(obj) { - if (!(this instanceof Event)) return new Event(obj); - if (obj) return mixin(obj, proto); -} - -/** - * Registers a callback - * with an event name. + * Options: * - * @param {String} name - * @param {Function} cb - * @return {Event} - */ -proto.on = function(name, cb) { - this._cbs = this._cbs || {}; - (this._cbs[name] || (this._cbs[name] = [])).unshift(cb); - return this; -}; - -/** - * Removes a single callback, - * or all callbacks associated - * with the passed event name. + * - `Model` A model constructor to use (must have `.toJSON()`) * - * @param {String} name - * @param {Function} cb - * @return {Event} + * @param {Object} options */ -proto.off = function(name, cb) { - this._cbs = this._cbs || {}; +module.exports = function(options) { - if (!name) return this._cbs = {}; - if (!cb) return delete this._cbs[name]; + /** + * Shortcut method for + * creating lazy views. + * + * @param {Object} options + * @return {Module} + */ + function fm(options) { + var Module = fm.modules[options.module]; + if (Module) return new Module(options); + } - var cbs = this._cbs[name] || []; - var i; + fm.create = module.exports; + fm.Model = options.Model; + fm.Events = events; + fm.Module = mod(fm); + fm.define = define(fm); + fm.util = utils; + fm.modules = {}; + fm.config = { + templateIterator: 'children', + templateInstance: 'child' + }; - while (cbs && ~(i = cbs.indexOf(cb))) cbs.splice(i, 1); - return this; + // Mixin events and return + return events(fm); }; -/** - * Fires an event. Which triggers - * all callbacks registered on this - * event name. - * - * @param {String} name - * @return {Event} - */ -proto.fire = function(options) { - this._cbs = this._cbs || {}; - var name = options.name || options; - var ctx = options.ctx || this; - var cbs = this._cbs[name]; +},{"./define":19,"./module":23,"event":24,"utils":28}],21:[function(require,module,exports){ - if (cbs) { - var args = [].slice.call(arguments, 1); - var l = cbs.length; - while (l--) cbs[l].apply(ctx, args); - } - - return this; -}; - -/** - * Util - */ +/*jslint browser:true, node:true*/ /** - * Mixes in the properties - * of the second object into - * the first. + * FruitMachine Singleton * - * @param {Object} a - * @param {Object} b - * @return {Object} + * Renders layouts/modules from a basic layout definition. + * If views require custom interactions devs can extend + * the basic functionality. + * + * @version 0.3.3 + * @copyright The Financial Times Limited [All Rights Reserved] + * @author Wilson Page */ -function mixin(a, b) { - for (var key in b) a[key] = b[key]; - return a; -} -},{}],9:[function(require,module,exports){ -var fruitmachine = require('../../../../lib/'); - -// Require these views so that -// fruitmachine registers them -var LayoutA = require('../layout-a'); -var ModuleApple = require('../module-apple'); -var ModuleOrange = require('../module-orange'); -var ModuleBanana = require('../module-banana'); - -module.exports = function(data) { - var layout = { - module: 'layout-a', - children: [ - { - id: 'slot_1', - module: 'apple', - model: { - title: data.title - } - }, - { - id: 'slot_2', - module: 'orange' - }, - { - id: 'slot_3', - module: 'banana' - } - ] - }; - - return fruitmachine(layout); -}; -},{"../../../../lib/":2,"../layout-a":18,"../module-apple":19,"../module-orange":20,"../module-banana":21}],11:[function(require,module,exports){ -var fruitmachine = require('../../../../lib/'); - -// Require these views so that -// fruitmachine registers them -var LayoutA = require('../layout-a'); -var ModuleApple = require('../module-apple'); -var ModuleOrange = require('../module-orange'); -var ModuleBanana = require('../module-banana'); - -module.exports = function(data) { - var layout = { - module: 'layout-a', - children: [ - { - id: 'slot_1', - module: 'apple', - model: { - title: data.title - } - }, - { - id: 'slot_2', - module: 'banana' - }, - { - id: 'slot_3', - module: 'orange' - } - ] - }; - - return fruitmachine(layout); -}; -},{"../../../../lib/":2,"../layout-a":18,"../module-apple":19,"../module-orange":20,"../module-banana":21}],13:[function(require,module,exports){ -var fruitmachine = require('../../../../lib/'); - -// Require these views so that -// fruitmachine registers them -var LayoutA = require('../layout-a'); -var ModuleApple = require('../module-apple'); -var ModuleOrange = require('../module-orange'); -var ModuleBanana = require('../module-banana'); - -module.exports = function(data) { - var layout = { - module: 'layout-a', - children: [ - { - id: 'slot_1', - module: 'orange' - }, - { - id: 'slot_2', - module: 'apple', - model: { - title: data.title - } - }, - { - id: 'slot_3', - module: 'banana' - } - ] - }; - - return fruitmachine(layout); -}; -},{"../../../../lib/":2,"../layout-a":18,"../module-apple":19,"../module-orange":20,"../module-banana":21}],22:[function(require,module,exports){ 'use strict'; /** - * Locals + * Module Dependencies */ -var has = {}.hasOwnProperty; +var fruitMachine = require('./fruitmachine'); +var Model = require('model'); /** * Exports */ -module.exports = function(main) { - var args = arguments; - var l = args.length; - var i = 0; - var src; - var key; - - while (++i < l) { - src = args[i]; - for (key in src) { - if (has.call(src, key)) { - main[key] = src[key]; - } - } - } - - return main; -}; - -},{}],6:[function(require,module,exports){ - -'use strict'; +module.exports = fruitMachine({ Model: Model }); +},{"./fruitmachine":20,"model":26}],22:[function(require,module,exports){ /** * Module Dependencies */ var events = require('event'); -var mixin = require('mixin'); /** * Exports */ -module.exports = Model; - -/** - * Locals - */ - -var proto = Model.prototype; - -/** - * Model constructor. - * - * @constructor - * @param {Object} data - * @api public - */ -function Model(data) { - this._data = mixin({}, data); -} - -/** - * Gets a value by key - * - * If no key is given, the - * whole model is returned. - * - * @param {String} key - * @return {*} - * @api public - */ -proto.get = function(key) { - return key - ? this._data[key] - : this._data; -}; - /** - * Sets data on the model. - * - * Accepts either a key and - * value, or an object literal. - * - * @param {String|Object} key - * @param {*|undefined} value + * Registers a event listener. + * + * @param {String} name + * @param {String} module + * @param {Function} cb + * @return {View} */ -proto.set = function(data, value) { +exports.on = function(name, module, cb) { - // If a string key is passed - // with a value. Set the value - // on the key in the data store. - if ('string' === typeof data && typeof value !== 'undefined') { - this._data[data] = value; - this.fire('change:' + data, value); + // cb can be passed as + // the second or third argument + if (arguments.length === 2) { + cb = module; + module = null; } - // Merge the object into the data store - if ('object' === typeof data) { - mixin(this._data, data); - for (var prop in data) this.fire('change:' + prop, data[prop]); + // if a module is provided + // pass in a special callback + // function that checks the + // module + if (module) { + events.prototype.on.call(this, name, function() { + if (this.event.target.module() === module) { + cb.apply(this, arguments); + } + }); + } else { + events.prototype.on.call(this, name, cb); } - // Always fire a - // generic change event - this.fire('change'); - - // Allow chaining return this; }; /** - * CLears the data store. + * Fires an event on a view. * - * @return {Model} + * @param {String} name + * @return {View} */ -proto.clear = function() { - this._data = {}; - this.fire('change'); +exports.fire = function(name) { + var _event = this.event; + var event = { + target: this, + propagate: true, + stopPropagation: function(){ this.propagate = false; } + }; + + propagate(this, arguments, event); + + // COMPLEX: + // If an earlier event object was + // cached, restore the the event + // back onto the view. If there + // wasn't an earlier event, make + // sure the `event` key has been + // deleted off the view. + if (_event) this.event = _event; + else delete this.event; // Allow chaining return this; }; -/** - * Deletes the data store. - * - * @return {undefined} - */ -proto.destroy = function() { - for (var key in this._data) this._data[key] = null; - delete this._data; - this.fire('destroy'); -}; - -/** - * Returns a shallow - * clone of the data store. - * - * @return {Object} - */ -proto.toJSON = function() { - return mixin({}, this._data); -}; +function propagate(view, args, event) { + if (!view || !event.propagate) return; -// Mixin events -events(proto); + view.event = event; + events.prototype.fire.apply(view, args); + propagate(view.parent, args, event); +} -},{"mixin":22,"event":17}],15:[function(require,module,exports){ +exports.fireStatic = events.prototype.fire; +exports.off = events.prototype.off; +},{"event":24}],23:[function(require,module,exports){ /*jshint browser:true, node:true*/ @@ -1386,11 +1225,11 @@ module.exports = function(fm) { proto._configure = function(options) { // Setup static properties - this._id = options.id || util.uniqueId('id-'); + this._id = options.id || util.uniqueId(); this._fmid = options.fmid || util.uniqueId('fmid'); this.tag = options.tag || this.tag || 'div'; - this.classes = this.classes || options.classes || []; - this.helpers = this.helpers || options.helpers || []; + this.classes = options.classes || this.classes || []; + this.helpers = options.helpers || this.helpers || []; this.template = this._setTemplate(options.template || this.template); this.slot = options.slot; @@ -2145,36 +1984,56 @@ module.exports = function(fm) { }; /** - * Returns a JSON represention of + * @deprecated + */ + proto.toJSON = function(options) { + return this.serialize(options); + }; + + /** + * Returns a serialized represention of * a FruitMachine Module. This can * be generated serverside and - * passed into new FruitMachine(json) + * passed into new FruitMachine(serialized) * to inflate serverside rendered * views. * + * Options: + * + * - `inflatable` Whether the returned object should retain references to DOM ids for use with client-side inflation of views + * + * @param {Object} options * @return {Object} * @api public */ - proto.toJSON = function() { - var json = {}; - json.children = []; + proto.serialize = function(options) { + var serialized = {}; + + // Shallow clone the options + options = mixin({ + inflatable: true + }, options); + + serialized.children = []; // Recurse this.each(function(child) { - json.children.push(child.toJSON()); + serialized.children.push(child.serialize()); }); - json.id = this.id(); - json.fmid = this._fmid; - json.module = this.module(); - json.model = this.model.toJSON(); - json.slot = this.slot; + serialized.id = this.id(); + serialized.module = this.module(); + serialized.model = this.model.toJSON(); + serialized.slot = this.slot; + + if (options.inflatable) serialized.fmid = this._fmid; // Fire a hook to allow third - // parties to alter the json output - this.fireStatic('tojson', json); + // parties to alter the output + this.fireStatic('tojson', serialized); // @deprecated + this.fireStatic('serialize', serialized); - return json; + return serialized; }; // Events @@ -2194,170 +2053,121 @@ module.exports = function(fm) { return Module; }; -},{"./events":23,"utils":16,"extend":24}],18:[function(require,module,exports){ - -/** - * Module Dependencies - */ - -var fm = require('../../../../lib/'); -var template = require('./template'); - -/** - * Exports - */ - -module.exports = fm.define({ - module: 'layout-a', - template: template -}); -},{"./template":25,"../../../../lib/":2}],19:[function(require,module,exports){ - -/** - * Module Dependencies - */ - -var fm = require('../../../../lib/'); -var template = require('./template'); - -/** - * Exports - */ - -module.exports = fm.define({ - module: 'apple', - template: template -}); -},{"./template":26,"../../../../lib/":2}],20:[function(require,module,exports){ - -/** - * Module Dependencies - */ - -var fm = require('../../../../lib/'); -var template = require('./template'); +},{"./events":22,"extend":25,"utils":28}],24:[function(require,module,exports){ /** - * Exports + * Event + * + * A super lightweight + * event emitter library. + * + * @version 0.1.4 + * @author Wilson Page */ -module.exports = fm.define({ - module: 'orange', - template: template -}); -},{"./template":27,"../../../../lib/":2}],21:[function(require,module,exports){ - /** - * Module Dependencies + * Locals */ -var fm = require('../../../../lib/'); -var template = require('./template'); +var proto = Event.prototype; /** - * Exports + * Expose `Event` */ -module.exports = fm.define({ - module: 'banana', - template: template -}); -},{"./template":28,"../../../../lib/":2}],25:[function(require,module,exports){ -module.exports = new Hogan(function(c,p,i){var _=this;_.b(i=i||"");_.b("
");_.b("\n" + i);_.b("
");_.b(_.t(_.f("slot_1",c,p,0)));_.b("
");_.b("\n" + i);_.b("
");_.b("\n" + i);_.b("
");_.b(_.t(_.f("slot_2",c,p,0)));_.b("
");_.b("\n" + i);_.b("
");_.b(_.t(_.f("slot_3",c,p,0)));_.b("
");_.b("\n" + i);_.b("
");_.b("\n" + i);_.b("
");return _.fl();;}); -},{}],26:[function(require,module,exports){ -module.exports = new Hogan(function(c,p,i){var _=this;_.b(i=i||"");_.b("
");_.b("\n" + i);_.b("
");_.b(_.v(_.f("title",c,p,0)));_.b("
");_.b("\n" + i);_.b("
");return _.fl();;}); -},{}],27:[function(require,module,exports){ -module.exports = new Hogan(function(c,p,i){var _=this;_.b(i=i||"");_.b("
Module Orange
");return _.fl();;}); -},{}],28:[function(require,module,exports){ -module.exports = new Hogan(function(c,p,i){var _=this;_.b(i=i||"");_.b("
Module Banana
");return _.fl();;}); -},{}],23:[function(require,module,exports){ +module.exports = Event; /** - * Module Dependencies + * Creates a new event emitter + * instance, or if passed an + * object, mixes the event logic + * into it. + * + * @param {Object} obj + * @return {Object} */ - -var events = require('event'); +function Event(obj) { + if (!(this instanceof Event)) return new Event(obj); + if (obj) return mixin(obj, proto); +} /** - * Exports + * Registers a callback + * with an event name. + * + * @param {String} name + * @param {Function} cb + * @return {Event} */ +proto.on = function(name, cb) { + this._cbs = this._cbs || {}; + (this._cbs[name] || (this._cbs[name] = [])).unshift(cb); + return this; +}; /** - * Registers a event listener. + * Removes a single callback, + * or all callbacks associated + * with the passed event name. * * @param {String} name - * @param {String} module * @param {Function} cb - * @return {View} + * @return {Event} */ -exports.on = function(name, module, cb) { +proto.off = function(name, cb) { + this._cbs = this._cbs || {}; - // cb can be passed as - // the second or third argument - if (arguments.length === 2) { - cb = module; - module = null; - } + if (!name) return this._cbs = {}; + if (!cb) return delete this._cbs[name]; - // if a module is provided - // pass in a special callback - // function that checks the - // module - if (module) { - events.prototype.on.call(this, name, function() { - if (this.event.target.module() === module) { - cb.apply(this, arguments); - } - }); - } else { - events.prototype.on.call(this, name, cb); - } + var cbs = this._cbs[name] || []; + var i; + while (cbs && ~(i = cbs.indexOf(cb))) cbs.splice(i, 1); return this; }; /** - * Fires an event on a view. + * Fires an event. Which triggers + * all callbacks registered on this + * event name. * * @param {String} name - * @return {View} + * @return {Event} */ -exports.fire = function(name) { - var parent = this.parent; - var _event = this.event; - var event = { - target: this, - propagate: true, - stopPropagation: function(){ this.propagate = false; } - }; - - propagate(this, arguments, event); +proto.fire = function(options) { + this._cbs = this._cbs || {}; + var name = options.name || options; + var ctx = options.ctx || this; + var cbs = this._cbs[name]; - // COMPLEX: - // If an earlier event object was - // cached, restore the the event - // back onto the view. If there - // wasn't an earlier event, make - // sure the `event` key has been - // deleted off the view. - if (_event) this.event = _event; - else delete this.event; + if (cbs) { + var args = [].slice.call(arguments, 1); + var l = cbs.length; + while (l--) cbs[l].apply(ctx, args); + } - // Allow chaining return this; }; -function propagate(view, args, event) { - if (!view || !event.propagate) return; +/** + * Util + */ - view.event = event; - events.prototype.fire.apply(view, args); - propagate(view.parent, args, event); +/** + * Mixes in the properties + * of the second object into + * the first. + * + * @param {Object} a + * @param {Object} b + * @return {Object} + */ +function mixin(a, b) { + for (var key in b) a[key] = b[key]; + return a; } - -exports.fireStatic = events.prototype.fire; -exports.off = events.prototype.off; -},{"event":17}],24:[function(require,module,exports){ +},{}],25:[function(require,module,exports){ /*jshint browser:true, node:true*/ 'use strict'; @@ -2427,5 +2237,225 @@ function protect(keys, ob) { } } } -},{"utils":16}]},{},[1]) -; \ No newline at end of file +},{"utils":28}],26:[function(require,module,exports){ + +'use strict'; + +/** + * Module Dependencies + */ + +var events = require('event'); +var mixin = require('mixin'); + +/** + * Exports + */ + +module.exports = Model; + +/** + * Locals + */ + +var proto = Model.prototype; + +/** + * Model constructor. + * + * @constructor + * @param {Object} data + * @api public + */ +function Model(data) { + this._data = mixin({}, data); +} + +/** + * Gets a value by key + * + * If no key is given, the + * whole model is returned. + * + * @param {String} key + * @return {*} + * @api public + */ +proto.get = function(key) { + return key + ? this._data[key] + : this._data; +}; + +/** + * Sets data on the model. + * + * Accepts either a key and + * value, or an object literal. + * + * @param {String|Object} key + * @param {*|undefined} value + */ +proto.set = function(data, value) { + + // If a string key is passed + // with a value. Set the value + // on the key in the data store. + if ('string' === typeof data && typeof value !== 'undefined') { + this._data[data] = value; + this.fire('change:' + data, value); + } + + // Merge the object into the data store + if ('object' === typeof data) { + mixin(this._data, data); + for (var prop in data) this.fire('change:' + prop, data[prop]); + } + + // Always fire a + // generic change event + this.fire('change'); + + // Allow chaining + return this; +}; + +/** + * CLears the data store. + * + * @return {Model} + */ +proto.clear = function() { + this._data = {}; + this.fire('change'); + + // Allow chaining + return this; +}; + +/** + * Deletes the data store. + * + * @return {undefined} + */ +proto.destroy = function() { + for (var key in this._data) this._data[key] = null; + delete this._data; + this.fire('destroy'); +}; + +/** + * Returns a shallow + * clone of the data store. + * + * @return {Object} + */ +proto.toJSON = function() { + return mixin({}, this._data); +}; + +// Mixin events +events(proto); + +},{"event":24,"mixin":27}],27:[function(require,module,exports){ + +'use strict'; + +/** + * Locals + */ + +var has = {}.hasOwnProperty; + +/** + * Exports + */ + +module.exports = function(main) { + var args = arguments; + var l = args.length; + var i = 0; + var src; + var key; + + while (++i < l) { + src = args[i]; + for (key in src) { + if (has.call(src, key)) { + main[key] = src[key]; + } + } + } + + return main; +}; + +},{}],28:[function(require,module,exports){ + +/*jshint browser:true, node:true*/ + +'use strict'; + +exports.bind = function(method, context) { + return function() { return method.apply(context, arguments); }; +}; + +exports.isArray = function(arg) { + return arg instanceof Array; +}, + +exports.mixin = function(original, source) { + for (var key in source) original[key] = source[key]; + return original; +}, + +exports.byId = function(id, el) { + if (el) return el.querySelector('#' + id); +}, + +/** + * Inserts an item into an array. + * Has the option to state an index. + * + * @param {*} item + * @param {Array} array + * @param {Number} index + * @return void + */ +exports.insert = function(item, array, index) { + if (typeof index !== 'undefined') { + array.splice(index, 0, item); + } else { + array.push(item); + } +}, + +exports.toNode = function(html) { + var el = document.createElement('div'); + el.innerHTML = html; + return el.removeChild(el.firstElementChild); +}, + +// Determine if we have a DOM +// in the current environment. +exports.hasDom = function() { + return typeof document !== 'undefined'; +}; + +var i = 0; +exports.uniqueId = function(prefix) { + return (prefix || 'id') + ((++i) * Math.round(Math.random() * 100000)); +}; + +exports.keys = function(object) { + var keys = []; + for (var key in object) keys.push(key); + return keys; +}; + +exports.isPlainObject = function(ob) { + if (!ob) return false; + var c = (ob.constructor || '').toString(); + return !!~c.indexOf('Object'); +}; +},{}]},{},[1]) \ No newline at end of file diff --git a/examples/express/lib/page-about/server.js b/examples/express/lib/page-about/server.js index 229ca29..3748282 100644 --- a/examples/express/lib/page-about/server.js +++ b/examples/express/lib/page-about/server.js @@ -10,7 +10,7 @@ var database = { module.exports = function(req, res){ var view = View(database); - res.expose(view.toJSON(), 'layout'); + res.expose(view.serialize(), 'layout'); res.render('wrapper', { title: 'About', body: view.toHTML() diff --git a/examples/express/lib/page-home/server.js b/examples/express/lib/page-home/server.js index 02a3a00..4724de7 100644 --- a/examples/express/lib/page-home/server.js +++ b/examples/express/lib/page-home/server.js @@ -10,7 +10,7 @@ var database = { module.exports = function(req, res){ var view = View(database); - res.expose(view.toJSON(), 'layout'); + res.expose(view.serialize(), 'layout'); res.render('wrapper', { title: 'Home', body: view.toHTML() diff --git a/examples/express/lib/page-links/server.js b/examples/express/lib/page-links/server.js index 0b4053b..ec7522e 100644 --- a/examples/express/lib/page-links/server.js +++ b/examples/express/lib/page-links/server.js @@ -10,7 +10,7 @@ var database = { module.exports = function(req, res){ var view = View(database); - res.expose(view.toJSON(), 'layout'); + res.expose(view.serialize(), 'layout'); res.render('wrapper', { title: 'Links', body: view.toHTML() diff --git a/lib/module/index.js b/lib/module/index.js index 10e10e3..41b961a 100644 --- a/lib/module/index.js +++ b/lib/module/index.js @@ -825,36 +825,56 @@ module.exports = function(fm) { }; /** - * Returns a JSON represention of + * @deprecated + */ + proto.toJSON = function(options) { + return this.serialize(options); + }; + + /** + * Returns a serialized represention of * a FruitMachine Module. This can * be generated serverside and - * passed into new FruitMachine(json) + * passed into new FruitMachine(serialized) * to inflate serverside rendered * views. * + * Options: + * + * - `inflatable` Whether the returned object should retain references to DOM ids for use with client-side inflation of views + * + * @param {Object} options * @return {Object} * @api public */ - proto.toJSON = function() { - var json = {}; - json.children = []; + proto.serialize = function(options) { + var serialized = {}; + + // Shallow clone the options + options = mixin({ + inflatable: true + }, options); + + serialized.children = []; // Recurse this.each(function(child) { - json.children.push(child.toJSON()); + serialized.children.push(child.serialize()); }); - json.id = this.id(); - json.fmid = this._fmid; - json.module = this.module(); - json.model = this.model.toJSON(); - json.slot = this.slot; + serialized.id = this.id(); + serialized.module = this.module(); + serialized.model = this.model.toJSON(); + serialized.slot = this.slot; + + if (options.inflatable) serialized.fmid = this._fmid; // Fire a hook to allow third - // parties to alter the json output - this.fireStatic('tojson', json); + // parties to alter the output + this.fireStatic('tojson', serialized); // @deprecated + this.fireStatic('serialize', serialized); - return json; + return serialized; }; // Events diff --git a/test/tests/module.serialize.js b/test/tests/module.serialize.js new file mode 100644 index 0000000..ef6346d --- /dev/null +++ b/test/tests/module.serialize.js @@ -0,0 +1,122 @@ + +buster.testCase('View#serialize()', { + + setUp: helpers.createView, + + "Should return an object": function() { + var apple = new Apple(); + var json = apple.serialize(); + + assert(json instanceof Object); + }, + + "Should call serialize of child": function() { + var apple = new Apple(); + var orange = new Orange(); + var spy = this.spy(orange, 'serialize'); + + apple.add(orange); + + apple.serialize(); + assert(spy.called); + }, + + "Should add the id": function() { + var apple = new Apple(); + var json = apple.serialize(); + + assert(json.id); + }, + + "Should add the fmid by default": function() { + var apple = new Apple(); + var json = apple.serialize(); + + assert(json.fmid); + }, + + "Should omit the fmid if inflatable is false": function() { + var apple = new Apple(); + var json = apple.serialize({inflatable: false}); + + refute.defined(json.fmid); + }, + + "Should add the module name": function() { + var apple = new Apple(); + var json = apple.serialize(); + + assert.equals(json.module, 'apple'); + }, + + "Should add the slot": function() { + var json = this.view.serialize(); + + refute.defined(json.slot); + assert.same(1, json.children[0].slot); + assert.same(2, json.children[1].slot); + assert.same(3, json.children[2].slot); + }, + + "Should call toJSON of model": function() { + var apple = new Apple(); + var model = { + toJSON: function() {} + }; + var spy = this.spy(model, 'toJSON'); + + apple.model = model; + + apple.serialize(); + assert(spy.called); + }, + + "Should fire `serialize` event": function() { + var apple = new Apple(); + var spy = this.spy(); + + apple.on('serialize', spy); + apple.serialize(); + + assert(spy.called); + }, + + "Should be able to manipulate json output via `serialize` event": function() { + var apple = new Apple(); + + apple.on('serialize', function(json) { + json.test = 'data'; + }); + + var json = apple.serialize(); + + assert.equals(json.test, 'data'); + }, + + "Should be able to inflate the output": function() { + var sandbox = helpers.createSandbox(); + var layout = new Layout({ + children: { + 1: { module: 'apple' } + } + }); + + layout + .render() + .inject(sandbox) + .setup(); + + var layoutEl = layout.el; + var appleEl = layout.module('apple').el; + var json = layout.serialize(); + var inflated = fruitmachine(json); + + inflated.setup(); + + var layoutElInflated = inflated.el; + var appleElInflated = inflated.module('apple').el; + + assert.equals(layoutEl, layoutElInflated); + assert.equals(appleEl, appleElInflated); + } +}); \ No newline at end of file diff --git a/test/tests/module.toJSON.js b/test/tests/module.toJSON.js deleted file mode 100644 index 35cf748..0000000 --- a/test/tests/module.toJSON.js +++ /dev/null @@ -1,59 +0,0 @@ - -buster.testCase('View#toJSON()', { - - "Should return an fmid": function() { - var apple = new Apple(); - var json = apple.toJSON(); - - assert(json.fmid); - }, - - "Should fire `tojson` event": function() { - var apple = new Apple(); - var spy = this.spy(); - - apple.on('tojson', spy); - apple.toJSON(); - - assert(spy.called); - }, - - "Should be able to manipulate json output via `tojson` event": function() { - var apple = new Apple(); - - apple.on('tojson', function(json) { - json.test = 'data'; - }); - - var json = apple.toJSON(); - - assert.equals(json.test, 'data'); - }, - - "Should be able to inflate the output": function() { - var sandbox = helpers.createSandbox(); - var layout = new Layout({ - children: { - 1: { module: 'apple' } - } - }); - - layout - .render() - .inject(sandbox) - .setup(); - - var layoutEl = layout.el; - var appleEl = layout.module('apple').el; - var json = layout.toJSON(); - var inflated = fruitmachine(json); - - inflated.setup(); - - var layoutElInflated = inflated.el; - var appleElInflated = inflated.module('apple').el; - - assert.equals(layoutEl, layoutElInflated); - assert.equals(appleEl, appleElInflated); - } -}); \ No newline at end of file