Widget Factory - Nemikor Blog

96
jQuery UI Widget Factory

Transcript of Widget Factory - Nemikor Blog

Page 1: Widget Factory - Nemikor Blog

jQuery UI

Widget Factory

Page 2: Widget Factory - Nemikor Blog

Scott GonzálezjQuery UI development leadhttp://nemikor.com@scott_gonzalez

$(λ);

Page 3: Widget Factory - Nemikor Blog

The widget factory- What is it?- Why do we need it?- How do we use it?

$.widget();

Page 4: Widget Factory - Nemikor Blog

Why we needed the widget factory

Page 5: Widget Factory - Nemikor Blog

jQuery revolutionizedthe way we work with the DOM

$('p').show();

Page 6: Widget Factory - Nemikor Blog

jQuery excels at simple tasks

$('p').fadeOut();

Page 7: Widget Factory - Nemikor Blog

$('#notification').text('Your file has been uploaded').show().fadeOut(2000);

Page 8: Widget Factory - Nemikor Blog

$('a.user').click(function() {var url = this.href;$('#user-info').load(url);return false;

});

Page 9: Widget Factory - Nemikor Blog

What about stateful widgets?

$('div').dialog();

Page 10: Widget Factory - Nemikor Blog

$.ajax({url: '/foo.html',beforeSend: function() { … },success: function() { … },error: function() { … }

});

Page 11: Widget Factory - Nemikor Blog

$(elem).dialog({modal: true,open: function() { … },beforeClose: function() { … },close: function() { … }

});

Page 12: Widget Factory - Nemikor Blog

How do we handle initializationand function calls?

$(elem).dialog('open');

Page 13: Widget Factory - Nemikor Blog

The widget factory:A solution to state management

Page 14: Widget Factory - Nemikor Blog

Defning an API

Page 15: Widget Factory - Nemikor Blog

$(elem).dialog({

draggable: false,modal: true

}).dialog('open');

Page 16: Widget Factory - Nemikor Blog

$(elem).createDialog({

draggable: false,modal: true

}).openDialog();

Page 17: Widget Factory - Nemikor Blog

$(elem).dialog({

draggable: false,modal: true

}).dialogOpen();

Page 18: Widget Factory - Nemikor Blog

$(elem).dialog({

draggable: false,modal: true

}).open();

Page 19: Widget Factory - Nemikor Blog

var dialog = new $.dialog(elem, {draggable: false,modal: true

});dialog.open();

Page 20: Widget Factory - Nemikor Blog

Any other models?

Page 21: Widget Factory - Nemikor Blog

jQuery UI actually implementsmany of these models.

$(elem).data('dialog');

Page 22: Widget Factory - Nemikor Blog

$(elem).dialog(options);

$(elem).dialog('open');

Page 23: Widget Factory - Nemikor Blog

$(elem).dialog(options);

$(elem).data('dialog').open();

Page 24: Widget Factory - Nemikor Blog

var dialog = new $.ui.dialog(elem, options);dialog._init();

dialog.open();

Page 25: Widget Factory - Nemikor Blog

How does it work?

$.ui.dialog.prototype

Page 26: Widget Factory - Nemikor Blog

$.ui.dialog.prototype.draggable =function(toggle) {

return this.option('draggable', toggle);

};$(elem).dialog('draggable', true);

Page 27: Widget Factory - Nemikor Blog

Using the widget factory

Page 28: Widget Factory - Nemikor Blog

The bare minimum

Page 29: Widget Factory - Nemikor Blog

$.widget('ui.progressbar', {_init: function() {

this.element.text(this.options.value + '%');

}});$.ui.progressbar.defaults = { value: 0 };

Page 30: Widget Factory - Nemikor Blog

$(elem).progressbar();

$(elem).progressbar({ value: 25 });

<div class=“{progressbar:{value:10}}”>$('div').progressbar();

Page 31: Widget Factory - Nemikor Blog

Working with options

Page 32: Widget Factory - Nemikor Blog

$(elem).progressbar();

$(elem).progressbar('option', 'value');

$(elem).progressbar('option', 'value', 5);

Page 33: Widget Factory - Nemikor Blog

