Example: Creating Custom Widget Classes With Extensions

This example shows how you can mix and match the WidgetPosition, WidgetPositionAlign, WidgetStack and WidgetStdMod extensions to build custom versions of the Widget class, using Base.create.

Widget with WidgetStdMod
Module Header
Module Body
Module Footer

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nunc pretium quam eu mi varius pulvinar. Duis orci arcu, ullamcorper sit amet, luctus ut, interdum ac, quam.

Widget with WidgetPosition, WidgetStack
Positionable Widget

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nunc pretium quam eu mi varius pulvinar. Duis orci arcu, ullamcorper sit amet, luctus ut, interdum ac, quam. Pellentesque euismod. Nam tincidunt, purus in ultrices congue, urna neque posuere arcu, aliquam tristique purus sapien id nulla. Etiam rhoncus nulla at leo. Cras scelerisque nisl in nibh. Sed eget odio. Morbi elit elit, porta a, convallis sit amet, rhoncus non, felis. Mauris nulla pede, pretium eleifend, porttitor at, rutrum id, orci. Quisque non urna. Nulla aliquam rhoncus est.

Widget with WidgetPosition, WidgetStack, WidgetPositionAlign

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nunc pretium quam eu mi varius pulvinar. Duis orci arcu, ullamcorper sit amet, luctus ut, interdum ac, quam. Pellentesque euismod. Nam tincidunt, purus in ultrices congue, urna neque posuere arcu, aliquam tristique purus sapien id nulla. Etiam rhoncus nulla at leo. Cras scelerisque nisl in nibh. Sed eget odio. Morbi elit elit, porta a, convallis sit amet, rhoncus non, felis. Mauris nulla pede, pretium eleifend, porttitor at, rutrum id, orci. Quisque non urna. Nulla aliquam rhoncus est.

Creating Custom Widget Classes

The Base class provides a create method which can be used to create custom versions of classes which derive from Base by adding extension classes to them.

Widget currently ships with four such extensions: WidgetPosition, WidgetStack, WidgetPositionAlign and WidgetStdMod. These extensions are used to create the basic Overlay widget, but can also be used individually, to create custom versions of the base Widget class.

Widget with WidgetStdMod support

Adding the WidgetStdMod extension to Widget, creates a statically positioned Widget, with support for standard module format sections - header, body and footer, which maybe useful in portal type use cases, where the positioning/stacking capabilities which come bundled with Overlay are not required.

To create a custom class, we use Base.create, which is described in detail on the documention page for Base.

We pass in Widget as the main class we want to add extensions to, and WidgetStdMod as the extension we'd like added to the main class:

var StandardModule = Y.Base.create("standardModule", Y.Widget, [Y.WidgetStdMod]);

// Render from Markup
var stdmod = new StandardModule({
    contentBox: "#widget1",
    width:"12em",
    height:"12em"
});
stdmod.render();

Base.create will:

  1. Create a new class which extends Widget
  2. Aggregate known Base and Widget fields, such as ATTRS and HTML_PARSER from WidgetStdMod on the new class
  3. Augment prototype methods from WidgetStdMod onto the new class prototype

The first argument to create is the NAME of the new class we are creating, just like the NAME we define when extending the Widget class directly.

Note that the Widget class is unchanged, allowing you to still create Widget instances without any standard module support, along with StandardModule instances which have standard module support.

Testing It Out

The example attempts to set content on an instance of the newly created StandardModule class, using the setStdModContent method which is added by the extension (which would otherwise not exist on the Widget instance).

var contentInput = Y.one("#content");
var sectionInput = Y.one("#section");

// This should work, since the StandardModule widget has settable 
// header/body/footer sections
Y.on("submit", function(e) {
    e.preventDefault();

    var content = Y.Escape.html(contentInput.get("value"));
    var section = sectionInput.get("value");

    stdmod.setStdModContent(section, content);

}, "#widget1-example");

To verify that no unrequested features are added, we also attempt to move the instance using the move method, which is not part of the base Widget class, and would be added by the WidgetPosition extension. This verifies that the other example classes we'll create, which do create new classes which use WidgetPosition, have not modified the base Widget class.

// This shoud fail, since the StandardModule widget is not positionable
Y.on("click", function(e) {
    try {
        stdmod.move([0,0]);
    } catch (e) {
        alert("move() is " + typeof stdmod.move + ", stdmod.hasImpl(Y.WidgetPosition) : "
        + stdmod.hasImpl(Y.WidgetPosition));
    }
}, "#tryMove");

