Skip to content

Commit

Permalink
refactor(binding-context): explicitly model scope, context and observ…
Browse files Browse the repository at this point in the history
…er lookup (#216)

* refactor(binding): explicitly model scope, override/bindingctx and observerlookup

* refactor(observer-locator): use direct lookup from bindingContext if available

* fix(subscriber-collection): remove circular dependency
  • Loading branch information
fkleuver authored and EisenbergEffect committed Oct 10, 2018
1 parent a22b18a commit c3e3779
Show file tree
Hide file tree
Showing 16 changed files with 376 additions and 297 deletions.
189 changes: 136 additions & 53 deletions packages/runtime/src/binding/binding-context.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
import { Reporter } from "@aurelia/kernel";

export interface IOverrideContext {
parentOverrideContext: IOverrideContext;
bindingContext: any;
}

export interface IScope {
bindingContext: any;
overrideContext: IOverrideContext;
}
import { IIndexable, Reporter } from '@aurelia/kernel';
import { StrictPrimitive } from './ast';
import { IBindScope, ObservedCollection, PropertyObserver } from './observation';
import { SetterObserver } from './property-observation';

const enum RuntimeError {
UndefinedScope = 250, // trying to evaluate on something that's not a valid binding
Expand All @@ -17,46 +10,81 @@ const enum RuntimeError {
NilParentScope = 253
}

export const BindingContext = {
createScope(bindingContext: any, overrideContext?: IOverrideContext): IScope {
return {
bindingContext: bindingContext,
overrideContext: overrideContext || BindingContext.createOverride(bindingContext)
};
},
export interface IObserversLookup<TObj extends IIndexable = IIndexable, TKey extends keyof TObj =
Exclude<keyof TObj, '$synthetic' | '$observers' | 'bindingContext' | 'overrideContext' | 'parentOverrideContext'>> {

createScopeFromOverride(overrideContext: IOverrideContext): IScope {
if (overrideContext === null || overrideContext === undefined) {
throw Reporter.error(RuntimeError.NilOverrideContext);
}

export type ObserversLookup<TObj extends IIndexable = IIndexable, TKey extends keyof TObj =
Exclude<keyof TObj, '$synthetic' | '$observers' | 'bindingContext' | 'overrideContext' | 'parentOverrideContext'>> =
{ [P in TKey]: PropertyObserver; } & { getOrCreate(obj: IBindingContext | IOverrideContext, key: string): PropertyObserver };

/*@internal*/
export class InternalObserversLookup {
public getOrCreate(obj: IBindingContext | IOverrideContext, key: string): PropertyObserver {
let observer = this[key];
if (observer === undefined) {
observer = this[key] = new SetterObserver(obj, key);
}
return {
bindingContext: overrideContext.bindingContext,
overrideContext
};
},

createScopeFromParent(parentScope: IScope, bindingContext: any): IScope {
if (parentScope === null || parentScope === undefined) {
throw Reporter.error(RuntimeError.NilParentScope);
return observer;
}
}

export interface IBindingContext {
[key: string]: ObservedCollection | StrictPrimitive | IIndexable;

readonly $synthetic?: true;
readonly $observers?: ObserversLookup<IOverrideContext>;
getObservers(): ObserversLookup<IOverrideContext>;
}

export interface IOverrideContext {
[key: string]: ObservedCollection | StrictPrimitive | IIndexable;

readonly $synthetic?: true;
readonly $observers?: ObserversLookup<IOverrideContext>;
readonly bindingContext: IBindingContext | IBindScope;
readonly parentOverrideContext: IOverrideContext | null;
getObservers(): ObserversLookup<IOverrideContext>;
}

export interface IScope {
readonly bindingContext: IBindingContext | IBindScope;
readonly overrideContext: IOverrideContext;
}

export class BindingContext implements IBindingContext {
[key: string]: ObservedCollection | StrictPrimitive | IIndexable;

public readonly $synthetic: true = true;

public $observers: ObserversLookup<IOverrideContext>;

private constructor(keyOrObj?: string | IIndexable, value?: ObservedCollection | StrictPrimitive | IIndexable) {
if (keyOrObj !== undefined) {
if (value !== undefined) {
// if value is defined then it's just a property and a value to initialize with
// tslint:disable-next-line:no-any
this[<any>keyOrObj] = value;
} else {
// can either be some random object or another bindingContext to clone from
for (const prop in <IIndexable>keyOrObj) {
if (keyOrObj.hasOwnProperty(prop)) {
this[prop] = keyOrObj[prop];
}
}
}
}
return {
bindingContext: bindingContext,
overrideContext: BindingContext.createOverride(
bindingContext,
parentScope.overrideContext
)
};
},

createOverride(bindingContext?: any, parentOverrideContext?: IOverrideContext): IOverrideContext {
return {
bindingContext: bindingContext,
parentOverrideContext: parentOverrideContext || null
};
},
}

public static create(obj?: IIndexable): BindingContext;
public static create(key: string, value: ObservedCollection | StrictPrimitive | IIndexable): BindingContext;
public static create(keyOrObj?: string | IIndexable, value?: ObservedCollection | StrictPrimitive | IIndexable): BindingContext {
return new BindingContext(keyOrObj, value);
}

// tslint:disable-next-line:no-reserved-keywords
get(scope: IScope, name: string, ancestor: number): any {
public static get(scope: IScope, name: string, ancestor: number): IBindingContext | IOverrideContext | IBindScope {
if (scope === undefined) {
throw Reporter.error(RuntimeError.UndefinedScope);
}
Expand All @@ -65,17 +93,16 @@ export const BindingContext = {
}
let overrideContext = scope.overrideContext;

if (ancestor) {
if (ancestor > 0) {
// jump up the required number of ancestor contexts (eg $parent.$parent requires two jumps)
while (ancestor && overrideContext) {
while (ancestor > 0) {
if (overrideContext.parentOverrideContext === null) {
return undefined;
}
ancestor--;
overrideContext = overrideContext.parentOverrideContext;
}

if (ancestor || !overrideContext) {
return undefined;
}

return name in overrideContext ? overrideContext : overrideContext.bindingContext;
}

Expand All @@ -92,4 +119,60 @@ export const BindingContext = {
// the name wasn't found. return the root binding context.
return scope.bindingContext || scope.overrideContext;
}
};

public getObservers(): ObserversLookup<IOverrideContext> {
let observers = this.$observers;
if (observers === undefined) {
this.$observers = observers = new InternalObserversLookup() as ObserversLookup<this>;
}
return observers;
}
}

export class Scope implements IScope {
private constructor(
public readonly bindingContext: IBindingContext | IBindScope,
public readonly overrideContext: IOverrideContext
) { }

public static create(bc: IBindingContext | IBindScope, oc: IOverrideContext | null): Scope {
return new Scope(bc, oc === null || oc === undefined ? OverrideContext.create(bc, oc) : oc);
}

public static fromOverride(oc: IOverrideContext): Scope {
if (oc === null || oc === undefined) {
throw Reporter.error(RuntimeError.NilOverrideContext);
}
return new Scope(oc.bindingContext, oc);
}

public static fromParent(ps: IScope, bc: IBindingContext | IBindScope): Scope {
if (ps === null || ps === undefined) {
throw Reporter.error(RuntimeError.NilParentScope);
}
return new Scope(bc, OverrideContext.create(bc, ps.overrideContext));
}
}

export class OverrideContext implements IOverrideContext {
[key: string]: ObservedCollection | StrictPrimitive | IIndexable;

public readonly $synthetic: true = true;

private constructor(
public readonly bindingContext: IBindingContext | IBindScope,
public readonly parentOverrideContext: IOverrideContext | null
) { }

public static create(bc: IBindingContext | IBindScope, poc: IOverrideContext | null): OverrideContext {
return new OverrideContext(bc, poc === undefined ? null : poc);
}

public getObservers(): ObserversLookup<IOverrideContext> {
let observers = this.$observers;
if (observers === undefined) {
this.$observers = observers = new InternalObserversLookup();
}
return observers as ObserversLookup<IOverrideContext>;
}
}
Empty file.
1 change: 1 addition & 0 deletions packages/runtime/src/binding/observation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export interface IBindingTargetObserver<
export type AccessorOrObserver = IBindingTargetAccessor | IBindingTargetObserver;

export type IObservable = (IIndexable | string | Node | INode | Collection) & {
readonly $synthetic?: false;
$observers?: Record<string, AccessorOrObserver>;
};

Expand Down
10 changes: 7 additions & 3 deletions packages/runtime/src/binding/observer-locator.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { DI, IIndexable, inject, Primitive, Reporter } from '@aurelia/kernel';
import { DOM } from '../dom';
import { getArrayObserver } from './array-observer';
import { IBindingContext, IOverrideContext } from './binding-context';
import { IChangeSet } from './change-set';
import { createComputedObserver } from './computed-observer';
import { IDirtyChecker } from './dirty-checker';
import { CheckedObserver, SelectValueObserver, ValueAttributeObserver } from './element-observation';
import { IEventManager } from './event-manager';
import { getMapObserver } from './map-observer';
import { AccessorOrObserver, CollectionKind, IBindingTargetAccessor, IBindingTargetObserver, ICollectionObserver, IObservable, IObservedArray, IObservedMap, IObservedSet, CollectionObserver } from './observation';
import { AccessorOrObserver, CollectionKind, CollectionObserver, IBindingTargetAccessor, IBindingTargetObserver, ICollectionObserver, IObservable, IObservedArray, IObservedMap, IObservedSet } from './observation';
import { PrimitiveObserver, SetterObserver } from './property-observation';
import { getSetObserver } from './set-observer';
import { ISVGAnalyzer } from './svg-analyzer';
import { ClassAttributeAccessor, DataAttributeAccessor, PropertyAccessor, StyleAttributeAccessor, XLinkAttributeAccessor, ElementPropertyAccessor } from './target-accessors';
import { ClassAttributeAccessor, DataAttributeAccessor, ElementPropertyAccessor, PropertyAccessor, StyleAttributeAccessor, XLinkAttributeAccessor } from './target-accessors';

const toStringTag = Object.prototype.toString;

Expand Down Expand Up @@ -56,7 +57,10 @@ export class ObserverLocator implements IObserverLocator {
private svgAnalyzer: ISVGAnalyzer
) {}

public getObserver(obj: IObservable, propertyName: string): AccessorOrObserver {
public getObserver(obj: IObservable | IBindingContext | IOverrideContext, propertyName: string): AccessorOrObserver {
if (obj.$synthetic === true) {
return obj.getObservers().getOrCreate(obj, propertyName);
}
let observersLookup = obj.$observers;
let observer;

Expand Down
9 changes: 4 additions & 5 deletions packages/runtime/src/binding/subscriber-collection.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { IIndexable, Primitive } from '@aurelia/kernel';
import { nativePush, nativeSplice } from './array-observer';
import { BindingFlags } from './binding-flags';
import { IBatchedCollectionSubscriber, IBatchedSubscriberCollection, IndexMap, IPropertySubscriber, ISubscriberCollection, MutationKind, MutationKindToBatchedSubscriber, MutationKindToSubscriber, SubscriberFlags } from './observation';

Expand Down Expand Up @@ -46,7 +45,7 @@ function addSubscriber<T extends MutationKind>(this: ISubscriberCollection<T>, s
this._subscriberFlags |= SubscriberFlags.SubscribersRest;
return true;
}
nativePush.call(this._subscribersRest, subscriber);
this._subscribersRest.push(subscriber);
return true;
}

Expand All @@ -71,7 +70,7 @@ function removeSubscriber<T extends MutationKind>(this: ISubscriberCollection<T>
const subscribers = this._subscribersRest;
for (let i = 0, ii = subscribers.length; i < ii; ++i) {
if (subscribers[i] === subscriber) {
nativeSplice.call(subscribers, i, 1);
subscribers.splice(i, 1);
if (ii === 1) {
this._subscriberFlags &= ~SubscriberFlags.SubscribersRest;
}
Expand Down Expand Up @@ -223,7 +222,7 @@ function addBatchedSubscriber(this: IBatchedSubscriberCollection<MutationKind.co
this._batchedSubscriberFlags |= SubscriberFlags.SubscribersRest;
return true;
}
nativePush.call(this._batchedSubscribersRest, subscriber);
this._batchedSubscribersRest.push(subscriber);
return true;
}

Expand All @@ -248,7 +247,7 @@ function removeBatchedSubscriber(this: IBatchedSubscriberCollection<MutationKind
const subscribers = this._batchedSubscribersRest;
for (let i = 0, ii = subscribers.length; i < ii; ++i) {
if (subscribers[i] === subscriber) {
nativeSplice.call(subscribers, i, 1);
subscribers.splice(i, 1);
if (ii === 1) {
this._batchedSubscriberFlags &= ~SubscriberFlags.SubscribersRest;
}
Expand Down
4 changes: 2 additions & 2 deletions packages/runtime/src/templating/custom-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
Reporter,
Writable
} from '@aurelia/kernel';
import { BindingContext } from '../binding/binding-context';
import { BindingContext, Scope } from '../binding/binding-context';
import { BindingFlags } from '../binding/binding-flags';
import { DOM, ICustomElementHost, INode, INodeSequence, IRenderLocation } from '../dom';
import { IResourceKind, IResourceType } from '../resource';
Expand Down Expand Up @@ -145,7 +145,7 @@ function hydrate(this: IInternalCustomElementImplementation, renderingEngine: IR
this.$attachables = [];
this.$isAttached = false;
this.$isBound = false;
this.$scope = BindingContext.createScope(this);
this.$scope = Scope.create(this, null); // TODO: get the parent from somewhere?
this.$projector = determineProjector(this, host, description);

renderingEngine.applyRuntimeBehavior(Type, this);
Expand Down
10 changes: 5 additions & 5 deletions packages/runtime/src/templating/resources/repeat.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { inject } from '@aurelia/kernel';
import { Binding, BindingContext, BindingFlags, CollectionObserver, ForOfStatement, getCollectionObserver, IBatchedCollectionSubscriber, IChangeSet, IObservedArray, IScope, ObservedCollection, SetterObserver } from '../../binding';
import { Binding, BindingContext, BindingFlags, CollectionObserver, ForOfStatement, getCollectionObserver, IBatchedCollectionSubscriber, IChangeSet, IObservedArray, IScope, ObservedCollection, SetterObserver, Scope } from '../../binding';
import { INode, IRenderLocation } from '../../dom';
import { bindable } from '../bindable';
import { ICustomAttribute, templateController } from '../custom-attribute';
Expand Down Expand Up @@ -113,18 +113,18 @@ export class Repeat<T extends ObservedCollection = IObservedArray> {
forOf.iterate(items, (arr, i, item) => {
const view = views[i];
if (!!view.$scope && view.$scope.bindingContext[local] === item) {
view.$bind(flags, BindingContext.createScopeFromParent($scope, view.$scope.bindingContext));
view.$bind(flags, Scope.fromParent($scope, view.$scope.bindingContext));
} else {
view.$bind(flags, BindingContext.createScopeFromParent($scope, { [local]: item }))
view.$bind(flags, Scope.fromParent($scope, BindingContext.create(local, item)));
}
});
} else {
forOf.iterate(items, (arr, i, item) => {
const view = views[i];
if (indexMap[i] === i) {
view.$bind(flags, BindingContext.createScopeFromParent($scope, view.$scope.bindingContext));
view.$bind(flags, Scope.fromParent($scope, view.$scope.bindingContext));
} else {
view.$bind(flags, BindingContext.createScopeFromParent($scope, { [local]: item }))
view.$bind(flags, Scope.fromParent($scope, BindingContext.create(local, item)));
}
});
}
Expand Down
4 changes: 2 additions & 2 deletions packages/runtime/src/templating/resources/with.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { inject } from '@aurelia/kernel';
import { BindingContext, IScope } from '../../binding/binding-context';
import { BindingContext, IScope, Scope } from '../../binding/binding-context';
import { BindingFlags } from '../../binding/binding-flags';
import { IRenderLocation } from '../../dom';
import { bindable } from '../bindable';
Expand Down Expand Up @@ -43,7 +43,7 @@ export class With {
private bindChild(flags: BindingFlags): void {
this.currentView.$bind(
flags,
BindingContext.createScopeFromParent(this.$scope, this.value)
Scope.fromParent(this.$scope, this.value)
);
}
}
Loading

0 comments on commit c3e3779

Please sign in to comment.