lazo-next
Version:
A client-server web framework built on Node.js that allows front-end developers to easily create a 100% SEO compliant, component MVC structured web application with an optimized first page load.
418 lines (353 loc) • 13.8 kB
JavaScript
define([
'underscore',
'backbone',
'base',
'resolver/main',
'utils/module',
'lazoCollectionView',
'lazoView',
'utils/ctlSerializor',
'context',
'async',
'utils/template',
'l!viewManager',
'utils/prune',
'bundler',
'jquery',
'l!jquerycookie'],
function (
_,
Backbone,
Base,
resolver,
module,
LazoCollectionView,
LazoBaseView,
serializor,
Context,
async,
template,
viewManager,
prune,
Bundler,
$) {
'use strict';
var bundle = new Bundler();
var Controller = Base.extend(_.extend({
constructor: function (options) {
if (!options) {
throw new TypeError('The options parameter is mandatory.'); // TODO: error handling
}
if (!options.name || typeof options.name !== 'string') {
throw new TypeError('The options.name parameter is mandatory.'); // TODO: error handling
}
this.cid = _.uniqueId(options.name);
this.ctx = options.ctx;
this.name = options.name;
},
orderedCss: [],
addChild: function (container, cmpName, options) {
module.addChild(container, cmpName, this, options);
},
index: function (options) {
return options.success('index');
},
navigate: function (action, options) {
var self = this;
options = options || {};
options = _.defaults(options || {}, { error: function () {
return;
}, success: function () {
return;
}});
// create a new context on navigation;
// inherit all previous properties that
// will not change and are not created by the
// context
this.ctx = new Context({
_rootCtx: this.ctx._rootCtx,
params: this.ctx.params,
assets: this.ctx.assets,
css: this.ctx.css,
headers: this.ctx.headers,
meta: this.ctx.meta,
});
if (LAZO.app.isClient) {
this._getEl().addClass('lazo-navigating');
}
this._execute(action, {
ctlNavigate: true,
error: function (err) {
return options.error(err);
},
success: function () {
setTimeout(function () {
viewManager.attachViews(self, function (err, resp) {
if (err) {
return options.error(err);
}
options.success(resp);
if (LAZO.app.isClient) {
self._getEl().removeClass('lazo-navigating').addClass('lazo-navigated');
}
});
}, 0);
}
});
},
transition: function (view, options) {
var self = this;
if (LAZO.app.isClient && this.currentView) {
viewManager.cleanup(this, this.currentView.cid);
}
this.currentView = view;
// only render html if ctl.navigate was called
// TODO: ensure views are getting attached
if (LAZO.app.isClient && options.ctlNavigate) {
view.getHtml({
error: options.error,
success: function (html) {
$('[lazo-cmp-id="' + self.cid + '"]').html(html);
options.success(self);
}
});
} else {
options.success(self);
}
},
setCookie: function (name, value, options) {
this.ctx.setCookie(name, value, options);
},
clearCookie: function (name, options) {
this.ctx.clearCookie(name, options);
},
loadModel: function (modelName, options) {
LAZO.app.loadModel(modelName, _.extend(options, { ctx: this.ctx }));
},
loadCollection: function (collectionName, options) {
LAZO.app.loadCollection(collectionName, _.extend(options, { ctx: this.ctx }));
},
createModel: function (modelName, attributes, options) {
LAZO.app.createModel(modelName, attributes, _.extend(options, { ctx: this.ctx }));
},
createCollection: function (collectionName, attributes, options) {
LAZO.app.createCollection(collectionName, attributes, _.extend(options, { ctx: this.ctx }));
},
setSharedData: function (key, val) {
this.ctx.setSharedData(key, val);
return this;
},
getSharedData: function (key) {
return this.ctx.getSharedData(key);
},
toJSON: function (rootCtx) {
return serializor.serialize(this, rootCtx);
},
serialize: function () {
return JSON.stringify(this.toJSON());
},
setPageTitle: function (title) {
this.ctx._rootCtx.pageTitle = title;
return this;
},
getPageTitle: function () {
return this.ctx._rootCtx.pageTitle;
},
getPath: function () {
var parent = this.parent;
var path = [];
var node = this;
if (!parent) {
return [{ name: this.name, container: null, position: 0 }];
}
function getPosition(node, parent) {
var children = parent.children;
for (var k in children) {
for (var i = 0; i < children[k].length; i++) {
if (children[k][i] === node) {
return { name: node.name, container: k, position: i };
}
}
}
}
while (parent) {
path.unshift(getPosition(node, parent));
node = parent;
parent = parent.parent;
}
path.unshift({ name: node.name, container: null, position: 0 });
return path;
},
augmentImportLink: function (link) {
return link;
},
augmentCssLink: function (link) {
return link;
},
getImport: function (relativePath) {
return bundle.resolveImport(relativePath, this.name);
},
_getEl: function () {
if (LAZO.app.isClient) {
return $('[lazo-cmp-id="' + this.cid + '"]');
}
},
_execute: function (action, options) {
var tasks = this._buildExecTaskList(action);
var self = this;
async.waterfall(tasks, function (err, view) {
if (err) {
options.error(err);
} else {
self.transition(view, options);
}
});
},
_createView: function (View, options) {
try {
return new View(_.extend({
ref: resolver.getPath(options.name, this.name, 'view'),
basePath: resolver.getBasePath(options.name, this.name, 'view'),
ctl: this
}, options));
} catch (e) {
throw e;
}
},
_buildExecTaskList: function (action) {
var tasks = [];
var self = this;
tasks.push(function (callback) {
callback(null, action);
});
tasks.push(_.bind(this._action, this));
tasks.push(_.bind(this._getCss, this));
tasks.push(_.bind(this._getImports, this));
tasks.push(_.bind(this._getView, this));
return tasks;
},
_action: function (action, callback) {
if (typeof this[action] !== 'function') {
return callback(new Error('The given action is not implemented.'));
}
try {
var actionCalled = false;
this[action]({
error: function (err) {
err = err || new Error('The component action failed and an error object was not passed');
actionCalled = true;
return callback(err, null);
},
success: function (viewName) {
if (!_.isString(viewName)) {
callback(new Error('Controller action method options.success requires a view or template name'), null);
}
actionCalled = true;
return callback(null, viewName);
}
});
} catch (error) {
if (!actionCalled) {
callback(error);
} else { // What can we do? THe error was in the callback, so let's not call that again!
throw error;
}
}
},
_getCss: function (viewName, callback) {
this._getLinks('css');
callback(null, viewName);
},
_getImports: function (viewName, callback) {
this._getLinks('imports');
callback(null, viewName);
},
_getLinks: function (type) {
var self = this;
var rootCtx = this.ctx._rootCtx;
var getMethod = type === 'imports' ? 'getComponentImports' : 'getComponentCss';
var augmentMethod = type === 'imports' ? 'augmentImportLink' : 'augmentCssLink';
var links = _.map(resolver[getMethod](self.name), function (link) {
var augmentedLink = self[augmentMethod]({ href: '/' + link });
augmentedLink['lazo-link-ctx'] = self.name;
return augmentedLink;
});
rootCtx.dependencies = rootCtx.dependencies || {};
rootCtx.dependencies[type] = rootCtx.dependencies[type] || [];
links = _.filter(links, function (link) {
return !_.find(rootCtx.dependencies[type], function (l) {
return l.href === link.href;
});
});
rootCtx.dependencies[type] = rootCtx.dependencies[type].concat(links);
},
_getView: function (viewName, callback) {
var self = this,
path = resolver.getPath(viewName, this.name, 'view');
resolver.isBase(path, 'view', function (isBase) {
if (isBase) {
return callback(null, self._createView(LazoBaseView, { name: viewName, isBase: true }));
} else {
module.getView(path, function (err, View) {
if (err) {
return callback(new Error('Controller _getView failed for ' + path + ' : ' + err.message), null);
}
module.addPath(path, self.ctx);
return callback(null, self._createView(View, { name: viewName, isBase: false }));
});
}
});
},
_getPath: function (moduleName, moduleType) { // used by collection view
return resolver.getPath(moduleName, this.name, moduleType);
},
_getBasePath: function (moduleName, moduleType) { // used by collection view
return resolver.getBasePath(moduleName, this.name, moduleType);
}
}, Backbone.Events),
{
create: function (cmpName, ctlOptions, options) {
var ctlPath = 'components/' + cmpName + '/controller';
function loadBase() {
var instance = new Controller(ctlOptions);
instance.isBase = true;
return options.success(instance);
}
function loadCtl() {
LAZO.require([ctlPath], function (Ctl) {
var instance = new Ctl(ctlOptions);
module.addPath(ctlPath, ctlOptions.ctx);
// Initialize default params, but only if context is set (otherwise we are deserializing,
// so the context will be set later with the params already set.
if (ctlOptions.ctx && instance.config && instance.config.params) {
_.defaults(instance.ctx.params, instance.config.params);
}
instance.isBase = false;
return options.success(instance);
}, function (err) {
return options.error(new Error('Controller create LAZO.require failed for ' + ctlPath + ' : ' + err.message));
});
}
// client rehydration
if (_.isBoolean(ctlOptions.isBase)) {
if (ctlOptions.isBase) {
return loadBase();
} else {
return loadCtl();
}
}
// server
resolver.isBase(ctlPath, 'controller', function (isBase) {
if (isBase) {
loadBase();
} else {
loadCtl();
}
});
},
deserialize: function (ctl, options) { // TODO: implement
return serializor.deserialize(ctl, _.extend({}, options, { Controller: Controller }));
}
});
return Controller;
});