Note that Base.create adds a hasImpl method to the built class, which allows you to query whether or not it has a particular extension applied.

CSS Considerations

We need to define the CSS which goes with this new StandardModule class we have created. The only rule really required out of the box is the rule which handles visibility (yui-standardmodule-hidden). The "standardmodule" used in the class name comes from the NAME property we set up for the new class, and is used to prefix all state related classes added to the widgets bounding box. Since the StandardModule class is not positionable, we use display:none to define the hidden state.

/* Visibility - How to handle visibility for this new widget */
.yui3-standardmodule-hidden {
    display:none;
}

The other "yui-standardmodule" rules are only used to create the required look/feel for this particular example, and do not impact the StandardModule widget's functionality.

Widget with WidgetPosition and WidgetStack support

As with StandardModule, we use Base.create to create the new Positionable widget class. This time we add WidgetPosition and WidgetStack support to the base Widget class to create a basic XY positionable widget, with shimming and z-index support.

var Positionable = Y.Base.create("positionable", Y.Widget, 
                            [Y.WidgetPosition, Y.WidgetStack]);

// Render from markup
var positionable = new Positionable({
    contentBox: "#widget2",
    width:"10em",
    height:"10em",
    zIndex:1
});
positionable.render("#widget2-example");

var xy = Y.one("#widget2-example > p").getXY();

positionable.move(xy[0], xy[1]);

We don't add WidgetPositionAlign or WidgetStdMod support, so the widget doesn't have extended positioning support (align, center) or standard module support. Hence we position it manually using the move method which the WidgetPosition extension provides.

Testing It Out

We should now be able to invoke the move method on an instance of the newly created Positionable class:

// This should work, since Positionable has basic XY Positioning support
Y.on("submit", function(e) {
    e.preventDefault();
    
    var x = parseInt(xInput.get("value"));
    var y = parseInt(yInput.get("value"));

    positionable.move(x,y);

}, "#widget2-example");

And, as with the StandardModule class, we should not be allowed to invoke any methods from an extension which we didn't request:

// This should fail, since Positionable does not have Standard Module sections
Y.on("click", function(e) {
    try {
        positionable.setStdModContent("header", "new content");
    } catch (e) {
        alert("setStdModContent() is " + typeof positionable.setStdModContent + 
            ", positionable.hasImpl(Y.WidgetStdMod) : " + positionable.hasImpl(Y.WidgetStdMod));
    }
}, "#tryContent");

CSS Considerations

Since now we have a positionable widget, with z-index support, we set the widget to be absolutely positioned by default, and control it's hidden state using visibility as opposed to display

/* Define absolute positioning as the default for positionable widgets */
.yui3-positionable {
    position:absolute;
}

/* 
   In order to be able to position the widget when hidden, we define hidden
   to use visibility, as opposed to display
*/
.yui3-positionable-hidden {
    visibility:hidden;
}

Widget with WidgetPosition, WidgetStack and WidgetPositionAlign support

Lastly, we'll attempt to create a new widget class, which, in addition to basic positioning and stacking support, also has extended positioning support, allowing us to align it with other elements on the page.

Again, we use Base.create to create our new Alignable widget class, by combining WidgetPosition, WidgetStack and WidgetPositionAlign with the base widget class:

var Alignable = Y.Base.create("alignable", Y.Widget, 
                    [Y.WidgetPosition, Y.WidgetPositionAlign, Y.WidgetStack]);

var alignable = new Alignable({
    width:"14em",
    align : {
        node: "#widget3-example",
        points: ["cc", "cc"]
    },
    zIndex:1
});
alignable.get("contentBox").set("innerHTML", 
    '<strong>Alignable Widget</strong><div id="alignment"><p>#widget3-example</p> \
    <p>[center, center]</p></div>');
alignable.render("#widget3-example");

Testing It Out

We'll attempt to align an instance of the Alignable class, using some of the additional attributes which WidgetPositionAlign adds to the base Widget class: align and centered:

// Align left-center egde of widget to 
// right-center edge of the node with id "widget3-example"
alignable.set("align", {node:"#widget3-example", points:["lc", "rc"]});

// Align top-right corner of widget to 
// bottom-right corner of the node with id "widget3-example"
alignable.set("align", {node:"#widget3-example", points:["tr", "br"]});

// Center the widget in the node with id "widget3-example"
alignable.set("centered", "widget3-example");

