Example: Attribute Change Events

Attribute change events are one of the key benefits of using attributes to maintain state for your objects, instead of regular object properties. This example shows how you can listen for attribute change events and work with the event payload they receive.

Enter a new value and click the "Change Value" button:

:

:

:

Listening For Attribute Change Events

In this example, we'll look at how you can setup listeners for attribute change events, and work with the event payload which the listeners receive.

Setting Up A Custom Class With Attribute

We start by setting up the same custom class we created for the basic example with 3 attributes foo, bar and foobar, using the code below:

YUI().use("attribute", "node", function(Y) {

    // Setup a custom class with attribute support
    function MyClass(cfg) {

        // Setup attribute configuration
        var attrs = {
            "foo" : {
                value:5
            },
     
            "bar" : {
                value:"Hello World!"
            },
    
            "foobar" : {
                value:true
            }
        };

        this.addAttrs(attrs, cfg);
    }

    Y.augment(MyClass, Y.Attribute);

});

Registering Event Listeners

Once we have an instance of the custom class, we can use the on and after methods provided by Attribute, to listen for changes in the value of each of the attributes:

var o1 = new MyClass();

...

// Event Listners
o1.after("fooChange", function(e) {
    displayEvent(e, "After fooChange");
    currentValSpan.set("innerHTML", Y.Escape.html(e.newVal+""));
});

o1.after("barChange", function(e) {
    displayEvent(e, "After barChange");
    currentValSpan.set("innerHTML", Y.Escape.html(e.newVal+""));
});

o1.on("foobarChange", function(e) {

    if (preventFoobarChk.get("checked")) {

        // Calling preventDefault, in an "on" listener
        // will prevent the attribute change from occuring
        // and the after listener being called.

        e.preventDefault();
        displayEvent(null, "On foobarChange (prevented)");
    }

});

o1.after("foobarChange", function(e) {

    // This foobar after listener will not get called, 
    // if we end up preventing default in the "on" 
    // listener above.

    displayEvent(e, "After foobarChange");
    currentValSpan.set("innerHTML", Y.Escape.html(e.newVal+""));
});

As seen in the above code, the event type for attribute change events is created by concatenating the attribute name with "Change" (e.g. "fooChange"), and this event type is used for both the on and after subscription methods. Whenever an attribute value is changed through Attribute's set method, both "on" and "after" subscribers are notified.

On vs. After

on : Subscribers to the "on" moment, will be notified before any actual state change has occurred. This provides the opportunity to prevent the state change from occurring, using the preventDefault method of the event facade object passed to the subscriber. If you use get to retrieve the value of the attribute in an "on" subscriber, you will receive the current, unchanged value. However the event facade provides access to the value which the attribute is being set to, through it's newVal property.

after : Subscribers to the "after" moment, will be notified after the attribute's state has been updated. This provides the opportunity to update state in other parts of your application, in response to a change in the attribute's state.

Based on the definition above, after listeners are not invoked if state change is prevented, for example, due to one of the on listeners calling preventDefault on the event object, as is done in the on listener for the foobar attribute:

o1.on("foobarChange", function(event) {

    // Calling preventDefault, in an "on" listener
    // will prevent the attribute change from occurring
    // and prevent the after listeners from being called
    displayEvent(event, "on foobarChange (change prevented)");

    event.preventDefault();
});

For primitive values (non-Object values), the after listeners will also not be invoked if there is no change in the actual value of the attribute. That is, if the new value of the attribute is the same as the current value (based on the identity operator, ===), the after listeners will not be notified because there is no change in state. You can see this, by setting an attribute to the same value twice in a row.

Event Facade

The event object (an instance of EventFacade) passed to attribute change event subscribers, has the following interesting properties and methods related to attribute management:

