improve design patterns example

This commit is contained in:
rolux 2012-06-26 03:15:38 +02:00
parent 9b79a0e585
commit a299d2f6b1

View file

@ -1,4 +1,3 @@
/* /*
The following examples explain the common design pattern for OxJS UI widgets: an The following examples explain the common design pattern for OxJS UI widgets: an
inheritance model that is neither classical nor prototypal, but "parasitical" (a 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'; '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 = {};
/*
<hr>
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
`$('<div>')`. Chaining works too: If you have `var $d = $('<div>'), $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 `[<div class="OxElement"></div>]`. Any widget's `0`
property is an actual DOM element, and in case you ever need the
jQuery-wrapped element &mdash; 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" The second part of the "constructor" function can be thought of as the
function that takes two (optional) arguments, `options` and `self`, and "initializer", and contains everything needed to set up the "instance". In
returns a widget object. It's not a constructor in JavaScript terms though: this case, we just define a minimum and maximum size and then set the
It doesn't have to be called with `new`, and doesn't return an `instanceof` widget's color and size.
anything. It just enhances another widget object and returns it.
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 Before setting the size, we make sure the value is between `minSize` and
shared private object. `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, As there isn't much to do yet, this method just displays some text.
its `this`. Every widget "inherits" from another widget by simple Here, `.addClass('OxMyText')` is equivalent to `.css({padding: '4px'})`.
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.
*/ */
var that = Ox.Element({}, self) that.empty();
.defaults({ text && that.append($('<div>').addClass('OxMyText').html(text));
color: [128, 128, 128],
size: [128, 128]
})
.options(options || {})
.update({
color: setColor,
size: setSize
})
.addClass('OxMyBox');
/* /*
The second part of the "constructor" function can be thought of as the Public methods should return `that`, for chaining.
"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 `[<div class="OxElement"></div>]`, and the
widget's prototype has all the methods of a `$('<div>')`, with
proper chaining. If you have `var $d = $('<div>'), $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($('<div>').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.
*/ */
return that; 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();
}
});
self.options.inverted && setColor(); /*
<hr>
Now we can "subclass" our Box. Let's build one that can have its color inverted.
*/
Ox.My.InvertibleBox = function(options, self) {
function getInvertedColor() { self = self || {};
return self.options.color.map(function(value) { /*
We no longer inherit from Ox.Element, but from `Ox.My.Box`.
We could have written
<pre>
var that = Ox.My.Box({}, self)
.defaults({
color: [128, 128, 128],
inverted: false,
size: [128, 128]
})
.options(options || ())
.update({
...
})
</pre>
&mdash; 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:
<pre>
.on({
click: function() {
that.invert();
}
})
</pre>
*/
.bindEvent({
doubleclick: function() {
that.invert();
}
});
/*
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; return 255 - value;
}); }) : self.options.color
} ).join(', ') + ')'});
return false;
}
function setColor() { /*
that.css({backgroundColor: 'rgb(' + ( The public `invert` method is added as a convenience for the users of our
self.options.inverted ? getInvertedColor() : self.options.color widget, so that when they want to toggle its inverted state, they don't have
).join(', ') + ')'}); to write
<pre>
$widget.options({
inverted: !$widget.options('inverted')
});
</pre>
all the time.
Also, we trigger an `invert` event, that anyone can bind to via
<pre>
$widget.bindEvent({
invert: function() { ... }
});
</pre>
*/
that.invert = function() {
that.options({inverted: !self.options.inverted});
that.triggerEvent('invert');
return that;
};
/*
And again, we return `that`.
*/
return that;
};
/*
<hr>
Now it's time for something more funky: A MetaBox &mdash; 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:
<pre>
Ox.My.MetaBox({
color: [
[[255, 0, 0], [255, 255, 0], [0, 255, 0]],
[[0, 255, 255], [0, 255, 0], [255, 0, 255]]
]
});
</pre>
*/
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;
};
/*
<hr>
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; return false;
} }
}
that.invert = function() { /*
that.options({inverted: !self.options.inverted}); Inverting a PixelBox is different from inverting a MetaBox, since we only
that.triggerEvent('invert'); want to invert one color channel per box. This is where the shared private
return that; `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
return that; 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), <hr>
Ox.splitInt(self.options.size[1], self.options.color.length) And finally &mdash; 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) { self = self || {};
return array.map(function(color, x) { /*
return Ox.My.InvertibleBox({ Loading the image is asynchronous, but we want to display a box immediately.
color: color, 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]] size: [self.sizes[0][x], self.sizes[1][y]]
}) })
/*
... remove its `doubleclick` handler ...
*/
.unbindEvent('doubleclick') .unbindEvent('doubleclick')
/*
... and append it to the ImageBox.
*/
.appendTo(that); .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
<pre>
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
get overwritten by the MetaBox default size.
*/ */
Ox.My.VideoBox = function(options, self) { that.invert = Ox.My.MetaBox(self.options, self).invert;
}; /*
But wait &mdash; where's our `doubleclick` handler?
*/
return that;
(function() { };
var h = Ox.random(360), s = 1, l = 0.5;
window.My = {}; /*
My.$backgroundBox = Ox.My.Box({ <hr>
size: [256, 256] This one is left as an exercise to the reader ;)
}) */
.append( Ox.My.VideoBox = function(options, self) {
My.$box = Ox.My.Box({
color: Ox.rgb(h, s, l) };
}),
My.$invertibleBox = Ox.My.InvertibleBox({ /*
color: Ox.rgb(h + 120, s, l) <hr>
}), Load the UI and Image modules.
My.$metaBox = Ox.My.MetaBox({ */
color: Ox.range(2).map(function(y) { Ox.load(['Image', 'UI'], function() {
return Ox.range(3).map(function(x) { var h = Ox.random(360), s = 1, l = 0.5;
return Ox.rgb(h + x * 60 + y * 180, s, l); window.My = {};
}); My.$backgroundBox = Ox.My.Box({
}) size: [256, 256]
}), })
My.$pixelBox = Ox.My.PixelBox({ .append(
color: Ox.rgb(h + 120, s, l) 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.$pixelBox = Ox.My.PixelBox({
My.$imageBox = Ox.My.ImageBox({ color: Ox.rgb(h + 120, s, l)
image: 'png/pandora32.png',
size: [256, 256]
}) })
.appendTo(Ox.$body); )
Ox.forEach(My, function($element, name) { .appendTo(Ox.$body);
$element.bindEvent({ My.$imageBox = Ox.My.ImageBox({
invert: function() { image: 'png/pandora32.png',
My.$box.displayText(name[1].toUpperCase() + name.slice(2)); 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));
}
})
});
}); });