// Align the right-center edge of the widget to 
// the right center edge of the viewport (since a node is not provided to 'align')
alignable.set("align", {points:["rc", "rc"]});

// Center the widget in the viewport (wince a node is not provided to 'centered')
alignable.set("centered", true);

// Return the node to it's original alignment 
// (centered in the node with id "widget3-example")
// NOTE: centered is a shortcut for align : { points:["cc", "cc"] }
alignable.set("align", {node:"#widget3-example", points:["cc", "cc"]});

CSS Considerations

The Alignable widget class, has the same core CSS rules as the Positionable class, to define how it is positioned and how it is hidden:

/* Define absolute positioning as the default for alignable widgets */
.yui3-alignable {
    position:absolute;
}

/* 
   In order to be able to position the widget when hidden, we define hidden
   to use visibility, as opposed to display
*/
.yui3-alignable-hidden {
    visibility:hidden;
}

Complete Example Source

<dl id="widget-build-examples">
<dt><code>Widget</code> with <code>WidgetStdMod</code></dt>
<dd>
    <form class="widget-build-example" id="widget1-example" action="#">
        <input type="text" id="content" value="">
        <select id="section">
            <option value="header">Header</option>
            <option value="body">Body</option>
            <option value="footer">Footer</option>
        </select>
        <button type="submit" id="setHTML">Set Content</button>
        <button type="button" class="fail" id="tryMove">move() (should fail)</button>
    
        <div id="widget1">
            <div class="yui3-widget-hd">Module Header</div>
            <div class="yui3-widget-bd">Module Body</div>
            <div class="yui3-widget-ft">Module Footer</div>
        </div>
    
        <p class="filler">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nunc pretium quam eu mi varius pulvinar. Duis orci arcu, ullamcorper sit amet, luctus ut, interdum ac, quam.</p>
    </form>
</dd>

<dt><code>Widget</code> with <code>WidgetPosition, WidgetStack</code></dt>

<dd>
    <form class="widget-build-example" id="widget2-example" action="#">
        <label>X: <input type="text" id="x" value="0" ></label>
        <label>Y: <input type="text" id="y" value="0" ></label>
        <button type="submit" id="move">Move</button>
        <button type="button" class="fail" id="tryContent">setStdModContent() (should fail)</button>
    
        <div id="widget2"><strong>Positionable Widget</strong></div>
    
        <p class="filler">
            <select>
                <option>Prevent IE6 Bleedthrough</option>
            </select>
            Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nunc pretium quam eu mi varius pulvinar. Duis orci arcu, ullamcorper sit amet, luctus ut, interdum ac, quam. Pellentesque euismod. Nam tincidunt, purus in ultrices congue, urna neque posuere arcu, aliquam tristique purus sapien id nulla. Etiam rhoncus nulla at leo. Cras scelerisque nisl in nibh. Sed eget odio. Morbi elit elit, porta a, convallis sit amet, rhoncus non, felis. Mauris nulla pede, pretium eleifend, porttitor at, rutrum id, orci. Quisque non urna. Nulla aliquam rhoncus est. 
        </p>
    </form>
</dd>

<dt><code>Widget</code> with <code>WidgetPosition, WidgetStack, WidgetPositionAlign</code></dt>

<dd>
    <div class="widget-build-example" id="widget3-example">
        <button type="button" id="run">Step Through Alignments (every 2.5s)</button>
        <p class="filler">
            <select>
                <option>Prevent IE6 Bleedthrough</option>
            </select>
            Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nunc pretium quam eu mi varius pulvinar. Duis orci arcu, ullamcorper sit amet, luctus ut, interdum ac, quam. Pellentesque euismod. Nam tincidunt, purus in ultrices congue, urna neque posuere arcu, aliquam tristique purus sapien id nulla. Etiam rhoncus nulla at leo. Cras scelerisque nisl in nibh. Sed eget odio. Morbi elit elit, porta a, convallis sit amet, rhoncus non, felis. Mauris nulla pede, pretium eleifend, porttitor at, rutrum id, orci. Quisque non urna. Nulla aliquam rhoncus est.
        </p>
    </div>
</dd>
</dl>

