Skip to content

Commit

Permalink
Basic binding provider support for custom elements (no param passing …
Browse files Browse the repository at this point in the history
…yet)
SteveSanderson committed Apr 24, 2014
1 parent 8012096 commit 85cbe7c
Showing 5 changed files with 132 additions and 6 deletions.
1 change: 1 addition & 0 deletions build/fragments/source-references.js
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@ knockoutDebugCallback([
'src/virtualElements.js',
'src/components/loaderRegistry.js',
'src/components/defaultLoader.js',
'src/components/customElements.js',
'src/binding/bindingProvider.js',
'src/binding/bindingAttributeSyntax.js',
'src/components/componentBinding.js',
87 changes: 87 additions & 0 deletions spec/components/customElementBehaviors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
describe('Components: Custom elements', function() {
beforeEach(function() {
jasmine.prepareTestNode();
jasmine.Clock.useMock();
});

afterEach(function() {
jasmine.Clock.reset();
ko.components.unregister('test-component');
});

it('Inserts components into custom elements with matching names', function() {
ko.components.register('test-component', {
template: 'custom element <span data-bind="text: 123"></span>'
});
var initialMarkup = '<div>hello <test-component></test-component></div>';
testNode.innerHTML = initialMarkup;

// Since components are loaded asynchronously, it doesn't show up synchronously
ko.applyBindings(null, testNode);
expect(testNode).toContainHtml(initialMarkup);

// ... but when the component is loaded, it does show up
jasmine.Clock.tick(1);
expect(testNode).toContainHtml('<div>hello <test-component>custom element <span data-bind="text: 123">123</span></test-component></div>');
});

it('Is possible to override getComponentNameForNode to determine which component goes into which element', function() {
ko.components.register('test-component', { template: 'custom element'});
this.restoreAfter(ko.components, 'getComponentNameForNode');

// Set up a getComponentNameForNode function that maps "A" tags to test-component
testNode.innerHTML = '<div>hello <a></a> <b>ignored</b></div>';
ko.components.getComponentNameForNode = function(node) {
return node.tagName === 'A' ? 'test-component' : null;
}

// See the component show up
ko.applyBindings(null, testNode);
jasmine.Clock.tick(1);
expect(testNode).toContainHtml('<div>hello <a>custom element</a> <b>ignored</b></div>');
});

it('Is possible to have regular data-bind bindings on a custom element, as long as they don\'t attempt to control descendants', function() {
ko.components.register('test-component', { template: 'custom element'});
testNode.innerHTML = '<test-component data-bind="visible: shouldshow"></test-component>';

// Bind with a viewmodel that controls visibility
var viewModel = { shouldshow: ko.observable(true) };
ko.applyBindings(viewModel, testNode);
jasmine.Clock.tick(1);
expect(testNode).toContainHtml('<test-component data-bind="visible: shouldshow">custom element</test-component>');
expect(testNode.childNodes[0].style.display).not.toBe('none');

// See that the 'visible' binding still works
viewModel.shouldshow(false);
expect(testNode.childNodes[0].style.display).toBe('none');
expect(testNode.childNodes[0].innerHTML).toBe('custom element');
});

it('Is not possible to have regular data-bind bindings on a custom element if they also attempt to control descendants', function() {
ko.components.register('test-component', { template: 'custom element'});
testNode.innerHTML = '<test-component data-bind="if: true"></test-component>';

expect(function() { ko.applyBindings(null, testNode); })
.toThrowContaining('Multiple bindings (if and component) are trying to control descendant bindings of the same element.');
});

it('Is possible to call applyBindings directly on a custom element', function() {
ko.components.register('test-component', { template: 'custom element'});
testNode.innerHTML = '<test-component></test-component>';
var customElem = testNode.childNodes[0];
expect(customElem.tagName).toBe('TEST-COMPONENT');

ko.applyBindings(null, customElem);
jasmine.Clock.tick(1);
expect(customElem.innerHTML).toBe('custom element');
});

it('Throws if you try to duplicate the \'component\' binding on a custom element that matches a component', function() {
ko.components.register('test-component', { template: 'custom element'});
testNode.innerHTML = '<test-component data-bind="component: {}"></test-component>';

expect(function() { ko.applyBindings(null, testNode); })
.toThrowContaining('Cannot use the "component" binding on a custom element matching a component');
});
});
1 change: 1 addition & 0 deletions spec/runner.html
Original file line number Diff line number Diff line change
@@ -56,6 +56,7 @@
<script type="text/javascript" src="components/loaderRegistryBehaviors.js"></script>
<script type="text/javascript" src="components/defaultLoaderBehaviors.js"></script>
<script type="text/javascript" src="components/componentBindingBehaviors.js"></script>
<script type="text/javascript" src="components/customElementBehaviors.js"></script>

<!-- Default bindings -->
<script type="text/javascript" src="defaultBindings/attrBehaviors.js"></script>
17 changes: 11 additions & 6 deletions src/binding/bindingProvider.js
Original file line number Diff line number Diff line change
@@ -8,20 +8,25 @@
ko.utils.extend(ko.bindingProvider.prototype, {
'nodeHasBindings': function(node) {
switch (node.nodeType) {
case 1: return node.getAttribute(defaultBindingAttributeName) != null; // Element
case 8: return ko.virtualElements.hasBindingValue(node); // Comment node
case 1: // Element
return node.getAttribute(defaultBindingAttributeName) != null
|| ko.components['getComponentNameForNode'](node);
case 8: // Comment node
return ko.virtualElements.hasBindingValue(node);
default: return false;
}
},

'getBindings': function(node, bindingContext) {
var bindingsString = this['getBindingsString'](node, bindingContext);
return bindingsString ? this['parseBindingsString'](bindingsString, bindingContext, node) : null;
var bindingsString = this['getBindingsString'](node, bindingContext),
parsedBindings = bindingsString ? this['parseBindingsString'](bindingsString, bindingContext, node) : null;
return ko.components.addBindingsForCustomElement(parsedBindings, node, bindingContext, /* valueAccessors */ false);
},

'getBindingAccessors': function(node, bindingContext) {
var bindingsString = this['getBindingsString'](node, bindingContext);
return bindingsString ? this['parseBindingsString'](bindingsString, bindingContext, node, {'valueAccessors':true}) : null;
var bindingsString = this['getBindingsString'](node, bindingContext),
parsedBindings = bindingsString ? this['parseBindingsString'](bindingsString, bindingContext, node, { 'valueAccessors': true }) : null;
return ko.components.addBindingsForCustomElement(parsedBindings, node, bindingContext, /* valueAccessors */ true);
},

// The following function is only used internally by this default provider.
32 changes: 32 additions & 0 deletions src/components/customElements.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
(function (undefined) {
// Overridable API for determining which component name applies to a given node. By overriding this,
// you can for example map specific tagNames to components that are not preregistered.
ko.components['getComponentNameForNode'] = function(node) {
var tagNameLower = ko.utils.tagNameLower(node);
return ko.components.isRegistered(tagNameLower) && tagNameLower;
};

ko.components.addBindingsForCustomElement = function(allBindings, node, bindingContext, valueAccessors) {
// Determine if it's really a custom element matching a component
if (node.nodeType === 1) {
var componentName = ko.components['getComponentNameForNode'](node);
if (componentName) {
// It does represent a component, so add a component binding for it
allBindings = allBindings || {};

if (allBindings['component']) {
// Avoid silently overwriting some other 'component' binding that may already be on the element
throw new Error('Cannot use the "component" binding on a custom element matching a component');
}

var componentBindingValue = { 'name': componentName };

allBindings['component'] = valueAccessors
? function() { return componentBindingValue; }
: componentBindingValue;
}
}

return allBindings;
}
})();

0 comments on commit 85cbe7c

Please sign in to comment.