Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code | Sign in
(606)

Side by Side Diff: app/assets/javascripts/d3-components.js

Issue 6828048: Micro Framework for Environment View
Patch Set: Created 12 years, 5 months ago
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments. Please Sign in to add in-line comments.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 'use strict';
2
3 /**
4 * Provides a declarative structure around interactive D3
5 * applications.
6 *
7 * @module d3-components
8 **/
9
10 YUI.add('d3-components', function(Y) {
11 var ns = Y.namespace('d3'),
12 L = Y.Lang,
13 Module = Y.Base.create('Module', Y.Base, [], {
14 /**
15 * @property events
16 * @type {object}
17 **/
18 events: {
19 scene: {},
20 d3: {},
21 yui: {}
22 },
23
24 initializer: function(options) {
25 options = options || {};
26 this.events = options.events ?
27 Y.merge(this.events, options.events) :
28 this.events;
29 }
30 }, {
31 ATTRS: {
32 component: {},
33 container: {getter: function() {
34 return this.get('component').get('container');}}
35 }
36 });
37 ns.Module = Module;
38
39
40 var Component = Y.Base.create('Component', Y.Base, [], {
thiago 2012/11/08 13:33:35 I like this code style. You could the same with th
benjamin.saller 2012/11/09 13:39:21 The linter complained that I didn't combine the va
41 /**
42 * @class Component
43 *
44 * Component collections modules implementing various portions
matthew.scott 2012/11/08 18:49:46 Minor - should be 'collects', I think? Was just c
45 * of an applications functionality in a declarative way. It
46 * is designed to allow both a cleaner separation of concerns
47 * and the ability to reuse the component in different ways.
48 *
49 * Component accomplishes these goals by:
50 * - Control how events are bound and unbound.
51 * - Providing patterns for update data cleanly.
52 * - Providing suggestions around updating the interactive portions
matthew.scott 2012/11/08 18:49:46 Minor - 'updating'
53 * of the application.
54 *
55 * @constructor
56 **/
57 initializer: function() {
58 this.modules = {};
59 this.events = {};
60
61 // If container is changed, rebind events.
62 // this.after('containerChange', this.bind());
thiago 2012/11/08 13:33:35 You could remove the commented code.
benjamin.saller 2012/11/09 13:39:21 Done.
63 },
64
65 /**
66 * @method addModule
67 * @chainable
68 * @param {Module} a Module class. Will be created and bound to Component
69 * internally.
70 *
matthew.scott 2012/11/08 18:49:46 Minor - Docs don't match method signature.
benjamin.saller 2012/11/09 13:39:21 Done.
71 * Add a Module to this Component. This will bind its events and set up all
72 * needed event subscriptions.
73 * Modules can return three sets of events that will be bound in
74 * different ways
75 *
76 * - scene: {selector: event-type: handlerName} -> YUI styled event
77 * delegation
78 * - d3 {selector: event-type: handlerName} -> Bound using
79 * specialized d3 event handling
80 * - yui {event-type: handlerName} -> collection of global and custom
81 * events the module reacts to.
82 **/
83
84 addModule: function(ModClassOrInstance, options) {
85 options = options || {};
thiago 2012/11/08 13:33:35 I don't like setting a value to one function param
benjamin.saller 2012/11/09 13:39:21 I agree its questionable, questionable but common.
86 var module = ModClassOrInstance;
87 if (!(ModClassOrInstance instanceof Module)) {
88 module = new ModClassOrInstance();
89 }
90 module.setAttrs({component: this,
91 options: options});
92
93 this.modules[module.name] = module;
thiago 2012/11/08 13:33:35 Funny indentation
benjamin.saller 2012/11/09 13:39:21 fixjsstyle must have done this, fixed.
94
95 var modEvents = module.events;
thiago 2012/11/08 13:33:35 Usually we have only one var per closure. Maybe yo
benjamin.saller 2012/11/09 13:39:21 Done.
96 this.events[module.name] = Y.clone(modEvents);
97 this.bind(module.name);
matthew.scott 2012/11/08 18:49:46 Curious about the necessity for not doing Y.clone(
benjamin.saller 2012/11/09 13:39:21 Its not needed now I think, it was because the Mod
98 return this;
99 },
100
101 /**
102 * @method removeModule
103 * @param {String} moduleName Module name to remove.
104 * @chainable
105 **/
106 removeModule: function(moduleName) {
107 this.unbind(moduleName);
108 delete this.events[moduleName];
109 delete this.modules[moduleName];
110 return this;
111 },
112
113 /**
114 * Internal implementation of
115 * binding both
116 * Module.events.scene and
117 * Module.events.yui.
118 **/
119 _bindEvents: function(modName) {
120 var self = this,
thiago 2012/11/08 13:33:35 It seems self is never used.
benjamin.saller 2012/11/09 13:39:21 Done.
121 modEvents = this.events[modName],
122 module = this.modules[modName],
123 owns = Y.Object.owns,
124 phase = 'on',
matthew.scott 2012/11/08 18:49:46 phase is set and carefully handled, but not used.
benjamin.saller 2012/11/09 13:39:21 It was intended to support before/after events. Of
125 subscriptions = [],
126 handlers,
127 handler;
128
129 function _bindEvent(name, handler, container, selector, context) {
130 // Adapt between d3 events and YUI delegates.
131 var d3Adaptor = function(evt) {
132 var selection = d3.select(evt.currentTarget.getDOMNode()),
133 d = selection.data()[0];
134 // This is a minor violation (extension)
135 // of the interface, but suits us well.
136 d3.event = evt;
137 return handler.call(
138 evt.currentTarget.getDOMNode(), d, context);
139 };
140
141 subscriptions.push(
142 Y.delegate(name, d3Adaptor, container, selector, context));
143 }
144
145 function _normalizeHandler(handler, module, selector) {
146 if (typeof handler === 'object') {
thiago 2012/11/08 13:33:35 Null is also an object... https://developer.mozill
benjamin.saller 2012/11/09 13:39:21 Changed it up, thanks.
147 phase = handler.phase || 'on';
148 handler = handler.callback;
149 }
150 if (typeof handler === 'string') {
151 handler = module[handler];
152 }
153 if (!handler) {
154 console.error('No Event handler for', selector, modName);
155 return;
thiago 2012/11/08 13:33:35 If that is an error, you could throw the exception
benjamin.saller 2012/11/09 13:39:21 Its the sort of error I wanted a developer to noti
156 }
157 if (!L.isFunction(handler)) {
158 console.error('Unable to resolve a proper callback for',
159 selector, handler, modName);
160 return;
thiago 2012/11/08 13:33:35 If that is an error, you could throw the exception
161 }
162 return handler;
163 }
164
165 this.unbind(modName);
166
167 // Bind 'scene' events
168 if (modEvents.scene) {
169 for (var selector in modEvents.scene) {
thiago 2012/11/08 13:33:35 YUI utility?
benjamin.saller 2012/11/09 13:39:21 Reworked, its passing the tests.
170 if (owns(modEvents.scene, selector)) {
171 handlers = modEvents.scene[selector];
172 for (var name in handlers) {
173 if (owns(handlers, name)) {
174 handler = _normalizeHandler(handlers[name], module, selector);
175 if (!handler) {
176 continue;
177 }
178 _bindEvent(name, handler,
179 this.get('container'), selector, this);
180 }
181 }
182 }
183 }
184 }
185
186 // Bind 'yui' custom/global subscriptions
187 // yui: {str: str_or_function}
188 // TODO {str: str/func/obj}
189 // where object includes phase (before, on, after)
matthew.scott 2012/11/08 18:49:46 From above, assuming this is the future use of pha
190 if (modEvents.yui) {
191 var resolvedHandler = {};
thiago 2012/11/08 13:33:35 var declarations should be the first statement in
benjamin.saller 2012/11/09 13:39:21 Moved
192
193 // Resolve any 'string' handlers to methods on module.
194 Y.each(modEvents.yui, function(handler, name) {
195 handler = _normalizeHandler(handler, module);
196 if (!handler) {
197 return;
198 }
199 resolvedHandler[name] = handler;
200 }, this);
201 // Bind resolved event handlers as a group.
202 subscriptions.push(Y.on(resolvedHandler));
203 }
204 return subscriptions;
205 },
206
207 /**
208 * @method bind
209 *
210 * Internal. Called automatically by addModule.
211 **/
212 bind: function(moduleName) {
213 var eventSet = this.events;
214 if (moduleName) {
215 var filtered = {};
thiago 2012/11/08 13:33:35 The blocks of scope in js are crazy. The filtered
benjamin.saller 2012/11/09 13:39:21 The structure here was the result of fighting with
216 filtered[moduleName] = eventSet[moduleName];
217 eventSet = filtered;
218 }
219
220 Y.each(Y.Object.keys(eventSet), function _bind(name) {
thiago 2012/11/08 13:33:35 Do you need to name this function ("_bind")?
benjamin.saller 2012/11/09 13:39:21 Anonymous now.
221 this.events[name].subscriptions = this._bindEvents(name);
222 }, this);
223 return this;
224 },
225
226 /**
227 * Specialized handling of events only found in d3.
228 * This is again an internal implementation detail.
229 *
230 * Its worth noting that d3 events don't use a delegate pattern
231 * and thus must be bound to nodes present in a selection.
232 * For this reason binding d3 events happens after render cycles.
233 *
234 * @method _bindD3Events
235 * @param {String} modName Module name.
236 **/
237 _bindD3Events: function(modName) {
238 // Walk each selector for a given module 'name', doing a
239 // d3 selection and an 'on' binding.
240 var modEvents = this.events[modName];
241
242 if (!modEvents || modEvents.d3 === undefined) {
thiago 2012/11/08 13:33:35 If modEvents.d3 is null you will have a false nega
243 return;
244 }
245
246 modEvents = modEvents.d3;
247 var module = this.modules[modName],
thiago 2012/11/08 13:33:35 Usually we have one "var" per closure.
248 owns = Y.Object.owns;
249
250 var selector, kind, handler,
251 handlers, name;
252
253 for (selector in modEvents) {
thiago 2012/11/08 13:33:35 What about the yui utility classes? You could use
254 if (owns(modEvents, selector)) {
255 handlers = modEvents[selector];
256 for (name in handlers) {
257 if (owns(handlers, name)) {
258 handler = handlers[name];
259 if (typeof handler === 'string') {
260 handler = module[handler];
261 }
262 d3.selectAll(selector).on(name, handler);
263 }
264 }
265 }
266 }
267 },
268
269 /**
270 * @method _unbindD3Events
271 *
272 * Internal Detail. Called by unbind automatically.
273 * D3 events follow a 'slot' like system. Setting the
274 * event to null unbinds existing handlers.
275 **/
276 _unbindD3Events: function(modName) {
277 var modEvents = this.events[modName];
278
279 if (!modEvents || !modEvents.d3) {
280 return;
281 }
282 modEvents = modEvents.d3;
283 var module = this.modules[modName],
284 owns = Y.Object.owns;
285
286 var selector, kind, handler,
287 handlers, name;
288
289 for (selector in modEvents) {
290 if (owns(modEvents, selector)) {
291 handlers = modEvents[selector];
292 for (name in handlers) {
293 if (owns(handlers, name)) {
294 d3.selectAll(selector).on(name, null);
thiago 2012/11/08 13:33:35 This method is really similar to the method above.
benjamin.saller 2012/11/09 13:39:21 They are similar but are called at different times
295 }
296 }
297 }
298 }
299 },
300
301 /**
302 * @method unbind
303 * Internal. Called automatically by removeModule.
304 **/
305 unbind: function(moduleName) {
306 var eventSet = this.events;
307 function _unbind(modEvents) {
308 Y.each(modEvents.subscriptions, function(handler) {
309 if (handler) {
310 handler.detach();
311 }
312 });
313 delete modEvents.subscriptions;
314 }
315
316 if (moduleName) {
317 var filtered = {};
thiago 2012/11/08 13:33:35 One var declaration per closure.
benjamin.saller 2012/11/09 13:39:21 Done.
318 filtered[moduleName] = eventSet[moduleName];
319 eventSet = filtered;
320 }
321 Y.each(Y.Object.values(eventSet), _unbind, this);
322 // Remove any d3 subscriptions as well.
323 this._unbindD3Events();
324
325 return this;
326 },
327
328 /**
329 * @method render
330 * @chainable
331 *
332 * Render each module bound to the canvas
333 */
334 render: function() {
335 var self = this;
thiago 2012/11/08 13:33:35 You dont need 'self'. You pass the 'this' object a
benjamin.saller 2012/11/09 13:39:21 The linter actually complained that 'this.method'
336 function renderAndBind(module, name) {
337 if (module && module.render) {
338 module.render();
339 }
340 self._bindD3Events(name);
341 }
342
343 // If the container isn't bound to the DOM
344 // do so now.
345 this.attachContainer();
346 // Render modules.
347 Y.each(this.modules, renderAndBind, this);
348 return this;
349 },
350
351 /**
352 * @method attachContainer
353 * @chainable
354 *
355 * Called by render, conditionally attach container to the DOM if
356 * it isn't already. The framework calls this before module
357 * rendering so that d3 Events will have attached DOM elements. If
358 * your application doesn't need this behavior feel free to override.
359 **/
360 attachContainer: function() {
361 var container = this.get('container');
362 if (container && !container.inDoc()) {
363 Y.one('body').append(container);
364 }
365 return this;
366 },
367
368 /**
369 * @method detachContainer
370 *
371 * Remove container from DOM returning container. This
372 * is explicitly not chainable.
373 **/
374 detachContainer: function() {
375 var container = this.get('container');
376 if (container.inDoc()) {
377 container.remove();
378 }
379 return container;
380 },
381
382 /**
383 *
384 * @method update
385 * @chainable
386 *
387 * Update the data for each module
388 * see also the dataBinding event hookup
389 */
390 update: function() {
391 Y.each(Y.Object.values(this.modules), function(mod) {
392 mod.update();
393 });
394 return this;
395 }
396 }, {
397 ATTRS: {
398 container: {}
399 }
400
401 });
402 ns.Component = Component;
403 }, '0.1', {
404 'requires': ['d3',
405 'base',
406 'array-extras',
407 'event']});
OLDNEW
« no previous file with comments | « [revision details] ('k') | app/modules.js » ('j') | test/test_d3_components.js » ('J')

Powered by Google App Engine
RSS Feeds Recent Issues | This issue
This is Rietveld f62528b