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 {