国外开发者解释有状态的插件和 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:
- <p id="reset">
- The system will be reset
- <time datetime="2010-05-05T7:13Z">every 20 minutes.</time>
- </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.
- <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
- <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:
- <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:
- (function($){
- // Our code will go here
- }(jQuery));
Now add the absolute bare minimum to get a widget instance going:
- $.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.
- <script>
- jQuery(function($){
- // Initialize our plugin on all time elements
- $("time").countdown();
- });
- </script>
Now, run a few commands and see what you get back:
- >>> $(":pixel-countdown")
- [ time ]
- >>> $("time").data('countdown')
- Object { element=, more...}
- >>> $("time").countdown("disable")
- [time.pixel-countdown-disabled]
- >>> $("time").countdown("enable")
- [time]
You can see with just a single call to the widget factory, there is already a lot of functionality built for you:
- A custom selector
:pixel-countdown
that can be used to find all of the widgets on a given page - Access to the actual widget object by calling
.data('countdown')
on the element itself - 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:
- $.widget('pixel.countdown', {
- _create: function(){
- var dateTime = this.element.attr('datetime') || this.element.text(),
- hour,
- min,
- setup = false;
- dateTime = dateTime.split('T');
- if( dateTime.length === 2 ){
- // Create a new date based on the year, month, day
- this._endTime = new Date(dateTime[0]);
- // Get hours and minutes
- dateTime = dateTime[1].split(':');
- hour = parseInt( dateTime[0], 10);
- min = parseInt( dateTime[1], 10);
- if( !isNaN(hour) && !isNaN(min) ){
- this._endTime.setUTCHours(hour, min);
- setup = true;
- }
- }
- // In this case we fail silently, and remove the widget
- // We could throw an error instead if wanted.
- if (!setup){
- this.destroy(); // Undo everything we did
- return false;
- }
- this._originalText = this.element.text();
- }
- });
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:
- $("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:
- timeLeft: function(){
- var left = this._endTime - (new Date()).getTime(),
- min = left / 1000 / 60,
- sec = min % 1 * 60;
- min = parseInt(min, 10);
- sec = Math.round(sec);
- return left <= 0 ? [0, 0] : [min, sec];
- }
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.
- $.widget('pixel.countdown', {
- _create: function(){
- ...
- this._timer = window.setInterval($.proxy(this.update, this), 1000);
- this.update(); // call initially to avoid 1 second delay
- },
- timeLeft: function() { ... },
- update: function(){
- var timeLeft = this.timeLeft(),
- replacementString = "in %m minutes and %s seconds.";
- if(timeLeft){
- this.element.text(
- replacementString.replace('%m', timeLeft[0])
- .replace('%s', timeLeft[1])
- );
- } else {
- this.element.text('now');
- }
- }
- });
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:
- Remove the widget without issues
- Customize how the text gets formatted
- 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.
- $.widget('pixel.countdown', {
- options: {
- interval: 1, // Interval in seconds to refresh the widget
- formatter: function(min, sec){
- if(min === 0 && sec === 0){
- return "now.";
- } else {
- var second_label = sec + (sec == 1 ? " second." : " seconds.");
- if( min === 0){
- return "in " + second_label;
- } else {
- return "in " + min + (min == 1 ? " minute" : " minutes") + " and " + second_label;
- }
- }
- }
- },
- _create: function() {
- ...
- this._timer = window.setInterval($.proxy(this.update, this), this.options.interval * 1000);
- this.update(); // call it initially to avoid a delay
- },
- timeLeft: function() { ... },
- update: function() {
- this.element.text(
- this.options.formatter.apply(this.element[0], this.timeLeft())
- );
- }
- });
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
:
- _setOption: function(key, value){
- if (this.options[key] === value) {
- return this; // Do nothing, the value didn't change
- }
- switch (key){
- case "interval":
- // Store the new value
- this.options.interval = value;
- // Clear the old timer
- window.clearInterval(this._timer);
- // Set a new timer based on the new interval
- this._timer = window.setInterval(
- $.proxy(this.update, this),
- this.options.interval * 1000
- );
- break;
- case "formatter":
- this.options.formatter = value;
- this.update();
- break;
- default:
- this.options[key] = value;
- }
- return this;
- }
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:
- milestones: [
- { label: "one_minute", minute: 1 },
- { label: "five_minutes", minute: 5, second: 30 }
- ]
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:
- _cleanMilestones: function(){
- var timeLeft = this.timeLeft(),
- vTL = timeLeft[0] + (timeLeft[1] / 100),
- milestones = $.map(this.options.milestones, function(ms){
- // Create a new literal that contains only the label
- // and the min/sec as a single value
- return { label: ms.label, time: ms.minute + ((ms.second || 0) / 100) };
- }),
- size = milestones.length;
- // Sort largest to smallest
- milestones.sort(function(a,b){
- if (a.time === b.time) return 0;
- else return a.time > b.time ? -1 : 1;
- });
- // Remove expired milestones
- while(milestones.length && milestones[0].time > vTL){
- milestones.shift();
- }
- return milestones;
- }
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
?):
- this.milestones = this._cleanMilestones();
- this.widgetEventPrefix = "countdown_";
Finally we need to update our _setOption
method and add an additional case statement:
- case "milestones":
- this.options.milestones = value;
- this.milestones = this._cleanMilestones();
- 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:
- update: function(){
- var timeLeft = this.timeLeft(),
- vTL = timeLeft[0] + (timeLeft[1] / 100),
- label;
- if(this.milestones.length && this.milestones[0].time > vTL){
- label = this.milestones.shift().label;
- this._trigger('milestone_reached', null , label);
- }
- if(vTL === 0){
- window.clearInterval(this._timer);
- this._trigger('complete');
- }
- this.element.text(
- this.options.formatter.apply(this.element, timeLeft)
- );
- }
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_reached
and 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:
- $("time").countdown({
- milestone_reached: function(label){
- alert(label);
- }
- });
- // or
- $("time")
- .countdown()
- .bind("countdown_milestone_reached", function(e, label){
- alert(label);
- });
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:
- destroy: function(){
- window.clearInterval( this._timer);
- this.element.text( this._originalText);
- // Call the parent destroy method
- $.Widget.prototype.destroy.call(this);
- }
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:
- // Public Facing site
- $("#repeat time").countdown({
- milestones: [ { label: "show_warning", minute: 1 }],
- milestone_reached: function(){
- // Turn the label red when under 1 minute remaining
- $(this).css('color','red');
- },
- complete: function(){
- // Reload the current page when the timer stops
- window.location.reload(true);
- }
- });
- // Backend System
- $("#repeat time").countdown({
- milestones: [ { label: "show_warning", minute: 5 } ],
- milestone_reached: function(){
- // Show a HTML warning box
- $("#warning").dialog("show");
- },
- complete: function(){
- alert('The CMS has been reset. If you want to ' +
- 'save what you are typing, be sure to ' +
- 'copy the text before refreshing the page.');
- }
- });
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!
相关推荐
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 ...
- **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...
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 ...
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 ...
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 ...
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...
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 5来构建一个具备状态转换机制的应用,例如订单处理...
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 ...
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 ...
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 ...
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,...
当我们谈论“Stateful Eloquent”时,这意味着我们将利用Eloquent来实现一个状态机,这是一种设计模式,用于管理对象的状态转换。状态机在业务逻辑中尤其有用,例如处理订单状态、用户账户状态等,它确保了对象状态...
### Spark 2018 欧洲峰会中关于Structured Streaming中的Stateful Stream Processing 在Spark 2018欧洲峰会中,有一场引人注目的演讲深入探讨了Structured Streaming框架下的状态流处理(stateful stream processing...
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 ...
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, ...
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...