Remember that Enyo kinds use regular JavaScript prototypes. A trivial kind has a simple lifecycle:
var MyKind = enyo.kind({
kind: null, // otherwise it will default to 'Control'
constructor: function() {
// do any initialization tasks
}
});
That's it. Now MyKind
is a function that you can use with the new
operator
to create instances. The MyKind
function is a copy of a boilerplate function
that will call your (optional) constructor
function.
myInstance = new MyKind();
Like all JavaScript objects, a kind instance is garbage-collected when there are no references to it.
Note: While the Component
and Control
kinds have some advanced features,
enyo.kind()
itself is fairly simple.
It's possible to base a kind on another kind. The new kind inherits all the
properties and methods of the old kind. If you override a method from the old
kind, you can call the overridden method using inherited()
:
var MyNextKind = enyo.kind({
kind: MyKind,
constructor: function() {
// do any initialization tasks before MyKind initializes
//
// do inherited initialization (optional, but usually a good idea)
this.inherited(arguments);
//
// do any initialization tasks after MyKind initializes
}
});
MyNextKind
starts with all the properties and methods of MyKind
, but then we
override constructor()
. Our new constructor can call the old constructor
using the this.inherited(arguments)
syntax. MyNextKind
can decide what
things to do before or after calling the old constructor, or can choose to not
call the old constructor at all.
This override system is the same for any method, not just for constructor
.
Note that you could call an inherited method using only raw JavaScript, like so:
MyKind.prototype.<method name>.apply(this, arguments);
The this.inherited
syntax is shorter and removes the need to specify the
superkind's name (in this case, MyKind
).
Also note that arguments
is a
JavaScript built-in
whose value is a pseudo-array of the arguments passed to the currently executing
function.
All kinds may have constructor methods as described above. The Component
kind
adds several new lifecycle methods, most notably create()
and destroy()
:
var myComponent = enyo.kind({
kind: enyo.Component,
constructor: function() {
// low-level or esoteric initialization, usually not needed at all
this.inherited(arguments);
},
create: function() {
// create is called *after* the constructor chain is finished
this.inherited(arguments);
// this.$ hash available only *after* calling inherited create
},
destroy: function() {
// do inherited teardown
this.inherited(arguments);
}
});
Technically, create()
differs from constructor()
in that the constructor
call finishes completely (including any chain of inherited calls) before
create()
is called, but normal initialization can be done in either method.
For convenience, most components can simply ignore constructor()
and rely on
create()
.
The other important feature of create()
is that the this.$
hash is ready
after Component.create()
has executed. This means that an override of
create
is generally where you do initialization tasks for owned Components.
For example:
components: [
{kind: "MyWorker"}
],
create: function() {
this.inherited(arguments);
// put MyWorker to work
this.$.myWorker.work();
}
Components frequently reference other components. In the example above, the
MyWorker
instance is referenced by this.$.myWorker
. The destroy()
method
cleans up these references and allows component to be garbage-collected. Component kinds also put general cleanup code in destroy()
.
destroy: function() {
// stop MyWorker from working
this.$.myWorker.stop();
// standard cleanup
this.inherited(arguments);
// this.$ hash is empty now
}
Note: If you've made a custom reference to a component that is not cleaned
up by a destroy()
method, then calling destroy()
will not actually make that
object subject to garbage collection. For this reason, there is a
this.destroyed
flag on each component. If this.destroyed
is true
, the
Component has been uninitialized and the reference should be removed.
Control
is a kind of component, so controls have the same constructor()
,
create()
, destroy()
system described above, but the Control
kind
introduces a few more methods for managing its representation in the DOM.
Control
is designed so that most of its methods and properties can be used
without regard to whether there is a corresponding DOM node or not. For
example, you can call
this.$.control.applyStyle("color", "blue");
If the control is rendered, that style is placed in the DOM; if not, the information is stored until the DOM is created. Either way, when you see it, the text will be blue.
There are certain things you cannot do unless DOM has been created. Obviously,
you cannot do work directly on a DomNode
if there is no DOM. Also, some methods
(e.g., getBounds()
) return values measured from DOM, so if there is no DOM,
you won't get accurate values.
In a control, if you need to do something immediately after the DOM is created,
you can do that in the rendered()
method:
rendered: function() {
// important! must call the inherited method
this.inherited(arguments);
// this is the first moment bounds are available
this.firstBounds = this.getBounds();
}
Another important thing to note here is that even though DOM elements have been
created when rendered()
is called, the visual representation of these elements
in the browser is generally not visible yet. Most browsers will not update the
display until Enyo yields the JavaScript thread. This means you have the
ability to make changes to the DOM in rendered()
without causing annoying
flashes.
Whenever you need to access a control's DOM node directly, use the hasNode()
method:
twiddle: function() {
if (this.hasNode()) {
buffNode(this.node); // buffNode is made-up
}
}
Remember that even if DOM is rendered, this.node
may not have a value. A
truthy result from hasNode()
means this.node
is initialized. Even within
rendered()
, you need to call hasNode()
first:
rendered: function() {
this.inherited(arguments); // important! must call the inherited method
if (this.hasNode()) {
buffNode(this.node);
}
});
In general, controls are not rendered until you you explicitly say so. Most
applications have a renderInto()
or write()
method at the very top that
renders the controls in the application.
Also, when creating controls dynamically, Enyo will not render those controls
until you call render()
on them, or on one of their containers. You will
often see code like the following:
updateControls: function() {
// destroy controls we made last time
// ('destroyClientControls' destroys only dynamic controls;
// 'destroyControls' destroys everything)
this.destroyClientControls();
// create new controls
for (var i=0; i<this.count; i++) {
// created but not rendered
this.createComponent({kind: "myCoolControl", index: i});
}
// render everything in one shot
this.render();
});
Destroying a control will cause it to be removed from the DOM right away, as
this is a necessary part of the cleanup process. There is no unrender
method.
The preceding examples use this syntax for creating kinds:
var MyKind = enyo.kind(...);
In most Enyo code, however, you'll see the following:
enyo.kind({
name: "MyKind"
...
});
Here the inclusion of a name
property is optional. If you include it, Enyo
will make a global reference to your kind using that name, which will be
available in instances as this.kindName
. The name
syntax is used mostly to
take advantage of these conveniences. For example, instead of
var foo = {
kinds: {
MyKind: enyo.kind(...);
}
}
one can write
enyo.kind({
name: "foo.kinds.MyKind",
...
});