Controls

enyo/Control

enyo/Control is a component that controls a DOM node (i.e., an element in the user interface). Controls are generally visible and the user often interacts with them directly. Things like buttons and input boxes are obviously controls, but in Enyo a control may become as complex as an entire application.

The Basics

In the following example, we define a Circle control, which we will put to use inside a TrafficLight control:

    var
        kind = require('enyo/kind'),
        Control = require('enyo/Control');

    module.exports = kind({
        name: 'Circle',
        kind: Control,
        published: {
            color: 'magenta',
            bgColor: 'black'
        },
        handlers: {
            ondown: 'downHandler',
            onup: 'upHandler'
        },
        content: 'Hi',
        style: 'padding: 2px 6px; border: 3px solid; border-radius: 20px; cursor: pointer;',
        create: function () {
            this.inherited(arguments);
            this.colorChanged();
        },
        colorChanged: function () {
            this.applyStyle('border-color', this.color);
        },
        bgColorChanged: function () {
            this.applyStyle('background-color', this.bgColor);
        },
        downHandler: function (sender, ev) {
            this.applyStyle('background-color', 'white');
        },
        upHandler: function (sender, ev) {
            this.applyStyle('background-color', 'black');
        }
    });

The Circle has a kind value of Control and therefore inherits and extends the behavior of enyo/Control. Since a control is a component (i.e., enyo/Control extends enyo/Component), it can publish properties, as is done here.

Manipulating a Control's DOM Node

A control exposes functionality to manipulate its DOM node. Notice the use of content and style in the Circle object. The content property sets the HTML that the control will render. Since the Control object publishes the content property, you may access the content by calling set() and get(). The style property specifies CSS styles that the control will use to decorate its node. You may also specify classes, attributes, or even a tag type. For example:

    {tag: 'input', classes: 'rounded', attributes: {value: 'foo'}}

These properties may be set either on control configuration blocks or in kind definitions.

There are also methods available to modify these properties; for example, applyStyle() sets a specific style property to a given value. There are many other such methods, including addStyles(), addClass(), setAttribute(), show(), hide(), and render(). See the API documentation for more info.

Controls in Controls

Here is our aforementioned TrafficLight control:

    var
        kind = require('enyo/kind'),
        Control = require('enyo/Control');

    module.exports = kind({
        name: 'TrafficLight',
        kind: Control,
        style: 'position: absolute; padding: 4px; border: 1px solid black; background-color: silver;',
        components: [
            {kind: Circle, color: 'red', ontap: 'circleTap'},
            {kind: Circle, color: 'yellow', ontap: 'circleTap'},
            {kind: Circle, color: 'green', ontap: 'circleTap'}
        ],
        circleTap: function (sender, ev) {
            var lights = {red: 'tomato', yellow: '#FFFF80', green: 'lightgreen'};
            if (this.lastCircle) {
                this.lastCircle.set('bgColor', 'black');
            }
            this.lastCircle = sender;
            this.lastCircle.set('bgColor', lights[sender.color]);
        }
    });
TrafficLight control

TrafficLight control

Because they are components, controls may contain other controls. A control will render any controls contained inside itself. Thus, a TrafficLight will render with three Circle instances inside it and, if our styling is correct, will look like an actual traffic light. Note that if the control contains other controls and also specifies a value for content, it will render the child controls and ignore the value of content.

Controls and Events

Enyo controls can handle common DOM events. In a component configuration block, specify the handler for a DOM event just as you would for any other Enyo event--with a named delegate. The TrafficLight kind does this for its circle controls by setting ontap: 'circleTap'. Since TrafficLight owns its circles, it will process their events; thus, circleTap should be the name of a handler method inside TrafficLight.

Now, the ontap event is not a DOM event per se; it actually belongs to a set of cross-platform events that behave like DOM events and so are referred to as "DOM-like". Enyo normalizes these events across different platforms so that users may write a single set of event handlers for applications that run on both mobile and desktop platforms.

Notice that the Circle kind handles some events itself. It reacts when the user presses down and then releases. DOM (and DOM-like) events that a kind should handle are specified in the handlers block. The Circle kind specifies handlers for the down and up events. These handlers are string delegate names of methods in the Circle kind that will handle the events. Again, strictly speaking, the down and up events are not DOM events, but are DOM-like.

As with all events, the first argument sent to the event handler is the sender object, the Enyo control that generated the event. DOM and DOM-like events pass the event object as the second argument.

Since DOM and DOM-like events bubble up through the DOM, they may be handled by any control in the control hierarchy. For example, a user of TrafficLight could implement an ontap event handler and receive taps on the circles inside the TrafficLight.

Lifecycle Methods: hasNode() and rendered()

Since enyo/Control is a kind of Component, it inherits the create() and destroy() methods that we saw in Components, as well as the constructor() method that we saw in Kinds. It also introduces a few additional 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 may 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.

Now, there are certain things you cannot do unless DOM has been created. Obviously, you cannot do work directly on a DOM node 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 perform an action immediately after the DOM is created, you can do it in the rendered() method:

    rendered: function () {
        // important! must call the inherited method
        this.inherited(arguments);
        // this is the first moment that bounds are available
        this.firstBounds = this.getBounds();
    }

An 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 visual artifacts.

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() before using this.node:

    rendered: function () {
        this.inherited(arguments); // important! must call the inherited method
        if (this.hasNode()) {
            buffNode(this.node);
        }
    });

When Controls Are Rendered

In general, controls are not rendered until you you explicitly say so. Most applications have a renderInto() or write() call 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.

Additional Reading