Skip to content

Commit

Permalink
#5 89b056e
Browse files Browse the repository at this point in the history
  • Loading branch information
brianmhunt committed Dec 20, 2017
1 parent e3efe66 commit d341bcb
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 70 deletions.
101 changes: 97 additions & 4 deletions packages/tko.binding.if/spec/withBehaviors.js
Expand Up @@ -4,7 +4,7 @@ import {
} from 'tko.utils'

import {
applyBindings, contextFor
applyBindings, contextFor, dataFor
} from 'tko.bind'

import {
Expand Down Expand Up @@ -167,7 +167,100 @@ describe('Binding: With', function () {
expect(contextFor(firstSpan).$parents[1].name).toEqual('top')
})

it('Should be able to define an \"with\" region using a containerless template', function () {
it('Should be able to access all parent bindings when using "as"', async function () {
testNode.innerHTML = `<div data-bind='with: topItem'>
<div data-bind="with: middleItem, as: 'middle'">
<div data-bind='with: middle.bottomItem'>
<span data-bind='text: name'></span>
<span data-bind='text: $parent.name'></span>
<span data-bind='text: middle.name'></span>
<span data-bind='text: $parents[1].name'></span>
<span data-bind='text: $root.name'></span>
</div>
</div>
</div>`
applyBindings({
name: 'outer',
topItem: {
name: 'top',
middleItem: {
name: 'middle',
bottomItem: {
name: 'bottom'
}
}
}
}, testNode)
const finalContainer = testNode.childNodes[0].children[0].children[0]
// This differs from ko 3.x in that `with` does not create a child context
// when using `as`.
const [name, parentName, middleName, parent1Name, rootName] = finalContainer.children
expect(name).toContainText('bottom')
expect(parentName).toContainText('top')
expect(middleName).toContainText('middle')
expect(parent1Name).toContainText('outer')
expect(rootName).toContainText('outer')
expect(contextFor(name).parents.length).toEqual(2)
})

it('Should not create a child context', function () {
testNode.innerHTML = "<div data-bind='with: someItem, as: \"item\"'><span data-bind='text: item.childProp'></span></div>"
var someItem = { childProp: 'Hello' }
applyBindings({ someItem: someItem }, testNode)

expect(testNode.childNodes[0].childNodes[0]).toContainText('Hello')
expect(dataFor(testNode.childNodes[0].childNodes[0])).toEqual(dataFor(testNode))
})

it('Should provide access to observable value', function () {
testNode.innerHTML = "<div data-bind='with: someItem, as: \"item\"'><input data-bind='value: item'/></div>"
var someItem = observable('Hello')
applyBindings({ someItem: someItem }, testNode)
expect(testNode.childNodes[0].childNodes[0].value).toEqual('Hello')

expect(dataFor(testNode.childNodes[0].childNodes[0])).toEqual(dataFor(testNode))

// Should update observable when input is changed
testNode.childNodes[0].childNodes[0].value = 'Goodbye'
triggerEvent(testNode.childNodes[0].childNodes[0], 'change')
expect(someItem()).toEqual('Goodbye')

// Should update the input when the observable changes
someItem('Hello again')
expect(testNode.childNodes[0].childNodes[0].value).toEqual('Hello again')
})

it('Should not re-render the nodes when an observable value changes', function () {
testNode.innerHTML = "<div data-bind='with: someItem, as: \"item\"'><span data-bind='text: item'></span></div>"
var someItem = observable('first')
applyBindings({ someItem }, testNode)
expect(testNode.childNodes[0]).toContainText('first')

var saveNode = testNode.childNodes[0].childNodes[0]
someItem('second')
expect(testNode.childNodes[0]).toContainText('second')
expect(testNode.childNodes[0].childNodes[0]).toEqual(saveNode)
})

it('Should remove nodes with an observable value become falsy', function () {
var someItem = observable(undefined)
testNode.innerHTML = "<div data-bind='with: someItem, as: \"item\"'><span data-bind='text: item().occasionallyExistentChildProp'></span></div>"
applyBindings({ someItem: someItem }, testNode)

// First it's not there
expect(testNode.childNodes[0].childNodes.length).toEqual(0)

// Then it's there
someItem({ occasionallyExistentChildProp: 'Child prop value' })
expect(testNode.childNodes[0].childNodes.length).toEqual(1)
expect(testNode.childNodes[0].childNodes[0]).toContainText('Child prop value')

// Then it's gone again
someItem(null)
expect(testNode.childNodes[0].childNodes.length).toEqual(0)
})

it('Should be able to define an "with" region using a containerless template', function () {
var someitem = observable(undefined)
testNode.innerHTML = 'hello <!-- ko with: someitem --><span data-bind="text: occasionallyexistentchildprop"></span><!-- /ko --> goodbye'
applyBindings({ someitem: someitem }, testNode)
Expand All @@ -184,7 +277,7 @@ describe('Binding: With', function () {
expect(testNode).toContainHtml('hello <!-- ko with: someitem --><!-- /ko --> goodbye')
})

it('Should be able to nest \"with\" regions defined by containerless templates', function () {
it('Should be able to nest "with" regions defined by containerless templates', function () {
testNode.innerHTML = 'hello <!-- ko with: topitem -->' +
'Got top: <span data-bind="text: topprop"></span>' +
'<!-- ko with: childitem -->' +
Expand Down Expand Up @@ -243,7 +336,7 @@ describe('Binding: With', function () {
self.items = observableArray([{ x: observable(4) }])
self.getTotal = function () {
var total = 0
arrayForEach(self.items(), item => total += item.x())
arrayForEach(self.items(), item => { total += item.x() })
return total
}
}
Expand Down
69 changes: 17 additions & 52 deletions packages/tko.binding.if/src/with.js
@@ -1,74 +1,39 @@
import {
unwrap, dependencyDetection
unwrap
} from 'tko.observable'

import ConditionalBindingHandler from './ConditionalBindingHandler'

import {
cloneNodes, virtualElements
} from 'tko.utils'

import { computed } from 'tko.computed'
import {
applyBindingsToDescendants, AsyncBindingHandler
bindingContext, inheritParentIndicator
} from 'tko.bind'

/**
* ⁉️ ⁉️ ⁉️ ⁉️ ⁉️ ⁉️
* Why this works and the other does not is a problematic quagmire to debug.
*
* If you look at _evalIfChanged in computed.js, it's called 9 times for this
* version, but only 4 for the other version.
*/
export class WithBindingHandler extends AsyncBindingHandler {
constructor (...args) {
super(...args)
const element = this.$element
let savedNodes

this.computed(() => {
const shouldDisplay = !!unwrap(this.value)
const isFirstRender = !savedNodes

// Save a copy of the inner nodes on the initial update, but only if we have dependencies.
if (isFirstRender && dependencyDetection.getDependenciesCount()) {
savedNodes = cloneNodes(virtualElements.childNodes(element), true /* shouldCleanNodes */)
}

if (shouldDisplay) {
if (!isFirstRender) {
virtualElements.setDomNodeChildren(element, cloneNodes(savedNodes))
}
applyBindingsToDescendants(
this.$context.createChildContext(this.valueAccessor),
element)
} else {
virtualElements.emptyNode(element)
}
})
}

get controlsDescendants () { return true }
static get allowVirtualElements () { return true }
}
import ConditionalBindingHandler from './ConditionalBindingHandler'

/**
* The following fails somewhere in the `limit` functions of Observables i.e.
* it's an issue related to async/deferUpdates.
*/
export class WithBindingHandlerFailsInexplicably extends ConditionalBindingHandler {
export class WithBindingHandler extends ConditionalBindingHandler {
constructor (...args) {
super(...args)
this.asOption = this.allBindings.get('as')

// If given `as`, reduce the condition to a boolean, so it does not
// change & refresh when the value is updated.
const conditionalFn = this.asOption
? () => Boolean(unwrap(this.value)) : () => unwrap(this.value)
this.conditional = this.computed(conditionalFn)

this.computed('render')
}

get bindingContext () {
return this.$context.createStaticChildContext(this.valueAccessor)
return this.asOption
? this.$context.extend({[this.asOption]: this.value})
: this.$context.createChildContext(this.valueAccessor)
}

renderStatus () {
if (typeof this.value === 'function') { this.value() } // Create dependency
const shouldDisplay = !!unwrap(this.value)
return {shouldDisplay}
const shouldDisplay = Boolean(this.conditional())
return { shouldDisplay }
}
}
72 changes: 62 additions & 10 deletions packages/tko.binding.template/spec/foreachBehaviors.js
@@ -1,5 +1,6 @@
/* global testNode */
import {
domData, setHtml, triggerEvent, removeNode, virtualElements
domData, setHtml, triggerEvent, removeNode, virtualElements, options
} from 'tko.utils'

import {
Expand All @@ -14,10 +15,6 @@ import { MultiProvider } from 'tko.provider.multi'
import { VirtualProvider } from 'tko.provider.virtual'
import { DataBindProvider } from 'tko.provider.databind'

import {
options
} from 'tko.utils'

import {bindings as templateBindings} from '../src'
import {bindings as ifBindings} from 'tko.binding.if'
import {bindings as coreBindings} from 'tko.binding.core'
Expand Down Expand Up @@ -389,7 +386,7 @@ describe('Binding: Foreach', function () {
expect(testNode.childNodes[0]).toContainText('added childfirst childhidden child')
})

it("Should double-unwrap if the internal is iterable", function () {
it('Should double-unwrap if the internal is iterable', function () {
testNode.innerHTML = "<div data-bind='foreach: myArray'><span data-bind='text: $data'></span></div>"
var myArrayWrapped = observable(observableArray(['data value']))
applyBindings({ myArray: myArrayWrapped }, testNode)
Expand Down Expand Up @@ -585,7 +582,7 @@ describe('Binding: Foreach', function () {

it('Should be able to give an alias to $data using \"as\", and use it within a nested loop', function () {
testNode.innerHTML = "<div data-bind='foreach: { data: someItems, as: \"item\" }'>" +
"<span data-bind='foreach: sub'>" +
"<span data-bind='foreach: item.sub'>" +
"<span data-bind='text: item.name+\":\"+$data'></span>," +
'</span>' +
'</div>'
Expand All @@ -596,7 +593,7 @@ describe('Binding: Foreach', function () {

it('Should be able to set up multiple nested levels of aliases using \"as\"', function () {
testNode.innerHTML = "<div data-bind='foreach: { data: someItems, as: \"item\" }'>" +
"<span data-bind='foreach: { data: sub, as: \"subvalue\" }'>" +
"<span data-bind='foreach: { data: item.sub, as: \"subvalue\" }'>" +
"<span data-bind='text: item.name+\":\"+subvalue'></span>," +
'</span>' +
'</div>'
Expand Down Expand Up @@ -645,7 +642,7 @@ describe('Binding: Foreach', function () {
}
})

it('Should provide access to observable array items through $rawData', function () {
it('Should provide access to observable items through $rawData', function () {
testNode.innerHTML = "<div data-bind='foreach: someItems'><input data-bind='value: $rawData'/></div>"
var x = observable('first'), y = observable('second'), someItems = observableArray([ x, y ])
applyBindings({ someItems: someItems }, testNode)
Expand All @@ -665,7 +662,7 @@ describe('Binding: Foreach', function () {
expect(testNode.childNodes[0]).toHaveValues(['third'])
})

it('Should not re-render the nodes when a observable array item changes', function () {
it('Should not re-render the nodes when an observable item changes', function () {
testNode.innerHTML = "<div data-bind='foreach: someItems'><span data-bind='text: $data'></span></div>"
var x = observable('first'), someItems = [ x ]
applyBindings({ someItems: someItems }, testNode)
Expand Down Expand Up @@ -722,6 +719,61 @@ describe('Binding: Foreach', function () {
expect(testNode).toContainText('--Mercury++--Venus++--Earth++--Mars++--Jupiter++--Saturn++')
})

/*
describe('With "noChildContextWithAs" and "as"')
There is no option in ko 4 for noChildContextWithAs; the default
is that there is no child context when `as` is used, for both the
`foreach` binding and the `template { foreach }`.
*/

it('Should not create a child context when `as` is used', function () {
testNode.innerHTML = "<div data-bind='foreach: { data: someItems, as: \"item\" }'><span data-bind='text: item'></span></div>"
var someItems = ['alpha', 'beta']
applyBindings({ someItems: someItems }, testNode)

expect(testNode.childNodes[0].childNodes[0]).toContainText('alpha')
expect(testNode.childNodes[0].childNodes[1]).toContainText('beta')

expect(dataFor(testNode.childNodes[0].childNodes[0])).toEqual(dataFor(testNode))
expect(dataFor(testNode.childNodes[0].childNodes[1])).toEqual(dataFor(testNode))
})

it('Should provide access to observable items', function () {
testNode.innerHTML = "<div data-bind='foreach: { data: someItems, as: \"item\" }'><input data-bind='value: item'/></div>"
var x = observable('first'), y = observable('second'), someItems = observableArray([ x, y ])
applyBindings({ someItems: someItems }, testNode)
expect(testNode.childNodes[0]).toHaveValues(['first', 'second'])

expect(dataFor(testNode.childNodes[0].childNodes[0])).toEqual(dataFor(testNode))
expect(dataFor(testNode.childNodes[0].childNodes[1])).toEqual(dataFor(testNode))

// Should update observable when input is changed
testNode.childNodes[0].childNodes[0].value = 'third'
triggerEvent(testNode.childNodes[0].childNodes[0], 'change')
expect(x()).toEqual('third')

// Should update the input when the observable changes
y('fourth')
expect(testNode.childNodes[0]).toHaveValues(['third', 'fourth'])

// Should update the inputs when the array changes
someItems([x])
expect(testNode.childNodes[0]).toHaveValues(['third'])
})

it('Should not re-render the nodes when an observable item changes', function () {
testNode.innerHTML = "<div data-bind='foreach: { data: someItems, as: \"item\" }'><span data-bind='text: item'></span></div>"
var x = observable('first'), someItems = [ x ]
applyBindings({ someItems: someItems }, testNode)
expect(testNode.childNodes[0]).toContainText('first')

var saveNode = testNode.childNodes[0].childNodes[0]
x('second')
expect(testNode.childNodes[0]).toContainText('second')
expect(testNode.childNodes[0].childNodes[0]).toEqual(saveNode)
})

it('Can modify the set of top-level nodes in a foreach loop', function () {
options.bindingProviderInstance.preprocessNode = function (node) {
// Replace <data /> with <span data-bind="text: $data"></span>
Expand Down
13 changes: 9 additions & 4 deletions packages/tko.binding.template/src/templating.js
Expand Up @@ -196,10 +196,15 @@ export default function renderTemplateForEach (template, arrayOrObservableArray,

// This will be called by setDomNodeChildrenFromArrayMapping to get the nodes to add to targetNode
function executeTemplateForArrayItem (arrayValue, index) {
// Support selecting template as a function of the data being rendered
arrayItemContext = parentBindingContext['createChildContext'](arrayValue, options['as'], function (context) {
context['$index'] = index
})
// Support selecting template as a function of the data being rendered
if (options.as) {
arrayItemContext = parentBindingContext.extend({
[options.as]: arrayValue,
$index: index
})
} else {
arrayItemContext = parentBindingContext.createChildContext(arrayValue, options.as, context => { context.$index = index })
}

var templateName = resolveTemplateName(template, arrayValue, arrayItemContext)
return executeTemplate(targetNode, 'ignoreTargetNode', templateName, arrayItemContext, options, afterBindingCallback)
Expand Down

0 comments on commit d341bcb

Please sign in to comment.