Export (0) Print
Expand All

Chapter 14: Widget QuickStart

This Widget QuickStart illustrates the way Project Silk uses the jQuery UI Widget Factory to create maintainable widgets that implement client-side behavior.

Business Scenario

Our team has been asked to enable cross-browser keyword lookup capabilities in our web pages by hyperlinking select keywords to popular websites. This feature will need to be added dynamically to all company web pages.

Another team has been tasked with tagging the keywords in the web pages. The words will be tagged dynamically, based on server-side business logic driven by agreements with third parties.

The focus of this QuickStart is to enable the client-side behavior for the tagged keywords. When a user hovers over a keyword, the browser will display a pop-up list of popular links for that keyword from the Delicious.com bookmarking service.

Walkthrough

Project Silk includes the source code for the Widget QuickStart. To run the QuickStart, ensure you have an Internet connection and follow the steps below:

  1. If you have not already installed Project Silk, download it from the Microsoft Download Center. To extract the download, run the .exe file. This will extract the source code and documentation into the folder of your choice.
  2. Open the default.htm file from the {silk}\QuickStarts\DeliciousWidgetQuickStart folder using Windows® Internet Explorer® 9. After the file's content is displayed, you'll need to click on the Allow blocked content button at the bottom of the browser window to enable scripts to run. Blocking active content by default is a security feature of Internet Explorer 9.

    Widget QuickStart (default.htm)

    Hh404102.3eaabace-580f-4878-85e6-820972b474c4(en-us,PandP.10).png

  3. After allowing blocked content, you'll notice that the keywords are displayed in a new color and have been underlined with a dashed line, as pictured below.

    Widget QuickStart after scripts are unblocked

    Hh404102.b4e647c0-1a6e-4f26-914f-a0260977c1c0(en-us,PandP.10).png

  4. Using your mouse, hover over an underlined keyword. A pop-up list with the ten most popular links for that keyword will be displayed. Notice that the keyword has been repeated in the title of the pop-up list.
    1. One second after moving your mouse away from the keyword, the pop-up list will close unless your mouse is within the boundaries of the pop-up list.
    2. If the keyword is on the left side of the page, the pop-up list will open to the right of the cursor. If the keyword is on the right side of the page, the pop-up list will open to the left of the cursor, as in the image below.

      Pop-up list for the keyword "jQuery"

      Hh404102.54e4ee60-1daa-4b12-9e51-ac83db01d76f(en-us,PandP.10).png

  5. Move your mouse over the pop-up list. You can now click on a link, which will open in a new browser window.

    Links from Delicious.com in the pop-up list

    Hh404102.c6d36b0c-28b1-4e3b-80c1-3d886a3c6ec3(en-us,PandP.10).png

  6. Moving your mouse outside the boundaries of the pop-up list will cause the pop-up list to close.

Conceptual View

This section illustrates the relationship of the jQuery UI widgets to the HTML page. A single infobox widget is attached to the page's body element. After it's attached, it creates a <div> element and dynamically adds it to the page's <body> element. Additionally, a tagger widget is attached to each keyword.

Relationship of the jQuery UI widgets to the HTML page

Hh404102.fd6a60bd-5627-44e0-9a72-2163ecd13734(en-us,PandP.10).png

The HTML below reveals a keyword tagging strategy that takes advantage of HTML5 data attributes. Each of the keywords has been wrapped in a span tag with the data-tag attribute applied. In this scenario, the keyword wrapping was accomplished on the server side.

<!-- Contained in default.htm -->
<!DOCTYPE html>
<html>
<head ...>
<body>
  <div id="container">
    <img src="projectsilk.png" />
    <h1>Project Silk Overview</h1>
    <p>
      Project Silk provides guidance and example implementations
      that describe and illustrate recommended practices for 
      building next generation web applications using web 
      technologies like <span data-tag>HTML5</span>, 
      <span data-tag>jQuery</span>, <span data-tag>CSS3</span>
      and Internet Explorer 9. The guidance will be taught in 
      the context of real-world development scenarios rather 
      than focusing on <span data-tag>technology</span> 
      features.</p>

Attaching Widgets

Once created, the widget is attached to an HTML element and its options can be set.

