Thursday, August 16, 2007

Firefox does not reflect input form field values via innerHTML

Keywords:
firefox innerHTML form input field value text select textarea

Problem:
In IE (and therefore the GWT test shell running on windows) you can call innerHTML on a DOM object and where it contains HTTP form input fields you will see an up-to-date reflection of the inputs from the user.

On firefox you simply get a reflection of the DOM as it was originally served up to the user.

This is a problem if you want to move HTML around and not loose the inputs already made by the user.

Eg: On clicking the button in firefox you won't see the input entered in the text box.
<form action="" method="get">       
    <SPAN id="MyContent">           
        <input type="text" name="textField" value="" /><br/>
    </SPAN>
</form>           
  
<button onClick="window.alert(MyContent.innerHTML);">discover user input</button>


Solution:
Found the solution on comp.lang.javascript - Firefox does not reflect selected option via innerHTML. I've extended the example code from this post to handle checkbox, radio and textarea ...

The idea is, every input field on the page must have an onBlur="updateDOM(this)" event handler, forcing the DOM to be updated and reflect the user's input.
<script type="text/javascript">
//
// Will be called by input fields when in 'update DOM' mode. This will
// make sure that changes to input fields in the form will be captured
// in the DOM - not necessary in IE but is required in Moz, etc as the DOM
// will otherwise reflect the page as it was initially.
//
// inputField : the input field that has just been tabbed out of (onBlur) OR the ID of the input field
function updateDOM(inputField) {
    // if the inputField ID string has been passed in, get the inputField object
    if (typeof inputField == "string") {
        inputField = document.getElementById(inputField);
    }
    
    if (inputField.type == "select-one") {
        for (var i=0; i<inputField.options.length; i++) {
            if (i == inputField.selectedIndex) {    
                inputField.options[i].setAttribute("selected","selected");
            } else {
                inputField.options[i].removeAttribute("selected");
            }
        }
    } else if (inputField.type == "select-multiple") {
        for (var i=0; i<inputField.options.length; i++) {
            if (inputField.options[i].selected) {
                inputField.options[i].setAttribute("selected","selected");
            } else {
                inputField.options[i].removeAttribute("selected");
            }
        }
    } else if (inputField.type == "text") {
        inputField.setAttribute("value",inputField.value);
    } else if (inputField.type == "textarea") {
        var text = inputField.value;
        inputField.innerHTML = text;
        inputField.setAttribute("value", text);
    } else if (inputField.type == "checkbox") {
        if (inputField.checked) {
            inputField.setAttribute("checked","checked");
        } else {
            inputField.removeAttribute("checked");
        }
    } else if (inputField.type == "radio") {
        var radioNames = document.getElementsByName(inputField.name);
        for(var i=0; i < radioNames.length; i++) {
            if (radioNames[i].checked) {
                radioNames[i].setAttribute("checked","checked");
            } else {
                radioNames[i].removeAttribute("checked");
            }
        }
    }
}
</script>

<form action="" method="get">        
    <SPAN id="MyContent">            
        <input type="text" name="textField" value="" onBlur="updateDOM(this)"/><br/>
    </SPAN>
</form>            
    
<button onClick="window.alert(MyContent.innerHTML);">discover user input</button>


Notes:
It gets slightly trickier if you have input fields that don't get filled in by the user - eg a date picker dropdown, which will set the textbox with the date for the user, hence they never click in the box and trigger the 'onBlur'. In this case, you'd put the onBlur event on the date picker button

Eg:
<input type="text" name="dateField" id="dateField" value="" onBlur="updateDOM(this)"/>
<button id="myDatePicker" onClick="... do datepicking stuff ..." onBlur="updateDOM('dateField')">
 ... date picking image ...
</button>


Post updated (Thu, 25 Mar 2010): With thanks to the helpful commentors, the above script incorporates better handling for textArea & radio fields as well as an issue I came across for 'select-multiple' (was missing from the original script). It should work fine with the update via document or form approaches discussed in the comments - as opposed to the onBlur which continues to be good enough for my usage.

Post updated (Fri, 5 Apr 2013): Beware trying to set innerHTML text with newlines - IE will lose them. Work around is set the value attribute afterwards and this will honour the newlines (and is meaningless for other browsers).

14 comments:

Anonymous said...

Great solution. Thanks!

Anonymous said...

Thank you _very_ much. After spending a few hours debugging this problem, I was starting to wonder whether this wasn't one of my numerous mistakes! Does this constitute a firefox bug?

Anonymous said...

To use this with textareas, make sure to set this.innerHTML to this.value.

Trig Edge said...

