Creating a Select Element that Plays Well with All Browsers
by Jason S. Kerchner on May 21, 2009 (filed in Development)
Creating DOM elements like text and password INPUTs, BUTTONs and TEXTAREAs is pretty simple. Creating a SELECT element, however, proved to be a little more tricky thanks to a quirk in the Opera browser.
Let’s look at a SELECT tag. It’s pretty simple, really. It has a SELECT tag with a number of OPTION tags inside it.
1 2 3 4 5 | <select id="colors" name="rgb_color">
<option value="r">Red</option>
<option value="g" selected="true">Green</option>
<option value="b">Blue</option>
</select> |
Each OPTION tag has a value attribute. If this particular SELECT element was submitted from a FORM, it would send to the server the name ‘rgb_color’ with the value ‘g’, since that’s the option that is currently selected.
From this example, it would seem that I could create a SELECT element, then add the OPTION elements to it with the selected attribute set for the appropriate option. Using the View’s DOM rendering functions, that would look something like this (in View.js).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | select: function(id, selected, options, props) { if (!props) props = { }; props.id = id; var optionElements = [ ]; if (options) { for (var value in options) { var optionProps = { selected: value == selected, value: value }; optionElements.push( this.createElement('option', optionProps, [options[value]]) ); } } return this.createElement('select', props, optionElements); } |
The first argument is the id of the SELECT element. The selected argument is the value of the currently selected OPTION element. This will allow us to set an option as already selected. The options argument is an object that defines which OPTIONs are displayed in the SELECT drop down list. The last argument is any additional properties to be set on the SELECT element. So, to create the SELECT element in our example above, we would make the following call.
1 2 3 4 5 | myView.select('colors', 'g', { r: 'Red', g: 'Green', b: 'Blue' }, { name: 'rgb_color' } ); |
When the select method is executed with the arguments in this call, it will loop through each of the options (lines 10 to 18 in the select function source code, above) and create an optionProps object for that option (lines 12 to 15). The optionProps object contains the value (which is the property name in the options object, ‘r’, ‘g’ or ‘b’ in this example) and the selected property, which is set to true if the selected argument matches the value of the current option. Next, it uses the createElement method to create the OPTION element (line 17), passing in the option text as a child (so that it appears between the OPTION’s open and close tags). The third argument of createElement expects an array, which is why the argument is surrounded in square brackets. Finally, the new option is added to the optionElements array using the array’s push function.
Now that the OPTION elements are created, the only thing left to do is create the SELECT element, and add the OPTION elements to it. Using the createElement function, this is very simple (line 21).
And it works like a charm in all browsers except Opera. For some reason, Opera does not like the selected property for the OPTION element. I’ve tried different variations, but none seem to work.
The solution is to use the selectedIndex property of the SELECT element. This is supported by all browsers.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | select: function(id, selected, options, props) { if (!props) props = { }; props.id = id; var optionElements = [ ]; var selectedIndex = 0; if (options) { for (var value in options) { var optionProps = { //selected: value == selected, // Not supported by Opera value: value }; if (value == selected) selectedIndex = optionElements.length; optionElements.push( this.createElement('option', optionProps, [options[value]]) ); } } var sel = this.createElement('select', props, optionElements); sel.selectedIndex = selectedIndex; return sel; } |
Line 7 adds a new variable to track the selected OPTION element, and is defaulted to zero (the first OPTION element). In line 14, I’ve removed the selected property, as it will not be used. Line 18 checks if the current option’s value matches the selected argument and, if it does, sets the selectedIndex appropriately (line 19). Line 25 creates the SELECT element, and line 26 sets the selectedIndex of the SELECT element. I have to set the selectedIndex after creating the element since the current version of createElement applies the props object before the child OPTION elements are added, and you can’t set selectedIndex when there are no OPTION elements. If this were not the case, if the props object was applied to the element after all OPTION elements were added, then I could just add selectedIndex to the props object. Hmm. Maybe a change to createElement is in order.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | createElement: function(tag, props, elements) { // Create the element var elem = document.createElement(tag); // MOVE THIS BLOCK DOWN TO LINE 24 //window.copy(props, elem, function(data) { // if (data.prop == 'cssFloat' && !elem.cssFloat) // data.prop = 'styleFloat'; //}); // Add any inner element to the newly created DOM object if (elements) { for (var i = 0, len = elements.length; i < len; i++) { if (isString(elements[i])) elem.appendChild(this.element(elements[i])); else if (elements[i]) // Make sure not null or undefined elem.appendChild(elements[i]); } } // Apply the properties to the element, correct property names where necessary (Globals.js) window.copy(props, elem, function(data) { if (data.prop == 'cssFloat' && !elem.cssFloat) data.prop = 'styleFloat'; }); // Lastly, return our new DOM element return elem; } |
That’s better. Now the props object will be applied after all child elements have been added. All unit tests still pass with this change. Will it break something else or cause problems in the future? Maybe. But for now, it simplifies the code for the select method, since I no longer need to track selectedIndex separately. Instead, I can update it in the props object directly. This also allows the selectedIndex property to be passed in to the props argument and have it be set correctly. That gives another option for setting the currently selected option other than the selected argument.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | select: function(id, selected, options, props) { if (!props) props = { }; props.id = id; var optionElements = [ ]; if (options) { for (var value in options) { var optionProps = { value: value }; if (value == selected) props.selectedIndex = optionElements.length; optionElements.push( this.createElement('option', optionProps, [options[value]]) ); } } return this.createElement('select', props, optionElements); } |
Note that the selectedIndex variable has been removed. If the option value and the selected argument values match, then line 17 sets the selectedIndex property of the props object. Thanks to JavaScript’s mutable objects, if this property doesn’t already exist, it will be created. Line 23 is the call to the createElement method to create the SELECT element and set the properties.
If you compare the original version (the one that didn’t work with Opera) to this version, one line of code was deleted and two were added, for a net result of one additional line of code. Not too bad when trying to correct browser incompatibilities.
So, there’s the final version for now. Of course, this does not support multiple option selects. That’s for another time, I think.
Leave a Reply