Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion builds/knockout/spec/bindingAttributeBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -594,7 +594,7 @@ describe('Binding attribute syntax', function() {
expect(testNode).toContainHtml('<p>replaced</p><textarea>test</textarea><p>replaced</p>');
});

it('<template>', function() {
xit('<template>', function() { //Disabled because TKO allows binding in <template> elements
document.createElement('template'); // For old IE
testNode.innerHTML = "<p>Hello</p><template>test</template><p>Goodbye</p>";
ko.applyBindings({ sometext: 'hello' }, testNode);
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
"type": "git",
"url": "https://github.com/knockout/tko.git"
},
"scripts": {},
"scripts": {
"test": "make sweep && make && make test-headless"
},
"bugs": "https://github.com/knockout/tko/issues",
"licenses": [
{
Expand Down
3 changes: 1 addition & 2 deletions packages/bind/spec/bindingAttributeBehaviors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -876,10 +876,9 @@ describe('Binding attribute syntax', function () {
})

it('<template>', function () {
document.createElement('template') // For old IE
testNode.innerHTML = '<p>Hello</p><template>test</template><p>Goodbye</p>'
applyBindings({ sometext: 'hello' }, testNode)
expect(testNode).toContainHtml('<p>replaced</p><template>test</template><p>replaced</p>')
expect(testNode).toContainHtml('<p>replaced</p><template>replaced</template><p>replaced</p>')
})
})

Expand Down
26 changes: 15 additions & 11 deletions packages/bind/src/applyBindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,8 @@ type BindingHandlerOrUndefined = (typeof BindingHandler & BindingHandler) | unde
const bindingDoesNotRecurseIntoElementTypes = {
// Don't want bindings that operate on text nodes to mutate <script> and <textarea> contents,
// because it's unexpected and a potential XSS issue.
// Also bindings should not operate on <template> elements since this breaks in Internet Explorer
// and because such elements' contents are always intended to be bound in a different context
// from where they appear in the document.
script: true,
textarea: true,
template: true
textarea: true
}

function getBindingProvider(): Provider {
Expand Down Expand Up @@ -398,7 +394,7 @@ function applyBindingsToNodeInternal<T>(

/**
*
* @param {HTMLElement} node
* @param {Node} node
* @param {Object} bindings
* @param {[Promise]} nodeAsyncBindingPromises
*/
Expand Down Expand Up @@ -438,7 +434,7 @@ function getBindingContext<T = any>(
}

export function applyBindingAccessorsToNode<T = any>(
node: HTMLElement,
node: Node,
bindings: Record<string, any>,
viewModelOrBindingContext?: BindingContext<T> | Observable<T> | T,
asyncBindingsApplied?: Set<any>
Expand All @@ -456,7 +452,7 @@ export function applyBindingAccessorsToNode<T = any>(
}

export function applyBindingsToNode<T = any>(
node: HTMLElement,
node: Node,
bindings: Record<string, any>,
viewModelOrBindingContext: BindingContext<T> | Observable<T> | T
): BindingResult {
Expand All @@ -473,7 +469,11 @@ export function applyBindingsToDescendants<T = any>(
): BindingResult {
const asyncBindingsApplied = new Set()
const bindingContext = getBindingContext(viewModelOrBindingContext)
if (rootNode.nodeType === Node.ELEMENT_NODE || rootNode.nodeType === Node.COMMENT_NODE) {
if (
rootNode.nodeType === Node.ELEMENT_NODE
|| rootNode.nodeType === Node.COMMENT_NODE
|| rootNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE
) {
applyBindingsToDescendantsInternal(bindingContext, rootNode, asyncBindingsApplied)
return new BindingResult({ asyncBindingsApplied, rootNode, bindingContext })
}
Expand All @@ -482,7 +482,7 @@ export function applyBindingsToDescendants<T = any>(

export function applyBindings<T = any>(
viewModelOrBindingContext: BindingContext<T> | Observable<T> | T,
rootNode: HTMLElement,
rootNode: Node,
extendContextCallback?: BindingContextExtendCallback<T>
): Promise<unknown> {
const asyncBindingsApplied = new Set()
Expand All @@ -493,7 +493,11 @@ export function applyBindings<T = any>(
if (!rootNode) {
throw Error('ko.applyBindings: could not find window.document.body; has the document been loaded?')
}
} else if (rootNode.nodeType !== Node.ELEMENT_NODE && rootNode.nodeType !== Node.COMMENT_NODE) {
} else if (
rootNode.nodeType !== Node.ELEMENT_NODE
&& rootNode.nodeType !== Node.COMMENT_NODE
&& rootNode.nodeType !== Node.DOCUMENT_FRAGMENT_NODE
) {
throw Error('ko.applyBindings: first parameter should be your view model; second parameter should be a DOM node')
}
const rootContext = getBindingContext<T>(viewModelOrBindingContext, extendContextCallback)
Expand Down
33 changes: 33 additions & 0 deletions packages/binding.component/spec/componentBindingBehaviors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1203,6 +1203,39 @@ describe('Components: Component binding', function () {
expect(innerText).toEqual(`X beep Y Gamma Zeta Q`)
})

it('processes default and named slots in template', function () {
const fragment = document.createDocumentFragment()
const template = document.createElement('template') as HTMLTemplateElement
fragment.appendChild(template)

template.innerHTML = `
<test-component>
<template slot='alpha'>beep</template>
Gamma
<div>Zeta</div>
</test-component>
`
class ViewModel extends components.ComponentABC {
static override get template() {
return `
<div>
X <slot name='alpha'></slot> Y <slot></slot> Q
</div>
`
}
}
ViewModel.register('test-component')

const copy = template.cloneNode(true) as HTMLTemplateElement
applyBindings(outerViewModel, copy)

const innerText = (copy.content.children[0] as HTMLElement).innerText.replace(/\s+/g, ' ').trim()
expect(innerText).toEqual(`X beep Y Gamma Zeta Q`)

const innerTextOrg = (template.content.children[0] as HTMLElement).innerText.replace(/\s+/g, ' ').trim()
expect(innerTextOrg).toEqual('Gamma Zeta')
})

it('inserts all component template nodes in an unnamed (default) slot', function () {
testNode.innerHTML = `
<test-component>
Expand Down
2 changes: 1 addition & 1 deletion packages/binding.foreach/src/foreach.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ export class ForEachBinding extends AsyncBindingHandler {
*/
activeChildElement(node) {
const active = document.activeElement
if (domNodeIsContainedBy(active!, node)) {
if (domNodeIsContainedBy(active, node)) {
return active
}
return null
Expand Down
10 changes: 10 additions & 0 deletions packages/binding.template/spec/foreachBehaviors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,16 @@ describe('Binding: Foreach', function () {
)
})

it('Should be able to use $data to reference each array item being bound in HTMLTemplate with HTMLSlotElement', function () {
testNode.innerHTML =
"<template><div data-bind='foreach: someItems'><slot data-bind='text: $data'></slot></div></template>"
const someItems = ['alpha', 'beta']
applyBindings({ someItems: someItems }, testNode)
expect((testNode.childNodes[0] as HTMLTemplateElement).content.firstChild).toContainHtml(
'<slot data-bind="text: $data">alpha</slot><slot data-bind="text: $data">beta</slot>'
)
})

it('Should add and remove nodes to match changes in the bound array', function () {
testNode.innerHTML = "<div data-bind='foreach: someItems'><span data-bind='text: childProp'></span></div>"
const someItems = observableArray([{ childProp: 'first child' }, { childProp: 'second child' }])
Expand Down
36 changes: 36 additions & 0 deletions packages/binding.template/spec/nativeTemplateEngineBehaviors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,42 @@ describe('Native template engine', function () {
})
})

describe('Template data context', function () {
it('should set $data to the template data value', function () {
const fragment = document.createDocumentFragment()
const template = document.createElement('template') as HTMLTemplateElement
fragment.appendChild(template)

template.innerHTML =
"<div data-bind='template: { data: someItem }'>" + "Value: <span data-bind='text: $data.val'></span>" + '</div>'
applyBindings({ someItem: { val: 'abc' } }, template)
expect(template.content.childNodes[0]).toContainText('Value: abc')
})

it('applyBindings to fragment should set $data to the template data value', function () {
const fragment = document.createDocumentFragment()
const template = document.createElement('template') as HTMLTemplateElement
fragment.appendChild(template)

template.innerHTML =
"<div data-bind='template: { data: someItem }'>" + "Value: <span data-bind='text: $data.val'></span>" + '</div>'
applyBindings({ someItem: { val: 'abc' } }, fragment)
expect(template.content.childNodes[0]).toContainText('Value: abc')
})

it('should set $data to the DIV at DocumentFragment', function () {
const fragment = document.createDocumentFragment()
const div = document.createElement('div') as HTMLDivElement

div.innerHTML =
"<div data-bind='template: { data: someItem }'>" + "Value: <span data-bind='text: $data.val'></span>" + '</div>'

fragment.appendChild(div)
applyBindings({ someItem: { val: 'abc' } }, div)
expect(div.childNodes[0]).toContainText('Value: abc')
})
})

describe('Data-bind syntax', function () {
it('should expose parent binding context as $parent if binding with an explicit \"data\" value', function () {
testNode.innerHTML =
Expand Down
21 changes: 21 additions & 0 deletions packages/utils/spec/domNodeDisposalBehaviors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,27 @@ describe('DOM node disposal', function () {
expect(testNode.childNodes.length).toEqual(0)
})

it('Should run registered disposal callbacks when a document fragment is cleaned', function () {
let didRunCount = 0

const fragment = document.createDocumentFragment()
const childNode = document.createElement('DIV')
const spanNode = document.createElement('SPAN')
childNode.appendChild(spanNode)
fragment.appendChild(childNode)

const array = [fragment, childNode, spanNode]
array.forEach(node => {
addDisposeCallback(node, function () {
didRunCount++
})
})

expect(didRunCount).toEqual(0)
cleanNode(fragment)
expect(didRunCount).toEqual(array.length)
})

it('Should be able to remove previously-registered disposal callbacks', function () {
let didRun = false
const callback = function () {
Expand Down
61 changes: 61 additions & 0 deletions packages/utils/spec/utilsDomBehaviors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as utils from '../dist'
import { registerEventHandler, virtualElements } from '../dist'
import options from '../dist/options'
import type { KnockoutInstance } from '@tko/builder'
import { domNodeIsContainedBy } from '../src'

const ko: KnockoutInstance = globalThis.ko || {}
ko.utils = utils
Expand All @@ -16,6 +17,66 @@ describe('startCommentRegex', function () {
})
})

describe('DOM-Info Tool', function () {
it('domNodeIsContainedBy with special values', function () {
const parent = document.createElement('div')
const test = document.createDocumentFragment()

expect(domNodeIsContainedBy(parent, test)).toBe(false)
expect(domNodeIsContainedBy(test, parent)).toBe(false)

expect(domNodeIsContainedBy(parent, undefined)).toBe(false)
expect(domNodeIsContainedBy(parent, null)).toBe(false)
expect(domNodeIsContainedBy(null, parent)).toBe(false)

test.appendChild(parent)

expect(domNodeIsContainedBy(parent, test)).toBe(true)
expect(domNodeIsContainedBy(test, parent)).toBe(false)

const testDiv = document.createElement('div')
const template = document.createElement('template')
template.content.appendChild(testDiv)
expect(domNodeIsContainedBy(testDiv, template)).toBe(false) //Because template.content is a DocumentFragment
expect(domNodeIsContainedBy(testDiv, template.content)).toBe(true)

parent.appendChild(template)
expect(domNodeIsContainedBy(template, parent)).toBe(true)
expect(domNodeIsContainedBy(template, test)).toBe(true)
expect(domNodeIsContainedBy(testDiv, parent)).toBe(false) //Because template.content is a DocumentFragment
})

it('Parent Node contains child', function () {
const parent = document.createElement('div')
const child = document.createElement('span')
parent.appendChild(child)

expect(domNodeIsContainedBy(child, parent)).toBe(true)
})

it('Node not contains node', function () {
const parent = document.createElement('div')
const child = document.createElement('span')

expect(domNodeIsContainedBy(child, parent)).toBe(false)
})

it('Parent Node contains subchild', function () {
const parent = document.createElement('div')
const child = document.createElement('span')
const subchild = document.createElement('em')
parent.appendChild(child)
child.appendChild(subchild)

const node = document.createTextNode('text')

expect(domNodeIsContainedBy(subchild, parent)).toBe(true)
expect(domNodeIsContainedBy(subchild, child)).toBe(true)
expect(domNodeIsContainedBy(subchild, subchild)).toBe(true)
expect(domNodeIsContainedBy(node, parent)).toBe(false)
})
})

describe('setTextContent', function () {
let element: HTMLElement

Expand Down
15 changes: 10 additions & 5 deletions packages/utils/src/dom/disposal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ const domDataKey = domData.nextKey()
// 1: Element
// 8: Comment
// 9: Document
const cleanableNodeTypes = { 1: true, 8: true, 9: true }
const cleanableNodeTypesWithDescendants = { 1: true, 9: true }
// 11: DocumentFragment
const cleanableNodeTypes = { 1: true, 8: true, 9: true, 11: true }
const cleanableNodeTypesWithDescendants = { 1: true, 9: true, 11: true }

function getDisposeCallbacksCollection(node: Node, createIfNotFound: boolean) {
let allDisposeCallbacks = domData.get(node, domDataKey)
Expand Down Expand Up @@ -86,14 +87,18 @@ export function removeDisposeCallback(node: Node, callback: (node: Node) => void
}
}

export function cleanNode(node: Node): typeof node {
export function cleanNode(node: Node): Node {
// First clean this node, where applicable
if (cleanableNodeTypes[node.nodeType]) {
cleanSingleNode(node)

// ... then its descendants, where applicable
if (cleanableNodeTypesWithDescendants[node.nodeType] && node instanceof Element) {
cleanNodesInList(node.getElementsByTagName('*'))
if (cleanableNodeTypesWithDescendants[node.nodeType]) {
if (node instanceof Element) {
cleanNodesInList(node.getElementsByTagName('*'))
} else if (node instanceof Document || node instanceof DocumentFragment) {
cleanNodesInList(node.querySelectorAll('*'))
}
}
}
return node
Expand Down
Loading