Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/master' into fix-svg-css-binding
Browse files Browse the repository at this point in the history
Conflicts:
	spec/defaultBindings/cssBehaviors.js
  • Loading branch information
brianmhunt committed Nov 13, 2014
2 parents 810e4ff + ff6b858 commit 864e19c
Show file tree
Hide file tree
Showing 18 changed files with 435 additions and 97 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "knockout",
"description": "Knockout makes it easier to create rich, responsive UIs with JavaScript",
"homepage": "http://knockoutjs.com/",
"version": "3.2.0",
"version": "3.3.0-alpha",
"license": "MIT",
"author": "The Knockout.js team",
"main": "build/output/knockout-latest.debug.js",
Expand Down
182 changes: 142 additions & 40 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 Expand Up @@ -477,46 +541,6 @@ describe('Components: Component binding', function() {
expect(testNode.firstChild).toContainHtml('');
});

it('Does not automatically subscribe to any observables you evaluate during createViewModel or a viewmodel constructor', function() {
// This clarifies that, if a developer wants to react when some observable parameter
// changes, then it's their responsibility to subscribe to it or use a computed.
// We don't rebuild the component just because you evaluated an observable from inside
// your viewmodel constructor, just like we don't if you evaluate one elsewhere
// in the viewmodel code.

// Note that currently, this behavior happens as a fluke of the implementation: because
// component loads always complete asynchronously, any observables accessed inside
// createViewModel or later don't get registered as dependencies on the binding computed.
// If we ever change that to support synchronous component loads, this test would
// start failing because we'd have broken this behavior.

var createViewModelCallCount = 0;
ko.components.register(testComponentName, {
viewModel: {
createViewModel: function(params, componentInfo) {
createViewModelCallCount++;
return { someData: params.someData() };
}
},
template: '<div data-bind="text: someData"></div>'
});

// Bind an instance
testComponentParams.someData = ko.observable('First');
ko.applyBindings(outerViewModel, testNode);
jasmine.Clock.tick(1);
expect(testNode).toContainText('First');
expect(createViewModelCallCount).toBe(1);

// See that changing the observable will have no effect, because the viewmodel
// itself doesn't subscribe to it.
testComponentParams.someData('Second');
jasmine.Clock.tick(1);
expect(testNode).toContainText('First');
expect(testComponentParams.someData.getSubscriptionsCount()).toBe(0);
expect(createViewModelCallCount).toBe(1); // ... and we didn't rebuild the component
});

it('Disregards component load completions that are no longer relevant', function() {
// This spec addresses the possibility of a race condition: if you change the
// component name faster than the component loads complete, then we need to
Expand Down Expand Up @@ -621,4 +645,82 @@ describe('Components: Component binding', function() {
testComponentParams.someData(456);
expect(testNode).toContainText('Hello! Your param is 456 Goodbye.');
});

describe('Does not automatically subscribe to any observables you evaluate during createViewModel or a viewmodel constructor', function() {
// This clarifies that, if a developer wants to react when some observable parameter
// changes, then it's their responsibility to subscribe to it or use a computed.
// We don't rebuild the component just because you evaluated an observable from inside
// your viewmodel constructor, just like we don't if you evaluate one elsewhere
// in the viewmodel code.

it('when loaded asynchronously', function() {
ko.components.register(testComponentName, {
viewModel: {
createViewModel: function(params, componentInfo) {
return { someData: params.someData() };
}
},
template: '<div data-bind="text: someData"></div>'
});

// Bind an instance
testComponentParams.someData = ko.observable('First');
ko.applyBindings(outerViewModel, testNode);
jasmine.Clock.tick(1);
expect(testNode).toContainText('First');
expect(testComponentParams.someData.getSubscriptionsCount()).toBe(0);

// See that changing the observable will have no effect
testComponentParams.someData('Second');
jasmine.Clock.tick(1);
expect(testNode).toContainText('First');
});

it('when loaded synchronously', function() {
ko.components.register(testComponentName, {
synchronous: true,
viewModel: {
createViewModel: function(params, componentInfo) {
return { someData: params.someData() };
}
},
template: '<div data-bind="text: someData"></div>'
});

// Bind an instance
testComponentParams.someData = ko.observable('First');
ko.applyBindings(outerViewModel, testNode);
expect(testNode).toContainText('First');
expect(testComponentParams.someData.getSubscriptionsCount()).toBe(0);

// See that changing the observable will have no effect
testComponentParams.someData('Second');
expect(testNode).toContainText('First');
});

it('when cached component is loaded synchronously', function() {
ko.components.register(testComponentName, {
synchronous: true,
viewModel: {
createViewModel: function(params, componentInfo) {
return { someData: params.someData() };
}
},
template: '<div data-bind="text: someData"></div>'
});

// Load the component manually so that the next load happens from the cache
ko.components.get(testComponentName, function() {});

// Bind an instance
testComponentParams.someData = ko.observable('First');
ko.applyBindings(outerViewModel, testNode);
expect(testNode).toContainText('First');
expect(testComponentParams.someData.getSubscriptionsCount()).toBe(0);

// See that changing the observable will have no effect
testComponentParams.someData('Second');
expect(testNode).toContainText('First');
});
});
});
40 changes: 27 additions & 13 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 Expand Up @@ -407,14 +422,13 @@ describe('Components: Custom elements', function() {
ko.components.unregister('special-list');
});

