Skip to content

Commit e655e8a

Browse files
masaokipkozlowski-opensource
authored andcommitted
fix(core): more accurate matching of classes during content projection (#48888)
Showing a minimum app to reproduce the bug. 1. Create the app and add angular material. ``` ng new project cd project ng add @angular/material ``` 1. Overwrite the src/app/app.component.html with minimal content. ``` <button mat-button *ngIf="true"><span *ngIf="true" class="{{'a'}}"></span></button> ``` 1. Run the app. The button is not shown because of an exception. ``` main.ts:6 ERROR TypeError: item.toLowerCase is not a function at isCssClassMatching (core.mjs:8726:35) at isNodeMatchingSelector (core.mjs:8814:22) at isNodeMatchingSelectorList (core.mjs:8931:13) at matchingProjectionSlotIndex (core.mjs:14179:13) at Module.ɵɵprojectionDef (core.mjs:14222:49) at MatButton_Template (button.mjs:113:99) at executeTemplate (core.mjs:10534:9) at renderView (core.mjs:10356:13) at renderComponent (core.mjs:11529:5) at renderChildComponents (core.mjs:10216:9) ``` Because isCssClassMatching() function does not take care if the value is not string, while attrs[] may contain AttributeMarker which is actually numbers, item.toLowerCase() throws the exception. Just inserted a check if the item is string. Created a testcase for the original fix. It causes an exception without the fix. fix(core): add a check code to avoid an exception inside isCssClassMatching Showing a minimum app to reproduce the bug. 1. Create the app and add angular material. ``` ng new project cd project ng add @angular/material ``` 1. Add `import { MatButtonModule } from '@angular/material/button'`, and also MatButtonModule inside @NgModule imports in src/app/app.module.ts to use MatButtonModule. 1. Overwrite the src/app/app.component.html with minimal content. ``` <button mat-button *ngIf="true"><span *ngIf="true" class="{{'a'}}"></span></button> ``` 1. Run the app. The button is not shown because of an exception. ``` main.ts:6 ERROR TypeError: item.toLowerCase is not a function at isCssClassMatching (core.mjs:8726:35) at isNodeMatchingSelector (core.mjs:8814:22) at isNodeMatchingSelectorList (core.mjs:8931:13) at matchingProjectionSlotIndex (core.mjs:14179:13) at Module.ɵɵprojectionDef (core.mjs:14222:49) at MatButton_Template (button.mjs:113:99) at executeTemplate (core.mjs:10534:9) at renderView (core.mjs:10356:13) at renderComponent (core.mjs:11529:5) at renderChildComponents (core.mjs:10216:9) ``` Because isCssClassMatching() function does not take care if the value is not string, while attrs[] may contain AttributeMarker which is actually numbers, item.toLowerCase() throws the exception. Just inserted a check if the item is string. PR Close #48888
1 parent d5946bd commit e655e8a

File tree

2 files changed

+60
-4
lines changed

2 files changed

+60
-4
lines changed

packages/core/src/render3/node_selector_matcher.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,19 @@ function isCssClassMatching(
3535
assertEqual(
3636
cssClassToMatch, cssClassToMatch.toLowerCase(), 'Class name expected to be lowercase.');
3737
let i = 0;
38+
// Indicates whether we are processing value from the implicit
39+
// attribute section (i.e. before the first marker in the array).
40+
let isImplicitAttrsSection = true;
3841
while (i < attrs.length) {
3942
let item = attrs[i++];
40-
if (isProjectionMode && item === 'class') {
41-
item = attrs[i] as string;
42-
if (classIndexOf(item.toLowerCase(), cssClassToMatch, 0) !== -1) {
43-
return true;
43+
if (typeof item === 'string' && isImplicitAttrsSection) {
44+
const value = attrs[i++] as string;
45+
if (isProjectionMode && item === 'class') {
46+
// We found a `class` attribute in the implicit attribute section,
47+
// check if it matches the value of the `cssClassToMatch` argument.
48+
if (classIndexOf(value.toLowerCase(), cssClassToMatch, 0) !== -1) {
49+
return true;
50+
}
4451
}
4552
} else if (item === AttributeMarker.Classes) {
4653
// We found the classes section. Start searching for the class.
@@ -49,6 +56,10 @@ function isCssClassMatching(
4956
if (item.toLowerCase() === cssClassToMatch) return true;
5057
}
5158
return false;
59+
} else if (typeof item === 'number') {
60+
// We've came across a first marker, which indicates
61+
// that the implicit attribute section is over.
62+
isImplicitAttrsSection = false;
5263
}
5364
}
5465
return false;

packages/core/test/acceptance/content_spec.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1321,6 +1321,51 @@ describe('projection', () => {
13211321
expect(fixture.nativeElement).toHaveText('Hello world!');
13221322
expect(xDirectives).toEqual(1);
13231323
});
1324+
1325+
it('should work without exception when subelement has both ngIf and class as interpolation',
1326+
() => {
1327+
@Component(
1328+
{selector: 'child-comp', template: '<ng-content select=".nomatch"></ng-content>'})
1329+
class ChildComp {
1330+
}
1331+
1332+
@Component({
1333+
selector: 'parent-comp',
1334+
template: `<child-comp><span *ngIf="true" class="{{'a'}}"></span></child-comp>`
1335+
})
1336+
class ParentComp {
1337+
}
1338+
1339+
TestBed.configureTestingModule({declarations: [ParentComp, ChildComp]});
1340+
const fixture = TestBed.createComponent<ParentComp>(ParentComp);
1341+
1342+
fixture.detectChanges();
1343+
expect(fixture.nativeElement.innerHTML).toBe('<child-comp></child-comp>');
1344+
});
1345+
});
1346+
1347+
it('selection of child element should properly work even with confusing attribute names', () => {
1348+
@Component({selector: 'child-comp', template: '<ng-content select=".title"></ng-content>'})
1349+
class ChildComp {
1350+
}
1351+
1352+
@Component({
1353+
selector: 'parent-comp',
1354+
template:
1355+
`<child-comp><span *ngIf="true" id="5" jjj="class" class="{{'a'}}" [title]="'abc'"></span></child-comp>`
1356+
})
1357+
class ParentComp {
1358+
}
1359+
1360+
TestBed.configureTestingModule({declarations: [ParentComp, ChildComp]});
1361+
const fixture = TestBed.createComponent<ParentComp>(ParentComp);
1362+
1363+
fixture.detectChanges();
1364+
// tNode.attrs will be ['id', '5', 'jjj', 'class', 3 /* AttributeMarker.Bindings */, 'class',
1365+
// 'title', 4 /* AttributeMarker.Template */, 'ngIf'] isNodeMatchingSelector() must not
1366+
// confuse it as 'class=title' attribute. <ng-content select=".title"> should not match the
1367+
// child.
1368+
expect(fixture.nativeElement.innerHTML).toBe('<child-comp></child-comp>');
13241369
});
13251370
});
13261371
});

0 commit comments

Comments
 (0)