UNPKG

10.5 kBJavaScriptView Raw
1var fs = require('fs'),
2 path = require('path');
3
4function Controller() {}
5
6/**
7 * Render response.
8 *
9 * @param {String} arg1 - view name [optional].
10 * @param {Object} arg2 - data passed to view as local variables [optional].
11 *
12 * When first parameter is omitted action name used as view name:
13 * ```
14 * action(function index() {
15 * render(); // will render 'index' action of current controller
16 * });
17 * ```
18 *
19 * Second argument is optional too, you can set local variables using `this`
20 * inside action:
21 * ```
22 * action('new', function () {
23 * this.title = 'Create new post';
24 * this.post = new Post;
25 * render();
26 * });
27 * ```
28 * will result the same as
29 * ```
30 * action('new', function () {
31 * render({
32 * title: 'Create new post',
33 * post: new Post
34 * });
35 * });
36 * ```
37 */
38Controller.prototype.render = function render(view, params, callback) {
39 var start = Date.now();
40 var self = this;
41 var compound = self.compound;
42 var app = compound.app;
43 var structure = compound.structure;
44 var views = structure.views;
45 var inAction = this.context.inAction;
46
47 Array.prototype.forEach.call(arguments, function (arg) {
48 switch (typeof arg) {
49 case 'object': return params = arg;
50 case 'function': return callback = arg;
51 case 'string': return view = arg;
52 }
53 });
54
55 if (typeof view !== 'string') view = self.actionName;
56 if (typeof params !== 'object') params = {};
57
58 params = safeMerge({}, params);
59
60 params.request = self.context.req;
61 params.params = self.params;
62
63 var vc = self.prepareViewContext(params);
64
65 ensureFlash(this.req);
66
67 var layout = getLayoutName();
68 var file = getViewFilename();
69
70 layout = layout ? 'layouts/' + layout : false;
71
72 compound.emit('render', vc, self, layout, file);
73
74 layout = calcView(compound, layout, params);
75 file = calcView(compound, file, params);
76
77 if (!layout) {
78 return self.renderView(file);
79 }
80
81 self.renderView(file, function(err, html) {
82 if (err) {
83 return self.next(err);
84 }
85 self.viewContext.body = html;
86 self.renderView(layout);
87 if (self.logger) {
88 self.logger.emit('render', file, layout, Date.now() - start);
89 }
90
91 });
92
93 function getLayoutName() {
94 if ('layout' in vc) {
95 return vc.layout ? vc.layout + '_layout' : false;
96 }
97 return self.layout();
98 }
99
100 function getViewFilename() {
101 return path.join(self.controllerName, view).replace(/\\/g, "/");
102 }
103
104 /**
105 * Run the calcview hook and change the result if a new view is returned.
106 */
107 function calcView(compound, view, params) {
108 var calcParams = {
109 view: view,
110 params: params
111 };
112
113 compound.emit('calcview', calcParams);
114
115 return calcParams.result || view;
116 }
117
118};
119
120Controller.prototype.prepareViewContext = function prepareViewContext(params) {
121 // if (this.viewContext) return this.viewContext;
122 var self = this;
123 var helpers = this.compound.structure.helpers;
124 this.viewContext = safeMerge(
125 params || {}, this.locals,
126 new Pers(helpers[this.controllerName + '_helper']),
127 new Pers(helpers.application_helper),
128 this.helpers()
129 );
130
131 return this.viewContext;
132
133 function Pers(helper) {
134 if (!helper) return;
135
136 for (var method in helper) {
137 if ('function' === typeof helper[method]) {
138 this[method] = Function.prototype.bind.call(helper[method], self);
139 }
140 }
141 }
142
143};
144
145Controller.prototype.renderView = function renderView(view, callback) {
146 var filename = this.compound.structure.views[view];
147 if (!filename) {
148 var err = new Error('Template ' + view + ' not found');
149 if (callback) {
150 return callback(err);
151 } else {
152 throw err;
153 }
154 }
155 if (callback) {
156 this.res.render(filename, this.viewContext, callback);
157 } else {
158 this.res.render(filename, this.viewContext);
159 if (this.context.inAction) {
160 this.next();
161 }
162 }
163};
164
165Controller.prototype.helpers = function () {
166 if (!this._helpers) {
167 this._helpers = this.compound.helpers.personalize(this);
168 }
169 return this._helpers;
170};
171
172Controller.prototype.contentFor = function (name, content) {
173 return this.helpers().contentFor(name, content);
174};
175
176Controller.prototype.sendError = function (err) {
177 this.res.send({
178 code: 500,
179 error: err
180 });
181};
182
183/**
184 * Handle specific format. When 'format' present in req.param(), desired handler
185 * called, otherwise default handler called. Default handler name can be specified
186 * by setting `app.set('default format', 'xml')`. If not specified - default is html
187 *
188 * Example of usage:
189 *
190 * BlogController.prototype.index = function(c) {
191 * Post.all(c.safe(function(post) {
192 * c.format({
193 * json: function() {c.send(posts);},
194 * html: function() {c.render({posts: posts});}
195 * });
196 * }));
197 * };
198 *
199 * @param {Object} handlers - hash of functions to handle each specific format
200 */
201Controller.prototype.format = function format(handlers) {
202 var requestedFormat = this.req.param('format');
203 if (requestedFormat in handlers) {
204 return handlers[requestedFormat].call(this.locals);
205 }
206 var defaultFormat = this.req.app.get('default format') || 'html';
207 if (defaultFormat in handlers) {
208 return handlers[defaultFormat].call(this.locals);
209 }
210};
211
212/**
213 * Return safe callback. This is utility function which allows to reduce nesting
214 * level and less code to track errors.
215 *
216 * Example of usage:
217 *
218 * CheckoutController.prototype.orderProducts = function(c) {
219 * c.basket.createOrder(c.safe(function(order) {
220 * this.order = order; // pass order to view
221 * c.render('success');
222 * }));
223 * };
224 *
225 * // same as
226 *
227 * CheckoutController.prototype.orderProducts = function(c) {
228 * c.basket.createOrder(function(err, order) {
229 * if (err) {
230 * c.next(err);
231 * } else {
232 * c.locals.order = order; // pass order to view
233 * c.render('success');
234 * }
235 * });
236 * };
237 *
238 */
239Controller.prototype.safe = function safe(callback) {
240 var c = this;
241 return function safeCallback(err) {
242 if (err) {
243 return c.next(err);
244 }
245 callback.apply(c.locals, Array.prototype.slice.call(arguments, 1));
246 };
247};
248
249function ensureFlash(req) {
250 if (req.flash) {
251 return;
252 }
253 req.flash = function _flash(type, msg) {
254 if (this.session === undefined) {
255 return [];//throw Error('req.flash() requires sessions');
256 }
257 var msgs = this.session.flash = this.session.flash || {};
258 if (type && msg) {
259 // util.format is available in Node.js 0.6+
260 // if (arguments.length > 2 && format) {
261 // var args = Array.prototype.slice.call(arguments, 1);
262 // msg = format.apply(undefined, args);
263 // }
264 return (msgs[type] = msgs[type] || []).push(msg);
265 } else if (type) {
266 var arr = msgs[type];
267 delete msgs[type];
268 return arr || [];
269 } else {
270 this.session.flash = {};
271 return msgs;
272 }
273 };
274}
275
276/**
277 * Layout setter/getter
278 *
279 * - when called without arguments, used as getter,
280 * - when called with string, used as setter
281 *
282 * When `layout` not called controller trying to get guess which layout to use.
283 * First of all controller looking for layout with the same name as controller,
284 * for example `users_controller` will choose `users_laout`, if there's no
285 * layout with this name, controller using `application_layout`.
286 *
287 * If you do not want to use any layout by default, you can just set it up:
288 *
289 * app.set('view options', {layout: false});
290 *
291 * this will prevent you from repeating `layout(false)` in each controller where
292 * you do not want to use layout, for example in api controllers.
293 *
294 * - choose
295 *
296 * @param {String} l - [optional] layout name.
297 * @return {String} layout name.
298 */
299Controller.prototype.layout = function layout(l) {
300 var compound = this.compound;
301 var viewOpts = compound.app.set('view options') || {};
302 if (viewOpts.layout === false) return false;
303
304 if (typeof l !== 'undefined') {
305 this.constructor.layout = l;
306 }
307 if (typeof this.constructor.layout === 'undefined') {
308 var layoutName = 'layouts/' + this.controllerName + '_layout';
309 this.constructor.layout = layoutName in compound.structure.views ?
310 this.controllerName : 'application';
311 }
312 return this.constructor.layout ? this.constructor.layout + '_layout' : null;
313};
314
315/**
316 * Load controller
317 *
318 * @param {String} controllerName - name of controller.
319 */
320Controller.prototype.load = function(controllerName) {
321 var fullName = controllerName + '_controller';
322 var source = this.compound.structure.controllers[fullName];
323 this.build(source);
324};
325
326/**
327 * Translation helper
328 *
329 * @return {String} translated version of arguments.
330 */
331Controller.prototype.t = function() {
332 if (!this._t) {
333 this._t = this.compound.T();
334 this._t.locale = this.app.settings.defaultLocale || 'en';
335 this._T = this.compound.T;
336 }
337 return this._t.apply(this, [].slice.call(arguments));
338
339};
340
341/**
342 * Setup locale for current request
343 *
344 * @param {String} locale - name of locale, for example 'jp' if you have
345 * `config/locales/jp.yml` with translations.
346 *
347 */
348Controller.prototype.setLocale = function (locale) {
349 if (!this._t) {
350 this._t = this.compound.T();
351 this._t.locale = locale;
352 this._T = this.compound.T;
353 } else {
354 this._t.locale = locale;
355 }
356};
357
358/**
359 * Module exports set of methods listed in Controller.prototype
360 */
361module.exports = Controller.prototype;
362
363function safeMerge(mergeWhat) {
364 mergeWhat = mergeWhat || {};
365 Array.prototype.forEach.call(arguments, function(mergeWith, i) {
366 if (i === 0) {
367 return;
368 }
369 for (var key in mergeWith) {
370 if (key in mergeWhat) continue;
371 mergeWhat[key] = mergeWith[key];
372 }
373 });
374 return mergeWhat;
375}