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
unspool
s
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"); } } });