From fdb1348b39fcc090f836040ce0a9a83cef41d10a Mon Sep 17 00:00:00 2001 From: rolux Date: Tue, 26 Jun 2012 16:33:50 +0200 Subject: [PATCH] improve design patterns example --- .../ui/widget_design_patterns/js/example.js | 164 +++++++++++++----- 1 file changed, 117 insertions(+), 47 deletions(-) diff --git a/examples/ui/widget_design_patterns/js/example.js b/examples/ui/widget_design_patterns/js/example.js index d8ab3c80..884f0d8a 100644 --- a/examples/ui/widget_design_patterns/js/example.js +++ b/examples/ui/widget_design_patterns/js/example.js @@ -1,7 +1,11 @@ /* -The following examples explain the common design pattern for OxJS UI widgets: an -inheritance model that is neither classical nor prototypal, but "parasitical" (a -term coined by +**Parasitical Inheritance** +*/ +/* +The following examples illustrate the common design pattern for `OxJS` UI +widgets: an inheritance model that is neither classical nor prototypal, but +"parasitical" (a term coined by Douglas Crockford). In a nutshell, "instances" are created by augmenting other instances, but in addition to private members (`var foo`) and public members @@ -14,13 +18,15 @@ be accessed from outside, but since `self` itself is an argument of the /* -Create our own namespace. Not required, but if you wanted to create a -module named `My`, this is how you would do it. +Create our own namespace. Not required, but if we wanted to create a module +named `My`, this is how we would do it. */ Ox.My = {}; /* -
+**Box** +*/ +/* First, lets build the most basic Box widget. A widget is a "constructor" function that takes two (optional) arguments, `options` and `self`, and returns a widget object. It's not a constructor in JavaScript terms though: It doesn't @@ -145,7 +151,9 @@ Ox.My.Box = function(options, self) { }; /* -
+**InvertibleBox** +*/ +/* Now we can "subclass" our Box. Let's build one that can have its color inverted. */ Ox.My.InvertibleBox = function(options, self) { @@ -264,7 +272,9 @@ Ox.My.InvertibleBox = function(options, self) { }; /* -
+**MetaBox** +*/ +/* Now it's time for something more funky: A MetaBox — that is, a box of boxes. */ @@ -273,7 +283,7 @@ Ox.My.MetaBox = function(options, self) { /* This time, we inherit from `Ox.My.InvertibleBox`. The one thing that's different though is the `color` option: It is no longer a single value, but - an array of array of values. That's how the boxes inside out meta-box are + an array of array of values. That's how the boxes inside out MetaBox are specified. The following would create a grid of boxes with two rows and three columns:
@@ -293,7 +303,7 @@ Ox.My.MetaBox = function(options, self) {
     /*
     But we keep the default color of `Ox.My.InvertibleBox` (`[128, 128, 128]`)
     as our own default color, and only here check if the color option is a
-    single value. In that case, we convert it to an array of one row and one
+    single RGB value. In that case, we convert it to an array of one row and one
     column. This way, whenever someone accidentally passes a single color value,
     our MetaBox can handle it.
     */
@@ -314,10 +324,11 @@ Ox.My.MetaBox = function(options, self) {
     ];
 
     /*
-    `self.$boxes` are the actual boxes. We use `Ox.My.InvertibleBox`, but
-    remove their `doubleclick` handlers, since our meta-box already has one. 
-    (`unbindEvent(event)` removes all handlers, `unbindEvent(event, handler)`
-    removes a specific one.) Then we simply append each box to the meta-box.
+    `self.$boxes` are the actual boxes. We use `Ox.My.InvertibleBox`, but remove
+    the `doubleclick` handlers, since our MetaBox already has one, being an
+    InvertibleBox itself. (`unbindEvent(event)` removes all handlers,
+    `unbindEvent(event, handler)` removes a specific one.) Then we simply append
+    each box to the meta-box.
     */
     self.$boxes = self.options.color.map(function(array, y) {
         return array.map(function(color, x) {
@@ -372,29 +383,42 @@ Ox.My.MetaBox = function(options, self) {
 };
 
 /*
-
+**PixelBox** +*/ +/* The next widget is a peculiar type of meta-box. A PixelBox has only one color, -but this color will be split into a red box, a green box and a blue box. +but this color will be split up into a red box, a green box and a blue box. */ Ox.My.PixelBox = function(options, self) { self = self || {}; - /* - ... - */ - self.options = Ox.extend(Ox.My.Box().defaults(), options || {}); - var that = Ox.My.MetaBox(Ox.extend(self.options, { - color: getColor() - }), self) - .update({ - color: setColor - }); /* - This is how the color gets split up into a red box, a green box and a blue - box. + The challenge here is that we want our PixelBox to be an instance of + `Ox.My.MetaBox`, but with a `color` option like `Ox.My.Box`. So we have to + parse the options ourselves, by first extending the defaults of `Ox.My.Box` + and then transforming the single-value `color` option into a multi-value + `color` option. Calling
+ `Ox.My.PixelBox().options('color')`
+ will now return
+ `[[[128, 0, 0], [0, 128, 0], [0, 0, 128]]]`. */ - function getColor() { + self.options = Ox.extend(Ox.My.Box().defaults(), options || {}); + self.options.color = getColor(); + /* + Now we can pass `self.options` to `Ox.My.MetaBox`. + */ + var that = Ox.My.MetaBox(self.options, self) + /* + Again, we add a custom handler for `color` updates. + */ + .update({color: setColor}); + + /* + This is how a single RGB value gets split up into a red box, a green box and + a blue box. + */ + function getColor(color) { return [[ [self.options.color[0], 0, 0], [0, self.options.color[1], 0], @@ -403,7 +427,11 @@ Ox.My.PixelBox = function(options, self) { } /* - ... + When the `color` option gets updated to a new single value, we update it + again, this time to multiple values, and return `false` to keep the MetaBox + handler from running. We have updated `color`, so our handler will get + called again, but now it does nothing, and the MetaBox handler will get + invoked. */ function setColor() { if (Ox.isNumber(self.options.color[0])) { @@ -429,12 +457,17 @@ Ox.My.PixelBox = function(options, self) { }); }; + /* + And that's the PixelBox. + */ return that; }; /* -
+**ImageBox** +*/ +/* And finally — a meta-meta-box! An ImageBox takes an image and, for each pixel, displays a PixelBox. */ @@ -447,6 +480,12 @@ Ox.My.ImageBox = function(options, self) { its `displayText` method. */ var that = Ox.My.Box({}, self).displayText('Loading...'); + /* + It's not required to define empty defaults — omitting them would + simply leave them undefined). Still, to add an explicit `null` default is a + good practice, as it makes it obvious to any reader of our code that + `Ox.My.ImageBox` expects an `image` option. + */ that.defaults(Ox.extend(that.defaults(), { image: null })) @@ -500,29 +539,40 @@ Ox.My.ImageBox = function(options, self) { We've inherited from `Ox.My.Box`, so we don't have an `invert` method yet. This is how we can borrow the one from `Ox.My.MetaBox`. We're passing our own `self`, so the `self.$boxes` that the `invert` method of `Ox.My.MetaBox` - operates on will be the PixelBoxes that we are assinging in the asynchronous - callback above. + operates on will be the PixelBoxes that we are assigning in the asynchronous + callback above. (This pattern is somewhat analogous to the + `someOtherObject.method.apply(this, args)` idiom that is common in + JavaScript.) - This is somewhat analogous to the -
-    someOtherObject.method.apply(this, args)
-    
- pattern that is common in JavaScript. + Note that we have to pass `self.options` too, otherwise our own + `self.options.size` would get overwritten by the MetaBox default size. - Note that we have to pass `self.options` too, otherwise our own size would - get overwritten by the MetaBox default size. + Passing `self` to `Ox.My.MetaBox` has another nice effect: the MetaBox will + add its own event handlers to it. So even though `Ox.My.ImageBox` is just an + `Ox.My.Box`, it has now inherited `doubleclick` handling from + `Ox.My.MetaBox`. + + (Internally, `Ox.Element` stores event handlers in `self`. So what happens + is this: `self` gets passed all the way down from `Ox.My.ImageBox` to + `Ox.My.MetaBox` to `Ox.My.InvertibleBox` to `Ox.My.Box` to Ox.Element, and + when `Ox.My.InvertibleBox` defines its `doubleclick` handler, it ends up on + `self`. So when the Ox.Element that is actually in the DOM — the + `Ox.My.Box` that `Ox.My.ImageBox` inherits from, which shares the same + `self` — receives a `doubleclick`, there is now a handler for it.) */ that.invert = Ox.My.MetaBox(self.options, self).invert; /* - But wait — where's our `doubleclick` handler? + And that's it. */ return that; }; /* -
+**VideoBox** +*/ +/* This one is left as an exercise to the reader ;) */ Ox.My.VideoBox = function(options, self) { @@ -530,13 +580,26 @@ Ox.My.VideoBox = function(options, self) { }; /* -
-Load the UI and Image modules. +**Demo** +*/ +/* +Load the Image and UI modules. */ Ox.load(['Image', 'UI'], function() { + /* + Pick a random color. Ox.rgb will convert HSL to RGB. + */ var h = Ox.random(360), s = 1, l = 0.5; + /* + Create a global variable, so that we can play with our widgets in the + console. + */ window.My = {}; - My.$backgroundBox = Ox.My.Box({ + /* + Since `Ox.My.Box` is a good multi-purpose container, we create one to + contain the first four boxes. + */ + My.$container = Ox.My.Box({ size: [256, 256] }) .append( @@ -558,16 +621,23 @@ Ox.load(['Image', 'UI'], function() { }) ) .appendTo(Ox.$body); + /* + The ImageBox is a bit larger. + */ My.$imageBox = Ox.My.ImageBox({ image: 'png/pandora32.png', size: [256, 256] }) .appendTo(Ox.$body); + /* + As a last step, we add a handler to the `invert` event of each widgets. It + will display the widget name inside the `Ox.My.Box`. + */ Ox.forEach(My, function($element, name) { $element.bindEvent({ invert: function() { My.$box.displayText(name[1].toUpperCase() + name.slice(2)); } - }) + }); }); -}); \ No newline at end of file +});