Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

22 tc39 observables #76

Merged
merged 14 commits into from
Jun 28, 2018
Merged
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: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ For TODO between alpha and release, see https://github.com/knockout/tko/issues/1
* Expose `createViewModel` on Components registered with `Component.register`
* Changed `Component.elementName` to `Component.customElementName` and use a kebab-case version of the class name for the custom element name by default
* Pass `{element, templateNodes}` to the `Component` constructor as the second parameter of descendants of the `Component` class
* Add support for `<ko binding='...'>`
* Add basic support for `ko.subscribable` as TC39-Observables

## 🚚 Alpha-4a (8 Nov 2017)

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"scripts": {
"prepublish": "yarn build",
"test": "lerna exec --concurrency=1 --loglevel=warn -- yarn test",
"fast-test": "lerna exec --concurrency=6 --loglevel=warn -- yarn test",
"build": "lerna exec --concurrency=1 --loglevel=warn -- yarn build",
"lint": "standard",
"repackage": "./tools/common-package-config.js packages/shared.package.json packages/*/package.json"
Expand Down
21 changes: 12 additions & 9 deletions packages/tko.bind/src/LegacyBindingHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,26 @@ export class LegacyBindingHandler extends BindingHandler {
constructor (params) {
super(params)
const handler = this.handler
this.onError = params.onError

if (typeof handler.dispose === 'function') {
this.addDisposable(handler)
}

try {
this.initReturn = handler.init && handler.init(...this.legacyArgs)
} catch (e) { params.onError('init', e) }
} catch (e) {
params.onError('init', e)
}
}

if (typeof handler.update === 'function') {
this.computed(() => {
try {
handler.update(...this.legacyArgs)
} catch (e) {
params.onError('update', e)
}
})
onValueChange () {
const handler = this.handler
if (typeof handler.update !== 'function') { return }
try {
handler.update(...this.legacyArgs)
} catch (e) {
this.onError('update', e)
}
}

Expand Down
6 changes: 6 additions & 0 deletions packages/tko.bind/src/applyBindings.js
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,12 @@ function applyBindingsToNodeInternal (node, sourceBindings, bindingContext, asyn
})
)

if (bindingHandler.onValueChange) {
dependencyDetection.ignore(() =>
bindingHandler.computed('onValueChange')
)
}

// Expose the bindings via domData.
allBindingHandlers[key] = bindingHandler

Expand Down
39 changes: 39 additions & 0 deletions packages/tko.binding.component/spec/componentBindingBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ import {
bindings as ifBindings
} from 'tko.binding.if'

import {
NATIVE_BINDINGS
} from 'tko.provider.native'

import {
bindings as componentBindings
} from '../src'
Expand Down Expand Up @@ -945,6 +949,41 @@ describe('Components: Component binding', function () {
children.unshift('rrr')
expect(testNode.children[0].innerHTML).toEqual('<b>xrrrabc</b>')
})

it('inserts and updates observable template', function () {
const t = observable(["abc"])
class ViewModel extends components.ComponentABC {
get template () {
return t
}
}
ViewModel.register('test-component')
applyBindings(outerViewModel, testNode)
expect(testNode.children[0].innerHTML).toEqual('abc')

t(["rr", "vv"])
expect(testNode.children[0].innerHTML).toEqual('rrvv')
})

it('gets params from the node', function () {
const x = {v: 'rrr'}
let seen = null
class ViewModel extends components.ComponentABC {
constructor (params) {
super(params)
seen = params
}

static get template () {
return { elementName: 'name', attributes: {}, children: [] }
}
}
ViewModel.register('test-component')
testNode.children[0][NATIVE_BINDINGS] = {x, y: () => x}
applyBindings(outerViewModel, testNode)
expect(seen.x).toEqual(x)
expect(seen.y()).toEqual(x)
})
})