// Contained in startup.js
(function ($) {
    var infobox = $('body').infobox({
        dataUrl: 'http://feeds.delicious.com/v2/json/popular/'
    });

    $('span[data-tag]').tagger({
        activated: function (event, data) {
            infobox.infobox('displayTagLinks', event, data.name);
        },
        deactivated: function () {
            infobox.infobox('hideTagLinks');
        }
    });
} (jQuery));

The code above demonstrates how the infobox widget is attached to the body element. The dataUrloption value will be used when performing popular keyword link lookups.

The jQuery selector span[data-tag] returns a jQuery wrapped set that contains all span tags with a data-tag attribute. A tagger widget will be attached to each of the span tags in the returned collection. The tagger widget has activated and deactivated options that are used as callbacks. These callbacks are used to handle events raised when the mouse hovers over the tag.

Widget Initialization

When a widget is created (attached), the jQuery UI widget factory will call the private method _create. This method provides the developer an opportunity to perform widget setup actions. Examples include building and injecting markup, adding CSS classes, and binding events.

// Contained in jquery.qs.infobox.js
_create: function () {
    var that = this,
        name = that.name;
    that.infoboxElement = $('<div class="qs-infobox" />');
    that.infoboxElement.appendTo('body')
    .bind('mouseenter.' + name, function () {
        mouseOverBox = true;
    })
    .bind('mouseleave.' + name, function () {
        mouseOverBox = false;
        that.hideTagLinks();
    });
},

The code snippet above first creates a variable for this called that within the closure, so the widget can be referenced within the mouseenter and mouseleave event handlers.

Recall that the infobox widget is attached to the body element. The element div.qs-infobox will contain the UI for this widget. It is stored in that.infoboxElement, attached to the body element, and bound to some events. The name variable holds the name of the widget and is appended to the name of the event it's binding to. This is a recommended practice when using jQuery; the reasons why will be explained later in the QuickStart.

Hh404102.note(en-us,PandP.10).gifNote:
Most of the time, widgets are attached to the element that they will control; however, there are times when a widget will need to create additional elements.
In the above _create function, the infobox widget creates a div to hold the list of links. The default.htm HTML page could have been modified to include the div in the first place, making it unnecessary for the widget to add an additional structure. However, the code was written this way to illustrate a widget adding UI elements to an existing HTML structure.

Widget Interactions

An interesting challenge in this scenario is giving the user enough time to click the links without showing the pop-up list longer than needed. The implementation requires coordination between the two widgets.

Mouse Entering a Keyword Span

When the mouse enters the keyword span, the mouseenter event handler in the tagger widget is invoked. The name being appended to the event name is the name of the widget and is used as a namespace for the event binding. This is a recommended practice. Any string can be used as the namespace, but using the name of the widget allows you to tap into a feature of the widget factory described later in the QuickStart.

// Contained in jquery.qs.tagger.js
.bind('mouseenter.' + name, function (event) {
    clearTimeout(timer);
    that._trigger('activated', event, {name: tag});
})

The clearTimeout call uses the timer variable, which is defined outside of the widget prototype and set in the handler for mouseleave, discussed in the next section. This means there will be only one timer created and shared among all instances of the tagger widget.

The next line raises the tagactivated event. It doesn't raise the taggeractivated event because the widget sets the widgetEventPrefix property, as shown in the next code snippet. It also doesn't raise the activated event, as you may have suspected, because the widget factory changes the name of raised events by prepending the name of the widget to the name of the event being triggered.