_setData: function(key, value) {this.options[key] = value;if (key == 'value') {

this.element.text(value + '%');}

}

Page 34: Widget Factory - Nemikor Blog

Adding methods

Page 35: Widget Factory - Nemikor Blog

progress: function(value) {this._setData('value',

this._constrain(value));},_constrain: function(value) {

return Math.max(0,Math.min(value, 100));

}

Page 36: Widget Factory - Nemikor Blog

progress: function(value) {if (this.options.disabled) {

return;}this._setData('value',

this._constrain(value));}

Page 37: Widget Factory - Nemikor Blog

Creating callbacks

Page 38: Widget Factory - Nemikor Blog

progress: function(value) {value = this._constrain(value);this._setData('value', value);this._trigger('change', null, {

value: value});

}

Page 39: Widget Factory - Nemikor Blog

progress: function(value) {value = this._constrain(value);var data = { value: value };if (false !==

this._trigger('change', null, data)) {this._setData('value', value);

}}

Page 40: Widget Factory - Nemikor Blog

progress: function(value) {value = this._constrain(value);var data = { value: value };if (false !==

this._trigger('change', null, data)) {this._setData('value', value);

}}

Page 41: Widget Factory - Nemikor Blog

Cleaning up

Page 42: Widget Factory - Nemikor Blog

destroy: function() {this.element.text('');$.widget.prototype.destroy.call(this);

}

Page 43: Widget Factory - Nemikor Blog

Even more goodies

Page 44: Widget Factory - Nemikor Blog

What's planned for the future?

Page 45: Widget Factory - Nemikor Blog

$.widget('ui.progressbar', {options: { value: 0 },_init: function() {

this.element.text(this.options.value + '%');

}});

Page 46: Widget Factory - Nemikor Blog

