improve design patterns example

This commit is contained in:
rolux 2012-06-26 16:33:50 +02:00
parent 69d125bc8e
commit fdb1348b39

View file

@ -1,7 +1,11 @@
/* /*
The following examples explain the common design pattern for OxJS UI widgets: an <br>
inheritance model that is neither classical nor prototypal, but "parasitical" (a **Parasitical Inheritance**
term coined by <a */
/*
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 <a
href="http://www.crockford.com/javascript/inheritance.html">Douglas href="http://www.crockford.com/javascript/inheritance.html">Douglas
Crockford</a>). In a nutshell, "instances" are created by augmenting other Crockford</a>). In a nutshell, "instances" are created by augmenting other
instances, but in addition to private members (`var foo`) and public members 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 Create our own namespace. Not required, but if we wanted to create a module
module named `My`, this is how you would do it. named `My`, this is how we would do it.
*/ */
Ox.My = {}; Ox.My = {};
/* /*
<hr> **Box**
*/
/*
First, lets build the most basic Box widget. A widget is a "constructor" First, lets build the most basic Box widget. A widget is a "constructor"
function that takes two (optional) arguments, `options` and `self`, and returns 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 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) {
}; };
/* /*
<hr> **InvertibleBox**
*/
/*
Now we can "subclass" our Box. Let's build one that can have its color inverted. Now we can "subclass" our Box. Let's build one that can have its color inverted.
*/ */
Ox.My.InvertibleBox = function(options, self) { Ox.My.InvertibleBox = function(options, self) {
@ -264,7 +272,9 @@ Ox.My.InvertibleBox = function(options, self) {
}; };
/* /*
<hr> **MetaBox**
*/
/*
Now it's time for something more funky: A MetaBox &mdash; that is, a box of Now it's time for something more funky: A MetaBox &mdash; that is, a box of
boxes. boxes.
*/ */
@ -273,7 +283,7 @@ Ox.My.MetaBox = function(options, self) {
/* /*
This time, we inherit from `Ox.My.InvertibleBox`. The one thing that's 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 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 specified. The following would create a grid of boxes with two rows and
three columns: three columns:
<pre> <pre>
@ -293,7 +303,7 @@ Ox.My.MetaBox = function(options, self) {
/* /*
But we keep the default color of `Ox.My.InvertibleBox` (`[128, 128, 128]`) 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 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, column. This way, whenever someone accidentally passes a single color value,
our MetaBox can handle it. 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 `self.$boxes` are the actual boxes. We use `Ox.My.InvertibleBox`, but remove
remove their `doubleclick` handlers, since our meta-box already has one. the `doubleclick` handlers, since our MetaBox already has one, being an
(`unbindEvent(event)` removes all handlers, `unbindEvent(event, handler)` InvertibleBox itself. (`unbindEvent(event)` removes all handlers,
removes a specific one.) Then we simply append each box to the meta-box. `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) { self.$boxes = self.options.color.map(function(array, y) {
return array.map(function(color, x) { return array.map(function(color, x) {
@ -372,29 +383,42 @@ Ox.My.MetaBox = function(options, self) {
}; };
/* /*
<hr> **PixelBox**
*/
/*
The next widget is a peculiar type of meta-box. A PixelBox has only one color, 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) { Ox.My.PixelBox = function(options, self) {
self = 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 The challenge here is that we want our PixelBox to be an instance of
box. `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<br>
`Ox.My.PixelBox().options('color')`<br>
will now return<br>
`[[[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 [[ return [[
[self.options.color[0], 0, 0], [self.options.color[0], 0, 0],
[0, self.options.color[1], 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() { function setColor() {
if (Ox.isNumber(self.options.color[0])) { if (Ox.isNumber(self.options.color[0])) {
@ -429,12 +457,17 @@ Ox.My.PixelBox = function(options, self) {
}); });
}; };
/*
And that's the PixelBox.
*/
return that; return that;
}; };
/* /*
<hr> **ImageBox**
*/
/*
And finally &mdash; a meta-meta-box! An ImageBox takes an image and, for each And finally &mdash; a meta-meta-box! An ImageBox takes an image and, for each
pixel, displays a PixelBox. pixel, displays a PixelBox.
*/ */
@ -447,6 +480,12 @@ Ox.My.ImageBox = function(options, self) {
its `displayText` method. its `displayText` method.
*/ */
var that = Ox.My.Box({}, self).displayText('Loading...'); var that = Ox.My.Box({}, self).displayText('Loading...');
/*
It's not required to define empty defaults &mdash; 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(), { that.defaults(Ox.extend(that.defaults(), {
image: null 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. 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 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` 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 operates on will be the PixelBoxes that we are assigning in the asynchronous
callback above. 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 Note that we have to pass `self.options` too, otherwise our own
<pre> `self.options.size` would get overwritten by the MetaBox default size.
someOtherObject.method.apply(this, args)
</pre>
pattern that is common in JavaScript.
Note that we have to pass `self.options` too, otherwise our own size would Passing `self` to `Ox.My.MetaBox` has another nice effect: the MetaBox will
get overwritten by the MetaBox default size. 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 &mdash; the
`Ox.My.Box` that `Ox.My.ImageBox` inherits from, which shares the same
`self` &mdash; receives a `doubleclick`, there is now a handler for it.)
*/ */
that.invert = Ox.My.MetaBox(self.options, self).invert; that.invert = Ox.My.MetaBox(self.options, self).invert;
/* /*
But wait &mdash; where's our `doubleclick` handler? And that's it.
*/ */
return that; return that;
}; };
/* /*
<hr> **VideoBox**
*/
/*
This one is left as an exercise to the reader ;) This one is left as an exercise to the reader ;)
*/ */
Ox.My.VideoBox = function(options, self) { Ox.My.VideoBox = function(options, self) {
@ -530,13 +580,26 @@ Ox.My.VideoBox = function(options, self) {
}; };
/* /*
<hr> **Demo**
Load the UI and Image modules. */
/*
Load the Image and UI modules.
*/ */
Ox.load(['Image', 'UI'], function() { 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; 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 = {}; 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] size: [256, 256]
}) })
.append( .append(
@ -558,16 +621,23 @@ Ox.load(['Image', 'UI'], function() {
}) })
) )
.appendTo(Ox.$body); .appendTo(Ox.$body);
/*
The ImageBox is a bit larger.
*/
My.$imageBox = Ox.My.ImageBox({ My.$imageBox = Ox.My.ImageBox({
image: 'png/pandora32.png', image: 'png/pandora32.png',
size: [256, 256] size: [256, 256]
}) })
.appendTo(Ox.$body); .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) { Ox.forEach(My, function($element, name) {
$element.bindEvent({ $element.bindEvent({
invert: function() { invert: function() {
My.$box.displayText(name[1].toUpperCase() + name.slice(2)); My.$box.displayText(name[1].toUpperCase() + name.slice(2));
} }
}) });
}); });
}); });