UNPKG

14.2 kBJavaScriptView Raw
1var derbyTemplates = require('derby-templates');
2var contexts = derbyTemplates.contexts;
3var expressions = derbyTemplates.expressions;
4var templates = derbyTemplates.templates;
5var DependencyOptions = derbyTemplates.options.DependencyOptions;
6var util = require('racer/lib/util');
7var components = require('./components');
8var EventModel = require('./eventmodel');
9var textDiff = require('./textDiff');
10var Controller = require('./Controller');
11var documentListeners = require('./documentListeners');
12
13module.exports = Page;
14
15function Page(app, model, req, res) {
16 Controller.call(this, app, this, model);
17 this.req = req;
18 this.res = res;
19 this.params = null;
20 if (this.init) this.init(model);
21 this.context = this._createContext();
22 this._eventModel = null;
23 this._removeModelListeners = null;
24 this._components = {};
25 this._addListeners();
26}
27
28util.mergeInto(Page.prototype, Controller.prototype);
29
30Page.prototype.$bodyClass = function(ns) {
31 if (!ns) return;
32 var classNames = [];
33 var segments = ns.split(':');
34 for (var i = 0, len = segments.length; i < len; i++) {
35 var className = segments.slice(0, i + 1).join('-');
36 classNames.push(className);
37 }
38 return classNames.join(' ');
39};
40
41Page.prototype.$preventDefault = function(e) {
42 e.preventDefault();
43};
44
45Page.prototype.$stopPropagation = function(e) {
46 e.stopPropagation();
47};
48
49Page.prototype._setRenderParams = function(ns) {
50 this.model.set('$render.ns', ns);
51 this.model.set('$render.params', this.params);
52 this.model.set('$render.url', this.params && this.params.url);
53 this.model.set('$render.query', this.params && this.params.query);
54};
55
56Page.prototype._setRenderPrefix = function(ns) {
57 var prefix = (ns) ? ns + ':' : '';
58 this.model.set('$render.prefix', prefix);
59};
60
61Page.prototype.get = function(viewName, ns, unescaped) {
62 this._setRenderPrefix(ns);
63 var view = this.getView(viewName, ns);
64 return view.get(this.context, unescaped);
65};
66
67Page.prototype.getFragment = function(viewName, ns) {
68 this._setRenderPrefix(ns);
69 var view = this.getView(viewName, ns);
70 return view.getFragment(this.context);
71};
72
73Page.prototype.getView = function(viewName, ns) {
74 return this.app.views.find(viewName, ns);
75};
76
77Page.prototype.render = function(ns) {
78 this.app.emit('render', this);
79 this.context.pause();
80 this._setRenderParams(ns);
81 var titleFragment = this.getFragment('TitleElement', ns);
82 var bodyFragment = this.getFragment('BodyElement', ns);
83 var titleElement = document.getElementsByTagName('title')[0];
84 titleElement.parentNode.replaceChild(titleFragment, titleElement);
85 document.body.parentNode.replaceChild(bodyFragment, document.body);
86 this.context.unpause();
87 if (this.create) this.create(this.model, this.dom);
88 this.app.emit('routeDone', this, 'render');
89};
90
91Page.prototype.attach = function() {
92 this.context.pause();
93 var ns = this.model.get('$render.ns');
94 var titleView = this.getView('TitleElement', ns);
95 var bodyView = this.getView('BodyElement', ns);
96 var titleElement = document.getElementsByTagName('title')[0];
97 titleView.attachTo(titleElement.parentNode, titleElement, this.context);
98 bodyView.attachTo(document.body.parentNode, document.body, this.context);
99 this.context.unpause();
100 if (this.create) this.create(this.model, this.dom);
101};
102
103Page.prototype._createContext = function() {
104 var contextMeta = new contexts.ContextMeta();
105 contextMeta.views = this.app && this.app.views;
106 var context = new contexts.Context(contextMeta, this);
107 context.expression = new expressions.PathExpression([]);
108 context.alias = '#root';
109 return context;
110};
111
112Page.prototype._addListeners = function() {
113 var eventModel = this._eventModel = new EventModel();
114 this._addModelListeners(eventModel);
115 this._addContextListeners(eventModel);
116};
117
118Page.prototype.destroy = function() {
119 this.emit('destroy');
120 this._removeModelListeners();
121 for (var id in this._components) {
122 var component = this._components[id];
123 component.destroy();
124 }
125 // Remove all data, refs, listeners, and reactive functions
126 // for the previous page
127 var silentModel = this.model.silent();
128 silentModel.destroy('_page');
129 silentModel.destroy('$components');
130 // Unfetch and unsubscribe from all queries and documents
131 silentModel.unloadAll && silentModel.unloadAll();
132};
133
134Page.prototype._addModelListeners = function(eventModel) {
135 var model = this.model;
136 if (!model) return;
137
138 if (model.get('$derbyFlags.immediateModelListeners')) {
139 // Registering model listeners with the *Immediate events helps to prevent
140 // a bug with binding updates where a model listener causes a change to the
141 // path being listened on, directly or indirectly. This flag will go away
142 // after a month or so of private testing, and if everything looks fine,
143 // we'll switch unconditionally to *Immediate listeners.
144 return this._addModelListenersImmediate(eventModel);
145 }
146
147 var context = this.context;
148 var changeListener = model.on('change', '**', function onChange(path, value, previous, pass) {
149 var segments = util.castSegments(path.split('.'));
150 // The pass parameter is passed in for special handling of updates
151 // resulting from stringInsert or stringRemove
152 eventModel.set(segments, previous, pass);
153 });
154 var loadListener = model.on('load', '**', function onLoad(path) {
155 var segments = util.castSegments(path.split('.'));
156 eventModel.set(segments);
157 });
158 var unloadListener = model.on('unload', '**', function onUnload(path) {
159 var segments = util.castSegments(path.split('.'));
160 eventModel.set(segments);
161 });
162 var insertListener = model.on('insert', '**', function onInsert(path, index, values) {
163 var segments = util.castSegments(path.split('.'));
164 eventModel.insert(segments, index, values.length);
165 });
166 var removeListener = model.on('remove', '**', function onRemove(path, index, values) {
167 var segments = util.castSegments(path.split('.'));
168 eventModel.remove(segments, index, values.length);
169 });
170 var moveListener = model.on('move', '**', function onMove(path, from, to, howMany) {
171 var segments = util.castSegments(path.split('.'));
172 eventModel.move(segments, from, to, howMany);
173 });
174
175 this._removeModelListeners = function() {
176 model.removeListener('change', changeListener);
177 model.removeListener('load', loadListener);
178 model.removeListener('unload', unloadListener);
179 model.removeListener('insert', insertListener);
180 model.removeListener('remove', removeListener);
181 model.removeListener('move', moveListener);
182 };
183};
184Page.prototype._addModelListenersImmediate = function(eventModel) {
185 var model = this.model;
186 if (!model) return;
187
188 // `util.castSegments(segments)` is needed to cast string segments into
189 // numbers, since EventModel#child does typeof checks against segments. This
190 // could be done once in Racer's Model#emit, instead of in every listener.
191 var changeListener = model.on('changeImmediate', function onChange(segments, eventArgs) {
192 // eventArgs[0] is the new value, which Derby bindings don't use directly.
193 var previous = eventArgs[1];
194 // The pass parameter is passed in for special handling of updates
195 // resulting from stringInsert or stringRemove
196 var pass = eventArgs[2];
197 segments = util.castSegments(segments.slice());
198 eventModel.set(segments, previous, pass);
199 });
200 var loadListener = model.on('loadImmediate', function onLoad(segments) {
201 segments = util.castSegments(segments.slice());
202 eventModel.set(segments);
203 });
204 var unloadListener = model.on('unloadImmediate', function onUnload(segments) {
205 segments = util.castSegments(segments.slice());
206 eventModel.set(segments);
207 });
208 var insertListener = model.on('insertImmediate', function onInsert(segments, eventArgs) {
209 var index = eventArgs[0];
210 var values = eventArgs[1];
211 segments = util.castSegments(segments.slice());
212 eventModel.insert(segments, index, values.length);
213 });
214 var removeListener = model.on('removeImmediate', function onRemove(segments, eventArgs) {
215 var index = eventArgs[0];
216 var values = eventArgs[1];
217 segments = util.castSegments(segments.slice());
218 eventModel.remove(segments, index, values.length);
219 });
220 var moveListener = model.on('moveImmediate', function onMove(segments, eventArgs) {
221 var from = eventArgs[0];
222 var to = eventArgs[1];
223 var howMany = eventArgs[2];
224 segments = util.castSegments(segments.slice());
225 eventModel.move(segments, from, to, howMany);
226 });
227
228 this._removeModelListeners = function() {
229 model.removeListener('changeImmediate', changeListener);
230 model.removeListener('loadImmediate', loadListener);
231 model.removeListener('unloadImmediate', unloadListener);
232 model.removeListener('insertImmediate', insertListener);
233 model.removeListener('removeImmediate', removeListener);
234 model.removeListener('moveImmediate', moveListener);
235 };
236};
237
238Page.prototype._addContextListeners = function(eventModel) {
239 this.context.meta.addBinding = addBinding;
240 this.context.meta.removeBinding = removeBinding;
241 this.context.meta.removeNode = removeNode;
242 this.context.meta.addItemContext = addItemContext;
243 this.context.meta.removeItemContext = removeItemContext;
244
245 function addItemContext(context) {
246 var segments = context.expression.resolve(context);
247 eventModel.addItemContext(segments, context);
248 }
249 function removeItemContext(context) {
250 // TODO
251 }
252 function addBinding(binding) {
253 patchTextBinding(binding);
254 var expressions = binding.template.expressions;
255 if (expressions) {
256 for (var i = 0, len = expressions.length; i < len; i++) {
257 addDependencies(eventModel, expressions[i], binding);
258 }
259 } else {
260 var expression = binding.template.expression;
261 addDependencies(eventModel, expression, binding);
262 }
263 }
264 function removeBinding(binding) {
265 var bindingWrappers = binding.meta;
266 if (!bindingWrappers) return;
267 for (var i = bindingWrappers.length; i--;) {
268 eventModel.removeBinding(bindingWrappers[i]);
269 }
270 }
271 function removeNode(node) {
272 var component = node.$component;
273 if (component && !component.singleton) {
274 component.destroy();
275 }
276 var destroyListeners = node.$destroyListeners;
277 if (destroyListeners) {
278 for (var i = 0; i < destroyListeners.length; i++) {
279 destroyListeners[i]();
280 }
281 }
282 }
283};
284
285function addDependencies(eventModel, expression, binding) {
286 var bindingWrapper = new BindingWrapper(eventModel, expression, binding);
287 bindingWrapper.updateDependencies();
288}
289
290// The code here uses object-based set pattern where objects are keyed using
291// sequentially generated IDs.
292var nextId = 1;
293function BindingWrapper(eventModel, expression, binding) {
294 this.eventModel = eventModel;
295 this.expression = expression;
296 this.binding = binding;
297 this.id = nextId++;
298 this.eventModels = null;
299 this.dependencies = null;
300 this.ignoreTemplateDependency = (
301 binding instanceof components.ComponentAttributeBinding
302 ) || (
303 (binding.template instanceof templates.DynamicText) &&
304 (binding instanceof templates.RangeBinding)
305 );
306 if (binding.meta) {
307 binding.meta.push(this);
308 } else {
309 binding.meta = [this];
310 }
311}
312BindingWrapper.prototype.updateDependencies = function() {
313 var dependencyOptions;
314 if (this.ignoreTemplateDependency && this.binding.condition instanceof templates.Template) {
315 dependencyOptions = new DependencyOptions();
316 dependencyOptions.setIgnoreTemplate(this.binding.condition);
317 }
318 var dependencies = this.expression.dependencies(this.binding.context, dependencyOptions);
319 if (this.dependencies) {
320 // Do nothing if dependencies haven't changed
321 if (equalDependencies(this.dependencies, dependencies)) return;
322 // Otherwise, remove current dependencies
323 this.eventModel.removeBinding(this);
324 }
325 // Add new dependencies
326 if (!dependencies) return;
327 this.dependencies = dependencies;
328 for (var i = 0, len = dependencies.length; i < len; i++) {
329 var dependency = dependencies[i];
330 if (dependency) this.eventModel.addBinding(dependency, this);
331 }
332};
333BindingWrapper.prototype.update = function(previous, pass) {
334 this.binding.update(previous, pass);
335 this.updateDependencies();
336};
337BindingWrapper.prototype.insert = function(index, howMany) {
338 this.binding.insert(index, howMany);
339 this.updateDependencies();
340};
341BindingWrapper.prototype.remove = function(index, howMany) {
342 this.binding.remove(index, howMany);
343 this.updateDependencies();
344};
345BindingWrapper.prototype.move = function(from, to, howMany) {
346 this.binding.move(from, to, howMany);
347 this.updateDependencies();
348};
349
350function equalDependencies(a, b) {
351 var lenA = a ? a.length : -1;
352 var lenB = b ? b.length : -1;
353 if (lenA !== lenB) return false;
354 for (var i = 0; i < lenA; i++) {
355 var itemA = a[i];
356 var itemB = b[i];
357 var lenItemA = itemA ? itemA.length : -1;
358 var lenItemB = itemB ? itemB.length : -1;
359 if (lenItemA !== lenItemB) return false;
360 for (var j = 0; j < lenItemB; j++) {
361 if (itemA[j] !== itemB[j]) return false;
362 }
363 }
364 return true;
365}
366
367function patchTextBinding(binding) {
368 if (
369 binding instanceof templates.AttributeBinding &&
370 binding.name === 'value' &&
371 (binding.element.tagName === 'INPUT' || binding.element.tagName === 'TEXTAREA') &&
372 documentListeners.inputSupportsSelection(binding.element) &&
373 binding.template.expression.resolve(binding.context)
374 ) {
375 binding.update = textInputUpdate;
376 }
377}
378
379function textInputUpdate(previous, pass) {
380 textUpdate(this, this.element, previous, pass);
381}
382function textUpdate(binding, element, previous, pass) {
383 if (pass) {
384 if (pass.$event && pass.$event.target === element) {
385 return;
386 } else if (pass.$stringInsert) {
387 return textDiff.onStringInsert(
388 element,
389 previous,
390 pass.$stringInsert.index,
391 pass.$stringInsert.text
392 );
393 } else if (pass.$stringRemove) {
394 return textDiff.onStringRemove(
395 element,
396 previous,
397 pass.$stringRemove.index,
398 pass.$stringRemove.howMany
399 );
400 }
401 }
402 binding.template.update(binding.context, binding);
403}
404
405util.serverRequire(module, './Page.server');