fix(ivy): pass ngContentSelectors through to `defineComponent()` calls (#27867)

Libraries that create components dynamically using component factories,
such as `@angular/upgrade` need to pass blocks of projected content
through to the `ComponentFactory.create()` method. These blocks
are extracted from the content by matching CSS selectors defined in
`<ng-content select="..">` tags found in the component's template.

The Angular compiler collects these CSS selectors when compiling a component's
template, and exposes them via the `ComponentFactory.ngContentSelectors`
property.

This change ensures that this property is filled correctly when the
component factory is created by compiling a component with the Ivy engine.

PR Close #27867
This commit is contained in:
Pete Bacon Darwin 2018-12-22 16:02:34 +00:00 committed by Andrew Kushnir
parent e8a57f0ee6
commit feebe03523
12 changed files with 301 additions and 262 deletions

View File

@ -1144,6 +1144,7 @@ describe('compiler compliance', () => {
type: SimpleComponent, type: SimpleComponent,
selectors: [["simple"]], selectors: [["simple"]],
factory: function SimpleComponent_Factory(t) { return new (t || SimpleComponent)(); }, factory: function SimpleComponent_Factory(t) { return new (t || SimpleComponent)(); },
ngContentSelectors: _c0,
consts: 2, consts: 2,
vars: 0, vars: 0,
template: function SimpleComponent_Template(rf, ctx) { template: function SimpleComponent_Template(rf, ctx) {
@ -1167,6 +1168,7 @@ describe('compiler compliance', () => {
type: ComplexComponent, type: ComplexComponent,
selectors: [["complex"]], selectors: [["complex"]],
factory: function ComplexComponent_Factory(t) { return new (t || ComplexComponent)(); }, factory: function ComplexComponent_Factory(t) { return new (t || ComplexComponent)(); },
ngContentSelectors: _c4,
consts: 4, consts: 4,
vars: 0, vars: 0,
template: function ComplexComponent_Template(rf, ctx) { template: function ComplexComponent_Template(rf, ctx) {
@ -1561,6 +1563,7 @@ describe('compiler compliance', () => {
($r3$.ɵqueryRefresh(($tmp$ = $r3$.ɵloadQueryList(queryStartIndex))) && ($instance$.someDir = $tmp$.first)); ($r3$.ɵqueryRefresh(($tmp$ = $r3$.ɵloadQueryList(queryStartIndex))) && ($instance$.someDir = $tmp$.first));
($r3$.ɵqueryRefresh(($tmp$ = $r3$.ɵloadQueryList((queryStartIndex + 1)))) && ($instance$.someDirList = $tmp$)); ($r3$.ɵqueryRefresh(($tmp$ = $r3$.ɵloadQueryList((queryStartIndex + 1)))) && ($instance$.someDirList = $tmp$));
}, },
ngContentSelectors: _c0,
consts: 2, consts: 2,
vars: 0, vars: 0,
template: function ContentQueryComponent_Template(rf, ctx) { template: function ContentQueryComponent_Template(rf, ctx) {

View File

@ -264,6 +264,13 @@ export function compileComponentFromMetadata(
const templateFunctionExpression = templateBuilder.buildTemplateFunction(template.nodes, []); const templateFunctionExpression = templateBuilder.buildTemplateFunction(template.nodes, []);
// We need to provide this so that dynamically generated components know what
// projected content blocks to pass through to the component when it is instantiated.
const ngContentSelectors = templateBuilder.getNgContentSelectors();
if (ngContentSelectors) {
definitionMap.set('ngContentSelectors', ngContentSelectors);
}
// e.g. `consts: 2` // e.g. `consts: 2`
definitionMap.set('consts', o.literal(templateBuilder.getConstCount())); definitionMap.set('consts', o.literal(templateBuilder.getConstCount()));

View File

@ -915,6 +915,12 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
getVarCount() { return this._pureFunctionSlots; } getVarCount() { return this._pureFunctionSlots; }
getNgContentSelectors(): o.Expression|null {
return this._hasNgContent ?
this.constantPool.getConstLiteral(asLiteral(this._ngContentSelectors), true) :
null;
}
private bindingContext() { return `${this._bindingContext++}`; } private bindingContext() { return `${this._bindingContext++}`; }
// Bindings must only be resolved after all local refs have been visited, so all // Bindings must only be resolved after all local refs have been visited, so all

View File

@ -119,7 +119,10 @@ export class ComponentFactory<T> extends viewEngine_ComponentFactory<T> {
super(); super();
this.componentType = componentDef.type; this.componentType = componentDef.type;
this.selector = componentDef.selectors[0][0] as string; this.selector = componentDef.selectors[0][0] as string;
this.ngContentSelectors = []; // The component definition does not include the wildcard ('*') selector in its list.
// It is implicitly expected as the first item in the projectable nodes array.
this.ngContentSelectors =
componentDef.ngContentSelectors ? ['*', ...componentDef.ngContentSelectors] : [];
} }
create( create(

View File

@ -182,6 +182,11 @@ export function defineComponent<T>(componentDefinition: {
*/ */
template: ComponentTemplate<T>; template: ComponentTemplate<T>;
/**
* An array of `ngContent[selector]` values that were found in the template.
*/
ngContentSelectors?: string[];
/** /**
* Additional set of instructions specific to view query processing. This could be seen as a * Additional set of instructions specific to view query processing. This could be seen as a
* set of instruction to be inserted into the template function. * set of instruction to be inserted into the template function.
@ -249,6 +254,7 @@ export function defineComponent<T>(componentDefinition: {
vars: componentDefinition.vars, vars: componentDefinition.vars,
factory: componentDefinition.factory, factory: componentDefinition.factory,
template: componentDefinition.template || null !, template: componentDefinition.template || null !,
ngContentSelectors: componentDefinition.ngContentSelectors,
hostBindings: componentDefinition.hostBindings || null, hostBindings: componentDefinition.hostBindings || null,
contentQueries: componentDefinition.contentQueries || null, contentQueries: componentDefinition.contentQueries || null,
contentQueriesRefresh: componentDefinition.contentQueriesRefresh || null, contentQueriesRefresh: componentDefinition.contentQueriesRefresh || null,

View File

@ -189,6 +189,11 @@ export interface ComponentDef<T> extends DirectiveDef<T> {
*/ */
readonly template: ComponentTemplate<T>; readonly template: ComponentTemplate<T>;
/**
* An array of `ngContent[selector]` values that were found in the template.
*/
readonly ngContentSelectors?: string[];
/** /**
* A set of styles that the component needs to be present for component to render correctly. * A set of styles that the component needs to be present for component to render correctly.
*/ */

View File

@ -18,7 +18,28 @@ describe('ComponentFactory', () => {
const cfr = injectComponentFactoryResolver(); const cfr = injectComponentFactoryResolver();
describe('constructor()', () => { describe('constructor()', () => {
it('should correctly populate public properties', () => { it('should correctly populate default properties', () => {
class TestComponent {
static ngComponentDef = defineComponent({
type: TestComponent,
selectors: [['test', 'foo'], ['bar']],
consts: 0,
vars: 0,
template: () => undefined,
factory: () => new TestComponent(),
});
}
const cf = cfr.resolveComponentFactory(TestComponent);
expect(cf.selector).toBe('test');
expect(cf.componentType).toBe(TestComponent);
expect(cf.ngContentSelectors).toEqual([]);
expect(cf.inputs).toEqual([]);
expect(cf.outputs).toEqual([]);
});
it('should correctly populate defined properties', () => {
class TestComponent { class TestComponent {
static ngComponentDef = defineComponent({ static ngComponentDef = defineComponent({
type: TestComponent, type: TestComponent,
@ -27,6 +48,7 @@ describe('ComponentFactory', () => {
consts: 0, consts: 0,
vars: 0, vars: 0,
template: () => undefined, template: () => undefined,
ngContentSelectors: ['a', 'b'],
factory: () => new TestComponent(), factory: () => new TestComponent(),
inputs: { inputs: {
in1: 'in1', in1: 'in1',
@ -42,7 +64,7 @@ describe('ComponentFactory', () => {
const cf = cfr.resolveComponentFactory(TestComponent); const cf = cfr.resolveComponentFactory(TestComponent);
expect(cf.componentType).toBe(TestComponent); expect(cf.componentType).toBe(TestComponent);
expect(cf.ngContentSelectors).toEqual([]); expect(cf.ngContentSelectors).toEqual(['*', 'a', 'b']);
expect(cf.selector).toBe('test'); expect(cf.selector).toBe('test');
expect(cf.inputs).toEqual([ expect(cf.inputs).toEqual([

View File

@ -30,31 +30,30 @@ withEachNg1Version(() => {
describe('(basic use)', () => { describe('(basic use)', () => {
it('should have AngularJS loaded', () => expect(angular.version.major).toBe(1)); it('should have AngularJS loaded', () => expect(angular.version.major).toBe(1));
fixmeIvy('FW-714: ng1 projected content is not being rendered') it('should instantiate ng2 in ng1 template and project content', async(() => {
.it('should instantiate ng2 in ng1 template and project content', async(() => { const ng1Module = angular.module('ng1', []);
const ng1Module = angular.module('ng1', []);
@Component({ @Component({
selector: 'ng2', selector: 'ng2',
template: `{{ 'NG2' }}(<ng-content></ng-content>)`, template: `{{ 'NG2' }}(<ng-content></ng-content>)`,
}) })
class Ng2 { class Ng2 {
} }
@NgModule({declarations: [Ng2], imports: [BrowserModule]}) @NgModule({declarations: [Ng2], imports: [BrowserModule]})
class Ng2Module { class Ng2Module {
} }
const element = const element =
html('<div>{{ \'ng1[\' }}<ng2>~{{ \'ng-content\' }}~</ng2>{{ \']\' }}</div>'); html('<div>{{ \'ng1[\' }}<ng2>~{{ \'ng-content\' }}~</ng2>{{ \']\' }}</div>');
const adapter: UpgradeAdapter = new UpgradeAdapter(Ng2Module); const adapter: UpgradeAdapter = new UpgradeAdapter(Ng2Module);
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
adapter.bootstrap(element, ['ng1']).ready((ref) => { adapter.bootstrap(element, ['ng1']).ready((ref) => {
expect(document.body.textContent).toEqual('ng1[NG2(~ng-content~)]'); expect(document.body.textContent).toEqual('ng1[NG2(~ng-content~)]');
ref.dispose(); ref.dispose();
}); });
})); }));
it('should instantiate ng1 in ng2 template and project content', async(() => { it('should instantiate ng1 in ng2 template and project content', async(() => {
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
@ -724,72 +723,68 @@ withEachNg1Version(() => {
}); });
})); }));
fixmeIvy('FW-714: ng1 projected content is not being rendered') it('should support multi-slot projection', async(() => {
.it('should support multi-slot projection', async(() => { const ng1Module = angular.module('ng1', []);
const ng1Module = angular.module('ng1', []);
@Component({ @Component({
selector: 'ng2', selector: 'ng2',
template: '2a(<ng-content select=".ng1a"></ng-content>)' + template: '2a(<ng-content select=".ng1a"></ng-content>)' +
'2b(<ng-content select=".ng1b"></ng-content>)' '2b(<ng-content select=".ng1b"></ng-content>)'
}) })
class Ng2 { class Ng2 {
} }
@NgModule({declarations: [Ng2], imports: [BrowserModule]}) @NgModule({declarations: [Ng2], imports: [BrowserModule]})
class Ng2Module { class Ng2Module {
} }
// The ng-if on one of the projected children is here to make sure // The ng-if on one of the projected children is here to make sure
// the correct slot is targeted even with structural directives in play. // the correct slot is targeted even with structural directives in play.
const element = html( const element = html(
'<ng2><div ng-if="true" class="ng1a">1a</div><div' + '<ng2><div ng-if="true" class="ng1a">1a</div><div' +
' class="ng1b">1b</div></ng2>'); ' class="ng1b">1b</div></ng2>');
const adapter: UpgradeAdapter = new UpgradeAdapter(Ng2Module); const adapter: UpgradeAdapter = new UpgradeAdapter(Ng2Module);
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
adapter.bootstrap(element, ['ng1']).ready((ref) => { adapter.bootstrap(element, ['ng1']).ready((ref) => {
expect(document.body.textContent).toEqual('2a(1a)2b(1b)'); expect(document.body.textContent).toEqual('2a(1a)2b(1b)');
ref.dispose(); ref.dispose();
}); });
})); }));
fixmeIvy('FW-714: ng1 projected content is not being rendered') it('should correctly project structural directives', async(() => {
.it('should correctly project structural directives', async(() => { @Component({selector: 'ng2', template: 'ng2-{{ itemId }}(<ng-content></ng-content>)'})
@Component( class Ng2Component {
{selector: 'ng2', template: 'ng2-{{ itemId }}(<ng-content></ng-content>)'}) // TODO(issue/24571): remove '!'.
class Ng2Component { @Input() itemId !: string;
// TODO(issue/24571): remove '!'. }
@Input() itemId !: string;
}
@NgModule({imports: [BrowserModule], declarations: [Ng2Component]}) @NgModule({imports: [BrowserModule], declarations: [Ng2Component]})
class Ng2Module { class Ng2Module {
} }
const adapter: UpgradeAdapter = new UpgradeAdapter(Ng2Module); const adapter: UpgradeAdapter = new UpgradeAdapter(Ng2Module);
const ng1Module = const ng1Module = angular.module('ng1', [])
angular.module('ng1', []) .directive('ng2', adapter.downgradeNg2Component(Ng2Component))
.directive('ng2', adapter.downgradeNg2Component(Ng2Component)) .run(($rootScope: angular.IRootScopeService) => {
.run(($rootScope: angular.IRootScopeService) => { $rootScope['items'] = [
$rootScope['items'] = [ {id: 'a', subitems: [1, 2, 3]}, {id: 'b', subitems: [4, 5, 6]},
{id: 'a', subitems: [1, 2, 3]}, {id: 'b', subitems: [4, 5, 6]}, {id: 'c', subitems: [7, 8, 9]}
{id: 'c', subitems: [7, 8, 9]} ];
]; });
});
const element = html(` const element = html(`
<ng2 ng-repeat="item in items" [item-id]="item.id"> <ng2 ng-repeat="item in items" [item-id]="item.id">
<div ng-repeat="subitem in item.subitems">{{ subitem }}</div> <div ng-repeat="subitem in item.subitems">{{ subitem }}</div>
</ng2> </ng2>
`); `);
adapter.bootstrap(element, [ng1Module.name]).ready(ref => { adapter.bootstrap(element, [ng1Module.name]).ready(ref => {
expect(multiTrim(document.body.textContent)) expect(multiTrim(document.body.textContent))
.toBe('ng2-a( 123 )ng2-b( 456 )ng2-c( 789 )'); .toBe('ng2-a( 123 )ng2-b( 456 )ng2-c( 789 )');
ref.dispose(); ref.dispose();
}); });
})); }));
it('should allow attribute selectors for components in ng2', async(() => { it('should allow attribute selectors for components in ng2', async(() => {
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => MyNg2Module)); const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => MyNg2Module));
@ -3110,7 +3105,7 @@ withEachNg1Version(() => {
}); });
})); }));
fixmeIvy('FW-714: ng1 projected content is not being rendered') fixmeIvy('FW-873: projected component injector hierarchy not wired up correctly')
.it('should respect hierarchical dependency injection for ng2', async(() => { .it('should respect hierarchical dependency injection for ng2', async(() => {
const ng1Module = angular.module('ng1', []); const ng1Module = angular.module('ng1', []);
@ -3213,45 +3208,44 @@ withEachNg1Version(() => {
}); });
describe('examples', () => { describe('examples', () => {
fixmeIvy('FW-714: ng1 projected content is not being rendered') it('should verify UpgradeAdapter example', async(() => {
.it('should verify UpgradeAdapter example', async(() => { const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); const module = angular.module('myExample', []);
const module = angular.module('myExample', []);
const ng1 = () => { const ng1 = () => {
return { return {
scope: {title: '='}, scope: {title: '='},
transclude: true, transclude: true,
template: 'ng1[Hello {{title}}!](<span ng-transclude></span>)' template: 'ng1[Hello {{title}}!](<span ng-transclude></span>)'
}; };
}; };
module.directive('ng1', ng1); module.directive('ng1', ng1);
@Component({ @Component({
selector: 'ng2', selector: 'ng2',
inputs: ['name'], inputs: ['name'],
template: 'ng2[<ng1 [title]="name">transclude</ng1>](<ng-content></ng-content>)' template: 'ng2[<ng1 [title]="name">transclude</ng1>](<ng-content></ng-content>)'
}) })
class Ng2 { class Ng2 {
} }
@NgModule({ @NgModule({
declarations: [adapter.upgradeNg1Component('ng1'), Ng2], declarations: [adapter.upgradeNg1Component('ng1'), Ng2],
imports: [BrowserModule], imports: [BrowserModule],
}) })
class Ng2Module { class Ng2Module {
} }
module.directive('ng2', adapter.downgradeNg2Component(Ng2)); module.directive('ng2', adapter.downgradeNg2Component(Ng2));
document.body.innerHTML = '<ng2 name="World">project</ng2>'; document.body.innerHTML = '<ng2 name="World">project</ng2>';
adapter.bootstrap(document.body.firstElementChild !, ['myExample']).ready((ref) => { adapter.bootstrap(document.body.firstElementChild !, ['myExample']).ready((ref) => {
expect(multiTrim(document.body.textContent)) expect(multiTrim(document.body.textContent))
.toEqual('ng2[ng1[Hello World!](transclude)](project)'); .toEqual('ng2[ng1[Hello World!](transclude)](project)');
ref.dispose(); ref.dispose();
}); });
})); }));
}); });
describe('registerForNg1Tests', () => { describe('registerForNg1Tests', () => {

View File

@ -22,82 +22,78 @@ withEachNg1Version(() => {
beforeEach(() => destroyPlatform()); beforeEach(() => destroyPlatform());
afterEach(() => destroyPlatform()); afterEach(() => destroyPlatform());
fixmeIvy('FW-714: ng1 projected content is not being rendered') it('should instantiate ng2 in ng1 template and project content', async(() => {
.it('should instantiate ng2 in ng1 template and project content', async(() => {
// the ng2 component that will be used in ng1 (downgraded) // the ng2 component that will be used in ng1 (downgraded)
@Component({selector: 'ng2', template: `{{ prop }}(<ng-content></ng-content>)`}) @Component({selector: 'ng2', template: `{{ prop }}(<ng-content></ng-content>)`})
class Ng2Component { class Ng2Component {
prop = 'NG2'; prop = 'NG2';
ngContent = 'ng2-content'; ngContent = 'ng2-content';
} }
// our upgrade module to host the component to downgrade // our upgrade module to host the component to downgrade
@NgModule({ @NgModule({
imports: [BrowserModule, UpgradeModule], imports: [BrowserModule, UpgradeModule],
declarations: [Ng2Component], declarations: [Ng2Component],
entryComponents: [Ng2Component] entryComponents: [Ng2Component]
}) })
class Ng2Module { class Ng2Module {
ngDoBootstrap() {} ngDoBootstrap() {}
} }
// the ng1 app module that will consume the downgraded component // the ng1 app module that will consume the downgraded component
const ng1Module = angular const ng1Module = angular
.module('ng1', []) .module('ng1', [])
// create an ng1 facade of the ng2 component // create an ng1 facade of the ng2 component
.directive('ng2', downgradeComponent({component: Ng2Component})) .directive('ng2', downgradeComponent({component: Ng2Component}))
.run(($rootScope: angular.IRootScopeService) => { .run(($rootScope: angular.IRootScopeService) => {
$rootScope['prop'] = 'NG1'; $rootScope['prop'] = 'NG1';
$rootScope['ngContent'] = 'ng1-content'; $rootScope['ngContent'] = 'ng1-content';
}); });
const element = const element = html('<div>{{ \'ng1[\' }}<ng2>~{{ ngContent }}~</ng2>{{ \']\' }}</div>');
html('<div>{{ \'ng1[\' }}<ng2>~{{ ngContent }}~</ng2>{{ \']\' }}</div>');
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => { bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => {
expect(document.body.textContent).toEqual('ng1[NG2(~ng1-content~)]'); expect(document.body.textContent).toEqual('ng1[NG2(~ng1-content~)]');
}); });
})); }));
fixmeIvy('FW-714: ng1 projected content is not being rendered') it('should correctly project structural directives', async(() => {
.it('should correctly project structural directives', async(() => { @Component({selector: 'ng2', template: 'ng2-{{ itemId }}(<ng-content></ng-content>)'})
@Component({selector: 'ng2', template: 'ng2-{{ itemId }}(<ng-content></ng-content>)'}) class Ng2Component {
class Ng2Component { // TODO(issue/24571): remove '!'.
// TODO(issue/24571): remove '!'. @Input() itemId !: string;
@Input() itemId !: string; }
}
@NgModule({ @NgModule({
imports: [BrowserModule, UpgradeModule], imports: [BrowserModule, UpgradeModule],
declarations: [Ng2Component], declarations: [Ng2Component],
entryComponents: [Ng2Component] entryComponents: [Ng2Component]
}) })
class Ng2Module { class Ng2Module {
ngDoBootstrap() {} ngDoBootstrap() {}
} }
const ng1Module = const ng1Module = angular.module('ng1', [])
angular.module('ng1', []) .directive('ng2', downgradeComponent({component: Ng2Component}))
.directive('ng2', downgradeComponent({component: Ng2Component})) .run(($rootScope: angular.IRootScopeService) => {
.run(($rootScope: angular.IRootScopeService) => { $rootScope['items'] = [
$rootScope['items'] = [ {id: 'a', subitems: [1, 2, 3]}, {id: 'b', subitems: [4, 5, 6]},
{id: 'a', subitems: [1, 2, 3]}, {id: 'b', subitems: [4, 5, 6]}, {id: 'c', subitems: [7, 8, 9]}
{id: 'c', subitems: [7, 8, 9]} ];
]; });
});
const element = html(` const element = html(`
<ng2 ng-repeat="item in items" [item-id]="item.id"> <ng2 ng-repeat="item in items" [item-id]="item.id">
<div ng-repeat="subitem in item.subitems">{{ subitem }}</div> <div ng-repeat="subitem in item.subitems">{{ subitem }}</div>
</ng2> </ng2>
`); `);
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => { bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => {
expect(multiTrim(document.body.textContent)) expect(multiTrim(document.body.textContent))
.toBe('ng2-a( 123 )ng2-b( 456 )ng2-c( 789 )'); .toBe('ng2-a( 123 )ng2-b( 456 )ng2-c( 789 )');
}); });
})); }));
it('should instantiate ng1 in ng2 template and project content', async(() => { it('should instantiate ng1 in ng2 template and project content', async(() => {
@ -145,39 +141,38 @@ withEachNg1Version(() => {
}); });
})); }));
fixmeIvy('FW-714: ng1 projected content is not being rendered') it('should support multi-slot projection', async(() => {
.it('should support multi-slot projection', async(() => {
@Component({ @Component({
selector: 'ng2', selector: 'ng2',
template: '2a(<ng-content select=".ng1a"></ng-content>)' + template: '2a(<ng-content select=".ng1a"></ng-content>)' +
'2b(<ng-content select=".ng1b"></ng-content>)' '2b(<ng-content select=".ng1b"></ng-content>)'
}) })
class Ng2Component { class Ng2Component {
constructor() {} constructor() {}
} }
@NgModule({ @NgModule({
declarations: [Ng2Component], declarations: [Ng2Component],
entryComponents: [Ng2Component], entryComponents: [Ng2Component],
imports: [BrowserModule, UpgradeModule] imports: [BrowserModule, UpgradeModule]
}) })
class Ng2Module { class Ng2Module {
ngDoBootstrap() {} ngDoBootstrap() {}
} }
const ng1Module = angular.module('ng1', []).directive( const ng1Module = angular.module('ng1', []).directive(
'ng2', downgradeComponent({component: Ng2Component})); 'ng2', downgradeComponent({component: Ng2Component}));
// The ng-if on one of the projected children is here to make sure // The ng-if on one of the projected children is here to make sure
// the correct slot is targeted even with structural directives in play. // the correct slot is targeted even with structural directives in play.
const element = html( const element = html(
'<ng2><div ng-if="true" class="ng1a">1a</div><div' + '<ng2><div ng-if="true" class="ng1a">1a</div><div' +
' class="ng1b">1b</div></ng2>'); ' class="ng1b">1b</div></ng2>');
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => { bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => {
expect(document.body.textContent).toEqual('2a(1a)2b(1b)'); expect(document.body.textContent).toEqual('2a(1a)2b(1b)');
}); });
})); }));
}); });
}); });

View File

@ -709,7 +709,7 @@ withEachNg1Version(() => {
}); });
})); }));
fixmeIvy('FW-714: ng1 projected content is not being rendered') fixmeIvy('FW-873: projected component injector hierarchy not wired up correctly')
.it('should respect hierarchical dependency injection for ng2', async(() => { .it('should respect hierarchical dependency injection for ng2', async(() => {
@Component({selector: 'parent', template: 'parent(<ng-content></ng-content>)'}) @Component({selector: 'parent', template: 'parent(<ng-content></ng-content>)'})
class ParentComponent { class ParentComponent {

View File

@ -952,7 +952,6 @@ withEachNg1Version(() => {
})); }));
fixmeIvy('FW-715: ngOnChanges being called a second time unexpectedly') fixmeIvy('FW-715: ngOnChanges being called a second time unexpectedly')
.fixmeIvy('FW-714: ng1 projected content is not being rendered')
.it('should run the lifecycle hooks in the correct order', async(() => { .it('should run the lifecycle hooks in the correct order', async(() => {
const logs: string[] = []; const logs: string[] = [];
let rootScope: angular.IRootScopeService; let rootScope: angular.IRootScopeService;

View File

@ -24,70 +24,69 @@ withEachNg1Version(() => {
it('should have AngularJS loaded', () => expect(angular.version.major).toBe(1)); it('should have AngularJS loaded', () => expect(angular.version.major).toBe(1));
fixmeIvy('FW-714: ng1 projected content is not being rendered') it('should verify UpgradeAdapter example', async(() => {
.it('should verify UpgradeAdapter example', async(() => {
// This is wrapping (upgrading) an AngularJS component to be used in an Angular // This is wrapping (upgrading) an AngularJS component to be used in an Angular
// component // component
@Directive({selector: 'ng1'}) @Directive({selector: 'ng1'})
class Ng1Component extends UpgradeComponent { class Ng1Component extends UpgradeComponent {
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
@Input() title !: string; @Input() title !: string;
constructor(elementRef: ElementRef, injector: Injector) { constructor(elementRef: ElementRef, injector: Injector) {
super('ng1', elementRef, injector); super('ng1', elementRef, injector);
} }
} }
// This is an Angular component that will be downgraded // This is an Angular component that will be downgraded
@Component({ @Component({
selector: 'ng2', selector: 'ng2',
template: 'ng2[<ng1 [title]="nameProp">transclude</ng1>](<ng-content></ng-content>)' template: 'ng2[<ng1 [title]="nameProp">transclude</ng1>](<ng-content></ng-content>)'
}) })
class Ng2Component { class Ng2Component {
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
@Input('name') nameProp !: string; @Input('name') nameProp !: string;
} }
// This module represents the Angular pieces of the application // This module represents the Angular pieces of the application
@NgModule({ @NgModule({
declarations: [Ng1Component, Ng2Component], declarations: [Ng1Component, Ng2Component],
entryComponents: [Ng2Component], entryComponents: [Ng2Component],
imports: [BrowserModule, UpgradeModule] imports: [BrowserModule, UpgradeModule]
}) })
class Ng2Module { class Ng2Module {
ngDoBootstrap() { /* this is a placeholder to stop the bootstrapper from ngDoBootstrap() { /* this is a placeholder to stop the bootstrapper from
complaining */ complaining */
} }
} }
// This module represents the AngularJS pieces of the application // This module represents the AngularJS pieces of the application
const ng1Module = const ng1Module =
angular angular
.module('myExample', []) .module('myExample', [])
// This is an AngularJS component that will be upgraded // This is an AngularJS component that will be upgraded
.directive( .directive(
'ng1', 'ng1',
() => { () => {
return { return {
scope: {title: '='}, scope: {title: '='},
transclude: true, transclude: true,
template: 'ng1[Hello {{title}}!](<span ng-transclude></span>)' template: 'ng1[Hello {{title}}!](<span ng-transclude></span>)'
}; };
}) })
// This is wrapping (downgrading) an Angular component to be used in // This is wrapping (downgrading) an Angular component to be used in
// AngularJS // AngularJS
.directive('ng2', downgradeComponent({component: Ng2Component})); .directive('ng2', downgradeComponent({component: Ng2Component}));
// This is the (AngularJS) application bootstrap element // This is the (AngularJS) application bootstrap element
// Notice that it is actually a downgraded Angular component // Notice that it is actually a downgraded Angular component
const element = html('<ng2 name="World">project</ng2>'); const element = html('<ng2 name="World">project</ng2>');
// Let's use a helper function to make this simpler // Let's use a helper function to make this simpler
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => { bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => {
expect(multiTrim(element.textContent)) expect(multiTrim(element.textContent))
.toBe('ng2[ng1[Hello World!](transclude)](project)'); .toBe('ng2[ng1[Hello World!](transclude)](project)');
}); });
})); }));
}); });
}); });