feat(core): added support for detecting lifecycle events based on interfaces

This commit is contained in:
vsavkin 2015-05-27 08:08:14 -07:00
parent 2b6a653050
commit 30b6542fc8
12 changed files with 257 additions and 60 deletions

View File

@ -1,5 +1,5 @@
import {CONST, normalizeBlank, isPresent, CONST_EXPR} from 'angular2/src/facade/lang'; import {CONST, CONST_EXPR} from 'angular2/src/facade/lang';
import {ListWrapper, List} from 'angular2/src/facade/collection'; import {List} from 'angular2/src/facade/collection';
import {Injectable} from 'angular2/src/di/annotations_impl'; import {Injectable} from 'angular2/src/di/annotations_impl';
import {DEFAULT} from 'angular2/change_detection'; import {DEFAULT} from 'angular2/change_detection';
@ -778,15 +778,6 @@ export class Directive extends Injectable {
this.compileChildren = compileChildren; this.compileChildren = compileChildren;
this.hostInjector = hostInjector; this.hostInjector = hostInjector;
} }
/**
* Returns true if a directive participates in a given `LifecycleEvent`.
*
* See {@link onChange}, {@link onDestroy}, {@link onAllChangesDone} for details.
*/
hasLifecycleHook(hook: LifecycleEvent): boolean {
return isPresent(this.lifecycle) ? ListWrapper.contains(this.lifecycle, hook) : false;
}
} }
/** /**

View File

@ -0,0 +1,26 @@
import 'package:angular2/src/core/annotations_impl/annotations.dart';
import 'package:angular2/src/core/compiler/interfaces.dart';
import 'package:angular2/src/reflection/reflection.dart';
bool hasLifecycleHook(LifecycleEvent e, type, Directive annotation) {
if (annotation.lifecycle != null) {
return annotation.lifecycle.contains(e);
} else {
if (type is! Type) return false;
final List interfaces = reflector.interfaces(type);
var interface;
if (e == onChange) {
interface = OnChange;
} else if (e == onDestroy) {
interface = OnDestroy;
} else if (e == onAllChangesDone) {
interface = OnAllChangesDone;
}
return interfaces.contains(interface);
}
}

View File

@ -0,0 +1,11 @@
import {Type, isPresent} from 'angular2/src/facade/lang';
import {LifecycleEvent, Directive} from 'angular2/src/core/annotations_impl/annotations';
export function hasLifecycleHook(e: LifecycleEvent, type, annotation: Directive): boolean {
if (isPresent(annotation.lifecycle)) {
return annotation.lifecycle.indexOf(e) !== -1;
} else {
if (!(type instanceof Type)) return false;
return e.name in(<any>type).prototype;
}
}

View File

@ -28,6 +28,7 @@ import {
onDestroy, onDestroy,
onAllChangesDone onAllChangesDone
} from 'angular2/src/core/annotations_impl/annotations'; } from 'angular2/src/core/annotations_impl/annotations';
import {hasLifecycleHook} from './directive_lifecycle_reflector';
import {ChangeDetector, ChangeDetectorRef} from 'angular2/change_detection'; import {ChangeDetector, ChangeDetectorRef} from 'angular2/change_detection';
import {QueryList} from './query_list'; import {QueryList} from './query_list';
import {reflector} from 'angular2/src/reflection/reflection'; import {reflector} from 'angular2/src/reflection/reflection';
@ -282,7 +283,6 @@ export class DirectiveBinding extends ResolvedBinding {
var resolvedViewInjectables = ann instanceof Component && isPresent(ann.viewInjector) ? var resolvedViewInjectables = ann instanceof Component && isPresent(ann.viewInjector) ?
resolveBindings(ann.viewInjector) : resolveBindings(ann.viewInjector) :
[]; [];
var metadata = new DirectiveMetadata({ var metadata = new DirectiveMetadata({
id: stringify(rb.key.token), id: stringify(rb.key.token),
type: ann instanceof type: ann instanceof
@ -300,9 +300,11 @@ export class DirectiveBinding extends ResolvedBinding {
null, null,
properties: isPresent(ann.properties) ? MapWrapper.createFromStringMap(ann.properties) : null, properties: isPresent(ann.properties) ? MapWrapper.createFromStringMap(ann.properties) : null,
readAttributes: DirectiveBinding._readAttributes(deps), readAttributes: DirectiveBinding._readAttributes(deps),
callOnDestroy: ann.hasLifecycleHook(onDestroy),
callOnChange: ann.hasLifecycleHook(onChange), callOnDestroy: hasLifecycleHook(onDestroy, rb.key.token, ann),
callOnAllChangesDone: ann.hasLifecycleHook(onAllChangesDone), callOnChange: hasLifecycleHook(onChange, rb.key.token, ann),
callOnAllChangesDone: hasLifecycleHook(onAllChangesDone, rb.key.token, ann),
changeDetection: ann instanceof changeDetection: ann instanceof
Component ? ann.changeDetection : null Component ? ann.changeDetection : null
}); });

View File

@ -12,6 +12,7 @@ export interface OnChange { onChange(changes: StringMap<string, any>): void; }
export interface OnDestroy { onDestroy(): void; } export interface OnDestroy { onDestroy(): void; }
/** /**
* Defines lifecycle method [onAllChangesDone ] called when the bindings of all its children have been changed. * Defines lifecycle method [onAllChangesDone ] called when the bindings of all its children have
* been changed.
*/ */
export interface OnAllChangesDone { onAllChangesDone (): void; } export interface OnAllChangesDone { onAllChangesDone(): void; }

