feat(ivy): Add InheritanceDefinitionFeature to support directive inheritance (#24570)
- Adds InheritanceDefinitionFeature to ivy - Ensures that lifecycle hooks are inherited from super classes whether they are defined as directives or not - Directives cannot inherit from Components - Components can inherit from Directives or Components - Ensures that Inputs, Outputs, and Host Bindings are inherited - Ensures that super class Features are run PR Close #24570
This commit is contained in:
@ -8,8 +8,8 @@
|
||||
|
||||
import '@angular/core/test/bundling/util/src/reflect_metadata';
|
||||
|
||||
import {CommonModule, NgForOf, NgIf} from '@angular/common';
|
||||
import {Component, Injectable, IterableDiffers, NgModule, defineInjector, ɵNgOnChangesFeature as NgOnChangesFeature, ɵdefineDirective as defineDirective, ɵdirectiveInject as directiveInject, ɵinjectTemplateRef as injectTemplateRef, ɵinjectViewContainerRef as injectViewContainerRef, ɵrenderComponent as renderComponent} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {Component, Injectable, NgModule, ɵrenderComponent as renderComponent} from '@angular/core';
|
||||
|
||||
class Todo {
|
||||
editing: boolean;
|
||||
@ -63,32 +63,32 @@ class TodoStore {
|
||||
<section class="todoapp">
|
||||
<header class="header">
|
||||
<h1>todos</h1>
|
||||
<input class="new-todo" placeholder="What needs to be done?" autofocus=""
|
||||
[value]="newTodoText"
|
||||
<input class="new-todo" placeholder="What needs to be done?" autofocus=""
|
||||
[value]="newTodoText"
|
||||
(keyup)="$event.code == 'Enter' ? addTodo() : newTodoText = $event.target.value">
|
||||
</header>
|
||||
<section *ngIf="todoStore.todos.length > 0" class="main">
|
||||
<input *ngIf="todoStore.todos.length"
|
||||
#toggleall class="toggle-all" type="checkbox"
|
||||
[checked]="todoStore.allCompleted()"
|
||||
<input *ngIf="todoStore.todos.length"
|
||||
#toggleall class="toggle-all" type="checkbox"
|
||||
[checked]="todoStore.allCompleted()"
|
||||
(click)="todoStore.setAllTo(toggleall.checked)">
|
||||
<ul class="todo-list">
|
||||
<li *ngFor="let todo of todoStore.todos"
|
||||
[class.completed]="todo.completed"
|
||||
<li *ngFor="let todo of todoStore.todos"
|
||||
[class.completed]="todo.completed"
|
||||
[class.editing]="todo.editing">
|
||||
<div class="view">
|
||||
<input class="toggle" type="checkbox"
|
||||
(click)="toggleCompletion(todo)"
|
||||
<input class="toggle" type="checkbox"
|
||||
(click)="toggleCompletion(todo)"
|
||||
[checked]="todo.completed">
|
||||
<label (dblclick)="editTodo(todo)">{{todo.title}}</label>
|
||||
<button class="destroy" (click)="remove(todo)"></button>
|
||||
</div>
|
||||
<input *ngIf="todo.editing"
|
||||
<input *ngIf="todo.editing"
|
||||
class="edit" #editedtodo
|
||||
[value]="todo.title"
|
||||
[value]="todo.title"
|
||||
(blur)="stopEditing(todo, editedtodo.value)"
|
||||
(keyup)="todo.title = $event.target.value"
|
||||
(keyup)="$event.code == 'Enter' && updateEditingTodo(todo, editedtodo.value)"
|
||||
(keyup)="todo.title = $event.target.value"
|
||||
(keyup)="$event.code == 'Enter' && updateEditingTodo(todo, editedtodo.value)"
|
||||
(keyup)="$event.code == 'Escape' && cancelEditingTodo(todo)">
|
||||
</li>
|
||||
</ul>
|
||||
@ -98,8 +98,8 @@ class TodoStore {
|
||||
<strong>{{todoStore.getRemaining().length}}</strong>
|
||||
{{todoStore.getRemaining().length == 1 ? 'item' : 'items'}} left
|
||||
</span>
|
||||
<button *ngIf="todoStore.getCompleted().length > 0"
|
||||
class="clear-completed"
|
||||
<button *ngIf="todoStore.getCompleted().length > 0"
|
||||
class="clear-completed"
|
||||
(click)="removeCompleted()">
|
||||
Clear completed
|
||||
</button>
|
||||
|
238
packages/core/test/render3/Inherit_definition_feature_spec.ts
Normal file
238
packages/core/test/render3/Inherit_definition_feature_spec.ts
Normal file
@ -0,0 +1,238 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {DoCheck, EventEmitter, Input, OnChanges, Output, SimpleChange, SimpleChanges} from '../../src/core';
|
||||
import {InheritDefinitionFeature} from '../../src/render3/features/inherit_definition_feature';
|
||||
import {DirectiveDefInternal, NgOnChangesFeature, defineComponent, defineDirective} from '../../src/render3/index';
|
||||
|
||||
describe('InheritDefinitionFeature', () => {
|
||||
it('should inherit lifecycle hooks', () => {
|
||||
class SuperDirective {
|
||||
ngOnInit() {}
|
||||
ngOnDestroy() {}
|
||||
ngAfterContentInit() {}
|
||||
ngAfterContentChecked() {}
|
||||
ngAfterViewInit() {}
|
||||
ngAfterViewChecked() {}
|
||||
ngDoCheck() {}
|
||||
}
|
||||
|
||||
class SubDirective extends SuperDirective {
|
||||
ngAfterViewInit() {}
|
||||
ngAfterViewChecked() {}
|
||||
ngDoCheck() {}
|
||||
|
||||
static ngDirectiveDef = defineDirective({
|
||||
type: SubDirective,
|
||||
selectors: [['', 'subDir', '']],
|
||||
factory: () => new SubDirective(),
|
||||
features: [InheritDefinitionFeature]
|
||||
});
|
||||
}
|
||||
|
||||
const finalDef = SubDirective.ngDirectiveDef as DirectiveDefInternal<any>;
|
||||
|
||||
|
||||
expect(finalDef.onInit).toBe(SuperDirective.prototype.ngOnInit);
|
||||
expect(finalDef.onDestroy).toBe(SuperDirective.prototype.ngOnDestroy);
|
||||
expect(finalDef.afterContentChecked).toBe(SuperDirective.prototype.ngAfterContentChecked);
|
||||
expect(finalDef.afterContentInit).toBe(SuperDirective.prototype.ngAfterContentInit);
|
||||
expect(finalDef.afterViewChecked).toBe(SubDirective.prototype.ngAfterViewChecked);
|
||||
expect(finalDef.afterViewInit).toBe(SubDirective.prototype.ngAfterViewInit);
|
||||
expect(finalDef.doCheck).toBe(SubDirective.prototype.ngDoCheck);
|
||||
});
|
||||
|
||||
it('should inherit inputs', () => {
|
||||
// tslint:disable-next-line:class-as-namespace
|
||||
class SuperDirective {
|
||||
static ngDirectiveDef = defineDirective({
|
||||
inputs: {
|
||||
superFoo: ['foo', 'declaredFoo'],
|
||||
superBar: 'bar',
|
||||
superBaz: 'baz',
|
||||
},
|
||||
type: SuperDirective,
|
||||
selectors: [['', 'superDir', '']],
|
||||
factory: () => new SuperDirective(),
|
||||
});
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:class-as-namespace
|
||||
class SubDirective extends SuperDirective {
|
||||
static ngDirectiveDef = defineDirective({
|
||||
type: SubDirective,
|
||||
inputs: {
|
||||
subBaz: 'baz',
|
||||
subQux: 'qux',
|
||||
},
|
||||
selectors: [['', 'subDir', '']],
|
||||
factory: () => new SubDirective(),
|
||||
features: [InheritDefinitionFeature]
|
||||
});
|
||||
}
|
||||
|
||||
const subDef = SubDirective.ngDirectiveDef as DirectiveDefInternal<any>;
|
||||
|
||||
expect(subDef.inputs).toEqual({
|
||||
foo: 'superFoo',
|
||||
bar: 'superBar',
|
||||
baz: 'subBaz',
|
||||
qux: 'subQux',
|
||||
});
|
||||
expect(subDef.declaredInputs).toEqual({
|
||||
declaredFoo: 'superFoo',
|
||||
bar: 'superBar',
|
||||
baz: 'subBaz',
|
||||
qux: 'subQux',
|
||||
});
|
||||
});
|
||||
|
||||
it('should inherit outputs', () => {
|
||||
// tslint:disable-next-line:class-as-namespace
|
||||
class SuperDirective {
|
||||
static ngDirectiveDef = defineDirective({
|
||||
outputs: {
|
||||
superFoo: 'foo',
|
||||
superBar: 'bar',
|
||||
superBaz: 'baz',
|
||||
},
|
||||
type: SuperDirective,
|
||||
selectors: [['', 'superDir', '']],
|
||||
factory: () => new SuperDirective(),
|
||||
});
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:class-as-namespace
|
||||
class SubDirective extends SuperDirective {
|
||||
static ngDirectiveDef = defineDirective({
|
||||
type: SubDirective,
|
||||
outputs: {
|
||||
subBaz: 'baz',
|
||||
subQux: 'qux',
|
||||
},
|
||||
selectors: [['', 'subDir', '']],
|
||||
factory: () => new SubDirective(),
|
||||
features: [InheritDefinitionFeature]
|
||||
});
|
||||
}
|
||||
|
||||
const subDef = SubDirective.ngDirectiveDef as DirectiveDefInternal<any>;
|
||||
|
||||
expect(subDef.outputs).toEqual({
|
||||
foo: 'superFoo',
|
||||
bar: 'superBar',
|
||||
baz: 'subBaz',
|
||||
qux: 'subQux',
|
||||
});
|
||||
});
|
||||
|
||||
it('should compose hostBindings', () => {
|
||||
const log: Array<[string, number, number]> = [];
|
||||
|
||||
// tslint:disable-next-line:class-as-namespace
|
||||
class SuperDirective {
|
||||
static ngDirectiveDef = defineDirective({
|
||||
type: SuperDirective,
|
||||
selectors: [['', 'superDir', '']],
|
||||
hostBindings: (directiveIndex: number, elementIndex: number) => {
|
||||
log.push(['super', directiveIndex, elementIndex]);
|
||||
},
|
||||
factory: () => new SuperDirective(),
|
||||
});
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:class-as-namespace
|
||||
class SubDirective extends SuperDirective {
|
||||
static ngDirectiveDef = defineDirective({
|
||||
type: SubDirective,
|
||||
selectors: [['', 'subDir', '']],
|
||||
hostBindings: (directiveIndex: number, elementIndex: number) => {
|
||||
log.push(['sub', directiveIndex, elementIndex]);
|
||||
},
|
||||
factory: () => new SubDirective(),
|
||||
features: [InheritDefinitionFeature]
|
||||
});
|
||||
}
|
||||
|
||||
const subDef = SubDirective.ngDirectiveDef as DirectiveDefInternal<any>;
|
||||
|
||||
subDef.hostBindings !(1, 2);
|
||||
|
||||
expect(log).toEqual([['super', 1, 2], ['sub', 1, 2]]);
|
||||
});
|
||||
|
||||
it('should throw if inheriting a component from a directive', () => {
|
||||
// tslint:disable-next-line:class-as-namespace
|
||||
class SuperComponent {
|
||||
static ngComponentDef = defineComponent({
|
||||
type: SuperComponent,
|
||||
template: () => {},
|
||||
selectors: [['', 'superDir', '']],
|
||||
factory: () => new SuperComponent()
|
||||
});
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
// tslint:disable-next-line:class-as-namespace
|
||||
class SubDirective extends SuperComponent{static ngDirectiveDef = defineDirective({
|
||||
type: SubDirective,
|
||||
selectors: [['', 'subDir', '']],
|
||||
factory: () => new SubDirective(),
|
||||
features: [InheritDefinitionFeature]
|
||||
});}
|
||||
}).toThrowError('Directives cannot inherit Components');
|
||||
});
|
||||
|
||||
it('should run inherited features', () => {
|
||||
const log: any[] = [];
|
||||
|
||||
// tslint:disable-next-line:class-as-namespace
|
||||
class SuperDirective {
|
||||
static ngDirectiveDef = defineDirective({
|
||||
type: SuperDirective,
|
||||
selectors: [['', 'superDir', '']],
|
||||
factory: () => new SuperDirective(),
|
||||
features: [
|
||||
(arg: any) => { log.push('super1', arg); },
|
||||
(arg: any) => { log.push('super2', arg); },
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
class SubDirective extends SuperDirective {
|
||||
@Output()
|
||||
baz = new EventEmitter();
|
||||
|
||||
@Output()
|
||||
qux = new EventEmitter();
|
||||
|
||||
static ngDirectiveDef = defineDirective({
|
||||
type: SubDirective,
|
||||
selectors: [['', 'subDir', '']],
|
||||
factory: () => new SubDirective(),
|
||||
features: [InheritDefinitionFeature, (arg: any) => { log.push('sub1', arg); }]
|
||||
});
|
||||
}
|
||||
|
||||
const superDef = SuperDirective.ngDirectiveDef as DirectiveDefInternal<any>;
|
||||
const subDef = SubDirective.ngDirectiveDef as DirectiveDefInternal<any>;
|
||||
|
||||
expect(log).toEqual([
|
||||
'super1',
|
||||
superDef,
|
||||
'super2',
|
||||
superDef,
|
||||
'super1',
|
||||
subDef,
|
||||
'super2',
|
||||
subDef,
|
||||
'sub1',
|
||||
subDef,
|
||||
]);
|
||||
});
|
||||
});
|
@ -38,8 +38,7 @@ NgForOf.ngDirectiveDef = defineDirective({
|
||||
type: NgTemplateOutletDef,
|
||||
selectors: [['', 'ngTemplateOutlet', '']],
|
||||
factory: () => new NgTemplateOutletDef(injectViewContainerRef()),
|
||||
features: [NgOnChangesFeature(
|
||||
{ngTemplateOutlet: 'ngTemplateOutlet', ngTemplateOutletContext: 'ngTemplateOutletContext'})],
|
||||
features: [NgOnChangesFeature],
|
||||
inputs:
|
||||
{ngTemplateOutlet: 'ngTemplateOutlet', ngTemplateOutletContext: 'ngTemplateOutletContext'}
|
||||
});
|
||||
|
@ -47,8 +47,8 @@ describe('lifecycle hooks', () => {
|
||||
selectors: [['lifecycle-comp']],
|
||||
factory: function LifecycleComp_Factory() { return new LifecycleComp(); },
|
||||
template: function LifecycleComp_Template(rf: $RenderFlags$, ctx: $LifecycleComp$) {},
|
||||
inputs: {nameMin: 'name'},
|
||||
features: [$r3$.ɵNgOnChangesFeature({nameMin: 'nameMin'})]
|
||||
inputs: {nameMin: ['name', 'nameMin']},
|
||||
features: [$r3$.ɵNgOnChangesFeature]
|
||||
});
|
||||
// /NORMATIVE
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ describe('template variables', () => {
|
||||
return new ForOfDirective($r3$.ɵinjectViewContainerRef(), $r3$.ɵinjectTemplateRef());
|
||||
},
|
||||
// TODO(chuckj): Enable when ngForOf enabling lands.
|
||||
// features: [NgOnChangesFeature(NgForOf)],
|
||||
// features: [NgOnChangesFeature],
|
||||
inputs: {forOf: 'forOf'}
|
||||
});
|
||||
// /NORMATIVE
|
||||
|
@ -1,120 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {DoCheck, OnChanges, SimpleChange, SimpleChanges} from '../../src/core';
|
||||
import {DirectiveDefInternal, NgOnChangesFeature, defineDirective} from '../../src/render3/index';
|
||||
|
||||
describe('define', () => {
|
||||
describe('component', () => {
|
||||
describe('NgOnChangesFeature', () => {
|
||||
it('should patch class', () => {
|
||||
class MyDirective implements OnChanges, DoCheck {
|
||||
public log: Array<string|SimpleChange> = [];
|
||||
public valA: string = 'initValue';
|
||||
public set valB(value: string) { this.log.push(value); }
|
||||
|
||||
public get valB() { return 'works'; }
|
||||
|
||||
ngDoCheck(): void { this.log.push('ngDoCheck'); }
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
this.log.push('ngOnChanges');
|
||||
this.log.push('valA', changes['valA']);
|
||||
this.log.push('valB', changes['valB']);
|
||||
}
|
||||
|
||||
static ngDirectiveDef = defineDirective({
|
||||
type: MyDirective,
|
||||
selectors: [['', 'myDir', '']],
|
||||
factory: () => new MyDirective(),
|
||||
features: [NgOnChangesFeature()],
|
||||
inputs: {valA: 'valA', valB: 'valB'}
|
||||
});
|
||||
}
|
||||
|
||||
const myDir = (MyDirective.ngDirectiveDef as DirectiveDefInternal<MyDirective>)
|
||||
.factory() as MyDirective;
|
||||
myDir.valA = 'first';
|
||||
expect(myDir.valA).toEqual('first');
|
||||
myDir.valB = 'second';
|
||||
expect(myDir.log).toEqual(['second']);
|
||||
expect(myDir.valB).toEqual('works');
|
||||
myDir.log.length = 0;
|
||||
(MyDirective.ngDirectiveDef as DirectiveDefInternal<MyDirective>).doCheck !.call(myDir);
|
||||
const changeA = new SimpleChange(undefined, 'first', true);
|
||||
const changeB = new SimpleChange(undefined, 'second', true);
|
||||
expect(myDir.log).toEqual(['ngOnChanges', 'valA', changeA, 'valB', changeB, 'ngDoCheck']);
|
||||
});
|
||||
|
||||
it('correctly computes firstChange', () => {
|
||||
class MyDirective implements OnChanges {
|
||||
public log: Array<string|SimpleChange> = [];
|
||||
public valA: string = 'initValue';
|
||||
// TODO(issue/24571): remove '!'.
|
||||
public valB !: string;
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
this.log.push('valA', changes['valA']);
|
||||
this.log.push('valB', changes['valB']);
|
||||
}
|
||||
|
||||
static ngDirectiveDef = defineDirective({
|
||||
type: MyDirective,
|
||||
selectors: [['', 'myDir', '']],
|
||||
factory: () => new MyDirective(),
|
||||
features: [NgOnChangesFeature()],
|
||||
inputs: {valA: 'valA', valB: 'valB'}
|
||||
});
|
||||
}
|
||||
|
||||
const myDir = (MyDirective.ngDirectiveDef as DirectiveDefInternal<MyDirective>)
|
||||
.factory() as MyDirective;
|
||||
myDir.valA = 'first';
|
||||
myDir.valB = 'second';
|
||||
(MyDirective.ngDirectiveDef as DirectiveDefInternal<MyDirective>).doCheck !.call(myDir);
|
||||
const changeA1 = new SimpleChange(undefined, 'first', true);
|
||||
const changeB1 = new SimpleChange(undefined, 'second', true);
|
||||
expect(myDir.log).toEqual(['valA', changeA1, 'valB', changeB1]);
|
||||
|
||||
myDir.log.length = 0;
|
||||
myDir.valA = 'third';
|
||||
(MyDirective.ngDirectiveDef as DirectiveDefInternal<MyDirective>).doCheck !.call(myDir);
|
||||
const changeA2 = new SimpleChange('first', 'third', false);
|
||||
expect(myDir.log).toEqual(['valA', changeA2, 'valB', undefined]);
|
||||
});
|
||||
|
||||
it('should not create a getter when only a setter is originally defined', () => {
|
||||
class MyDirective implements OnChanges {
|
||||
public log: Array<string|SimpleChange> = [];
|
||||
|
||||
public set onlySetter(value: string) { this.log.push(value); }
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
this.log.push('ngOnChanges');
|
||||
this.log.push('onlySetter', changes['onlySetter']);
|
||||
}
|
||||
|
||||
static ngDirectiveDef = defineDirective({
|
||||
type: MyDirective,
|
||||
selectors: [['', 'myDir', '']],
|
||||
factory: () => new MyDirective(),
|
||||
features: [NgOnChangesFeature()],
|
||||
inputs: {onlySetter: 'onlySetter'}
|
||||
});
|
||||
}
|
||||
|
||||
const myDir = (MyDirective.ngDirectiveDef as DirectiveDefInternal<MyDirective>)
|
||||
.factory() as MyDirective;
|
||||
myDir.onlySetter = 'someValue';
|
||||
expect(myDir.onlySetter).toBeUndefined();
|
||||
(MyDirective.ngDirectiveDef as DirectiveDefInternal<MyDirective>).doCheck !.call(myDir);
|
||||
const changeSetter = new SimpleChange(undefined, 'someValue', true);
|
||||
expect(myDir.log).toEqual(['someValue', 'ngOnChanges', 'onlySetter', changeSetter]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -995,7 +995,7 @@ describe('di', () => {
|
||||
selectors: [['', 'myIf', '']],
|
||||
factory: () => new IfDirective(injectTemplateRef(), injectViewContainerRef()),
|
||||
inputs: {myIf: 'myIf'},
|
||||
features: [PublicFeature, NgOnChangesFeature()]
|
||||
features: [PublicFeature, NgOnChangesFeature]
|
||||
});
|
||||
}
|
||||
|
||||
|
39
packages/core/test/render3/jit/directive_spec.ts
Normal file
39
packages/core/test/render3/jit/directive_spec.ts
Normal file
@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {extendsDirectlyFromObject} from '../../../src/render3/jit/directive';
|
||||
|
||||
describe('extendsDirectlyFromObject', () => {
|
||||
it('should correctly behave with instanceof', () => {
|
||||
expect(new Child() instanceof Object).toBeTruthy();
|
||||
expect(new Child() instanceof Parent).toBeTruthy();
|
||||
expect(new Parent() instanceof Child).toBeFalsy();
|
||||
|
||||
expect(new Child5() instanceof Object).toBeTruthy();
|
||||
expect(new Child5() instanceof Parent5).toBeTruthy();
|
||||
expect(new Parent5() instanceof Child5).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should detect direct inheritance form Object', () => {
|
||||
expect(extendsDirectlyFromObject(Parent)).toBeTruthy();
|
||||
expect(extendsDirectlyFromObject(Child)).toBeFalsy();
|
||||
|
||||
expect(extendsDirectlyFromObject(Parent5)).toBeTruthy();
|
||||
expect(extendsDirectlyFromObject(Child5)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
// Inheritance Example using Classes
|
||||
class Parent {}
|
||||
class Child extends Parent {}
|
||||
|
||||
// Inheritance Example using Function
|
||||
const Parent5 = function Parent5() {} as any as{new (): {}};
|
||||
const Child5 = function Child5() {} as any as{new (): {}};
|
||||
Child5.prototype = new Parent5;
|
||||
Child5.prototype.constructor = Child5;
|
@ -1932,8 +1932,8 @@ describe('lifecycles', () => {
|
||||
type: Component,
|
||||
selectors: [[name]],
|
||||
factory: () => new Component(),
|
||||
features: [NgOnChangesFeature({b: 'val2'})],
|
||||
inputs: {a: 'val1', b: 'publicName'}, template,
|
||||
features: [NgOnChangesFeature],
|
||||
inputs: {a: 'val1', b: ['publicName', 'val2']}, template,
|
||||
directives: directives
|
||||
});
|
||||
};
|
||||
@ -1953,8 +1953,8 @@ describe('lifecycles', () => {
|
||||
type: Directive,
|
||||
selectors: [['', 'dir', '']],
|
||||
factory: () => new Directive(),
|
||||
features: [NgOnChangesFeature({b: 'val2'})],
|
||||
inputs: {a: 'val1', b: 'publicName'}
|
||||
features: [NgOnChangesFeature],
|
||||
inputs: {a: 'val1', b: ['publicName', 'val2']}
|
||||
});
|
||||
}
|
||||
|
||||
@ -1976,11 +1976,10 @@ describe('lifecycles', () => {
|
||||
renderToHtml(Template, {val1: '1', val2: 'a'}, defs);
|
||||
expect(events).toEqual(['comp=comp val1=1 val2=a - changed=[val1,val2]']);
|
||||
|
||||
events.length = 0;
|
||||
|
||||
renderToHtml(Template, {val1: '2', val2: 'b'}, defs);
|
||||
expect(events).toEqual([
|
||||
'comp=comp val1=1 val2=a - changed=[val1,val2]',
|
||||
'comp=comp val1=2 val2=b - changed=[val1,val2]'
|
||||
]);
|
||||
expect(events).toEqual(['comp=comp val1=2 val2=b - changed=[val1,val2]']);
|
||||
});
|
||||
|
||||
it('should call parent onChanges before child onChanges', () => {
|
||||
@ -2336,7 +2335,7 @@ describe('lifecycles', () => {
|
||||
selectors: [[name]],
|
||||
factory: () => new Component(),
|
||||
inputs: {val: 'val'}, template,
|
||||
features: [NgOnChangesFeature()],
|
||||
features: [NgOnChangesFeature],
|
||||
directives: directives
|
||||
});
|
||||
};
|
||||
|
325
packages/core/test/render3/ng_on_changes_feature_spec.ts
Normal file
325
packages/core/test/render3/ng_on_changes_feature_spec.ts
Normal file
@ -0,0 +1,325 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {DoCheck, EventEmitter, Input, OnChanges, Output, SimpleChange, SimpleChanges} from '../../src/core';
|
||||
import {InheritDefinitionFeature} from '../../src/render3/features/inherit_definition_feature';
|
||||
import {DirectiveDefInternal, NgOnChangesFeature, defineComponent, defineDirective} from '../../src/render3/index';
|
||||
|
||||
describe('NgOnChangesFeature', () => {
|
||||
it('should patch class', () => {
|
||||
class MyDirective implements OnChanges, DoCheck {
|
||||
public log: Array<string|SimpleChange> = [];
|
||||
public valA: string = 'initValue';
|
||||
public set valB(value: string) { this.log.push(value); }
|
||||
|
||||
public get valB() { return 'works'; }
|
||||
|
||||
ngDoCheck(): void { this.log.push('ngDoCheck'); }
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
this.log.push('ngOnChanges');
|
||||
this.log.push('valA', changes['valA']);
|
||||
this.log.push('valB', changes['valB']);
|
||||
}
|
||||
|
||||
static ngDirectiveDef = defineDirective({
|
||||
type: MyDirective,
|
||||
selectors: [['', 'myDir', '']],
|
||||
factory: () => new MyDirective(),
|
||||
features: [NgOnChangesFeature],
|
||||
inputs: {valA: 'valA', valB: 'valB'}
|
||||
});
|
||||
}
|
||||
|
||||
const myDir =
|
||||
(MyDirective.ngDirectiveDef as DirectiveDefInternal<MyDirective>).factory() as MyDirective;
|
||||
myDir.valA = 'first';
|
||||
expect(myDir.valA).toEqual('first');
|
||||
myDir.valB = 'second';
|
||||
expect(myDir.log).toEqual(['second']);
|
||||
expect(myDir.valB).toEqual('works');
|
||||
myDir.log.length = 0;
|
||||
(MyDirective.ngDirectiveDef as DirectiveDefInternal<MyDirective>).doCheck !.call(myDir);
|
||||
const changeA = new SimpleChange(undefined, 'first', true);
|
||||
const changeB = new SimpleChange(undefined, 'second', true);
|
||||
expect(myDir.log).toEqual(['ngOnChanges', 'valA', changeA, 'valB', changeB, 'ngDoCheck']);
|
||||
});
|
||||
|
||||
it('should inherit the behavior from super class', () => {
|
||||
const log: any[] = [];
|
||||
|
||||
class SuperDirective implements OnChanges, DoCheck {
|
||||
valA = 'initValue';
|
||||
|
||||
set valB(value: string) { log.push(value); }
|
||||
|
||||
get valB() { return 'works'; }
|
||||
|
||||
ngDoCheck(): void { log.push('ngDoCheck'); }
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
log.push('ngOnChanges');
|
||||
log.push('valA', changes['valA']);
|
||||
log.push('valB', changes['valB']);
|
||||
log.push('valC', changes['valC']);
|
||||
}
|
||||
|
||||
static ngDirectiveDef = defineDirective({
|
||||
type: SuperDirective,
|
||||
selectors: [['', 'superDir', '']],
|
||||
factory: () => new SuperDirective(),
|
||||
features: [NgOnChangesFeature],
|
||||
inputs: {valA: 'valA', valB: 'valB'},
|
||||
});
|
||||
}
|
||||
|
||||
class SubDirective extends SuperDirective {
|
||||
valC = 'initValue';
|
||||
|
||||
static ngDirectiveDef = defineDirective({
|
||||
type: SubDirective,
|
||||
selectors: [['', 'subDir', '']],
|
||||
factory: () => new SubDirective(),
|
||||
features: [InheritDefinitionFeature],
|
||||
inputs: {valC: 'valC'},
|
||||
});
|
||||
}
|
||||
|
||||
const myDir = (SubDirective.ngDirectiveDef as DirectiveDefInternal<SubDirective>)
|
||||
.factory() as SubDirective;
|
||||
myDir.valA = 'first';
|
||||
expect(myDir.valA).toEqual('first');
|
||||
|
||||
myDir.valB = 'second';
|
||||
expect(myDir.valB).toEqual('works');
|
||||
|
||||
myDir.valC = 'third';
|
||||
expect(myDir.valC).toEqual('third');
|
||||
|
||||
log.length = 0;
|
||||
(SubDirective.ngDirectiveDef as DirectiveDefInternal<SubDirective>).doCheck !.call(myDir);
|
||||
const changeA = new SimpleChange(undefined, 'first', true);
|
||||
const changeB = new SimpleChange(undefined, 'second', true);
|
||||
const changeC = new SimpleChange(undefined, 'third', true);
|
||||
|
||||
expect(log).toEqual(
|
||||
['ngOnChanges', 'valA', changeA, 'valB', changeB, 'valC', changeC, 'ngDoCheck']);
|
||||
});
|
||||
|
||||
it('should not run the parent doCheck if it is not called explicitly on super class', () => {
|
||||
const log: any[] = [];
|
||||
|
||||
class SuperDirective implements OnChanges, DoCheck {
|
||||
valA = 'initValue';
|
||||
|
||||
ngDoCheck(): void { log.push('ERROR: Child overrides it without super call'); }
|
||||
ngOnChanges(changes: SimpleChanges): void { log.push(changes.valA, changes.valB); }
|
||||
|
||||
static ngDirectiveDef = defineDirective({
|
||||
type: SuperDirective,
|
||||
selectors: [['', 'superDir', '']],
|
||||
factory: () => new SuperDirective(),
|
||||
features: [NgOnChangesFeature],
|
||||
inputs: {valA: 'valA'},
|
||||
});
|
||||
}
|
||||
|
||||
class SubDirective extends SuperDirective implements DoCheck {
|
||||
valB = 'initValue';
|
||||
|
||||
ngDoCheck(): void { log.push('sub ngDoCheck'); }
|
||||
|
||||
static ngDirectiveDef = defineDirective({
|
||||
type: SubDirective,
|
||||
selectors: [['', 'subDir', '']],
|
||||
factory: () => new SubDirective(),
|
||||
features: [InheritDefinitionFeature],
|
||||
inputs: {valB: 'valB'},
|
||||
});
|
||||
}
|
||||
|
||||
const myDir = (SubDirective.ngDirectiveDef as DirectiveDefInternal<SubDirective>)
|
||||
.factory() as SubDirective;
|
||||
myDir.valA = 'first';
|
||||
myDir.valB = 'second';
|
||||
|
||||
(SubDirective.ngDirectiveDef as DirectiveDefInternal<SubDirective>).doCheck !.call(myDir);
|
||||
const changeA = new SimpleChange(undefined, 'first', true);
|
||||
const changeB = new SimpleChange(undefined, 'second', true);
|
||||
expect(log).toEqual([changeA, changeB, 'sub ngDoCheck']);
|
||||
});
|
||||
|
||||
it('should run the parent doCheck if it is inherited from super class', () => {
|
||||
const log: any[] = [];
|
||||
|
||||
class SuperDirective implements OnChanges, DoCheck {
|
||||
valA = 'initValue';
|
||||
|
||||
ngDoCheck(): void { log.push('super ngDoCheck'); }
|
||||
ngOnChanges(changes: SimpleChanges): void { log.push(changes.valA, changes.valB); }
|
||||
|
||||
static ngDirectiveDef = defineDirective({
|
||||
type: SuperDirective,
|
||||
selectors: [['', 'superDir', '']],
|
||||
factory: () => new SuperDirective(),
|
||||
features: [NgOnChangesFeature],
|
||||
inputs: {valA: 'valA'},
|
||||
});
|
||||
}
|
||||
|
||||
class SubDirective extends SuperDirective implements DoCheck {
|
||||
valB = 'initValue';
|
||||
|
||||
static ngDirectiveDef = defineDirective({
|
||||
type: SubDirective,
|
||||
selectors: [['', 'subDir', '']],
|
||||
factory: () => new SubDirective(),
|
||||
features: [InheritDefinitionFeature],
|
||||
inputs: {valB: 'valB'},
|
||||
});
|
||||
}
|
||||
|
||||
const myDir = (SubDirective.ngDirectiveDef as DirectiveDefInternal<SubDirective>)
|
||||
.factory() as SubDirective;
|
||||
myDir.valA = 'first';
|
||||
myDir.valB = 'second';
|
||||
|
||||
(SubDirective.ngDirectiveDef as DirectiveDefInternal<SubDirective>).doCheck !.call(myDir);
|
||||
const changeA = new SimpleChange(undefined, 'first', true);
|
||||
const changeB = new SimpleChange(undefined, 'second', true);
|
||||
expect(log).toEqual([changeA, changeB, 'super ngDoCheck']);
|
||||
});
|
||||
|
||||
it('should apply the feature to inherited properties if on sub class', () => {
|
||||
const log: any[] = [];
|
||||
|
||||
class SuperDirective {
|
||||
valC = 'initValue';
|
||||
|
||||
static ngDirectiveDef = defineDirective({
|
||||
type: SuperDirective,
|
||||
selectors: [['', 'subDir', '']],
|
||||
factory: () => new SuperDirective(),
|
||||
features: [],
|
||||
inputs: {valC: 'valC'},
|
||||
});
|
||||
}
|
||||
|
||||
class SubDirective extends SuperDirective implements OnChanges, DoCheck {
|
||||
valA = 'initValue';
|
||||
|
||||
set valB(value: string) { log.push(value); }
|
||||
|
||||
get valB() { return 'works'; }
|
||||
|
||||
ngDoCheck(): void { log.push('ngDoCheck'); }
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
log.push('ngOnChanges');
|
||||
log.push('valA', changes['valA']);
|
||||
log.push('valB', changes['valB']);
|
||||
log.push('valC', changes['valC']);
|
||||
}
|
||||
|
||||
static ngDirectiveDef = defineDirective({
|
||||
type: SubDirective,
|
||||
selectors: [['', 'superDir', '']],
|
||||
factory: () => new SubDirective(),
|
||||
// Inheritance must always be before OnChanges feature.
|
||||
features: [
|
||||
InheritDefinitionFeature,
|
||||
NgOnChangesFeature,
|
||||
],
|
||||
inputs: {valA: 'valA', valB: 'valB'}
|
||||
});
|
||||
}
|
||||
|
||||
const myDir = (SubDirective.ngDirectiveDef as DirectiveDefInternal<SubDirective>)
|
||||
.factory() as SubDirective;
|
||||
myDir.valA = 'first';
|
||||
expect(myDir.valA).toEqual('first');
|
||||
|
||||
myDir.valB = 'second';
|
||||
expect(log).toEqual(['second']);
|
||||
expect(myDir.valB).toEqual('works');
|
||||
|
||||
myDir.valC = 'third';
|
||||
expect(myDir.valC).toEqual('third');
|
||||
|
||||
log.length = 0;
|
||||
(SubDirective.ngDirectiveDef as DirectiveDefInternal<SubDirective>).doCheck !.call(myDir);
|
||||
const changeA = new SimpleChange(undefined, 'first', true);
|
||||
const changeB = new SimpleChange(undefined, 'second', true);
|
||||
const changeC = new SimpleChange(undefined, 'third', true);
|
||||
expect(log).toEqual(
|
||||
['ngOnChanges', 'valA', changeA, 'valB', changeB, 'valC', changeC, 'ngDoCheck']);
|
||||
});
|
||||
|
||||
it('correctly computes firstChange', () => {
|
||||
class MyDirective implements OnChanges {
|
||||
public log: Array<string|SimpleChange> = [];
|
||||
public valA: string = 'initValue';
|
||||
// TODO(issue/24571): remove '!'.
|
||||
public valB !: string;
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
this.log.push('valA', changes['valA']);
|
||||
this.log.push('valB', changes['valB']);
|
||||
}
|
||||
|
||||
static ngDirectiveDef = defineDirective({
|
||||
type: MyDirective,
|
||||
selectors: [['', 'myDir', '']],
|
||||
factory: () => new MyDirective(),
|
||||
features: [NgOnChangesFeature],
|
||||
inputs: {valA: 'valA', valB: 'valB'}
|
||||
});
|
||||
}
|
||||
|
||||
const myDir =
|
||||
(MyDirective.ngDirectiveDef as DirectiveDefInternal<MyDirective>).factory() as MyDirective;
|
||||
myDir.valA = 'first';
|
||||
myDir.valB = 'second';
|
||||
(MyDirective.ngDirectiveDef as DirectiveDefInternal<MyDirective>).doCheck !.call(myDir);
|
||||
const changeA1 = new SimpleChange(undefined, 'first', true);
|
||||
const changeB1 = new SimpleChange(undefined, 'second', true);
|
||||
expect(myDir.log).toEqual(['valA', changeA1, 'valB', changeB1]);
|
||||
|
||||
myDir.log.length = 0;
|
||||
myDir.valA = 'third';
|
||||
(MyDirective.ngDirectiveDef as DirectiveDefInternal<MyDirective>).doCheck !.call(myDir);
|
||||
const changeA2 = new SimpleChange('first', 'third', false);
|
||||
expect(myDir.log).toEqual(['valA', changeA2, 'valB', undefined]);
|
||||
});
|
||||
|
||||
it('should not create a getter when only a setter is originally defined', () => {
|
||||
class MyDirective implements OnChanges {
|
||||
public log: Array<string|SimpleChange> = [];
|
||||
|
||||
public set onlySetter(value: string) { this.log.push(value); }
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
this.log.push('ngOnChanges');
|
||||
this.log.push('onlySetter', changes['onlySetter']);
|
||||
}
|
||||
|
||||
static ngDirectiveDef = defineDirective({
|
||||
type: MyDirective,
|
||||
selectors: [['', 'myDir', '']],
|
||||
factory: () => new MyDirective(),
|
||||
features: [NgOnChangesFeature],
|
||||
inputs: {onlySetter: 'onlySetter'}
|
||||
});
|
||||
}
|
||||
|
||||
const myDir =
|
||||
(MyDirective.ngDirectiveDef as DirectiveDefInternal<MyDirective>).factory() as MyDirective;
|
||||
myDir.onlySetter = 'someValue';
|
||||
expect(myDir.onlySetter).toBeUndefined();
|
||||
(MyDirective.ngDirectiveDef as DirectiveDefInternal<MyDirective>).doCheck !.call(myDir);
|
||||
const changeSetter = new SimpleChange(undefined, 'someValue', true);
|
||||
expect(myDir.log).toEqual(['someValue', 'ngOnChanges', 'onlySetter', changeSetter]);
|
||||
});
|
||||
});
|
@ -11,7 +11,7 @@ import {stringifyElement} from '@angular/platform-browser/testing/src/browser_ut
|
||||
import {Injector} from '../../src/di/injector';
|
||||
import {CreateComponentOptions} from '../../src/render3/component';
|
||||
import {extractDirectiveDef, extractPipeDef} from '../../src/render3/definition';
|
||||
import {ComponentDefInternal, ComponentTemplate, ComponentType, DirectiveDefInternal, DirectiveType, PublicFeature, RenderFlags, defineComponent, defineDirective, renderComponent as _renderComponent, tick} from '../../src/render3/index';
|
||||
import {ComponentTemplate, ComponentType, DirectiveDefInternal, DirectiveType, PublicFeature, RenderFlags, defineComponent, defineDirective, renderComponent as _renderComponent, tick} from '../../src/render3/index';
|
||||
import {NG_HOST_SYMBOL, renderTemplate} from '../../src/render3/instructions';
|
||||
import {DirectiveDefList, DirectiveDefListOrFactory, DirectiveTypesOrFactory, PipeDef, PipeDefList, PipeDefListOrFactory, PipeTypesOrFactory} from '../../src/render3/interfaces/definition';
|
||||
import {LElementNode} from '../../src/render3/interfaces/node';
|
||||
|
@ -966,7 +966,7 @@ describe('ViewContainerRef', () => {
|
||||
textBinding(0, interpolation1('', cmp.name, ''));
|
||||
}
|
||||
},
|
||||
features: [NgOnChangesFeature()],
|
||||
features: [NgOnChangesFeature],
|
||||
inputs: {name: 'name'}
|
||||
});
|
||||
}
|
||||
|
Reference in New Issue
Block a user