Why worry about the event...

function updateAllDOMFields(theForm){
var inputNodes = getAllFormFields(theForm);
for(x=0; x < inputNodes.length; x++){
var theNode = inputNodes[x];
updateDOMField(theNode)
}
}

function getAllFormFields(theForm){
try{
var inputFields = theForm.getElementsByTagName("input");
var selectFields = theForm.getElementsByTagName("select");
var textFields = theForm.getElementsByTagName("textarea");
var array = new Array(inputFields + selectFields + textFields);
for(i=0; i < array.length; i++){
for(x=0; x < inputFields.length; x++){
array[i] = inputFields[x];
i++
}
for(a=0; a < selectFields.length; a++){
array[i] = selectFields[a];
i++
}
for(t=0; t < textFields.length; t++){
debug("Text box Found"+textFields.name);
array[i] = textFields[t];
i++
}
}
}
catch(e){alert("Error when evoking getAllFormFields(): \nSomething is probably wrong with the form you passed in\n\n"+e.message)}
return array;
}

Anonymous said...

You saved me a lot of work - I owe you a beer ;)

Anonymous said...

Thanks. Good script. Thats solve my problem for FF.

Anonymous said...

thank u for this code,it helped a lot

Anonymous said...

thank u for this code,it helped lot

Anonymous said...

thank u for this code,it helped lot

Anonymous said...

I also found using onblur to miss a few edge cases, so I followed Ian's lead and wrote a function to be called before the innerHTML rewrites the section. The function takes in an html element id and finds all input fields within that element and then applies Kevin's updateDOM function to each of those elements:


function updateDOMForFields(id) {
  elem=document.getElementById(id);
  i=elem.getElementsByTagName('input');
  s=elem.getElementsByTagName('select');
  t=elem.getElementsByTagName('textarea');
  field_sets=Array(i,s,t);
  for(x=0;x<field_sets.length;x++){
    set=field_sets[x];
    for(y=0;y<set.length;y++){
      updateDOM(field_sets[x][y]);
    }
  }
}

Unknown said...

My version, pass a document reference:

function updateDOMinnerHTML(doc) {
        try {
    var l_i=doc.getElementsByTagName('input');
        var l_s=doc.getElementsByTagName('select');
        var l_t=doc.getElementsByTagName('textarea');
        var l_field_sets=Array(l_i,l_s,l_t);
        var l_set, l_input, l_count=0, l_sel;
        for(var x=0;x<l_field_sets.length;x++) {
            l_set=l_field_sets[x];
            for(var y=0;y<l_set.length;y++) {
                l_input=l_set[y];
                ++l_count;
                    if (l_input.type=="select-one") {
                        l_sel=l_input.selectedIndex;
                        for (var i=0; i<l_input.options.length; i++) { // remove all selected
                                     l_input.options[i].removeAttribute("selected");
                                l_input.options[i].selected=false;
                        }
                        l_input.options[l_sel].setAttribute("selected","selected");
                        l_input.options[l_sel].selected=true;
                        alert(l_input.options[l_sel].value+' '+l_input.options[l_sel].text);
                    } else if (l_input.type=="text") l_input.setAttribute("value",l_input.value);
                     else if (l_input.type=="textarea" || l_input.type=="hidden") l_input.setAttribute("value",l_input.value);
                     else if (l_input.type=="checkbox" || l_input.type=="radio") {
                         if (l_input.checked) l_input.setAttribute("checked","checked");
                         else l_input.removeAttribute("checked");
                    } else alert('updateDOMinnerHTML cannot update field type '+l_input.type);
            }
        }
        } catch(e) { alert(e); }
}

Maciek said...

Thank you for this function! It's very useful. However it doesn't handle radio buttons correctly - it updates only a radio button which had blur event, not ALL radio buttons with the same name. Solution is simple, change:

} else if ((inputField.type == "checkbox") || (inputField.type == "radio")) {
if (inputField.checked) {
inputField.setAttribute("checked","checked");
} else {
inputField.removeAttribute("checked");
}
}

To:
} else if (inputField.type == "checkbox") {
if (inputField.checked) {
inputField.setAttribute("checked","checked");
} else {
inputField.removeAttribute("checked");
}
} else if (inputField.type == "radio") {
var radioNames = document.getElementsByName(inputField.name);
for(var i=0; i < radioNames.length; i++)
{
if (radioNames[i].checked) {
radioNames[i].setAttribute("checked","checked");
} else {
radioNames[i].removeAttribute("checked");
}
}
}

Unknown said...

You People are really great!.

Anonymous said...

Thanks! Very usefull!