// Contained in jquery.qs.tagger.js
$.widget('qs.tagger', {

    widgetEventPrefix: 'tag',

    options: {

When the tagactivated event is raised, the displayTagLinks method is called on the infobox widget. As you will notice from having a look at jquery.qs.infobox.js, it never binds to this event. Doing so would create a dependency between the widgets. A better option is to follow a recommended pattern that takes advantage of a related jQuery UI feature. It is recommended that a widget provide callback options for all of the events it raises.

// Contained in jquery.qs.tagger.js
options: {
    activated: null,
    deactivated: null
},

The jQuery UI widget factory will automatically call any option with the same name as the event being raised. This feature allows the event handlers to be associated with the event by setting the value of the option. The QuickStart does this in the startup file.

// Contained in startup.js
$('span[data-tag]').tagger({
    activated: function (event, data) {
        infobox.infobox('displayTagLinks', event, data.name);
    },
    deactivated: function () {
        infobox.infobox('hideTagLinks');
    }
});

This approach is also a nice way to avoid having to know if the event is called tagactivated or taggeractivated or something else. The displayTagLinks method accepts a browser event and the name to look up. The first part of the method sets up enclosed variables to be used in the second part of the method.

// Contained in jquery.qs.infobox.js
displayTagLinks: function (event, tagName) {
    var i,        html,
        that = this,
        options = that.options,
        elem = that.infoboxElement,
        top = event.pageY + offsetY,
        left = event.pageX + offsetX,
        url = options.dataUrl + tagName + '?count=' + options.maxItems,
        displayResult = function () {
                elem.html(html);
                elem.css({top: top, left: left});
                elem.show();
            };
            
        if (event.pageX > window.screenWidth / 2) {
            left = event.pageX + leftSideAdjustment; 
        }

After the closure is prepared, left is adjusted in case the tag is on the right-hand side of the page. The second part of the displayTagLinks method is an Ajax call to the url, constructed above, for the Delicious bookmarking service.

// Contained in jquery.qs.infobox.js
$.ajax({
    url: url,
    dataType: 'jsonp',
    success: function (data) {
        if (data != null) {
            html = '<h1>Popular Links for ' + tagName + '</h1><ul>';
            for (i = 0; i < data.length - 1; i += 1) {
                html += '<li><a href="' +
                        data[i].u + 
                        '" target="_blank">' + 
                        data[i].d + '</a></li>';
            }
            html += '</ul>';
        } else {
            html = '<h1>Data Error</h1><p>[snipped]</p>';
        }
        displayResult();
    },
    error: function (jqXHR, textStatus, errorThrown) {
        html = '<h1>Ajax Error</h1>' +
               '<p>The Ajax call returned the following error: ' + 
               jqXHR.statusText + '.</p>';
        displayResult();
    }
});

The local displayResult function is scoped only to the displayTagLinks method since it was needed for both success and error conditions and nowhere else. This is the method that applies the result to the element for the user to see.

Mouse Leaving a Keyword Span

When the mouse leaves the tag's span, a similar coordination occurs. The tagger widget has a namespaced event bound to the span's mouseleave event.

// Contained in jquery.qs.tagger.js
.bind('mouseleave.' + name, function () {
    timer = setTimeout(function () {
        that._trigger('deactivated');
    }, hideAfter);
});

The timer is set to raise the tagdeactivated event after 1000 milliseconds, which is the value of hideAfter.

When the tagger widget was applied to the span elements, a function was supplied to the deactivated callback, as you also saw earlier in the QuickStart.

// Contained in startup.js
$('span[data-tag]').tagger({
    activated: function (event, data) {
        infobox.infobox('displayTagLinks', event, data.name);
    },
    deactivated: function () {
        infobox.infobox('hideTagLinks');
    }
});

The function invokes the hideTagLinks method on the infobox widget.

// Contained in jquery.qs.infobox.js
hideTagLinks: function () {
    !mouseOverBox && this.infoboxElement.hide();
},

The infobox is only hidden if the mouse is not over it. Effectively, the 1000 ms delay provides the user time to move the mouse from the keywords to the links.

Mouse Entering the Pop-up List

Internally, the infobox widget uses the mouseOverBox variable to maintain state indicating whether or not the mouse is over the pop-up list. This variable is defined in the closure created by the self-executing anonymous function wrapping the file.

// Contained in jquery.qs.infobox.js
(function ($) {
    var offsetX = 20,
        offsetY = 20,
        mouseOverBox = false,
        leftSideAdjustment = -270;
    $.widget('qs.infobox', {

When the mouse enters the infobox, mouseOverBox is set to true.

// Contained in jquery.qs.infobox.js: _create
.bind('mouseenter.' + name, function () {
    mouseOverBox = true;
})

Mouse Leaving the Pop-up List

When the mouse leaves the pop-up list, mouseOverBox is set to false and hideTagLinks is invoked.

// Contained in infobox.js
.bind('mouseleave.' + name, function () {
    mouseOverBox = false;
    that.hideTagLinks();
});

hideTagLinks: function () {
    !mouseOverBox && this.infoboxElement.hide();
},

Further Reading

For more information about the jQuery UI widget factory, see the following resources:


Show:
© 2014 Microsoft