fix(ivy): fix property names of ngOnChanges (#27714)

- #reslove FW-812
- #reslove FW-844

PR Close #27714
This commit is contained in:
Misko Hevery 2018-12-17 13:17:42 -08:00 committed by Miško Hevery
parent 4774a1abff
commit 1c93afe956
8 changed files with 237 additions and 217 deletions

View File

@ -110,7 +110,7 @@ function baseDirectiveFields(
meta, elVarExp, contextVarExp, styleBuilder, bindingParser, constantPool, hostVarsCount)); meta, elVarExp, contextVarExp, styleBuilder, bindingParser, constantPool, hostVarsCount));
// e.g 'inputs: {a: 'a'}` // e.g 'inputs: {a: 'a'}`
definitionMap.set('inputs', conditionallyCreateMapObjectLiteral(meta.inputs)); definitionMap.set('inputs', conditionallyCreateMapObjectLiteral(meta.inputs, true));
// e.g 'outputs: {a: 'a'}` // e.g 'outputs: {a: 'a'}`
definitionMap.set('outputs', conditionallyCreateMapObjectLiteral(meta.outputs)); definitionMap.set('outputs', conditionallyCreateMapObjectLiteral(meta.outputs));

View File

@ -67,20 +67,36 @@ export function asLiteral(value: any): o.Expression {
return o.literal(value, o.INFERRED_TYPE); return o.literal(value, o.INFERRED_TYPE);
} }
export function conditionallyCreateMapObjectLiteral(keys: {[key: string]: string | string[]}): export function conditionallyCreateMapObjectLiteral(
o.Expression|null { keys: {[key: string]: string | string[]}, keepDeclared?: boolean): o.Expression|null {
if (Object.getOwnPropertyNames(keys).length > 0) { if (Object.getOwnPropertyNames(keys).length > 0) {
return mapToExpression(keys); return mapToExpression(keys, keepDeclared);
} }
return null; return null;
} }
function mapToExpression(map: {[key: string]: any}): o.Expression { function mapToExpression(
map: {[key: string]: string | string[]}, keepDeclared?: boolean): o.Expression {
return o.literalMap(Object.getOwnPropertyNames(map).map(key => { return o.literalMap(Object.getOwnPropertyNames(map).map(key => {
// canonical syntax: `dirProp: elProp` // canonical syntax: `dirProp: publicProp`
// if there is no `:`, use dirProp = elProp // if there is no `:`, use dirProp = elProp
const parts = splitAtColon(key, [key, map[key]]); const value = map[key];
return {key: parts[0], quoted: false, value: asLiteral(parts[1])}; let declaredName: string;
let publicName: string;
let minifiedName: string;
if (Array.isArray(value)) {
[publicName, declaredName] = value;
} else {
[declaredName, publicName] = splitAtColon(key, [key, value]);
}
minifiedName = declaredName;
return {
key: minifiedName,
quoted: false,
value: (keepDeclared && publicName !== declaredName) ?
o.literalArr([asLiteral(publicName), asLiteral(declaredName)]) :
asLiteral(publicName)
};
})); }));
} }

View File