<script type="text/javascript">
YUI().use("widget", "widget-position", "widget-stack", "widget-position-align", "widget-stdmod", "async-queue", "escape", function(Y) {

    // WIDGET 1 : Build Widget with StdMod, but no positioning/stacking support.

    var StandardModule = Y.Base.create("standardModule", Y.Widget, [Y.WidgetStdMod]);

    var stdmod = new StandardModule({
        srcNode: "#widget1",
        width:"12em",
        height:"12em"
    });
    stdmod.render();

    var contentInput = Y.one("#content");
    var sectionInput = Y.one("#section");

    Y.on("submit", function(e) {
        e.preventDefault();
        
        var content = Y.Escape.html(contentInput.get("value"));
        var section = sectionInput.get("value");

        stdmod.setStdModContent(section, content);

    }, "#widget1-example");

    Y.on("click", function(e) {
        try {
            stdmod.move([0,0]);
        } catch (e) {
            alert("move() is " + typeof stdmod.move + ", stdmod.hasImpl(Y.WidgetPosition) : " + stdmod.hasImpl(Y.WidgetPosition));
        }
    }, "#tryMove");

    // WIDGET 2: Build Widget with Position support and Stack support, but no StdMod support or Position Extras support.

    var Positionable = Y.Base.create("positionable", Y.Widget, [Y.WidgetPosition, Y.WidgetStack]);

    var positionable = new Positionable({
        srcNode: "#widget2",
        width:"10em",
        zIndex:1
    });
    positionable.render("#widget2-example");

    var xy = Y.one("#widget2-example > p").getXY();

    positionable.move(xy[0], xy[1]);

    var xInput = Y.one("#x");
    var yInput = Y.one("#y");

    xInput.set("value", Math.round(xy[0]));
    yInput.set("value", Math.round(xy[1]));

    Y.on("submit", function(e) {
        e.preventDefault();
        
        var x = parseInt(xInput.get("value"));
        var y = parseInt(yInput.get("value"));

        positionable.move(x,y);

    }, "#widget2-example");

    Y.on("click", function(e) {
        try {
            positionable.setStdModContent("header", "new content");
        } catch (e) {
            alert("setStdModContent() is " + typeof positionable.setStdModContent + ", positionable.hasImpl(Y.WidgetStdMod) : " + positionable.hasImpl(Y.WidgetStdMod));
        }
    }, "#tryContent");



    // WIDGET 3: Build Widget with Position, PositionExt and Stack support, but no StdMod support.

    var Alignable = Y.Base.create("alignable", Y.Widget, [Y.WidgetPosition, Y.WidgetPositionAlign, Y.WidgetStack]);

    var alignable = new Alignable({
        width:"14em",
        align : {
            node: "#widget3-example",
            points: ["cc", "cc"]
        },
        zIndex:1
    });
    alignable.get("contentBox").set("innerHTML", '<strong>Alignable Widget</strong><div id="alignment"><p>#widget3-example</p><p>[center, center]</p></div>');
    alignable.render("#widget3-example");

    // Example Alignments Queue
    var stepDelay = 2500;
    var currAlignment = Y.one("#alignment");
    var steps = [
        function() {
            alignable.set("align", {node:"#widget3-example", points:["lc", "rc"]});
            currAlignment.set("innerHTML", "<p>#widget3-example</p><p>[left-center, right-center]</p>");
        },
        function() {
            alignable.set("align", {node:"#widget3-example", points:["tr", "br"]});
            currAlignment.set("innerHTML", "<p>#widget3-example</p><p>[top-right, bottom-right]</p>");
        },
        function() {
            alignable.set("centered", "#widget3-example");
            currAlignment.set("innerHTML", "<p>#widget3-example</p><p>centered</p>");
        },
        function() {
            alignable.set("align", {points:["rc", "rc"]});
            currAlignment.set("innerHTML", "<p>viewport</p><p>[right-center, right-center]</p>");
        },
        function() {
            alignable.set("centered", true);
            currAlignment.set("innerHTML", "<p>viewport</p><p>centered</p>");
        },
        function() {
            alignable.set("align", {node:"#widget3-example", points:["cc", "cc"]});
            currAlignment.set("innerHTML", "<p>#widget3-example</p><p>[center, center]</p>");
        }
    ];

    var runBtn = Y.one("#run"); 
    var label = runBtn.get("text");

    var q = new Y.AsyncQueue();
    q.after("complete", function() {
        runBtn.set("text", label);
    });

    runBtn.on('click', function() {
        if (q.isRunning()) {
            q.stop();
            this.set("text", label);
        } else {
            for (var i = 0; i < steps.length; i++){
                q.add({
                    timeout: (i === 0) ? 0 : stepDelay,
                    fn:steps[i]
                });
            }
    
            q.run();
            this.set("text", "Stop");
        }
    });

});
</script>