Advertisement

Create a Scalable Widget Using YUI3: Part 4

by

Welcome to the last part in the YUI3 widget tutorial; although we’ve actually finished building the widget, we're going to look at how easy it is to add extra functionality to a widget without having to re-write it.

Let's get started right away!

If the functionality is required for a particular module it's an extension. Otherwise, it's a plugin.

There are two ways of adding functionality – extensions and plugins. The difference between them is subtle but essentially boils down to whether or not the functionality is required or optional. If the functionality is required for a particular module it's an extension, if the functionality is optional, it's a plugin.

The plugin that we'll add will handle the paging functionality for our widget; maybe not all developers will want to add paging, or some may want to add it to some instances of the widget but not others. Adding the functionality makes sense when viewed in this way – if the developer wants to make use of paging, they can use the plugin, but we don't force developers to run all the extra code that is required if they aren't going to use it.


Creating a Plugin

The process for creating a plugin is similar to that for creating a widget, so many of the constructs that we'll use here should be familiar from the previous parts of this tutorial. Just like when creating a widget, we use YUI's add() method as a wrapper for our code:

YUI.add("tweet-search-paging", function (Y) {

},
  • The name of the plugin (the name that developers will use to initialize the plugin) is the first argument of the method
  • an anonymous callback function is the second parameter. The function receives a reference to the current YUI instance.
  • the third argument is the version number of the plugin and
  • the fourth is an object listing any dependencies required by the plugin.

The Constructor and Namespace

Just like with our widget, we need to add a constructor for our plugin so that it can initialised and set the namespace for it. Unlike our plugin, setting the namespace is required. Add the following code within the anonymous function we just added:

var Node = Y.Node;

function TweetSearchPaging(config) {
    TweetSearchPaging.superclass.constructor.apply(this, arguments);
}

Y.namespace("Plugin.DW").TweetSearchPaging = TweetSearchPaging;

We start by caching references to any frequently used YUI resources, which in this case is just the Node utility. We add the constructor for the plugin in the same way as before; the TweetSearchPaging plugin method is defined as a function that accepts a configuration object. The class is initialized using the apply() method of the superclass's constructor.

We set a namespace for our plugin, but this time the namespace is attached to the Plugin namespace as opposed to the YUI object.


Static Properties

As before there are some static properties we should set for our plugin, these are as follows:

TweetSearchPaging.NAME = "tweetsearch-paging";

TweetSearchPaging.NS = "paging";

TweetSearchPaging.ATTRS = {
        
    origShowUIValue: null,

    strings: {
        value: {
            nextLink: "Next Page",
            prevLink: "Previous Page"
        }
    }
};

TweetSearchPaging.PAGING_CLASS = Y.ClassNameManager.getClassName(TweetSearchPaging.NAME, "link");

TweetSearchPaging.LINK_TEMPLATE = "<a class={linkclass} href={url}>{linktext}</a>";

The name of the plugin is set with the NAME property, and also the NS property, which can be used to refer to the plugin from the host (the host is the widget or module that the plugin is connected to) class.

We can also use the ATTRS property to set any configuration attributes for the plugin. These attributes also use the YUI3 Attributes module, just like the widget attributes, and can be used in the same way. The attributes we define are the origShowUIValue attribute, which the plugin will set to store whether the search UI was initially displayed in the widget when the plugin is initialized. We also store the text strings used by the plugin, again for easy internationalization.

We manually generate a class name for the elements that we’ll create using the classNameManager, and define the template that our paging links will be created with. As there is only a single class name and template we don't need to worry about using a for loop.


Extending the Plugin Base Class

Just like we did when creating the class for our widget, we use YUI’s extend() method to extend an underlying module. In the case of a plugin, it's the Plugin.Base class that we're extending. The extend() method should appear as follows:

Y.extend(TweetSearchPaging, Y.Plugin.Base, {

});

We pass in our plugin as the first argument to the extend() method, the class we're extending as the second method and an object literal containing the functionality we're adding.


Life Cycle Methods

Plugins also get access to several life-cycle methods that can be overridden to add custom code that the plugin will execute for us at appropriate times. We can make use of use the initializer and destructor life-cycle methods:

initializer: function () {

    Y.StyleSheet("tweetSearchPagingBase").set(".yui3-tweetsearch-paging-link", { float: "right" });

    if (Y.one(".yui3-skin-sam")) {
        Y.StyleSheet("tweetSearchPagingSkin").set(".yui3-skin-sam .yui3-tweetsearch-paging-link", { marginLeft: "2%" });
    }

    var widget = this.get("host");

    if (!widget.get("showUI")) {
        this.set("_origShowUIValue", false);
        widget.set("showUI", true);
    } else {
        this.set("_origShowUIValue", true);
    }

    this.afterHostEvent("tweetsChange", this._afterHostTweetsChange);
},

destructor: function () {
    Y.StyleSheet("tweetSearchPagingBase").unset(".yui3-tweetsearch-paging-link", "float");

    if (Y.one(".yui3-skin-sam")) {
        Y.StyleSheet("tweetSearchPagingSkin").unset(".yui3-skin-sam .yui3-tweetsearch-paging-link", "marginLeft");
    }

    if (!this.get("_origShowUIValue")) {
        this.get("host").set("showUI", false);
        Y.one(".yui3-tweetsearch-ui").remove();
    }
},

The initializer method will be executed when the plugin is initialized; in this method we first create the base style sheet our plugin needs. We could just include a separate CSS file, but as we only need a single style rule it makes sense to cut down on the number of files that any implementing developer needs to manage.

We use YUI's StyleSheet() method to create our new style sheet. This method accepts a single argument which is the name of the new style sheet. We then use the set() method to set the styles that we require. The set() method takes two arguments; the first is the selector we wish to target and the second is an object literal containing the styles that should be applied to the selector, which in this case is simply float: right.

We then check whether the .yui3-sam-skin selector exists in the document; if it does, we then go ahead and create a skin style sheet for the plugin. If the sam skin is not in use, it is not worth creating any skin styles as the implementing developer will no doubt have custom styles that they may wish to apply.

Next, we need to check whether the showUI attribute of the widget is enabled. We can get access to the host class that the plugin is attached to using the built-in host attribute, which we get using the get() method just like any other attribute. The showUI attribute of the widget must be enabled if the plugin is used, so if the attribute is not set originally we set it here.

When using plugins we have the ability to detect and react to any of the host's attributes changing. We add an attribute change-handler for the when the tweets attribute of our widget changes using the afterHostEvent() method. This method accepts two arguments; the first is the attribute to monitor, the second is the method to execute when the attribute changes.

The destructor function is called when the plugin is destroyed; this method is used to tidy up after the plugin. Any changes to the page should be reversed, as well as any changes we make to the widget. The changes we make to the page that we have to undo are the addition of the style sheets, so this is what we do first. The style sheet styles can be removed using the unset() method; this method takes the selector to unset as the first argument and the styles to unset as the second argument.

We then check whether the _origShowUiValue variable is set to true or false; if the variable is set to false we know we have to revert its value, so we set the attribute of the host back to false. If the value was changed and the UI was shown by the plugin, we hide it so that the widget is returned to its original state.


Attribute Change-Handlers

We only use a single attribute change-handling method in this plugin; the one that is called when the tweet attribute of the host changes. This method should appear as follows:

_afterHostTweetsChange: function () {

    var widget = this.get("host");

    if (widget.get("tweets").next_page) {
        var nextPageUrl = widget.get("tweets").next_page,
            nextLink = Node.create(Y.substitute(TweetSearchPaging.LINK_TEMPLATE, {
            linkclass: TweetSearchPaging.PAGING_CLASS, url: ["http://search.twitter.com/search.json", nextPageUrl, "&callback={callback}"].join(""), linktext: this.get("strings").nextLink }));

        if (this._nextLinkNode) {
            this._nextLinkNode.remove();
        }

        this._nextLinkNode = widget._uiNode.appendChild(nextLink);

        Y.on("click", Y.bind(this._getPage, this), this._nextLinkNode);
    }

    if (widget.get("tweets").previous_page) {
        var prevPageUrl = widget.get("tweets").previous_page,
            prevLink = Node.create(Y.substitute(TweetSearchPaging.LINK_TEMPLATE, { 
            linkclass: TweetSearchPaging.PAGING_CLASS, url: ["http://search.twitter.com/search.json", prevPageUrl, "&callback={callback}"].join(""), linktext: this.get("strings").prevLink }));

        if (this._prevLinkNode) {
            this._prevLinkNode.remove();
        }
        this._prevLinkNode = widget._uiNode.appendChild(prevLink);
        Y.on("click", Y.bind(this._getPage, this), this._prevLinkNode);
    }
},

We first store a reference to the host class once more as we'll need to refer to it several times. We now need to determine whether or not there are paged results in the response from Twitter and whether there are previous or next pages of results. The cool thing about the response from twitter is that it will automatically maintain which page of results we are viewing if there are more results than the configured number of results per page.

If there is another page of results after the current page, there will be a property in the JSON response object called next_page. Similarly, if there is a previous page of results, there will be a previous_page property. All we need to do is check for the presence of these properties and create next page and previous page links.

The links are created using the template we stored earlier in the plugin class, and are given the generated CLASS_NAME. The next_page and previous_page response objects are obtained from Twitter using a URL with a special ID in the query string. When we create these new nodes, the URL provided in these properties is added to each link respectively. The links are appended to the searchUI node of the host, and click handlers are added for them. These click handlers point to a utility method called _getPage(). We'll add this method next.


Custom Prototype Methods

Just like when creating the widget, we can add any number of custom prototype methods that are used to execute any custom code required by our plugin in response to user interaction or state changes. In this plugin, we only need to add a single method, which should appear as follows:

_getPage: function (e) {
    var widget = this.get("host");

    e.preventDefault();

    widget._viewerNode.empty().hide();
    widget._loadingNode.show();

    widget.set("baseURL", e.target.get("href")),

    widget._retrieveTweets();

    Y.all(".yui3-tweetsearch-paging-link").remove();
}

First, we store a reference to the host class, and then prevent the paging link that was clicked being followed. We then remove any existing tweets in the widget's viewer and show the loading node. Remember, each paging link (or whichever link exists if we are on the first or last page) will have the URL that retrieves the next (or previous) page of results, so we retrieve this URL from the link's href and set the baseURL attribute of the widget. One this is done, we call the _retrieveTweets() method of our widget to request the next page. Finally, we remove the current paging links as they will be recreated if there are next or previous pages included in the new response object.


Using the Plugin

Now that we’ve created our plugin we can see how easy it is to use with our widget. We need to update our use() method to use our plugin, and call the plug() method before the widget is rendered:

YUI().use("tweet-search", "tweet-search-paging", function (Y) {
    var myTweetSearch = new Y.DW.TweetSearch({
        srcNode: "#ts"
    });
    myTweetSearch.plug(Y.Plugin.DW.TweetSearchPaging);
    myTweetSearch.render();
});

The plug() method connects our plugin, which is accessible via the Plugin namespace and whatever namespace we specified when defining the plugin's class. Now when we run the page, we should have paging links at the bottom of the widget:

Nettuts+

One of the features of our plugin (just like our widget) is easy internationalization; in order to provide strings for the plugin in another language (or override any attributes if a plugin), we can simply provide the configuration object as the second argument to the plug() method, e.g.:

myTweetSearch.plug(Y.Plugin.DW.TweetSearchPaging, {
    strings: {
        nextLink: "Página Siguiente",
        prevLink: "Página Anterior"
    }
});

The paging link should now appear like so:

Nettuts+

Wrapping Up

In this part of the tutorial, we looked at how easy it is to create a plugin that can be used to enhance existing widgets or other modules. This is a great way of providing extra functionality that is not essential, which developers can choose to include if they wish. We saw that the structure of a plugin is similar to that of a widget on a smaller scale.

In this example, the plugin was very tightly coupled to our widget; it wouldn't be possible to use the plugin with a different widget for example. This does not have to be the case and plugins, as well as extensions can be much more loosely coupled to add or enhance functionality for a range of different modules.

This now brings us to the end of the series on YUI3 widgets; I hope I’ve given some insight into the powerful mechanisms put in place by the library that enable us to easily create scalable and robust widgets that leverage the strengths of the library.

Let us know what you think in the comments section below and thank you so much for reading!

Advertisement