Merge lp:~bcsaller/juju-gui/component-framework into lp:juju-gui/experimental

Proposed by Benjamin Saller
Status: Merged
Merged at revision: 238
Proposed branch: lp:~bcsaller/juju-gui/component-framework
Merge into: lp:juju-gui/experimental
Diff against target: 609 lines (+578/-0)
4 files modified
app/assets/javascripts/d3-components.js (+401/-0)
app/modules.js (+3/-0)
test/index.html (+1/-0)
test/test_d3_components.js (+173/-0)
To merge this branch: bzr merge lp:~bcsaller/juju-gui/component-framework
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+133391@code.launchpad.net

Description of the change

Micro Framework for Environment View

A subclass of Component will container 1 or more modules
which implement their own areas of application concern.

The pattern is that we can declaratively define event listeners
and respond to those across modules. For example A canvas click
might fire an application event 'clickedCanvas' which each module
can have subscribers for that do things like close menus and remove
drag lines, etc.

https://codereview.appspot.com/6828048/

To post a comment you must log in.
Revision history for this message
Benjamin Saller (bcsaller) wrote :

Reviewers: mp+133391_code.launchpad.net,

Message:
Please take a look.

Description:
Micro Framework for Environment View

A subclass of Component will container 1 or more modules
which implement their own areas of application concern.

The pattern is that we can declaratively define event listeners
and respond to those across modules. For example A canvas click
might fire an application event 'clickedCanvas' which each module
can have subscribers for that do things like close menus and remove
drag lines, etc.

https://code.launchpad.net/~bcsaller/juju-gui/component-framework/+merge/133391

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/6828048/

Affected files:
   A [revision details]
   A app/assets/javascripts/d3-components.js
   M app/modules.js
   M test/index.html
   A test/test_d3_components.js

Revision history for this message
Thiago Veronezi (tveronezi) wrote :
Download full text (5.1 KiB)

Hi Ben!

Thanks for this branch. It seems you had a lot of fun doing this. :O)

This is a review for your main file. I still need to look at the tests.

https://codereview.appspot.com/6828048/diff/1/app/assets/javascripts/d3-components.js
File app/assets/javascripts/d3-components.js (right):

