feat(core): made directives shadow native element properties
BREAKING CHANGE Previously, if an element had a property, Angular would update that property even if there was a directive placed on the same element with the same property. Now, the directive would have to explicitly update the native elmement by either using hostProperties or the renderer.
This commit is contained in:
parent
3c58878b19
commit
3437d56904
|
@ -330,20 +330,27 @@ const STYLE_PREFIX = 'style';
|
||||||
|
|
||||||
function buildElementPropertyBindings(
|
function buildElementPropertyBindings(
|
||||||
schemaRegistry: ElementSchemaRegistry, protoElement: /*element*/ any, isNgComponent: boolean,
|
schemaRegistry: ElementSchemaRegistry, protoElement: /*element*/ any, isNgComponent: boolean,
|
||||||
bindingsInTemplate: Map<string, ASTWithSource>, directiveTempaltePropertyNames: Set<string>):
|
bindingsInTemplate: Map<string, ASTWithSource>, directiveTemplatePropertyNames: Set<string>):
|
||||||
List<api.ElementPropertyBinding> {
|
List<api.ElementPropertyBinding> {
|
||||||
var propertyBindings = [];
|
var propertyBindings = [];
|
||||||
|
|
||||||
MapWrapper.forEach(bindingsInTemplate, (ast, propertyNameInTemplate) => {
|
MapWrapper.forEach(bindingsInTemplate, (ast, propertyNameInTemplate) => {
|
||||||
var propertyBinding = createElementPropertyBinding(schemaRegistry, ast, propertyNameInTemplate);
|
var propertyBinding = createElementPropertyBinding(schemaRegistry, ast, propertyNameInTemplate);
|
||||||
if (isValidElementPropertyBinding(schemaRegistry, protoElement, isNgComponent,
|
|
||||||
propertyBinding)) {
|
if (isPresent(directiveTemplatePropertyNames) &&
|
||||||
|
SetWrapper.has(directiveTemplatePropertyNames, propertyNameInTemplate)) {
|
||||||
|
// We do nothing because directives shadow native elements properties.
|
||||||
|
|
||||||
|
} else if (isValidElementPropertyBinding(schemaRegistry, protoElement, isNgComponent,
|
||||||
|
propertyBinding)) {
|
||||||
propertyBindings.push(propertyBinding);
|
propertyBindings.push(propertyBinding);
|
||||||
} else if (!isPresent(directiveTempaltePropertyNames) ||
|
|
||||||
!SetWrapper.has(directiveTempaltePropertyNames, propertyNameInTemplate)) {
|
} else {
|
||||||
// directiveTempaltePropertyNames is null for host property bindings
|
|
||||||
var exMsg =
|
var exMsg =
|
||||||
`Can't bind to '${propertyNameInTemplate}' since it isn't a known property of the '<${DOM.tagName(protoElement).toLowerCase()}>' element`;
|
`Can't bind to '${propertyNameInTemplate}' since it isn't a known property of the '<${DOM.tagName(protoElement).toLowerCase()}>' element`;
|
||||||
if (isPresent(directiveTempaltePropertyNames)) {
|
|
||||||
|
// directiveTemplatePropertyNames is null for host property bindings
|
||||||
|
if (isPresent(directiveTemplatePropertyNames)) {
|
||||||
exMsg += ' and there are no matching directives with a corresponding property';
|
exMsg += ' and there are no matching directives with a corresponding property';
|
||||||
}
|
}
|
||||||
throw new BaseException(exMsg);
|
throw new BaseException(exMsg);
|
||||||
|
|
|
@ -31,6 +31,7 @@ import {
|
||||||
isJsObject,
|
isJsObject,
|
||||||
global,
|
global,
|
||||||
stringify,
|
stringify,
|
||||||
|
isBlank,
|
||||||
CONST,
|
CONST,
|
||||||
CONST_EXPR
|
CONST_EXPR
|
||||||
} from 'angular2/src/facade/lang';
|
} from 'angular2/src/facade/lang';
|
||||||
|
@ -1359,8 +1360,8 @@ export function main() {
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (!IS_DARTIUM) {
|
describe('Property bindings', () => {
|
||||||
describe('Missing property bindings', () => {
|
if (!IS_DARTIUM) {
|
||||||
it('should throw on bindings to unknown properties',
|
it('should throw on bindings to unknown properties',
|
||||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder,
|
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder,
|
||||||
async) => {
|
async) => {
|
||||||
|
@ -1386,8 +1387,47 @@ export function main() {
|
||||||
.createAsync(MyComp)
|
.createAsync(MyComp)
|
||||||
.then((val) => { async.done(); });
|
.then((val) => { async.done(); });
|
||||||
}));
|
}));
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
it('should not be created when there is a directive with the same property',
|
||||||
|
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||||
|
tcb.overrideView(MyComp, new viewAnn.View({
|
||||||
|
template: '<span [title]="ctxProp"></span>',
|
||||||
|
directives: [DirectiveWithTitle]
|
||||||
|
}))
|
||||||
|
.createAsync(MyComp)
|
||||||
|
.then((rootTC) => {
|
||||||
|
rootTC.componentInstance.ctxProp = "TITLE";
|
||||||
|
rootTC.detectChanges();
|
||||||
|
|
||||||
|
var el = DOM.querySelector(rootTC.nativeElement, "span");
|
||||||
|
expect(isBlank(el.title) || el.title == '').toBeTruthy();
|
||||||
|
|
||||||
|
async.done();
|
||||||
|
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should work when a directive uses hostProperty to update the DOM element',
|
||||||
|
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||||
|
tcb.overrideView(MyComp, new viewAnn.View({
|
||||||
|
template: '<span [title]="ctxProp"></span>',
|
||||||
|
directives: [DirectiveWithTitleAndHostProperty]
|
||||||
|
}))
|
||||||
|
.createAsync(MyComp)
|
||||||
|
.then((rootTC) => {
|
||||||
|
rootTC.componentInstance.ctxProp = "TITLE";
|
||||||
|
rootTC.detectChanges();
|
||||||
|
|
||||||
|
var el = DOM.querySelector(rootTC.nativeElement, "span");
|
||||||
|
expect(el.title).toEqual("TITLE");
|
||||||
|
|
||||||
|
async.done();
|
||||||
|
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('different proto view storages', () => {
|
describe('different proto view storages', () => {
|
||||||
function runWithMode(mode: string) {
|
function runWithMode(mode: string) {
|
||||||
|
@ -1505,6 +1545,16 @@ class MyDir {
|
||||||
constructor() { this.dirProp = ''; }
|
constructor() { this.dirProp = ''; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Directive({selector: '[title]', properties: ['title']})
|
||||||
|
class DirectiveWithTitle {
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Directive({selector: '[title]', properties: ['title'], host: {'[title]': 'title'}})
|
||||||
|
class DirectiveWithTitleAndHostProperty {
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
@Component({selector: 'push-cmp', properties: ['prop'], changeDetection: ON_PUSH})
|
@Component({selector: 'push-cmp', properties: ['prop'], changeDetection: ON_PUSH})
|
||||||
@View({template: '{{field}}'})
|
@View({template: '{{field}}'})
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|
|
@ -104,42 +104,53 @@ export function main() {
|
||||||
var pv = builder.build(new DomElementSchemaRegistry(), templateCloner);
|
var pv = builder.build(new DomElementSchemaRegistry(), templateCloner);
|
||||||
expect(pv.elementBinders[0].propertyBindings[0].property).toEqual('readOnly');
|
expect(pv.elementBinders[0].propertyBindings[0].property).toEqual('readOnly');
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('property binding types', () => {
|
describe('property binding', () => {
|
||||||
it('should detect property names', () => {
|
describe('types', () => {
|
||||||
builder.bindElement(el('<div/>')).bindProperty('tabindex', emptyExpr());
|
it('should detect property names', () => {
|
||||||
var pv = builder.build(new DomElementSchemaRegistry(), templateCloner);
|
builder.bindElement(el('<div/>')).bindProperty('tabindex', emptyExpr());
|
||||||
expect(pv.elementBinders[0].propertyBindings[0].type).toEqual(PropertyBindingType.PROPERTY);
|
var pv = builder.build(new DomElementSchemaRegistry(), templateCloner);
|
||||||
|
expect(pv.elementBinders[0].propertyBindings[0].type)
|
||||||
|
.toEqual(PropertyBindingType.PROPERTY);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect attribute names', () => {
|
||||||
|
builder.bindElement(el('<div/>')).bindProperty('attr.someName', emptyExpr());
|
||||||
|
var pv = builder.build(new DomElementSchemaRegistry(), templateCloner);
|
||||||
|
expect(pv.elementBinders[0].propertyBindings[0].type)
|
||||||
|
.toEqual(PropertyBindingType.ATTRIBUTE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect class names', () => {
|
||||||
|
builder.bindElement(el('<div/>')).bindProperty('class.someName', emptyExpr());
|
||||||
|
var pv = builder.build(new DomElementSchemaRegistry(), templateCloner);
|
||||||
|
expect(pv.elementBinders[0].propertyBindings[0].type).toEqual(PropertyBindingType.CLASS);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect style names', () => {
|
||||||
|
builder.bindElement(el('<div/>')).bindProperty('style.someName', emptyExpr());
|
||||||
|
var pv = builder.build(new DomElementSchemaRegistry(), templateCloner);
|
||||||
|
expect(pv.elementBinders[0].propertyBindings[0].type).toEqual(PropertyBindingType.STYLE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect style units', () => {
|
||||||
|
builder.bindElement(el('<div/>')).bindProperty('style.someName.someUnit', emptyExpr());
|
||||||
|
var pv = builder.build(new DomElementSchemaRegistry(), templateCloner);
|
||||||
|
expect(pv.elementBinders[0].propertyBindings[0].unit).toEqual('someUnit');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should detect attribute names', () => {
|
it('should not create a property binding when there is already same directive property binding',
|
||||||
builder.bindElement(el('<div/>')).bindProperty('attr.someName', emptyExpr());
|
() => {
|
||||||
var pv = builder.build(new DomElementSchemaRegistry(), templateCloner);
|
var binder = builder.bindElement(el('<div/>'));
|
||||||
expect(pv.elementBinders[0].propertyBindings[0].type)
|
|
||||||
.toEqual(PropertyBindingType.ATTRIBUTE);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should detect class names', () => {
|
binder.bindProperty('tabindex', emptyExpr());
|
||||||
builder.bindElement(el('<div/>')).bindProperty('class.someName', emptyExpr());
|
binder.bindDirective(0).bindProperty('tabindex', emptyExpr(), 'tabindex');
|
||||||
var pv = builder.build(new DomElementSchemaRegistry(), templateCloner);
|
|
||||||
expect(pv.elementBinders[0].propertyBindings[0].type).toEqual(PropertyBindingType.CLASS);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should detect style names', () => {
|
var pv = builder.build(new DomElementSchemaRegistry(), templateCloner);
|
||||||
builder.bindElement(el('<div/>')).bindProperty('style.someName', emptyExpr());
|
expect(pv.elementBinders[0].propertyBindings.length).toEqual(0);
|
||||||
var pv = builder.build(new DomElementSchemaRegistry(), templateCloner);
|
});
|
||||||
expect(pv.elementBinders[0].propertyBindings[0].type).toEqual(PropertyBindingType.STYLE);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should detect style units', () => {
|
|
||||||
builder.bindElement(el('<div/>')).bindProperty('style.someName.someUnit', emptyExpr());
|
|
||||||
var pv = builder.build(new DomElementSchemaRegistry(), templateCloner);
|
|
||||||
expect(pv.elementBinders[0].propertyBindings[0].unit).toEqual('someUnit');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue