Add two way data binding.

All reflected attributes two way bind on SkyElement, so now doing
<sky-element name="sky-input" attributes="value:string"> is enough
to get two way binding on the value attribute so users doing
<sky-input value="{{ inputValue }}"> will get the inputValue property
updated as the user types.

R=abarth@chromium.org, ojan@chromium.org

Review URL: https://codereview.chromium.org/850383002
diff --git a/sky/examples/widgets/index.sky b/sky/examples/widgets/index.sky
index d4e55c0..98b0ab4 100644
--- a/sky/examples/widgets/index.sky
+++ b/sky/examples/widgets/index.sky
@@ -25,7 +25,10 @@
   }
   </style>
 
-  <sky-input id="text" value="Ready" />
+  <sky-box title='Text'>
+    <sky-input id="text" value="{{ inputValue }}" />
+    <div>value = {{ inputValue }}</div>
+  </sky-box>
 
   <sky-box title='Buttons'>
     <sky-button id='button' on-click='handleClick'>Button</sky-button>
@@ -37,7 +40,8 @@
     <div><sky-checkbox id='checkbox' />Checkbox</div>
     <div class="output">highlight: {{ myCheckbox.highlight }}</div>
     <div class="output">checked: {{ myCheckbox.checked }}</div>
-    <div><sky-checkbox id='checkbox' checked='true'/>Checkbox, default checked.</div>
+    <div><sky-checkbox id='checkbox' checked='{{ checked }}'/>Checkbox, default checked.</div>
+    <div class="output">checked: {{ checked }}</div>
   </sky-box>
 
   <sky-box title='Radios'>
@@ -61,6 +65,8 @@
     this.myCheckbox = null;
     this.myText = null;
     this.clickCount = 0;
+    this.inputValue = "Ready";
+    this.checked = false;
   }
   attached() {
     this.myButton = this.shadowRoot.getElementById('button');
@@ -70,7 +76,8 @@
   }
   handleClick(event) {
     this.clickCount++;
-    this.myText.value = "Moar clicking " + this.clickCount;
+    this.checked = !this.checked;
+    this.inputValue = "Moar clicking " + this.clickCount;
   }
 }.register();
 </script>
diff --git a/sky/framework/sky-element/observe.sky b/sky/framework/sky-element/observe.sky
index 23c6fb7..415b4b8 100644
--- a/sky/framework/sky-element/observe.sky
+++ b/sky/framework/sky-element/observe.sky
@@ -601,6 +601,9 @@
     return this.value_;
   },
 
+  setValue: function(newValue) {
+  },
+
   close: function() {
     if (this.state_ != OPENED)
       return;
@@ -887,17 +890,12 @@
 
 function identFn(value) { return value; }
 
-function ObserverTransform(observable, getValueFn, setValueFn,
-                           dontPassThroughSet) {
+function ObserverTransform(observable, getValueFn) {
   this.callback_ = undefined;
   this.target_ = undefined;
   this.value_ = undefined;
   this.observable_ = observable;
   this.getValueFn_ = getValueFn || identFn;
-  this.setValueFn_ = setValueFn || identFn;
-  // TODO(rafaelw): This is a temporary hack. PolymerExpressions needs this
-  // at the moment because of a bug in it's dependency tracking.
-  this.dontPassThroughSet_ = dontPassThroughSet;
 }
 
 ObserverTransform.prototype = {
@@ -918,6 +916,9 @@
     this.callback_.call(this.target_, this.value_, oldValue);
   },
 
+  setValue: function(oldValue) {
+  },
+
   discardChanges: function() {
     this.value_ = this.getValueFn_(this.observable_.discardChanges());
     return this.value_;
@@ -927,12 +928,6 @@
     return this.observable_.deliver();
   },
 
-  setValue: function(value) {
-    value = this.setValueFn_(value);
-    if (!this.dontPassThroughSet_ && this.observable_.setValue)
-      return this.observable_.setValue(value);
-  },
-
   close: function() {
     if (this.observable_)
       this.observable_.close();
@@ -941,7 +936,6 @@
     this.observable_ = undefined;
     this.value_ = undefined;
     this.getValueFn_ = undefined;
-    this.setValueFn_ = undefined;
   }
 }
 
diff --git a/sky/framework/sky-element/sky-binder.sky b/sky/framework/sky-element/sky-binder.sky
index 8eff2f2..aca84a3 100644
--- a/sky/framework/sky-element/sky-binder.sky
+++ b/sky/framework/sky-element/sky-binder.sky
@@ -160,6 +160,8 @@
         node[name] = value;
       });
     }
+    if (typeof node.addPropertyBinding == 'function')
+      node.addPropertyBinding(this.name, observable);
     return observable;
   }
 }
diff --git a/sky/framework/sky-element/sky-element.sky b/sky/framework/sky-element/sky-element.sky
index a76874a..7b7933a 100644
--- a/sky/framework/sky-element/sky-element.sky
+++ b/sky/framework/sky-element/sky-element.sky
@@ -160,6 +160,8 @@
 
   createdCallback() {
     this.isAttached = false;
+    this.propertyBindings = null;
+    this.dirtyPropertyBindings = null;
     this.created();
 
     Object.preventExtensions(this);
@@ -208,6 +210,8 @@
   }
 
   notifyPropertyChanged(name, oldValue, newValue) {
+    if (oldValue == newValue)
+      return;
     var notifier = Object.getNotifier(this);
     notifier.notify({
       type: 'update',
@@ -217,6 +221,38 @@
     var handler = this[name + 'Changed'];
     if (typeof handler == 'function')
       handler.call(this, oldValue, newValue);
+    this.schedulePropertyBindingUpdate(name);
+  }
+
+  addPropertyBinding(name, binding) {
+    if (!this.propertyBindings)
+      this.propertyBindings = new Map();
+    this.propertyBindings.set(name, binding);
+  }
+
+  getPropertyBinding(name) {
+    if (!this.propertyBindings)
+      return null;
+    return this.propertyBindings.get(name);
+  }
+
+  schedulePropertyBindingUpdate(name) {
+    if (!this.dirtyPropertyBindings) {
+      this.dirtyPropertyBindings = new Set();
+      Promise.resolve().then(this.updatePropertyBindings.bind(this));
+    }
+    this.dirtyPropertyBindings.add(name);
+  }
+
+  updatePropertyBindings() {
+    for (var name of this.dirtyPropertyBindings) {
+      var binding = this.getPropertyBinding(name);
+      if (binding) {
+        binding.setValue(this[name]);
+        binding.discardChanges();
+      }
+    }
+    this.dirtyPropertyBindings = null;
   }
 };
 
diff --git a/sky/tests/framework/observe-expected.txt b/sky/tests/framework/observe-expected.txt
index dac0010..b7db27a 100644
--- a/sky/tests/framework/observe-expected.txt
+++ b/sky/tests/framework/observe-expected.txt
@@ -1,7 +1,7 @@
 ERROR: Exception caught during observer callback: ouch 
-SOURCE: http://127.0.0.1:8000/sky/framework/sky-element/observe.sky:627
+SOURCE: http://127.0.0.1:8000/sky/framework/sky-element/observe.sky:630
 ERROR: Exception caught during observer callback: ouch 
-SOURCE: http://127.0.0.1:8000/sky/framework/sky-element/observe.sky:627
+SOURCE: http://127.0.0.1:8000/sky/framework/sky-element/observe.sky:630
 Running 79 tests
 ok 1 Path constructor throws
 ok 2 Path path validity
diff --git a/sky/tests/framework/observe.sky b/sky/tests/framework/observe.sky
index 2fa8925..a2bc395 100644
--- a/sky/tests/framework/observe.sky
+++ b/sky/tests/framework/observe.sky
@@ -334,11 +334,8 @@
 
     function valueFn(value) { return value * 2; }
 
-    function setValueFn(value) { return value / 2; }
-
     observer = new ObserverTransform(new PathObserver(obj, 'foo'),
-                                     valueFn,
-                                     setValueFn);
+                                     valueFn);
     observer.open(callback);
 
     obj.foo = 2;
@@ -347,11 +344,10 @@
     assertNoChanges();
 
     observer.setValue(2);
