feat(view): reimplemented property setters using change detection

This commit is contained in:
vsavkin
2015-04-21 11:47:53 -07:00
parent 8a92a1f13e
commit 8ccafb0524
36 changed files with 510 additions and 469 deletions

View File

@ -187,8 +187,8 @@ import {DEFAULT} from 'angular2/change_detection';
*
*
* A directive can also query for other child directives. Since parent directives are instantiated before child
* directives, a directive can't simply inject the list of child directives. Instead, the directive
* injects a {@link QueryList}, which updates its contents as children are added, removed, or moved by any
* directives, a directive can't simply inject the list of child directives. Instead, the directive
* injects a {@link QueryList}, which updates its contents as children are added, removed, or moved by any
* {@link Viewport} directive such as a `for`, an `if`, or a `switch`.
*
* ```
@ -199,7 +199,7 @@ import {DEFAULT} from 'angular2/change_detection';
* }
* ```
*
* This directive would be instantiated with a {@link QueryList} which contains `Dependency` 4 and 6. Here, `Dependency`
* This directive would be instantiated with a {@link QueryList} which contains `Dependency` 4 and 6. Here, `Dependency`
* 5 would not be included, because it is not a direct child.
*
* ### Injecting a live collection of descendant directives
@ -444,6 +444,29 @@ export class Directive extends Injectable {
*/
hostListeners:any; // StringMap
/**
* Specifies which DOM properties a directives updates.
*
* ## Syntax
*
* ```
* @Decorator({
* selector: 'input',
* hostProperties: {
* 'value': 'value'
* }
* })
* class InputDecorator {
* value:string;
* }
*
* In this example every time the value property of the decorator changes, Angular will update the value property of
* the host element.
* ```
*/
hostProperties:any; // String map
/**
* Specifies a set of lifecycle hostListeners in which the directive participates.
*
@ -457,12 +480,14 @@ export class Directive extends Injectable {
properties,
events,
hostListeners,
hostProperties,
lifecycle
}:{
selector:string,
properties:any,
events:List,
hostListeners: any,
hostProperties: any,
lifecycle:List
}={})
{
@ -471,6 +496,7 @@ export class Directive extends Injectable {
this.properties = properties;
this.events = events;
this.hostListeners = hostListeners;
this.hostProperties = hostProperties;
this.lifecycle = lifecycle;
}
@ -579,20 +605,22 @@ export class Component extends Directive {
*/
injectables:List;
@CONST()
@CONST()
constructor({
selector,
properties,
events,
hostListeners,
injectables,
lifecycle,
changeDetection = DEFAULT
selector,
properties,
events,
hostListeners,
hostProperties,
injectables,
lifecycle,
changeDetection = DEFAULT
}:{
selector:string,
properties:Object,
events:List,
hostListeners:Object,
hostListeners:any,
hostProperties:any,
injectables:List,
lifecycle:List,
changeDetection:string
@ -603,6 +631,7 @@ export class Component extends Directive {
properties: properties,
events: events,
hostListeners: hostListeners,
hostProperties: hostProperties,
lifecycle: lifecycle
});
@ -667,17 +696,19 @@ export class DynamicComponent extends Directive {
@CONST()
constructor({
selector,
properties,
events,
hostListeners,
injectables,
lifecycle
selector,
properties,
events,
hostListeners,
hostProperties,
injectables,
lifecycle
}:{
selector:string,
properties:Object,
properties:any,
events:List,
hostListeners:Object,
hostListeners:any,
hostProperties:any,
injectables:List,
lifecycle:List
}={}) {
@ -686,6 +717,7 @@ export class DynamicComponent extends Directive {
properties: properties,
events: events,
hostListeners: hostListeners,
hostProperties: hostProperties,
lifecycle: lifecycle
});
@ -767,6 +799,7 @@ export class Decorator extends Directive {
properties,
events,
hostListeners,
hostProperties,
lifecycle,
compileChildren = true,
}:{
@ -774,16 +807,18 @@ export class Decorator extends Directive {
properties:any,
events:List,
hostListeners:any,
hostProperties:any,
lifecycle:List,
compileChildren:boolean
}={})
{
super({
selector: selector,
properties: properties,
events: events,
hostListeners: hostListeners,
lifecycle: lifecycle
selector: selector,
properties: properties,
events: events,
hostListeners: hostListeners,
hostProperties: hostProperties,
lifecycle: lifecycle
});
this.compileChildren = compileChildren;
}
@ -889,20 +924,24 @@ export class Viewport extends Directive {
properties,
events,
hostListeners,
hostProperties,
lifecycle
}:{
selector:string,
properties:any,
events:List,
lifecycle:List
selector:string,
properties:any,
hostListeners:any,
hostProperties:any,
events:List,
lifecycle:List
}={})
{
super({
selector: selector,
properties: properties,
events: events,
hostListeners: hostListeners,
lifecycle: lifecycle
selector: selector,
properties: properties,
events: events,
hostListeners: hostListeners,
hostProperties: hostProperties,
lifecycle: lifecycle
});
}
}

View File

@ -1,28 +1,6 @@
import {CONST} from 'angular2/src/facade/lang';
import {DependencyAnnotation} from 'angular2/di';
/**
* Specifies that a function for setting host properties should be injected.
*
* NOTE: This is changing pre 1.0.
*
* The directive can inject a property setter that would allow setting this property on the host element.
*
* @exportedAs angular2/annotations
*/
export class PropertySetter extends DependencyAnnotation {
propName: string;
@CONST()
constructor(propName) {
super();
this.propName = propName;
}
get token() {
return Function;
}
}
/**
* Specifies that a constant attribute value should be injected.
*

View File

@ -223,12 +223,8 @@ export class Compiler {
renderType = renderApi.DirectiveMetadata.DECORATOR_TYPE;
compileChildren = ann.compileChildren;
}
var setters = [];
var readAttributes = [];
ListWrapper.forEach(directiveBinding.dependencies, (dep) => {
if (isPresent(dep.propSetterName)) {
ListWrapper.push(setters, dep.propSetterName);
}
if (isPresent(dep.attributeName)) {
ListWrapper.push(readAttributes, dep.attributeName);
}
@ -239,8 +235,8 @@ export class Compiler {
selector: ann.selector,
compileChildren: compileChildren,
hostListeners: isPresent(ann.hostListeners) ? MapWrapper.createFromStringMap(ann.hostListeners) : null,
hostProperties: isPresent(ann.hostProperties) ? MapWrapper.createFromStringMap(ann.hostProperties) : null,
properties: isPresent(ann.properties) ? MapWrapper.createFromStringMap(ann.properties) : null,
setters: setters,
readAttributes: readAttributes
});
}

View File

@ -5,7 +5,7 @@ import {List, ListWrapper, MapWrapper} from 'angular2/src/facade/collection';
import {Injector, Key, Dependency, bind, Binding, ResolvedBinding, NoBindingError,
AbstractBindingError, CyclicDependencyError} from 'angular2/di';
import {Parent, Ancestor} from 'angular2/src/core/annotations/visibility';
import {PropertySetter, Attribute, Query} from 'angular2/src/core/annotations/di';
import {Attribute, Query} from 'angular2/src/core/annotations/di';
import * as viewModule from 'angular2/src/core/compiler/view';
import {ViewContainer} from 'angular2/src/core/compiler/view_container';
import {NgElement} from 'angular2/src/core/compiler/ng_element';
@ -195,15 +195,13 @@ export class TreeNode {
export class DirectiveDependency extends Dependency {
depth:int;
propSetterName:string;
attributeName:string;
queryDirective;
constructor(key:Key, asPromise:boolean, lazy:boolean, optional:boolean, properties:List,
depth:int, propSetterName: string, attributeName:string, queryDirective) {
depth:int, attributeName:string, queryDirective) {
super(key, asPromise, lazy, optional, properties);
this.depth = depth;
this.propSetterName = propSetterName;
this.attributeName = attributeName;
this.queryDirective = queryDirective;
this._verify();
@ -211,17 +209,15 @@ export class DirectiveDependency extends Dependency {
_verify():void {
var count = 0;
if (isPresent(this.propSetterName)) count++;
if (isPresent(this.queryDirective)) count++;
if (isPresent(this.attributeName)) count++;
if (count > 1) throw new BaseException(
'A directive injectable can contain only one of the following @PropertySetter, @Attribute or @Query.');
'A directive injectable can contain only one of the following @Attribute or @Query.');
}
static createFrom(d:Dependency):Dependency {
return new DirectiveDependency(d.key, d.asPromise, d.lazy, d.optional,
d.properties, DirectiveDependency._depth(d.properties),
DirectiveDependency._propSetterName(d.properties),
DirectiveDependency._attributeName(d.properties),
DirectiveDependency._query(d.properties)
);
@ -234,11 +230,6 @@ export class DirectiveDependency extends Dependency {
return 0;
}
static _propSetterName(properties):string {
var p = ListWrapper.find(properties, (p) => p instanceof PropertySetter);
return isPresent(p) ? p.propName : null;
}
static _attributeName(properties):string {
var p = ListWrapper.find(properties, (p) => p instanceof Attribute);
return isPresent(p) ? p.attributeName : null;
@ -268,6 +259,10 @@ export class DirectiveBinding extends ResolvedBinding {
}
}
get displayName() {
return this.key.displayName;
}
get eventEmitters():List<string> {
return isPresent(this.annotation) && isPresent(this.annotation.events) ? this.annotation.events : [];
}
@ -735,7 +730,6 @@ export class ElementInjector extends TreeNode {
}
_getByDependency(dep:DirectiveDependency, requestor:Key) {
if (isPresent(dep.propSetterName)) return this._buildPropSetter(dep);
if (isPresent(dep.attributeName)) return this._buildAttribute(dep);
if (isPresent(dep.queryDirective)) return this._findQuery(dep.queryDirective).list;
if (dep.key.id === StaticKeys.instance().elementRefId) {
@ -744,15 +738,6 @@ export class ElementInjector extends TreeNode {
return this._getByKey(dep.key, dep.depth, dep.optional, requestor);
}
_buildPropSetter(dep) {
var view = this._getPreBuiltObjectByKeyId(StaticKeys.instance().viewId);
var renderer = view.renderer;
var index = this._proto.index;
return function(v) {
renderer.setElementProperty(view.render, index, dep.propSetterName, v);
};
}
_buildAttribute(dep): string {
var attributes = this._proto.attributes;
if (isPresent(attributes) && MapWrapper.contains(attributes, dep.attributeName)) {

View File

@ -3,7 +3,7 @@ import {List, ListWrapper, MapWrapper} from 'angular2/src/facade/collection';
import {isPresent, isBlank} from 'angular2/src/facade/lang';
import {reflector} from 'angular2/src/reflection/reflection';
import {ChangeDetection} from 'angular2/change_detection';
import {ChangeDetection, DirectiveIndex} from 'angular2/change_detection';
import {Component, Viewport, DynamicComponent} from '../annotations/annotations';
import * as renderApi from 'angular2/src/render/api';
@ -18,7 +18,8 @@ export class ProtoViewFactory {
this._changeDetection = changeDetection;
}
createProtoView(componentBinding:DirectiveBinding, renderProtoView: renderApi.ProtoViewDto, directives:List<DirectiveBinding>):AppProtoView {
createProtoView(componentBinding:DirectiveBinding, renderProtoView: renderApi.ProtoViewDto,
directives:List<DirectiveBinding>):AppProtoView {
var protoChangeDetector;
if (isBlank(componentBinding)) {
protoChangeDetector = this._changeDetection.createProtoChangeDetector('root', null);
@ -43,7 +44,7 @@ export class ProtoViewFactory {
this._createElementBinder(
protoView, renderElementBinder, protoElementInjector, sortedDirectives
);
this._createDirectiveBinders(protoView, sortedDirectives);
this._createDirectiveBinders(protoView, i, sortedDirectives);
}
MapWrapper.forEach(renderProtoView.variableBindings, (mappedName, varName) => {
protoView.bindVariable(varName, mappedName);
@ -128,27 +129,34 @@ export class ProtoViewFactory {
return elBinder;
}
_createDirectiveBinders(protoView, sortedDirectives) {
for (var i=0; i<sortedDirectives.renderDirectives.length; i++) {
var renderDirectiveMetadata = sortedDirectives.renderDirectives[i];
_createDirectiveBinders(protoView, boundElementIndex, sortedDirectives) {
for (var i = 0; i < sortedDirectives.renderDirectives.length; i++) {
var directiveBinder = sortedDirectives.renderDirectives[i];
// directive properties
MapWrapper.forEach(renderDirectiveMetadata.propertyBindings, (astWithSource, propertyName) => {
MapWrapper.forEach(directiveBinder.propertyBindings, (astWithSource, propertyName) => {
// TODO: these setters should eventually be created by change detection, to make
// it monomorphic!
var setter = reflector.setter(propertyName);
protoView.bindDirectiveProperty(i, astWithSource, propertyName, setter);
});
// host properties
MapWrapper.forEach(directiveBinder.hostPropertyBindings, (astWithSource, propertyName) => {
var directiveIndex = new DirectiveIndex(boundElementIndex, i);
protoView.bindHostElementProperty(astWithSource, propertyName, directiveIndex);
});
// directive events
protoView.bindEvent(renderDirectiveMetadata.eventBindings, i);
protoView.bindEvent(directiveBinder.eventBindings, i);
}
}
}
class SortedDirectives {
componentDirective: DirectiveBinding;
viewportDirective: DirectiveBinding;
renderDirectives: List<renderApi.DirectiveMetadata>;
renderDirectives: List<renderApi.DirectiveBinder>;
directives: List<DirectiveBinding>;
constructor(renderDirectives, allDirectives) {
@ -156,18 +164,18 @@ class SortedDirectives {
this.directives = [];
this.viewportDirective = null;
this.componentDirective = null;
ListWrapper.forEach(renderDirectives, (renderDirectiveMetadata) => {
var directiveBinding = allDirectives[renderDirectiveMetadata.directiveIndex];
ListWrapper.forEach(renderDirectives, (renderDirectiveBinder) => {
var directiveBinding = allDirectives[renderDirectiveBinder.directiveIndex];
if ((directiveBinding.annotation instanceof Component) || (directiveBinding.annotation instanceof DynamicComponent)) {
// component directives need to be the first binding in ElementInjectors!
this.componentDirective = directiveBinding;
ListWrapper.insert(this.renderDirectives, 0, renderDirectiveMetadata);
ListWrapper.insert(this.renderDirectives, 0, renderDirectiveBinder);
ListWrapper.insert(this.directives, 0, directiveBinding);
} else {
if (directiveBinding.annotation instanceof Viewport) {
this.viewportDirective = directiveBinding;
}
ListWrapper.push(this.renderDirectives, renderDirectiveMetadata);
ListWrapper.push(this.renderDirectives, renderDirectiveBinder);
ListWrapper.push(this.directives, directiveBinding);
}
});

View File

@ -1,6 +1,6 @@
import {ListWrapper, MapWrapper, Map, StringMapWrapper, List} from 'angular2/src/facade/collection';
import {AST, Locals, ChangeDispatcher, ProtoChangeDetector, ChangeDetector,
ChangeRecord, BindingRecord, DirectiveRecord, ChangeDetectorRef} from 'angular2/change_detection';
ChangeRecord, BindingRecord, DirectiveRecord, DirectiveIndex, ChangeDetectorRef} from 'angular2/change_detection';
import {ProtoElementInjector, ElementInjector, PreBuiltObjects, DirectiveBinding} from './element_injector';
import {ElementBinder} from './element_binder';
@ -124,12 +124,12 @@ export class AppView {
}
}
getDirectiveFor(directive:DirectiveRecord) {
getDirectiveFor(directive:DirectiveIndex) {
var elementInjector = this.elementInjectors[directive.elementIndex];
return elementInjector.getDirectiveAtIndex(directive.directiveIndex);
}
getDetectorFor(directive:DirectiveRecord) {
getDetectorFor(directive:DirectiveIndex) {
var elementInjector = this.elementInjectors[directive.elementIndex];
return elementInjector.getChangeDetector();
}
@ -267,6 +267,14 @@ export class AppProtoView {
ListWrapper.push(this.bindings, b);
}
/**
* Adds an host property binding for the last created ElementBinder via bindElement
*/
bindHostElementProperty(expression:AST, setterName:string, directiveIndex:DirectiveIndex):void {
var b = BindingRecord.createForHostProperty(directiveIndex, expression, setterName);
ListWrapper.push(this.bindings, b);
}
/**
* Adds an event binding for the last created ElementBinder via bindElement.
*
@ -323,7 +331,7 @@ export class AppProtoView {
var changeDetection = binding.changeDetection;
MapWrapper.set(this._directiveRecordsMap, id,
new DirectiveRecord(elementInjectorIndex, directiveIndex,
new DirectiveRecord(new DirectiveIndex(elementInjectorIndex, directiveIndex),
binding.callOnAllChangesDone, binding.callOnChange, changeDetection));
}