Enyo: Up and Running (2015)
Chapter 2. Core Concepts
Introduction
In this chapter, we’ll cover the core concepts of Enyo that we only touched on in the last chapter. You will be able to write powerful apps after absorbing the information in just this chapter. We’ll go over the concepts one by one and illustrate each with code you can run in your browser.
One of the driving ideas behind Enyo is that you can combine simple pieces to create more complex ones. Enyo introduces four concepts to assist you: kinds, encapsulation, components, and layout. We’ll cover components and layout more thoroughly in Chapter 3 and Chapter 4, respectively.
Kinds
Enyo is an object-oriented framework. It is true that every JavaScript application regardless of framework (or lack thereof) contains objects. However, Enyo’s core features provide a layer on top of JavaScript that makes it easier to express object-oriented concepts such as inheritance and encapsulation.
In Enyo, kinds are the building blocks that make up apps. The widgets that appear on screen are instances of kinds, as are the objects that perform Ajax requests. Kinds are not strictly for making visual components. Basically, kinds provide a template from which the actual objects that make up your app are generated.
Be Kind
One of the simplest possible declarations for a kind is:
enyo.kind({ name: 'MyKind' });
NAMES
Kinds don’t even need names. Enyo will automatically assign unique names, though you won’t know what they are. Anonymous kinds are often used in Enyo apps. You saw one in Chapter 1 when the color of the light was set to green in the view declaration.
Top-level kinds (those declared outside of other kinds) automatically get a global object created with that name (for example, Light in the previous chapter). It is possible to put kinds into a namespace by separating name parts with periods. For example, using name: myApp.Light will result in a myApp object with a Light member. Namespaces provide a good mechanism for preventing naming conflicts with your apps, particularly when using reusable components.
As a convention, we use uppercase names for kind definitions and lowercase names for instances of kinds (those kinds declared in the components block).
enyo.kind() is a “factory” for creating new kinds. In this case, we get a new object that inherits from the Enyo control kind, enyo.Control. Control is the base component for objects that will render when placed on a web page.
When creating kinds, you pass in an object that defines the starting state of the kind as well as any methods it will need. For example, control kinds have a content property:
enyo.kind({ name: 'MyKind', content: 'Hello World!' });
As you saw in Chapter 1, when rendered onto a page this code will create a div tag with the content placed in it. To render this into a body on a web page, you specify it as the view of an Application.
We can add behaviors to our kind by adding methods (for example, the tap handling method we added to the Light kind). As you may recall, we referenced the method name in the handlers block using a string. We use strings so Enyo can bind our methods as kinds are created.
Encapsulation
Encapsulation is a fancy computer science term that refers to restricting outside objects’ access to an object’s internal features through providing an interface for interacting with the data contained in the object. JavaScript does not have very many ways to prohibit access to an object’s data and methods from outside, so Enyo promotes encapsulation by giving programmers various tools and conventions.
By convention, Enyo kinds should have no dependencies on their parent or sibling kinds and they should not rely on implementation details of their children. While it is certainly possible to create Enyo kinds that violate these rules, Enyo provides several mechanisms to make that unnecessary. Those mechanisms include properties and events.
By being aware of encapsulation, Enyo programmers can tap in to the benefits of code reuse, easy testing, and drop-in components.
Properties
Kinds can declare properties (for example, the color and powered properties from Chapter 1). The property system allows for some very powerful features, such as two-way data binding and notification for changes to values. We’ll discuss the basic features of properties first and then dive into the more advanced features of bindings and observers.
Basic Properties
Properties are accessed using the get() and set() methods defined on all kinds. In addition, there is a mechanism for tracking changes to properties. Properties don’t need to even be declared on a kind, though you should at least document their presence so that users of your kinds (including yourself) will know (remember) that they exist.
enyo.kind({
name: 'MyKind',
myValue: 3
});
As you can see, you also specify a default value for a property. Within MyKind you can read the property directly using this.myValue. When you are accessing myValue externally (e.g., from a parent control), you should use the get() or set() methods. Whenever the value is modified using the setter, Enyo will automatically call a “changed” method. In this case, the changed method is myValueChanged(). When called, the changed method will be passed the previous value of the property as an argument.
WARNING
The set() method does not call the changed method if the value to be set is the same as the current value. You can, however, override this behavior by passing a truthy value as a third argument to set().
If you look back to our earlier discussion on kinds you may have noticed that we passed in some values for properties when we were declaring our kinds. Those values set the initial contents of those properties. Enyo does not call the changed method during construction. If you have special processing that needs to occur, you should call the changed method directly within create():
enyo.kind({
name: 'MyKind',
myValue: 3,
create: function() {
this.inherited(arguments);
this.myValueChanged();
},
myValueChanged: function(oldValue) {
// Some processing
}
});
If you want to tie the value of a property to another property within your kind (such as the content of a control), you can use a binding, which is triggered during construction. We’ll cover bindings in the next section.
WARNING
You should only specify simple values (strings, numbers, booleans, etc.) for the default values of properties and member variables. Using arrays and objects can lead to strange problems. See Instance Constructors for a method to initialize complex values.
Bindings and Observers
The set() method makes it possible to set up bindings that tie the value of two properties together. You can even monitor properties on other kinds. Bindings really stand out when it comes to associating data with the contents of controls. We’ll cover the use of bindings with data-driven applications in Chapter 5.
At their simplest, bindings create a one-way association between two properties. The following example creates a property called copy that will be updated any time the value of original changes:
enyo.kind({
name: 'ShadowKind',
original: 3,
copy: null,
bindings: [
{ from: 'original', to: 'copy' }
]
});
We could have accomplished the same thing using an originalChanged() method — however, it would have taken more code and we would not be able to monitor the value of properties declared on components declared within our kind. Further, we would have to set up a copyChanged()method if we wanted to create a two-way connection. Using bindings, we can do this with one simple change:
enyo.kind({
name: 'ShadowKind',
original: 3,
copy: null,
bindings: [
{ from: 'original', to: 'copy', oneWay: false }
]
});
Bindings have even more power, including the ability to transform values when they are triggered. We’ll cover transformations in Chapter 5.
Observers, like bindings, monitor properties for changes. When an observer detects a value change, it invokes the method specified in the observer declaration. We can rewrite the earlier changed example as follows:
enyo.kind({
name: 'MyKind',
myValue: 3,
observers: [
{ path: 'myValue', method: 'myValueUpdated' }
],
myValueUpdated: function(oldValue, newValue) {
// Some processing
}
});
Note that we did not need to override create() to invoke myValueUpdated() because bindings and observers will be triggered during initialization.
TIP
With bindings and observers, the path to a property is a string and is relative to this. Binding to a nested component’s property (see Chapter 3) can be accomplished like so: $.component.value.
Events
If properties provide a way for parent kinds to communicate with their children, then events provide a way for kinds to communicate with their parents. Enyo events give kinds a way to be notified when something they’re interested in occurs. Events can include data relevant to the event. Events are declared like this:
enyo.kind({
name: 'Eventer',
handlers: { ontap: 'myTap' },
events: { onMyEvent: '' },
content: 'Click for the answer',
myTap: function() {
this.doMyEvent({ answer: 42 });
}
});
Event names are always prefixed with “on” and are always invoked by calling a method whose name is prefixed with “do”. Enyo creates the “do” helper method for us and it takes care of checking that the event has been subscribed to. The first parameter passed to the “do” method, if present, is passed to the subscriber. Any data to be passed with the event must be wrapped in an object.
Subscribing is easy:
enyo.kind({
name: 'Subscriber',
components: [{ kind: 'Eventer', onMyEvent: 'answered' }],
answered: function(sender, event) {
alert('The answer is: ' + event.answer);
return(true);
}
});
TIP
The sender parameter is the kind that last bubbled the event (which may be different from the kind that originated the event). The event parameter contains the data that was sent from the event. The object will always have at least one member, originator, which is the Enyo component that started the event.
When responding to an event, you should return a truthy value to indicate that the event has been handled. Otherwise, Enyo will keep searching through the sender’s ancestors for other event handlers. If you need to prevent the default action for DOM events, use event.preventDefault().
TIP
Enyo kinds cannot subscribe to their own events, including DOM events, using the onXxx syntax. If you need to subscribe to an event that originates on the kind, you can use the handlers block, as we did for the previous tap event.
Advanced Events
The standard events described previously are bubbling events, meaning that they only go up the app hierarchy from the object that originated them through the object’s parent. Sometimes it’s necessary to send events out to other objects, regardless of where they are located. While it might be possible to send an event up to a shared common parent and then call back down to the target, this is far from clean. Enyo provides a method called signals to handle this circumstance.
To send a signal, call the send() method on the enyo.Signals object. To subscribe to a signal, include a Signals kind in your components block and subscribe to the signal you want to listen to in the kind declaration. The following example shows how to use signals:
enyo.kind({
name: 'Signaller',
components: [
{ kind: 'Button', content: 'Click', ontap: 'sendit' }
],
sendit: function() {
enyo.Signals.send('onButtonSignal');
}
});
enyo.kind({
name: 'Receiver',
components: [
{ name: 'display', content: 'Waiting...' },
{ kind: 'Signals', onButtonSignal: 'update' }
],
update: function(sender, event) {
this.set('$.display.content', 'Got it!');
}
});
TIP
Like regular events, signals have names prefixed with “on”. Unlike events, signals are broadcast to all subscribers. You cannot prevent other subscribers from receiving signals by passing back a truthy return from the signal handler. Multiple signals can be subscribed to using a single Signalsinstance.
Signals should be used sparingly. If you begin to rely on signals for passing information back and forth between objects, you run the risk of breaking the encapsulation Enyo tries to help you reinforce. It might be better to use a shared model to hold the data. We’ll discuss models in Chapter 5.
TIP
Enyo uses the signals mechanism for processing DOM events that do not target a specific control, such as onbeforeunload and onkeypress.
Final Thoughts on Encapsulation
While properties and events go a long way towards helping you create robust applications, they are not always enough. Most kinds will have methods they need to expose (an API, if you will) and methods they wish to keep private. While Enyo does not have any mechanisms to enforce that separation, code comments and documentation can serve to help other users of your kinds understand what is and isn’t available to outside kinds.
Inheritance
Enyo provides an easy method for deriving new kinds from existing kinds. This process is called inheritance. When you derive a kind from an existing kind, it inherits the properties, events, and methods from that existing kind. All kinds inherit from at least one other kind. The ultimate ancestor for nearly all Enyo kinds is enyo.Object. Usually, however, kinds derive from enyo.Component or enyo.Control.
To specify the parent kind, set the kind property during creation:
enyo.kind({
name: "InheritedKind",
kind: "enyo.Control"
});
As mentioned, if you don’t specify the kind, Enyo will automatically determine the kind for you. In most cases, this will be Control. An example of an instance where Enyo will pick a different kind is when creating menu items for an Onyx Menu kind. By default, components created within aMenu will be of kind MenuItem. If you want to specify the kind for child components in your own components, set the defaultKind property.
If you override a method on a derived kind and wish to call the same named method on the parent, use the inherited() method. You may recall that we did this for the create() method in the Light kind. You must always pass arguments as the parameter to the inherited() method.
Advanced Kinds
Enyo provides two additional features for declaring kinds, which are most often used when creating reusable kinds: instance constructors and statics.
Instance Constructors
For some kinds, initialization must take place when an instance of that kind is created. One particular use case is defining array properties. If you were to declare an array member in a kind definition then all instances would be initialized with the last value set to the array. This is unlikely to be the behavior you wanted. When declaring a constructor, be sure to call the inherited() method so that any parent objects can perform their initialization as well. The following is a sample constructor:
constructor: function() {
this.instanceArray = [];
this.inherited(arguments);
}
TIP
It’s worth noting that constructor() is available for all kinds. The create() method used in many examples is only available for descendants of enyo.Component.
Statics
Enyo supports declaring methods that are defined on the kind constructor. These methods are accessed by the kind name rather than from a particular instance of the kind. Statics are often used for utility methods that do not require an instance and for properties that should be shared among all instances, such as a count of the number of instances created. The following kind implements an instance counter and shows off both statics and constructors:
enyo.kind({
name: 'InstanceCounter',
constructor: function() {
InstanceCounter.count += 1;
this.inherited(arguments);
},
statics: {
count: 0,
currentCount: function() {
return(this.count);
}
}
});
TIP
STRUCTURE OF A KIND
It’s good to be consistent when declaring kinds. It helps you and others who may need to read your code later to know where to look for important information about a kind. In general, kinds should be declared in the following order:
§ Name of the kind
§ Parent kind
§ Properties, events, and handlers
§ Kind variables
§ Classes and styles
§ Components
§ Bindings and observers
§ Public methods
§ Protected and private methods
§ Static members
Summary
We have now explored the core features of Enyo. You should now understand the object oriented features that allow for creating robust and reliable apps. We’ll build upon this knowledge in the next chapters by exploring the additional libraries and features that make up the Enyo framework.