Skip to content

Commit

Permalink
feat(computedFrom): support expressions
Browse files Browse the repository at this point in the history
fixes #149
  • Loading branch information
jdanyow committed Mar 14, 2016
1 parent 542a0bd commit 461a3d5
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 107 deletions.
102 changes: 45 additions & 57 deletions src/computed-observation.js
@@ -1,75 +1,63 @@
import {subscriberCollection} from './subscriber-collection';
import {Expression} from './ast';
import {createOverrideContext} from './scope';
import {ExpressionObserver} from './expression-observer';

const computedContext = 'ComputedPropertyObserver';
export function hasDeclaredDependencies(descriptor) {
return descriptor && descriptor.get && descriptor.get.dependencies && descriptor.get.dependencies.length > 0;
}

@subscriberCollection()
export class ComputedPropertyObserver {
constructor(obj, propertyName, descriptor, observerLocator) {
this.obj = obj;
this.propertyName = propertyName;
this.descriptor = descriptor;
this.observerLocator = observerLocator;
}
export function declarePropertyDependencies(ctor, propertyName, dependencies) {
let descriptor = Object.getOwnPropertyDescriptor(ctor.prototype, propertyName);
descriptor.get.dependencies = dependencies;
}

getValue(){
return this.obj[this.propertyName];
export function computedFrom(...rest){
return function(target, key, descriptor){
descriptor.get.dependencies = rest;
return descriptor;
}
}

setValue(newValue){
this.obj[this.propertyName] = newValue;
}
export class ComputedExpression extends Expression {
constructor(name, dependencies) {
super();

call(context) {
let newValue = this.getValue();
if (this.oldValue === newValue)
return;
this.callSubscribers(newValue, this.oldValue);
this.oldValue = newValue;
return;
this.name = name;
this.dependencies = dependencies;
this.isAssignable = true;
}

subscribe(context, callable) {
if (!this.hasSubscribers()) {
this.oldValue = this.getValue();

let dependencies = this.descriptor.get.dependencies;
this.observers = [];
for (let i = 0, ii = dependencies.length; i < ii; i++) {
let observer = this.observerLocator.getObserver(this.obj, dependencies[i]);
// todo: consider throwing when a dependency's observer is an instance of DirtyCheckProperty.
this.observers.push(observer);
observer.subscribe(computedContext, this);
}
}
evaluate(scope, lookupFunctions) {
return scope.bindingContext[this.name];
}

this.addSubscriber(context, callable);
assign(scope, value) {
scope.bindingContext[this.name] = value;
}

unsubscribe(context, callable) {
if (this.removeSubscriber(context, callable) && !this.hasSubscribers()) {
this.oldValue = undefined;
accept(visitor) {
throw new Error('not implemented');
}

let i = this.observers.length;
while(i--) {
this.observers[i].unsubscribe(computedContext, this);
}
this.observers = null;
connect(binding, scope) {
let dependencies = this.dependencies;
let i = dependencies.length;
while (i--) {
dependencies[i].connect(binding, scope);
}
}
}

export function hasDeclaredDependencies(descriptor) {
return descriptor && descriptor.get && descriptor.get.dependencies && descriptor.get.dependencies.length > 0;
}

export function declarePropertyDependencies(ctor, propertyName, dependencies) {
let descriptor = Object.getOwnPropertyDescriptor(ctor.prototype, propertyName);
descriptor.get.dependencies = dependencies;
}

export function computedFrom(...rest){
return function(target, key, descriptor){
descriptor.get.dependencies = rest;
return descriptor;
export function createComputedObserver(obj, propertyName, descriptor, observerLocator) {
let dependencies = descriptor.get.dependencies;
if (!(dependencies instanceof ComputedExpression)) {
let i = dependencies.length;
while (i--) {
dependencies[i] = observerLocator.parser.parse(dependencies[i]);
}
dependencies = descriptor.get.dependencies = new ComputedExpression(propertyName, dependencies);
}

let scope = { bindingContext: obj, overrideContext: createOverrideContext(obj) };
return new ExpressionObserver(scope, dependencies, observerLocator);
}
1 change: 1 addition & 0 deletions src/expression-observer.js
Expand Up @@ -37,6 +37,7 @@ export class ExpressionObserver {
unsubscribe(context, callable) {
if (this.removeSubscriber(context, callable) && !this.hasSubscribers()) {
this.unobserve(true);
this.oldValue = undefined;
}
}

Expand Down
10 changes: 6 additions & 4 deletions src/observer-locator.js
Expand Up @@ -4,6 +4,7 @@ import {getArrayObserver} from './array-observation';
import {getMapObserver} from './map-observation';
import {getSetObserver} from './set-observation';
import {EventManager} from './event-manager';
import {Parser} from './parser';
import {DirtyChecker, DirtyCheckProperty} from './dirty-checking';
import {
SetterObserver,
Expand All @@ -22,18 +23,19 @@ import {
import {ClassObserver} from './class-observer';
import {
hasDeclaredDependencies,
ComputedPropertyObserver
createComputedObserver
} from './computed-observation';
import {SVGAnalyzer} from './svg';

export class ObserverLocator {
static inject = [TaskQueue, EventManager, DirtyChecker, SVGAnalyzer];
static inject = [TaskQueue, EventManager, DirtyChecker, SVGAnalyzer, Parser];

constructor(taskQueue, eventManager, dirtyChecker, svgAnalyzer) {
constructor(taskQueue, eventManager, dirtyChecker, svgAnalyzer, parser) {
this.taskQueue = taskQueue;
this.eventManager = eventManager;
this.dirtyChecker = dirtyChecker;
this.svgAnalyzer = svgAnalyzer;
this.parser = parser;
this.adapters = [];
}

Expand Down Expand Up @@ -132,7 +134,7 @@ export class ObserverLocator {
descriptor = Object.getPropertyDescriptor(obj, propertyName);

if (hasDeclaredDependencies(descriptor)) {
return new ComputedPropertyObserver(obj, propertyName, descriptor, this)
return createComputedObserver(obj, propertyName, descriptor, this);
}

let existingGetterOrSetter;
Expand Down
64 changes: 54 additions & 10 deletions test/computed-observation.spec.js
@@ -1,38 +1,82 @@
import './setup';
import {declarePropertyDependencies} from '../src/computed-observation';
import {ComputedPropertyObserver} from '../src/computed-observation';
import {declarePropertyDependencies, computedFrom} from '../src/computed-observation';
import {ExpressionObserver} from '../src/expression-observer';
import {createObserverLocator, Person, Foo} from './shared';

describe('declarePropertyDependencies', () => {
it('should declare dependencies for properties with a getter', () => {
var dependencies = ['firstName', 'lastName'],
class Person {
constructor() {
this.firstName = 'John';
this.lastName = 'Doe';
}
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
}

let dependencies = ['firstName', 'lastName'],
person = new Person();
declarePropertyDependencies(Person, 'fullName', dependencies);
expect(Object.getOwnPropertyDescriptor(person.constructor.prototype, 'fullName').get.dependencies)
.toBe(dependencies);
});

it('should declare dependencies for properties with a setter', () => {
var dependencies = ['baz'],
foo = new Foo();
class Foo {
constructor() {
this._bar = null;
}
get bar() {
return this._bar;
}
set bar(newValue) {
this._bar = newValue;
}
}

let dependencies = ['baz'],
foo = new Foo();
declarePropertyDependencies(Foo, 'bar', dependencies);
expect(Object.getOwnPropertyDescriptor(foo.constructor.prototype, 'bar').get.dependencies)
.toBe(dependencies);
});
});

describe('ComputedObservationAdapter', () => {
describe('createComputedObserver', () => {
var person, observer, locator;

class Person {
constructor() {
this.obj = { firstName: 'John', lastName: 'Doe' };
}
@computedFrom('obj.firstName', 'obj.lastName')
get fullName() {
return `${this.obj.firstName} ${this.obj.lastName}`;
}
}

class Foo {
constructor() {
this._bar = null;
}
@computedFrom('_bar');
get bar() {
return this._bar;
}
set bar(newValue) {
this._bar = newValue;
}
}

beforeAll(() => {
locator = createObserverLocator();
person = new Person();
observer = locator.getObserver(person, 'fullName');
});

it('should be an ComputedPropertyObserver', () => {
expect(observer instanceof ComputedPropertyObserver).toBe(true);
it('should be an ExpressionObserver', () => {
expect(observer instanceof ExpressionObserver).toBe(true);
});

it('gets the value', () => {
Expand All @@ -56,11 +100,11 @@ describe('ComputedObservationAdapter', () => {
observer.subscribe(callback);
expect(observer.oldValue).toBe(observer.getValue());

person.lastName = 'Dough';
person.obj.lastName = 'Dough';
setTimeout(() => {
expect(callback).toHaveBeenCalledWith(person.fullName, oldValue);
oldValue = observer.getValue();
person.firstName = 'Jon';
person.obj.firstName = 'Jon';
setTimeout(() => {
expect(callback).toHaveBeenCalledWith(person.fullName, oldValue);
observer.unsubscribe(callback);
Expand Down
15 changes: 13 additions & 2 deletions test/dirty-checking.spec.js
@@ -1,16 +1,27 @@
import './setup';
import {DirtyCheckProperty} from '../src/dirty-checking';
import {
FooNoDep,
executeSharedPropertyObserverTests,
createObserverLocator
} from './shared';

describe('DirtyCheckProperty', () => {
var obj, observerLocator, observer;

class Foo {
constructor() {
this._bar = null;
}
get bar() {
return this._bar;
}
set bar(newValue) {
this._bar = newValue;
}
}

beforeAll(() => {
obj = new FooNoDep();
obj = new Foo();
observerLocator = createObserverLocator();
observer = observerLocator.getObserver(obj, 'bar');
});
Expand Down
34 changes: 0 additions & 34 deletions test/shared.js
Expand Up @@ -41,40 +41,6 @@ export function getBinding(observerLocator, model, modelProperty, view, viewProp
};
}

export class Person {
constructor() {
this.firstName = 'John';
this.lastName = 'Doe';
}
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
}

export class Foo {
constructor() {
this._bar = null;
}
get bar() {
return this._bar;
}
set bar(newValue) {
this._bar = newValue;
}
}

export class FooNoDep {
constructor() {
this._bar = null;
}
get bar() {
return this._bar;
}
set bar(newValue) {
this._bar = newValue;
}
}

function countSubscribers(observer) {
let count = 0;
if (observer._context0) { count++; }
Expand Down

0 comments on commit 461a3d5

Please sign in to comment.