yellow team mailing list archive
-
yellow team
-
Mailing list archive
-
Message #01492
[Merge] lp:~bcsaller/juju-gui/component-framework into lp:juju-gui
Benjamin Saller has proposed merging lp:~bcsaller/juju-gui/component-framework into lp:juju-gui.
Requested reviews:
Juju GUI Hackers (juju-gui)
For more details, see:
https://code.launchpad.net/~bcsaller/juju-gui/component-framework/+merge/133391
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
Your team Juju GUI Hackers is requested to review the proposed merge of lp:~bcsaller/juju-gui/component-framework into lp:juju-gui.
=== added file 'app/assets/javascripts/d3-components.js'
--- app/assets/javascripts/d3-components.js 1970-01-01 00:00:00 +0000
+++ app/assets/javascripts/d3-components.js 2012-11-08 01:50:28 +0000
@@ -0,0 +1,407 @@
+'use strict';
+
+/**
+ * Provides a declarative structure around interactive D3
+ * applications.
+ *
+ * @module d3-components
+ **/
+
+YUI.add('d3-components', function(Y) {
+ var ns = Y.namespace('d3'),
+ L = Y.Lang,
+ Module = Y.Base.create('Module', Y.Base, [], {
+ /**
+ * @property events
+ * @type {object}
+ **/
+ events: {
+ scene: {},
+ d3: {},
+ yui: {}
+ },
+
+ initializer: function(options) {
+ options = options || {};
+ this.events = options.events ?
+ Y.merge(this.events, options.events) :
+ this.events;
+ }
+ }, {
+ ATTRS: {
+ component: {},
+ container: {getter: function() {
+ return this.get('component').get('container');}}
+ }
+ });
+ ns.Module = Module;
+
+
+ var Component = Y.Base.create('Component', Y.Base, [], {
+ /**
+ * @class Component
+ *
+ * Component collections modules implementing various portions
+ * of an applications functionality in a declarative way. It
+ * is designed to allow both a cleaner separation of concerns
+ * and the ability to reuse the component in different ways.
+ *
+ * Component accomplishes these goals by:
+ * - Control how events are bound and unbound.
+ * - Providing patterns for update data cleanly.
+ * - Providing suggestions around updating the interactive portions
+ * of the application.
+ *
+ * @constructor
+ **/
+ initializer: function() {
+ this.modules = {};
+ this.events = {};
+
+ // If container is changed, rebind events.
+ // this.after('containerChange', this.bind());
+ },
+
+ /**
+ * @method addModule
+ * @chainable
+ * @param {Module} a Module class. Will be created and bound to Component
+ * internally.
+ *
+ * Add a Module to this Component. This will bind its events and set up all
+ * needed event subscriptions.
+ * Modules can return three sets of events that will be bound in
+ * different ways
+ *
+ * - scene: {selector: event-type: handlerName} -> YUI styled event
+ * delegation
+ * - d3 {selector: event-type: handlerName} -> Bound using
+ * specialized d3 event handling
+ * - yui {event-type: handlerName} -> collection of global and custom
+ * events the module reacts to.
+ **/
+
+ addModule: function(ModClassOrInstance, options) {
+ options = options || {};
+ var module = ModClassOrInstance;
+ if (!(ModClassOrInstance instanceof Module)) {
+ module = new ModClassOrInstance();
+ }
+ module.setAttrs({component: this,
+ options: options});
+
+ this.modules[module.name] = module;
+
+ var modEvents = module.events;
+ this.events[module.name] = Y.clone(modEvents);
+ this.bind(module.name);
+ return this;
+ },
+
+ /**
+ * @method removeModule
+ * @param {String} moduleName Module name to remove.
+ * @chainable
+ **/
+ removeModule: function(moduleName) {
+ this.unbind(moduleName);
+ delete this.events[moduleName];
+ delete this.modules[moduleName];
+ return this;
+ },
+
+ /**
+ * Internal implementation of
+ * binding both
+ * Module.events.scene and
+ * Module.events.yui.
+ **/
+ _bindEvents: function(modName) {
+ var self = this,
+ modEvents = this.events[modName],
+ module = this.modules[modName],
+ owns = Y.Object.owns,
+ phase = 'on',
+ subscriptions = [],
+ handlers,
+ handler;
+
+ function _bindEvent(name, handler, container, selector, context) {
+ // Adapt between d3 events and YUI delegates.
+ var d3Adaptor = function(evt) {
+ var selection = d3.select(evt.currentTarget.getDOMNode()),
+ d = selection.data()[0];
+ // This is a minor violation (extension)
+ // of the interface, but suits us well.
+ d3.event = evt;
+ return handler.call(
+ evt.currentTarget.getDOMNode(), d, context);
+ };
+
+ subscriptions.push(
+ Y.delegate(name, d3Adaptor, container, selector, context));
+ }
+
+ function _normalizeHandler(handler, module, selector) {
+ if (typeof handler === 'object') {
+ phase = handler.phase || 'on';
+ handler = handler.callback;
+ }
+ if (typeof handler === 'string') {
+ handler = module[handler];
+ }
+ if (!handler) {
+ console.error('No Event handler for', selector, modName);
+ return;
+ }
+ if (!L.isFunction(handler)) {
+ console.error('Unable to resolve a proper callback for',
+ selector, handler, modName);
+ return;
+ }
+ return handler;
+ }
+
+ this.unbind(modName);
+
+ // Bind 'scene' events
+ if (modEvents.scene) {
+ for (var selector in modEvents.scene) {
+ if (owns(modEvents.scene, selector)) {
+ handlers = modEvents.scene[selector];
+ for (var name in handlers) {
+ if (owns(handlers, name)) {
+ handler = _normalizeHandler(handlers[name], module, selector);
+ if (!handler) {
+ continue;
+ }
+ _bindEvent(name, handler,
+ this.get('container'), selector, this);
+ }
+ }
+ }
+ }
+ }
+
+ // Bind 'yui' custom/global subscriptions
+ // yui: {str: str_or_function}
+ // TODO {str: str/func/obj}
+ // where object includes phase (before, on, after)
+ if (modEvents.yui) {
+ var resolvedHandler = {};
+
+ // Resolve any 'string' handlers to methods on module.
+ Y.each(modEvents.yui, function(handler, name) {
+ handler = _normalizeHandler(handler, module);
+ if (!handler) {
+ return;
+ }
+ resolvedHandler[name] = handler;
+ }, this);
+ // Bind resolved event handlers as a group.
+ subscriptions.push(Y.on(resolvedHandler));
+ }
+ return subscriptions;
+ },
+
+ /**
+ * @method bind
+ *
+ * Internal. Called automatically by addModule.
+ **/
+ bind: function(moduleName) {
+ var eventSet = this.events;
+ if (moduleName) {
+ var filtered = {};
+ filtered[moduleName] = eventSet[moduleName];
+ eventSet = filtered;
+ }
+
+ Y.each(Y.Object.keys(eventSet), function _bind(name) {
+ this.events[name].subscriptions = this._bindEvents(name);
+ }, this);
+ return this;
+ },
+
+ /**
+ * Specialized handling of events only found in d3.
+ * This is again an internal implementation detail.
+ *
+ * Its worth noting that d3 events don't use a delegate pattern
+ * and thus must be bound to nodes present in a selection.
+ * For this reason binding d3 events happens after render cycles.
+ *
+ * @method _bindD3Events
+ * @param {String} modName Module name.
+ **/
+ _bindD3Events: function(modName) {
+ // Walk each selector for a given module 'name', doing a
+ // d3 selection and an 'on' binding.
+ var modEvents = this.events[modName];
+
+ if (!modEvents || modEvents.d3 === undefined) {
+ return;
+ }
+
+ modEvents = modEvents.d3;
+ var module = this.modules[modName],
+ owns = Y.Object.owns;
+
+ var selector, kind, handler,
+ handlers, name;
+
+ for (selector in modEvents) {
+ if (owns(modEvents, selector)) {
+ handlers = modEvents[selector];
+ for (name in handlers) {
+ if (owns(handlers, name)) {
+ handler = handlers[name];
+ if (typeof handler === 'string') {
+ handler = module[handler];
+ }
+ d3.selectAll(selector).on(name, handler);
+ }
+ }
+ }
+ }
+ },
+
+ /**
+ * @method _unbindD3Events
+ *
+ * Internal Detail. Called by unbind automatically.
+ * D3 events follow a 'slot' like system. Setting the
+ * event to null unbinds existing handlers.
+ **/
+ _unbindD3Events: function(modName) {
+ var modEvents = this.events[modName];
+
+ if (!modEvents || !modEvents.d3) {
+ return;
+ }
+ modEvents = modEvents.d3;
+ var module = this.modules[modName],
+ owns = Y.Object.owns;
+
+ var selector, kind, handler,
+ handlers, name;
+
+ for (selector in modEvents) {
+ if (owns(modEvents, selector)) {
+ handlers = modEvents[selector];
+ for (name in handlers) {
+ if (owns(handlers, name)) {
+ d3.selectAll(selector).on(name, null);
+ }
+ }
+ }
+ }
+ },
+
+ /**
+ * @method unbind
+ * Internal. Called automatically by removeModule.
+ **/
+ unbind: function(moduleName) {
+ var eventSet = this.events;
+ function _unbind(modEvents) {
+ Y.each(modEvents.subscriptions, function(handler) {
+ if (handler) {
+ handler.detach();
+ }
+ });
+ delete modEvents.subscriptions;
+ }
+
+ if (moduleName) {
+ var filtered = {};
+ filtered[moduleName] = eventSet[moduleName];
+ eventSet = filtered;
+ }
+ Y.each(Y.Object.values(eventSet), _unbind, this);
+ // Remove any d3 subscriptions as well.
+ this._unbindD3Events();
+
+ return this;
+ },
+
+ /**
+ * @method render
+ * @chainable
+ *
+ * Render each module bound to the canvas
+ */
+ render: function() {
+ var self = this;
+ function renderAndBind(module, name) {
+ if (module && module.render) {
+ module.render();
+ }
+ self._bindD3Events(name);
+ }
+
+ // If the container isn't bound to the DOM
+ // do so now.
+ this.attachContainer();
+ // Render modules.
+ Y.each(this.modules, renderAndBind, this);
+ return this;
+ },
+
+ /**
+ * @method attachContainer
+ * @chainable
+ *
+ * Called by render, conditionally attach container to the DOM if
+ * it isn't already. The framework calls this before module
+ * rendering so that d3 Events will have attached DOM elements. If
+ * your application doesn't need this behavior feel free to override.
+ **/
+ attachContainer: function() {
+ var container = this.get('container');
+ if (container && !container.inDoc()) {
+ Y.one('body').append(container);
+ }
+ return this;
+ },
+
+ /**
+ * @method detachContainer
+ *
+ * Remove container from DOM returning container. This
+ * is explicitly not chainable.
+ **/
+ detachContainer: function() {
+ var container = this.get('container');
+ if (container.inDoc()) {
+ container.remove();
+ }
+ return container;
+ },
+
+ /**
+ *
+ * @method update
+ * @chainable
+ *
+ * Update the data for each module
+ * see also the dataBinding event hookup
+ */
+ update: function() {
+ Y.each(Y.Object.values(this.modules), function(mod) {
+ mod.update();
+ });
+ return this;
+ }
+ }, {
+ ATTRS: {
+ container: {}
+ }
+
+ });
+ ns.Component = Component;
+}, '0.1', {
+ 'requires': ['d3',
+ 'base',
+ 'array-extras',
+ 'event']});
=== modified file 'app/modules.js'
--- app/modules.js 2012-11-01 13:30:58 +0000
+++ app/modules.js 2012-11-08 01:50:28 +0000
@@ -12,6 +12,9 @@
modules: {
'd3': {
'fullpath': '/juju-ui/assets/javascripts/d3.v2.min.js'
+ },
+ 'd3-components': {
+ fullpath: '/juju-ui/assets/javascripts/d3-components.js'
}
}
},
=== modified file 'test/index.html'
--- test/index.html 2012-11-01 13:12:28 +0000
+++ test/index.html 2012-11-08 01:50:28 +0000
@@ -15,6 +15,7 @@
mocha.setup({'ui': 'bdd', 'ignoreLeaks': false})
</script>
+ <script src="test_d3_components.js"></script>
<script src="test_env.js"></script>
<script src="test_model.js"></script>
<script src="test_notifications.js"></script>
=== added file 'test/test_d3_components.js'
--- test/test_d3_components.js 1970-01-01 00:00:00 +0000
+++ test/test_d3_components.js 2012-11-08 01:50:28 +0000
@@ -0,0 +1,171 @@
+'use strict';
+
+describe('d3-components', function() {
+ var Y, NS, TestModule, modA, state,
+ container, comp;
+
+ before(function(done) {
+ Y = YUI(GlobalConfig).use(['d3-components',
+ 'node',
+ 'node-event-simulate'],
+ function(Y) {
+ NS = Y.namespace('d3');
+
+ TestModule = Y.Base.create('TestModule', NS.Module, [], {
+ events: {
+ scene: { '.thing': {click: 'decorateThing'}},
+ d3: {'.target': {click: 'targetTarget'}},
+ yui: {
+ cancel: 'cancelHandler'
+ }
+ },
+
+ decorateThing: function(evt) {
+ state.thing = 'decorated';
+ },
+
+ targetTarget: function(evt) {
+ state.targeted = true;
+ },
+
+ cancelHandler: function(evt) {
+ state.cancelled = true;
+ }
+ });
+
+ done();
+ });
+ });
+
+ beforeEach(function() {
+ container = Y.Node.create('<div id="test" style="visibility: hidden">' +
+ '<button class="thing"></button>' +
+ '<button class="target"></button>' +
+ '</div>');
+ state = {};
+ });
+
+ afterEach(function() {
+ container.remove();
+ container.destroy();
+ if (comp) {
+ comp.unbind();
+ }
+ });
+
+
+ it('should be able to create a component and add a module', function() {
+ comp = new NS.Component();
+ Y.Lang.isValue(comp).should.equal(true);
+ });
+
+ it('should be able to add and remove a module', function() {
+ comp = new NS.Component();
+ comp.setAttrs({container: container});
+ comp.addModule(TestModule);
+ });
+
+ it('should be able to (un)bind module event subscriptions', function() {
+ comp = new NS.Component();
+ comp.setAttrs({container: container});
+ comp.addModule(TestModule);
+
+ // Test that default bindings work by simulating
+ Y.fire('cancel');
+ state.cancelled.should.equal(true);
+
+ // XXX: While on the plane I determined that things like
+ // 'events' are sharing state with other runs/modules.
+ // This must be fixed before this can work again.
+
+ // Manually set state, remove the module and test again
+ state.cancelled = false;
+ comp.removeModule('TestModule');
+
+ Y.fire('cancel');
+ state.cancelled.should.equal(false);
+
+ // Adding the module back again doesn't create any issues.
+ comp.addModule(TestModule);
+ Y.fire('cancel');
+ state.cancelled.should.equal(true);
+
+ // Simulated events on DOM handlers better work.
+ // These require a bound DOM element however
+ comp.render();
+ Y.one('.thing').simulate('click');
+ state.thing.should.equal('decorated');
+ });
+
+ it('should allow event bindings through the use of a declartive object',
+ function() {
+ comp = new NS.Component();
+ comp.setAttrs({container: container});
+
+ // Change test module to use rich captures on some events.
+ // This defines a phase for click (before, after, on (default))
+ // and also shows an inline callback (which is discouraged but allowed)
+ modA = new TestModule();
+ modA.events.scene['.thing'] = {
+ click: {phase: 'after',
+ callback: 'afterThing'},
+ dblclick: {phase: 'on',
+ callback: function(evt) {
+ state.dbldbl = true;
+ }}};
+ modA.afterThing = function(evt) {
+ state.clicked = true;
+ };
+ comp.addModule(modA);
+ comp.render();
+
+ Y.one('.thing').simulate('click');
+ state.clicked.should.equal(true);
+
+ Y.one('.thing').simulate('dblclick');
+ state.dbldbl.should.equal(true);
+
+ });
+
+ it('should support basic rendering from all modules',
+ function() {
+ var modA = new TestModule(),
+ modB = new TestModule();
+
+ comp = new NS.Component();
+ // Give each of these a render method that adds to container
+ modA.name = 'moda';
+ modA.render = function() {
+ this.get('container').append(Y.Node.create('<div id="fromA"></div>'));
+ };
+
+ modB.name = 'modb';
+ modB.render = function() {
+ this.get('container').append(Y.Node.create('<div id="fromB"></div>'));
+ };
+
+ comp.setAttrs({container: container});
+ comp.addModule(modA)
+ .addModule(modB);
+
+ comp.render();
+ Y.Lang.isValue(Y.one('#fromA')).should.equal(true);
+ Y.Lang.isValue(Y.one('#fromB')).should.equal(true);
+ });
+
+ it('should support d3 event bindings post render', function() {
+ comp = new NS.Component();
+ comp.setAttrs({container: container});
+
+ comp.addModule(TestModule);
+
+ comp.render();
+
+ // This is a d3 bound handler that occurs only after render.
+ container.one('.target').simulate('click');
+ state.targeted.should.equal(true);
+ });
+
+});
+
+
Follow ups
-
Re: Micro Framework for Environment View (issue 6828048)
From: Benjamin Saller, 2012-11-13
-
[Merge] lp:~bcsaller/juju-gui/component-framework into lp:juju-gui
From: noreply, 2012-11-13
-
Re: Micro Framework for Environment View (issue 6828048)
From: Thiago Veronezi, 2012-11-09
-
Re: Micro Framework for Environment View (issue 6828048)
From: Matthew Scott, 2012-11-09
-
Re: Micro Framework for Environment View (issue 6828048)
From: Benjamin Saller, 2012-11-09
-
Re: Micro Framework for Environment View (issue 6828048)
From: Benjamin Saller, 2012-11-09
-
Re: Micro Framework for Environment View (issue 6828048)
From: Matthew Scott, 2012-11-08
-
Re: Micro Framework for Environment View (issue 6828048)
From: Thiago Veronezi, 2012-11-08
-
Re: Micro Framework for Environment View (issue 6828048)
From: Thiago Veronezi, 2012-11-08
-
Micro Framework for Environment View (issue 6828048)
From: Benjamin Saller, 2012-11-08
-
[Merge] lp:~bcsaller/juju-gui/component-framework into lp:juju-gui
From: Benjamin Saller, 2012-11-08