From a299d2f6b195d853876d43f25e53bffa272bce2c Mon Sep 17 00:00:00 2001 From: rolux Date: Tue, 26 Jun 2012 03:15:38 +0200 Subject: [PATCH] improve design patterns example --- .../ui/widget_design_patterns/js/example.js | 817 +++++++++++------- 1 file changed, 516 insertions(+), 301 deletions(-) diff --git a/examples/ui/widget_design_patterns/js/example.js b/examples/ui/widget_design_patterns/js/example.js index 6a614179..d8ab3c80 100644 --- a/examples/ui/widget_design_patterns/js/example.js +++ b/examples/ui/widget_design_patterns/js/example.js @@ -1,4 +1,3 @@ - /* The following examples explain the common design pattern for OxJS UI widgets: an inheritance model that is neither classical nor prototypal, but "parasitical" (a @@ -13,346 +12,562 @@ be accessed from outside, but since `self` itself is an argument of the */ 'use strict'; + /* -Load the UI module. +Create our own namespace. Not required, but if you wanted to create a +module named `My`, this is how you would do it. */ -Ox.load(['Image', 'UI'], function() { +Ox.My = {}; + +/* +
+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 +have to be called with `new`, and doesn't return an `instanceof` anything. It +just enhances another widget object and returns it. +*/ +Ox.My.Box = function(options, self) { /* - Create our own namespace. + This is how every widget "constructor" begins. `self` is the widget's shared + private object. */ - Ox.My = {}; + self = self || {}; + /* + `that` is the widget itself, its public object, or, in JavaScript terms, its + `this`. Every widget "inherits" from another widget by simple assignment. + All public properties of the "super" widget, i.e. all properties of its + `that`, will be present on our own `that`. In this case, we use Ox.Element, + the "root" widget at the end of the inheritance chain, and pass an empty + options object. But we always pass our own `self`, which means that any + property that Ox.Element (or any other widget in the inheritance chain) adds + to `self` will be present on our own `self`. + */ + var that = Ox.Element({}, self) + /* + Then we call the public `defaults`, `options` and `update` methods of + Ox.Element. `defaults` assigns the defaults object to `self.defaults` + and copies it to `self.options`, `options` extends `self.options` with + the options object, and `update` adds one or more callbacks that are + invoked whenever, by way of calling the `options` method, a property of + `self.options` is modified or added. + */ + .defaults({ + color: [128, 128, 128], + size: [128, 128] + }) + .options(options || {}) + .update({ + color: setColor, + size: setSize + }) + /* + `addClass` is a jQuery method. In fact, Ox.Element (and any widget + derived from it) provides, on its prototype, all methods of a jQuery + `$('
')`. Chaining works too: If you have `var $d = $('
'), $e = + Ox.Element();`, then `$d.appendTo($e)` returns `$d`, and `$e.append($d)` + returns `$e`. If you type Ox.Element() in the console, you will get + something like `[
]`. Any widget's `0` + property is an actual DOM element, and in case you ever need the + jQuery-wrapped element — that's the widget's `$element` property. + + The purpose of the `OxMyBox` class is just to allow us to add CSS + declarations in an external style sheet. In this case, `.css({float: + 'left'})` would do the same thing. + */ + .addClass('OxMyBox'); /* - 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 have to be called with `new`, and doesn't return an `instanceof` - anything. It just enhances another widget object and returns it. + The second part of the "constructor" function can be thought of as the + "initializer", and contains everything needed to set up the "instance". In + this case, we just define a minimum and maximum size and then set the + widget's color and size. + + We could have used `var minSize` and `var maxSize` here, but by using `self` + for private variables that we want to be accessible across all the widget's + methods, we can be sure that inside such methods, any local `var` is + actually local to the method. */ - Ox.My.Box = function(options, self) { + self.minSize = 1; + self.maxSize = 256; + setColor(); + setSize(); + + /* + Third, we declare the widget's private methods. These are just function + declarations, hoisted to the top of the "constructor". + */ + function setColor() { + that.css({ + backgroundColor: 'rgb(' + self.options.color.join(', ') + ')', + }); + } + + function setSize() { /* - This is how every widget "constructor" begins. `self` is the widget's - shared private object. + Before setting the size, we make sure the value is between `minSize` and + `maxSize`. */ - self = self || {}; + self.options.size = self.options.size.map(function(value) { + return Ox.limit(value, self.minSize, self.maxSize); + }); + that.css({ + width: self.options.size[0] + 'px', + height: self.options.size[1] + 'px' + }); + } + + /* + Next, we define the widgets public methods, as properties of `that`. (Note + that unlike private methods, they are not hoisted.) + */ + that.displayText = function(text) { /* - `that` is the widget itself, its public object, or, in JavaScript terms, - its `this`. Every widget "inherits" from another widget by simple - assignment. All public properties of the "super" widget, i.e. all - properties of its `that`, will be present on our own `that`. In this - case, we use Ox.Element, the "root" widget at the end of the inheritance - chain, and pass an empty options object. But we always pass our own - `self`, which means that any property that Ox.Element (or any other - widget in the inheritance chain) adds to `self` will be present on our - own `self`. Then we call the public `defaults`, `options` and `update` - methods of Ox.Element. `defaults` assigns the defaults object to - `self.defaults` and copies it to `self.options`, `options` extends - `self.options` with the options object, and `update` adds one or more - callbacks that are invoked whenever, by way of calling the `options` - method, a property of `self.options` is modified or added. + As there isn't much to do yet, this method just displays some text. + Here, `.addClass('OxMyText')` is equivalent to `.css({padding: '4px'})`. */ - var that = Ox.Element({}, self) - .defaults({ - color: [128, 128, 128], - size: [128, 128] - }) - .options(options || {}) - .update({ - color: setColor, - size: setSize - }) - .addClass('OxMyBox'); - + that.empty(); + text && that.append($('
').addClass('OxMyText').html(text)); /* - The second part of the "constructor" function can be thought of as the - "initializer", and contains everything needed to set up the "instance". - In this case, we just define a minimum and maximum size and then set the - widget's color and size. We could have used `var minSize` and `var - maxSize` here, but by using `self` for private variables that we want to - be accessible across all the widget's methods, we can be sure that - inside such methods, any local `var` is actually local to the method. - */ - self.minSize = 1; - self.maxSize = 256; - - setColor(); - setSize(); - - /* - Third, we declare the widget's private methods. These are just function - declarations, hoisted to the top of the "constructor". - */ - function setColor() { - /* - To interact with the DOM, Ox.Element (and any widget derived from - it) wraps jQuery. If you type Ox.Element() in the console, you - will get something like `[
]`, and the - widget's prototype has all the methods of a `$('
')`, with - proper chaining. If you have `var $d = $('
'), $e = - Ox.Element();`, then `$d.appendTo($e)` returns `$d`, and - `$e.append($d)` returns `$e`. - */ - that.css({ - backgroundColor: 'rgb(' + self.options.color.join(', ') + ')', - }); - } - - function setSize() { - /* - Before setting the size, we make sure the value is between `minSize` - and `maxSize`. - */ - self.options.size = self.options.size.map(function(value) { - return Ox.limit(value, self.minSize, self.maxSize); - }); - that.css({ - width: self.options.size[0] + 'px', - height: self.options.size[1] + 'px' - }); - } - - /* - Next, we define the widgets public methods, as properties of `that`. - (Note that unlike private methods, they are not hoisted.) - */ - that.displayText = function(text) { - /* - As there isn't much to do yet, this method just displays the - widget's options. - */ - that.empty(); - text && that.append($('
').addClass('OxMyText').html(text)); - /* - Public methods should return `that`, for chaining. - */ - return that; - }; - - /* - And finally, at the very end of the "constructor", we return `that`. And - that's it. + Public methods should return `that`, for chaining. */ return that; - }; - Ox.My.InvertibleBox = function(options, self) { + /* + And finally, at the very end of the "constructor", we return `that`. And + that's it. + */ + return that; + +}; - self = self || {}; - var that = Ox.My.Box({}, self); - that.defaults(Ox.extend(that.defaults(), { - inverted: false - })) - .options(options || {}) - .update({ - color: setColor, - inverted: setColor - }) - .addClass('OxMyInvertibleBox') - .bindEvent({ - doubleclick: function() { - that.invert(); - } - }); +/* +
+Now we can "subclass" our Box. Let's build one that can have its color inverted. +*/ +Ox.My.InvertibleBox = function(options, self) { - self.options.inverted && setColor(); + self = self || {}; + /* + We no longer inherit from Ox.Element, but from `Ox.My.Box`. + We could have written +
+    var that = Ox.My.Box({}, self)
+        .defaults({
+            color: [128, 128, 128],
+            inverted: false,
+            size: [128, 128]
+        })
+        .options(options || ())
+        .update({
+            ...
+        })
+    
+ — but why repeat the defaults of `Ox.My.Box` if we can simply extend + them. (Just like `options()` returns all options of a widget, `defaults()` + returns all its defaults.) + */ + var that = Ox.My.Box({}, self); + that.defaults(Ox.extend(that.defaults(), { + inverted: false + })) + .options(options || {}) + /* + Again, we add handlers that run when the widget's options are updated. + The original handlers of `Ox.My.Box` will run next, so we just add the + ones we need. We leave out `size`, so when the `size` option changes, + we'll get the original behavior. + */ + .update({ + color: setColor, + inverted: setColor + }) + /* + The same as `.css({cursor: 'pointer'})`. + */ + .addClass('OxMyInvertibleBox') + /* + Ox.Element and its descendants provide a number of public methods + (`bindEvent`, `bindEventOnce`, `triggerEvent` and `unbindEvent`) that + allow widgets to communicate via custom events. Here, we add a handler + for Ox.Element's `doubleclick` event. If we just wanted to handle a + `click` event, we could also use jQuery here: +
+        .on({
+            click: function() {
+                that.invert();
+            }
+        })
+        
+ */ + .bindEvent({ + doubleclick: function() { + that.invert(); + } + }); - function getInvertedColor() { - return self.options.color.map(function(value) { + /* + The idea is that our widget's inverted state is separate from its color. If + the inverted option is set, then the color option stays the same, but has + the inverse effect. This means that when initializing the widget, we have + to call our custom `setColor` method if `self.options.inverted` is `true`. + */ + self.options.inverted && setColor(); + + /* + When `setColor` is invoked as an update handler, returning `false` signals + that no other handler should run. Otherwise, the original handler of + `Ox.My.Box` would run next, and revert any inversion we might have done + here. + */ + function setColor() { + that.css({backgroundColor: 'rgb(' + ( + self.options.inverted ? self.options.color.map(function(value) { return 255 - value; - }); - } + }) : self.options.color + ).join(', ') + ')'}); + return false; + } - function setColor() { - that.css({backgroundColor: 'rgb(' + ( - self.options.inverted ? getInvertedColor() : self.options.color - ).join(', ') + ')'}); + /* + The public `invert` method is added as a convenience for the users of our + widget, so that when they want to toggle its inverted state, they don't have + to write +
+    $widget.options({
+        inverted: !$widget.options('inverted')
+    });
+    
+ all the time. + + Also, we trigger an `invert` event, that anyone can bind to via +
+    $widget.bindEvent({
+        invert: function() { ... }
+    });
+    
+ */ + that.invert = function() { + that.options({inverted: !self.options.inverted}); + that.triggerEvent('invert'); + return that; + }; + + /* + And again, we return `that`. + */ + return that; + +}; + +/* +
+Now it's time for something more funky: A MetaBox — that is, a box of +boxes. +*/ +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 + specified. The following would create a grid of boxes with two rows and + three columns: +
+    Ox.My.MetaBox({
+        color: [
+            [[255, 0, 0], [255, 255, 0], [0, 255, 0]],
+            [[0, 255, 255], [0, 255, 0], [255, 0, 255]]
+        ]
+    });
+    
+ */ + self = self || {}; + var that = Ox.My.InvertibleBox({}, self) + .options(options || {}) + .update({color: setColor}); + + /* + 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 + column. This way, whenever someone accidentally passes a single color value, + our MetaBox can handle it. + */ + if (Ox.isNumber(self.options.color[0])) { + self.options.color = [[self.options.color]]; + } + + /* + `self.sizes` holds the width of each column and the height of each row. + `self.options.color.length` is the number of rows, + `self.options.color[0].length` the number of columns, and Ox.splitInt(a, b) + "splits" an integer `a` into an array of `b` integers that sum up to `a`. + (We don't want fractional pixel sizes.) + */ + self.sizes = [ + Ox.splitInt(self.options.size[0], self.options.color[0].length), + Ox.splitInt(self.options.size[1], self.options.color.length) + ]; + + /* + `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 = self.options.color.map(function(array, y) { + return array.map(function(color, x) { + return Ox.My.InvertibleBox({ + color: color, + size: [self.sizes[0][x], self.sizes[1][y]] + }) + .unbindEvent('doubleclick') + .appendTo(that); + }); + }); + + /* + To set the color of a meta-box means to set the color of each box. + */ + function setColor() { + self.$boxes.forEach(function(array, y) { + array.forEach(function($box, x) { + $box.options({color: self.options.color[y][x]}); + }); + }); + } + + /* + This is the rare case of a shared private method. Its purpose will become + apparent a bit later. Otherwise, we could just have made a private function, + or an anonymous function in the loop below. + */ + self.invertBox = function($box) { + $box.invert(); + }; + + /* + Here, we override the public `invert` method of `Ox.My.InvertibleBox`. When + inverting an `Ox.My.MetaBox`, we have to invert each of its boxes. (If we + wanted to keep the original method around, we could store it as + `that.superInvert` before.) + */ + that.invert = function() { + self.$boxes.forEach(function(array) { + array.forEach(self.invertBox); + }); + that.triggerEvent('invert'); + return that; + }; + + /* + And that's all it takes to make a meta-box. + */ + return that; + +}; + +/* +
+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. +*/ +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. + */ + function getColor() { + return [[ + [self.options.color[0], 0, 0], + [0, self.options.color[1], 0], + [0, 0, self.options.color[2]] + ]]; + } + + /* + ... + */ + function setColor() { + if (Ox.isNumber(self.options.color[0])) { + that.options({color: getColor()}); return false; } + } - that.invert = function() { - that.options({inverted: !self.options.inverted}); - that.triggerEvent('invert'); - return that; - }; - - return that; - + /* + Inverting a PixelBox is different from inverting a MetaBox, since we only + want to invert one color channel per box. This is where the shared private + `invertBox` method of `Ox.My.MetaBox` comes into play. Since we share the + same `self`, we can simply override it. (Alternatively, we could have added + an `invertBox` option to `Ox.My.MetaBox`, but overriding a shared private + method is much more elegant than cluttering the public API of + `Ox.My.MetaBox` with such an option.) + */ + self.invertBox = function($box, x) { + $box.options({ + color: $box.options('color').map(function(value, i) { + return i == x ? 255 - value : value + }) + }); }; - Ox.My.MetaBox = function(options, self) { + return that; - self = self || {}; - var that = Ox.My.Box({}, self); - that.defaults(Ox.extend(that.defaults(), { - color: [[[128, 128, 128]]] - })) - .options(options || {}) - .update({color: setColor}) - .bindEvent({ - doubleclick: function() { - that.invert(); - } - }); +}; - self.sizes = [ - Ox.splitInt(self.options.size[0], self.options.color[0].length), - Ox.splitInt(self.options.size[1], self.options.color.length) - ]; +/* +
+And finally — a meta-meta-box! An ImageBox takes an image and, for each +pixel, displays a PixelBox. +*/ +Ox.My.ImageBox = function(options, self) { - self.$boxes = self.options.color.map(function(array, y) { - return array.map(function(color, x) { - return Ox.My.InvertibleBox({ - color: color, + self = self || {}; + /* + Loading the image is asynchronous, but we want to display a box immediately. + So we just subclass `Ox.My.Box`. Also, this seems to be a good use case for + its `displayText` method. + */ + var that = Ox.My.Box({}, self).displayText('Loading...'); + that.defaults(Ox.extend(that.defaults(), { + image: null + })) + .options(options || {}); + + /* + Ox.Image takes a URI and passes an image object to its callback function. + */ + self.options.image && Ox.Image(self.options.image, function(image) { + var size = image.getSize(); + size = [size.width, size.height]; + /* + Again, we have to compute the width of each column and the height of + each row. + */ + self.sizes = size.map(function(value, index) { + return Ox.splitInt(self.options.size[index], value); + }); + /* + Remove the 'Loading...' message. + */ + that.displayText(); + /* + For each pixel ... + */ + self.$boxes = Ox.range(size[1]).map(function(y) { + return Ox.range(size[0]).map(function(x) { + /* + ... create a PixelBox ... + */ + return Ox.My.PixelBox({ + /* + (`image.pixel` returns RGBA, so discard alpha) + */ + color: image.pixel(x, y).slice(0, 3), size: [self.sizes[0][x], self.sizes[1][y]] }) + /* + ... remove its `doubleclick` handler ... + */ .unbindEvent('doubleclick') + /* + ... and append it to the ImageBox. + */ .appendTo(that); }); }); - - function setColor() { - self.$boxes.forEach(function(array, y) { - array.forEach(function($box, x) { - $box.options({color: self.options.color[y][x]}); - }); - }); - } - - self.invertBox = function($box) { - $box.invert(); - }; - - that.invert = function() { - self.$boxes.forEach(function(array) { - array.forEach(self.invertBox); - }); - that.triggerEvent('invert'); - return that; - }; - - return that; - - }; - - 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: function() { - setColor(); - return false; - } - }); - - self.invertBox = function($box, x) { - $box.options({ - color: $box.options('color').map(function(value, i) { - return i == x ? 255 - value : value - }) - }); - }; - - function getColor() { - return [[ - [self.options.color[0], 0, 0], - [0, self.options.color[1], 0], - [0, 0, self.options.color[2]] - ]]; - } - - function setColor() { - self.$pixel.options({color: getColor()}); - } - - return that; - - }; - - Ox.My.ImageBox = function(options, self) { - - self = self || {}; - var that = Ox.My.Box({}, self).displayText('Loading...') - that.defaults({ - image: null - }) - .options(options || {}); - - Ox.Image(self.options.image, function(image) { - var size = image.getSize(); - size = [size.width, size.height]; - self.sizes = size.map(function(value, index) { - return Ox.splitInt(self.options.size[index], value); - }); - that.displayText(); - self.$boxes = Ox.range(size[1]).map(function(y) { - return Ox.range(size[0]).map(function(x) { - return Ox.My.PixelBox({ - color: image.pixel(x, y).slice(0, 3), - size: [self.sizes[0][x], self.sizes[1][y]] - }) - .unbindEvent('doubleclick') - .appendTo(that); - }); - }); - }); - - that.invert = Ox.My.MetaBox(self.options, self).invert; - - return that; - - }; + }); /* - This is left as an exercise for the reader ;) + 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. + + 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 size would + get overwritten by the MetaBox default size. */ - Ox.My.VideoBox = function(options, self) { - - }; + that.invert = Ox.My.MetaBox(self.options, self).invert; - (function() { - var h = Ox.random(360), s = 1, l = 0.5; - window.My = {}; - My.$backgroundBox = Ox.My.Box({ - size: [256, 256] - }) - .append( - My.$box = Ox.My.Box({ - color: Ox.rgb(h, s, l) - }), - My.$invertibleBox = Ox.My.InvertibleBox({ - color: Ox.rgb(h + 120, s, l) - }), - My.$metaBox = Ox.My.MetaBox({ - color: Ox.range(2).map(function(y) { - return Ox.range(3).map(function(x) { - return Ox.rgb(h + x * 60 + y * 180, s, l); - }); - }) - }), - My.$pixelBox = Ox.My.PixelBox({ - color: Ox.rgb(h + 120, s, l) + /* + But wait — where's our `doubleclick` handler? + */ + return that; + +}; + +/* +
+This one is left as an exercise to the reader ;) +*/ +Ox.My.VideoBox = function(options, self) { + +}; + +/* +
+Load the UI and Image modules. +*/ +Ox.load(['Image', 'UI'], function() { + var h = Ox.random(360), s = 1, l = 0.5; + window.My = {}; + My.$backgroundBox = Ox.My.Box({ + size: [256, 256] + }) + .append( + My.$box = Ox.My.Box({ + color: Ox.rgb(h, s, l) + }), + My.$invertibleBox = Ox.My.InvertibleBox({ + color: Ox.rgb(h + 120, s, l) + }), + My.$metaBox = Ox.My.MetaBox({ + color: Ox.range(2).map(function(y) { + return Ox.range(3).map(function(x) { + return Ox.rgb(h + x * 60 + y * 180, s, l); + }); }) - ) - .appendTo(Ox.$body); - My.$imageBox = Ox.My.ImageBox({ - image: 'png/pandora32.png', - size: [256, 256] + }), + My.$pixelBox = Ox.My.PixelBox({ + color: Ox.rgb(h + 120, s, l) }) - .appendTo(Ox.$body); - Ox.forEach(My, function($element, name) { - $element.bindEvent({ - invert: function() { - My.$box.displayText(name[1].toUpperCase() + name.slice(2)); - } - }) - }); - }()); - -}); + ) + .appendTo(Ox.$body); + My.$imageBox = Ox.My.ImageBox({ + image: 'png/pandora32.png', + size: [256, 256] + }) + .appendTo(Ox.$body); + 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