View File

@ -1,6 +1,5 @@
library reflection.reflection_capabilities; library reflection.reflection_capabilities;
import 'reflection.dart';
import 'package:angular2/src/facade/lang.dart'; import 'package:angular2/src/facade/lang.dart';
import 'types.dart'; import 'types.dart';
import 'dart:mirrors'; import 'dart:mirrors';

View File

@ -97,9 +97,7 @@ export class ReflectionCapabilities {
return []; return [];
} }
interfaces(type): List<any> { interfaces(type): List<any> { throw new BaseException("JavaScript does not support interfaces"); }
throw new BaseException("JavaScript does not support interfaces");
}
getter(name: string): GetterFn { return new Function('o', 'return o.' + name + ';'); } getter(name: string): GetterFn { return new Function('o', 'return o.' + name + ';'); }

View File

@ -1,7 +1,13 @@
import {Type, isPresent, stringify, BaseException} from 'angular2/src/facade/lang'; import {Type, isPresent, stringify, BaseException} from 'angular2/src/facade/lang';
import {List, ListWrapper, Map, MapWrapper, StringMapWrapper} from 'angular2/src/facade/collection'; import {
List,
ListWrapper,
Map,
MapWrapper,
StringMap,
StringMapWrapper
} from 'angular2/src/facade/collection';
import {SetterFn, GetterFn, MethodFn} from './types'; import {SetterFn, GetterFn, MethodFn} from './types';
export {SetterFn, GetterFn, MethodFn} from './types';
export class Reflector { export class Reflector {
_typeInfo: Map<Type, any>; _typeInfo: Map<Type, any>;
@ -18,7 +24,7 @@ export class Reflector {
this.reflectionCapabilities = reflectionCapabilities; this.reflectionCapabilities = reflectionCapabilities;
} }
registerType(type: Type, typeInfo: Map<Type, any>): void { registerType(type: Type, typeInfo: StringMap<string, any>): void {
MapWrapper.set(this._typeInfo, type, typeInfo); MapWrapper.set(this._typeInfo, type, typeInfo);
} }
@ -29,32 +35,32 @@ export class Reflector {
registerMethods(methods: Map<string, MethodFn>): void { _mergeMaps(this._methods, methods); } registerMethods(methods: Map<string, MethodFn>): void { _mergeMaps(this._methods, methods); }
factory(type: Type): Function { factory(type: Type): Function {
if (MapWrapper.contains(this._typeInfo, type)) { if (this._containsTypeInfo(type)) {
return MapWrapper.get(this._typeInfo, type)["factory"]; return this._getTypeInfoField(type, "factory", null);
} else { } else {
return this.reflectionCapabilities.factory(type); return this.reflectionCapabilities.factory(type);
} }
} }
parameters(typeOfFunc): List<any> { parameters(typeOrFunc): List<any> {
if (MapWrapper.contains(this._typeInfo, typeOfFunc)) { if (MapWrapper.contains(this._typeInfo, typeOrFunc)) {
return MapWrapper.get(this._typeInfo, typeOfFunc)["parameters"]; return this._getTypeInfoField(typeOrFunc, "parameters", []);
} else { } else {
return this.reflectionCapabilities.parameters(typeOfFunc); return this.reflectionCapabilities.parameters(typeOrFunc);
} }
} }
annotations(typeOfFunc): List<any> { annotations(typeOrFunc): List<any> {
if (MapWrapper.contains(this._typeInfo, typeOfFunc)) { if (MapWrapper.contains(this._typeInfo, typeOrFunc)) {
return MapWrapper.get(this._typeInfo, typeOfFunc)["annotations"]; return this._getTypeInfoField(typeOrFunc, "annotations", []);
} else { } else {
return this.reflectionCapabilities.annotations(typeOfFunc); return this.reflectionCapabilities.annotations(typeOrFunc);
} }
} }
interfaces(type): List<any> { interfaces(type): List<any> {
if (MapWrapper.contains(this._typeInfo, type)) { if (MapWrapper.contains(this._typeInfo, type)) {
return MapWrapper.get(this._typeInfo, type)["interfaces"]; return this._getTypeInfoField(type, "interfaces", []);
} else { } else {
return this.reflectionCapabilities.interfaces(type); return this.reflectionCapabilities.interfaces(type);
} }
@ -83,6 +89,13 @@ export class Reflector {
return this.reflectionCapabilities.method(name); return this.reflectionCapabilities.method(name);
} }
} }
_getTypeInfoField(typeOrFunc, key, defaultValue) {
var res = MapWrapper.get(this._typeInfo, typeOrFunc)[key];
return isPresent(res) ? res : defaultValue;
}
_containsTypeInfo(typeOrFunc) { return MapWrapper.contains(this._typeInfo, typeOrFunc); }
} }
function _mergeMaps(target: Map<any, any>, config: Map<string, Function>): void { function _mergeMaps(target: Map<any, any>, config: Map<string, Function>): void {

View File

@ -1,23 +0,0 @@
import {ddescribe, describe, it, iit, expect, beforeEach} from 'angular2/test_lib';
import {Directive, onChange} from 'angular2/src/core/annotations_impl/annotations';
export function main() {
describe("Directive", () => {
describe("lifecycle", () => {
it("should be false when no lifecycle specified", () => {
var d = new Directive();
expect(d.hasLifecycleHook(onChange)).toBe(false);
});
it("should be false when the lifecycle does not contain the hook", () => {
var d = new Directive({lifecycle: []});
expect(d.hasLifecycleHook(onChange)).toBe(false);
});
it("should be true otherwise", () => {
var d = new Directive({lifecycle: [onChange]});
expect(d.hasLifecycleHook(onChange)).toBe(true);
});
});
});
}

View File

@ -0,0 +1,75 @@
library angular2.test.core.compiler.directive_lifecycle_spec;
import 'package:angular2/test_lib.dart';
import 'package:angular2/angular2.dart';
import 'package:angular2/src/core/compiler/element_injector.dart';
main() {
describe('Create DirectiveMetadata', () {
describe('lifecycle', () {
metadata(type, annotation) => DirectiveBinding.createFromType(type, annotation).metadata;
describe("onChange", () {
it("should be true when the directive implements OnChange", () {
expect(metadata(DirectiveImplementingOnChange, new Directive()).callOnChange).toBe(true);
});
it("should be true when the lifecycle includes onChange", () {
expect(metadata(DirectiveNoHooks, new Directive(lifecycle: [onChange])).callOnChange).toBe(true);
});
it("should be false otherwise", () {
expect(metadata(DirectiveNoHooks, new Directive()).callOnChange).toBe(false);
});
it("should be false when empty lifecycle", () {
expect(metadata(DirectiveImplementingOnChange, new Directive(lifecycle: [])).callOnChange).toBe(false);
});
});
describe("onDestroy", () {
it("should be true when the directive implements OnDestroy", () {
expect(metadata(DirectiveImplementingOnDestroy, new Directive()).callOnDestroy).toBe(true);
});
it("should be true when the lifecycle includes onDestroy", () {
expect(metadata(DirectiveNoHooks, new Directive(lifecycle: [onDestroy])).callOnDestroy).toBe(true);
});
it("should be false otherwise", () {
expect(metadata(DirectiveNoHooks, new Directive()).callOnDestroy).toBe(false);
});
});
describe("onAllChangesDone", () {
it("should be true when the directive implements OnAllChangesDone", () {
expect(metadata(DirectiveImplementingOnAllChangesDone, new Directive()).callOnAllChangesDone).toBe(true);
});
it("should be true when the lifecycle includes onAllChangesDone", () {
expect(metadata(DirectiveNoHooks, new Directive(lifecycle: [onAllChangesDone])).callOnAllChangesDone).toBe(true);
});
it("should be false otherwise", () {
expect(metadata(DirectiveNoHooks, new Directive()).callOnAllChangesDone).toBe(false);
});
});
});
});
}
class DirectiveNoHooks {
}
class DirectiveImplementingOnChange implements OnChange {
onChange(_){}
}
class DirectiveImplementingOnDestroy implements OnDestroy {
onDestroy(){}
}
class DirectiveImplementingOnAllChangesDone implements OnAllChangesDone {
onAllChangesDone(){}
}

View File

@ -0,0 +1,100 @@
import {
AsyncTestCompleter,
beforeEach,
xdescribe,
ddescribe,
describe,
el,
expect,
iit,
inject,
IS_DARTIUM,
it,
SpyObject,
proxy
} from 'angular2/test_lib';
import {
Directive,
onChange,
onDestroy,
onAllChangesDone
} from 'angular2/src/core/annotations_impl/annotations';
import {DirectiveBinding} from 'angular2/src/core/compiler/element_injector';
export function main() {
describe('Create DirectiveMetadata', () => {
describe('lifecycle', () => {
function metadata(type, annotation) {
return DirectiveBinding.createFromType(type, annotation).metadata;
}
describe("onChange", () => {
it("should be true when the directive has the onChange method", () => {
expect(metadata(DirectiveWithOnChangeMethod, new Directive({})).callOnChange).toBe(true);
});
it("should be true when the lifecycle includes onChange", () => {
expect(metadata(DirectiveNoHooks, new Directive({lifecycle: [onChange]})).callOnChange)
.toBe(true);
});
it("should be false otherwise",
() => { expect(metadata(DirectiveNoHooks, new Directive()).callOnChange).toBe(false); });
it("should be false when empty lifecycle", () => {
expect(metadata(DirectiveWithOnChangeMethod, new Directive({lifecycle: []})).callOnChange)
.toBe(false);
});
});
describe("onDestroy", () => {
it("should be true when the directive has the onDestroy method", () => {
expect(metadata(DirectiveWithOnDestroyMethod, new Directive({})).callOnDestroy)
.toBe(true);
});
it("should be true when the lifecycle includes onDestroy", () => {
expect(metadata(DirectiveNoHooks, new Directive({lifecycle: [onDestroy]})).callOnDestroy)
.toBe(true);
});
it("should be false otherwise", () => {
expect(metadata(DirectiveNoHooks, new Directive()).callOnDestroy).toBe(false);
});
});
describe("onAllChangesDone", () => {
it("should be true when the directive has the onAllChangesDone method", () => {
expect(
metadata(DirectiveWithOnAllChangesDoneMethod, new Directive({})).callOnAllChangesDone)
.toBe(true);
});
it("should be true when the lifecycle includes onAllChangesDone", () => {
expect(metadata(DirectiveNoHooks, new Directive({lifecycle: [onAllChangesDone]}))
.callOnAllChangesDone)
.toBe(true);
});
it("should be false otherwise", () => {
expect(metadata(DirectiveNoHooks, new Directive()).callOnAllChangesDone).toBe(false);
});
});
});
});
}
class DirectiveNoHooks {}
class DirectiveWithOnChangeMethod {
onChange(_) {}
}
class DirectiveWithOnDestroyMethod {
onDestroy(_) {}
}
class DirectiveWithOnAllChangesDoneMethod {
onAllChangesDone(_) {}
}

View File

@ -42,8 +42,7 @@ class TestObj {
class Interface {} class Interface {}
class ClassImplementingInterface implements Interface { class ClassImplementingInterface implements Interface {}
}
export function main() { export function main() {
describe('Reflector', () => { describe('Reflector', () => {
@ -85,6 +84,11 @@ export function main() {
reflector.registerType(TestObj, {"parameters": [1, 2]}); reflector.registerType(TestObj, {"parameters": [1, 2]});
expect(reflector.parameters(TestObj)).toEqual([1, 2]); expect(reflector.parameters(TestObj)).toEqual([1, 2]);
}); });
it("should return an empty list when no paramters field in the stored type info", () => {
reflector.registerType(TestObj, {});
expect(reflector.parameters(TestObj)).toEqual([]);
});
}); });
describe("annotations", () => { describe("annotations", () => {