Skip to content

Commit 5e5df58

Browse files
committed
Widget Factory section
1 parent 59f11c4 commit 5e5df58

File tree

1 file changed

+355
-0
lines changed

1 file changed

+355
-0
lines changed
Lines changed: 355 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,355 @@
1+
---
2+
chapter : plugins
3+
section : 5
4+
title : Writing Stateful Plugins with the jQuery UI Widget Factory
5+
attribution: jQuery Fundamentals
6+
---
7+
## Writing Stateful Plugins with the jQuery UI Widget Factory
8+
9+
<div class="note" markdown="1">
10+
### Note
11+
12+
This section is based, with permission, on the blog post [Building Stateful jQuery Plugins](http://blog.nemikor.com/2010/05/15/building-stateful-jquery-plugins/) by Scott Gonzalez.
13+
</div>
14+
15+
While most existing jQuery plugins are stateless — that is, we call them on an element and that is the extent of our interaction with the plugin — there’s a large set of functionality that doesn’t fit into the basic plugin pattern.
16+
17+
In order to fill this gap, jQuery UI has implemented a more advanced plugin system.
18+
The new system manages state, allows multiple functions to be exposed via a single plugin, and provides various extension points.
19+
This system is called the widget factory and is exposed as `jQuery.widget` as part of jQuery UI 1.8; however, it can be used independently of jQuery UI.
20+
21+
To demonstrate the capabilities of the widget factory, we'll build a simple progress bar plugin.
22+
23+
To start, we’ll create a progress bar that just lets us set the progress once.
24+
As we can see below, this is done by calling jQuery.widget with two parameters: the name of the plugin to create and an object literal containing functions to support our plugin.
25+
When our plugin gets called, it will create a new plugin instance and all functions will be executed within the context of that instance.
26+
This is different from a standard jQuery plugin in two important ways.
27+
First, the context is an object, not a DOM element.
28+
Second, the context is always a single object, never a collection.
29+
30+
<div class="example" markdown="1">
31+
A simple, stateful plugin using the jQuery UI widget factory
32+
33+
$.widget("nmk.progressbar", {
34+
_create: function() {
35+
var progress = this.options.value + "%";
36+
this.element
37+
.addClass("progressbar")
38+
.text(progress);
39+
}
40+
});
41+
</div>
42+
43+
The name of the plugin must contain a namespace; in this case we’ve used the `nmk` namespace.
44+
There is a limitation that namespaces be exactly one level deep — that is, we can't use a namespace like `nmk.foo`.
45+
We can also see that the widget factory has provided two properties for us. `this.element` is a jQuery object containing exactly one element.
46+
If our plugin is called on a jQuery object containing multiple elements, a separate plugin instance will be created for each element, and each instance will have its own `this.element`.
47+
The second property, `this.options`, is a hash containing key/value pairs for all of our plugin’s options.
48+
These options can be passed to our plugin as shown here.
49+
50+
<div class="note" markdown="1">
51+
### Note
52+
53+
In our example we use the `nmk` namespace. The `ui` namespace is reserved for official jQuery UI plugins. When building your own plugins, you should create your own namespace.
54+
This makes it clear where the plugin came from and whether it is part of a larger collection.
55+
</div>
56+
57+
<div class="example" markdown="1">
58+
Passing options to a widget
59+
60+
$("&lt;div>&lt;/div>")
61+
.appendTo( "body" )
62+
.progressbar({ value: 20 });
63+
</div>
64+
65+
When we call `jQuery.widget `it extends jQuery by adding a method to `jQuery.fn` (the same way we'd create a standard plugin).
66+
The name of the function it adds is based on the name you pass to `jQuery.widget`, without the namespace; in our case it will create `jQuery.fn.progressbar`.
67+
The options passed to our plugin get set in `this.options` inside of our plugin instance.
68+
As shown below, we can specify default values for any of our options.
69+
When designing your API, you should figure out the most common use case for your plugin so that you can set appropriate default values and make all options truly optional.
70+
71+
<div class="example" markdown="1">
72+
Setting default options for a widget
73+
74+
$.widget("nmk.progressbar", {
75+
// default options
76+
options: {
77+
value: 0
78+
},
79+
80+
_create: function() {
81+
var progress = this.options.value + "%";
82+
this.element
83+
.addClass( "progressbar" )
84+
.text( progress );
85+
}
86+
});
87+
</div>
88+
89+
### Adding Methods to a Widget
90+
91+
Now that we can initialize our progress bar, we’ll add the ability to perform actions by calling methods on our plugin instance.
92+
To define a plugin method, we just include the function in the object literal that we pass to `jQuery.widget`.
93+
We can also define “private” methods by prepending an underscore to the function name.
94+
95+
<div class="example" markdown="1">
96+
Creating widget methods
97+
98+
$.widget("nmk.progressbar", {
99+
options: {
100+
value: 0
101+
},
102+
103+
_create: function() {
104+
var progress = this.options.value + "%";
105+
this.element
106+
.addClass("progressbar")
107+
.text(progress);
108+
},
109+
110+
// create a public method
111+
value: function(value) {
112+
// no value passed, act as a getter
113+
if (value === undefined) {
114+
return this.options.value;
115+
// value passed, act as a setter
116+
} else {
117+
this.options.value = this._constrain(value);
118+
var progress = this.options.value + "%";
119+
this.element.text(progress);
120+
}
121+
},
122+
123+
// create a private method
124+
_constrain: function(value) {
125+
if (value > 100) {
126+
value = 100;
127+
}
128+
if (value < 0) {
129+
value = 0;
130+
}
131+
return value;
132+
}
133+
});
134+
</div>
135+
136+
To call a method on a plugin instance, you pass the name of the method to the jQuery plugin. If you are calling a method that accepts parameters, you simply pass those parameters after the method name.
137+
138+
<div class="example" markdown="1">
139+
Calling methods on a plugin instance
140+
141+
var bar = $("&lt;div>&lt;/div>")
142+
.appendTo("body")
143+
.progressbar({ value: 20 });
144+
145+
// get the current value
146+
alert(bar.progressbar("value"));
147+
148+
// update the value
149+
bar.progressbar("value", 50);
150+
151+
// get the current value again
152+
alert(bar.progressbar("value"));
153+
</div>
154+
155+
<div class="note" markdown="1">
156+
### Note
157+
158+
Executing methods by passing the method name to the same jQuery function that was used to initialize the plugin may seem odd. This is done to prevent pollution of the jQuery namespace while maintaining the ability to chain method calls.
159+
</div>
160+
161+
### Working with Widget Options
162+
163+
One of the methods that is automatically available to our plugin is the option method.
164+
The option method allows you to get and set options after initialization.
165+
This method works exactly like jQuery’s css and `attr` methods: you can pass just a name to use it as a setter, a name and value to use it as a single setter, or a hash of name/value pairs to set multiple values.
166+
When used as a getter, the plugin will return the current value of the option that corresponds to the name that was passed in.
167+
When used as a setter, the plugin’s `_setOption` method will be called for each option that is being set.
168+
We can specify a `_setOption` method in our plugin to react to option changes.
169+
170+
<div class="example" markdown="1">
171+
Responding when an option is set
172+
173+
$.widget("nmk.progressbar", {
174+
options: {
175+
value: 0
176+
},
177+
178+
_create: function() {
179+
this.element.addClass("progressbar");
180+
this._update();
181+
},
182+
183+
_setOption: function(key, value) {
184+
this.options[key] = value;
185+
this._update();
186+
},
187+
188+
_update: function() {
189+
var progress = this.options.value + "%";
190+
this.element.text(progress);
191+
}
192+
});
193+
</div>
194+
195+
### Adding Callbacks
196+
197+
One of the easiest ways to make your plugin extensible is to add callbacks so
198+
users can react when the state of your plugin changes. We can see below how to
199+
add a callback to our progress bar to signify when the progress has reached
200+
100%. The `_trigger` method takes three parameters: the name of the callback,
201+
a native event object that initiated the callback, and a hash of data relevant
202+
to the event. The callback name is the only required parameter, but the others
203+
can be very useful for users who want to implement custom functionality on top
204+
of your plugin. For example, if we were building a draggable plugin, we could
205+
pass the native mousemove event when triggering a drag callback; this would
206+
allow users to react to the drag based on the x/y coordinates provided by the
207+
event object.
208+
209+
<div class="example" markdown="1">
210+
Providing callbacks for user extension
211+
212+
$.widget("nmk.progressbar", {
213+
options: {
214+
value: 0
215+
},
216+
217+
_create: function() {
218+
this.element.addClass("progressbar");
219+
this._update();
220+
},
221+
222+
_setOption: function(key, value) {
223+
this.options[key] = value;
224+
this._update();
225+
},
226+
227+
_update: function() {
228+
var progress = this.options.value + "%";
229+
this.element.text(progress);
230+
if (this.options.value == 100) {
231+
this._trigger("complete", null, { value: 100 });
232+
}
233+
}
234+
});
235+
</div>
236+
237+
Callback functions are essentially just additional options, so you can get and
238+
set them just like any other option. Whenever a callback is executed, a
239+
corresponding event is triggered as well. The event type is determined by
240+
concatenating the plugin name and the callback name. The callback and event
241+
both receive the same two parameters: an event object and a hash of data
242+
relevant to the event, as we’ll see below.
243+
244+
If your plugin has functionality that you want to allow the user to prevent,
245+
the best way to support this is by creating cancelable callbacks. Users can
246+
cancel a callback, or its associated event, the same way they cancel any native
247+
event: by calling `event.preventDefault()` or using `return false`. If the user
248+
cancels the callback, the `_trigger` method will return false so you can
249+
implement the appropriate functionality within your plugin.
250+
251+
<div class="example" markdown="1">
252+
Binding to widget events
253+
254+
var bar = $("<div></div>")
255+
.appendTo("body")
256+
.progressbar({
257+
complete: function(event, data) {
258+
alert( "Callbacks are great!" );
259+
}
260+
})
261+
.bind("progressbarcomplete", function(event, data) {
262+
alert("Events bubble and support many handlers for extreme flexibility.");
263+
alert("The progress bar value is " + data.value);
264+
});
265+
266+
bar.progressbar("option", "value", 100);
267+
</div>
268+
269+
### The Widget Factory: Under the Hood
270+
271+
When you call jQuery.widget, it creates a constructor function for your plugin and sets the object literal that you pass in as the prototype for your plugin instances. All of the functionality that automatically gets added to your plugin comes from a base widget prototype, which is defined as jQuery.Widget.prototype. When a plugin instance is created, it is stored on the original DOM element using jQuery.data, with the plugin name as the key.
272+
273+
Because the plugin instance is directly linked to the DOM element, you can access the plugin instance directly instead of going through the exposed plugin method if you want. This will allow you to call methods directly on the plugin instance instead of passing method names as strings and will also give you direct access to the plugin’s properties.
274+
275+
<div class="example" markdown="1">
276+
var bar = $("<div></div>")
277+
.appendTo("body")
278+
.progressbar()
279+
.data("progressbar" );
280+
281+
// call a method directly on the plugin instance
282+
bar.option("value", 50);
283+
284+
// access properties on the plugin instance
285+
alert(bar.options.value);
286+
</div>
287+
288+
One of the biggest benefits of having a constructor and prototype for a plugin
289+
is the ease of extending the plugin. By adding or modifying methods on the
290+
plugin’s prototype, we can modify the behavior of all instances of our plugin.
291+
For example, if we wanted to add a method to our progress bar to reset the
292+
progress to 0% we could add this method to the prototype and it would instantly
293+
be available to be called on any plugin instance.
294+
295+
<div class="example" markdown="1">
296+
$.nmk.progressbar.prototype.reset = function() {
297+
this._setOption("value", 0);
298+
};
299+
</div>
300+
301+
### Cleaning Up
302+
303+
In some cases, it will make sense to allow users to apply and then later
304+
unapply your plugin. You can accomplish this via the destroy method. Within the
305+
`destroy` method, you should undo anything your plugin may have done during
306+
initialization or later use. The `destroy` method is automatically called if the
307+
element that your plugin instance is tied to is removed from the DOM, so this
308+
can be used for garbage collection as well. The default `destroy` method removes
309+
the link between the DOM element and the plugin instance, so it’s important to
310+
call the base function from your plugin’s `destroy` method.
311+
312+
<div class="example" markdown="1">
313+
Adding a destroy method to a widget
314+
315+
$.widget( "nmk.progressbar", {
316+
options: {
317+
value: 0
318+
},
319+
320+
_create: function() {
321+
this.element.addClass("progressbar");
322+
this._update();
323+
},
324+
325+
_setOption: function(key, value) {
326+
this.options[key] = value;
327+
this._update();
328+
},
329+
330+
_update: function() {
331+
var progress = this.options.value + "%";
332+
this.element.text(progress);
333+
if (this.options.value == 100 ) {
334+
this._trigger("complete", null, { value: 100 });
335+
}
336+
},
337+
338+
destroy: function() {
339+
this.element
340+
.removeClass("progressbar")
341+
.text("");
342+
343+
// call the base destroy function
344+
$.Widget.prototype.destroy.call(this);
345+
}
346+
});
347+
</div>
348+
349+
### Conclusion
350+
351+
The widget factory is only one way of creating stateful plugins. There are a
352+
few different models that can be used and each have their own advantages and
353+
disadvantages. The widget factory solves lots of common problems for you and
354+
can greatly improve productivity, it also greatly improves code reuse, making
355+
it a great fit for jQuery UI as well as many other stateful plugins.

0 commit comments

Comments
 (0)