newVal
The value which the attribute will be set to (in the case of "on" subscribers), or has been set to (in the case of "after" subscribers
prevVal
The value which the attribute is currently set to (in the case of "on" subscribers), or was previously set to (in the case of "after" subscribers
attrName
The name of the attribute which is being set
subAttrName
Attribute also allows you to set nested properties of attributes which have values which are objects through the set method (e.g. o1.set("x.y.z")). This property will contain the path to the property which was changed.
preventDefault()
This method can be called in an "on" subscriber to prevent the attribute's value from being updated (the default behavior). Calling this method in an "after" listener has no impact, since the default behavior has already been invoked.
stopImmediatePropagation()
This method can be called in "on" or "after" subscribers, and will prevent the rest of the subscriber stack from being invoked, but will not prevent the attribute's value from being updated.

The "Attribute Event Based Speed Dating" example provides a look at how you can leverage attribute change events in your applications, to decouple logic both within your class, and when interacting with other objects.

Complete Example Source

<form id="changeValue" class="attrs" action="#">
    <div class="header">Enter a new value and click the "Change Value" button:</div>
    <div class="body">
        <p>
            <label for="attrSel">Attribute</label>: 
            <select id="attrSel">
                <option value="foo">foo</option>
                <option value="bar">bar</option>
                <option value="foobar">foobar</option>
            </select>
            <label id="preventFoobar" class="hidden"><input type="checkbox" checked="true"> Prevent change</label>
        </p>
        <p><label for="currentVal">Current Value</label>: <span id="currentVal"></span></p>
        <p><label for="newVal">New Value</label>: <input type="text" id="newVal" /></p>
    </div>
    <div class="footer">
        <button type="submit">Change Value</button>
    </div>
</form>

<div id="example-out"></div>

<script type="text/javascript">
// Get a new YUI instance 
YUI().use("node", "attribute", "escape", function(Y) {

    // Setup a custom class with attribute support
    function MyClass(cfg) {

        // Setup attribute configuration
        var attrs = {
            "foo" : {
                value:5
            },
     
            "bar" : {
                value:"Hello World!"
            },
    
            "foobar" : {
                value:true
            }
        };

        this.addAttrs(attrs, cfg);
    }

    Y.augment(MyClass, Y.Attribute);

    var o1 = new MyClass();

    function displayEvent(e, title) {
        var str = '<div class="event"><div class="event-title">' + title + '</div>';

        if (e) {
            str += 
            '<ul class="event-props"><li>e.attrName: ' 
            + e.attrName 
            + '</li><li>e.prevVal: '
            + Y.Escape.html(e.prevVal + "")
            + '</li><li>e.newVal: '
            + Y.Escape.html(e.newVal + "")
            + '</li></ul></div>';
        }

        str += '</div>';
 
        Y.one("#example-out").prepend(str);
    }

    // Start Example Form Handling
    var attrSel = Y.one("#attrSel");
    var newValTxt = Y.one("#newVal");
    var currentValSpan = Y.one("#currentVal");
    var preventFoobarChk = Y.one("#preventFoobar input[type=checkbox]");
    var preventFoobarLbl = Y.one("#preventFoobar");

    var attrOpts = attrSel.get("options");

    function updateVal(e) {
        e.preventDefault();
        
        var selIndex = attrSel.get("selectedIndex");
        var attr = attrOpts.item(selIndex).get("value");
        o1.set(attr, newValTxt.get("value"));
    }

    Y.on("submit", updateVal, "#changeValue");

    function populateCurrentValue() {
        var selIndex = attrSel.get("selectedIndex");
        var attr = attrOpts.item(selIndex).get("value");

        currentValSpan.set("innerHTML", Y.Escape.html(o1.get(attr) + ""));
        newValTxt.set("value", "");

        if (attr === "foobar") {
            preventFoobarLbl.removeClass("hidden");
        } else {
            preventFoobarLbl.addClass("hidden");
        }
    }

    populateCurrentValue();

    Y.on("change", populateCurrentValue, attrSel);
    // End Example Form Handling

    // Attribute Change Event Listners

    o1.after("fooChange", function(e) {
        displayEvent(e, "After fooChange");
        currentValSpan.set("innerHTML", Y.Escape.html(e.newVal+""));
    });

    o1.after("barChange", function(e) {
        displayEvent(e, "After barChange");
        currentValSpan.set("innerHTML", Y.Escape.html(e.newVal+""));
    });

    o1.on("foobarChange", function(e) {

        if (preventFoobarChk.get("checked")) {

            // Calling preventDefault, in an "on" listener
            // will prevent the attribute change from occuring
            // and the after listener being called.

            e.preventDefault();
            displayEvent(null, "On foobarChange (prevented)");
        }

    });

    o1.after("foobarChange", function(e) {

        // This foobar after listener will not get called, 
        // if we end up preventing default in the "on" 
        // listener above.

        displayEvent(e, "After foobarChange");
        currentValSpan.set("innerHTML", Y.Escape.html(e.newVal+""));
    });

});
</script>