diff --git a/src/model/LinkModel.js b/src/model/LinkModel.js index 9cd2dc85e6..526cd40afc 100755 --- a/src/model/LinkModel.js +++ b/src/model/LinkModel.js @@ -36,9 +36,9 @@ export default class LinkModel extends ModelBase { this.owner = owner; this.target = target; this.key = key === undefined ? owner.key : key; - if (owner.isLink) this.sourcePath = `${owner.sourcePath}.${this.key}`; + if (owner && owner.isLink) this.sourcePath = `${owner.sourcePath}.${this.key}`; - target.registerLink(this); + if (target) target.registerLink(this); if (parent) this.isReadonly = parent.isReadonly; @@ -148,7 +148,7 @@ export default class LinkModel extends ModelBase { target = rebindMatch(this.sourcePath, target, this.target); if (!target || this.target === target) return; - this.target.unregisterLink(this); + this.target && this.target.unregisterLink(this); this.target = target; this.children.forEach(c => { diff --git a/src/model/ModelBase.js b/src/model/ModelBase.js index 491d9d3af4..a948073181 100755 --- a/src/model/ModelBase.js +++ b/src/model/ModelBase.js @@ -196,7 +196,7 @@ export default class ModelBase { while (i--) { const link = this.links[i]; // only relink the root of the link tree - if (link.owner._link) link.relinking(next, safe); + if (link.owner && link.owner._link) link.relinking(next, safe); } i = this.children.length; diff --git a/src/view/resolvers/ReferenceExpressionProxy.js b/src/view/resolvers/ReferenceExpressionProxy.js index ec41be4dea..d1bc016016 100755 --- a/src/view/resolvers/ReferenceExpressionProxy.js +++ b/src/view/resolvers/ReferenceExpressionProxy.js @@ -1,255 +1,94 @@ -import Model from 'src/model/Model'; -import { findBoundValue } from 'src/model/ModelBase'; +import { fireShuffleTasks } from 'src/model/ModelBase'; import { REFERENCE } from 'config/types'; import { rebindMatch } from 'shared/rebind'; -import { handleChange, mark, marked } from 'shared/methodCallers'; -import { isEqual, isString } from 'utils/is'; +import { isArray, isString } from 'utils/is'; import { escapeKey } from 'shared/keypaths'; import ExpressionProxy from './ExpressionProxy'; import resolveReference from './resolveReference'; import resolve from './resolve'; -import { hasOwn } from 'utils/object'; -import { capture } from 'src/global/capture'; -class ReferenceExpressionChild extends Model { - constructor(parent, key) { - super(parent, key); - this.dirty = true; - } - - applyValue(value) { - if (isEqual(value, this.value)) return; - - let parent = this.parent; - const keys = [this.key]; - while (parent) { - if (parent.base) { - const target = parent.model.joinAll(keys); - target.applyValue(value); - break; - } - - keys.unshift(parent.key); - - parent = parent.parent; - } - } - - get(shouldCapture, opts) { - this.retrieve(); - return super.get(shouldCapture, opts); - } - - joinKey(key) { - if (key === undefined || key === '') return this; - - if (!hasOwn(this.childByKey, key)) { - const child = new ReferenceExpressionChild(this, key); - this.children.push(child); - this.childByKey[key] = child; - } - - return this.childByKey[key]; - } - - mark() { - this.dirty = true; - super.mark(); - } +import LinkModel, { Missing } from 'src/model/LinkModel'; - retrieve() { - if (this.dirty) { - this.dirty = false; - const parent = this.parent.get(); - this.value = parent && parent[this.key]; - } - - return this.value; - } -} - -const missing = { get() {} }; - -export default class ReferenceExpressionProxy extends Model { +export default class ReferenceExpressionProxy extends LinkModel { constructor(fragment, template) { - super(null, null); - this.dirty = true; + super(null, null, null, '@undefined'); this.root = fragment.ractive.viewmodel; this.template = template; + this.rootLink = true; - this.base = resolve(fragment, template); + let base = resolve(fragment, template); + let idx; - const intermediary = (this.intermediary = { - handleChange: () => this.handleChange(), + const proxy = (this.proxy = { rebind: (next, previous) => { - if (previous === this.base) { + if (previous === base) { next = rebindMatch(template, next, previous); - if (next !== this.base) { - this.base.unregister(intermediary); - this.base = next; + if (next !== base) { + base = next; } - } else { - const idx = this.members.indexOf(previous); - if (~idx) { - // only direct references will rebind... expressions handle themselves - next = rebindMatch(template.m[idx].n, next, previous); - if (next !== this.members[idx]) { - this.members.splice(idx, 1, next || missing); - } + } else if (~(idx = members.indexOf(previous))) { + next = rebindMatch(template.m[idx].n, next, previous); + if (next !== members[idx]) { + members.splice(idx, 1, next || Missing); } } - if (next !== previous) previous.unregister(intermediary); - if (next) next.addShuffleTask(() => next.register(intermediary)); - - this.bubble(); + if (next !== previous) previous.unregister(proxy); + if (next) next.addShuffleTask(() => next.register(proxy)); + }, + handleChange: () => { + pathChanged(); } }); - this.members = template.m.map(template => { - if (isString(template)) { - return { get: () => template }; + base.register(proxy); + + const members = template.m.map(tpl => { + if (isString(tpl)) { + return { get: () => tpl }; } let model; - if (template.t === REFERENCE) { - model = resolveReference(fragment, template.n); - model.register(intermediary); + if (tpl.t === REFERENCE) { + model = resolveReference(fragment, tpl.n); + model.register(proxy); return model; } - model = new ExpressionProxy(fragment, template); - model.register(intermediary); + model = new ExpressionProxy(fragment, tpl); + model.register(proxy); return model; }); - this.base.register(intermediary); - - this.bubble(); - } - - bubble() { - if (!this.base) return; - if (!this.dirty) this.handleChange(); - } - - get(shouldCapture, opts) { - if (shouldCapture) capture(this); - if (this.dirty) { - this.bubble(); - - const keys = this.members.map(m => escapeKey(String(m.get()))); - const model = this.base.joinAll(keys); + const pathChanged = () => { + const model = base.joinAll( + members.reduce((list, m) => { + const k = m.get(); + if (isArray(k)) return list.concat(k); + else list.push(escapeKey(String(k))); + return list; + }, []) + ); if (model !== this.model) { - if (this.model) { - this.model.unregister(this); - this.model.unregisterTwowayBinding(this); - } - this.model = model; - this.parent = model.parent; - this.model.register(this); - this.model.registerTwowayBinding(this); - - pathChanged(this); + this.relinking(model); + fireShuffleTasks(); + refreshPathDeps(this); } + }; - this.value = this.model.get(shouldCapture, opts); - this.dirty = false; - this.mark(); - return this.value; - } else { - return this.model ? this.model.get(shouldCapture, opts) : undefined; - } - } - - // indirect two-way bindings - getValue() { - this.value = this.model ? this.model.get() : undefined; - - let i = this.bindings.length; - while (i--) { - const value = this.bindings[i].getValue(); - if (value !== this.value) return value; - } - - // check one-way bindings - const oneway = findBoundValue(this.deps); - if (oneway) return oneway.value; - - return this.value; + pathChanged(); } getKeypath() { return this.model ? this.model.getKeypath() : '@undefined'; } - - handleChange() { - this.dirty = true; - this.mark(); - } - - joinKey(key) { - if (key === undefined || key === '') return this; - - if (!hasOwn(this.childByKey, key)) { - const child = new ReferenceExpressionChild(this, key); - this.children.push(child); - this.childByKey[key] = child; - } - - return this.childByKey[key]; - } - - mark() { - if (this.dirty) { - this.deps.forEach(handleChange); - } - - this.links.forEach(marked); - this.children.forEach(mark); - } - - rebind() { - this.handleChange(); - } - - retrieve() { - return this.value; - } - - set(value) { - this.model.set(value); - } - - teardown() { - if (this.base) { - this.base.unregister(this.intermediary); - } - if (this.model) { - this.model.unregister(this); - this.model.unregisterTwowayBinding(this); - } - if (this.members) { - this.members.forEach(m => m && m.unregister && m.unregister(this.intermediary)); - } - } - - unreference() { - super.unreference(); - if (!this.deps.length && !this.refs) this.teardown(); - } - - unregister(dep) { - super.unregister(dep); - if (!this.deps.length && !this.refs) this.teardown(); - } } -function pathChanged(proxy) { +function refreshPathDeps(proxy) { let len = proxy.deps.length; let i, v; @@ -261,6 +100,6 @@ function pathChanged(proxy) { len = proxy.children.length; for (i = 0; i < len; i++) { - pathChanged(proxy.children[i]); + refreshPathDeps(proxy.children[i]); } } diff --git a/tests/browser/references.js b/tests/browser/references.js index b3fb1d3650..d0167fd344 100644 --- a/tests/browser/references.js +++ b/tests/browser/references.js @@ -643,4 +643,38 @@ export default function() { t.htmlEqual(fixture.innerHTML, '3 is last 3|0 is last 0'); }); + + test(`reference expressions with array members`, t => { + const r = new Ractive({ + target: fixture, + template: `{{~/[path]}} {{some[sub][0]}}`, + data: { + path: ['some', 'foo', 'baz', 'foo'], + sub: ['bar', 'baz', 'bip'], + some: { + foo: { + baz: { + foo: 42 + } + }, + bar: { + baz: { + bip: [1, 2, 3], + bop: ['a', 'b', 'c'] + } + } + } + } + }); + + t.htmlEqual(fixture.innerHTML, '42 1'); + + r.set('path', ['some', 'bar', 'baz', 'bip', '1']); + + t.htmlEqual(fixture.innerHTML, '2 1'); + + r.set('sub', ['bar', 'baz', 'bop']); + + t.htmlEqual(fixture.innerHTML, '2 a'); + }); }