In part one of this series, we reviewed some of the necessary constructs to use when creating a widget with YUI3. We looked at the static properties we needed to set, the class constructor and namespacing, and briefly looked at the extend()
method.
In this part of the tutorial, we'll review the prototype methods we can override or create in order to make our widget function.
Before we begin, let’s just remind ourselves of the method now, as this method houses all the code below:
TweetSearch = Y.extend(TweetSearch, Y.Widget, { });
The third argument is what we are interested in, in this part of the tutorial. All of the functionality we add that is specific to our widget will be within functions that are added as values to different properties of the object passed to the extend()
method. Some of these methods are added automatically for us --we just need to override them with custom functionality. We'll look at these methods first.
Lifecycle Methods
Several methods executed at different points in the widget instances life cycle. The first of these is an initializer
method (remember to add this code within the extend()
method shown above):
initializer: function () { this._retrieveTweets(); },
The underscore convention to indicate the method should be treated as private, and not called directly by any implementing developer.
The initializer method is provided to allow us to do any tasks that are required as soon as the widget is initialized. Within any prototype methods we attach to our widget, whether inherited or created ourselves, the value of this is set to the widget instance.
All our widget needs to do at this point is retrieve the search results from Twitter. We package this up as a separate function (which we'll look at in more detail a little later), instead of just retrieving the results directly within initializer
so that we can reuse the functionality and retrieve search results any time we wish. The _retrieveTweets()
method uses the underscore convention to indicate the method should be treated as private, and not called directly by any implementing developer. It can be called directly of course, but may result in weirdness.
The next life-cycle method inherited from Widget is renderUI()
, which we can use to perform any necessary setup, the creation and insertion of new elements, etc, our widget requires. Add this code directly after that shown above:
renderUI: function () { var contentBox = this.get("contentBox"), strings = this.get("strings"), viewer = Node.create(Y.substitute(TweetSearch.VIEWER_TEMPLATE, { viewerclass: TweetSearch.VIEWER_CLASS })), loadingNode = Node.create(Y.substitute(TweetSearch.LOADER_TEMPLATE, { loaderclass: TweetSearch.LOADER_CLASS })); if (this.get("showTitle")) { this._createTitle(); } this._loadingNode = contentBox.appendChild(loadingNode); this._viewerNode = contentBox.appendChild(viewer); if (this.get("showUI")) { this._createSearchUI(); } contentBox.addClass("yui3-widget-content"); },
When a widget is initialised, YUI will automatically create a wrapper element for the element that was passed to the constructor.
Within the renderUI()
method, we first store a reference to the contentBox
attribute of the widget. The contentBox
represents the inner container of the widget and is one of the attributes automatically inherited from Widget, like the srcNode
attribute that we saw briefly in part 1. When a widget is initialised, YUI will automatically create a wrapper element for the element that was passed to the constructor, with the inner element becoming the contentBox
. The wrapper is known as the bounding box (available as the boundingBox
attribute).
We also get a reference to the strings
attribute that contains the localizable strings used by elements created by the widget. We then create two new elements; the viewer which will be used to contain the list of tweets returned by Twitter's search API, and a loading element that will be displayed while the request is in progress.
We use the create()
method of the YUI Node module to create our new elements. This element can accept the string representation of an element, which it will then create. Instead of passing it a string directly however, we use YUI's substitute()
method to replace the tokenised templates that we created in part one of this tutorial.
The substitute()
method takes two arguments;
- the first is the string to perform substitution on.
- the second is an object whose keys map directly to the tokens within the string.
The values of each property are swapped into the string, so for example, our viewer template will be stored like this:
"<div class={viewerclass}></div>"
The object passed as the second argument to the substitute()
method used to create the viewer node contains a key called viewerclass
, so the value of this key will be swapped with the matching token in the source string. In this case, we use the stored class name as the substitution, so the viewer will be given the class name yui3-tweetsearch-viewer
(the class names were all created and stored on our widget instance in part one).
We then check whether the showTitle
attribute of our widget is set to true
, which it is by default, but may be disabled by the implementing developer. If the attribute is set to true
we call the custom (i.e. not inherited) _createTitle()
method. The reason we package this up as a separate unit of code, instead of just creating the widget is because the showTitle
attribute may be set at any time by someone implementing our widget, so it can't just reside within a life-cycle method. We will look at our custom methods in detail after looking at the inherited life-cycle methods.
After we do or do not (depending on configuration) create the title node, we then insert the new elements into the DOM by adding them as child nodes of the contentBox
. Note that we also store the new elements on the widget instance so that we can easily refer to them later on.
We then check whether the showUI
attribute is enabled (again, it is by default, but it could be changed in the configuration), and if so call the _createSearchUI()
method. This is a separate method for the same reason as last time – so that it can be reused throughout the widget instance's life.
Finally, we add the class name yui3-widget-content
to the contentBox
. This isn't strictly necessary, as the implementing developer may not be using any of the YUI’s style sheets (base, fonts, reset, etc), but as the class name isn’t added for us automatically, we should include in case the developer does wish to pick up some of the styling provided by the library.
The final life-cycle method we are going to use is bindUI()
, which allows us to hook up any handlers that should be called when an attribute changes value, or an event occurs. Add the following code directly after the renderUI()
method:
bindUI: function () { if (this.get("showUI")) { Y.on("click", Y.bind(this._setTerm, this), this._buttonNode); this.after("termChange", this._afterTermChange); } this.after("showTitleChange", this._afterShowTitleChange); this.after("showUIChange", this._afterShowUIChange); this.after("tweetsChange", this._afterTweetsChange); },
The first thing we do is check whether the showUI
attribute is enabled; if it has been disabled we don't need to worry about adding event handlers for it. If it is enabled, we use YUI's on()
method to add a click-handler bound to the custom _setTerm()
method. We ensure the widget instance remains bound to the this keyword within the event handler by passing this (which at this point refers to the widget instance) as the second argument to the bind()
method.
We also use the after()
method that is automatically attached to our widget instance by the library to add a listener that reacts to the term
attribute changing. A listener can be bound to any of our custom attributes by simply suffixing After
to any attribute name. The term
attribute will only change if the search UI is enabled. We then add listeners for each of the other attributes we need to monitor; showTitle
, showUI
and tweets
, hooking these up with the relevant handlers.
Note: There is another life cycle method provided by the Widget class, but in this particular example we don't need to make use of it. This method is the destructor
, which will be called just before the widget is destroyed. It is used to tidy up after the widget, but only needs to be used if elements are added to the DOM outside of the boundingBox
(the outer wrapper) of the widget.
Automated Prototype Methods
Remember the validator we specified as part of the ATTRS
object in the first part of this tutorial? The method that we set as the value of this property will be called automatically whenever an attempt is made to update the attribute. Let’s take a look at it now; add the following code directly after bindUI()
:
_validateTerm: function (val) { return val !== this.get("term"); },
The method must return true
or false
and automatically receives the new value (that is, the value that may become the new value if it passes validation) as the first argument; if true
is returned, the attribute is updated with the new value, if false
is returned the attribute is not updated.
The logic we supply is pretty simple in this example – we simply check that the new value is not the same as the old value. There’s no point after all in making another AJAX call only to receive exactly the same set of results.
Non-Inherited Prototype Methods
Next we can start adding our custom methods that will add more functionality to our widget. The first function we referenced within the initializer
method was _retrieveTweets()
, so we'll look at that first:
_retrieveTweets: function () { var that = this, url = [this.get("baseURL"), "&q=", encodeURI(this.get("term")), "&rpp=", this.get("numberOfTweets")].join(""), handler = function (data) { that.set("tweets", data); }, request = new Y.JSONPRequest(url, handler); request.send(); },
We first set a few variables; the this
keyword will no longer point to our widget instance inside the success callback that we'll specify when we make the request to Twitter, so we store a reference to this
in a variable called that
, as convention dictates.
We also create the request URL; we retrieve the baseURL
, the term
and the numberOfTweets
attributes, storing each as an item in an array and then using JavaScript's join()
function to concatenate them all into a string. Using an array and the join()
method is way faster than concatenating strings with the +
operator.
Next we define our success callback; all this simple function needs to do is set the widget's tweets
attribute to the response received from the request. The response will be automatically passed to the callback function.
The last variable we define is for the request itself, which is initialised using YUI's JSONPRequest()
method. This method accepts two arguments; the first is the URL to make the request to and the second is the callback function to invoke on success. Finally, to initiate the request we simply call the send()
method.
Our next custom method is _createTitle()
, which we call from the renderUI()
method:
_createTitle: function () { var strings = this.get("strings"), titleNode = Node.create(Y.substitute(TweetSearch.TITLE_TEMPLATE, { titleclass: TweetSearch.TITLE_CLASS, title: strings.title, subtitle: strings.subTitle, term: this.get("term") })); this._titleNode = this.get("contentBox").prepend(titleNode); },
We also store a reference to the strings
attribute for use within the function. A title is created using the same principles as before, although this time we have a few more tokens to replace in our substitute()
method. This method is only called if the showTitle
attribute is set to true
. Note that the get()
method is chainable, so we can call the prepend()
method to insert the title directly after it.
The code here is very similar to what has been used before, as is the case for our next method, _createSearchUI()
:
_createSearchUI: function () { var contentBox = this.get("contentBox"), strings = this.get("strings"), ui = Node.create(Y.substitute(TweetSearch.UI_TEMPLATE, { uiclass: TweetSearch.UI_CLASS })), label = Node.create(Y.substitute(TweetSearch.LABEL_TEMPLATE, { labelclass: TweetSearch.LABEL_CLASS, labeltext: strings.label })), input = Node.create(Y.substitute(TweetSearch.INPUT_TEMPLATE, { inputclass: TweetSearch.INPUT_CLASS })), button = Node.create(Y.substitute(TweetSearch.BUTTON_TEMPLATE, { buttonclass: TweetSearch.BUTTON_CLASS, buttontext: strings.button })); this._uiNode = ui; this._labelNode = this._uiNode.appendChild(label); this._inputNode = this._uiNode.appendChild(input); this._buttonNode = this._uiNode.appendChild(button); this._uiNode.appendTo(contentBox); },
Again, very similar to what we have seen before. Remember, the only reason this is in a separate function is so that the UI can be switched on or off at any point during the widget's life-cycle. This method is only called if the showUI
attribute is set to true
.
Next up is the _setTerm()
method, which is called by the event listener attached to the _buttonNode
when the button is clicked:
_setTerm: function () { this.set("term", this._inputNode.get("value")); },
In this simple method, we just try to set the term
attribute to the string entered into the <input>
. In trying to set the attribute, our validator will be called and will only update the attribute if the value is different to the attribute's current value.
The last of our custom methods is another simple method used to update the subtitle in the header of the widget to the new search term; add the following code:
_uiSetTitle: function (val) { this._titleNode.one("h2 span").setContent(val); },
This method will receive the new value as an argument (we'll call this method manually from an attribute change-handling method that we'll look at in the next part of this series). We call YUI's one()
method on our title node to select the <span>
within the subtitle, and then use the setContent()
method to update its inner-text.
Summary
In this part of the tutorial, we first looked at the life-cycle methods that we get as a result of extending the Widget superclass. These methods are called automatically for us by the library at different points in the widget's life-cycle.
Although the methods that we've added all look similar in structure, there are distinctions between the two; for example, the life-cycle methods receive more 'protection' than those methods we add ourselves, hence why these methods are not prefixed with an underscore. These methods, unlike our custom ones, can't be called directly by the implementing developer.
We also took a look at a validator method; these methods will also be called by the library automatically when appropriate which makes them incredibly useful for ensuring data is in a particular format, or meets a particular requirement before an attribute is updated.
Lastly, we looked at the custom prototype methods that we need in order to make our widget function. We saw that we can easily use the built-in get()
and set()
methods to get and set attributes, and that within each method the this
keyword Is helpfully set to our widget's instance, so that we can easily obtain and manipulate different aspects of the widget.
In the next part of this tutorial, we'll look at the attribute change-handling methods that need to be added in order to make our widget respond to user interaction or changes in the page's state. We can also look at the CSS we need to provide for our widget, and how the widget is initialized and used.
If you have any questions, please let me know in the comments section below. Thank you so much for reading!
Comments