`
zha_zi
  • 浏览: 592993 次
  • 性别: Icon_minigender_1
  • 来自: 西安
社区版块
存档分类
最新评论

Introduction to Stateful Plugins and the Widget Factory

 
阅读更多

 国外开发者解释有状态的插件和 widget 工厂

     The jQuery UI Widget Factory is a separate component of the jQuery UI Library that provides an easy, object oriented way to create stateful jQuery plugins. Plugins created using the Widget Factory can be simple or very robust as evidenced by the official jQuery UI widgets, each of which are built on top of the Widget Factory. This article will first look briefly at why you might want to use the Widget Factory, then present an in depth walkthrough of creating a stateful plugin using the Factory.

What is a stateful plugin?

A stateful plugin is an advanced type of jQuery plugin that is self-aware — it maintains its own state, and often provides an external interface for outside code to interact with and alter the plugins state. Stateful plugins, or widgets as they are often called, often trigger events and provide callback hooks into important parts of their functionality. A plugin of this type normally focuses on solving a single task, often constrained to a single area of a finished web page. These widgets try to make as few assumptions about their final use as possible, choosing to expose options instead of internal hard coded settings wherever possible.

Widgets like Date Pickers, Tabs and Dialogs are examples of potentially stateful plugins, though a Media Player, Photo Slideshow, or Shopping Cart widget would also make a good candidate as a stateful plugin. On the other hand, a plugin that takes a paragraph and turns @names into Twitter links wouldnot be a promising use case for a stateful plugin since it needs to run only once per target element.

Why use jQuery UI Widget Factory?

The Widget Factory, like many frameworks, is a tool to assist in rapid, maintainable development. The Widget Factory provides a helpful and consistent structure for creating and interacting with these plugins. It is not necessary to use the Widget Factory when building a stateful plugin, but it does remove many of the redundant tasks involved in setting up a standard configuration. The Widget Factory has the additional benefit of providing an API consistent with jQuery UI, which might afford those who use your plugins a smaller learning curve.

Are they jQuery UI Widgets or jQuery Plugins?

It’s important to realize up front that if you use the Widget Factory to create a stateful jQuery plugin, that you are not creating a jQuery UI Widget or extending jQuery UI. The final product is still a jQuery Plugin. The important difference is that jQuery UI Widgets are officially maintained by the jQuery Project and often have more dependencies on jQuery UI than your plugin is likely to have.

Digging in: Building a jQuery Stateful Plugin

A Countdown Timer

Since stateful plugins are often rather advanced jQuery plugins, we will need to use a simplified example for this walkthrough. I decided on a simple countdown timer that can provide triggers at specific times and will allow the way the information is output to be configurable.

As a fictional use case, assume you are building a demo for a Content Management System where every 15 minutes the system gets wiped clean. This countdown timer needs to be used on both the public site and the backend system to inform the user how many minutes are left until the next install. On the backend, you want to warn the user at 5 minutes left that their work will be deleted shortly and again when the timer hits 0, you want to let them know they need to refresh the page if they want to continue using the system. On the front end, since the user presumably isn’t interacting with the site more than just looking around, you want the page to automatically reload when the timer hits 0. Any other warning on the front end would need to be subtle.

The Markup

The plugin will need to get the end time when it is initialized. The best way to do this is to have the HTML contain a date time string that the plugin can parse and use to display the countdown. For this example, I have chosen to use the new HTML5 time element, though we will make sure the plugin works with normal elements as well. Remember to include the HTML5 shim for IE if you plan on styling the `time` element:

  1. <p id="reset">
  2.   The system will be reset
  3.   <time datetime="2010-05-05T7:13Z">every 20 minutes.</time>
  4. </p>

The plugin will only require any element that either contains the date time string or instead has thedatatime attribute set.

Create a basic HTML file and put the previous HTML snippet somewhere within the opening and closingbody tags. I will be using HTML5 syntax as we go along, so if you prefer HTML4 or XHTML, be sure to add any required attributes yourself (i.e.type="text/javascript"on the script tags, etc.).

Starting the Widget

The very first thing you will want to do is reference jQuery and the jQuery UI Widget factory. Remember, you do not need to include all of jQuery UI. Go ahead and download jquery.ui.widget.js from Github and add links to jQuery and the widget factory to your HTML file.

  1. <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
  2. <script src="js/jquery.ui.widget.js"></script>

Next, create a blank JavaScript file, and link to it in your HTML file. Your JavaScript file should be named using one of the following patterns:

  • jquery.pluginname.js
  • jquery.namespace.pluginname.js

Note about namespaces: Every Widget Factory widget lives inside a specific namespace or scope. The official jQuery UI Widgets occupy the ui namespace, and ui is included in each file name. You mustchoose your own namespace, and you should not use ui since it is reserved for officially maintained UI widgets. Many people use their initials or some short variation on their name. Since I own and run Pixel Graphic Design Studio, I will use pixel as the namespace for this walkthrough:

  1. <script src="js/jquery.pixel.countdown.js"></script>

Like any self-respecting plugin author, the first bit of code you want to put in our script is a self-executing anonymous function that will provide a closure where the code can safely run. Be sure to pass the jQuery variable into the function so you can use $ internally without fear of it being overwritten by another library:

  1. (function($){
  2.  
  3.   // Our code will go here
  4.  
  5. }(jQuery));

Now add the absolute bare minimum to get a widget instance going:

  1. $.widget('pixel.countdown'{});

That’s it! You must use a dot separated formula of namespace.pluginname when using the widget factory. For instance, the UI DatePicker uses ui.datepicker in the same way. Of course right now your widget won't do much, but let's see just how much functionality it already has because of the widget factory. Add this to your HTML file below the script includes, then load up the file in Firefox with Firebugrunning.

  1. <script>
  2.   jQuery(function($){
  3.     // Initialize our plugin on all time elements
  4.     $("time").countdown();
  5.   });
  6. </script>

Now, run a few commands and see what you get back:

  1. >>> $(":pixel-countdown")
  2. [ time ]
  3.  
  4. >>> $("time").data('countdown')
  5. Object { element=,  more...}
  6.  
  7. >>> $("time").countdown("disable")
  8. [time.pixel-countdown-disabled]
  9.  
  10. >>> $("time").countdown("enable")
  11. [time]

You can see with just a single call to the widget factory, there is already a lot of functionality built for you:

  1. A custom selector :pixel-countdown that can be used to find all of the widgets on a given page
  2. Access to the actual widget object by calling .data('countdown') on the element itself
  3. The ability to enable or disable the widget by calling countdown() and passing a string of "enable" or "disable" (Though the widget doesn’t change state in response to being enabled or disabled currently).

This is a pretty useless plugin right now, but it was important to understand a few of the shortcuts that were already created for you just by using the Widget Factory. Now it is time to add some meat to the plugin.

Widget Initialization

The Widget Factory class provides a few methods you will need to override to really provide functionality to the plugin. We will start by overriding _create() which is called when the widget is instantiated on an element. In this function, the plugin should attempt to grab the date from either the datetime attribute or the contents of the element. For this demo, we are not implementing a full date/time parser according to the HTML5 spec; we will be expecting the string in this format: “YYYY-MM-DDTHH:MMZ”. All times will be in UTC, based on a 24 hour clock.

Inside our widget, you can refer to this.element to get a jQuery wrapped version of the DOM element on which the plugin was instantiated. Remember: If you are attempting to use this.element inside a second closure, it will fail. Be sure to store this in a variable outside the second closure before trying to access it.

Your create method should look like this:

  1. $.widget('pixel.countdown'{
  2.   _create: function(){
  3.     var dateTime = this.element.attr('datetime') || this.element.text(),
  4.         hour,
  5.         min,
  6.         setup    = false;
  7.  
  8.     dateTime = dateTime.split('T');
  9.  
  10.     if( dateTime.length === 2 ){
  11.       // Create a new date based on the year, month, day
  12.       this._endTime = new Date(dateTime[0]);
  13.  
  14.       // Get hours and minutes
  15.       dateTime = dateTime[1].split(':');
  16.       hour = parseInt( dateTime[0], 10);
  17.       min  = parseInt( dateTime[1], 10);
  18.  
  19.       if( !isNaN(hour) && !isNaN(min) ){
  20.         this._endTime.setUTCHours(hour, min);
  21.         setup = true;
  22.       }
  23.     }
  24.  
  25.     // In this case we fail silently, and remove the widget
  26.     // We could throw an error instead if wanted.
  27.     if (!setup){
  28.       this.destroy(); // Undo everything we did
  29.       return false;
  30.     }
  31.  
  32.     this._originalText = this.element.text();
  33.   }
  34. });

Let’s quickly review what we just wrote. First, we used the function name _create. It’s important to note that method names beginning with an underscore are treated as private functions, and all other methods are treated as public. Users will be able to access your public methods a number of ways, the simplest of which looks something like this:

  1. $("time").countdown("functionname", argument1, argument2);

In the create method the plugin takes care of retrieving and storing the date, but doesn’t do anything with it yet. It additionally stores a copy of the original text content so it can be restored later.

Custom Functions

You can have as few or as many custom functions on your plugin as you want or is necessary. It is becoming apparent that the widget needs a “timeLeft” function where it will subtract the current time from the _endTime variable, and return the information. Since we might also want to use this method from outside the widget, make the method public:

  1. timeLeft: function(){
  2.   var left = this._endTime - (new Date()).getTime(),
  3.       min  = left / 1000 / 60,
  4.       sec  = min % 1 * 60;
  5.  
  6.   min  = parseInt(min, 10);
  7.   sec  = Math.round(sec);
  8.  
  9.   return left <= 0 ? [00] : [min, sec];
  10. }

You can access this function from within the widget code by using this.timeLeft() and outside the widget with $(selector).countdown("timeLeft"). Now we need a way to take this information, and display it on the page. Let’s add an update function, and set it up to be called every 1 second. We will use thejQuery.proxy method to ensure this remains equal to the widget object inside the update function.

  1. $.widget('pixel.countdown'{
  2.   _create: function(){
  3.     ...
  4.  
  5.     this._timer = window.setInterval($.proxy(this.update, this), 1000);
  6.     this.update(); // call initially to avoid 1 second delay
  7.   },
  8.   timeLeft: function() { ... },
  9.   update: function(){
  10.     var timeLeft = this.timeLeft(),
  11.         replacementString = "in %m minutes and %s seconds.";
  12.     if(timeLeft){
  13.       this.element.text(
  14.         replacementString.replace('%m', timeLeft[0])
  15.                          .replace('%s', timeLeft[1])
  16.       );
  17.     } else {
  18.       this.element.text('now');
  19.     }
  20.   }
  21. });

At this point, if you reload the test page, you should see the time element transformed to one of two things: “now” or “in x minutes and x seconds”.  If you see “now”, take a moment to update the date in your HTML to point to some point in the future in UTC time.

Quick Review

So far you have a countdown widget that successfully parses the date and time and replaces the contents of the element with the remaining time. However, you cannot currently:

  1. Remove the widget without issues
  2. Customize how the text gets formatted
  3. Trigger any special callbacks when milestones are hit as our use case requires

Using Options

User configurable options are at the heart of every stateful plugin. Remember to offer as few options as are needed to keep your widgets simple but as many as are needed to give a good amount of outside control. This widget needs at least a few customizable options. Lets add two options as a start: interval, and formatter.

You add options by providing an object literal with keys and values to an options property on the widget. We will revise our _create and updatemethods at the same time to reflect the new options.

  1. $.widget('pixel.countdown'{
  2.   options: {
  3.     interval: 1// Interval in seconds to refresh the widget
  4.     formatter: function(min, sec){
  5.       if(min === 0 && sec === 0){
  6.         return "now.";
  7.       } else {
  8.         var second_label = sec + (sec == 1 ? " second." : " seconds.");
  9.         if( min === 0){
  10.           return "in " + second_label;
  11.         } else {
  12.           return "in " + min + (min == 1 ? " minute" : " minutes") + " and " + second_label;
  13.         }
  14.       }
  15.     }
  16.   },
  17.   _create:  function() {
  18.     ... 
  19.  
  20.     this._timer = window.setInterval($.proxy(this.update, this), this.options.interval * 1000);
  21.     this.update(); // call it initially to avoid a delay
  22.   },
  23.   timeLeft: function() { ... },
  24.   update:   function() {
  25.     this.element.text(
  26.       this.options.formatter.apply(this.element[0], this.timeLeft())
  27.     );
  28.   }
  29. });

By using apply when calling the formatter function, you are able to pass the DOM element in as the context. You could just have easily passed in the widget object, but since jQuery developers are used to this being equal to the related DOM node (in event callbacks), this makes a nice practice for the widget as well.

You can get an option’s value by using this.options.key inside the widget and$(selector).countdown("option", "key") outside the widget.

Setting options, however, is something you need to implement differently. Plugins created using the Widget Factory should be able to have their options updated at any time and should respond accordingly to the changes. To facilitate this, the Widget Factory provides a _setOption function you should override when implementing your widget. Here is the function filled out for the two optionsinterval and formatter:

  1. _setOption: function(key, value){
  2.   if (this.options[key] === value) {
  3.       return this// Do nothing, the value didn't change
  4.   }
  5.  
  6.   switch (key){
  7.     case "interval":
  8.       // Store the new value
  9.       this.options.interval = value;
  10.  
  11.       // Clear the old timer
  12.       window.clearInterval(this._timer);
  13.  
  14.       // Set a new timer based on the new interval
  15.       this._timer = window.setInterval(
  16.         $.proxy(this.update, this),
  17.         this.options.interval * 1000
  18.       );
  19.       break;
  20.  
  21.     case "formatter":
  22.       this.options.formatter = value;
  23.       this.update();
  24.       break;
  25.  
  26.     default:
  27.       this.options[key] = value;
  28.   }
  29.  
  30.   return this;
  31. }

Now, options can be set by calling this._setOption(key,value) internally and by calling either$(selector).countdown("option","key", value) or $(selector).countdown("option", { key: value }) from outside our widget. Because we added code to respond to any changes, the widget is always kept in sync.

Event Binding and Custom Events

Since this widget needs no user interaction, we are not binding any events. However, when you are binding events it is important that you always namespace your events using the following formula:eventName + '.' + this.widgetName. This will allow you to unbind all your widget’s events later without disturbing existing event handlers added from outside your widget. In fact, all events are automatically detached using this formula in the destroy method of the Widget class.

Custom events allow your plugin users to be alerted as important situations occur inside your plugin. To illustrate the use of how to trigger custom events when using the Widget Factory, we will add amilestones option and a milestone_reached custom event to the widget.

In your options literal, add the following:

  1. milestones: [
  2.   { label: "one_minute", minute: 1 },
  3.   { label: "five_minutes", minute: 5, second: 30 }
  4. ]

Next we will add a private method to prepare the milestones by first sorting them, then removing all milestones that have already expired when the widget is created. What is really important in this example is that we are not altering the options.milestones value. Instead we are altering a new object which we will store in a different variable (next code block). Since we are altering, sorting, and removing the milestones, it is important the original option remains untouched:

  1. _cleanMilestones: function(){
  2.   var timeLeft   = this.timeLeft(),
  3.       vTL        = timeLeft[0] + (timeLeft[1] / 100),
  4.       milestones = $.map(this.options.milestones, function(ms){
  5.         // Create a new literal that contains only the label
  6.         // and the min/sec as a single value
  7.         return { label: ms.label, time: ms.minute + ((ms.second || 0) / 100};
  8.       }),
  9.       size       = milestones.length;
  10.  
  11.   // Sort largest to smallest
  12.   milestones.sort(function(a,b){
  13.     if (a.time === b.time) return 0;
  14.     else return a.time > b.time ? -1 : 1;
  15.   });
  16.  
  17.   // Remove expired milestones
  18.   while(milestones.length && milestones[0].time > vTL){
  19.     milestones.shift();
  20.   }
  21.  
  22.   return milestones;
  23. }

Add the following lines to your _create method (before the this._timer ... line). The first line calls the clean function and the second line sets the value of the prefix that is automatically added to each custom event you trigger. The Widget Factory will assume you want widgetname as the value if we do not override it (And who wants to bind to countdownmilestone_reached ?):

  1. this.milestones = this._cleanMilestones();
  2. this.widgetEventPrefix = "countdown_";

Finally we need to update our _setOption method and add an additional case statement:

  1. case "milestones":
  2.   this.options.milestones = value;
  3.   this.milestones = this._cleanMilestones();
  4.   break;

Now that you have our milestones all set up and configured, let’s get down to actually triggering the custom event when needed! Refine your update method as follows. At this time we will also add an additional custom event named complete for when the timer runs out:

  1. update: function(){
  2.   var timeLeft = this.timeLeft(),
  3.       vTL = timeLeft[0] + (timeLeft[1] / 100),
  4.       label;
  5.  
  6.   if(this.milestones.length && this.milestones[0].time > vTL){
  7.     label = this.milestones.shift().label;
  8.     this._trigger('milestone_reached', null , label);
  9.   }
  10.  
  11.   if(vTL === 0){
  12.     window.clearInterval(this._timer);
  13.     this._trigger('complete');
  14.   }
  15.  
  16.   this.element.text(
  17.     this.options.formatter.apply(this.element, timeLeft)
  18.   );
  19. }

This method will now trigger an event named countdown_milestone_reached on the DOM element (in this case, the time element) and pass it an extra parameter with the milestone label. But this is not all the Widget Factory does for you! The _trigger method will also look for an option named milestone_reachedand if it is a function, it will call it as well, passing in the label. This means the following two code formats are both valid ways to listen for the custom event from outside the widget:

  1. $("time").countdown({
  2.   milestone_reached: function(label){
  3.     alert(label);
  4.   }
  5. });
  6.  
  7. // or
  8.  
  9. $("time")
  10.   .countdown()
  11.   .bind("countdown_milestone_reached"function(e, label){
  12.     alert(label);
  13.   });

Cleaning Up

One glaring issue we have been avoiding is how to clean up all the stuff we are doing if the widget is to be removed. Thankfully the Widget Factory makes this easy as well. Its base destroy function does most of the work for us, so you’ll just need to add a few lines to restore the original text content and remove the timer:

  1. destroy: function(){
  2.   window.clearInterval( this._timer);
  3.   this.element.text( this._originalText);
  4.  
  5.   // Call the parent destroy method
  6.   $.Widget.prototype.destroy.call(this);
  7. }

Now your widget will properly respond when $(selector).countdown("destroy") is called on it or if.remove() is called on the associated DOM element.

Reader Exercise

When we overrode the _setOption function, we never reimplemented the enabled and disabled code. Another core expectation of a Widget Factory widget, is that the API supports enabling and disabling the plugin. Add the necessary code to support this functionality. Hint, look in jquery.ui.widget.js for an idea on how you might accomplish this.

Using Your Widget

Let’s circle back to the original use cases presented at the beginning of this article and see how it would look if implemented with our new widget:

  1. // Public Facing site
  2. $("#repeat time").countdown({
  3.   milestones: [ { label: "show_warning", minute: 1 }],
  4.   milestone_reached: function(){
  5.     // Turn the label red when under 1 minute remaining
  6.     $(this).css('color','red');
  7.   },
  8.   complete: function(){
  9.     // Reload the current page when the timer stops
  10.     window.location.reload(true);
  11.   }
  12. });
  13.  
  14. // Backend System
  15. $("#repeat time").countdown({
  16.   milestones: [ { label: "show_warning", minute: 5 } ],
  17.   milestone_reached: function(){
  18.     // Show a HTML warning box
  19.     $("#warning").dialog("show");
  20.   },
  21.   complete: function(){
  22.     alert('The CMS has been reset. If you want to ' +
  23.           'save what you are typing, be sure to ' +
  24.           'copy the text before refreshing the page.');
  25.   }
  26. });

Conclusion

In this article you have learned how to build a stateful plugin using the jQuery UI Widget Factory. You know how to set and use options, initialize and destroy the widget as well as trigger custom events. I challenge you to use the Widget Factory in a real project or for the next open source plugin you write. It is really an amazing tool available to jQuery plugin developers and the best way to learn to use it is by… you guessed it… just using it!

分享到:
评论

相关推荐

    Introduction to Apache Flink

     End-to-End Consistency and the Stream Processor as a Database  Flink Performance: the Yahoo! Streaming Benchmark  Conclusion Chapter 6 Batch Is a Special Case of Streaming  Batch Processing ...

    Ubuntu The Complete Reference

    - **Introduction to the Shell**: Explanation of the role of the shell in Unix-like systems and how it serves as the primary interface for interacting with the system. - **Common Shell Commands**: List...

    英文原版-The Book of PF 2nd Edition

    A No-Nonsense Guide to the OpenBSD FirewallOpenBSD’s stateful packet filter, PF, is the heart of the OpenBSD firewall and a necessity for any admin working in a BSD environment. With a little effort ...

    Mastering Kubernetes

    Using real-world use cases, we explain the options for network configuration and provides guidelines on how to set up, operate, and troubleshoot various Kubernetes networking plugins. Finally, we ...

    SAP PO/PI教程 Process Orchestration The Comprehensive Guide

    3.5.6 Connecting the SLD to CTS+ to Facilitate the Export and Import of SLD Data 3.6 Exercise: Configuring the System Landscape Directory 3.6.1 Exercise Description 3.6.2 Exercise Solution ...

    Manning.Ajax.in.Practice.Jun.2007.pdf

    top of this stateless protocol, opening the door to stateful applications such as reservation systems and online commerce. Encrypted layers were built on top of the core protocol, to give confidence...

    Mastering Kubernetes [Gigi Sayfan]

    Using real-world use cases, we explain the options for network configuration and provides guidelines on how to set up, operate, and troubleshoot various Kubernetes networking plugins. Finally, we ...

    Laravel开发-stateful

    在Laravel框架中,"stateful"通常指的是应用或组件具有状态管理能力,即它可以跟踪并根据当前状态执行特定操作。这里的"Laravel开发-stateful"可能是指使用Laravel 5来构建一个具备状态转换机制的应用,例如订单处理...

    VMware vSphere 6.5 Cookbook 3rd Edition.pdf

    Chapter 5, Using vSphere Auto Deploy, you will learn how to deploy stateless and stateful ESXi host without the need to have to run the ISO installation on the server hardware. Chapter 6, Using ...

    The Book of PF(NoStarch,3ed,2014)

    OpenBSD's stateful packet filter, PF, is the heart of the OpenBSD firewall and a necessity for any admin working in a BSD environment. With a little effort and this book, you'll gain the insight ...

    安全FWSM新书Cisco Secure Firewall Services Module (FWSM)

    Description: The Firewall Services Module (FWSM) is a high-performance stateful-inspection firewall that integrates into the Cisco® 6500 switch and 7600 router chassis. The FWSM monitors traffic ...

    Programming Microsoft Azure Service Fabric

    learn how to set up your development environment, and how to program and deploy a Service Fabric application to a local or a cloud-based cluster. You’ll learn about Service Fabric programming models,...

    Laravel开发-stateful-eloquent

    当我们谈论“Stateful Eloquent”时,这意味着我们将利用Eloquent来实现一个状态机,这是一种设计模式,用于管理对象的状态转换。状态机在业务逻辑中尤其有用,例如处理订单状态、用户账户状态等,它确保了对象状态...

    spark2018欧洲峰会中关于StructuredStreaming中stateful stream processing的ppt

    ### Spark 2018 欧洲峰会中关于Structured Streaming中的Stateful Stream Processing 在Spark 2018欧洲峰会中,有一场引人注目的演讲深入探讨了Structured Streaming框架下的状态流处理(stateful stream processing...

    React.js Essentials(PACKT,2015)

    Create stateless and stateful components and make them reactive, learn to interact between your components and lifecycle methods and gauge how to effectively integrate your user interface components ...

    Internetworking IPv6 with Cisco Routers

    Overview, Why IPv6, Why a new address scheme, Best Effort: is it enough, Requisites to be satisfied by IPv6, An address space to last, To unify Intranets and the Internet, A good support for ATM, ...

    EJB3 in action ORALCE PPT

    To illustrate the differences between EJB2 and EJB3, consider the following comparison based on the example provided: **EJB2.1 Session Bean Class:** ```java public class CartEJB implements Session...

Global site tag (gtag.js) - Google Analytics