@ -345,8 +345,8 @@ export function defineNgModule<T>(def: {type: T} & Partial<NgModuleDef<T>>): nev
* @Input() * @Input()
* propName1: string; * propName1: string;
* *
* @Input('publicName') * @Input('publicName2')
* propName2: number; * declaredPropName2: number;
* } * }
* ``` * ```
* *
@ -354,26 +354,35 @@ export function defineNgModule<T>(def: {type: T} & Partial<NgModuleDef<T>>): nev
* *
* ``` * ```
* { * {
* a0: 'propName1', * propName1: 'propName1',
* b1: ['publicName', 'propName2'], * declaredPropName2: ['publicName2', 'declaredPropName2'],
* } * }
* ``` * ```
* *
* becomes * which is than translated by the minifier as:
* *
* ``` * ```
* { * {
* 'propName1': 'a0', * minifiedPropName1: 'propName1',
* 'publicName': 'b1' * minifiedPropName2: ['publicName2', 'declaredPropName2'],
* } * }
* ``` * ```
* *
* Optionally the function can take `secondary` which will result in: * becomes: (public name => minifiedName)
* *
* ``` * ```
* { * {
* 'propName1': 'a0', * 'propName1': 'minifiedPropName1',
* 'propName2': 'b1' * 'publicName2': 'minifiedPropName2',
* }
* ```
*
* Optionally the function can take `secondary` which will result in: (public name => declared name)
*
* ```
* {
* 'propName1': 'propName1',
* 'publicName2': 'declaredPropName2',
* } * }
* ``` * ```
* *
@ -384,7 +393,7 @@ function invertObject(obj: any, secondary?: any): any {
const newLookup: any = {}; const newLookup: any = {};
for (const minifiedKey in obj) { for (const minifiedKey in obj) {
if (obj.hasOwnProperty(minifiedKey)) { if (obj.hasOwnProperty(minifiedKey)) {
let publicName = obj[minifiedKey]; let publicName: string = obj[minifiedKey];
let declaredName = publicName; let declaredName = publicName;
if (Array.isArray(publicName)) { if (Array.isArray(publicName)) {
declaredName = publicName[1]; declaredName = publicName[1];
@ -392,7 +401,7 @@ function invertObject(obj: any, secondary?: any): any {
} }
newLookup[publicName] = minifiedKey; newLookup[publicName] = minifiedKey;
if (secondary) { if (secondary) {
(secondary[declaredName] = minifiedKey); (secondary[publicName] = declaredName);
} }
} }
} }

View File

@ -39,11 +39,13 @@ type OnChangesExpando = OnChanges & {
* ``` * ```
*/ */
export function NgOnChangesFeature<T>(definition: DirectiveDef<T>): void { export function NgOnChangesFeature<T>(definition: DirectiveDef<T>): void {
const declaredToMinifiedInputs = definition.declaredInputs; const publicToDeclaredInputs = definition.declaredInputs;
const publicToMinifiedInputs = definition.inputs;
const proto = definition.type.prototype; const proto = definition.type.prototype;
for (const declaredName in declaredToMinifiedInputs) { for (const publicName in publicToDeclaredInputs) {
if (declaredToMinifiedInputs.hasOwnProperty(declaredName)) { if (publicToDeclaredInputs.hasOwnProperty(publicName)) {
const minifiedKey = declaredToMinifiedInputs[declaredName]; const minifiedKey = publicToMinifiedInputs[publicName];
const declaredKey = publicToDeclaredInputs[publicName];
const privateMinKey = PRIVATE_PREFIX + minifiedKey; const privateMinKey = PRIVATE_PREFIX + minifiedKey;
// Walk the prototype chain to see if we find a property descriptor // Walk the prototype chain to see if we find a property descriptor
@ -72,12 +74,12 @@ export function NgOnChangesFeature<T>(definition: DirectiveDef<T>): void {
} }
const isFirstChange = !this.hasOwnProperty(privateMinKey); const isFirstChange = !this.hasOwnProperty(privateMinKey);
const currentChange = simpleChanges[declaredName]; const currentChange = simpleChanges[declaredKey];
if (currentChange) { if (currentChange) {
currentChange.currentValue = value; currentChange.currentValue = value;
} else { } else {
simpleChanges[declaredName] = simpleChanges[declaredKey] =
new SimpleChange(this[privateMinKey], value, isFirstChange); new SimpleChange(this[privateMinKey], value, isFirstChange);
} }

View File

@ -84,10 +84,10 @@ describe('InheritDefinitionFeature', () => {
qux: 'subQux', qux: 'subQux',
}); });
expect(subDef.declaredInputs).toEqual({ expect(subDef.declaredInputs).toEqual({
declaredFoo: 'superFoo', foo: 'declaredFoo',
bar: 'superBar', bar: 'bar',
baz: 'subBaz', baz: 'baz',
qux: 'subQux', qux: 'qux',
}); });
}); });
@ -228,7 +228,7 @@ describe('InheritDefinitionFeature', () => {
expect(subDef.declaredInputs).toEqual({ expect(subDef.declaredInputs).toEqual({
input1: 'input1', input1: 'input1',
input2: 'input2', input2: 'input2',
input3: 'input3', alias3: 'input3',
input4: 'input4', input4: 'input4',
input5: 'input5', input5: 'input5',
}); });

View File

@ -264,7 +264,7 @@ ivyEnabled && describe('render3 jit', () => {
const InputCompAny = InputComp as any; const InputCompAny = InputComp as any;
expect(InputCompAny.ngComponentDef.inputs).toEqual({publicName: 'privateName'}); expect(InputCompAny.ngComponentDef.inputs).toEqual({publicName: 'privateName'});
expect(InputCompAny.ngComponentDef.declaredInputs).toEqual({privateName: 'privateName'}); expect(InputCompAny.ngComponentDef.declaredInputs).toEqual({publicName: 'privateName'});
}); });
it('should add @Input properties to a directive', () => { it('should add @Input properties to a directive', () => {
@ -277,7 +277,7 @@ ivyEnabled && describe('render3 jit', () => {
const InputDirAny = InputDir as any; const InputDirAny = InputDir as any;
expect(InputDirAny.ngDirectiveDef.inputs).toEqual({publicName: 'privateName'}); expect(InputDirAny.ngDirectiveDef.inputs).toEqual({publicName: 'privateName'});
expect(InputDirAny.ngDirectiveDef.declaredInputs).toEqual({privateName: 'privateName'}); expect(InputDirAny.ngDirectiveDef.declaredInputs).toEqual({publicName: 'privateName'});
}); });
it('should add ngBaseDef to types with @Input properties', () => { it('should add ngBaseDef to types with @Input properties', () => {

View File

@ -1915,194 +1915,189 @@ withEachNg1Version(() => {
}); });
})); }));
fixmeIvy( it('should call `$onChanges()` on binding destination', fakeAsync(() => {
'FW-844: Directive input bindings cannot be assigned after the `@Directive` decorator has been compiled') const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
.it('should call `$onChanges()` on binding destination', fakeAsync(() => { const $onChangesControllerSpyA = jasmine.createSpy('$onChangesControllerA');
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); const $onChangesControllerSpyB = jasmine.createSpy('$onChangesControllerB');
const $onChangesControllerSpyA = jasmine.createSpy('$onChangesControllerA'); const $onChangesScopeSpy = jasmine.createSpy('$onChangesScope');
const $onChangesControllerSpyB = jasmine.createSpy('$onChangesControllerB'); let ng2Instance: any;
const $onChangesScopeSpy = jasmine.createSpy('$onChangesScope');
let ng2Instance: any;
@Component({ @Component({
selector: 'ng2', selector: 'ng2',
template: '<ng1-a [valA]="val"></ng1-a> | <ng1-b [valB]="val"></ng1-b>' template: '<ng1-a [valA]="val"></ng1-a> | <ng1-b [valB]="val"></ng1-b>'
}) })
class Ng2Component { class Ng2Component {
constructor() { ng2Instance = this; } constructor() { ng2Instance = this; }
} }
angular.module('ng1', []) angular.module('ng1', [])
.directive('ng1A', () => ({ .directive('ng1A', () => ({
template: '', template: '',
scope: {valA: '<'}, scope: {valA: '<'},
bindToController: true, bindToController: true,
controllerAs: '$ctrl', controllerAs: '$ctrl',
controller: function($scope: angular.IScope) { controller: function($scope: angular.IScope) {
this.$onChanges = $onChangesControllerSpyA; this.$onChanges = $onChangesControllerSpyA;
} }
})) }))
.directive('ng1B', () => ({ .directive(
template: '', 'ng1B',
scope: {valB: '<'}, () => ({
bindToController: false, template: '',
controllerAs: '$ctrl', scope: {valB: '<'},
controller: class { bindToController: false,
$onChanges(changes: SimpleChanges) { controllerAs: '$ctrl',
$onChangesControllerSpyB(changes); controller: class {
} $onChanges(changes: SimpleChanges) { $onChangesControllerSpyB(changes); }
} }
})) }))
.directive('ng2', adapter.downgradeNg2Component(Ng2Component)) .directive('ng2', adapter.downgradeNg2Component(Ng2Component))
.run(($rootScope: angular.IRootScopeService) => { .run(($rootScope: angular.IRootScopeService) => {
Object.getPrototypeOf($rootScope).$onChanges = $onChangesScopeSpy; Object.getPrototypeOf($rootScope).$onChanges = $onChangesScopeSpy;
}); });
@NgModule({ @NgModule({
declarations: [ declarations: [
adapter.upgradeNg1Component('ng1A'), adapter.upgradeNg1Component('ng1B'), adapter.upgradeNg1Component('ng1A'), adapter.upgradeNg1Component('ng1B'),
Ng2Component Ng2Component
], ],
imports: [BrowserModule], imports: [BrowserModule],
}) })
class Ng2Module { class Ng2Module {
} }
const element = html(`<div><ng2></ng2></div>`); const element = html(`<div><ng2></ng2></div>`);
adapter.bootstrap(element, ['ng1']).ready((ref) => { adapter.bootstrap(element, ['ng1']).ready((ref) => {
// Initial `$onChanges()` call // Initial `$onChanges()` call
tick(); tick();
expect($onChangesControllerSpyA.calls.count()).toBe(1); expect($onChangesControllerSpyA.calls.count()).toBe(1);
expect($onChangesControllerSpyA.calls.argsFor(0)[0]).toEqual({ expect($onChangesControllerSpyA.calls.argsFor(0)[0]).toEqual({
valA: jasmine.any(SimpleChange) valA: jasmine.any(SimpleChange)
}); });
expect($onChangesControllerSpyB).not.toHaveBeenCalled(); expect($onChangesControllerSpyB).not.toHaveBeenCalled();
expect($onChangesScopeSpy.calls.count()).toBe(1); expect($onChangesScopeSpy.calls.count()).toBe(1);
expect($onChangesScopeSpy.calls.argsFor(0)[0]).toEqual({ expect($onChangesScopeSpy.calls.argsFor(0)[0]).toEqual({
valB: jasmine.any(SimpleChange) valB: jasmine.any(SimpleChange)
}); });
$onChangesControllerSpyA.calls.reset(); $onChangesControllerSpyA.calls.reset();
$onChangesControllerSpyB.calls.reset(); $onChangesControllerSpyB.calls.reset();
$onChangesScopeSpy.calls.reset(); $onChangesScopeSpy.calls.reset();
// `$onChanges()` call after a change // `$onChanges()` call after a change
ng2Instance.val = 'new value'; ng2Instance.val = 'new value';
tick(); tick();
ref.ng1RootScope.$digest(); ref.ng1RootScope.$digest();
expect($onChangesControllerSpyA.calls.count()).toBe(1); expect($onChangesControllerSpyA.calls.count()).toBe(1);
expect($onChangesControllerSpyA.calls.argsFor(0)[0]).toEqual({ expect($onChangesControllerSpyA.calls.argsFor(0)[0]).toEqual({
valA: jasmine.objectContaining({currentValue: 'new value'}) valA: jasmine.objectContaining({currentValue: 'new value'})
}); });
expect($onChangesControllerSpyB).not.toHaveBeenCalled(); expect($onChangesControllerSpyB).not.toHaveBeenCalled();
expect($onChangesScopeSpy.calls.count()).toBe(1); expect($onChangesScopeSpy.calls.count()).toBe(1);
expect($onChangesScopeSpy.calls.argsFor(0)[0]).toEqual({ expect($onChangesScopeSpy.calls.argsFor(0)[0]).toEqual({
valB: jasmine.objectContaining({currentValue: 'new value'}) valB: jasmine.objectContaining({currentValue: 'new value'})
}); });
ref.dispose(); ref.dispose();
}); });
})); }));
fixmeIvy( it('should call `$onDestroy()` on controller', fakeAsync(() => {
'FW-843: destroy hooks are not registered on upgraded ng1 components contained in ng2 component templates under ivy') const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
.it('should call `$onDestroy()` on controller', fakeAsync(() => { const $onDestroySpyA = jasmine.createSpy('$onDestroyA');
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); const $onDestroySpyB = jasmine.createSpy('$onDestroyB');
const $onDestroySpyA = jasmine.createSpy('$onDestroyA'); let ng2ComponentInstance: Ng2Component;
const $onDestroySpyB = jasmine.createSpy('$onDestroyB');
let ng2ComponentInstance: Ng2Component;
@Component({ @Component({
selector: 'ng2', selector: 'ng2',
template: ` template: `
<div *ngIf="!ng2Destroy"> <div *ngIf="!ng2Destroy">
<ng1-a></ng1-a> | <ng1-b></ng1-b> <ng1-a></ng1-a> | <ng1-b></ng1-b>
</div> </div>
` `
}) })
class Ng2Component { class Ng2Component {
ng2Destroy: boolean = false; ng2Destroy: boolean = false;
constructor() { ng2ComponentInstance = this; } constructor() { ng2ComponentInstance = this; }
} }
// On browsers that don't support `requestAnimationFrame` (IE 9, Android <= 4.3), // On browsers that don't support `requestAnimationFrame` (IE 9, Android <= 4.3),
// `$animate` will use `setTimeout(..., 16.6)` instead. This timeout will still be // `$animate` will use `setTimeout(..., 16.6)` instead. This timeout will still be
// on // on
// the queue at the end of the test, causing it to fail. // the queue at the end of the test, causing it to fail.
// Mocking animations (via `ngAnimateMock`) avoids the issue. // Mocking animations (via `ngAnimateMock`) avoids the issue.
angular.module('ng1', ['ngAnimateMock']) angular.module('ng1', ['ngAnimateMock'])
.directive('ng1A', () => ({ .directive('ng1A', () => ({
template: '', template: '',
scope: {}, scope: {},
bindToController: true, bindToController: true,
controllerAs: '$ctrl', controllerAs: '$ctrl',
controller: class {$onDestroy() { $onDestroySpyA(); }} controller: class {$onDestroy() { $onDestroySpyA(); }}
})) }))
.directive( .directive('ng1B', () => ({
'ng1B', () => ({ template: '',
template: '', scope: {},
scope: {}, bindToController: false,
bindToController: false, controllerAs: '$ctrl',
controllerAs: '$ctrl', controller: function() { this.$onDestroy = $onDestroySpyB; }
controller: function() { this.$onDestroy = $onDestroySpyB; } }))
})) .directive('ng2', adapter.downgradeNg2Component(Ng2Component));
.directive('ng2', adapter.downgradeNg2Component(Ng2Component));
@NgModule({ @NgModule({
declarations: [ declarations: [
adapter.upgradeNg1Component('ng1A'), adapter.upgradeNg1Component('ng1B'), adapter.upgradeNg1Component('ng1A'), adapter.upgradeNg1Component('ng1B'),
Ng2Component Ng2Component
], ],
imports: [BrowserModule], imports: [BrowserModule],
}) })
class Ng2Module { class Ng2Module {
} }
const element = html(`<div ng-if="!ng1Destroy"><ng2></ng2></div>`); const element = html(`<div ng-if="!ng1Destroy"><ng2></ng2></div>`);
adapter.bootstrap(element, ['ng1']).ready((ref) => { adapter.bootstrap(element, ['ng1']).ready((ref) => {
const $rootScope = ref.ng1RootScope as any; const $rootScope = ref.ng1RootScope as any;
$rootScope.ng1Destroy = false; $rootScope.ng1Destroy = false;
tick(); tick();
$rootScope.$digest(); $rootScope.$digest();
expect($onDestroySpyA).not.toHaveBeenCalled(); expect($onDestroySpyA).not.toHaveBeenCalled();
expect($onDestroySpyB).not.toHaveBeenCalled(); expect($onDestroySpyB).not.toHaveBeenCalled();
$rootScope.ng1Destroy = true; $rootScope.ng1Destroy = true;
tick(); tick();
$rootScope.$digest(); $rootScope.$digest();
expect($onDestroySpyA).toHaveBeenCalled(); expect($onDestroySpyA).toHaveBeenCalled();
expect($onDestroySpyB).toHaveBeenCalled(); expect($onDestroySpyB).toHaveBeenCalled();
$onDestroySpyA.calls.reset(); $onDestroySpyA.calls.reset();
$onDestroySpyB.calls.reset(); $onDestroySpyB.calls.reset();
$rootScope.ng1Destroy = false; $rootScope.ng1Destroy = false;
tick(); tick();
$rootScope.$digest(); $rootScope.$digest();
expect($onDestroySpyA).not.toHaveBeenCalled(); expect($onDestroySpyA).not.toHaveBeenCalled();
expect($onDestroySpyB).not.toHaveBeenCalled(); expect($onDestroySpyB).not.toHaveBeenCalled();
ng2ComponentInstance.ng2Destroy = true; ng2ComponentInstance.ng2Destroy = true;
tick(); tick();
$rootScope.$digest(); $rootScope.$digest();
expect($onDestroySpyA).toHaveBeenCalled(); expect($onDestroySpyA).toHaveBeenCalled();
expect($onDestroySpyB).toHaveBeenCalled(); expect($onDestroySpyB).toHaveBeenCalled();
ref.dispose(); ref.dispose();
}); });
})); }));
it('should not call `$onDestroy()` on scope', fakeAsync(() => { it('should not call `$onDestroy()` on scope', fakeAsync(() => {
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
@ -3053,40 +3048,37 @@ withEachNg1Version(() => {
})); }));
}); });
fixmeIvy( it('should bind input properties (<) of components', async(() => {
'FW-844: Directive input bindings cannot be assigned after the `@Directive` decorator has been compiled') const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
.it('should bind input properties (<) of components', async(() => { const ng1Module = angular.module('ng1', []);
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
const ng1Module = angular.module('ng1', []);
const ng1 = { const ng1 = {
bindings: {personProfile: '<'}, bindings: {personProfile: '<'},
template: template: 'Hello {{$ctrl.personProfile.firstName}} {{$ctrl.personProfile.lastName}}',
'Hello {{$ctrl.personProfile.firstName}} {{$ctrl.personProfile.lastName}}', controller: class {}
controller: class {} };
}; ng1Module.component('ng1', ng1);
ng1Module.component('ng1', ng1);
@Component({selector: 'ng2', template: '<ng1 [personProfile]="goku"></ng1>'}) @Component({selector: 'ng2', template: '<ng1 [personProfile]="goku"></ng1>'})
class Ng2 { class Ng2 {
goku = {firstName: 'GOKU', lastName: 'SAN'}; goku = {firstName: 'GOKU', lastName: 'SAN'};
} }
@NgModule({ @NgModule({
declarations: [adapter.upgradeNg1Component('ng1'), Ng2], declarations: [adapter.upgradeNg1Component('ng1'), Ng2],
imports: [BrowserModule], imports: [BrowserModule],
}) })
class Ng2Module { class Ng2Module {
} }
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
const element = html(`<div><ng2></ng2></div>`); const element = html(`<div><ng2></ng2></div>`);
adapter.bootstrap(element, ['ng1']).ready((ref) => { adapter.bootstrap(element, ['ng1']).ready((ref) => {
expect(multiTrim(document.body.textContent)).toEqual(`Hello GOKU SAN`); expect(multiTrim(document.body.textContent)).toEqual(`Hello GOKU SAN`);
ref.dispose(); ref.dispose();
}); });
})); }));
it('should support ng2 > ng1 > ng2', async(() => { it('should support ng2 > ng1 > ng2', async(() => {
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));

View File

@ -3968,6 +3968,7 @@ withEachNg1Version(() => {
}); });
})); }));
// fixmeIvy('FW-724: upgraded ng1 components are not being rendered')
it('should support ng2 > ng1 > ng2 (with inputs/outputs)', fakeAsync(() => { it('should support ng2 > ng1 > ng2 (with inputs/outputs)', fakeAsync(() => {
let ng2ComponentAInstance: Ng2ComponentA; let ng2ComponentAInstance: Ng2ComponentA;
let ng2ComponentBInstance: Ng2ComponentB; let ng2ComponentBInstance: Ng2ComponentB;