Skip to content

Commit

Permalink
Merge pull request knockout#1596 from knockout/1596-writable-computed…
Browse files Browse the repository at this point in the history
…-in-component

Create writable computed within custom-element component
  • Loading branch information
mbest committed Nov 3, 2014
2 parents 25289df + 2d05afe commit 348f9ea
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 12 deletions.
64 changes: 64 additions & 0 deletions spec/components/componentBindingBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,70 @@ describe('Components: Component binding', function() {
expect(secondViewModelInstance).not.toBe(firstViewModelInstance);
});

it('Is possible to pass expressions that can vary observably and evaluate as writable observable instances', function() {
// This spec is copied, with small modifications, from customElementBehaviors.js to show that the same component
// definition can be used with the component binding and with custom elements.
var constructorCallCount = 0;
ko.components.register('test-component', {
template: '<input data-bind="value: myval"/>',
viewModel: function(params) {
constructorCallCount++;
this.myval = params.somevalue;

// See we received a writable observable
expect(ko.isWritableObservable(this.myval)).toBe(true);
}
});

// Bind to a viewmodel with nested observables; see the expression is evaluated as expected
// The component itself doesn't have to know or care that the supplied value is nested - the
// custom element syntax takes care of producing a single computed property that gives the
// unwrapped inner value.
var innerObservable = ko.observable('inner1'),
outerObservable = ko.observable({ inner: innerObservable });
testNode.innerHTML = '<div data-bind="component: { name: \'' + testComponentName + '\', params: { somevalue: outer().inner } }"></div>';
ko.applyBindings({ outer: outerObservable }, testNode);
jasmine.Clock.tick(1);
expect(testNode.childNodes[0].childNodes[0].value).toEqual('inner1');
expect(outerObservable.getSubscriptionsCount()).toBe(1);
expect(innerObservable.getSubscriptionsCount()).toBe(1);
expect(constructorCallCount).toBe(1);

// See we can mutate the inner value and see the result show up
innerObservable('inner2');
expect(testNode.childNodes[0].childNodes[0].value).toEqual('inner2');
expect(outerObservable.getSubscriptionsCount()).toBe(1);
expect(innerObservable.getSubscriptionsCount()).toBe(1);
expect(constructorCallCount).toBe(1);

// See that we can mutate the observable from within the component
testNode.childNodes[0].childNodes[0].value = 'inner3';
ko.utils.triggerEvent(testNode.childNodes[0].childNodes[0], 'change');
expect(innerObservable()).toEqual('inner3');

// See we can mutate the outer value and see the result show up (cleaning subscriptions to the old inner value)
var newInnerObservable = ko.observable('newinner');
outerObservable({ inner: newInnerObservable });
jasmine.Clock.tick(1); // modifying the outer observable causes the component to reload, which happens asynchronously
expect(testNode.childNodes[0].childNodes[0].value).toEqual('newinner');
expect(outerObservable.getSubscriptionsCount()).toBe(1);
expect(innerObservable.getSubscriptionsCount()).toBe(0);
expect(newInnerObservable.getSubscriptionsCount()).toBe(1);
expect(constructorCallCount).toBe(2);

// See that we can mutate the new observable from within the component
testNode.childNodes[0].childNodes[0].value = 'newinner2';
ko.utils.triggerEvent(testNode.childNodes[0].childNodes[0], 'change');
expect(newInnerObservable()).toEqual('newinner2');
expect(innerObservable()).toEqual('inner3'); // original one hasn't changed

// See that subscriptions are disposed when the component is
ko.cleanNode(testNode);
expect(outerObservable.getSubscriptionsCount()).toBe(0);
expect(innerObservable.getSubscriptionsCount()).toBe(0);
expect(newInnerObservable.getSubscriptionsCount()).toBe(0);
});

