YUI 3.x Home -

YUI Library Examples: Attribute: Attribute Getters, Setters and Validators

Attribute: Attribute Getters, Setters and Validators

The "Basic Attribute Configuration" example shows how you can add attributes to a host class, and set up default values for them using the attribute configuration object. This example explores how you can configure setter, getter and validator functions for individual attributes, which can be used to modify or normalize attribute values during get and set invocations, and prevent invalid values from being stored.
Enter new values and click the "Set" buttons
  • Try entering valid and invalid values for x, y; or values which attempt to position the box outside it's parent (parent box co-ordinates are displayed next to the text box).
  • Try entering rgb, hex or keyword color values [ rgb(255,0,0), #ff0000, red ].

Getter, Setter And Validator Functions

Attribute lets you configure getter and setter functions for each attribute. These functions are invoked when the user calls Attribute's get and set methods, and provide a way to modify the value returned or the value stored respectively.

You can also define a validator function for each attribute, which is used to validate the final value before it gets stored.

All these functions receive the value and name of the attribute being set or retrieved, as shown in the example code below. The name is not used in this example, but is provided to support use cases where you may wish to share the same function between different attributes.

Creating The Box Class - The X, Y And XY Attributes

In this example, we'll set up a custom Box class representing a positionable element, with x, y and xy attributes.

Only the xy attribute will actually store the page co-ordinate position of the box. The x and y attributes provide the user a convenient way to set only one of the co-ordinates. However we don't want to store the actual values in the x and y attributes, to avoid having to constantly synchronize all three. The getter and setter functions provide us with an easy way to achieve this. We'll define getter and setter functions for both the x and y attributes, which simply pass through to the xy attribute to store and retrieve values:

  1. // Setup a custom class with attribute support
  2. function Box(cfg) {
  3.  
  4. ...
  5.  
  6. // Attribute configuration
  7. var attrs = {
  8.  
  9. "parent" : {
  10. value: null
  11. },
  12.  
  13. "x" : {
  14. setter: function(val, name) {
  15. // Pass through x value to xy
  16. this.set("xy", [parseInt(val), this.get("y")]);
  17. },
  18.  
  19. getter: function(val, name) {
  20. // Get x value from xy
  21. return this.get("xy")[0];
  22. }
  23. },
  24.  
  25. "y" : {
  26. setter: function(val, name) {
  27. // Pass through y value to xy
  28. this.set("xy", [this.get("x"), parseInt(val)]);
  29. },
  30.  
  31. getter: function() {
  32. // Get y value from xy
  33. return this.get("xy")[1];
  34. }
  35. },
  36.  
  37. "xy" : {
  38. // Actual stored xy co-ordinates
  39. value: [0, 0],
  40.  
  41. setter: function(val, name) {
  42. // Constrain XY value to the parent element.
  43.  
  44. // Returns the constrained xy value, which will
  45. // be the final value stored.
  46. return this.constrain(val);
  47. },
  48.  
  49. validator: function(val, name) {
  50. // Ensure we only store a valid data value
  51. return (Y.Lang.isArray(val) &&
  52. val.length == 2 &&
  53. Y.Lang.isNumber(val[0]) && Y.Lang.isNumber(val[1]));
  54. }
  55. },
  56.  
  57. ...
  58.  
  59. this.addAttrs(attrs, cfg);
  60.  
  61. ...
  62. }
// Setup a custom class with attribute support
function Box(cfg) {
 
    ...
 
    // Attribute configuration
    var attrs = {
 
        "parent" : {
            value: null
        },
 
        "x" : {
            setter: function(val, name) {
                // Pass through x value to xy
                this.set("xy", [parseInt(val), this.get("y")]);
            },
 
            getter: function(val, name) {
                // Get x value from xy
                return this.get("xy")[0];
            }
        },
 
        "y" : {
            setter: function(val, name) {
                // Pass through y value to xy
                this.set("xy", [this.get("x"), parseInt(val)]);
            },
 
            getter: function() {
                // Get y value from xy
                return this.get("xy")[1];
            }
        },
 
        "xy" : {
            // Actual stored xy co-ordinates
            value: [0, 0],
 
            setter: function(val, name) {
                // Constrain XY value to the parent element.
 
                // Returns the constrained xy value, which will
                // be the final value stored.
                return this.constrain(val);
            },
 
            validator: function(val, name) {
                // Ensure we only store a valid data value
                return (Y.Lang.isArray(val) && 
                        val.length == 2 && 
                        Y.Lang.isNumber(val[0]) && Y.Lang.isNumber(val[1]));
            }
        },
 
    ...
 
    this.addAttrs(attrs, cfg);
 
    ...
}

The validator function for xy ensures that only valid values finally end up being stored.

The xy attribute also has a setter function configured, which makes sure that the box is always constrained to it's parent element. The constrain method which it delegates to, takes the xy value the user is trying to set and returns a constrained value if the x or y values fall outside the parent box. The value which is returned by the setter is the value which is ultimately stored for the xy attribute:

  1. // Get min, max unconstrained values for X.
  2. // Using Math.round to handle FF3's sub-pixel region values
  3. Box.prototype.getXConstraints = function() {
  4. var parentRegion = this.get("parent").get("region");
  5. return [Math.round(parentRegion.left + Box.BUFFER),
  6. Math.round(parentRegion.right - this._node.get("offsetWidth") - Box.BUFFER)];
  7. };
  8.  
  9. // Get min, max unconstrained values for Y.
  10. // Using Math.round to handle FF3's sub-pixel region values
  11. Box.prototype.getYConstraints = function() {
  12. var parentRegion = this.get("parent").get("region");
  13. return [Math.round(parentRegion.top + Box.BUFFER),
  14. Math.round(parentRegion.bottom - this._node.get("offsetHeight") - Box.BUFFER)];
  15. };
  16.  
  17. // Constrains given x,y values
  18. Box.prototype.constrain = function(val) {
  19.  
  20. // If the X value places the box outside it's parent,
  21. // modify it's value to place the box inside it's parent.
  22.  
  23. var xConstraints = this.getXConstraints();
  24.  
  25. if (val[0] < xConstraints[0]) {
  26. val[0] = xConstraints[0];
  27. } else {
  28. if (val[0] > xConstraints[1]) {
  29. val[0] = xConstraints[1];
  30. }
  31. }
  32.  
  33. // If the Y value places the box outside it's parent,
  34. // modify it's value to place the box inside it's parent.
  35.  
  36. var yConstraints = this.getYConstraints();
  37.  
  38. if (val[1] < yConstraints[0]) {
  39. val[1] = yConstraints[0];
  40. } else {
  41. if (val[1] > yConstraints[1]) {
  42. val[1] = yConstraints[1];
  43. }
  44. }
  45.  
  46. return val;
  47. };
// Get min, max unconstrained values for X. 
// Using Math.round to handle FF3's sub-pixel region values
Box.prototype.getXConstraints = function() {
    var parentRegion = this.get("parent").get("region");
    return [Math.round(parentRegion.left + Box.BUFFER), 
            Math.round(parentRegion.right - this._node.get("offsetWidth") - Box.BUFFER)];
};
 
// Get min, max unconstrained values for Y.  
// Using Math.round to handle FF3's sub-pixel region values
Box.prototype.getYConstraints = function() {
    var parentRegion = this.get("parent").get("region");
    return [Math.round(parentRegion.top + Box.BUFFER), 
            Math.round(parentRegion.bottom - this._node.get("offsetHeight") - Box.BUFFER)];
};
 
// Constrains given x,y values
Box.prototype.constrain = function(val) {
 
    // If the X value places the box outside it's parent,
    // modify it's value to place the box inside it's parent.
 
    var xConstraints = this.getXConstraints();
 
    if (val[0] < xConstraints[0]) {
        val[0] = xConstraints[0];
    } else {
        if (val[0] > xConstraints[1]) {
            val[0] = xConstraints[1];
        }
    }
 
    // If the Y value places the box outside it's parent,
    // modify it's value to place the box inside it's parent.
 
    var yConstraints = this.getYConstraints();
 
    if (val[1] < yConstraints[0]) {
        val[1] = yConstraints[0];
    } else {
        if (val[1] > yConstraints[1]) {
            val[1] = yConstraints[1];
        }
    }
 
    return val;
};

The setter, getter and validator functions are invoked with the host object as the context, so that they can refer to the host object using "this", as we see in the setter function for xy.

The Color Attribute - Normalizing Stored Values Through Get

The Box class also has a color attribute which also has a getter and validator functions defined:

  1. ...
  2. "color" : {
  3. value: "olive",
  4.  
  5. getter: function(val, name) {
  6. if (val) {
  7. return Y.Color.toHex(val);
  8. } else {
  9. return null;
  10. }
  11. },
  12.  
  13. validator: function(val, name) {
  14. return (Y.Color.re_RGB.test(val) || Y.Color.re_hex.test(val)
  15. || Y.Color.KEYWORDS[val]);
  16. }
  17. }
  18. ...
...
"color" : {
    value: "olive",
 
    getter: function(val, name) {
        if (val) {
            return Y.Color.toHex(val);
        } else {
            return null;
        }
    },
 
    validator: function(val, name) {
        return (Y.Color.re_RGB.test(val) || Y.Color.re_hex.test(val) 
                    || Y.Color.KEYWORDS[val]);
    }
}
...

The role of the getter handler in this case is to normalize the actual stored value of the color attribute, so that users always receive the hex value, regardless of the actual value stored, which maybe a color keyword (e.g. "red"), an rgb value (e.g.rbg(255,0,0)), or a hex value (#ff0000). The validator ensures the the stored value is one of these three formats.

Syncing Changes Using Attribute Change Events

Another interesting aspect of this example, is it's use of attribute change events to listen for changes to the attribute values. Box's _bind method configures a set of attribute change event listeners which monitor changes to the xy, color and parent attributes and update the rendered DOM for the Box in response:

  1. // Bind listeners for attribute change events
  2. Box.prototype._bind = function() {
  3.  
  4. // Reflect any changes in xy, to the rendered Node
  5. this.after("xyChange", this._syncXY);
  6.  
  7. // Reflect any changes in color, to the rendered Node
  8. // and output the color value received from get
  9. this.after("colorChange", this._syncColor);
  10.  
  11. // Append the rendered node to the parent provided
  12. this.after("parentChange", this._syncParent);
  13.  
  14. };
// Bind listeners for attribute change events
Box.prototype._bind = function() {
 
    // Reflect any changes in xy, to the rendered Node
    this.after("xyChange", this._syncXY);
 
    // Reflect any changes in color, to the rendered Node
    // and output the color value received from get
    this.after("colorChange", this._syncColor);
 
    // Append the rendered node to the parent provided
    this.after("parentChange", this._syncParent);
 
};

Since only xy stores the final co-ordinates, we don't need to monitor the x and y attributes individually for changes.

DOM Event Listeners And Delegation

Although not an integral part of the example, it's worth highlighting the code which is used to setup the DOM event listeners for the form elements used by the example:

  1. // Set references to form controls
  2. var xTxt = Y.one("#x");
  3. var yTxt = Y.one("#y");
  4. var colorTxt = Y.one("#color");
  5.  
  6. // Use event delegation for the action button clicks
  7. Y.on("delegate", function(e) {
  8.  
  9. // Get Node target from the event object.
  10.  
  11. // We already know it's a button which has an action because
  12. // of our selector (button.action), so all we need to do is
  13. // route it based on the id.
  14. var id = e.currentTarget.get("id");
  15.  
  16. switch (id) {
  17. case "setXY":
  18. box.set("xy", [parseInt(xTxt.get("value")), parseInt(yTxt.get("value"))]);
  19. break;
  20. case "setX":
  21. box.set("x", parseInt(xTxt.get("value")));
  22. break;
  23. case "setY":
  24. box.set("y", parseInt(yTxt.get("value")));
  25. break;
  26. case "setColor":
  27. box.set("color", Y.Lang.trim(colorTxt.get("value")));
  28. break;
  29. case "setAll":
  30. box.set("xy", [parseInt(xTxt.get("value")), parseInt(yTxt.get("value"))]);
  31. box.set("color", Y.Lang.trim(colorTxt.get("value")));
  32. break;
  33. case "getAll":
  34. getAll();
  35. break;
  36. default:
  37. break;
  38. }
  39.  
  40. }, "#attrs", "click", "button.action");
// Set references to form controls
var xTxt = Y.one("#x");
var yTxt = Y.one("#y");
var colorTxt = Y.one("#color");
 
// Use event delegation for the action button clicks
Y.on("delegate", function(e) {
 
    // Get Node target from the event object. 
 
    // We already know it's a button which has an action because
    // of our selector (button.action), so all we need to do is 
    // route it based on the id.
    var id = e.currentTarget.get("id");
 
    switch (id) {
        case "setXY":
            box.set("xy", [parseInt(xTxt.get("value")), parseInt(yTxt.get("value"))]);
            break;
        case "setX":
            box.set("x", parseInt(xTxt.get("value")));
            break;
        case "setY":
            box.set("y", parseInt(yTxt.get("value")));
            break;
        case "setColor":
            box.set("color", Y.Lang.trim(colorTxt.get("value")));
            break;
        case "setAll":
            box.set("xy", [parseInt(xTxt.get("value")), parseInt(yTxt.get("value"))]);
            box.set("color", Y.Lang.trim(colorTxt.get("value")));
            break;
        case "getAll":
            getAll();
            break;
        default:
            break;
    }
 
}, "#attrs", "click", "button.action");

Rather than attach individual listeners to each button, the above code uses YUI 3's delegate support, to listen for clicks from buttons with an action class which bubble up to the attrs element.

The delegate listener uses the Event Facade which normalizes cross-browser access to DOM event properties, such as currentTarget, to route to the appropriate button handler. Note the use of selector syntax when we specify the elements for the listener (e.g. #attrs, button.actions) and the use of the Node facade when dealing with references to HTML elements (e.g. xTxt, yTxt, colorTxt).

Copyright © 2009 Yahoo! Inc. All rights reserved.

Privacy Policy - Terms of Service - Copyright Policy - Job Openings