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
This commit is contained in:
Elliott Sprehn 2015-01-15 15:03:44 -08:00
parent 8f1c3bacfc
commit 5ef6774cd9
9 changed files with 107 additions and 32 deletions

View File

@ -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 @@ module.exports = class extends SkyElement {
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 @@ module.exports = class extends SkyElement {
}
handleClick(event) {
this.clickCount++;
this.myText.value = "Moar clicking " + this.clickCount;
this.checked = !this.checked;
this.inputValue = "Moar clicking " + this.clickCount;
}
}.register();
</script>

View File

@ -601,6 +601,9 @@ Observer.prototype = {
return this.value_;
},
setValue: function(newValue) {
},
close: function() {
if (this.state_ != OPENED)
return;
@ -887,17 +890,12 @@ CompoundObserver.prototype = createObject({
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 @@ ObserverTransform.prototype = {
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 @@ ObserverTransform.prototype = {
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 @@ ObserverTransform.prototype = {
this.observable_ = undefined;
this.value_ = undefined;
this.getValueFn_ = undefined;
this.setValueFn_ = undefined;
}
}

View File

@ -160,6 +160,8 @@ class PropertyDirective {
node[name] = value;
});
}
if (typeof node.addPropertyBinding == 'function')
node.addPropertyBinding(this.name, observable);
return observable;
}
}

View File

@ -160,6 +160,8 @@ class SkyElement extends HTMLElement {
createdCallback() {
this.isAttached = false;
this.propertyBindings = null;
this.dirtyPropertyBindings = null;
this.created();
Object.preventExtensions(this);
@ -208,6 +210,8 @@ class SkyElement extends HTMLElement {
}
notifyPropertyChanged(name, oldValue, newValue) {
if (oldValue == newValue)
return;
var notifier = Object.getNotifier(this);
notifier.notify({
type: 'update',
@ -217,6 +221,38 @@ class SkyElement extends HTMLElement {
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;
}
};

View File

@ -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

View File

@ -334,11 +334,8 @@ describe('ObserverTransform', function() {
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 @@ describe('ObserverTransform', function() {
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();
});

View File

@ -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 6 SkyElement should convert boolean reflected attributes
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

View File

@ -131,6 +131,43 @@ describe("SkyElement", function() {
});
});
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 @@ describe("SkyElement", function() {
});
});
</script>
</sky>
</sky>

View File

@ -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 {