This tutorial is the follow up to last month’s Convert a List into a Select Dropdown using jQuery article. In it, we learned how to create an in-place editor for an ordered or unordered list by converting it into a <SELECT> dropdown. Today we’ll be modifying our conversion code to make it more efficient as well as adding more functionality.
New and Improved
To save you from having to visit the original article again, here is what the conversion code looked like:
var $list = $(this), $select = $('<select />'); $list.children('li').each(function(index) { $select.append($('<option />').attr('value', index).html($(this).html())); }); $list.replaceWith($select);
Devin Weaver pointed out some issues with using the html() function to set the content of the option tag. Besides the fact that HTML markup should not go into an option tag, including HTML tags opens up the potential for a script injection attack. For those reasons, the text() method should be employed instead. It strips out the HTML and returns the sanitized text content. Mr. Weaver also suggests using map() instead of each() to return the newly created options as an array and then append them all at once. That will save jQuery from having to manipulate the DOM every loop iteration, which could slow down the browser.
Devin, I heard you on both counts, so I updated the existing code accordingly:
$select.append($list.children('li').map(function(index) { return $('<option />').attr('value', index).text($(this).text()).get(0); }));
The only caveat to using map() – and it’s a small one – is that the append() function can only accept an array of raw elements – i.e. non-jQuery wrapped. To fetch the unwrapped element, we invoke the jQuery Collection .get() method, passing in an element index of zero (0) because the $() element constructor generates a collection of exactly one <option> element.
Handling the List Header tag
Did you know that lists may have a List Header (<LH>) tag, similar to the <TH> tag in tables? It’s true!
<UL class="dropdown multiple"> <LH>Table Fruit</LH> ...
Rather than waste a perfectly good tag, we can convert it into a Label to go along with our new SELECT control:
var $list = $(this), id = 'converted_dropdown_' + (index + 1), $lh = $('lh'); //set the list options $select.append($list.children('li').map(function(index) { return $('<option />').attr('value', index).text($(this).text()).get(0); })); //convert the LH element into a label if ($lh) { $('<label />').attr('for', id).html($lh.html() + ':<br />').insertBefore($list); } $list.replaceWith($select);
When creating the label, it’s OK to use the html() function because it could potentially contain HTML markup.
Class Power
Adding your own attributes to existing element tags is frowned upon. Instead, add your own classes to elements to organize and reference them, just as you would with CSS. For instance, including a class of “dropdown” could be used to identify all editable lists:
<UL class="dropdown"> <!-- options --> </UL>
Now we can convert all of them on a common event such as a button click.
The following code attaches our conversion code to the “editButton”‘s click() event. Notice the use of the $(‘.dropdown’) selector to collect all elements with the “dropdown” class. The each() method accepts an index argument, so we don’t have to use the index() function anymore to create the unique IDs.
jQuery(document).ready(function() { $('#editButton').click(function() { $('.dropdown').each(function(index) { var $list = $(this), id = 'converted_dropdown_' + (index + 1), $select = $('<select />').attr('id', id), $lh = $('lh'); //set the list options $select.append($list.children('li').map(function(index) { return $('<option />').attr('value', index).text($(this).text()).get(0); })); //convert the LH element into a label if ($lh) { $('<label />').attr('for', id).html($lh.html() + ':<br />').insertBefore($list); } $list.replaceWith($select); }); }); });
Adding Custom Attributes
We can expand on the class technique to introduce custom attributes as well.
Here’s a list with a couple of extra classes that affect the appearance of our converted list:
<OL class="dropdown multiple autoheight"> <!-- options --> </OL>
The “multiple” attribute can be transfered to our converted select list so that it is displayed with a listbox style rather than a combobox. The “autoheight” attribute tells our script to set the list SIZE to the length of the options – in other words, the same height as the list contents:
var $list = $(this), id = 'converted_dropdown_' + (index + 1), $select = $('<select />').attr('id', id), $lh = $('lh'); //optional properties if ( $list.hasClass('multiple') ) { $select.attr( 'multiple', '' ); } if ( $list.hasClass('autoheight') ) { $select.attr( 'size', $list.children('li').length ); } //set the list options $select.append($list.children('li').map(function(index) { return $('<option />').attr('value', index).text($(this).text()).get(0); }));
Here is some markup that includes our custom classes:
<UL class="dropdown multiple"> <LH>Table Fruit</LH> <LI>apples</LI> <LI>oranges</LI> <LI>bananas</LI> <LI>strawberries</LI> <LI>raspberries</LI> <LI>melons</LI> <LI>grapes</LI> <LI>tangerines</LI> </UL> <OL class="dropdown autoheight"> <LH>Table Fruit</LH> <LI>apples</LI> <LI>oranges</LI> <LI>bananas</LI> <LI>strawberries</LI> <LI>raspberries</LI> <LI>melons</LI> <LI>grapes</LI> <LI>tangerines</LI> </OL> <button id="editButton">Edit</button>
Clicking the Edit button produces the following SELECT controls:
Conclusion
With one page Web apps being all the rage right now, providing in-pace editing is a feature that’s sure to be appreciated by your visitors. Just make sure to have a workaround for the 0.1 percent of users who have JavaScript turned off!