In the last part of this series, we looked at the life-cycle methods, automatic methods and the custom methods that our widget requires or can make use of. In this part, we're going to finish defining the widget's class by adding the attribute change-handling methods that we attached in the bindUI()
life-cycle method.
Let's get started right away!
Attribute Change Handlers
The attribute change-handling group of methods are called when some of our attributes change values. We'll start by adding the method that is called when the showTitle
attribute changes; add the following code directly after the _uiSetTitle()
method:
_afterShowTitleChange: function () { var contentBox = this.get("contentBox"), title = contentBox.one(".yui3-tweetsearch-title"); if (title) { title.remove(); this._titleNode = null; } else { this._createTitle(); } },
We first get a reference to the contentBox
, and then use this to select the title node. Remember this is the container in which reside the title and subtitle in the header of the widget.
If the title node already exists, we remove it using YUI's remove()
method. We also set the _titleNode
of the widget to null. If the node doesn't exist, we simple call the _createTitle()
method of our widget to generate and display it.
Next we can handle the showUI
attribute changing:
_afterShowUIChange: function () { var contentBox = this.get("contentBox"), ui = contentBox.one(".yui3-tweetsearch-ui"); if (ui) { ui.remove(); this._uiNode = null; } else { this._createSearchUI(); } },
This method is almost identical to the last one -- all that changes is that we are looking for the change of a different attribute, and either removing or creating a different group of elements. Again, we set the _uiNode
property of our widget to null
, so that the widget is aware of the latest state of its UI.
Our next method is called after the term
attribute changes:
_afterTermChange: function () { this._viewerNode.empty().hide(); this._loadingNode.show(); this._retrieveTweets(); if (this._titleNode) { this._uiSetTitle(this.get("term")); } },
When the term
attribute changes, we first remove any previous search results from the viewer by calling YUI's (specifically the Node module's) empty()
method followed by the hide()
method. We also show our loader node for some visual feedback that something is happening.
We then call our _retrieveTweets()
method to initiate a new request to Twitter's search API. This will trigger a cascade of additional methods to be called, that result ultimately in the viewer being updated with a new set of tweets. Finally, we check whether the widget currently has a _titleNode
, and if so we call the _uiSetTitle()
method in order to update the subtitle with the new search term.
Our last attribute change-handler is by far the largest and deals with the tweets
attribute changes, which will occur as a result of the request to Twitter being made:
_afterTweetsChange: function () { var x, results = this.get("tweets").results, not = this.get("numberOfTweets"), limit = (not > results.length - 1) ? results.length : not; if (results.length) { for (x = 0; x < limit; x++) { var tweet = results[x], text = this._formatTweet(tweet.text), tweetNode = Node.create(Y.substitute(TweetSearch.TWEET_TEMPLATE, { userurl: "http://twitter.com/" + tweet.from_user, avatar: tweet.profile_image_url, username: tweet.from_user, text: text })); if (this.get("showUI") === false && x === limit - 1) { tweetNode.addClass("last"); } this._viewerNode.appendChild(tweetNode); } this._loadingNode.hide(); this._viewerNode.show(); } else { var errorNode = Node.create(Y.substitute(TweetSearch.ERROR_TEMPLATE, { errorclass: TweetSearch.ERROR_CLASS, message: this.get("strings").errorMsg })); this._viewerNode.appendChild(errorNode); this._loadingNode.hide(); this._viewerNode.show(); } },
First up, we set the variables we'll need within the method including a counter variable for use in the for loop
, the results
array from the response that is stored in the tweets
attribute, the value of the numberOfTweets
attribute and the limit
, which is either the number of results in the results
array, or the configured number of tweets if there are fewer items in the array than the number of tweets.
The remaining code for this method is encased within an if
conditional which checks to see if there are actually results, which may not be the case if there were no tweets containing the search term. If there are results in the array, we iterate over each of them using a for loop
. On each iteration, we get the current tweet and pass it to a _formatTweet()
utility method that will add any links, usernames or hash tags found within the text, and then create a new node for the tweet using the same principles that we looked at in the last part of this tutorial.
When the searchUI
is not visible, we should alter the styling of the widget slightly to prevent a double border at the bottom of the widget. We check whether the showUI
attribute is set to false
, and is the last tweet being processed, and if so add the class name last
to the tweet using YUI's addClass()
method. We then add the newly created node to the viewer node to display it in the widget.
After the for
loop has completed, we hide the loading node, which will at this point be visible having already been displayed earlier on, and then show the viewer node.
If the results
array does not have a length, it means that the search did not return any results. In this case, we create an error node to display to the user and append it to the viewer node, then hide the loading node and show the viewer node as before.
A Final Utility Method
We've added all of the methods that support changing attribute values. At this point, we have just one further method to add; the _formatTweet()
method that we reference from the within the for loop
of the method we just added. This method is as follows:
_formatTweet: function (text) { var linkExpr = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig, atExpr = /(@[\w]+)/g, hashExpr = /[#]+[A-Za-z0-9-_]+/g, string = text.replace(linkExpr, function (match) { return match.link(match); }); string = string.replace(atExpr, function (match) { return match.link("http://twitter.com/" + match.substring(1)); }); string = string.replace(hashExpr, function (match) { return match.link("http://twitter.com/search?q=" + encodeURI(match)); }); return string; }
This method accepts a single argument, which is the text from the 'current' item of the results
array that we want to linkify/atify/hashify. We start by defining three regular expressions, the first will match any links within the text that start with http, https or ftp and contain any characters that are allowed within URLs. The second will match any Twitter usernames (any strings that start with the @ symbol), and the last will match any strings that start with the # symbol.
We then set a variable called string which is used to contain the transformed text. First, we add the links. JavaScript's replace()
function accepts the regular expression for matching links as the first argument and a function as the second argument -- the function will be executed each time a match is found and is passed the matching text as an argument. The function then returns the match having converted it to a link element using JavaScript's link()
function. This function accepts a URL that is used for the href
of the resulting link. The matching text is used for the href
.
We then use the replace()
function on the string once again, but this time we pass in the @ matching regular expression as the first argument. This function works in the same way as before, but also adds Twitter's URL to the start of the href
that is used to wrap the matching text. The string variable is then operated on in the same way to match and convert any hashed words, but this time Twitter's search API URL is used to create the link(s). After the text has been operated on, we return the resulting string.
This brings us to the end of our widget's class; at this point we should have an almost fully functioning widget (we haven't yet added the paging, this will be the subject of the next and final instalment in this series). We should be able to run the page and get results:
Styling the Widget
We should provide at least 2 style sheets for our widget; a base style sheet that contains the basic styles that the widget requires in order to display correctly, and a theme style sheet that controls how the widget appears visually. We'll look at the base style sheet first; add the following code to a new file:
.yui3-tweetsearch-title { padding:1%; } .yui3-tweetsearch-title h1, .yui3-tweetsearch-title h2 { margin:0; float:left; } .yui3-tweetsearch-title h1 { padding-left:60px; margin-right:1%; background:url(/img/logo.png) no-repeat 0 50%; } .yui3-tweetsearch-title h2 { padding-top:5px; float:right; font-size:100%; } .yui3-tweetsearch-content { margin:1%; } .yui3-tweetsearch-viewer article, .yui3-tweetsearch-ui { padding:1%; } .yui3-tweetsearch-viewer img { width:48px; height:48px; margin-right:1%; float:left; } .yui3-tweetsearch-viewer h1 { margin:0; } .yui3-tweetsearch-label { margin-right:1%; } .yui3-tweetsearch-input { padding:0 0 .3%; margin-right:.5%; } .yui3-tweetsearch-title:after, .yui3-tweetsearch-viewer article:after, .yui3-tweetsearch-ui:after { content:""; display:block; height:0; visibility:hidden; clear:both; }
Save this style sheet as tweet-search-base.css
in the css
folder. As you can see, we target all of the elements within the widget using the class names we generated in part one. There may be multiple instances of the widget on a single page and we don't want our styles to affect any other elements on the page outside of our widget, so using class names in this way is really the only reliable solution.
The styling has been kept as light as possible, using only the barest necessary styles. The widget has no fixed width and uses percentages for things like padding and margins so that it can be put into any sized container by the implementing developer.
Next, we can add the skin file; add the following code in another new file:
.yui3-skin-sam .yui3-tweetsearch-content { border:1px solid #A3A3A3; border-radius:7px; } .yui3-skin-sam .yui3-tweetsearch-title { border-bottom:1px solid #A3A3A3; border-top:1px solid #fff; background-color:#EDF5FF; } .yui3-skin-sam .yui3-tweetsearch-title span { color:#EB8C28; } .yui3-skin-sam .yui3-tweetsearch-loader, .yui3-skin-sam .yui3-tweetsearch-error { padding-top:9%; margin:2% 0; color:#EB8C28; font-weight:bold; text-align:center; background:url(/img/ajax-loader.gif) no-repeat 50% 0; } .yui3-skin-sam .yui3-tweetsearch-error { background-image:url(/img/error.png); } .yui3-skin-sam .yui3-tweetsearch article { border-bottom:1px solid #A3A3A3; border-top:2px solid #fff; background:#f9f9f9; background:-moz-linear-gradient(top, #f9f9f9 0%, #f3f3f3 100%, #ffffff 100%); background:-webkit-gradient(linear, left top, left bottom, color-stop(0%,#f9f9f9), color-stop(100%,#f3f3f3), color-stop(100%,#ffffff)); background:-webkit-linear-gradient(top, #f9f9f9 0%,#f3f3f3 100%,#ffffff 100%); background:-o-linear-gradient(top, #f9f9f9 0%,#f3f3f3 100%,#ffffff 100%); background:-ms-linear-gradient(top, #f9f9f9 0%,#f3f3f3 100%,#ffffff 100%); background:linear-gradient(top, #f9f9f9 0%,#f3f3f3 100%,#ffffff 100%); filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f9f9f9', endColorstr='#ffffff',GradientType=0); } .yui3-skin-sam .yui3-tweetsearch article.last { border-bottom:none; } .yui3-skin-sam .yui3-tweetsearch a { color:#356DE4; } .yui3-skin-sam .yui3-tweetsearch a:hover { color:#EB8C28; } .yui3-skin-sam .yui3-tweetsearch-ui { border-top:1px solid #fff; background-color:#EDF5FF; }
Save this file as tweet-search-skin.css
in the css
folder. Although we also use our generated class names here, each rule is prefixed with the yui3-skin-sam
class name so that the rules are only applied when the default Sam theme is in use. This makes it very easy for the overall look of the widget to be changed. This does mean however that the implementing developer will need to add the yui3-skin-sam
class name to an element on the page, usually the , but this is likely to be in use already if other modules of the library are being used.
Like before, we've added quite light styling, although we do have a little more freedom of expression with a skin file, hence the subtle niceties such as the rounded-corners and css-gradients. We should also recommended that the css-reset, css-fonts and css-base YUI style sheets are also used when implementing our widget, as doing so is part of the reason the custom style sheets used by the widget are nice and small.
Implementing the Widget
Our work as widget builders is complete (for now), but we should spend a little while looking at how the widget is actually used. Create the following HTML page in your text editor:
<!DOCTYPE html> <html lang="en"> <head> <title>YUI3 Twitter Search Client</title> <link rel="stylesheet" href="http://yui.yahooapis.com/combo?3.4.1/build/cssreset/cssreset-min.css&3.4.1/build/cssfonts/cssfonts-min.css&3.4.1/build/cssbase/cssbase-min.css"> <link rel="stylesheet" href="css/tweet-search-base.css" /> <link rel="stylesheet" href="css/tweet-search-skin.css" /> </head> <body class="yui3-skin-sam"> <div id="ts"></div> <script src="//yui.yahooapis.com/3.4.1/build/yui/yui-min.js"></script> <script src="js/tweet-search.js"></script> <script> YUI().use("tweet-search", function (Y) { var myTweetSearch = new Y.DW.TweetSearch({ srcNode: "#ts" }); myTweetSearch.render(); }); </script> </body> </html>
The only YUI script file we need to link to is the YUI seed file which sets up the YUI global object and loads the required modules.
Save this file in the root project directory. First of all we link to the CDN hosted YUI reset, base and fonts combined style sheet, as well as our two custom style sheets that we just created. We also add the yui3-skin-sam class name to the of the page to pick up the theme styling for our widget. On the page, we add a container for our widget and give it an
id
attribute for easy selecting.
The only YUI script file we need to link to is the YUI seed file; this file sets up the YUI global object and contains the YUI loader which dynamically loads the modules required by the page. We also link to our plugin's script file, of course.
Within the final script element we instantiate the YUI global object and call the use()
method specifying our widget's name (not the static NAME
used internally by our widget, but the name specified in the add()
method of our widget's class wrapper) as the first argument.
Each YUI instance is a self-contained sandbox in which only the named modules are accessible.
The second argument is an anonymous function in which the initialisation code for our widget is added. This function accepts a single argument which refers to the current YUI instance. We can use any number of YUI objects on the page, each with its own modules. Each YUI instance is a self-contained sandbox in which only the named modules (and their dependencies) are accessible. This means we can have any number of self-contained blocks of code, all independent from each other on the same page.
Within the callback function, we create a new instance of our widget stored in a variable. Our widget's constructor is available via the namespace we specified in the widget's class, which is attached to the YUI instance as a property. Our widget's constructor accepts a configuration object as an argument; we use this to specify the container that we want to render our widget into, in this case the empty <div>
we added to the page. The specified element will become the contentBox
of our widget. Finally, we call the render()
method on the variable our widget instance is stored in, which renders the HTML for our widget into the specified container.
In the configuration object, we can override any of the default attributes of our widget, so if we wanted to disable the title of the widget and the search UI, we could pass the following configuration object into our widget's constructor:
{ srcNode: "#ts", showTitle: false, showUI: false }
I mentioned in an earlier part of the widget that by including all of the text strings used by the widget in an attribute, we could easily enable extremely easy internationalization. To render the widget in Spanish, for example, all we need to do is override the strings attribute, like this:
{ srcNode: "#ts", strings: { title: "Twitter Search Widget", subTitle: "Mostrando resultados de:", label: "Término de búsqueda", button: "Búsqueda", errorMsg: "Lo siento, ese término de búsqueda no ha obtenido ningún resultado. Por favor, intente un término diferente" } }
Now when we run the widget, all of the visible text (aside from the tweets of course) for the widget is in Spanish:
Summary
In this part of the tutorial, we completed our widget by adding the attribute change-handling methods and a small utility method for formatting the flat text of each tweet into mark-up. We also looked at the styling required by our widget and how the styles should be categorized, i.e. whether they are base styles or skin styles.
We also saw how easy it is to initialise and configure the widget and how it can easily be converted into display in another language. In the next part of this tutorial, we'll look at a close relative to the widget – the plugin and add a paging feature to our widget.
Comments