-    assert.strictEqual(obj.foo, 1);
-    assertPathChanges(2, 4);
+    assert.strictEqual(obj.foo, 2);
 
     obj.foo = 10;
-    assertPathChanges(20, 2);
+    assertPathChanges(20, 4);
 
     observer.close();
   });
diff --git a/sky/tests/framework/templates-expected.txt b/sky/tests/framework/templates-expected.txt
index 587ea4e..b59402a 100644
--- a/sky/tests/framework/templates-expected.txt
+++ b/sky/tests/framework/templates-expected.txt
@@ -1,4 +1,4 @@
-Running 12 tests
+Running 13 tests
 ok 1 SkyElement should stamp when the element is inserted
 ok 2 SkyElement should update isAttached when inserting
 ok 3 SkyElement should handle parser created elements with attributes
@@ -8,9 +8,10 @@
 ok 7 SkyElement should convert string reflected attributes
 ok 8 SkyElement should convert number reflected attributes
 ok 9 SkyElement should connect data binding
-ok 10 SkyElement should connect template event handlers
-ok 11 SkyElement should connect host event handlers
-ok 12 SkyElement should call shadowRootReady after creating the template instance
-12 tests
-12 pass
+ok 10 SkyElement should two way bind attributes
+ok 11 SkyElement should connect template event handlers
+ok 12 SkyElement should connect host event handlers
+ok 13 SkyElement should call shadowRootReady after creating the template instance
+13 tests
+13 pass
 0 fail
diff --git a/sky/tests/framework/templates.sky b/sky/tests/framework/templates.sky
index aca46d8..747d906 100644
--- a/sky/tests/framework/templates.sky
+++ b/sky/tests/framework/templates.sky
@@ -131,6 +131,43 @@
     });
   });
 
+  it("should two way bind attributes", function(done) {
+    sandbox.appendChild(element);
+    var checkbox = element.shadowRoot.getElementById("checkbox");
+    assert.isFalse(checkbox.checked);
+    assert.isFalse(element.checked);
+    element.checked = true;
+    assert.isTrue(element.checked);
+    assert.isFalse(checkbox.checked);
+    Promise.resolve().then(function() {
+      assert.isTrue(checkbox.checked);
+      checkbox.checked = false;
+      assert.isFalse(checkbox.checked);
+      return Promise.resolve().then(function() {
+        assert.isFalse(element.checked);
+        assert.isFalse(checkbox.checked);
+        checkbox.checked = true;
+        assert.isTrue(checkbox.checked);
+        return Promise.resolve().then(function() {
+          assert.isTrue(element.checked);
+          element.checked = true;
+          assert.isTrue(element.checked);
+          assert.isTrue(checkbox.checked);
+          element.checked = false;
+          assert.isFalse(element.checked);
+          assert.isTrue(checkbox.checked);
+          return Promise.resolve().then(function() {
+            assert.isFalse(checkbox.checked);
+            assert.isFalse(element.checked);
+            done();
+          });
+        });
+      });
+    }).catch(function(e) {
+      done(e);
+    });
+  });
+
   it("should connect template event handlers", function() {
     sandbox.appendChild(element);
     var inside = element.shadowRoot.getElementById("inside");
@@ -160,4 +197,4 @@
   });
 });
 </script>
-</sky>
\ No newline at end of file
+</sky>
diff --git a/sky/tests/resources/test-element.sky b/sky/tests/resources/test-element.sky
index 5c48950..be6403c 100644
--- a/sky/tests/resources/test-element.sky
+++ b/sky/tests/resources/test-element.sky
@@ -4,6 +4,7 @@
 // found in the LICENSE file.
 -->
 <import src="/sky/framework/sky-element/sky-element.sky" as="SkyElement" />
+<import src="/sky/framework/sky-checkbox/sky-checkbox.sky" />
 
 <sky-element
     name="test-element"
@@ -11,6 +12,7 @@
     on-host-event="handleEvent">
 <template>
   <div id="inside" on-test-event="handleEvent" lang="{{ value }}">{{ value }}</div>
+  <sky-checkbox id="checkbox" checked="{{ checked }}" />
 </template>
 <script>
 module.exports = class extends SkyElement {