fix(upgrade): allow nesting components from different downgraded modules (#27217)
PR Close #27217
This commit is contained in:
parent
c51b0b9564
commit
4ae1880642
@ -11,7 +11,7 @@ import {ComponentFactory, ComponentFactoryResolver, Injector, NgZone, Type} from
|
|||||||
import * as angular from './angular1';
|
import * as angular from './angular1';
|
||||||
import {$COMPILE, $INJECTOR, $PARSE, INJECTOR_KEY, LAZY_MODULE_REF, REQUIRE_INJECTOR, REQUIRE_NG_MODEL} from './constants';
|
import {$COMPILE, $INJECTOR, $PARSE, INJECTOR_KEY, LAZY_MODULE_REF, REQUIRE_INJECTOR, REQUIRE_NG_MODEL} from './constants';
|
||||||
import {DowngradeComponentAdapter} from './downgrade_component_adapter';
|
import {DowngradeComponentAdapter} from './downgrade_component_adapter';
|
||||||
import {LazyModuleRef, UpgradeAppType, controllerKey, getTypeName, getUpgradeAppType, isFunction, validateInjectionKey} from './util';
|
import {LazyModuleRef, UpgradeAppType, controllerKey, getDowngradedModuleCount, getTypeName, getUpgradeAppType, isFunction, validateInjectionKey} from './util';
|
||||||
|
|
||||||
|
|
||||||
interface Thenable<T> {
|
interface Thenable<T> {
|
||||||
@ -91,6 +91,10 @@ export function downgradeComponent(info: {
|
|||||||
!isNgUpgradeLite ? cb => cb : cb => () => NgZone.isInAngularZone() ? cb() : ngZone.run(cb);
|
!isNgUpgradeLite ? cb => cb : cb => () => NgZone.isInAngularZone() ? cb() : ngZone.run(cb);
|
||||||
let ngZone: NgZone;
|
let ngZone: NgZone;
|
||||||
|
|
||||||
|
// When downgrading multiple modules, special handling is needed wrt injectors.
|
||||||
|
const hasMultipleDowngradedModules =
|
||||||
|
isNgUpgradeLite && (getDowngradedModuleCount($injector) > 1);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
restrict: 'E',
|
restrict: 'E',
|
||||||
terminal: true,
|
terminal: true,
|
||||||
@ -102,10 +106,11 @@ export function downgradeComponent(info: {
|
|||||||
// been compiled.
|
// been compiled.
|
||||||
|
|
||||||
const ngModel: angular.INgModelController = required[1];
|
const ngModel: angular.INgModelController = required[1];
|
||||||
let parentInjector: Injector|Thenable<Injector>|undefined = required[0];
|
const parentInjector: Injector|Thenable<Injector>|undefined = required[0];
|
||||||
|
let moduleInjector: Injector|Thenable<Injector>|undefined = undefined;
|
||||||
let ranAsync = false;
|
let ranAsync = false;
|
||||||
|
|
||||||
if (!parentInjector) {
|
if (!parentInjector || hasMultipleDowngradedModules) {
|
||||||
const downgradedModule = info.downgradedModule || '';
|
const downgradedModule = info.downgradedModule || '';
|
||||||
const lazyModuleRefKey = `${LAZY_MODULE_REF}${downgradedModule}`;
|
const lazyModuleRefKey = `${LAZY_MODULE_REF}${downgradedModule}`;
|
||||||
const attemptedAction = `instantiating component '${getTypeName(info.component)}'`;
|
const attemptedAction = `instantiating component '${getTypeName(info.component)}'`;
|
||||||
@ -113,12 +118,55 @@ export function downgradeComponent(info: {
|
|||||||
validateInjectionKey($injector, downgradedModule, lazyModuleRefKey, attemptedAction);
|
validateInjectionKey($injector, downgradedModule, lazyModuleRefKey, attemptedAction);
|
||||||
|
|
||||||
const lazyModuleRef = $injector.get(lazyModuleRefKey) as LazyModuleRef;
|
const lazyModuleRef = $injector.get(lazyModuleRefKey) as LazyModuleRef;
|
||||||
parentInjector = lazyModuleRef.injector || lazyModuleRef.promise as Promise<Injector>;
|
moduleInjector = lazyModuleRef.injector || lazyModuleRef.promise as Promise<Injector>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const doDowngrade = (injector: Injector) => {
|
// Notes:
|
||||||
|
//
|
||||||
|
// There are two injectors: `finalModuleInjector` and `finalParentInjector` (they might be
|
||||||
|
// the same instance, but that is irrelevant):
|
||||||
|
// - `finalModuleInjector` is used to retrieve `ComponentFactoryResolver`, thus it must be
|
||||||
|
// on the same tree as the `NgModule` that declares this downgraded component.
|
||||||
|
// - `finalParentInjector` is used for all other injection purposes.
|
||||||
|
// (Note that Angular knows to only traverse the component-tree part of that injector,
|
||||||
|
// when looking for an injectable and then switch to the module injector.)
|
||||||
|
//
|
||||||
|
// There are basically three cases:
|
||||||
|
// - If there is no parent component (thus no `parentInjector`), we bootstrap the downgraded
|
||||||
|
// `NgModule` and use its injector as both `finalModuleInjector` and
|
||||||
|
// `finalParentInjector`.
|
||||||
|
// - If there is a parent component (and thus a `parentInjector`) and we are sure that it
|
||||||
|
// belongs to the same `NgModule` as this downgraded component (e.g. because there is only
|
||||||
|
// one downgraded module, we use that `parentInjector` as both `finalModuleInjector` and
|
||||||
|
// `finalParentInjector`.
|
||||||
|
// - If there is a parent component, but it may belong to a different `NgModule`, then we
|
||||||
|
// use the `parentInjector` as `finalParentInjector` and this downgraded component's
|
||||||
|
// declaring `NgModule`'s injector as `finalModuleInjector`.
|
||||||
|
// Note 1: If the `NgModule` is already bootstrapped, we just get its injector (we don't
|
||||||
|
// bootstrap again).
|
||||||
|
// Note 2: It is possible that (while there are multiple downgraded modules) this
|
||||||
|
// downgraded component and its parent component both belong to the same NgModule.
|
||||||
|
// In that case, we could have used the `parentInjector` as both
|
||||||
|
// `finalModuleInjector` and `finalParentInjector`, but (for simplicity) we are
|
||||||
|
// treating this case as if they belong to different `NgModule`s. That doesn't
|
||||||
|
// really affect anything, since `parentInjector` has `moduleInjector` as ancestor
|
||||||
|
// and trying to resolve `ComponentFactoryResolver` from either one will return
|
||||||
|
// the same instance.
|
||||||
|
|
||||||
|
// If there is a parent component, use its injector as parent injector.
|
||||||
|
// If this is a "top-level" Angular component, use the module injector.
|
||||||
|
const finalParentInjector = parentInjector || moduleInjector !;
|
||||||
|
|
||||||
|
// If this is a "top-level" Angular component or the parent component may belong to a
|
||||||
|
// different `NgModule`, use the module injector for module-specific dependencies.
|
||||||
|
// If there is a parent component that belongs to the same `NgModule`, use its injector.
|
||||||
|
const finalModuleInjector = moduleInjector || parentInjector !;
|
||||||
|
|
||||||
|
const doDowngrade = (injector: Injector, moduleInjector: Injector) => {
|
||||||
|
// Retrieve `ComponentFactoryResolver` from the injector tied to the `NgModule` this
|
||||||
|
// component belongs to.
|
||||||
const componentFactoryResolver: ComponentFactoryResolver =
|
const componentFactoryResolver: ComponentFactoryResolver =
|
||||||
injector.get(ComponentFactoryResolver);
|
moduleInjector.get(ComponentFactoryResolver);
|
||||||
const componentFactory: ComponentFactory<any> =
|
const componentFactory: ComponentFactory<any> =
|
||||||
componentFactoryResolver.resolveComponentFactory(info.component) !;
|
componentFactoryResolver.resolveComponentFactory(info.component) !;
|
||||||
|
|
||||||
@ -146,18 +194,20 @@ export function downgradeComponent(info: {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const downgradeFn = !isNgUpgradeLite ? doDowngrade : (injector: Injector) => {
|
const downgradeFn =
|
||||||
|
!isNgUpgradeLite ? doDowngrade : (pInjector: Injector, mInjector: Injector) => {
|
||||||
if (!ngZone) {
|
if (!ngZone) {
|
||||||
ngZone = injector.get(NgZone);
|
ngZone = pInjector.get(NgZone);
|
||||||
}
|
}
|
||||||
|
|
||||||
wrapCallback(() => doDowngrade(injector))();
|
wrapCallback(() => doDowngrade(pInjector, mInjector))();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isThenable<Injector>(parentInjector)) {
|
if (isThenable(finalParentInjector) || isThenable(finalModuleInjector)) {
|
||||||
parentInjector.then(downgradeFn);
|
Promise.all([finalParentInjector, finalModuleInjector])
|
||||||
|
.then(([pInjector, mInjector]) => downgradeFn(pInjector, mInjector));
|
||||||
} else {
|
} else {
|
||||||
downgradeFn(parentInjector);
|
downgradeFn(finalParentInjector, finalModuleInjector);
|
||||||
}
|
}
|
||||||
|
|
||||||
ranAsync = true;
|
ranAsync = true;
|
||||||
|
@ -6,13 +6,13 @@
|
|||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, ApplicationRef, Component, DoCheck, Inject, Injector, Input, NgModule, NgZone, OnChanges, OnDestroy, OnInit, StaticProvider, Type, ViewRef, destroyPlatform, getPlatform} from '@angular/core';
|
import {AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, ApplicationRef, Compiler, Component, Directive, DoCheck, ElementRef, Inject, Injectable, Injector, Input, NgModule, NgZone, OnChanges, OnDestroy, OnInit, StaticProvider, Type, ViewRef, destroyPlatform, getPlatform} from '@angular/core';
|
||||||
import {async, fakeAsync, tick} from '@angular/core/testing';
|
import {async, fakeAsync, tick} from '@angular/core/testing';
|
||||||
import {BrowserModule} from '@angular/platform-browser';
|
import {BrowserModule} from '@angular/platform-browser';
|
||||||
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
|
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
|
||||||
import {browserDetection} from '@angular/platform-browser/testing/src/browser_util';
|
import {browserDetection} from '@angular/platform-browser/testing/src/browser_util';
|
||||||
import {fixmeIvy} from '@angular/private/testing';
|
import {fixmeIvy} from '@angular/private/testing';
|
||||||
import {downgradeComponent, downgradeModule} from '@angular/upgrade/static';
|
import {UpgradeComponent, downgradeComponent, downgradeModule} from '@angular/upgrade/static';
|
||||||
import * as angular from '@angular/upgrade/static/src/common/angular1';
|
import * as angular from '@angular/upgrade/static/src/common/angular1';
|
||||||
import {$EXCEPTION_HANDLER, $ROOT_SCOPE, INJECTOR_KEY, LAZY_MODULE_REF} from '@angular/upgrade/static/src/common/constants';
|
import {$EXCEPTION_HANDLER, $ROOT_SCOPE, INJECTOR_KEY, LAZY_MODULE_REF} from '@angular/upgrade/static/src/common/constants';
|
||||||
import {LazyModuleRef} from '@angular/upgrade/static/src/common/util';
|
import {LazyModuleRef} from '@angular/upgrade/static/src/common/util';
|
||||||
@ -78,6 +78,259 @@ withEachNg1Version(() => {
|
|||||||
setTimeout(() => expect(element.textContent).toBe('a | b'));
|
setTimeout(() => expect(element.textContent).toBe('a | b'));
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
it('should support nesting components from different downgraded modules', async(() => {
|
||||||
|
@Directive({selector: 'ng1A'})
|
||||||
|
class Ng1ComponentA extends UpgradeComponent {
|
||||||
|
constructor(elementRef: ElementRef, injector: Injector) {
|
||||||
|
super('ng1A', elementRef, injector);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ng2A',
|
||||||
|
template: 'ng2A(<ng1A></ng1A>)',
|
||||||
|
})
|
||||||
|
class Ng2ComponentA {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ng2B',
|
||||||
|
template: 'ng2B',
|
||||||
|
})
|
||||||
|
class Ng2ComponentB {
|
||||||
|
}
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [Ng1ComponentA, Ng2ComponentA],
|
||||||
|
entryComponents: [Ng2ComponentA],
|
||||||
|
imports: [BrowserModule],
|
||||||
|
})
|
||||||
|
class Ng2ModuleA {
|
||||||
|
ngDoBootstrap() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [Ng2ComponentB],
|
||||||
|
entryComponents: [Ng2ComponentB],
|
||||||
|
imports: [BrowserModule],
|
||||||
|
})
|
||||||
|
class Ng2ModuleB {
|
||||||
|
ngDoBootstrap() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const doDowngradeModule = (module: Type<any>) => {
|
||||||
|
const bootstrapFn = (extraProviders: StaticProvider[]) => {
|
||||||
|
const platformRef = getPlatform() || platformBrowserDynamic(extraProviders);
|
||||||
|
return platformRef.bootstrapModule(module);
|
||||||
|
};
|
||||||
|
return downgradeModule(bootstrapFn);
|
||||||
|
};
|
||||||
|
|
||||||
|
const downModA = doDowngradeModule(Ng2ModuleA);
|
||||||
|
const downModB = doDowngradeModule(Ng2ModuleB);
|
||||||
|
const ng1Module =
|
||||||
|
angular.module('ng1', [downModA, downModB])
|
||||||
|
.directive('ng1A', () => ({template: 'ng1A(<ng2-b ng-if="showB"></ng2-b>)'}))
|
||||||
|
.directive('ng2A', downgradeComponent({
|
||||||
|
component: Ng2ComponentA,
|
||||||
|
downgradedModule: downModA, propagateDigest,
|
||||||
|
}))
|
||||||
|
.directive('ng2B', downgradeComponent({
|
||||||
|
component: Ng2ComponentB,
|
||||||
|
downgradedModule: downModB, propagateDigest,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const element = html('<ng2-a></ng2-a>');
|
||||||
|
const $injector = angular.bootstrap(element, [ng1Module.name]);
|
||||||
|
const $rootScope = $injector.get($ROOT_SCOPE) as angular.IRootScopeService;
|
||||||
|
|
||||||
|
// Wait for module A to be bootstrapped.
|
||||||
|
setTimeout(() => {
|
||||||
|
// Wait for the upgraded component's `ngOnInit()`.
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(element.textContent).toBe('ng2A(ng1A())');
|
||||||
|
|
||||||
|
$rootScope.$apply('showB = true');
|
||||||
|
|
||||||
|
// Wait for module B to be bootstrapped.
|
||||||
|
setTimeout(() => expect(element.textContent).toBe('ng2A(ng1A(ng2B))'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
fixmeIvy('FW-714: ng1 projected content is not being rendered')
|
||||||
|
.it('should support nesting components from different downgraded modules (via projection)',
|
||||||
|
async(() => {
|
||||||
|
@Component({
|
||||||
|
selector: 'ng2A',
|
||||||
|
template: 'ng2A(<ng-content></ng-content>)',
|
||||||
|
})
|
||||||
|
class Ng2ComponentA {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ng2B',
|
||||||
|
template: 'ng2B',
|
||||||
|
})
|
||||||
|
class Ng2ComponentB {
|
||||||
|
}
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [Ng2ComponentA],
|
||||||
|
entryComponents: [Ng2ComponentA],
|
||||||
|
imports: [BrowserModule],
|
||||||
|
})
|
||||||
|
class Ng2ModuleA {
|
||||||
|
ngDoBootstrap() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [Ng2ComponentB],
|
||||||
|
entryComponents: [Ng2ComponentB],
|
||||||
|
imports: [BrowserModule],
|
||||||
|
})
|
||||||
|
class Ng2ModuleB {
|
||||||
|
ngDoBootstrap() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const doDowngradeModule = (module: Type<any>) => {
|
||||||
|
const bootstrapFn = (extraProviders: StaticProvider[]) => {
|
||||||
|
const platformRef = getPlatform() || platformBrowserDynamic(extraProviders);
|
||||||
|
return platformRef.bootstrapModule(module);
|
||||||
|
};
|
||||||
|
return downgradeModule(bootstrapFn);
|
||||||
|
};
|
||||||
|
|
||||||
|
const downModA = doDowngradeModule(Ng2ModuleA);
|
||||||
|
const downModB = doDowngradeModule(Ng2ModuleB);
|
||||||
|
const ng1Module = angular.module('ng1', [downModA, downModB])
|
||||||
|
.directive('ng2A', downgradeComponent({
|
||||||
|
component: Ng2ComponentA,
|
||||||
|
downgradedModule: downModA, propagateDigest,
|
||||||
|
}))
|
||||||
|
.directive('ng2B', downgradeComponent({
|
||||||
|
component: Ng2ComponentB,
|
||||||
|
downgradedModule: downModB, propagateDigest,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const element = html('<ng2-a><ng2-b ng-if="showB"></ng2-b></ng2-a>');
|
||||||
|
const $injector = angular.bootstrap(element, [ng1Module.name]);
|
||||||
|
const $rootScope = $injector.get($ROOT_SCOPE) as angular.IRootScopeService;
|
||||||
|
|
||||||
|
// Wait for module A to be bootstrapped.
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(element.textContent).toBe('ng2A()');
|
||||||
|
|
||||||
|
$rootScope.$apply('showB = true');
|
||||||
|
|
||||||
|
// Wait for module B to be bootstrapped.
|
||||||
|
setTimeout(() => expect(element.textContent).toBe('ng2A(ng2B)'));
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
fixmeIvy('FW-714: ng1 projected content is not being rendered')
|
||||||
|
.it('should support manually setting up a root module for all downgraded modules',
|
||||||
|
fakeAsync(() => {
|
||||||
|
@Injectable({providedIn: 'root'})
|
||||||
|
class CounterService {
|
||||||
|
private static counter = 0;
|
||||||
|
value = ++CounterService.counter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ng2A',
|
||||||
|
template: 'ng2A(Counter:{{ counter.value }} | <ng-content></ng-content>)',
|
||||||
|
})
|
||||||
|
class Ng2ComponentA {
|
||||||
|
constructor(public counter: CounterService) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ng2B',
|
||||||
|
template: 'Counter:{{ counter.value }}',
|
||||||
|
})
|
||||||
|
class Ng2ComponentB {
|
||||||
|
constructor(public counter: CounterService) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [Ng2ComponentA],
|
||||||
|
entryComponents: [Ng2ComponentA],
|
||||||
|
})
|
||||||
|
class Ng2ModuleA {
|
||||||
|
}
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [Ng2ComponentB],
|
||||||
|
entryComponents: [Ng2ComponentB],
|
||||||
|
})
|
||||||
|
class Ng2ModuleB {
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Empty" module that will serve as root for all downgraded modules,
|
||||||
|
// ensuring there will only be one instance for all injectables provided in "root".
|
||||||
|
@NgModule({
|
||||||
|
imports: [BrowserModule],
|
||||||
|
})
|
||||||
|
class Ng2ModuleRoot {
|
||||||
|
ngDoBootstrap() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let rootInjectorPromise: Promise<Injector>|null = null;
|
||||||
|
const doDowngradeModule = (module: Type<any>) => {
|
||||||
|
const bootstrapFn = (extraProviders: StaticProvider[]) => {
|
||||||
|
if (!rootInjectorPromise) {
|
||||||
|
rootInjectorPromise = platformBrowserDynamic(extraProviders)
|
||||||
|
.bootstrapModule(Ng2ModuleRoot)
|
||||||
|
.then(ref => ref.injector);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rootInjectorPromise.then(rootInjector => {
|
||||||
|
const compiler = rootInjector.get(Compiler);
|
||||||
|
const moduleFactory = compiler.compileModuleSync(module);
|
||||||
|
|
||||||
|
return moduleFactory.create(rootInjector);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return downgradeModule(bootstrapFn);
|
||||||
|
};
|
||||||
|
|
||||||
|
const downModA = doDowngradeModule(Ng2ModuleA);
|
||||||
|
const downModB = doDowngradeModule(Ng2ModuleB);
|
||||||
|
const ng1Module = angular.module('ng1', [downModA, downModB])
|
||||||
|
.directive('ng2A', downgradeComponent({
|
||||||
|
component: Ng2ComponentA,
|
||||||
|
downgradedModule: downModA, propagateDigest,
|
||||||
|
}))
|
||||||
|
.directive('ng2B', downgradeComponent({
|
||||||
|
component: Ng2ComponentB,
|
||||||
|
downgradedModule: downModB, propagateDigest,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const element = html(`
|
||||||
|
<ng2-a><ng2-b ng-if="showB1"></ng2-b></ng2-a>
|
||||||
|
<ng2-b ng-if="showB2"></ng2-b>
|
||||||
|
`);
|
||||||
|
const $injector = angular.bootstrap(element, [ng1Module.name]);
|
||||||
|
const $rootScope = $injector.get($ROOT_SCOPE) as angular.IRootScopeService;
|
||||||
|
|
||||||
|
tick(); // Wait for module A to be bootstrapped.
|
||||||
|
expect(multiTrim(element.textContent)).toBe('ng2A(Counter:1 | )');
|
||||||
|
|
||||||
|
// Nested component B should use the same `CounterService` instance.
|
||||||
|
$rootScope.$apply('showB1 = true');
|
||||||
|
|
||||||
|
tick(); // Wait for module B to be bootstrapped.
|
||||||
|
expect(multiTrim(element.children[0].textContent))
|
||||||
|
.toBe('ng2A(Counter:1 | Counter:1)');
|
||||||
|
|
||||||
|
// Top-level component B should use the same `CounterService` instance.
|
||||||
|
$rootScope.$apply('showB2 = true');
|
||||||
|
tick();
|
||||||
|
|
||||||
|
expect(multiTrim(element.children[1].textContent)).toBe('Counter:1');
|
||||||
|
}));
|
||||||
|
|
||||||
it('should support downgrading a component and propagate inputs', async(() => {
|
it('should support downgrading a component and propagate inputs', async(() => {
|
||||||
@Component(
|
@Component(
|
||||||
{selector: 'ng2A', template: 'a({{ value }}) | <ng2B [value]="value"></ng2B>'})
|
{selector: 'ng2A', template: 'a({{ value }}) | <ng2B [value]="value"></ng2B>'})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user