it('Disposes the viewmodel if the element is cleaned', function() {
function testViewModel() { }
testViewModel.prototype.dispose = function() { this.wasDisposed = true; };
Expand Down
27 changes: 21 additions & 6 deletions spec/components/customElementBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,9 +210,10 @@ describe('Components: Custom elements', function() {
this.receivedobservable = params.suppliedobservable;
this.dispose = function() { this.wasDisposed = true; };

// See we didn't get the original observable instance. Instead we got a computed property.
// See we didn't get the original observable instance. Instead we got a read-only computed property.
expect(this.receivedobservable).not.toBe(rootViewModel.myobservable);
expect(ko.isComputed(this.receivedobservable)).toBe(true);
expect(ko.isWritableObservable(this.receivedobservable)).toBe(false);

// The $raw value for this param is a computed property whose value is raw result
// of evaluating the binding value. Since the raw result in this case is itself not
Expand Down Expand Up @@ -247,14 +248,17 @@ describe('Components: Custom elements', function() {
expect(rootViewModel.myobservable.getSubscriptionsCount()).toBe(0);
});

it('Is possible to pass expressions that can vary observably and evaluate as observable instances', function() {
it('Is possible to pass expressions that can vary observably and evaluate as writable observable instances', function() {
var constructorCallCount = 0;
ko.components.register('test-component', {
template: '<p>the value: <span data-bind="text: myval"></span></p>',
template: '<input data-bind="value: myval"/>',
viewModel: function(params) {
constructorCallCount++;
this.myval = params.somevalue;

// See we received a writable observable
expect(ko.isWritableObservable(this.myval)).toBe(true);

// See we received a computed, not either of the original observables
expect(ko.isComputed(this.myval)).toBe(true);

Expand All @@ -278,27 +282,38 @@ describe('Components: Custom elements', function() {
testNode.innerHTML = '<test-component params="somevalue: outer().inner"></test-component>';
ko.applyBindings({ outer: outerObservable }, testNode);
jasmine.Clock.tick(1);
expect(testNode).toContainText('the value: inner1');
expect(testNode.childNodes[0].childNodes[0].value).toEqual('inner1');
expect(outerObservable.getSubscriptionsCount()).toBe(1);
expect(innerObservable.getSubscriptionsCount()).toBe(1);
expect(constructorCallCount).toBe(1);

// See we can mutate the inner value and see the result show up
innerObservable('inner2');
expect(testNode).toContainText('the value: inner2');
expect(testNode.childNodes[0].childNodes[0].value).toEqual('inner2');
expect(outerObservable.getSubscriptionsCount()).toBe(1);
expect(innerObservable.getSubscriptionsCount()).toBe(1);
expect(constructorCallCount).toBe(1);

// See that we can mutate the observable from within the component
testNode.childNodes[0].childNodes[0].value = 'inner3';
ko.utils.triggerEvent(testNode.childNodes[0].childNodes[0], 'change');
expect(innerObservable()).toEqual('inner3');

// See we can mutate the outer value and see the result show up (cleaning subscriptions to the old inner value)
var newInnerObservable = ko.observable('newinner');
outerObservable({ inner: newInnerObservable });
expect(testNode).toContainText('the value: newinner');
expect(testNode.childNodes[0].childNodes[0].value).toEqual('newinner');
expect(outerObservable.getSubscriptionsCount()).toBe(1);
expect(innerObservable.getSubscriptionsCount()).toBe(0);
expect(newInnerObservable.getSubscriptionsCount()).toBe(1);
expect(constructorCallCount).toBe(1);

// See that we can mutate the new observable from within the component
testNode.childNodes[0].childNodes[0].value = 'newinner2';
ko.utils.triggerEvent(testNode.childNodes[0].childNodes[0], 'change');
expect(newInnerObservable()).toEqual('newinner2');
expect(innerObservable()).toEqual('inner3'); // original one hasn't changed

// See that subscriptions are disposed when the component is
ko.cleanNode(testNode);
expect(outerObservable.getSubscriptionsCount()).toBe(0);
Expand Down
18 changes: 13 additions & 5 deletions src/components/customElements.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,26 @@
return ko.computed(paramValue, null, { disposeWhenNodeIsRemoved: elem });
}),
result = ko.utils.objectMap(rawParamComputedValues, function(paramValueComputed, paramName) {
var paramValue = paramValueComputed.peek();
// Does the evaluation of the parameter value unwrap any observables?
if (!paramValueComputed.isActive()) {
// No it doesn't, so there's no need for any computed wrapper. Just pass through the supplied value directly.
// Example: "someVal: firstName, age: 123" (whether or not firstName is an observable/computed)
return paramValueComputed.peek();
return paramValue;
} else {
// Yes it does. Supply a computed property that unwraps both the outer (binding expression)
// level of observability, and any inner (resulting model value) level of observability.
// This means the component doesn't have to worry about multiple unwrapping.
return ko.computed(function() {
return ko.utils.unwrapObservable(paramValueComputed());
}, null, { disposeWhenNodeIsRemoved: elem });
// This means the component doesn't have to worry about multiple unwrapping. If the value is a
// writable observable, the computed will also be writable and pass the value on to the observable.
return ko.computed({
'read': function() {
return ko.utils.unwrapObservable(paramValueComputed());
},
'write': ko.isWriteableObservable(paramValue) && function(value) {
paramValueComputed()(value);
},
disposeWhenNodeIsRemoved: elem
});
}
});

Expand Down
2 changes: 1 addition & 1 deletion src/subscribables/dependentObservable.js
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ ko.computed = ko.dependentObservable = function (evaluatorFunctionOrOptions, eva

dependentObservable.peek = peek;
dependentObservable.getDependenciesCount = function () { return _dependenciesCount; };
dependentObservable.hasWriteFunction = typeof options["write"] === "function";
dependentObservable.hasWriteFunction = typeof writeFunction === "function";
dependentObservable.dispose = function () { dispose(); };
dependentObservable.isActive = isActive;

Expand Down

0 comments on commit 348f9ea

Please sign in to comment.