In this Nettuts+ tutorial, we are going to learn how to create a dynamic content editing system using the jQuery UI Widget Factory. We'll go over how to develop a logical, object orientated jQuery UI Widget, transform various nodes to editable text fields, delegate events within the widget framework, manage context, toggle icons, serialize data, and of course edit, restore and delete data using jQuery's fantastic AJAX functionality!
Creating a Widget Blueprint
The first thing we'll do is set up a blueprint for our widget which we'll name "editable" and whose namespace will be "ui". The widget's name will be the first argument when calling the widget factory method. The second argument will be an object literal containing the various properties and methods for the widget itself. It's a very simple, clean and efficient way to churn out plug-ins. There are a number of inherent props and methods to a UI widget. We'll be using the following vanilla $.widget
props:
-
Options{...}
This object literal is the only property of our widget that will be exposed to users. Here, users will be able to pass their own arguments to the widget which will, of course, override any default options we specify. Examples include callbacks, element selectors, element classes etc.
-
_Create()
This is the first method called during the widgets initialization and it's only called once. This is our "set up" method, where we'll create / manipulate elements in the DOM as well as bind event handlers.
-
_Destroy()
This method is the only our widget's only public method in that users can specify "destroy" as an argument to calling the widget. This method is used to tear down / destroy an instantiated widget. For example, we'll be using destroy to remove any HTML generated by our widget, unbind event handlers and of course, to destroy the instance of the widget relative to the element it was called on.
Here's What We'll Be Using:
$.widget("ui.editable", { options: {}, _create: function(){}, destroy: function(){} }
Take note of the underscore which precedes the _create
method. Underscores denote a private / internal method which is not accessible from outside the widget.
For more information on UI Widget development, as well as the other inherent methods such as _trigger(), _getData(), _setData() and so you can check the following great resources:
- jQuery UI Widget Factory Development Wiki
- jQuery UI API Developer Guide
- Tips for Developing jQuery UI 1.8 Widgets by Eric Hynds
- jQuery UI Widget upgrade guide for jQuery 1.8
- jQuery Forums - A great post by Richard D. Worth, Project Lead for the jQuery UI and a brilliant developer, contrasting and explaining the _init and _create, methods and their uses.
Brainstorming: What Methods Do We Need? What Does Our Widget Need to Do?
As we're creating a content editing widget we'll obviously need some way to edit content. Additionally, we'll want the ability to delete content as well as cancel revisions. We'll also need "edit" triggers, say icons which, when clicked, will toggle the state of our content as well as toggle state between default and editor icons. With this functionality in mind, let's define some general methods now:
-
_CreateEditor()
Typically, it's good to separate logic in a logical, clean easy to read manner. Although most of the DOM creation and insertion can be done within the _create() method itself, since we'll be creating editors for multiple sets of content (i.e. table rows) let's use an independent method which we can call from a for loop! The _createEditor() will be called to handle the creation and insertion of the core editor elements. This separation of logic should make the widget easier to understand at a glance as well as help us maintain our code down the road.
-
_ClickHandler()
This method speaks for itself, it's the handler which will be used when the user clicks on one of our "editor" icons. Although we could bind a handler to each individual icon type let's just use a single handler and a case switch to call appropriate methods based on which icon was clicked. There are minor benefits and drawbacks to binding a single handler for all icons and individual handlers for each icons so it's really just a matter of preference and performance.
-
_ShowIconSet()
This is the method will be used to change our icons from their "default" state to their "editor" state. As mentioned above, we'll want the ability to edit / delete an item as well as save / cancel changes. We only need to display two icons at any given time (edit/remove and save/cancel, respectively) so this method will toggle our icons between those two states.
-
_TransformElements()
Here's where the core logic of transforming elements back and forth from their original state to editable fields takes and vice versa. This method will be used when we make elements editable, save changes or cancel a change. This method will also be responsible for populating / repopulating the element / input with the relevant text.
-
_RemoveElements()
This method will be responsible for removing elements from the DOM when an item is deleted by the user.
-
_Encode()
As we'll be saving / deleting data with jQuery's powerful AJAX functionality, we'll need a way to convert these text nodes and form inputs, along with their corresponding "field names", into a standard URL-encoded string. As such, this method will handle the serialization of text / input values.
-
_Post()
This method will handle all of our AJAX logic. It will be responsible for both saving and deleting data and it's callbacks will be used to update the DOM when appropriate. Additionally, any user defined AJAX callbacks will be fired from here as well.
Here's a Look at Our Widget's Blueprint So Far:
$.widget("ui.editable", { options: {}, _create: function(){}, _createEditor:function(){}, _clickHandler: function(event){}, _showIconSet: function(){}, _transformElements: function(){}, _removeElements: function(){}, _encode: function(){}, _post: function(){}, destroy: function(){} });
Above, notice that most all of our methods are preceded with an underscore. The underscore denotes a private method which means that any property or method preceded by an underscore will not be accessible from outside the widget. There's not much need to make any of our methods public, so we'll proceed them with an underscore.
Full Screencast
Options, Options and More Options!
Now, let's define some user overridable options for our widget! This is always a fun step as there are a plethora of really interesting and neat things that can be done here. For example, creating callbacks. We can create optional callbacks for nearly anything, saving data, deleting data, errors, success, completion, events etc. Additionally, we can easily define the context of these callbacks. So, want to animate the color of a row after a successful update? No problem! Want to have each field blink in rapid succession as a visual confirmation? Easy as pie! This, for me, is one the most fun and powerful features of any given widget. The ability to mold it's functionality and interactivity to suit the users needs.
Additionally, we'll also be defining some important core options such a selector for those elements we want to make editable, where we want to insert our icons, custom class names as well as some other AJAX options. Here's a look at our options which is an object literal:
<strong>options: {...}</strong>
Some General Options
Here, we'll define some core options for our widget such as element selectors, class names, field names and so on...
-
Elements:
This will be a selector string for those elements we want to make editable. We'll use a selector for table cell elements
"td"
as the default selector. This can be overridden, though. Any valid jQuery selector string would work. For example:- "td"
- "td>small"
- ".editable-content"
- ".editable p"
- "div:has(p)"
For more information on jQuery selectors, see the jQuery Selectors API
-
elementParent:
Here, we'll allow the user to define a selector which will server as the parent container for sets of editable content. You could almost think of this as sort of pseudo fieldset used to group inputs together or even a table row which groups cells together. This is useful if the user wants to make something other than table rows editable.
As such, this selector is very important. We'll be looping through each
elementParent
while creating, appending, binding, and caching elements! As with theelements
property, any valid jQuery selector will work here within the context of the element upon which the widget was called. As a default value, we'll usetbody tr
to select all of the table rows within the table body.Note: In case your wondering why we don't specify the parent container by doing something like this:
this.element.parent();
Consider that the immediate parent of the element may not be the mutual parent of all the editable nodes for a given row / block. What if the nodes were editing are nested within another element? By specifying an implicit parent container we know that we are dealing, specifically, with the appropriate top level container for all of the editable content within a given row / block.
-
insertMethod:
Here, we'll allow the user to define which jQuery insert method will be used for appending the icons and their container. A valid jQuery insert method string, such as
appendTo
,prependTo
,insertBefore
orinsertAfter
should be specified here. The default insert method will beappendTo
. -
Classes:
Allowing the user to specify their own / additional class names opens up a slew of new possibilities such as using a css framework, like the jQuery UI CSS Framework, or perhaps icon classes from an image sprite they've created! Here, the user can specify additional class names, as a string, for a number of different elements. We'll set some default classes for the icons using jQuery UI Themeroller icon classes. So,
classes
will be an object literal with properties as follows:- iconContainer: A space separated string representing additional classes for the parent element of the icons
- edit: A space separated string representing additional classes for the edit icon
- remove: A space separated string representing additional classes for the remove icon
- save: A space separated string representing additional classes for the save icon
- cancel: A space separated string representing additional classes for the cancel icon
- th: A space separated string representing additional classes for the table header element which will be created conditionally
For more information on the jQuery UI Themeroller CSS Framework see the jQuery UI Themeroller API as well as the jQuery UI Themeroller
-
fieldNames:
This option will be a simple array of representing the numerically indexed field names of the content which is being made editable. So, for example, if the editable content represents "last-name", "first-name" and "address" fields, we would specify those field names in chronological order, the order in which these items are displayed in the DOM, from first to last, ["last-name","first-name""address"]. For example:
fieldNames: ["last-name", "first-name", "address"]
We'll initialize an empty array as the default value.
fieldNames: []
-
Action:
This option will is an object literal used to specify the pseudo
action
property for our AJAX requests. In simpler terms, it will contain the url strings to which jQuery's$.ajax
will submit. It will contain two properties:-
Edit:
The url string used when editing content.
-
Remove:
The url string used when deleting content.
As default values, we'll use javascript's
location.href
property which is the url of the current page:action : { edit : location.href, remove : location.href }
-
-
ajaxType:
This string will determine the type of
$.ajax
request we'll use. The default method is ispost
but the user can specify something else should the need arise.
Callbacks
Here, we'll define some empty functions / placeholders for user defined AJAX callbacks...
-
Create:
This optional callback will be called at the end of the
_create
method in the context ofthis.element
. -
Destroy:
This optional pre-callback will be called at the beginning of the
destroy
method also in the context ofthis.element
. -
ajaxBefore:
This optional callback will be called before an AJAX request begins in the context of the editable contents shared parent. This can be used for a number of things, such as, for example, displaying an AJAX loader gif.
-
ajaxError:
This optional callback will be called if there is an error in the AJAX request in the context of the editable contents shared parent. With this callback, you give some feedback back to the user letting them know that their post did not complete.
-
ajaxSuccess:
This optional callback will be called, again in the context of the editable contents shared parent, if there is AJAX call request successfully. This can, as above, be used for a number of different things such as letting the user know the request was a success.
-
ajaxComplete:
This optional callback will be called when AJAX request is complete - regardless of success or error also in the context of the editable contents shared parent. This could, for example, be used to remove an AJAX loader gif signifying the request is complete.
Let's Have a Look at the Widget Now That We Have Our Options Defined:
$.widget("ui.editable", { options: { elements : "TD", elementParent : "TBODY TR", insertMethod : "appendTo", classes : { iconContainer : "", edit : "ui-icon ui-icon-pencil", remove : "ui-icon ui-icon-closethick", save : "ui-icon ui-icon-check", cancel : "ui-icon ui-icon-arrowreturnthick-1-w", th: "" }, fieldNames : [], ajaxType : "post", action : { edit : location.href, remove : location.href }, create : null, destroy : null, ajaxBefore : null, ajaxError : null, ajaxSuccess : null, ajaxComplete : null }, _create: function(){}, _createEditor:function(){}, _clickHandler: function(event){}, _showIconSet: function(){}, _transformElements: function(){}, _removeElements: function(){}, _encode: function(){}, _post: function(){}, destroy(){} });
Creating, Appending, Binding and Caching With the _Create Method!
Having defined the blueprint of our widget, as well as it's options, our next step will be to _create the widget itself! This method is called only once, at the widgets initialization.
In the _create
method, we'll first define a number of internal props (read: properties) to be used later, within the widget. Essentially, we'll be caching selectors and booleans as well as defining class names and such. Then, we'll proceed to element creation, insertion and caching as well as adding classes and binding an event handler.
<strong>_create: function(){}</strong>
Defining Some Properties
As mentioned above, the first step in _create
will be to define some properties. Let's go through them, one at a time:
-
this._element:
This property will be the jQuery selector representing the element which was used to instantiate the widget
this._element = $(this.element),
-
this._table:
This boolean property will use a ternary operator to determine if
this._element
is a table. If this property evaluated to true, later on, we'll create a table header element so that the number of columns in thethead
andtbody
correspond.For more information on ternary / conditional operators see the Mozilla Developer Network or MDN for short
this._table = (this._element.is('table') ? true : false);
Note: Rather than using a ternary here we could have just used the "this._element.is('table')" expression as it returns a boolean value. It's better to just cache the boolean here rather than repeatedly calling the
is
method. This is also a lot more readable. -
this._elementParent:
Using jQuery selector string from the options argument, we'll cache a jQuery selector for the editable contents shared parent element within the context of
this._element
this._elementParent = $(this.options.elementParent, this._element),
-
this._editable:
Here, we'll initialize an empty array which will, shortly, be populated with jQuery collections of editable content relative to the index of their parent container. The array key will directly correspond to the index of the parent element. In other words,
_editable[0]
will be the jQuery collection of all editable content within_elementParent[0]
. This is a nifty / speedy way to cache editable collections for use later on.this._editable = [],
-
this._triggers:
This will be an object literal containing the "action" classes for our editor icons. I call them action classes as these classes will be used to determine what action should be taken in our event handler.
In defining these classes, we'll use another property provided by widget framework,
this.widgetBaseClass
. This property always returns a base class for the widget, which, in this case, will beui-editable
.So, essentially, we'll concatenate the
widgetBaseClass
with appropriate class names for each of our icons. These triggers classes are independent of any other classes the icons may have. Although they may be used to style the icons with css, they are primarily used in our event handler.For more information on string concatenation see the Mozilla Develops Network.
this._triggers = { edit : this.widgetBaseClass + "-edit", remove : this.widgetBaseClass + "-remove", save : this.widgetBaseClass + "-save", cancel : this.widgetBaseClass + "-cancel" },
-
this._classes:
Not unlike the
_triggers
property above,this._classes
will also be an object literal of class names. The difference here is that_classes
will contain a concatenation of predefined and user defined class names.Using a ternary operator, we'll concatenate base classes with any optional classes specified in the
options.classes
prop.this._classes = { editable : this.widgetBaseClass + '-element', iconContainer : this.widgetBaseClass + "-icons" + (this.options.classes.iconContainer ? " " + this.options.classes.iconContainer : ""), tableHeader : this.widgetBaseClass + "-header" + (this.options.classes.th ? " " + this.options.classes.th : ""), edit : this._triggers.edit + (this.options.classes.edit ? " " + this.options.classes.edit : ""), remove : this._triggers.remove + (this.options.classes.remove ? " " + this.options.classes.remove : ""), save : this._triggers.save + (this.options.classes.save ? " " + this.options.classes.save : ""), cancel : this._triggers.cancel + (this.options.classes.cancel ? " " + this.options.classes.cancel : "") },
Adding Classes to the Editable Content
Now that we've defined a number of properties, we're ready to add the appropriate class to our editable content.
This step is as simple as simple can be. We'll use a jQuery selector for all of the editable content in the context of this._elementParent
. We'll define the selector with the string given to us in the options.elements
argument. Then, using jQuery's addClass
method, we'll add the class name we cached a moment ago in the _classes
object.
-
$(this.options.elements, this._elementParent).addClass(this._classes.editable);
For more information on jQuery's addClass
method see the jQuery API
Creating Our Editor and Binding Away!
The icons and their containers will be created as we loop through our _elementParent
collection.
In other words, each _elementParent
will have it's own set of icons.
We'll also cache selectors, in context, for these icons, their container as well as the editable content relative to the current _elementParent
in the loop.
-
A Few Variables for the Loop
Before we being our
for(){...}
loop, we'll initialize a variable for the initial expression, which is the iterator which is incremented is the loop. We'll then define an integer representing how many elements are cached in the_elementParent
collection, using jQuery'ssize
method.var i, total = this._elementParent.size();
For more information on
for
statements see the Mozilla Developers Network
For documentation of the jQuerysize
method see the jQuery API -
Caching and Creating the Icons and Their Container:
We'll be populating the empty
_iconContainer
array here by assigning the return value of the_createEditor
method to it as a new prop,i
. As you'll see, momentarily,_createEditor
will return the jQuery selector for the newly created icon container.So, since
i
is the equal to the index of the current_elementParent
in our loop,_iconContainer[i]
will be the container for our icons within_elementParent[i]
only!As we iterate, we'll be using the iterator,
i
as the array key for several jQuery collections including the_editable
array, the_icons
array as well as the_iconContainer
array.this._iconContainer[i] = this._createEditor(i);
Note: The array keys for the
_editable
,_iconContainer
and_icons
collections will always correspond to the index of the_elementParent
in the current iteration of thefor
loop. -
Caching an Editable Selector in Context
In similar manner to the
_iconContainer
array, we'll now cache a selector for all editable content in the context of_elementParent[i]
.We'll define the editable content's selector string by concatenating a
.
with the appropriate class name stored in our_classes.editable
prop.this._editable[i] = $('.' + this._classes.editable, this._elementParent[i]);
-
Binding an Event Handler to Our Icons
The last thing we'll do in our
for
loop is bind a handler to the click event of our newly created icons. As were binding a handler within the context of a jQuery UI Widget / Object, we'll need to pay special attention here as the context ofthis
in a typical jQuery event handler, refers to the element which received the event. This is one of jQuery's nicest features as it standardizes whatthis
is across all browsers. (I'm lookin at you IE ;)So, here are two simple approaches to handling context within a widget event handler.
-
event.data
First and foremost, we can simply pass this instance of our widget to the handler as an
event.data
argument which is provided by some of the various jQuerybind
type methods. Essentially, the second argument, which proceeds the function call, can be an object literal of data we want to pass to the handler. In the handler, this data is accessible in theevent
object as theevent.data
property! Here's how it looks:this._icons[i].bind('click', {widget:this, i:i}, this._clickHandler);
So,
event.data.widget
will be a reference to our widget instance andevent.data.i
will be a reference to our iterator!Passing an event.data argument is great when you don't want to alter the context of
this
within your handler. Additionally, it doesn't require another function call as the next method,$.proxy
, does. -
$.proxy()
As stated in the jQuery API, "This method is most useful for attaching event handlers to an element where the context is pointing back to a different object."
In other words, by proxying our widget to the handler,
this
will refer to our widget instance rather than the element which received the event.Even though the target element would no longer be assigned to
this
, it would still be accessible in jQuery'sevent
object as theevent.target
property. As many developers are already in the habit of caching a selector for$(this)
in event handlers, it wouldn't be to much of a stretch to cache an$(event.target)
selector instead.Without further adieu, here's the $.proxy method in action:
this._icons[i].bind('click', $.proxy(this, this._clickHandler);
-
-
Binding a handler using the
event.data
argumentWith an understanding of both
event.data
and$.proxy
, let's use theevent.data
argument to pass our widget and iterator along to the handler.this._icons[i].bind('click', {widget:this, i:i}, this._clickHandler);
Let's have a look at the logic of our for
loop now, in its entirety:
var i, total = this._elementParent.size(); for(i=0; i<total; i++){ this._iconContainer[i] = this._createEditor(i); this._editable[i] = $('.' + this._classes.editable, this._elementParent[i]); this._icons[i].bind('click', {widget:this, i:i}, this._clickHandler); };
What About Table Headers?
With the icons created, inserted and bound we'll need to make a minor adjustment if this.element
is a table.
Although we haven't defined the _createEditor
method yet, the type of icon container element it creates is relative to what this.element
is.
So, if were working for a table, it will create a td
and append it to the table. If were working any other type of element, it will create a div
.
As such, appending another td
to a table row leaves us with an incomplete table head as there will be more tables cells in the body than there are table headers in the head.
So, we'll need check if this.element
is a table or not and if it is we'll create, populate and append, using the insertMethod
specified in the options
argument, a th
element. Although creating DOM elements with jQuery is simple and clean I sometimes like to write it out in plain javascript. Let's take a look at the logic:
-
Checking for a Table
First, we'll check our
table
property which, as you may remember, is a boolean representing whether or notthis.element
is a table.if(this._table){ }
-
It It's a Table, Create a Header!
If
this.element
is a table, we'll need to create and append a newth
to the table head's first and hopefully only table row, here's how we'll do it:-
Creating the table header
If
this.element
is a table, we'll create a newth
element as the variableth
.if(this._table){ var th = document.createElement('th'); }
-
Giving it some class!
Next, we'll set the
className
property for this element as thetableHeader
class name specified in the_classes
object.if(this._table){ var th = document.createElement('th'); th.className = this._classes.tableHeader; }
-
Setting the
innerHTML
of the the table headerThen, we'll set the
innerHtml
, which in this case is just text, of this element.if(this._table){ var th = document.createElement('th'); th.className = this._classes.tableHeader; th.innerHTML = "Edit"; }
-
How about a little
style
?We'll specify one css rule,
text-align
ascenter
. This will center the header text and look nice as the icons themselves will also be centered via css.if(this._table){ var th = document.createElement('th'); th.className = this._classes.tableHeader; th.innerHTML = "Edit"; th.style.textAlign = "center"; }
-
Appending the table header to the table head
Lastly, using the jQuery insertMethod specified by the user, we'll dynamically insert the element into the table header of our table. Take note of our DOM insertion:
if(this._table){ var th = document.createElement('th'); th.className = this._classes.tableHeader; th.innerHTML = "Edit"; th.style.textAlign = "center"; $(th)[this.options.insertMethod]('thead tr:first', this._element) }
Did you notice that we have our insert method in square brackets right after the jQuery selector?
You may be wondering what this is or how it works. Basically, think of jQuery as one big (awesome) object. As an object, you can use associative array / square bracket style notation with it! This shorthand allows us a great deal of flexibility in calling methods dynamically based on relative conditions!
-
How About a Callback!
The last thing we'll do in the _create
method is call the user defined create
callback if it's been specified.
There are a plethora of reasons to use a callback in the _cerate method. Here's one example!
Let's say the user, with a success callback, is changing the color of table cells to reflect a successful update. Should the widget be destroyed, they'll need some way to restore the color of their table cells. Of course, they could implicitly set the color manually in the destroy callback but how about saving the table cell color using jQuery's $.data
method which is a much more dynamic approach! That data would be attached to this.element
as that will be the context of our callback.
create: function(){ var color = $("td", this).css("color"); $.data(this, "color", color); }
Thus, when the widget is destroyed, the user could simply set the color back to it's default value by accessing the appropriate $.data
destroy: function(){ var color = $.data(this, "color"); $("td", this).css("color", color); }
Widget Callbacks Explained
As user defined callbacks are optional, their default values will always be set to null
.
As such, we'll use jQuery's isFunction
method to determine if the callback's type is function
and then call it if it is.
Alternately, we could use javascript's native typeof
operator to determine if the typeof
callback is a function. We'd have to enclose the typeof
operator in parens so that we could take it's return value and convert it lower case, using javascript's native toLowerCase
method, so as to standardize the output. It's much simpler, though, to use jQuery's isFunction
method but here's a look at this technique in vanilla javascript:
-
if((typeof this.options.create).toLowerCase() === "function"){...}
Here's the how we'll do it with jQuery:
if($.isFunction(this.options.create)) this.options.create.call(this.element);
So, if the callback is a function, we'll execute it using javascript's native call
method.
The call
method allows us to specify the context of our callbacks. In other words, it will define what this
refers to within the callback function.
As such, we'll use this.element
as the context here which will define this
as the the element to which our widget was instantiated.
Inside the _createEditor Method
In the previous section we discovered the logic of how we'll create elements. Now, let's a take a look at the implementation. The _createEditor
method will be called from within the _create
methods for
loop to do all the element creation and insertion relative to the index of the current _elementParent
in the loop.
<strong>_createEditor: function(i){}</strong>
Arguments
The _createEditor
method takes one argument, i
, which is equal to the index of the current _elementParent
from the _create
methods for
loop.
Initializing a Few Variables
The first thing we'll do here is initialize a few variables which we'll use in our element creation. We'll a initialize a variables for the edit icon as well as the remove icon.
var edit, remove;
Creating Our Icons
With these variables initialized, let's go ahead and create our icons using some simple javascript! Essentially, we'll use the same techniques as we used for the conditional table header but with one exception. Rather than setting the innerHtml
of the link we're creating, we'll be setting the title attribute instead. This way, on mouseover, the user we'll see a tooltip description of what the icon does! Here's a look at our anchor / icon creation:
edit = document.createElement('a'); edit.className = this._classes.edit; edit.title = 'Edit'; remove = document.createElement('a'); remove.className = this._classes.remove; remove.title = 'Remove';
Creating the Icon Container
With the icons ready to go, we'll need to create a container for them. The element type of this container will depend on whether or not we're working with a table. So, using the _table
boolean we defined in the _create
method and a ternary operator we'll create a td
element if we're working with a table and div
element if we're working with anything else. Then, we'll set the container's className
using the _iconContainer
class we defined in the _classes
object.
_iconContainer[i] = document.createElement( (this._table) ? 'td' : 'div'); _iconContainer[i].className = this._classes.iconContainer;
Caching a Selector for Our Icons
We'll now begin populating the _icons
array with a jQuery selector for the newly created icons, we'll pass these icons along as an array.
The array key for this collection will be i
, thus _icons[i]
will represent all of the icons contained in _elementParent[i]
.
this._icons[i] = $([edit,remove]);
Returning the Icon Container After Setting Its HTML and Appending It to the DOM
Now, we'll chain a few jQuery methods together in order to set the html of _iconContainer[i]
and append it to the DOM.
First, using jQuery's html
method, we'll set the html of the _iconContainer[i]
to this._icons[i]
.
Next, with our icons appended to the _iconContainer[i]
, we'll append it to the DOM, dynamically, using the options.insertMethod
string in square bracket / associative array notation.
Lastly, as most jQuery methods return
the actual jQuery selector being used in the expression itself, this expression will return
$(iconContainer[i])
.
Thus, this._iconContainer[i]
which is being defined in the _create
method's for
loop, will equal $(iconContainer[i])
.
return $(_iconContainer[i]).html(this._icons[i])[this.options.insertMethod](this._elementParent[i]);
For more information on returning values from a function, check the Mozilla Developers Network
Documentation of the jQuery html method can be found in the jQuery API
Our Widget's Event Handler, the _ClickHandler Method
With our widget instantiated and ready to go it's now time to handle it's behavior. The logic of this widget is such that the click handler will serve as a sort of "controller" in that it will determine what action is to be taken based on the class of the icon which received the click.
In other words, if the users clicks an edit icon, the handler will check the icons class and then call the appropriate edit functionality of the widget, and so on...
<strong>_clickHandler: function(event){}</strong>
Arguments
The _clickHandler
method takes one argument, event
. By specifying an empty argument here, jQuery will automatically assign the event
object to this argument. The event object contains a number of very useful properties such as target
, timeStamp
, preventDefault
and stopPropagation
to name a few. We'll be using a couple of these props / methods in our event handler.
Preventing Default Behavior
The first thing we'll do in our event handler is call the preventDefault
method of the jQuery event
object. This method will prevent any default actions from occurring as a result of the event. What this means for us is that when a user clicks one of the icons, which is actually an anchor, preventDefault
will stop any type of navigation / scrolling from being triggered - pretty nifty eh?
event.preventDefault();
Defining a Few Variables
First, we'll cache a jQuery selector for this
, which is, of course, the element which received the event.
Next, we'll define the variable i
which was passed to our handler in event.data
This was the iterator used in the _create
method's for
loop to cache selectors for various elements relative to _elementParent[i]
.
We'll then define the variable widget
by assigning evet.data.widget
to it which is a reference to the current instance of our widget.
Now, we'll define a sort of shortcut variable, triggers
as the widget._triggers
property. This property contains all of our icons trigger classes which we'll be using to determine what actions to take.
Lastly, we'll initialize the variables, data
and inputs
. These vars will only be used when we need to serialize data for AJAX requests.
var target = $(this), i = event.data.i, widget = event.data.widget, triggers = widget._triggers, data, inputs;
Determining What to Do With a Case Switch
The switch
statement is very much like an if
statement in that it evaluates an expression and only executes certain code if the condition is met. In some circumstance, it may be cleaner / easier to read. There are, however, various benefits and drawbacks to either statement but either would be appropriate given the proper conditions.
We'll be evaluating four conditions based on the switch
statements label which we'll set as true
. We are going to check the icon's class using jQuery's hasClass
method which returns a boolean
. If the icon has the class name specified in the argument, it will return true
.
switch(true){ case(target.hasClass(triggers.edit)): break; case(target.hasClass(triggers.remove)): break; case(target.hasClass(triggers.save)): break; case(target.hasClass(triggers.cancel)): break; default: break; };
As you can see, this a clean, readable way to evaluate which icon was clicked. Obviously, there are a number of different ways to do this and an if
statement would work just as well. Now, let's take a closer look at each case and what we'll be doing!
-
The Edit Icon
If the edit icon has been clicked we need to, first, toggle the default icons, which are "Edit" and "Remove" to the editor icons which are "Save" and "Cancel". So we'll call the
_showIconSet
method.You may recall this method from the widget blueprint we created earlier. Although we haven't defined it yet, it takes two arguments,
i
which will be used to select the icons we'll toggle andeditor
. Theeditor
argument specifies which iconset to show, default or editor.Once we defined more of our widget's methods, you'll see that most of them fall into line with jQuery in that they usually
return this
. As a result, we chain most of these methods together. So, let's add another method to this expression,transformElements
.transformElements
is sort of a workhorse in that it's responsible for making content editable, restoring content to it's previous state as well as updating content that's been edited. This method takes two arguments as well,i
, which, again, we'll be used to select the appropriate_editable
collection and i,type
which will define the type of "transformation". Here's a look:case(target.hasClass(triggers.edit)): widget._showIconSet(i, "editor")._transformElements(i, "edit"); break;
-
The Remove Icon
If the remove icon has been clicked, we'll need to remove this editable content both from the DOM as well as from the database.
We will, of course, be using an AJAX request to delete this item from the database but since we're not submitting a form here and we don't really have any inputs, which are automatically serialized on submission, we'll need a way to serialize / encode the text of these elements into a valid url string.
So, we'll call our widget's
_encode
method to serialize this data. We'll pass one argument to_encode
, the jQuery selector for the elements to be removed._encode
will serialize the text of those elements for us and return the a valid url string. So, we'll define the variabledata
as this value.Lastly, we'll call our widget's
_post
method which will submit an AJAX request to the server. We'll pass three arguments to_post
, the serialized data, a variable which indicates whether we're saving or deleting data andi
, which will be used in the$.ajax
success callback to actually remove the deleted elements from the DOM.case(target.hasClass(triggers.remove)): data = widget._encode(widget._editable[i]); widget._post(data, "remove", i) break;
-
The Save Icon
If the save icon was clicked, we'll need to serialize our inputs, as we did above, using the
_serialize
method. Then, we'll need to call our widget's post method in order to submit the AJAX request to the server.As such, we'll define two variables here,
inputs
anddata
.inputs
will be defined as the jQuery collection of inputs within the context of this element anddata
will be thereturn
value from the_encode
method, just as above.Finally, we'll call the
_post
method with it's three arguments, the serializeddata
, the type of post we're making (in this case, "save") andi
which is used in the$.ajax
success callback to update the appropriate elements in the DOM.case(target.hasClass(triggers.save)): inputs = $('input', widget._editable[i]); data = widget._encode(inputs); widget._post(data, "save", i) break;
-
The Cancel Icon
Finally, if the cancel icon has been clicked, we'll restore the icons and editable content back to their default states.
To do this, we'll first call the
_showIconSet
method with two arguments,i
anddefault
.i
will be used to select the appropriate collection from our_icons
array anddefault
specifies which icon set to show.Next, we'll chain the
_transformElements
method to this expression with one argument,i
, which, as usual, is used to select the appropriate collection from our_editable
array.case(target.hasClass(triggers.cancel)): widget._showIconSet(i, "default")._transformElements(i, "restore"); break;
Note: If you're not exactly sure as to what's going on in the methods we're calling, it's ok. We haven't defined these methods yet so there's no reason you really should understand yet. Hang tight, we'll be defining these methods shortly.
-
The Full Switch Statement
switch(true){ case(target.hasClass(triggers.edit)): widget._showIconSet(i, "editor")._transformElements(i, "edit"); break; case(target.hasClass(triggers.remove)): data = widget._encode(widget._editable[i]); widget._post(data, "remove", i); break; case(target.hasClass(triggers.save)): inputs = $('input', widget._editable[i]); data = widget._encode(inputs); widget._post(data, "save", i); break; case(target.hasClass(triggers.cancel)): widget._showIconSet(i, "default")._transformElements(i, "restore"); break; default: break; }
That's It for the Click!
Here's how the complete _clickHandler
method looks now that we've defined it:
_clickHandler: function(event){ event.preventDefault(); var target = $(this), i = event.data.i, widget = event.data.widget, triggers = widget._triggers, data, inputs; switch(true){ case(target.hasClass(triggers.edit)): widget._showIconSet(i, "editor")._transformElements(i, "edit"); break; case(target.hasClass(triggers.remove)): data = widget._encode(widget._editable[i]); widget._post(data, "remove", i); break; case(target.hasClass(triggers.save)): inputs = $('input', widget._editable[i]); data = widget._encode(inputs); widget._post(data, "save", i); break; case(target.hasClass(triggers.cancel)): widget._showIconSet(i, "default")._transformElements(i, "restore"); break; default: break; } }
Toggling the Icons To and Fro!
In our event handler, we call the _showIconSet
method whenever we need to toggle icon state between editor and default. This is the method responsible for making those changes.
The _showIconSet
method, step by step!
Taking advantage of jQuery's awesome chaining capability, let's take a look at how we can change icon state with just one variable and statement!
-
Var Titles
First off, were going to define a new variable,
titles
as an array, using a ternary operator.This variable will contain the title attributes for our icons.
We'll check the
iconSet
argument to determine which icons we're displaying and the populate the array with the appropriate titles accordingly.var titles = (iconSet === "default") ? ['Edit','Remove'] : ['Save','Cancel'];
-
this._icons[i]
Now, using
i
we'll select the appropriate element collection from our_icons
array.var titles = (iconSet === "default") ? ['Edit','Remove'] : ['Save','Cancel']; this._icons[i]
-
Eq(0)
The
eq
method is used as a filter against a jQuery collection in that it returns the element with the specified index. Thus, callingeq(0)
in the context of_icons[i]
method will narrow our selection down to the icon with an index of 0 within that collection. In other words, the first icon.var titles = (iconSet === "default") ? ['Edit','Remove'] : ['Save','Cancel']; this._icons[i].eq(0)
For more information on jQuery's
eq
method check the jQuery API -
toggleClass()
Using jQuery's
toggleClass
method, we'll add and remove classes to our icons, respectively. So, if a specified class is present, it will be removed. Conversely, if a specified class is not present, it will be added.We'll concatenate the appropriate classes for each icon with a space as the
toggleClass
method accepts multiple class names as a space separated list. Pretty nifty!var titles = (iconSet === "default") ? ['Edit','Remove'] : ['Save','Cancel']; this._icons[i].eq(0) .toggleClass(this._classes.edit + ' ' + this._classes.save)
Documentation for jQuery's
toggleClass
method can be found in the jQuery API -
Attr('title', Titles[0])
With our first icon's classes now set, we'll define it's title attribute using the jQuery
attr
method. We'll set the title using the appropriate value stored in thetitle
array we created just a moment ago. The array keys for thetitle
array match the index of the icon currently selected so the title for this icon is available astitles[0]
.var titles = (iconSet === "default") ? ['Edit','Remove'] : ['Save','Cancel']; this._icons[i].eq(0) .toggleClass(this._classes.edit + ' ' + this._classes.save) .attr('title', titles[0])
To read up on jQuery's
attr
method, check the jQuery API -
End()
jQuery's
end
method is used to remove the most recent filter applied to a collection. By callingend
here, we'll be removing theeq
filter from our collection. In other words, we'll be working with_icons[i] again.
var titles = (iconSet === "default") ? ['Edit','Remove'] : ['Save','Cancel']; this._icons[i].eq(0) .toggleClass(this._classes.edit + ' ' + this._classes.save) .attr('title', titles[0]) .end()
Documentation for jQuery's
end
method can be found in the jQuery API -
... the Next Verse Same as the First!
We'll use jQuery's eq method again to specify the second icon now. The second icon will have an index of one, since indicies are zero based, and as such we'll use
eq(1)
to select it.Following suite with the above expression, we'll us the exact same methods for the second icon as we did for the first but with the appropriate class names and title.
var titles = (iconSet === "default") ? ['Edit','Remove'] : ['Save','Cancel']; this._icons[i].eq(0) .toggleClass(this._classes.edit + ' ' + this._classes.save) .attr('title', titles[0]) .end().eq(1) .toggleClass(this._classes.remove + ' ' + this._classes.cancel) .attr('title', titles[1]);
-
Return This
The last thing we'll do here is
return this
so as not to break this methods chainability.var titles = (iconSet === "default") ? ['Edit','Remove'] : ['Save','Cancel']; this._icons[i].eq(0) .toggleClass(this._classes.edit + ' ' + this._classes.save) .attr('title', titles[0]) .end().eq(1) .toggleClass(this._classes.remove + ' ' + this._classes.cancel) .attr('title', titles[1]); return this;
Voilà!
Now that we've defined our variable and expression, let's take look at the entire _showIconSet
method now:
_showIconSet: function(i, iconSet){ var titles = (iconSet === "default") ? ['Edit','Remove'] : ['Save','Cancel']; this._icons[i].eq(0) .toggleClass(this._classes.edit + ' ' + this._classes.save) .attr('title', titles[0]) .end().eq(1) .toggleClass(this._classes.remove + ' ' + this._classes.cancel) .attr('title', titles[1]) return this; },
_transformElements
: The workhorse method!
In this method, we'll be looping through the editable content in order to get / set an appropriate text / html string for that element relative to the type of transformation we're making. Then, we'll simply set the element's html.
After the loop, as usual, we'll return this
!
Arguments
This method takes two arguments, i
and type
.
i
, will be used to select the appropriate jQuery collection from the _editable
array relative to _iconContainer[i]
from the _create
method.
The type
argument simply specifies the type of transformation that's being called. This method handles three three types of "transformation":
- Making content editable and storing current values for possible retrieval
- Restoring editable content back to its default state with it's original values
- Restoring editable content back to its default state with new values
As such, type will either be "edit", "restore" or "update".
_transformElements: function(i, type){},
A Necessary Shortcut...
First off, we'll need to define a variable which references the options.fieldNames
array.
It's necessary to define this reference within the context of this method so that it will be available to the inner function, $.each
, we'll be calling shortly.
The fieldNames
var will only be used when making elements editable.
var fieldNames = this.options.fieldNames;
Iterating Through Our Editable Content
Using jQuery's $.each
method, we'll iterate through _editable[i]
. We'll pass the empty argument, index
, to $.each
. The numeric index of each element in the loop will be assigned to this argument.
this._editable[i].each(function(index){...});
Variables Within the $.each Method
Within the scope of $.each
, we'll define / initialize two more variables respectively:
-
Html:
Well initialize the
html
variable, here, in the scope of$.each
. This var will contain an html string if we're making content editable and a text string if we're updating or restoring content.this._editable[i].each(function(index){ var html; });
-
Self:
We'll cache the jQuery selector for
this
, the selector for the current element in the loop.this._editable[i].each(function(index){ var html, self = $(this); });
Using a Switch to Get and Set!
With our variables initialized and defined, we'll now use a switch statement against the type
argument in order to determine what exactly we'll be doing.
-
Case "Edit":
If the case is
edit
, we'll be making this element editable. As a result, we'll need to do three things:-
The text value of the element
First off, we'll define a new variable,
val
, asthis
elementstext
value.switch (type){ case "edit": var val = self.text(); break;
-
Storing the text for later
Using jQuery's
$.data
method, we'll attachval
tothis
element! The$.data
method is a fantastic way to associate data with an element. For setting data, we'll need to specify three arguments,element
,key
andvalue
.switch (type){ case "edit": var val = self.text(); $.data(this, 'value', val); break;
For more information on jQuery's $.data method have a look at the jQuery API
-
Defining the
html
stringLastly, we'll define the
html
var as a string representation of an input element. This string will be used to both create and inject this element with just one call to jQuery's html method.We'll set the name attribute of this input by concatenating the appropriate value from the
fieldNames
array into the html string. This is where we'll use theindex
argument as it represents the index of the current element in the loop. So, if were working with_editable[0]
the appropriate field name should be stored infieldNames[0]
!In like manner, we'll the concatenate
val
into the string as the input's value attribute.switch (type){ case "edit": var val = self.text(); $.data(this, 'value', val); html = '<input type="text" name="' + fieldNames[index] + '" value="' + val + '">'; break;
-
-
Case "Restore":
If
type
evaluates torestore
, we'll only need to retrieve the text for this element.-
Retrieving
$.data
Retrieving $.data is just as simple as setting it. We only need to call the $.data method with two arguments,
element
andkey
.case "restore": html = $.data(this, 'value'); break;
-
-
Case "Update":
If the
type
argument evaluates toupdate
we'll define ourhtml
variable as the updated value from thevalue
attribute of this element'sinput
.Then, using jQuery's
$.data
method again, we'll update the value we attached to this element so that it reflects the new value!-
Getting the Inputs Value
Using jQuery's
attr
method. we'll define thehtml
var as the value attribute of the input.case "update": html = $("input", self).attr('value'); $.data(this, 'value', html); break;
-
-
Default:
-
Setting the Default:
We'll have the switch default to return in the case that the type argument somehow doesn't evaluate to one of the appropriate strings.
default: return;
-
The entire switch
statement
With all of our cases defined, here's a look at the entire switch statement
switch (type){ case "edit": var val = self.text(); $.data(this, 'value', val); html = '<input type="text" name="' + fieldNames[index] + '" value="' + val + '">'; break; case "restore": html = $.data(this, 'value'); break; case "update": html = $("input", self).attr('value'); $.data(this, 'value', html); break; default: return; };
Setting the HTML
Using jQuery's html
method, we'll now set the string we've defined as the html for this
element.
self.html(html);
Returning Something Useful
With our $.each
loop finished, we're now ready to finish this method. We'll do so by returning this
so that this method is chainable.
return this;
_transformElements
in its entirety
That's it, here's a look at the completed _transformElements method:
_transformElements: function(i, type){ var fieldNames = this.options.fieldNames; this._editable[i].each(function(index){ var html, self = $(this); switch (true){ case(type === "edit"): var val = self.text(); $.data(this, 'value', val); html = '<input type="text" name="' + fieldNames[index] + '" value="' + val + '">'; break; case(type === "restore"): html = $.data(this, 'value'); break; case(type === "update"): html = $("input", self).attr('value'); $.data(this, 'value', html); break; default: return; }; self.html(html); }); return this; },
Deleting elements from the DOM with _removeElements
The _removeElements
method will be called only when a user has successfully deleted data with an AJAX request. In other words, this method will be a callback to a successful AJAX deletion.
Arguments
This method takes only one argument, i
, which will be used to select the appropriate selector for the _elementParent
we're deleting.
<strong>_removeElements: function(i){...},</strong>
Selecting the Content
First, we'll specify which _elementParent
we're removing by using i
in square bracket / associative array notation. This will give us the element with an index of i
rather than the jQuery selector. So, we'll make a new jQuery selection from this expression.
We could just as easily use jQuery's get
or eq
methods here without making a new jQuery selection. However, in keeping in line with the coding convention we've been using, let's just do the following:
_removeElements: function(i){ $(this._elementParent[i]) },
For more information on jQuery's get method, check the jQuery API
Documentation for jQuery's eq method can also be found in the jQuery API
Removing the Content
Using jQuery's remove
method, we'll delete these elements from the DOM!
_removeElements: function(i){ $(this._elementParent[i]).remove(); },
Documentation for jQuery's remove method can be found in the jQuery API
Returning this
Lastly, we'll return this
so as not to break the chain.
_removeElements: function(i){ $(this._elementParent[i]).remove(); return this; },
_encode
: Preparing data for submission!
The _encode
method is called just before an AJAX request. Using jQuery's serialize
and $.param
methods, _encode
serializes key / value pairs and returns a url encoded string.
Arguments
This method takes one argument, inputs
, which represents a jQuery collection of the elements we want to serialize.
_encode: function(inputs){...},
Serializing Inputs, the Easy Way!
Using jQuery's is
method, which returns a boolean, we'll check the inputs
argument against an "input"
selector. If we are dealing with inputs, is
will return true
and as such, we'll then call jQuery's serialize
method on this collection. serialize
takes a form or a collection of inputs and returns a url encoded string of key / value pairs.
_encode: function(inputs){ if(inputs.is("input")) return inputs.serialize(); },
For more information on jQuery's is
method, check the jQuery API
documentation for jQuery's serialize
method can also be found in the jQuery API
Serializing Everything Else!
If the inputs
collection is not comprised of input elements, we'll build an object literal comprised of key value pairs representing, sort of, pseudo name and value attributes.
Once we've created this object, we can pass it to jQuery's $.param
as an argument. $.param
will serialize an array or object and return the url encoded string not unlike the jQuery serialize
method.
-
Initializing an Empty Object
Let's initialize the variable
data
an empty object. We'll populate this object in a moment as we iterate through ourinputs
.var data = {},
-
Making
this.options.fieldNames
availableNext, we'll define the variable
fieldNames
as a reference tothis.options.fieldNames
. By doing this, we're makingthis.options.fieldNames
available to this method's inner function,$.each
.fieldNames
also serves as a sort of shorthand way to access theoptions.fieldNames
array.var data = {}, fieldNames = this.options.fieldNames;
-
Looping though
inputs
Using jQuery's
$.each
method, we'll being iterating thoughinputs
. Again, we're passing an empty argument,index
, toinputs
. As a result, the index of the current element in the loop will be assigned toindex
. Next up, the logic of our loop!inputs.each(function(index){ });
-
Caching
this
First, let's cache the jQuery selector for
this
, the current element in our loop.inputs.each(function(index){ var self = $(this), });
-
Defining the
key
The variable
key
will represent thename
attribute for this element. The value of key is stored in thefieldNames
array and it's index within that array is equal to the index of the current element in our loop. Thus,fieldNames[index]
should contain the appropriate field name forinputs[index]
.inputs.each(function(index){ var self = $(this), key = fieldNames[index], });
-
Fetching the
value
Lastly, we define the variable
val
as a sort of pseudovalue
attribute. We'll use jQuery'stext
method to get the text of the current element in the loop.inputs.each(function(index){ var self = $(this), key = fieldNames[index], val = self.text(); });
-
Building the
data
objectWith our key / value pairs defined, we'll begin populating the object using square bracket / associative array notation. We'll set the key for each item as
key
and the value asval
. The benefit of using this type of notation, in this particular context, is that we can set the key with a variable. In literal notation, the variablekey
would be interpreted as the string"key"
.inputs.each(function(index){ var self = $(this), key = fieldNames[index], val = self.text(); data[key] = val; });
-
-
Returning the Encoded Data
Now that our iteration is complete and the
data
object is populated, we'll call jQuery's$.param
method withdata
as its argument.$.param
will return a url encoded string representation of the object or array passed to it.return $.param(data);
For more information on the jQuery $.param method check the jQuery API.
The full _serialize
method
Here's a look at the complete _serialize
method:
_encode: function(inputs){ if(inputs.is("input")) return inputs.serialize(); var data = {}, fieldNames = this.options.fieldNames; inputs.each(function(index){ var self = $(this), key = fieldNames[index], val = self.text(); data[key] = val; }); return $.param(data); },
Comments