improve design patterns example
This commit is contained in:
parent
69d125bc8e
commit
fdb1348b39
1 changed files with 117 additions and 47 deletions
|
@ -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 — that is, a box of
|
Now it's time for something more funky: A MetaBox — 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 — a meta-meta-box! An ImageBox takes an image and, for each
|
And finally — 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 — 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 — 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;
|
that.invert = Ox.My.MetaBox(self.options, self).invert;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
But wait — 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));
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
Loading…
Reference in a new issue