Skip to content

Commit feebe03

Browse files
petebacondarwinAndrewKushnir
authored andcommitted
fix(ivy): pass ngContentSelectors through to defineComponent() calls (angular#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 angular#27867
1 parent e8a57f0 commit feebe03

File tree

12 files changed

+316
-277
lines changed

12 files changed

+316
-277
lines changed

packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1144,6 +1144,7 @@ describe('compiler compliance', () => {
11441144
type: SimpleComponent,
11451145
selectors: [["simple"]],
11461146
factory: function SimpleComponent_Factory(t) { return new (t || SimpleComponent)(); },
1147+
ngContentSelectors: _c0,
11471148
consts: 2,
11481149
vars: 0,
11491150
template: function SimpleComponent_Template(rf, ctx) {
@@ -1167,6 +1168,7 @@ describe('compiler compliance', () => {
11671168
type: ComplexComponent,
11681169
selectors: [["complex"]],
11691170
factory: function ComplexComponent_Factory(t) { return new (t || ComplexComponent)(); },
1171+
ngContentSelectors: _c4,
11701172
consts: 4,
11711173
vars: 0,
11721174
template: function ComplexComponent_Template(rf, ctx) {
@@ -1561,6 +1563,7 @@ describe('compiler compliance', () => {
15611563
($r3$.ɵqueryRefresh(($tmp$ = $r3$.ɵloadQueryList(queryStartIndex))) && ($instance$.someDir = $tmp$.first));
15621564
($r3$.ɵqueryRefresh(($tmp$ = $r3$.ɵloadQueryList((queryStartIndex + 1)))) && ($instance$.someDirList = $tmp$));
15631565
},
1566+
ngContentSelectors: _c0,
15641567
consts: 2,
15651568
vars: 0,
15661569
template: function ContentQueryComponent_Template(rf, ctx) {

packages/compiler/src/render3/view/compiler.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,13 @@ export function compileComponentFromMetadata(
264264

265265
const templateFunctionExpression = templateBuilder.buildTemplateFunction(template.nodes, []);
266266

267+
// We need to provide this so that dynamically generated components know what
268+
// projected content blocks to pass through to the component when it is instantiated.
269+
const ngContentSelectors = templateBuilder.getNgContentSelectors();
270+
if (ngContentSelectors) {
271+
definitionMap.set('ngContentSelectors', ngContentSelectors);
272+
}
273+
267274
// e.g. `consts: 2`
268275
definitionMap.set('consts', o.literal(templateBuilder.getConstCount()));
269276

packages/compiler/src/render3/view/template.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -915,6 +915,12 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
915915

916916
getVarCount() { return this._pureFunctionSlots; }
917917

918+
getNgContentSelectors(): o.Expression|null {
919+
return this._hasNgContent ?
920+
this.constantPool.getConstLiteral(asLiteral(this._ngContentSelectors), true) :
921+
null;
922+
}
923+
918924
private bindingContext() { return `${this._bindingContext++}`; }
919925

920926
// Bindings must only be resolved after all local refs have been visited, so all

packages/core/src/render3/component_ref.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,10 @@ export class ComponentFactory<T> extends viewEngine_ComponentFactory<T> {
119119
super();
120120
this.componentType = componentDef.type;
121121
this.selector = componentDef.selectors[0][0] as string;
122-
this.ngContentSelectors = [];
122+
// The component definition does not include the wildcard ('*') selector in its list.
123+
// It is implicitly expected as the first item in the projectable nodes array.
124+
this.ngContentSelectors =
125+
componentDef.ngContentSelectors ? ['*', ...componentDef.ngContentSelectors] : [];
123126
}
124127

125128
create(

packages/core/src/render3/definition.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,11 @@ export function defineComponent<T>(componentDefinition: {
182182
*/
183183
template: ComponentTemplate<T>;
184184

185+
/**
186+
* An array of `ngContent[selector]` values that were found in the template.
187+
*/
188+
ngContentSelectors?: string[];
189+
185190
/**
186191
* Additional set of instructions specific to view query processing. This could be seen as a
187192
* set of instruction to be inserted into the template function.
@@ -249,6 +254,7 @@ export function defineComponent<T>(componentDefinition: {
249254
vars: componentDefinition.vars,
250255
factory: componentDefinition.factory,
251256
template: componentDefinition.template || null !,
257+
ngContentSelectors: componentDefinition.ngContentSelectors,
252258
hostBindings: componentDefinition.hostBindings || null,
253259
contentQueries: componentDefinition.contentQueries || null,
254260
contentQueriesRefresh: componentDefinition.contentQueriesRefresh || null,

packages/core/src/render3/interfaces/definition.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,11 @@ export interface ComponentDef<T> extends DirectiveDef<T> {
189189
*/
190190
readonly template: ComponentTemplate<T>;
191191

192+
/**
193+
* An array of `ngContent[selector]` values that were found in the template.
194+
*/
195+
readonly ngContentSelectors?: string[];
196+
192197
/**
193198
* A set of styles that the component needs to be present for component to render correctly.
194199
*/

packages/core/test/render3/component_ref_spec.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,28 @@ describe('ComponentFactory', () => {
1818
const cfr = injectComponentFactoryResolver();
1919

2020
describe('constructor()', () => {
21-
it('should correctly populate public properties', () => {
21+
it('should correctly populate default properties', () => {
22+
class TestComponent {
23+
static ngComponentDef = defineComponent({
24+
type: TestComponent,
25+
selectors: [['test', 'foo'], ['bar']],
26+
consts: 0,
27+
vars: 0,
28+
template: () => undefined,
29+
factory: () => new TestComponent(),
30+
});
31+
}
32+
33+
const cf = cfr.resolveComponentFactory(TestComponent);
34+
35+
expect(cf.selector).toBe('test');
36+
expect(cf.componentType).toBe(TestComponent);
37+
expect(cf.ngContentSelectors).toEqual([]);
38+
expect(cf.inputs).toEqual([]);
39+
expect(cf.outputs).toEqual([]);
40+
});
41+
42+
it('should correctly populate defined properties', () => {
2243
class TestComponent {
2344
static ngComponentDef = defineComponent({
2445
type: TestComponent,
@@ -27,6 +48,7 @@ describe('ComponentFactory', () => {
2748
consts: 0,
2849
vars: 0,
2950
template: () => undefined,
51+
ngContentSelectors: ['a', 'b'],
3052
factory: () => new TestComponent(),
3153
inputs: {
3254
in1: 'in1',
@@ -42,7 +64,7 @@ describe('ComponentFactory', () => {
4264
const cf = cfr.resolveComponentFactory(TestComponent);
4365

4466
expect(cf.componentType).toBe(TestComponent);
45-
expect(cf.ngContentSelectors).toEqual([]);
67+
expect(cf.ngContentSelectors).toEqual(['*', 'a', 'b']);
4668
expect(cf.selector).toBe('test');
4769

4870
expect(cf.inputs).toEqual([

0 commit comments

Comments
 (0)