// First define a reusable 'special-list' component that produces a <ul> in which the <li>s have special CSS classes
// It also injects and binds a supplied template for each list item
// First define a reusable 'special-list' component that produces a <ul> in which the <li>s are filled with the supplied template
// Note: It would be even simpler to write "template: { nodes: $componentTemplateNodes }", which would also work.
// However it's useful to have test coverage for the more longwinded approach of passing nodes via your
// viewmodel as well, so retaining the longer syntax for this test.
ko.components.register('special-list', {
template: '<ul class="my-special-list" data-bind="foreach: specialListItems">'
+ '<li class="special-list-item" data-bind="template: { nodes: $component.suppliedItemTemplate }">'
+ '<li data-bind="template: { nodes: $component.suppliedItemTemplate }">'
+ '</li>'
+ '</ul>',
viewModel: {
Expand All @@ -430,7 +444,7 @@ describe('Components: Custom elements', function() {
// Now make some view markup that uses <special-list> and supplies a template to be used inside each list item
testNode.innerHTML = '<h1>Cheeses</h1>'
+ '<special-list params="items: cheeses">'
+ '<em data-bind="text: name"></em> has quality <em data-bind="text: quality"></em>'
+ '<em data-bind="text: name">x</em> has quality <em data-bind="text: quality">x</em>'
+ '</special-list>';

// Finally, bind it all to some data
Expand All @@ -447,13 +461,13 @@ describe('Components: Custom elements', function() {
expect(testNode.childNodes[1].childNodes[0].tagName.toLowerCase()).toEqual('ul');
expect(testNode.childNodes[1].childNodes[0].className).toEqual('my-special-list');
expect(testNode.childNodes[1].childNodes[0]).toContainHtml(
'<li class="special-list-item" data-bind="template: { nodes: $component.supplieditemtemplate }">'
'<li data-bind="template: { nodes: $component.supplieditemtemplate }">'
+ '<em data-bind="text: name">brie</em> has quality <em data-bind="text: quality">7</em>'
+ '</li>'
+ '<li class="special-list-item" data-bind="template: { nodes: $component.supplieditemtemplate }">'
+ '<li data-bind="template: { nodes: $component.supplieditemtemplate }">'
+ '<em data-bind="text: name">cheddar</em> has quality <em data-bind="text: quality">9</em>'
+ '</li>'
+ '<li class="special-list-item" data-bind="template: { nodes: $component.supplieditemtemplate }">'
+ '<li data-bind="template: { nodes: $component.supplieditemtemplate }">'
+ '<em data-bind="text: name">roquefort</em> has quality <em data-bind="text: quality">3</em>'
+ '</li>'
);
Expand Down
21 changes: 21 additions & 0 deletions spec/dependentObservableBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,27 @@ describe('Dependent Observable', function() {
expect(result()).toEqual(42);
});

it('Should fire "awake" event when deferred computed is first evaluated', function() {
var data = ko.observable('A'),
computed = ko.computed({ read: data, deferEvaluation: true });

var notifySpy = jasmine.createSpy('notifySpy');
computed.subscribe(notifySpy, null, 'awake');

expect(notifySpy).not.toHaveBeenCalled();

expect(computed()).toEqual('A');
expect(notifySpy).toHaveBeenCalledWith('A');
expect(notifySpy.calls.length).toBe(1);

// Subscribing or updating data shouldn't trigger any more notifications
notifySpy.reset();
computed.subscribe(function() {});
data('B');
computed();
expect(notifySpy).not.toHaveBeenCalled();
});

it('Should prevent recursive calling of read function', function() {
var observable = ko.observable(0),
computed = ko.computed(function() {
Expand Down
2 changes: 1 addition & 1 deletion spec/lib/jasmine.extensions.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jasmine.Matchers.prototype.toContainHtml = function (expectedHtml) {
};

jasmine.nodeText = function(node) {
return 'textContent' in node ? node.textContent : node.innerText;
return node.nodeType == 3 ? node.data : 'textContent' in node ? node.textContent : node.innerText;
}

jasmine.Matchers.prototype.toContainText = function (expectedText) {
Expand Down
Loading

0 comments on commit 864e19c

Please sign in to comment.