Understand the major features of ComponentJS
ComponentJS, but
this can be configured or even changed afterwards.
Usually you want to use the short cs
(for "component system") as the global API identifier.
index.html
of your HTML5 App use one of the following two approaches:
<!-- in simple environment -->
<script src="component.js"></script>
<script>ComponentJS.symbol("cs");</script>
/* under curl.js environment */
curl([ "component.js" ], function () {
ComponentJS.symbol("cs");
curl([ "app.js", … ], function () {
…
})
})
/* under YepNope environment */
yepnope([{
load: [ "component.js" ],
complete: function () { ComponentJS.symbol("cs"); }
}, {
load: [ "app.js", … ]
complete: function () { … }
}]);
/* under CommonJS environment */
var cs = require("component.js").ComponentJS;
Ext.define() mechanism.
Hence, for working with ComponentJS, the classes you
use for components can be created in arbitrary ways (not required
to use $cs.clazz) as
long as one can still instanciate its objects with the
regular JavaScript new directive.
/* define class com.example.Foo (variant 1: explicit namespace) */
cs.ns("com.example"); com.example.Foo = cs.clazz({ … });
/* define class com.example.Foo (variant 2: ad-hoc namespace) */
cs.ns("com.example").Foo = cs.clazz({ … });
/* define class com.example.Foo (variant 3: implicit namespace) */
cs.clazz({ name: "com.example.Foo", … });
new directive of JavaScript:
/* instanciate class foo.bar.quux */ var foo = new com.example.Foo();
com.example.Foo = cs.clazz({
statics: { IDX_FOO: 1, IDX_BAR: 2 },
dynamics: { foo: null, bar: null, quux: null },
cons: function (foo, bar, quux) {
this.foo = foo;
this.bar = bar;
this.quux = quux;
},
protos: {
doFooBarQuux: function (…) {
…
},
…
}
});
statics hash contains the name/value pairs
of all statically assigned fields. These have to be treated like constants
as they are shared across all class instances.dynamics hash contains the name/value pairs
of all dynamically assigned fields. These are deep-cloned and
re-assigned for each class instance and hence are individual
to each class instance. These are usually the private fields
of a class.cons function is called just
after the class instance is created. It receives the
parameters passed to the new com.example.Foo(…)
call. It is usually used to set dynamics fields
based on the input parameters.protos hash contains the name/function
pairs of all class methods. These are shared by all class
instances and the instances of sub-classes by means of JavaScript's
regular prototype objects.cs.param() function is just a utility
function which allows a method to conveniently support
both positional and name-based parameters at the same time. For instance,
the above constructor could use it:
com.example.Foo = cs.clazz({
…
cons: function () {
var args = cs.params("cons", arguments, {
foo: { pos: 0, def: "", valid: "string", req: true },
bar: { pos: 1, def: true, valid: "boolean", req: true },
quux: { pos: 2, def: cs.nop, valid: "function", }
});
this.foo = args.foo;
this.bar = args.bar;
this.quux = args.quux;
},
…
});
This then allows both forms of calling:
/* use positional parameters */
var foo = new com.example.Foo("foo", true, function () { … });
/* use named parameters */
var foo = new com.example.Foo({ foo: "foo", bar: true, quux: function () { … } });
Addtionally, it allows one to leave out optional parameters
(from the right side of the parameter list for positional parameters only, of course)
if they are not required or at least have default values:
/* use first positional parameters only */
var foo = new com.example.Foo("foo");
/* use any named parameters */
var foo = new com.example.Foo({ bar: true });
/* define class */
com.example.Shape = cs.clazz({
dynamics: { x: 0, y: 0 },
cons: function (x, y) {
this.x = x;
this.y = y;
}
});
/* define sub-class */
com.example.Circle = cs.clazz({
extend: com.example.Shape,
dynamics: { r: 0 },
cons: function (x, y, r) {
this.base(x, y);
this.r = r;
},
});
The extend class definition field references the single previously defined
super class. With the this.base(…) call you can (and should) call
the constructor (or any other overloaded method) from the super class.
It is important to notice how calls to any method resolve and how calls to this.base()
in any method of a class resolves.
Foo and its instanciated object
foo a method foo.bar() is called,
the following happens:
bar on object foo is
tried. This can exist on foo through (in priority order)
a bar in either the dynamics
definition of a mixin of Foo,
or in the statics
definition of a mixin of Foo,
or in the dynamics
definition of Foo,
or in the statics
definition of Foo.bar on
object foo is tried.
This can exist on foo through (in priority order)
a bar in either the protos
definition of Foo or
in the protos
definition of any extend of Foo.Foo and its instanciated object
foo in any method foo.bar() the this.base() is called,
the following happens:
mixin trait chain is attempted.
The mixins are traversed in the reverse order of the trait specification in the
mixin array, i.e., the last trait's mixins are tried first.extend inheritance class chain is attempted.
First the directly extend class is attempted, then the
extend class of this class, etc.extend, ComponentJS's class
mechanism also support traits, i.e.,
classes which can be mixed in with
the mixin class definition field:
/* define a trait */
com.example.Property = cs.trait({
dynamics: { __prop: {} },
protos: {
property: function (name, value_new) {
var value_old = this.__prop[name];
if (typeof value_new !== "undefined")
this.__prop[name] = value_new;
return value_old;
}
}
});
/* define another trait */
com.example.Observable = cs.trait({
…
});
/* define a class */
com.example.Circle = cs.clazz({
extend: com.example.Shape,
mixin: [ com.example.Property, com.example.Observable ],
…
});
Traits are defined like classes but with
two differences: they cannot have a
class they extend and instead in addition
to the cons method they
can have a setup method. This
setup method is called at the end
of a class(!) instanciation in order to give
each mixed in trait the chance to post-adjust
the newly created object. This allows traits
not only to statically mixin properties but to
also dynamically post-adjust instanciated objects.
/* silly method renaming trait */
com.example.Foo2Bar = $cs.trait({
setup: function () {
if (this.hasOwnProperty("foo")) {
this["bar"] = this["foo"];
delete this["foo"];
}
}
});
/* define a class */
com.example.Foo = cs.clazz({
mixin: [ com.example.Foo2Bar ],
dynamics: {
foo: 42 /* is automatically renamed to "bar" */
}
…
});
com.example.ui.Panel).
The generic component object serves as a proxy
to the shadow object for driving all ComponentJS
functionalities without making any constraints
(usually like requiring you to sub-class a special "component" base
class as other frameworks require) on the corresponding shadow object.
/* the simplest ComponentJS-style shadow object */
com.example.ui.Window = cs.clazz({});
/* the simplest JavaScript-style shadow object */
com.example.ui.Window = function () {};
The UI component hierarchy usually corresponds to
the inherently hierarchical structure of UI
dialogs. A UI dialog can be a whole UI screen
or just a group of widgets. But it usually is
never just a single widget (for the management
of single widgets the UI toolkit is responsible
for) or even the whole UI of the application
(for this singleton-scenario to use a component system
like ComponentJS is useless). Every UI component has a unique name,
consisting of the slash-separated path of UI
component names, from the implicitly existing
root UI component to the addressed UI component (e.g. /example/ui/panel).
When creating a UI component you can address it
with a fully-qualified path or relatively to
an already existing UI component.
/* UI component creation with fully-qualified paths */
cs.create("/example/ui/", com.example.ui.Window);
cs.create("/example/ui/dialog1", com.example.ui.Dialog1);
cs.create("/example/ui/dialog1/list", com.example.ui.Dialog1List);
cs.create("/example/ui/dialog1/detail", com.example.ui.Dialog1Detail);
cs.create("/example/ui/dialog2", com.example.ui.Dialog2);
/* UI component creation with relative paths */
var ui = cs.create("/example/ui", com.example.ui.Window);
ui.create("dialog1", com.example.ui.Dialog1);
ui.create("dialog1/list", com.example.ui.Dialog1List);
ui.create("dialog1/detail", com.example.ui.Dialog1Detail);
ui.create("dialog2", com.example.ui.Dialog2);
There are three distinct ways to create a
component and its corresponding shadow object: you
can implicitly let ComponentJS instanciate the
shadow object (assuming no parameters need to be
passed to the shadow object's constructor),
you can on-the-fly explicitly instanciate the
shadow object yourself (usually when you need
to pass parameters to its constructor) and in
case of special requirements you can even
first create a UI component without a corresponding
shadow object and then attach it yourself manually.
/* implicit shadow object instanciation */
cs.create("/example/ui/panel", com.example.ui.Panel);
/* explicit shadow object instanciation */
cs.create("/example/ui/panel", new com.example.ui.Panel("foo"));
/* explicit shadow object instanciation and delayed attachment */
var comp = cs.create("/example/ui/panel", null);
…
var obj = new com.example.ui.Panel("foo");
comp.obj(obj);
At any time you can easily go from the component
to its shadow object and vice versa:
/* get the shadow object through a component */ var obj = comp.obj(); /* get the component through a shadow object */ var comp = cs(obj);But you are safe to always call
cs() on either the shadow-object
or even the component itself (in case you are unsure what you have at hand,
e.g., in case of a this pointer in a callback):
/* protected from double-wrapping */
if (comp !== cs(comp))
alert("will not happen");
ComponentJS but as
explained above in practice usually mapped to
cs) is actually a function (which
directly maps onto cs.lookup(), but
one never specifies this explicitly in practice), a method to
lookup a component in various ways:
/* lookup component by absolute path */
var comp = cs("/example/ui/panel");
/* lookup component by relative path */
var ui = cs("/example/ui");
var panel = cs(ui, "panel");
var dialog1 = cs(panel, "../dialog1");
/* check for component existance */
cs("/") /* === cs("<root>") */
cs("/").exists() /* === true */
cs("/not/exising") /* === cs("<none>") */
cs("/not/exising").exists() /* === false */
But usually you never assign the result of a
lookup to a variable. Instead you use it to
call the ComponentJS functionalities on it, similar
to the API jQuery
provides for calling functionalities on DOM elements. For
instance, if you want, you can even combine
lookup with relative component creation:
/* create UI component under path /example/ui/panel */
cs("/example/ui").create("panel", com.example.ui.Panel);
/* resolve parent component */
cs("/foo/bar").parent() /* === cs("/foo") */
cs("/foo/bar/..") /* === cs("/foo") */
cs(cs("/foo/bar"), "..") /* === cs("/foo") */
/* resolve path of all parent components */
cs("/foo/bar/quux").path() /* === [ cs("/"), cs("/foo"), cs("/foo/bar") ] */
cs("/foo/bar/quux").path("/") /* === "/foo/bar/quux" */
/* walk to all parent components (recursively) */
var output = cs("/foo/bar/quux").walk_up(function (depth, comp, output) {
output += depth + ":" + comp.name() + " ";
return output;
}, "");
/* output === "0:quux 1:bar 2:foo 3:<root> " */
/* resolve all direct child components */
cs("/foo").create("bar");
cs("/foo").create("quux");
var childs = cs("/foo").children();
/* childs === [ cs("/foo/bar"), cs("/foo/quux") ] */
/* walk to all transitive child components (recursively) */
cs.create("/ui/foo")
cs.create("/ui/foo/bar")
cs.create("/ui/foo/bar/baz")
cs.create("/ui/foo/quux")
var output = cs("/ui").walk_down(function (depth, comp, output, depth_first) {
if (depth_first)
output += depth + ":" + comp.name() + " ";
return output;
}, "");
/* output === "3:baz 2:bar 2:quux 1:foo 0:ui " */
var output = cs("/ui").walk_down(function (depth, comp, output, depth_first) {
if (!depth_first)
output += depth + ":" + comp.name() + " ";
return output;
}, "");
/* output === "0:ui 1:foo 2:bar 3:baz 2:quux " */
/* set a few properities */
cs("/").property("foo", "val1");
cs("/example").property("bar", "val2");
cs("/example/ui/panel").property("quux", "val3");
/* get properties on the leaf component */
var foo = cs("/example/ui/panel").property("foo"); /* === "val1" */
var foo = cs("/example/ui/panel").property("bar"); /* === "val2" */
var foo = cs("/example/ui/panel").property("quux"); /* === "val3" */
/* get properties anywhere */
var foo = cs("/example/ui").property("foo"); /* === "val1" */
var foo = cs("/example/ui").property("bar"); /* === "val2" */
var foo = cs("/example/ui").property("quux"); /* === null */
Sometimes a property on a parent component should
resolve to different values when looked-up on different child
components. This can be achieved by scoping the
property name to the name of the child component.
/* set a few properities */
cs("/example/ui/panel").property("foo", "val-for-any");
cs("/example/ui/panel").property("foo@dialog2", "val-for-dialog2");
cs("/example/ui/panel").property("foo@dialog3", "val-for-dialog3");
/* get properties */
var foo = cs("/example/ui/panel/dialog1").property("foo"); /* === "val-for-any" */
var foo = cs("/example/ui/panel/dialog2").property("foo"); /* === "val-for-dialog2" */
var foo = cs("/example/ui/panel/dialog3").property("foo"); /* === "val-for-dialog3" */
cs("/foo").state("bar"). State
transitions are fully aware of the UI component
hierarchy and the defined life-cycle, which
is directly reflected in the two possible
state changing szenarios:
.state_auto_increase() on
all childs components beforehand or in case you want to increase all child
components transitively more conveniently set the property
ComponentJS:state-auto-increase on the dialog component itself.
In other words: "state-auto-increase" on a component means the component
automatically increases with its parent component.
.state_auto_decrease() on
the parent component beforehand or in case you want to decrease all parent
components transitively more conveniently set the property
ComponentJS:state-auto-decrease on the parent components itself.
In other words: "state-auto-decrease" on a component means the component
automatically decreases with its child components.
/* remove all pre-defined states */
cs.transition(null);
/* configure: state name, enter method, leave method */
cs.transition("created", "create", "destroy");
cs.transition("prepared", "prepare", "cleanup");
cs.transition("materialized", "render", "release");
cs.transition("visible", "show", "hide" );
A more detailed description of each state
(the examples use some silly jQuery-based
UI rendering, for illustration purposes
only):
create and destroy methods on the target component.
In create the component usually creates sub-components (if existing).
In destroy the component usually destroys sub-components (if existing).
com.example.ui.foo = cs.clazz({
protos: {
create: function () {
cs.create("subcomp1", com.example.ui.subcomp1);
cs.create("subcomp2", com.example.ui.subcomp2);
},
destroy: function () {
cs.destroy("subcomp1");
cs.destroy("subcomp2");
},
…
}
});
prepare and cleanup methods on the target component.
In prepare the component usually loads its presentation model.
In cleanup the component usually releases the presentation model.
com.example.ui.foo = cs.clazz({
dynamics: {
model: null
},
protos: {
prepare: function () {
/* fetch content from backend service */
var self = this;
$.get("http://backend.example.com/…", function (data) {
self.model = data;
});
},
cleanup: function () {
/* cleanup content */
this.model = null;
},
…
}
});
render and release methods on the target component.
In render the component usually renders its view (perhaps still hidden).
In release the component usually drops its view.
com.example.ui.foo = cs.clazz({
dynamics: {
model: null
},
protos: {
render: function () {
var self = this;
/* add dialog to DOM */
$(…).html(
'<div style="display: none;">' +
' <div class="content">' + this.model + '</div>' +
' <div class="overlay" style="display: none; z-index: 100;"></div>' +
'</div>'
).bind("click", function (ev) {
self.click(this, ev);
});
},
release: function () {
/* remove the dialog from DOM */
$(…).html("").unbind("click");
},
click: function (el, ev) {
…
},
…
}
});
show and hide methods on the target component.
In show the component usually shows its (already rendered) view.
In hide the component usually hides its (already rendered) view.
com.example.ui.foo = cs.clazz({
protos: {
show: function () {
/* show dialog */
$(…).css("display", "block");
},
hide: function () {
/* hide dialog */
$(…).css("display", "none");
},
…
}
});
false, the current
transitioning process is immediately stopped
and suspended for resuming later. This allows
methods to act as transition guards for entering
or leaving a state. This is usually needed in case
a resource is allocated/gathered asynchronously
in a lower state and the transition to a higher
state has to be prevented until the resource allocation
was finished.
com.example.ui.dialog = cs.clazz({
dynamics: {
model: null
},
protos: {
prepare: function () {
/* fetch content from backend service */
var self = this
$.get("http://backend.example.com/…", function (data) {
self.model = data;
});
},
render: function () {
if (this.model === null)
return false;
…
},
release: function () {
if (this.model === null)
return false;
…
},
cleanup: function () {
/* cleanup content */
this.model = null;
},
…
}
});
This allows the guard indicators to be arbitrary
complex and calculated on the fly, but this
direct transition guards approach works
only as long as (1) the guard indicator (above it
is the field this.model) is directly
accessible (usually within the same
component) and (2) the transition request is explicitly re-triggered
by yourself (as ComponentJS cannot know when your
guard indicator has switched state). Because of these
nasty drawbacks, instead of driving the guards yourself,
you usually use the ComponentJS guard mechanism:
com.example.ui.dialog = cs.clazz({
dynamics: {
model: null
},
protos: {
prepare: function () {
/* fetch content from backend service */
var self = this;
cs(self).guard("render", +1);
$.get("http://backend.example.com/…", function (data) {
self.model = data;
cs(self).guard("render", -1);
});
},
render: function () {
…
},
release: function () {
…
},
cleanup: function () {
/* cleanup content */
this.model = null;
},
…
}
});
This prevents the component from entering the higher
state through "render", asynchronously fetches the
resource and once finished and then allows the
previously requested state transition to finally
proceed. The advantage is that guards easily can be
set even on child components and that on guard deactivation
ComponentJS has a chance to resume the pending state
transitions.
subscribe call there has to be a corresponding unsubscribe call.register call there has to be a corresponding unregister call.latch call there has to be a corresponding unlatch call.observe call there has to be a corresponding unobserve call.plug call there has to be a corresponding unplug call.
com.example.ui.dialog = cs.clazz({
dynamics: {
subscription: null,
registration: null,
hooking: null,
observation: null,
plugging: null
},
protos: {
prepare: function () {
this.subscription = cs(this).subscribe({ … });
this.registration = cs(this).register ({ … });
this.hooking = cs(this).latch ({ … });
this.observation = cs(this).observe ({ … });
this.plugging = cs(this).plug ({ … });
},
cleanup: function () {
cs(this).unplug (this.plugging);
cs(this).unobserve (this.observation);
cs(this).unlatch (this.hooking);
cs(this).unregister (this.registration);
cs(this).unsubscribe(this.subscription);
}
}
});
For lots of such "allocations" this can be nasty, especially
because you have to explicitly remember the resulting ids in the component.
As this pattern is such common, you can use the spool
parameter of subscribe, register, latch,
observe and plug. This spools the corresponding
"deallocation" operation under the supplied name for later all-at-once
"deallication" via unspool. To better understand the
generic spooling mechanism, here is how you could use it manually:
com.example.ui.dialog = cs.clazz({
protos: {
prepare: function () {
var subscription = cs(this).subscribe({ … });
var registration = cs(this).register ({ … });
var hooking = cs(this).latch ({ … });
var observation = cs(this).observe ({ … });
var plugging = cs(this).plug ({ … });
cs(this).spool("prepared", cs(this), "unsubscribe", subscription);
cs(this).spool("prepared", cs(this), "unregister", registration);
cs(this).spool("prepared", cs(this), "unlatch", hooking);
cs(this).spool("prepared", cs(this), "unobserve", observation);
cs(this).spool("prepared", cs(this), "unplug", plugging);
},
cleanup: function () {
cs(this).unspool("prepared");
}
}
});
This already avoids the explicit storing of the ids and
bundles "allocation" and "deallocation" together.
During unspool the actions are executed in
reverse spooling order. With the
common spool parameter this further can be reduced to:
com.example.ui.dialog = cs.clazz({
protos: {
prepare: function () {
cs(this).subscribe({ …, spool: "prepared" });
cs(this).register ({ …, spool: "prepared" });
cs(this).latch ({ …, spool: "prepared" });
cs(this).observe ({ …, spool: "prepared" });
cs(this).plug ({ …, spool: "prepared" });
},
cleanup: function () {
cs(this).unspool("prepared");
}
}
});
Finally, ComponentJS automatically unspools
actions on every state enter/leave which were spooled under
the internal name ComponentJS:state:name:enter
and ComponentJS:state:name:leave
This way, if wished, the above can be even further reduced to:
com.example.ui.dialog = cs.clazz({
protos: {
prepare: function () {
cs(this).subscribe({ …, spool: "ComponentJS:state:prepared:leave" });
cs(this).register ({ …, spool: "ComponentJS:state:prepared:leave" });
cs(this).latch ({ …, spool: "ComponentJS:state:prepared:leave" });
cs(this).observe ({ …, spool: "ComponentJS:state:prepared:leave" });
cs(this).plug ({ …, spool: "ComponentJS:state:prepared:leave" });
}
}
});
com.example.ui.dialog1 = cs.clazz({
protos: {
create: function () {
cs(this).create("dialog2", com.example.ui.dialog2);
},
render: function () {
$("…").html(
'<div>' +
' <div class="content">…</div>' +
' <div class="childs"></div>'
'</div>' +
);
cs(this).socket($("… .childs"),
function (el) { $(this).append(el); },
function (el) { $(el).remove(); }
);
},
…
}
});
com.example.ui.dialog2 = cs.clazz({
dynamics: {
id: null
},
protos: {
render: function () {
var ui = $('<div>…</div>');
this.id = cs(this).plug(ui);
},
release: function () {
cs(this).unplug(this.id);
},
…
}
});
Sometimes the parent UI component wants to provide multiple sockets
to its clients. Then one at least has to hard-code names between parent
and childs, of course. But the components are still de-coupled, because
there is still no reason to explictly pass the particular parent UI widget object
to each child.
com.example.ui.dialog1 = cs.clazz({
protos: {
create: function () {
cs(this).create("dialog2", com.example.ui.dialog2);
cs(this).create("dialog3", com.example.ui.dialog3);
},
render: function () {
$("…").html(
'<div>' +
' <div class="content">…</div>' +
' <div class="childs1"></div>'
' <div class="childs2"></div>'
'</div>' +
);
cs(this).socket({
name: "childs1",
ctx: $("… .childs1"),
plug: function (el) { $(this).append(el); },
unplug: function (el) { $(el).remove(); }
});
cs(this).socket({
name: "childs2",
ctx: $("… .childs2"),
plug: function (el) { $(this).append(el); },
unplug: function (el) { $(el).remove(); }
});
},
…
}
});
com.example.ui.dialog2 = cs.clazz({
dynamics: {
id: null
},
protos: {
render: function () {
var ui = $('<div>…</div>');
this.id = cs(this).plug({ name: "childs1", object: ui });
},
release: function () {
cs(this).unplug(this.id);
},
…
}
});
com.example.ui.dialog3 = cs.clazz({
dynamics: {
id: null
},
protos: {
render: function () {
var ui = $('<div>…</div>');
this.id = cs(this).plug({ name: "childs2", object: ui });
},
release: function () {
cs(this).unplug(this.id);
},
…
}
});
In rare cases the parent UI component even might
need to give a particular child UI component a
different socket — without having to tell
the particular child the different socket. This
can be achieved by scoping the socket to the
child UI component:
com.example.ui.dialog1 = cs.clazz({
protos: {
create: function () {
cs(this).create("view", com.example.ui.View);
cs(this).create("detail", com.example.ui.Details);
},
render: function () {
$("…").html(
'<div>' +
' <div class="content">…</div>' +
' <div class="view"></div>'
' <div class="detail"></div>'
'</div>' +
);
cs(this).socket({
ctx: $("… .view"),
plug: function (el) { $(this).append(el); },
unplug: function (el) { $(el).remove(); }
});
cs(this).socket({
scope: "detail",
ctx: $("… .detail"),
plug: function (el) { $(this).append(el); },
unplug: function (el) { $(el).remove(); }
});
},
…
}
});
com.example.ui.Details = cs.clazz({
dynamics: {
id: null
},
protos: {
render: function () {
var ui = $('<div>…</div>');
this.id = cs(this).plug(ui); /* plugs into "detail" above */
},
release: function () {
cs(this).unplug(this.id); /* unplugs from "detail" above */
},
…
}
});
com.example.ui.panel = cs.clazz({
protos: {
create: function () {
cs(this).create("list", com.example.ui.list);
cs(this).create("preview", com.example.ui.preview);
cs(this).create("detail", com.example.ui.detail);
},
render: function () {
$("…").html(
'<div>' +
' <div class="list">…</div>' +
' <div class="preview"></div>'
' <div class="detail"></div>'
'</div>' +
);
cs(this).socket({
scope: "list",
ctx: $("… .list"),
plug: function (el) { $(this).append(el); },
unplug: function (el) { $(el).remove(); }
});
cs(this).socket({
scope: "preview",
ctx: $("… .preview"),
plug: function (el) { $(this).append(el); },
unplug: function (el) { $(el).remove(); }
});
cs(this).socket({
scope: "detail",
ctx: $("… .detail"),
plug: function (el) { $(this).append(el); },
unplug: function (el) { $(el).remove(); }
});
cs(this).subscribe({
name: "list-selection",
func: function (ev, item) {
cs("preview", this).call("show-item", item);
cs("detail", this).call("show-item", item);
}
});
},
…
}
});
com.example.ui.list = cs.clazz({
protos: {
render: function () {
var self = this;
var ui = $('<div>…</div>');
cs(self).plug(ui);
$("div", ui).click(function (ev) {
var item = … /* determine id of clicked item */
cs(self).publish("list-selection", item);
});
},
…
}
});
com.example.ui.preview = cs.clazz({
dynamics: {
view: null
},
protos: {
create: function () {
cs(this).register("show-item", this.show_item);
},
render: function () {
var view = $('<div>…</div>');
cs(this).plug(view);
},
show_item: function (item) {
var preview = make_preview_of(item);
$("div", this.view).html(preview);
},
…
}
});
com.example.ui.detail = cs.clazz({
…
});
This shows just the basic functionality of subscribe
and publish. When called with named parameters it
unleashes its additional functionalities: subscription arguments,
event capturing and bubbling, exclusive subscribers, asynchronous delivery, etc.
The mechanism is actually such powerful that even the Service
and Hook mechanisms (see below) are directly driving on top of
the Event mechanism.
/* controller (non-reusable) */
com.example.ui.menu1 = cs.clazz({
mixin: [ cs.marker.controller ],
protos: {
create: function () {
/* instanciate the reusable menu model and view sub-components */
cs(this).create(
"model", com.example.ui.menu.model,
"view", com.example.ui.menu.view
);
},
prepare: function () {
var self = this
/* route default socket of menu into specific
"menu1" socket of our panel component */
cs(self).link(self, "menu1")
/* provision view/presentation model via business model */
cs(self).register({
name: "menu1-set-items",
spool: "prepared",
func: function (items, active) {
cs(self, "model").value("data:itemList", items);
cs(self, "model").value("state:activeItem", active);
}
})
/* act on model actions */
cs(self, "menu").observe({
name: "event:selectedItem",
spool: "prepared",
func: function (ev, pos) {
cs(self).publish("menu1-active-item", pos)
}
})
},
cleanup: function () {
cs(this).unspool("prepared")
}
}
});
/* model (reusable) */
com.example.ui.menu.model = cs.clazz({
mixin: [ cs.marker.model ],
protos: {
create: function () {
/* allow model and view to automatically increase state */
cs(this).property("ComponentJS:state-auto-increase", true);
/* define the presentation model */
cs(this).model({
"data:itemList": { value: [], valid: "[string*]" },
"state:activeItem": { value: -1, valid: "number" },
"event:selectedItem": { value: -1, valid: "number", autoreset: true }
});
}
}
});
/* view (reusable) */
com.example.ui.menu.view = cs.clazz({
mixin: [ cs.marker.view ],
protos: {
render: function () {
var self = this;
/* generate outer DOM item list element */
var dom = $("<ul></ul>");
cs(self).plug({ object: dom, spool: "materialized" });
/* generate inner DOM item elements */
cs(self).observe({
name: "data:itemList",
spool: "materialized",
touchonce: true,
func: function (ev, items) {
var active = cs(self).value("state:activeItem");
for (var i = 0; i < items.length; i++) {
var clazz = "item" + (i === active ? " active" : "");
html += "<li class=\"" + clazz + "\"" +
" data-pos=\"" + i + "\" >" + items[i] + "</li>";
}
$(dom).html($html);
}
})
/* fetch items from presentation model */
cs(self).observe({
name: "state:activeItem",
spool: "materialized",
touchonce: true,
func: function (ev, active) {
$("li.active", dom).removeClass("active");
$("li", dom).eq(active).addClass("active");
}
});
/* attach to the selection event and convert the
technical DOM event into a logical model event */
$("li", dom).click(function (ev) {
var pos = parseInt($(ev.target).data("pos"));
cs(self).value("event:selectedItem", pos);
});
},
release: function () {
cs(this).unspool("materialized");
}
}
});