UNPKG

8.94 kBJavaScriptView Raw
1/*
2 * App.js
3 *
4 * Provides the glue between views, controllers, and routes for an
5 * application's functionality. Apps are responsible for creating pages.
6 *
7 */
8
9var path = require('path');
10var EventEmitter = require('events').EventEmitter;
11var tracks = require('tracks');
12var util = require('racer/lib/util');
13var derbyTemplates = require('derby-templates');
14var templates = derbyTemplates.templates;
15var documentListeners = require('./documentListeners');
16var components = require('./components');
17var Page = require('./Page');
18var serializedViews = require('./_views');
19
20module.exports = App;
21
22function App(derby, name, filename, options) {
23 EventEmitter.call(this);
24 this.derby = derby;
25 this.name = name;
26 this.filename = filename;
27 this.scriptHash = '{{DERBY_SCRIPT_HASH}}';
28 this.bundledAt = '{{DERBY_BUNDLED_AT}}';
29 this.Page = createAppPage();
30 this.proto = this.Page.prototype;
31 this.views = new templates.Views();
32 this.tracksRoutes = tracks.setup(this);
33 this.model = null;
34 this.page = null;
35 this._init(options);
36}
37
38function createAppPage() {
39 // Inherit from Page so that we can add controller functions as prototype
40 // methods on this app's pages
41 function AppPage() {
42 Page.apply(this, arguments);
43 }
44 AppPage.prototype = Object.create(Page.prototype);
45 return AppPage;
46}
47
48util.mergeInto(App.prototype, EventEmitter.prototype);
49
50// Overriden on server
51App.prototype._init = function() {
52 this._waitForAttach = true;
53 this._cancelAttach = false;
54 this.model = new this.derby.Model();
55 serializedViews(derbyTemplates, this.views);
56 // Must init async so that app.on('model') listeners can be added.
57 // Must also wait for content ready so that bundle is fully downloaded.
58 this._contentReady();
59};
60App.prototype._finishInit = function() {
61 var script = this._getScript();
62 var data = JSON.parse(script.nextSibling.innerHTML);
63 this.model.createConnection(data);
64 this.emit('model', this.model);
65 util.isProduction = data.nodeEnv === 'production';
66 if (!util.isProduction) this._autoRefresh();
67 this.model.unbundle(data);
68 var page = this.createPage();
69 page.params = this.model.get('$render.params');
70 this.emit('ready', page);
71 this._waitForAttach = false;
72 // Instead of attaching, do a route and render if a link was clicked before
73 // the page finished attaching
74 if (this._cancelAttach) {
75 this.history.refresh();
76 return;
77 }
78 // Since an attachment failure is *fatal* and could happen as a result of a
79 // browser extension like AdBlock, an invalid template, or a small bug in
80 // Derby or Saddle, re-render from scratch on production failures
81 if (util.isProduction) {
82 try {
83 page.attach();
84 } catch (err) {
85 this.history.refresh();
86 console.warn('attachment error', err.stack);
87 }
88 } else {
89 page.attach();
90 }
91 this.emit('load', page);
92};
93// Modified from: https://github.com/addyosmani/jquery.parts/blob/master/jquery.documentReady.js
94App.prototype._contentReady = function() {
95 // Is the DOM ready to be used? Set to true once it occurs.
96 var isReady = false;
97 var app = this;
98
99 // The ready event handler
100 function onDOMContentLoaded() {
101 if (document.addEventListener) {
102 document.removeEventListener('DOMContentLoaded', onDOMContentLoaded, false);
103 } else {
104 // we're here because readyState !== 'loading' in oldIE
105 // which is good enough for us to call the dom ready!
106 document.detachEvent('onreadystatechange', onDOMContentLoaded);
107 }
108 onDOMReady();
109 }
110
111 // Handle when the DOM is ready
112 function onDOMReady() {
113 // Make sure that the DOM is not already loaded
114 if (isReady) return;
115 // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
116 if (!document.body) return setTimeout(onDOMReady, 0);
117 // Remember that the DOM is ready
118 isReady = true;
119 // Make sure this is always async and then finishin init
120 setTimeout(function() {
121 app._finishInit();
122 }, 0);
123 }
124
125 // The DOM ready check for Internet Explorer
126 function doScrollCheck() {
127 if (isReady) return;
128 try {
129 // If IE is used, use the trick by Diego Perini
130 // http://javascript.nwbox.com/IEContentLoaded/
131 document.documentElement.doScroll('left');
132 } catch (err) {
133 setTimeout(doScrollCheck, 0);
134 return;
135 }
136 // and execute any waiting functions
137 onDOMReady();
138 }
139
140 // Catch cases where called after the browser event has already occurred.
141 if (document.readyState !== 'loading') return onDOMReady();
142
143 // Mozilla, Opera and webkit nightlies currently support this event
144 if (document.addEventListener) {
145 // Use the handy event callback
146 document.addEventListener('DOMContentLoaded', onDOMContentLoaded, false);
147 // A fallback to window.onload, that will always work
148 window.addEventListener('load', onDOMContentLoaded, false);
149 // If IE event model is used
150 } else if (document.attachEvent) {
151 // ensure firing before onload,
152 // maybe late but safe also for iframes
153 document.attachEvent('onreadystatechange', onDOMContentLoaded);
154 // A fallback to window.onload, that will always work
155 window.attachEvent('onload', onDOMContentLoaded);
156 // If IE and not a frame
157 // continually check to see if the document is ready
158 var toplevel;
159 try {
160 toplevel = window.frameElement == null;
161 } catch (err) {}
162 if (document.documentElement.doScroll && toplevel) {
163 doScrollCheck();
164 }
165 }
166};
167
168App.prototype._getScript = function() {
169 return document.querySelector('script[data-derby-app]');
170};
171
172App.prototype.use = util.use;
173App.prototype.serverUse = util.serverUse;
174
175App.prototype.loadViews = function() {};
176
177App.prototype.loadStyles = function() {};
178
179App.prototype.component = function(viewName, constructor) {
180 if (typeof viewName === 'function') {
181 constructor = viewName;
182 viewName = null;
183 }
184
185 // Inherit from Component
186 components.extendComponent(constructor);
187
188 // Load template view from filename
189 if (constructor.prototype.view) {
190 var viewFilename = constructor.prototype.view;
191 viewName = constructor.prototype.name || path.basename(viewFilename, '.html');
192 this.loadViews(viewFilename, viewName);
193
194 } else if (!viewName) {
195 if (constructor.prototype.name) {
196 viewName = constructor.prototype.name;
197 var view = this.views.register(viewName);
198 view.template = templates.emptyTemplate;
199 } else {
200 throw new Error('No view name specified for component');
201 }
202 }
203
204 // Associate the appropriate view with the component type
205 var view = this.views.find(viewName);
206 if (!view) {
207 var message = this.views.findErrorMessage(viewName);
208 throw new Error(message);
209 }
210 view.componentFactory = components.createFactory(constructor);
211
212 // Make chainable
213 return this;
214};
215
216App.prototype.createPage = function() {
217 if (this.page) {
218 this.emit('destroyPage', this.page);
219 this.page.destroy();
220 }
221 var page = new this.Page(this, this.model);
222 this.page = page;
223 return page;
224};
225
226App.prototype.onRoute = function(callback, page, next, done) {
227 if (this._waitForAttach) {
228 // Cancel any routing before the initial page attachment. Instead, do a
229 // render once derby is ready
230 this._cancelAttach = true;
231 return;
232 }
233 this.emit('route', page);
234 // HACK: To update render in transitional routes
235 page.model.set('$render.params', page.params);
236 page.model.set('$render.url', page.params.url);
237 page.model.set('$render.query', page.params.query);
238 // If transitional
239 if (done) {
240 var app = this;
241 var _done = function() {
242 app.emit('routeDone', page, 'transition');
243 done();
244 };
245 callback.call(page, page, page.model, page.params, next, _done);
246 return;
247 }
248 callback.call(page, page, page.model, page.params, next);
249};
250
251App.prototype._autoRefresh = function() {
252 var app = this;
253 var connection = this.model.connection;
254 connection.on('connected', function() {
255 connection.send({
256 derby: 'app',
257 name: app.name,
258 hash: app.scriptHash
259 });
260 });
261 connection.on('receive', function(request) {
262 if (request.data.derby) {
263 var message = request.data;
264 request.data = null;
265 app._handleMessage(message.derby, message);
266 }
267 });
268};
269
270App.prototype._handleMessage = function(action, message) {
271 if (action === 'refreshViews') {
272 var fn = new Function('return ' + message.views)(); // jshint ignore:line
273 fn(derbyTemplates, this.views);
274 var ns = this.model.get('$render.ns');
275 this.page.render(ns);
276
277 } else if (action === 'refreshStyles') {
278 var styleElement = document.querySelector('style[data-filename="' +
279 message.filename + '"]');
280 if (styleElement) styleElement.innerHTML = message.css;
281
282 } else if (action === 'reload') {
283 this.model.whenNothingPending(function() {
284 window.location = window.location;
285 });
286 }
287};
288
289util.serverRequire(module, './App.server');