diff --git a/packages/spark/patch.js b/packages/spark/patch.js index ab0a156ca5..3319567a58 100644 --- a/packages/spark/patch.js +++ b/packages/spark/patch.js @@ -361,15 +361,25 @@ Spark._Patcher._copyAttributes = function(tgt, src) { tgt.style.cssText = ''; var isRadio = false; + var finalChecked = null; if (tgt.nodeName === "INPUT") { // Record for later whether this is a radio button. isRadio = (tgt.type === 'radio'); - // Clearing the attributes of a checkbox won't necessarily - // uncheck it, eg in FF12, so we uncheck explicitly - // (if necessary; we don't want to generate spurious - // propertychange events in old IE). - if (tgt.checked === true && src.checked === false) { - tgt.checked = false; + + // Figure out whether this should be checked or not. If the re-rendering + // changed its idea of checkedness, go with that; otherwsie go with whatever + // the control's current setting is. + if (isRadio || tgt.type === 'checkbox') { + var tgtOriginalChecked = !!tgt._sparkOriginalRenderedChecked && + tgt._sparkOriginalRenderedChecked[0]; + var srcOriginalChecked = !!src._sparkOriginalRenderedChecked && + src._sparkOriginalRenderedChecked[0]; + if (tgtOriginalChecked === srcOriginalChecked) { + finalChecked = !!tgt.checked; + } else { + finalChecked = !!srcOriginalChecked; + tgt._sparkOriginalRenderedChecked = [finalChecked]; + } } } @@ -446,9 +456,6 @@ Spark._Patcher._copyAttributes = function(tgt, src) { if (srcExpando) src._sparkOriginalRenderedValue = srcExpando; - if (typeof tgt.checked !== "undefined" && src.checked) - tgt.checked = src.checked; - if (src.name) tgt.name = src.name; @@ -463,8 +470,7 @@ Spark._Patcher._copyAttributes = function(tgt, src) { if (name === "type") { // can't change type of INPUT in IE; don't support it } else if (name === "checked") { - tgt.checked = tgt.defaultChecked = (value && value !== "false"); - tgt.setAttribute("checked", "checked"); + // handled specially below } else if (name === "style") { tgt.style.cssText = src.style.cssText; } else if (name === "class") { @@ -544,4 +550,19 @@ Spark._Patcher._copyAttributes = function(tgt, src) { // around we'll be comparing to this rendered value instead of the old one. tgt._sparkOriginalRenderedValue = [srcOriginalRenderedValue]; } + + // Deal with checkboxes and radios. + if (finalChecked !== null) { + // Don't do a no-op write to 'checked', since in some browsers that triggers + // events. + if (tgt.checked !== finalChecked) + tgt.checked = finalChecked; + + // Set various other fields related to checkedness. + tgt.defaultChecked = finalChecked; + if (finalChecked) + tgt.setAttribute("checked", "checked"); + else + tgt.removeAttribute("checked"); + } }; diff --git a/packages/spark/patch_tests.js b/packages/spark/patch_tests.js index 890c34e808..50975bd4f7 100644 --- a/packages/spark/patch_tests.js +++ b/packages/spark/patch_tests.js @@ -138,6 +138,7 @@ Tinytest.add("spark - patch - copyAttributes", function(test) { var nodeHtml = buf.join(''); var frag = DomUtils.htmlToFragment(nodeHtml); var n = frag.firstChild; + n._sparkOriginalRenderedChecked = [n.checked]; if (! node) { node = n; } else { diff --git a/packages/spark/spark.js b/packages/spark/spark.js index ad7025c6ac..4fda7c1252 100644 --- a/packages/spark/spark.js +++ b/packages/spark/spark.js @@ -311,9 +311,15 @@ _.extend(Spark._Renderer.prototype, { // We save it in a one-element array expando. We use the array because IE8 // gets confused by expando properties with scalar values and exposes them // as HTML attributes. + // + // We also save the values of CHECKED for radio and checkboxes. _.each(DomUtils.findAll(ret, '[value], textarea, select'), function (node) { node._sparkOriginalRenderedValue = [DomUtils.getElementValue(node)]; }); + _.each(DomUtils.findAll(ret, 'input[type=checkbox], input[type=radio]'), + function (node) { + node._sparkOriginalRenderedChecked = [node.checked]; + }); return ret; } diff --git a/packages/spark/spark_tests.js b/packages/spark/spark_tests.js index 060051efdc..160adfa14d 100644 --- a/packages/spark/spark_tests.js +++ b/packages/spark/spark_tests.js @@ -2712,6 +2712,74 @@ Tinytest.add("spark - controls - radio", function(test) { div.kill(); }); +Tinytest.add("spark - controls - checkbox", function(test) { + var labels = ["Foo", "Bar", "Baz"]; + var Rs = {}; + _.each(labels, function (label) { + Rs[label] = ReactiveVar(false); + }); + var changeBuf = []; + var div = OnscreenDiv(renderWithPreservation(function() { + var buf = []; + _.each(labels, function (label) { + var checked = Rs[label].get() ? 'checked="checked"' : ''; + buf.push(''); + }); + return buf.join(''); + })); + + Meteor.flush(); + + // get the three boxes; they should be considered 'labeled' by the patcher and + // not change identities! + var boxes = nodesToArray(div.node().getElementsByTagName("INPUT")); + + test.equal(_.pluck(boxes, 'checked'), [false, false, false]); + + // Re-render with first one checked. + Rs.Foo.set(true); + Meteor.flush(); + test.equal(_.pluck(boxes, 'checked'), [true, false, false]); + + // Re-render with first one unchecked again. + Rs.Foo.set(false); + Meteor.flush(); + test.equal(_.pluck(boxes, 'checked'), [false, false, false]); + + // User clicks the second one. + clickElement(boxes[1]); + test.equal(_.pluck(boxes, 'checked'), [false, true, false]); + Meteor.flush(); + test.equal(_.pluck(boxes, 'checked'), [false, true, false]); + + // Re-render with third one checked. Second one should stay checked because + // it's a user update! + Rs.Baz.set(true); + Meteor.flush(); + test.equal(_.pluck(boxes, 'checked'), [false, true, true]); + + // User turns second and third off. + clickElement(boxes[1]); + clickElement(boxes[2]); + test.equal(_.pluck(boxes, 'checked'), [false, false, false]); + Meteor.flush(); + test.equal(_.pluck(boxes, 'checked'), [false, false, false]); + + // Re-render with first one checked. Third should stay off because it's a user + // update! + Rs.Foo.set(true); + Meteor.flush(); + test.equal(_.pluck(boxes, 'checked'), [true, false, false]); + + // Re-render with first one unchecked. Third should still stay off. + Rs.Foo.set(false); + Meteor.flush(); + test.equal(_.pluck(boxes, 'checked'), [false, false, false]); + + div.kill(); +}); + _.each(['textarea', 'text', 'password', 'submit', 'button', 'reset', 'select', 'hidden'], function (type) { Tinytest.add("spark - controls - " + type, function(test) {