describe('slots', function () {
Expand Down
37 changes: 26 additions & 11 deletions packages/tko.binding.component/src/componentBinding.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,21 @@ import {
} from 'tko.utils'

import {
unwrap
unwrap, isObservable
} from 'tko.observable'

import {
DescendantBindingHandler, bindingEvent
DescendantBindingHandler, applyBindingsToDescendants
} from 'tko.bind'

import {
jsxToNode
jsxToNode, maybeJsx
} from 'tko.utils.jsx'

import {
NATIVE_BINDINGS
} from 'tko.provider.native'

import {LifeCycle} from 'tko.lifecycle'

import registry from 'tko.utils.component'
Expand All @@ -33,15 +37,25 @@ export default class ComponentBinding extends DescendantBindingHandler {
this.computed('computeApplyComponent')
}

setDomNodesFromJsx (jsx, element) {
const jsxArray = Array.isArray(jsx) ? jsx : [jsx]
const domNodeChildren = jsxArray.map(jsxToNode)
virtualElements.setDomNodeChildren(element, domNodeChildren)
}

cloneTemplateIntoElement (componentName, template, element) {
if (!template) {
throw new Error('Component \'' + componentName + '\' has no template')
}
const possibleJsxPartial = Array.isArray(template) && template.length
if (possibleJsxPartial && template[0].hasOwnProperty('elementName')) {
virtualElements.setDomNodeChildren(element, template.map(jsxToNode))
} else if (template.elementName) {
virtualElements.setDomNodeChildren(element, [jsxToNode(template)])

if (maybeJsx(template)) {
if (isObservable(template)) {
this.subscribe(template, jsx => {
this.setDomNodesFromJsx(jsx, element)
applyBindingsToDescendants(this.childBindingContext, this.$element)
})
}
this.setDomNodesFromJsx(unwrap(template), element)
} else {
const clonedNodesArray = cloneNodes(template)
virtualElements.setDomNodeChildren(element, clonedNodesArray)
Expand All @@ -64,7 +78,8 @@ export default class ComponentBinding extends DescendantBindingHandler {
componentName = value
} else {
componentName = unwrap(value.name)
componentParams = unwrap(value.params)
componentParams = NATIVE_BINDINGS in this.$element
? this.$element[NATIVE_BINDINGS] : unwrap(value.params)
}

this.latestComponentName = componentName
Expand Down Expand Up @@ -117,11 +132,11 @@ export default class ComponentBinding extends DescendantBindingHandler {
$componentTemplateNodes: this.originalChildNodes
})

const childBindingContext = this.$context.createChildContext(componentViewModel, /* dataItemAlias */ undefined, ctxExtender)
this.childBindingContext = this.$context.createChildContext(componentViewModel, /* dataItemAlias */ undefined, ctxExtender)
this.currentViewModel = componentViewModel

const onBinding = this.onBindingComplete.bind(this, componentViewModel)
const applied = this.applyBindingsToDescendants(childBindingContext, onBinding)
this.applyBindingsToDescendants(this.childBindingContext, onBinding)
}

onBindingComplete (componentViewModel, bindingResult) {
Expand Down
2 changes: 0 additions & 2 deletions packages/tko.binding.template/src/templating.js
Original file line number Diff line number Diff line change
Expand Up @@ -281,8 +281,6 @@ export class TemplateBindingHandler extends AsyncBindingHandler {
} else {
this.bindAnonymousTemplate()
}

this.computed(this.onValueChange.bind(this))
}

bindNamedTemplate () {
Expand Down
7 changes: 6 additions & 1 deletion packages/tko.computed/src/computed.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import {
extenders,
valuesArePrimitiveAndEqual,
observable,
subscribable
subscribable,
LATEST_VALUE
} from 'tko.observable'

const computedState = createSymbolOrString('_state')
Expand Down Expand Up @@ -397,6 +398,10 @@ computed.fn = {
return state.latestValue
},

get [LATEST_VALUE] () {
return this.peek()
},

limit (limitFunction) {
const state = this[computedState]
// Override the limit function with one that delays evaluation as well
Expand Down
10 changes: 10 additions & 0 deletions packages/tko.observable/spec/observableBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -348,8 +348,18 @@ describe('Observable', function () {
expect(myObservable.customFunction1).toBe(customFunction1)
expect(myObservable.customFunction2).toBe(customFunction2)
})

it('immediately emits any value when called with {next: ...}', function () {
const instance = observable(1)
let x
instance.subscribe({next: v => (x = v)})
expect(x).toEqual(1)
observable(2)
expect(x).toEqual(1)
})
})


describe('unwrap', function () {
it('Should return the supplied value for non-observables', function () {
var someObject = { abc: 123 }
Expand Down
8 changes: 8 additions & 0 deletions packages/tko.observable/spec/subscribableBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,4 +176,12 @@ describe('Subscribable', function () {
// Issue #2252: make sure .toString method does not throw error
expect(new subscribable().toString()).toBe('[object Object]')
})

it('subscribes with TC39 Observable {next: () =>}', function () {
var instance = new subscribable()
var notifiedValue
instance.subscribe({ next (value) { notifiedValue = value } })
instance.notifySubscribers(123)
expect(notifiedValue).toEqual(123)
})
})
32 changes: 32 additions & 0 deletions packages/tko.observable/src/Subscription.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@

import {
removeDisposeCallback, addDisposeCallback
} from 'tko.utils'


export default class Subscription {
constructor (target, observer, disposeCallback) {
this._target = target
this._callback = observer.next
this._disposeCallback = disposeCallback
this._isDisposed = false
this._domNodeDisposalCallback = null
}

dispose () {
if (this._domNodeDisposalCallback) {
removeDisposeCallback(this._node, this._domNodeDisposalCallback)
}
this._isDisposed = true
this._disposeCallback()
}

disposeWhenNodeIsRemoved (node) {
this._node = node
addDisposeCallback(node, this._domNodeDisposalCallback = this.dispose.bind(this))
}

// TC39 Observable API
unsubscribe () { this.dispose() }
get closed () { return this._isDisposed }
}
2 changes: 1 addition & 1 deletion packages/tko.observable/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export {
observable, isObservable, unwrap, peek,
isWriteableObservable, isWritableObservable
} from './observable'
export { isSubscribable, subscribable } from './subscribable'
export { isSubscribable, subscribable, LATEST_VALUE } from './subscribable'
export { observableArray, isObservableArray } from './observableArray'
export { trackArrayChanges, arrayChangeEventName } from './observableArray.changeTracking'
export { toJS, toJSON } from './mappingHelpers'
Expand Down
22 changes: 10 additions & 12 deletions packages/tko.observable/src/observable.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,35 @@
// ---
//
import {
createSymbolOrString, options, overwriteLengthPropertyIfSupported
options, overwriteLengthPropertyIfSupported
} from 'tko.utils'

import * as dependencyDetection from './dependencyDetection.js'
import { deferUpdates } from './defer.js'
import { subscribable, defaultEvent } from './subscribable.js'
import { subscribable, defaultEvent, LATEST_VALUE } from './subscribable.js'
import { valuesArePrimitiveAndEqual } from './extenders.js'

var observableLatestValue = createSymbolOrString('_latestValue')

export function observable (initialValue) {
function Observable () {
if (arguments.length > 0) {
// Write
// Ignore writes if the value hasn't changed
if (Observable.isDifferent(Observable[observableLatestValue], arguments[0])) {
if (Observable.isDifferent(Observable[LATEST_VALUE], arguments[0])) {
Observable.valueWillMutate()
Observable[observableLatestValue] = arguments[0]
Observable[LATEST_VALUE] = arguments[0]
Observable.valueHasMutated()
}
return this // Permits chained assignments
} else {
// Read
dependencyDetection.registerDependency(Observable) // The caller only needs to be notified of changes if they did a "read" operation
return Observable[observableLatestValue]
return Observable[LATEST_VALUE]
}
}

overwriteLengthPropertyIfSupported(Observable, { value: undefined })

Observable[observableLatestValue] = initialValue
Observable[LATEST_VALUE] = initialValue

subscribable.fn.init(Observable)

Expand All @@ -50,13 +48,13 @@ export function observable (initialValue) {
// Define prototype for observables
observable.fn = {
equalityComparer: valuesArePrimitiveAndEqual,
peek () { return this[observableLatestValue] },
peek () { return this[LATEST_VALUE] },
valueHasMutated () {
this.notifySubscribers(this[observableLatestValue], 'spectate')
this.notifySubscribers(this[observableLatestValue])
this.notifySubscribers(this[LATEST_VALUE], 'spectate')
this.notifySubscribers(this[LATEST_VALUE])
},
valueWillMutate () {
this.notifySubscribers(this[observableLatestValue], 'beforeChange')
this.notifySubscribers(this[LATEST_VALUE], 'beforeChange')
},

// Some observables may not always be writeable, notably computeds.
Expand Down
Loading