https://codereview.appspot.com/6828048/diff/1/app/assets/javascripts/d3-components.js#newcode40
app/assets/javascripts/d3-components.js:40: var Component =
Y.Base.create('Component', Y.Base, [], {
I like this code style. You could the same with the class above. I
mean...

var Module = ...

https://codereview.appspot.com/6828048/diff/1/app/assets/javascripts/d3-components.js#newcode62
app/assets/javascripts/d3-components.js:62: //
this.after('containerChange', this.bind());
You could remove the commented code.

https://codereview.appspot.com/6828048/diff/1/app/assets/javascripts/d3-components.js#newcode85
app/assets/javascripts/d3-components.js:85: options = options || {};
I don't like setting a value to one function parameter.

https://codereview.appspot.com/6828048/diff/1/app/assets/javascripts/d3-components.js#newcode93
app/assets/javascripts/d3-components.js:93: this.modules[module.name] =
module;
Funny indentation

https://codereview.appspot.com/6828048/diff/1/app/assets/javascripts/d3-components.js#newcode95
app/assets/javascripts/d3-components.js:95: var modEvents =
module.events;
Usually we have only one var per closure. Maybe you can declare the
variables in the first line and then attribute values to it here.

https://codereview.appspot.com/6828048/diff/1/app/assets/javascripts/d3-components.js#newcode120
app/assets/javascripts/d3-components.js:120: var self = this,
It seems self is never used.

https://codereview.appspot.com/6828048/diff/1/app/assets/javascripts/d3-components.js#newcode146
app/assets/javascripts/d3-components.js:146: if (typeof handler ===
'object') {
Null is also an object...
https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Operators/typeof

Maybe you could use "Lang" for all the cases here.

https://codereview.appspot.com/6828048/diff/1/app/assets/javascripts/d3-components.js#newcode155
app/assets/javascripts/d3-components.js:155: return;
If that is an error, you could throw the exception.

https://codereview.appspot.com/6828048/diff/1/app/assets/javascripts/d3-components.js#newcode160
app/assets/javascripts/d3-components.js:160: return;
If that is an error, you could throw the exception.

https://codereview.appspot.com/6828048/diff/1/app/assets/javascripts/d3-components.js#newcode169
app/assets/javascripts/d3-components.js:169: for (var selector in
modEvents.scene) {
YUI utility?

https://codereview.appspot.com/6828048/diff/1/app/assets/javascripts/d3-components.js#newcode191
app/assets/javascripts/d3-components.js:191: var resolvedHandler = {};
var declarations should be the first statement in the function body. I
dont know why lint didn't catch this.
http://javascript.crockford.com/code.html

https://codereview.appspot.com/6828048/diff/1/app/assets/javascripts/d3-components.js#newcode215
app/assets/javascripts/d3-components.js:215: var filtered = {};
The blocks of scope in js are crazy. The filtered variable ...

Read more...

Revision history for this message
Thiago Veronezi (tveronezi) wrote :
Revision history for this message
Madison Scott-Clary (makyo) wrote :

This all looks pretty good to me, with a few little
minors/clarifications and Thiago's comments. Eager to see it plugged
in, for sure.

https://codereview.appspot.com/6828048/diff/1/app/assets/javascripts/d3-components.js
File app/assets/javascripts/d3-components.js (right):

https://codereview.appspot.com/6828048/diff/1/app/assets/javascripts/d3-components.js#newcode44
app/assets/javascripts/d3-components.js:44: * Component collections
modules implementing various portions
Minor - should be 'collects', I think? Was just confusing.

https://codereview.appspot.com/6828048/diff/1/app/assets/javascripts/d3-components.js#newcode52
app/assets/javascripts/d3-components.js:52: * - Providing suggestions
around updating the interactive portions
Minor - 'updating'

https://codereview.appspot.com/6828048/diff/1/app/assets/javascripts/d3-components.js#newcode70
app/assets/javascripts/d3-components.js:70: *
Minor - Docs don't match method signature.

https://codereview.appspot.com/6828048/diff/1/app/assets/javascripts/d3-components.js#newcode97
app/assets/javascripts/d3-components.js:97: this.bind(module.name);
Curious about the necessity for not doing Y.clone(module.events); here.
Clarity?

https://codereview.appspot.com/6828048/diff/1/app/assets/javascripts/d3-components.js#newcode124
app/assets/javascripts/d3-components.js:124: phase = 'on',
phase is set and carefully handled, but not used. Is this a future
implementation thing?

https://codereview.appspot.com/6828048/diff/1/app/assets/javascripts/d3-components.js#newcode189
app/assets/javascripts/d3-components.js:189: // where object
includes phase (before, on, after)
 From above, assuming this is the future use of phase?

https://codereview.appspot.com/6828048/

Revision history for this message
Benjamin Saller (bcsaller) wrote :
Download full text (7.4 KiB)

Thank you both for the review feedback. I've made the changes and will
propose again after getting these comments out.

The most significant change is the expanded handling around event
phases.

https://codereview.appspot.com/6828048/diff/1/app/assets/javascripts/d3-components.js
File app/assets/javascripts/d3-components.js (right):

https://codereview.appspot.com/6828048/diff/1/app/assets/javascripts/d3-components.js#newcode40
app/assets/javascripts/d3-components.js:40: var Component =
Y.Base.create('Component', Y.Base, [], {
On 2012/11/08 13:33:35, thiago wrote:
> I like this code style. You could the same with the class above. I
mean...

> var Module = ...

The linter complained that I didn't combine the var decls into one set,
I prefer the other style as well. I'll see if I cna get lint to shut up.

https://codereview.appspot.com/6828048/diff/1/app/assets/javascripts/d3-components.js#newcode62
app/assets/javascripts/d3-components.js:62: //
this.after('containerChange', this.bind());
On 2012/11/08 13:33:35, thiago wrote:
> You could remove the commented code.

Done.

https://codereview.appspot.com/6828048/diff/1/app/assets/javascripts/d3-components.js#newcode70
app/assets/javascripts/d3-components.js:70: *
On 2012/11/08 18:49:46, matthew.scott wrote:
> Minor - Docs don't match method signature.

Done.

https://codereview.appspot.com/6828048/diff/1/app/assets/javascripts/d3-components.js#newcode85
app/assets/javascripts/d3-components.js:85: options = options || {};
On 2012/11/08 13:33:35, thiago wrote:
> I don't like setting a value to one function parameter.

I agree its questionable, questionable but common. Still, I can alter
it.

https://codereview.appspot.com/6828048/diff/1/app/assets/javascripts/d3-components.js#newcode93
app/assets/javascripts/d3-components.js:93: this.modules[module.name] =
module;
On 2012/11/08 13:33:35, thiago wrote:
> Funny indentation

fixjsstyle must have done this, fixed.

https://codereview.appspot.com/6828048/diff/1/app/assets/javascripts/d3-components.js#newcode95
app/assets/javascripts/d3-components.js:95: var modEvents =
module.events;
On 2012/11/08 13:33:35, thiago wrote:
> Usually we have only one var per closure. Maybe you can declare the
variables in
> the first line and then attribute values to it here.

Done.

https://codereview.appspot.com/6828048/diff/1/app/assets/javascripts/d3-components.js#newcode97
app/assets/javascripts/d3-components.js:97: this.bind(module.name);
On 2012/11/08 18:49:46, matthew.scott wrote:
> Curious about the necessity for not doing Y.clone(module.events);
here.
> Clarity?

Its not needed now I think, it was because the Module was returning its
real dict and so there was a Module level instance with shared state
being used. This could leave subscriptions bound to all instances.
Module was changed to do the shallow copy of events in its init method
and this was removed.

https://codereview.appspot.com/6828048/diff/1/app/assets/javascripts/d3-components.js#newcode120
app/assets/javascripts/d3-components.js:120: var self = this,
On 2012/11/08 13:33:35, thiago wrote:
> It seems self is never used.

Done.

https://codereview.appspot.com/6828048/diff/1/app/assets/javascripts/d3-...

Read more...

227. By Benjamin Saller

use yui methods for iteration more places, support phase event bindings for custom events

228. By Benjamin Saller

lint

Revision history for this message
Benjamin Saller (bcsaller) wrote :
229. By Benjamin Saller

basic assertion around addModule

Revision history for this message
Madison Scott-Clary (makyo) wrote :

Thanks for the changes! Looks good to me.

https://codereview.appspot.com/6828048/

Revision history for this message
Thiago Veronezi (tveronezi) wrote :

On 2012/11/09 18:07:59, matthew.scott wrote:
> Thanks for the changes! Looks good to me.

Thanks, Ben. It looks good to me too.

https://codereview.appspot.com/6828048/

Revision history for this message
Benjamin Saller (bcsaller) wrote :

*** Submitted:

Micro Framework for Environment View

A subclass of Component will container 1 or more modules
which implement their own areas of application concern.

The pattern is that we can declaratively define event listeners
and respond to those across modules. For example A canvas click
might fire an application event 'clickedCanvas' which each module
can have subscribers for that do things like close menus and remove
drag lines, etc.

R=thiago, matthew.scott, benjamin.saller
CC=
https://codereview.appspot.com/6828048

https://codereview.appspot.com/6828048/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'app/assets/javascripts/d3-components.js'
2--- app/assets/javascripts/d3-components.js 1970-01-01 00:00:00 +0000
3+++ app/assets/javascripts/d3-components.js 2012-11-09 14:20:28 +0000
4@@ -0,0 +1,401 @@
5+'use strict';
6+
7+/**
8+ * Provides a declarative structure around interactive D3
9+ * applications.
10+ *
11+ * @module d3-components
12+ **/
13+
14+YUI.add('d3-components', function(Y) {
15+ var ns = Y.namespace('d3'),
16+ L = Y.Lang;
17+
18+ var Module = Y.Base.create('Module', Y.Base, [], {
19+ /**
20+ * @property events
21+ * @type {object}
22+ **/
23+ events: {
24+ scene: {},
25+ d3: {},
26+ yui: {}
27+ },
28+
29+ initializer: function() {
30+ this.events = Y.merge(this.events);
31+ }
32+ }, {
33+ ATTRS: {
34+ component: {},
35+ options: {},
36+ container: {getter: function() {
37+ return this.get('component').get('container');}}
38+ }
39+ });
40+ ns.Module = Module;
41+
42+
43+ var Component = Y.Base.create('Component', Y.Base, [], {
44+ /**
45+ * @class Component
46+ *
47+ * Component collects modules implementing various portions of an
48+ * applications functionality in a declarative way. It is designed to allow
49+ * both a cleaner separation of concerns and the ability to reuse the
50+ * component in different ways.
51+ *
52+ * Component accomplishes these goals by:
53+ * - Control how events are bound and unbound.
54+ * - Providing patterns for update data cleanly.
55+ * - Providing suggestions around updating the interactive portions
56+ * of the application.
57+ *
58+ * @constructor
59+ **/
60+ initializer: function() {
61+ this.modules = {};
62+ this.events = {};
63+ },
64+
65+ /**
66+ * @method addModule
67+ * @chainable
68+ * @param {Module} ModClassOrInstance bound will be to this.
69+ * @param {Object} options dict of options set as options attribute on
70+ * module.
71+ *
72+ * Add a Module to this Component. This will bind its events and set up all
73+ * needed event subscriptions. Modules can return three sets of events
74+ * that will be bound in 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+ var config = options || {},
86+ module = ModClassOrInstance,
87+ modEvents;
88+
89+ if (!(ModClassOrInstance instanceof Module)) {
90+ module = new ModClassOrInstance();
91+ }
92+ module.setAttrs({
93+ component: this,
94+ options: config});
95+
96+ this.modules[module.name] = module;
97+
98+ modEvents = module.events;
99+ this.events[module.name] = modEvents;
100+ this.bind(module.name);
101+ return this;
102+ },
103+
104+ /**
105+ * @method removeModule
106+ * @param {String} moduleName Module name to remove.
107+ * @chainable
108+ **/
109+ removeModule: function(moduleName) {
110+ this.unbind(moduleName);
111+ delete this.events[moduleName];
112+ delete this.modules[moduleName];
113+ return this;
114+ },
115+
116+ /**
117+ * Internal implementation of
118+ * binding both
119+ * Module.events.scene and
120+ * Module.events.yui.
121+ **/
122+ _bindEvents: function(modName) {
123+ var self = this,
124+ modEvents = this.events[modName],
125+ module = this.modules[modName],
126+ owns = Y.Object.owns,
127+ container = this.get('container'),
128+ subscriptions = [],
129+ handlers,
130+ handler;
131+
132+ function _bindEvent(name, handler, container, selector, context) {
133+ // Adapt between d3 events and YUI delegates.
134+ var d3Adaptor = function(evt) {
135+ var selection = d3.select(evt.currentTarget.getDOMNode()),
136+ d = selection.data()[0];
137+ // This is a minor violation (extension)
138+ // of the interface, but suits us well.
139+ d3.event = evt;
140+ return handler.call(
141+ evt.currentTarget.getDOMNode(), d, context);
142+ };
143+
144+ subscriptions.push(
145+ Y.delegate(name, d3Adaptor, container, selector, context));
146+ }
147+
148+ // Return a resolved handler object in the form
149+ // {phase: str, callback: function}
150+ function _normalizeHandler(handler, module, selector) {
151+ var result = {};
152+
153+ if (L.isString(handler)) {
154+ result.callback = module[handler];
155+ result.phase = 'on';
156+ }
157+
158+ if (L.isObject(handler)) {
159+ result.phase = handler.phase || 'on';
160+ result.callback = handler.callback;
161+ }
162+
163+ if (L.isString(result.callback)) {
164+ result.callback = module[result.callback];
165+ }
166+
167+ if (!result.callback) {
168+ console.error('No Event handler for', selector, modName);
169+ return;
170+ }
171+ if (!L.isFunction(result.callback)) {
172+ console.error('Unable to resolve a proper callback for',
173+ selector, handler, modName, result);
174+ return;
175+ }
176+ return result;
177+ }
178+
179+ this.unbind(modName);
180+
181+ // Bind 'scene' events
182+ Y.each(modEvents.scene, function(handlers, selector, sceneEvents) {
183+ Y.each(handlers, function(handler, trigger) {
184+ handler = _normalizeHandler(handler, module, selector);
185+ if (L.isValue(handler)) {
186+ _bindEvent(trigger, handler.callback, container, selector, self);
187+ }
188+ });
189+ });
190+
191+ // Bind 'yui' custom/global subscriptions
192+ // yui: {str: str_or_function}
193+ // TODO {str: str/func/obj}
194+ // where object includes phase (before, on, after)
195+ if (modEvents.yui) {
196+ // Resolve any 'string' handlers to methods on module.
197+ Y.each(['after', 'before', 'on'], function(eventPhase) {
198+ var resolvedHandler = {};
199+ Y.each(modEvents.yui, function(handler, name) {
200+ handler = _normalizeHandler(handler, module);
201+ if (!handler || handler.phase !== eventPhase) {
202+ return;
203+ }
204+ resolvedHandler[name] = handler.callback;
205+ }, this);
206+ // Bind resolved event handlers as a group.
207+ if (Y.Object.keys(resolvedHandler).length) {
208+ subscriptions.push(Y[eventPhase](resolvedHandler));
209+ }
210+ });
211+ }
212+ return subscriptions;
213+ },
214+
215+ /**
216+ * @method bind
217+ *
218+ * Internal. Called automatically by addModule.
219+ **/
220+ bind: function(moduleName) {
221+ var eventSet = this.events,
222+ filtered = {};
223+
224+ if (moduleName) {
225+ filtered[moduleName] = eventSet[moduleName];
226+ eventSet = filtered;
227+ }
228+
229+ Y.each(Y.Object.keys(eventSet), function(name) {
230+ this.events[name].subscriptions = this._bindEvents(name);
231+ }, this);
232+ return this;
233+ },
234+
235+ /**
236+ * Specialized handling of events only found in d3.
237+ * This is again an internal implementation detail.
238+ *
239+ * Its worth noting that d3 events don't use a delegate pattern
240+ * and thus must be bound to nodes present in a selection.
241+ * For this reason binding d3 events happens after render cycles.
242+ *
243+ * @method _bindD3Events
244+ * @param {String} modName Module name.
245+ **/
246+ _bindD3Events: function(modName) {
247+ // Walk each selector for a given module 'name', doing a
248+ // d3 selection and an 'on' binding.
249+ var modEvents = this.events[modName],
250+ owns = Y.Object.owns,
251+ module;
252+ if (!modEvents || !modEvents.d3) {
253+ return;
254+ }
255+ modEvents = modEvents.d3;
256+ module = this.modules[modName];
257+
258+ function _normalizeHandler(handler, module) {
259+ if (handler && !L.isFunction(handler)) {
260+ handler = module[handler];
261+ }
262+ return handler;
263+ }
264+
265+ Y.each(modEvents, function(handlers, selector) {
266+ Y.each(handlers, function(handler, trigger) {
267+ handler = _normalizeHandler(handler, module);
268+ d3.selectAll(selector).on(trigger, handler);
269+ });
270+ });
271+ },
272+
273+ /**
274+ * @method _unbindD3Events
275+ *
276+ * Internal Detail. Called by unbind automatically.
277+ * D3 events follow a 'slot' like system. Setting the
278+ * event to null unbinds existing handlers.
279+ **/
280+ _unbindD3Events: function(modName) {
281+ var modEvents = this.events[modName],
282+ owns = Y.Object.owns,
283+ module;
284+
285+ if (!modEvents || !modEvents.d3) {
286+ return;
287+ }
288+ modEvents = modEvents.d3;
289+ module = this.modules[modName];
290+
291+ Y.each(modEvents, function(handlers, selector) {
292+ Y.each(handlers, function(handler, trigger) {
293+ d3.selectAll(selector).on(trigger, null);
294+ });
295+ });
296+ },
297+
298+ /**
299+ * @method unbind
300+ * Internal. Called automatically by removeModule.
301+ **/
302+ unbind: function(moduleName) {
303+ var eventSet = this.events,
304+ filtered = {};
305+
306+ function _unbind(modEvents) {
307+ Y.each(modEvents.subscriptions, function(handler) {
308+ if (handler) {
309+ handler.detach();
310+ }
311+ });
312+ delete modEvents.subscriptions;
313+ }
314+
315+ if (moduleName) {
316+ filtered[moduleName] = eventSet[moduleName];
317+ eventSet = filtered;
318+ }
319+ Y.each(Y.Object.values(eventSet), _unbind, this);
320+ // Remove any d3 subscriptions as well.
321+ this._unbindD3Events();
322+
323+ return this;
324+ },
325+
326+ /**
327+ * @method render
328+ * @chainable
329+ *
330+ * Render each module bound to the canvas
331+ */
332+ render: function() {
333+ var self = this;
334+ function renderAndBind(module, name) {
335+ if (module && module.render) {
336+ module.render();
337+ }
338+ self._bindD3Events(name);
339+ }
340+
341+ // If the container isn't bound to the DOM
342+ // do so now.
343+ this.attachContainer();
344+ // Render modules.
345+ Y.each(this.modules, renderAndBind, this);
346+ return this;
347+ },
348+
349+ /**
350+ * @method attachContainer
351+ * @chainable
352+ *
353+ * Called by render, conditionally attach container to the DOM if
354+ * it isn't already. The framework calls this before module
355+ * rendering so that d3 Events will have attached DOM elements. If
356+ * your application doesn't need this behavior feel free to override.
357+ **/
358+ attachContainer: function() {
359+ var container = this.get('container');
360+ if (container && !container.inDoc()) {
361+ Y.one('body').append(container);
362+ }
363+ return this;
364+ },
365+
366+ /**
367+ * @method detachContainer
368+ *
369+ * Remove container from DOM returning container. This
370+ * is explicitly not chainable.
371+ **/
372+ detachContainer: function() {
373+ var container = this.get('container');
374+ if (container.inDoc()) {
375+ container.remove();
376+ }
377+ return container;
378+ },
379+
380+ /**
381+ *
382+ * @method update
383+ * @chainable
384+ *
385+ * Update the data for each module
386+ * see also the dataBinding event hookup
387+ */
388+ update: function() {
389+ Y.each(Y.Object.values(this.modules), function(mod) {
390+ mod.update();
391+ });
392+ return this;
393+ }
394+ }, {
395+ ATTRS: {
396+ container: {}
397+ }
398+
399+ });
400+ ns.Component = Component;
401+}, '0.1', {
402+ 'requires': ['d3',
403+ 'base',
404+ 'array-extras',
405+ 'event']});
406
407=== modified file 'app/modules.js'
408--- app/modules.js 2012-11-01 13:30:58 +0000
409+++ app/modules.js 2012-11-09 14:20:28 +0000
410@@ -12,6 +12,9 @@
411 modules: {
412 'd3': {
413 'fullpath': '/juju-ui/assets/javascripts/d3.v2.min.js'
414+ },
415+ 'd3-components': {
416+ fullpath: '/juju-ui/assets/javascripts/d3-components.js'
417 }
418 }
419 },
420
421=== modified file 'test/index.html'
422--- test/index.html 2012-11-01 13:12:28 +0000
423+++ test/index.html 2012-11-09 14:20:28 +0000
424@@ -15,6 +15,7 @@
425 mocha.setup({'ui': 'bdd', 'ignoreLeaks': false})
426 </script>
427
428+ <script src="test_d3_components.js"></script>
429 <script src="test_env.js"></script>
430 <script src="test_model.js"></script>
431 <script src="test_notifications.js"></script>
432
433=== added file 'test/test_d3_components.js'
434--- test/test_d3_components.js 1970-01-01 00:00:00 +0000
435+++ test/test_d3_components.js 2012-11-09 14:20:28 +0000
436@@ -0,0 +1,173 @@
437+'use strict';
438+
439+describe('d3-components', function() {
440+ var Y, NS, TestModule, modA, state,
441+ container, comp;
442+
443+ before(function(done) {
444+ Y = YUI(GlobalConfig).use(['d3-components',
445+ 'node',
446+ 'node-event-simulate'],
447+ function(Y) {
448+ NS = Y.namespace('d3');
449+
450+ TestModule = Y.Base.create('TestModule', NS.Module, [], {
451+ events: {
452+ scene: { '.thing': {click: 'decorateThing'}},
453+ d3: {'.target': {click: 'targetTarget'}},
454+ yui: {
455+ cancel: 'cancelHandler'
456+ }
457+ },
458+
459+ decorateThing: function(evt) {
460+ state.thing = 'decorated';
461+ },
462+
463+ targetTarget: function(evt) {
464+ state.targeted = true;
465+ },
466+
467+ cancelHandler: function(evt) {
468+ state.cancelled = true;
469+ }
470+ });
471+
472+ done();
473+ });
474+ });
475+
476+ beforeEach(function() {
477+ container = Y.Node.create('<div id="test" style="visibility: hidden">' +
478+ '<button class="thing"></button>' +
479+ '<button class="target"></button>' +
480+ '</div>');
481+ state = {};
482+ });
483+
484+ afterEach(function() {
485+ container.remove();
486+ container.destroy();
487+ if (comp) {
488+ comp.unbind();
489+ }
490+ });
491+
492+
493+ it('should be able to create a component and add a module', function() {
494+ comp = new NS.Component();
495+ Y.Lang.isValue(comp).should.equal(true);
496+ });
497+
498+ it('should be able to add and remove a module', function() {
499+ comp = new NS.Component();
500+ comp.setAttrs({container: container});
501+ comp.addModule(TestModule);
502+ Y.Lang.isValue(comp.events).should.equal(true);
503+ Y.Lang.isValue(comp.modules).should.equal(true);
504+ });
505+
506+ it('should be able to (un)bind module event subscriptions', function() {
507+ comp = new NS.Component();
508+ comp.setAttrs({container: container});
509+ comp.addModule(TestModule);
510+
511+ // Test that default bindings work by simulating
512+ Y.fire('cancel');
513+ state.cancelled.should.equal(true);
514+
515+ // XXX: While on the plane I determined that things like
516+ // 'events' are sharing state with other runs/modules.
517+ // This must be fixed before this can work again.
518+
519+ // Manually set state, remove the module and test again
520+ state.cancelled = false;
521+ comp.removeModule('TestModule');
522+
523+ Y.fire('cancel');
524+ state.cancelled.should.equal(false);
525+
526+ // Adding the module back again doesn't create any issues.
527+ comp.addModule(TestModule);
528+ Y.fire('cancel');
529+ state.cancelled.should.equal(true);
530+
531+ // Simulated events on DOM handlers better work.
532+ // These require a bound DOM element however
533+ comp.render();
534+ Y.one('.thing').simulate('click');
535+ state.thing.should.equal('decorated');
536+ });
537+
538+ it('should allow event bindings through the use of a declartive object',
539+ function() {
540+ comp = new NS.Component();
541+ comp.setAttrs({container: container});
542+
543+ // Change test module to use rich captures on some events.
544+ // This defines a phase for click (before, after, on (default))
545+ // and also shows an inline callback (which is discouraged but allowed)
546+ modA = new TestModule();
547+ modA.events.scene['.thing'] = {
548+ click: {phase: 'after',
549+ callback: 'afterThing'},
550+ dblclick: {phase: 'on',
551+ callback: function(evt) {
552+ state.dbldbl = true;
553+ }}};
554+ modA.afterThing = function(evt) {
555+ state.clicked = true;
556+ };
557+ comp.addModule(modA);
558+ comp.render();
559+
560+ Y.one('.thing').simulate('click');
561+ state.clicked.should.equal(true);
562+
563+ Y.one('.thing').simulate('dblclick');
564+ state.dbldbl.should.equal(true);
565+
566+ });
567+
568+ it('should support basic rendering from all modules',
569+ function() {
570+ var modA = new TestModule(),
571+ modB = new TestModule();
572+
573+ comp = new NS.Component();
574+ // Give each of these a render method that adds to container
575+ modA.name = 'moda';
576+ modA.render = function() {
577+ this.get('container').append(Y.Node.create('<div id="fromA"></div>'));
578+ };
579+
580+ modB.name = 'modb';
581+ modB.render = function() {
582+ this.get('container').append(Y.Node.create('<div id="fromB"></div>'));
583+ };
584+
585+ comp.setAttrs({container: container});
586+ comp.addModule(modA)
587+ .addModule(modB);
588+
589+ comp.render();
590+ Y.Lang.isValue(Y.one('#fromA')).should.equal(true);
591+ Y.Lang.isValue(Y.one('#fromB')).should.equal(true);
592+ });
593+
594+ it('should support d3 event bindings post render', function() {
595+ comp = new NS.Component();
596+ comp.setAttrs({container: container});
597+
598+ comp.addModule(TestModule);
599+
600+ comp.render();
601+
602+ // This is a d3 bound handler that occurs only after render.
603+ container.one('.target').simulate('click');
604+ state.targeted.should.equal(true);
605+ });
606+
607+});
608+
609+

Subscribers

People subscribed via source and target branches