$.widget('ui.complexDialog',$.ui.simpleDialog, {_init: function() {

// …}

});

Page 47: Widget Factory - Nemikor Blog

Go build some widgetsGive us some feedback

Page 48: Widget Factory - Nemikor Blog

http://nemikor.com@scott_gonzalezhttp://speakerrate.com/scott.gonzalez

$(λ);

Page 49: Widget Factory - Nemikor Blog

1

jQuery UI

Widget Factory

Page 50: Widget Factory - Nemikor Blog

2

Scott GonzálezjQuery UI development leadhttp://nemikor.com@scott_gonzalez

$(λ);

Hi. My name is Scott González and I'm the development lead of jQuery UI.

I'm going to be talking about jQuery UI's widget factory and showing off some of its lesser-known features.

I'll also be showing how to use the widget factory to build your own plugins.

Page 51: Widget Factory - Nemikor Blog

3

The widget factory- What is it?- Why do we need it?- How do we use it?

$.widget();

Many of you are probably unaware that the widget factory even exists.

So I'll be explaining why we needed it for jQuery UI and how we use it to solve some of the problems we faced.

Page 52: Widget Factory - Nemikor Blog

4

Why we needed the widget factory

We'll start with some background info about why jQuery didn't solve our problems out o the box.

Page 53: Widget Factory - Nemikor Blog

5

jQuery revolutionizedthe way we work with the DOM

$('p').show();

jQuery revolutionized the way we work with the DOM.

The "fi nd elements, do something" philosophy, along with chaining, has changed the way we approach tasks and made our lives easier.

Page 54: Widget Factory - Nemikor Blog

6

jQuery excels at simple tasks

$('p').fadeOut();

A large part of jQuery's success comes from the fact that jQuery is based on, and excels at, performing simple tasks.

I've heard people describe jQuery by saying, "The way you do something in jQuery is to think about the word that describes what you want to do; and then you write it and it just works."

Page 55: Widget Factory - Nemikor Blog

7

$('#notification').text('Your file has been uploaded').show().fadeOut(2000);

Usually we want to do something more complex.

Luckily, with jQuery, performing a complex task is nothing more than chaining together several simple tasks.

:: describe code functionality ::

Page 56: Widget Factory - Nemikor Blog

8

$('a.user').click(function() {var url = this.href;$('#user-info').load(url);return false;

});

Even ajax and progressive enhancement are dead simple with jQuery, as we can see in this example.

:: describe code functionality ::

Page 57: Widget Factory - Nemikor Blog

9

What about stateful widgets?

$('div').dialog();

However, all core methods are essentially stateless.

So what happens when you build a plugin that needs to manage state?

Page 58: Widget Factory - Nemikor Blog

10

$.ajax({url: '/foo.html',beforeSend: function() { … },success: function() { … },error: function() { … }

});

The closest example to a stateful widget in jQuery core is probably the ajax method.

The XMLHttpRequest that it generates has state and jQuery.ajax allows you to bind callbacks to events that occur based on state changes to the request.

:: describe code functionality ::

If we follow this pattern to build a stateful widget, everything seems fi ne at fi rst glance.

Page 59: Widget Factory - Nemikor Blog

11

$(elem).dialog({modal: true,open: function() { … },beforeClose: function() { … },close: function() { … }

});

:: describe code functionality ::

However, we'll quickly notice that something's missing: jQuery does not provide methods for modifying the options or state after the call to jQuery.ajax has been made.

This makes sense when dealing with XHR requests, but would be unacceptable for UI widgets.

In this example, we would have no way to programatically open or close the dialog.

Page 60: Widget Factory - Nemikor Blog

12

How do we handle initializationand function calls?

$(elem).dialog('open');

So we're left with the question “how do we handle initialization, function calls and state management?”

Page 61: Widget Factory - Nemikor Blog

13

The widget factory:A solution to state management

We knew that whatever solution we came up with would be able to be applied to all of our plugins.

So we built the widget factory to handle all of the common functionality based around state management.

Page 62: Widget Factory - Nemikor Blog

14

Defning an API

As I just mentioned, the overarching question behind this whole process was “how do we handle initialization and function calls?”

So, the fi rst step in building the widget factory was deciding what the exposed API should look like.

Page 63: Widget Factory - Nemikor Blog

15

$(elem).dialog({

draggable: false,modal: true

}).dialog('open');

Most of you are probably aware of the jQuery UI concept.

You call a plugin with no parameters or a set of options to initialize the plugin.

After initialization, you can call the plugin and pass a method name as a string to execute the method on the plugin instance.

Show of hands: how many people like the jQuery UI model for initialization and function calls?

What else can we do?

Page 64: Widget Factory - Nemikor Blog

16

$(elem).createDialog({

draggable: false,modal: true

}).openDialog();

We can expose many functions in the jQuery.fn namespace.

Unfortunately, this pollutes jQuery and creates multi-word function names.

Page 65: Widget Factory - Nemikor Blog

17

$(elem).dialog({

draggable: false,modal: true

}).dialogOpen();

We can do a little better than this by using a common prefi x for all functions, but it's essentially the same as the previous model.

Page 66: Widget Factory - Nemikor Blog

18

$(elem).dialog({

draggable: false,modal: true

}).open();

Another option is to return the plugin instance, either on initialization or on subsequent calls.

However, this breaks chaining and only works with one element at a time.

A similar implementation has been suggested where the plugin instance would actually be a modifi ed version of a jQuery instance.

This solves the problem of only working with one element, but can be confusing to have different methods available based on context and it increases the chances of name collisions.

Page 67: Widget Factory - Nemikor Blog

19

var dialog = new $.dialog(elem, {draggable: false,modal: true

});dialog.open();

We could also create a purely object oriented implementation that doesn't go through jQuery at all.

While this may not seem very jQuery-like, it may actually be the most convenient model when building complex pages or applications because it's the most direct.

Page 68: Widget Factory - Nemikor Blog

20

Any other models?

Does anyone have any other methods they've used or ideas they've been thinking over for dealing with stateful widgets?

Let's do another show of hands for some of the more common approaches:

- pass method name as string- return plugin instance- direct instantiation with OOP

Page 69: Widget Factory - Nemikor Blog

21

jQuery UI actually implementsmany of these models.

$(elem).data('dialog');

jQuery UI actually implements three of the models, with some minor caveats.

Page 70: Widget Factory - Nemikor Blog

22

$(elem).dialog(options);

$(elem).dialog('open');

We've got the model that we've been publicly supporting for years.

We expose a single jQuery method that is used for both initialization and method calls.

Page 71: Widget Factory - Nemikor Blog

23

$(elem).dialog(options);

$(elem).data('dialog').open();

We also support getting the plugin instance after initialization.

The caveat here is that we never return the plugin instance through the plugin method, you have to get it yourself using the data method.

Page 72: Widget Factory - Nemikor Blog

24

var dialog = new $.ui.dialog(elem, options);dialog._init();

dialog.open();

Finally, we support pure OO use of our widgets.

The caveat here is that you have to call the init method yourself after calling the constructor.

Page 73: Widget Factory - Nemikor Blog

25

How does it work?

$.ui.dialog.prototype

So, how does this all work?

All jQuery UI widgets have a constructor and a prototype.

When you initialize a plugin, it constructs a new plugin instance and stores it on the element using jQuery.data().

Then subsequent plugin calls are delegated out to the plugin instance.

Page 74: Widget Factory - Nemikor Blog

26

$.ui.dialog.prototype.draggable =function(toggle) {

return this.option('draggable', toggle);

};$(elem).dialog('draggable', true);

This means that it's extremely easy to add or modify the functionality of a widget.

:: describe code functionality ::

Because the widget factory just delegates all plugin calls to the plugin instance, new methods are automatically available through the exposed jQuery method.

Page 75: Widget Factory - Nemikor Blog

27

Using the widget factory

At this point we know the main problem that we need to solve for creating stateful widgets.

We've also seen how the widget factory addresses these issues.

So let's take a look at how to use it to build our own plugins.

We'll walk through some example code with an extremely simple progress bar plugin.

Page 76: Widget Factory - Nemikor Blog

28

The bare minimum

We'll start with the bare minimum code needed to make a working plugin.

Page 77: Widget Factory - Nemikor Blog

29

$.widget('ui.progressbar', {_init: function() {

this.element.text(this.options.value + '%');

}});$.ui.progressbar.defaults = { value: 0 };

To create the plugin, we call jQuery.widget and pass two parameters: the name of the plugin, including the namespace, and the prototype.

The widget factory creates a constructor, based on the plugin name, and calls the _init method whenever a new plugin instance is created.

You can also set default values for all of the plugin's options as shown here.

The jQuery.ui.progressbar namespace is created for us by the widget factory (this is actually the constructor that it generates).

Page 78: Widget Factory - Nemikor Blog

30

$(elem).progressbar();

$(elem).progressbar({ value: 25 });

<div class=“{progressbar:{value:10}}”>$('div').progressbar();

At this point we can initialize our plugin, with or without options.

There are a few different ways to set the options for a plugin during initialization. The options are determined by looking at the default values, then any metadata provided and fi nally the options hash passed in.

The values are combined using a deep extend and use of metadata requires the metadata plugin.

The merging happens before the _init method is called, so you don't even need to think about this when building a plugin.

It's a good idea to make all options truly optional by providing the most common settings as defaults.

Page 79: Widget Factory - Nemikor Blog

31

Working with options

The options used to confi gure the plugin are essentially the heart of our plugin's state.

So now that we have a working widget, we'll want to support changing the options after initialization.

Page 80: Widget Factory - Nemikor Blog

32

$(elem).progressbar();

$(elem).progressbar('option', 'value');

$(elem).progressbar('option', 'value', 5);

We can see here that there's an option method that allows you to get and set individual options. This is defi ned in the base widget prototype which our progress bar plugin extends.

The option method also accepts a hash of key/value pairs, just like the css and attr methods.

Page 81: Widget Factory - Nemikor Blog

33

_setData: function(key, value) {this.options[key] = value;if (key == 'value') {

this.element.text(value + '%');}

}

To add custom functionality, we can implement a _setData method, which takes a key and value.

Here we're reacting to the user setting the “value” option by updating the text of our progress bar.

We also update the options hash to make sure that it always represent the current state of the plugin. This makes it easy for users to query the plugin for its current state.

Page 82: Widget Factory - Nemikor Blog

34

Adding methods

Now that we're able to control the plugin's state through it's options, we'll probably want to add some methods to control its behavior.

Page 83: Widget Factory - Nemikor Blog

35

progress: function(value) {this._setData('value',

this._constrain(value));},_constrain: function(value) {

return Math.max(0,Math.min(value, 100));

}

We can add methods to our plugin just by defi ning them in the prototype that we pass to jQuery.widget.

Any functions that start with an underscore cannot be called through the plugin's exposed method. However, if the user accesses the plugin instance directly they can still execute these functions.

:: describe code functionality ::

Page 84: Widget Factory - Nemikor Blog

36

progress: function(value) {if (this.options.disabled) {

return;}this._setData('value',

this._constrain(value));}

The base widget prototype also adds a disabled option, along with enable and disable methods.

The enable and disable methods are just convenience methods that delegate out to the _setData method. The default implementation will also add or remove a disabled class, in this case ui-progressbar-disabled.

Page 85: Widget Factory - Nemikor Blog

37

Creating callbacks

At this point we have a lot of functionality built into our plugin with just a little bit of code, but we still need to provide a way for users to customize the behavior of our plugin.

Adding callbacks to your plugins is one of the easiest ways to make your plugins extensible.

Page 86: Widget Factory - Nemikor Blog

38

progress: function(value) {value = this._constrain(value);this._setData('value', value);this._trigger('change', null, {

value: value});

}

To execute a callback, you can call the _trigger method on your plugin. This will execute a callback as well as trigger an event.

The callback can be specified in the options and events can be bound on the element or any parent since the event bubbles. The name of the event will be the name of the callback prepended with the name of the plugin, to prevent name collisions with other events.

:: explain parameters for _trigger ::

Page 87: Widget Factory - Nemikor Blog

39

progress: function(value) {value = this._constrain(value);var data = { value: value };if (false !==

this._trigger('change', null, data)) {this._setData('value', value);

}}

In many cases it will make sense to allow the user to prevent an action from occurring, e.g., preventing a dialog from closing. To accommodate this, the _trigger method will return false if the user returns false or calls event.preventDefault() in an event handler or the callback.

It wouldn't really make sense to prevent changing the progress of progress bar, but we've implemented it here just for reference.

Page 88: Widget Factory - Nemikor Blog

40

progress: function(value) {value = this._constrain(value);var data = { value: value };if (false !==

this._trigger('change', null, data)) {this._setData('value', value);

}}

In many cases it will make sense to allow the user to prevent an action from occurring, e.g., preventing a dialog from closing. To accommodate this, the _trigger method will return false if the user returns false or calls event.preventDefault() in an event handler or the callback.

It wouldn't really make sense to prevent changing the progress of progress bar, but we've implemented it here just for reference.

Page 89: Widget Factory - Nemikor Blog

41

Cleaning up

In many cases it will make sense to allow a user to apply and then later unapply a widget.

So we need a way to undo whatever our plugin has done.

Page 90: Widget Factory - Nemikor Blog

42

destroy: function() {this.element.text('');$.widget.prototype.destroy.call(this);

}

This is exactly what the destroy method is for.

This is useful for reverting changes such as unbinding event handlers or removing classes.

The destroy method is automatically called if the element is removed from the DOM, so this acts as a form of garbage collection as well.

Page 91: Widget Factory - Nemikor Blog

43

Even more goodies

There are a few more features that the widget factory provides and some details that are worth learning, but are a bit outside the scope of this talk.

Page 92: Widget Factory - Nemikor Blog

44

What's planned for the future?

While the widget factory has been around for a while, it's still evolving. We're working on making it even easier to use and adding some more functionality.

Page 93: Widget Factory - Nemikor Blog

45

$.widget('ui.progressbar', {options: { value: 0 },_init: function() {

this.element.text(this.options.value + '%');

}});

For example, we're moving the default options into the prototype so you don't need to do additional setup after your call to the widget factory.

Page 94: Widget Factory - Nemikor Blog

46

$.widget('ui.complexDialog',$.ui.simpleDialog, {_init: function() {

// …}

});

We're also adding in inheritance, so extending an existing widget is as simple as passing that widget in your call to the widget factory.

Page 95: Widget Factory - Nemikor Blog

47

Go build some widgetsGive us some feedback

Page 96: Widget Factory - Nemikor Blog

48

http://nemikor.com@scott_gonzalezhttp://speakerrate.com/scott.gonzalez

$(λ);