Skip to content

Commit

Permalink
Support params.$raw.paramName for accessing raw computeds extracted f…
Browse files Browse the repository at this point in the history
…rom the binding
  • Loading branch information
SteveSanderson committed Apr 24, 2014
1 parent 7934d79 commit 24eb381
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 22 deletions.
49 changes: 48 additions & 1 deletion spec/components/customElementBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,24 @@ describe('Components: Custom elements', function() {
var suppliedParams = [];
ko.components.register('test-component', {
template: 'Ignored',
viewModel: function(params) { suppliedParams.push(params); }
viewModel: function(params) {
suppliedParams.push(params);

// The raw value for each param is a computed giving the literal value
ko.utils.objectForEach(params, function(key, value) {
if (key !== '$raw') {
expect(ko.isComputed(params.$raw[key])).toBe(true);
expect(params.$raw[key]()).toBe(value);
}
});
}
});

testNode.innerHTML = '<test-component params="nothing: null, num: 123, bool: true, obj: { abc: 123 }, str: \'mystr\'"></test-component>';
ko.applyBindings(null, testNode);
jasmine.Clock.tick(1);

delete suppliedParams[0].$raw; // Don't include '$raw' in the following assertion, as we only want to compare supplied values
expect(suppliedParams).toEqual([{ nothing: null, num: 123, bool: true, obj: { abc: 123 }, str: 'mystr' }]);
});

Expand All @@ -106,6 +117,10 @@ describe('Components: Custom elements', function() {
this.receivedobservable = params.suppliedobservable;
expect(this.receivedobservable.subprop).toBe('subprop');
this.dispose = function() { this.wasDisposed = true; };

// The $raw value for this param is a computed giving the observable instance
expect(ko.isComputed(params.$raw.suppliedobservable)).toBe(true);
expect(params.$raw.suppliedobservable()).toBe(params.suppliedobservable);
}
});

Expand Down Expand Up @@ -141,6 +156,12 @@ describe('Components: Custom elements', function() {
// See we didn't get the original observable instance. Instead we got a computed property.
expect(this.receivedobservable).not.toBe(rootViewModel.myobservable);
expect(ko.isComputed(this.receivedobservable)).toBe(true);

// 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
// observable, it's the same value as the regular (non-$raw) supplied parameter.
expect(ko.isComputed(params.$raw.suppliedobservable)).toBe(true);
expect(params.$raw.suppliedobservable()).toBe(params.suppliedobservable());
}
});

Expand Down Expand Up @@ -179,6 +200,15 @@ describe('Components: Custom elements', function() {

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

// See we can reach the original inner observable directly if needed via $raw
// (e.g., because it has subobservables or similar)
var originalObservable = params.$raw.somevalue();
expect(ko.isObservable(originalObservable)).toBe(true);
expect(ko.isComputed(originalObservable)).toBe(false);
if (originalObservable() === 'inner1') {
expect(originalObservable).toBe(innerObservable); // See there's no wrapper
}
}
});

Expand Down Expand Up @@ -219,6 +249,23 @@ describe('Components: Custom elements', function() {
expect(newInnerObservable.getSubscriptionsCount()).toBe(0);
});

it('Supplies any custom parameter called "$raw" in preference to the function that yields raw parameter values', function() {
var constructorCallCount = 0,
suppliedValue = {};
ko.components.register('test-component', {
template: 'Ignored',
viewModel: function(params) {
constructorCallCount++;
expect(params.$raw).toBe(suppliedValue);
}
});

testNode.innerHTML = '<test-component params="$raw: suppliedValue"></test-component>';
ko.applyBindings({ suppliedValue: suppliedValue }, testNode);
jasmine.Clock.tick(1);
expect(constructorCallCount).toBe(1);
});

it('Disposes the component when the custom element is cleaned', function() {
// This is really a behavior of the component binding, not custom elements.
// This spec just shows that custom elements don't break it for any reason.
Expand Down
47 changes: 26 additions & 21 deletions src/components/customElements.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,29 +36,34 @@
var paramsAttribute = elem.getAttribute('params');

if (paramsAttribute) {
var params = nativeBindingProviderInstance['parseBindingsString'](paramsAttribute, bindingContext, elem, { 'valueAccessors': true });
return ko.utils.objectMap(params, function(paramValue, paramName) {
// Does the evaluation of the parameter value unwrap any observables?
var computed = ko.computed(paramValue, null, { 'disposeWhenNodeIsRemoved': elem }),
computedValue = computed.peek();
if (!computed.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 computedValue;
} else {
// Yes it does. Is the resulting value itself observable?
if (!ko.isObservable(computedValue)) {
// No it isn't, so supply a computed property whose value is the result of the binding expression.
// Example: "someVal: firstName().length"
return computed;
var params = nativeBindingProviderInstance['parseBindingsString'](paramsAttribute, bindingContext, elem, { 'valueAccessors': true }),
rawParamComputedValues = ko.utils.objectMap(params, function(paramValue, paramName) {
return ko.computed(paramValue, null, { 'disposeWhenNodeIsRemoved': elem });
}),
result = ko.utils.objectMap(rawParamComputedValues, function(paramValueComputed, paramName) {
// 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();
} else {
// Yes it is, so create a further wrapper that supplies the inner unwrapped value (otherwise
// the component would have to double-unwrap this parameter to get the intended value).
// Example: "someVal: manager().firstName" (where firstName is observable)
return ko.computed(function() { return ko.utils.unwrapObservable(computed()); }, null, { 'disposeWhenNodeIsRemoved': elem });
// 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.unwrap(paramValueComputed());
}, null, { 'disposeWhenNodeIsRemoved': elem });
}
}
});
});

// Give access to the raw computeds, as long as that wouldn't overwrite any custom param also called 'raw'
// This is in case the developer wants to react to outer (binding) observability separately from inner
// (model value) observability seperately, or in case the model value observable has subobservables.
if (!result.hasOwnProperty('$raw')) {
result['$raw'] = rawParamComputedValues;
}

return result;
} else {
return null;
}
Expand Down

0 comments on commit 24eb381

Please sign in to comment.