UNPKG

10.1 kBJavaScriptView Raw
1'use strict';
2
3var fs = require('fs');
4var Base = require('base');
5var debug = require('debug')('base:templates:views');
6var plugin = require('./plugins');
7var utils = require('./utils');
8var List = require('./list');
9var routes = require('base-routes');
10
11/**
12 * Expose `Views`
13 */
14
15module.exports = exports = Views;
16
17/**
18 * Create an instance of `Views` with the given `options`.
19 *
20 * ```js
21 * var collection = new Views();
22 * collection.addView('foo', {content: 'bar'});
23 * ```
24 * @param {Object} `options`
25 * @api public
26 */
27
28function Views(options) {
29 if (!(this instanceof Views)) {
30 return new Views(options);
31 }
32
33 Base.call(this);
34
35 this.is('views');
36 this.define('isCollection', true);
37 this.use(utils.option());
38 this.use(utils.plugin());
39 this.init(options || {});
40}
41
42/**
43 * Inherit `Base` and load plugins
44 */
45
46plugin.static(Base, Views, 'Views');
47
48/**
49 * Initialize `Views` defaults
50 */
51
52/**
53 * Expose static properties
54 */
55
56Views.prototype.init = function(opts) {
57 debug('initializing');
58
59 // add constructors to the instance
60 this.define('Item', opts.Item || Views.Item);
61 this.define('View', opts.View || Views.View);
62
63 // decorate the instance
64 this.use(plugin.init);
65 this.use(plugin.renameKey());
66 this.use(plugin.context);
67 this.use(utils.engines());
68 this.use(utils.helpers());
69 this.use(utils.routes());
70 this.use(plugin.item('view', 'View', {emit: false}));
71
72 // setup listeners
73 this.listen(this);
74 this.views = {};
75
76 // if an instance of `List` of `Views` is passed, load it now
77 if (Array.isArray(opts) || opts.isList) {
78 this.option(opts.options || {});
79 this.addList(opts.items || opts);
80
81 } else if (opts.isCollection) {
82 this.option(opts.options || {});
83 this.addViews(opts.views);
84
85 } else {
86 this.option(opts);
87 }
88};
89
90/**
91 * Built-in listeners
92 */
93
94Views.prototype.listen = function(collection) {
95 // ensure that plugins are loaded onto views
96 // created after the plugins are registered
97 this.on('use', function(fn) {
98 if (typeof fn !== 'function') return;
99 for (var key in collection.views) {
100 if (collection.views.hasOwnProperty(key)) {
101 var view = collection.views[key];
102 if (typeof view.use === 'function') {
103 view.use(fn);
104 }
105 }
106 }
107 });
108};
109
110/**
111 * Add a view to `collection.views`. This is identical to [addView](#addView)
112 * except `setView` returns the collection instance, and `addView` returns
113 * the item instance.
114 *
115 * ```js
116 * collection.setView('foo', {content: 'bar'});
117 * ```
118 *
119 * @param {String|Object} `key` View key or object
120 * @param {Object} `value` If key is a string, value is the view object.
121 * @developer This method is decorated onto the collection in the constructor using the `createView` utility method.
122 * @return {Object} returns the `view` instance.
123 * @api public
124 */
125
126Views.prototype.addView = function(key, value) {
127 var view = this.view(key, value);
128 debug('adding view "%s"', view.path);
129
130 // set the `viewType` (partial, layout, or renderable)
131 this.setType(view);
132
133 // set the name to be used by the `inspect` method and emitter
134 var name = this.options.inflection || 'view';
135 utils.setInstanceNames(view, name);
136
137 // run plugins on `view`
138 if (typeof view.use === 'function') {
139 this.run(view);
140 }
141
142 // emit that the view has been loaded
143 this.emit('load', view, this);
144 this.emit('view', view, this);
145 this.emit(name, view, this);
146 this.extendView(view);
147
148 // set the view on `collection.views`
149 this.views[view.key] = view;
150 return view;
151};
152
153/**
154 * Set a view on the collection. This is identical to [addView](#addView)
155 * except `setView` does not emit an event for each view.
156 *
157 * ```js
158 * collection.setView('foo', {content: 'bar'});
159 * ```
160 *
161 * @param {String|Object} `key` View key or object
162 * @param {Object} `value` If key is a string, value is the view object.
163 * @developer This method is decorated onto the collection in the constructor using the `createView` utility method.
164 * @return {Object} returns the `view` instance.
165 * @api public
166 */
167
168Views.prototype.setView = function(/*key, value*/) {
169 this.addView.apply(this, arguments);
170 return this;
171};
172
173/**
174 * Get view `name` from `collection.views`.
175 *
176 * ```js
177 * collection.getView('a.html');
178 * ```
179 * @param {String} `key` Key of the view to get.
180 * @param {Function} `fn` Optionally pass a function to modify the key.
181 * @return {Object}
182 * @api public
183 */
184
185Views.prototype.getView = function(name, options, fn) {
186 if (typeof name !== 'string') {
187 throw new TypeError('expected a string');
188 }
189
190 debug('getting view "%s"', name);
191 if (typeof options === 'function') {
192 fn = options;
193 options = {};
194 }
195
196 var view = this.views[name] || this.views[this.renameKey(name)];
197 if (view) return view;
198
199 view = utils.getView(name, this.views, fn);
200 if (view) return view;
201
202 if (utils.fileExists(name)) {
203 return this.addView(name, {
204 contents: fs.readFileSync(name)
205 });
206 }
207};
208
209/**
210 * Delete a view from collection `views`.
211 *
212 * ```js
213 * views.deleteView('foo.html');
214 * ```
215 * @param {String} `key`
216 * @return {Object} Returns the instance for chaining
217 * @api public
218 */
219
220Views.prototype.deleteView = function(view) {
221 if (typeof view === 'string') {
222 view = this.getView(view);
223 }
224 debug('deleting view "%s"', view.key);
225 delete this.views[view.key];
226 return this;
227};
228
229/**
230 * Load multiple views onto the collection.
231 *
232 * ```js
233 * collection.addViews({
234 * 'a.html': {content: '...'},
235 * 'b.html': {content: '...'},
236 * 'c.html': {content: '...'}
237 * });
238 * ```
239 * @param {Object|Array} `views`
240 * @return {Object} returns the `collection` object
241 * @api public
242 */
243
244Views.prototype.addViews = function(views, view) {
245 this.emit('addViews', views);
246 if (utils.hasGlob(views)) {
247 var name = '"' + this.options.plural + '"';
248 throw new Error('glob patterns are not supported by the ' + name + ' collection');
249 }
250 if (Array.isArray(views)) {
251 return this.addList.apply(this, arguments);
252 }
253 if (arguments.length > 1 && utils.isView(view)) {
254 this.addView.apply(this, arguments);
255 return this;
256 }
257 this.visit('addView', views);
258 return this;
259};
260
261/**
262 * Load an array of views onto the collection.
263 *
264 * ```js
265 * collection.addList([
266 * {path: 'a.html', content: '...'},
267 * {path: 'b.html', content: '...'},
268 * {path: 'c.html', content: '...'}
269 * ]);
270 * ```
271 * @param {Array} `list`
272 * @return {Object} returns the `views` instance
273 * @api public
274 */
275
276Views.prototype.addList = function(list, fn) {
277 this.emit('addList', list);
278
279 if (utils.hasGlob(list)) {
280 var name = '"' + this.options.plural + '"';
281 throw new Error('glob patterns are not supported by the ' + name + ' collection');
282 }
283
284 if (!Array.isArray(list)) {
285 throw new TypeError('expected list to be an array.');
286 }
287 if (typeof fn !== 'function') {
288 fn = utils.identity;
289 }
290
291 var len = list.length;
292 var idx = -1;
293 while (++idx < len) {
294 this.addView(fn(list[idx]));
295 }
296 return this;
297};
298
299/**
300 * Group all collection `views` by the given property,
301 * properties or compare functions. See [group-array][]
302 * for the full range of available features and options.
303 *
304 * ```js
305 * var collection = new Collection();
306 * collection.addViews(...);
307 * var groups = collection.groupBy('data.date', 'data.slug');
308 * ```
309 * @return {Object} Returns an object of grouped views.
310 * @api public
311 */
312
313Views.prototype.groupBy = function() {
314 var list = new List(this);
315 return list.groupBy.apply(list, arguments);
316};
317
318/**
319 * Used for extending `view` with custom properties or methods.
320 *
321 * ```js
322 * collection.extendView(view);
323 * ```
324 * @param {Object} `view`
325 * @return {Object}
326 */
327
328Views.prototype.extendView = function(view) {
329 return plugin.view(this, view, this.options);
330};
331
332/**
333 * Return true if the collection belongs to the given
334 * view `type`.
335 *
336 * ```js
337 * collection.isType('partial');
338 * ```
339 * @param {String} `type` (`renderable`, `partial`, `layout`)
340 * @api public
341 */
342
343Views.prototype.isType = function(type) {
344 if (!this.options.viewType || !this.options.viewType.length) {
345 this.viewType();
346 }
347 return this.options.viewType.indexOf(type) !== -1;
348};
349
350/**
351 * Set view types for the collection.
352 *
353 * @param {String} `plural` e.g. `pages`
354 * @param {Object} `options`
355 */
356
357Views.prototype.viewType = function(types) {
358 this.options.viewType = utils.arrayify(this.options.viewType);
359 types = utils.arrayify(types);
360
361 var validTypes = ['partial', 'layout', 'renderable'];
362 var len = types.length;
363 var idx = -1;
364
365 while (++idx < len) {
366 var type = types[idx];
367 if (validTypes.indexOf(type) === -1) {
368 var msg = 'Invalid viewType: "' + type
369 + '". viewTypes must be either: "partial", '
370 + '"renderable", or "layout".';
371 throw new Error(msg);
372 }
373 if (this.options.viewType.indexOf(type) === -1) {
374 this.options.viewType.push(type);
375 }
376 }
377
378 if (this.options.viewType.length === 0) {
379 this.options.viewType.push('renderable');
380 }
381 return this.options.viewType;
382};
383
384/**
385 * Alias for `viewType`
386 *
387 * @api public
388 */
389
390Views.prototype.viewTypes = function() {
391 return this.viewType.apply(this, arguments);
392};
393
394/**
395 * Update the `options.viewType` property on a view.
396 *
397 * @param {Object} `view` The view to update
398 */
399
400Views.prototype.setType = function(view) {
401 view.options.viewType = utils.arrayify(view.options.viewType);
402 var types = this.viewType();
403 var len = types.length;
404
405 while (len--) {
406 var type = types[len];
407 if (view.options.viewType.indexOf(type) === -1) {
408 view.options.viewType.push(type);
409 }
410 }
411 view.options.viewType.sort();
412};
413
414/**
415 * Resolve the layout to use for the given `view`
416 *
417 * @param {Object} `view`
418 * @return {String} Returns the name of the layout to use.
419 */
420
421Views.prototype.resolveLayout = function(view) {
422 if (!utils.isPartial(view) && typeof view.layout === 'undefined') {
423 return this.option('layout');
424 }
425 return view.layout;
426};
427
428/**
429 * Expose static properties
430 */
431
432utils.define(Views, 'Item', require('vinyl-item'));
433utils.define(Views, 'View', require('vinyl-view'));