Left: | ||
Right: |
OLD | NEW |
---|---|
(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']}); | |
OLD | NEW |