FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

fix(compiler): match namespaced root nodes of control flow blocks against ng-content tag selectors by aminesbdev · Pull Request #71163 · angular/angular · GitHub

fix(compiler): match namespaced root nodes of control flow blocks against ng-content tag selectors - #71163

Open
aminesbdev wants to merge 1 commit into
angular:mainfrom
aminesbdev:fix/compiler-svg-control-flow-projection
Open

aminesbdev wants to merge 1 commit into
angular:mainfrom
aminesbdev:fix/compiler-svg-control-flow-projection

Conversation

aminesbdev commented Oct 3, 2026 •
edited
Loading

Copy link
Copy Markdown
Contributor

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Documentation content changes
  • angular.dev application / infrastructure changes
  • Other... Please describe:

What is the current behavior?

When an <svg> or <math> element is the only root node of an @if, @for or @switch branch, it isn't projected into an ng-content slot with a tag selector such as select="svg". It goes to the default slot instead. The compiler passes the root's tag name to ɵɵconditionalCreate / ɵɵrepeaterCreate with its namespace prefix (":svg:svg"), so the runtime never matches it against svg.

Issue Number: Fixes #71162

What is the new behavior?

The namespace prefix is stripped in ingestControlFlowInsertionPoint, the same way it already is for regular elements and structural templates, so the generated tag name is "svg" / "math" and the block lands in the right slot.

I added acceptance tests for @if/@else, @for/@empty and @switch with SVG and MathML roots, plus one with an <svg *ngIf> root since that goes through the template branch of the same code. There's also a compliance test that checks the emitted tag names and that the branch templates still call ɵɵnamespaceSVG/ɵɵnamespaceMathML. Without the change in ingest.ts, all six new tests fail.

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

Strictly speaking, apps that rely on the current behavior (an <svg> root inside a control flow block ending up in the default slot next to a select="svg" slot) will see it move to the svg slot, which is where it would go without the control flow block. The workaround people may have used, ngProjectAs="svg", keeps working.

pullapprove Bot requested review from JeanMeche and JoostK October 3, 2026 20:24
angular-robot Bot added the area: compiler Issues related to `ngc`, Angular's template compiler label Oct 3, 2026
ngbot Bot added this to the Backlog milestone Oct 3, 2026
}

const fixture = TestBed.createComponent(App);
fixture.autoDetectChanges();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

NIT:

Suggested change
fixture.autoDetectChanges();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Thanks for taking a look! I tried removing it, but this file runs with provideZoneChangeDetection() (in the beforeEach at the top), so unlike the @if/@switch specs the fixture doesn't render on its own. Without autoDetectChanges() the test fails with Expected 'svg: (), math: ()' to be 'svg: (12), math: ()'.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Maybe we can replace it with detectChanges in that case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

done

aminesbdev force-pushed the fix/compiler-svg-control-flow-projection branch from 9a385a0 to 139c97c Compare October 3, 2026 20:32

Copy link
Copy Markdown
Member

I was looking at this change and noticed a potential bug that will cause the Angular compiler to crash with a TypeError on valid template syntax.

In packages/compiler/src/template/pipeline/src/ingest.ts, the new logic assumes root.tagName is always a string:

const [, tagName] = splitNsName(root instanceof t.Element ? root.name : root.tagName!);

However, if a developer places a structural directive directly on an <ng-template> at the root of a control flow block, root.tagName is null. As documented in packages/compiler/src/render3/r3_ast.ts, tagName is explicitly set to null to distinguish synthetic templates from actual <ng-template> elements.

The Crash

Because of the non-null assertion (!), splitNsName(null) gets called. Inside splitNsName (in packages/compiler/src/ml_parser/tags.ts), it evaluates if (elementName[0] != ':'), throwing a TypeError: Cannot read properties of null (reading '0') and crashing the compiler.

Failing Test Case

Here is a test case (e.g. for packages/core/test/acceptance/control_flow_if_spec.ts) that demonstrates the crash during compilation:

it('should not crash the compiler when an @if block root is an ng-template with a structural directive', () => {
  @Component({
    selector: 'test-cmp',
    template: 'Main: <ng-content></ng-content> Slot: <ng-content select="svg"></ng-content>',
  })
  class TestComponent {}

  @Component({
    imports: [TestComponent, NgIf],
    template: `
      <test-cmp>
        @if (true) {
          <ng-template *ngIf="true">
            <svg><text>foo</text></svg>
          </ng-template>
        }
      </test-cmp>
    `,
  })
  class App {}

  // This will throw TypeError: Cannot read properties of null (reading '0')
  expect(() => TestBed.createComponent(App)).not.toThrow();
});

Suggested Fix

You might want to handle the null case gracefully before passing it to splitNsName, similar to how the old code handled it:

const rawTagName = root instanceof t.Element ? root.name : root.tagName;
const tagName = rawTagName ? splitNsName(rawTagName)[1] : null;

// Don't pass along `ng-template` tag name since it enables directive matching.
return tagName === NG_TEMPLATE_TAG_NAME ? null : tagName;

Copy link
Copy Markdown
Contributor Author

I was looking at this change and noticed a potential bug that will cause the Angular compiler to crash with a TypeError on valid template syntax.

In packages/compiler/src/template/pipeline/src/ingest.ts, the new logic assumes root.tagName is always a string:

const [, tagName] = splitNsName(root instanceof t.Element ? root.name : root.tagName!);

However, if a developer places a structural directive directly on an <ng-template> at the root of a control flow block, root.tagName is null. As documented in packages/compiler/src/render3/r3_ast.ts, tagName is explicitly set to null to distinguish synthetic templates from actual <ng-template> elements.

The Crash

Because of the non-null assertion (!), splitNsName(null) gets called. Inside splitNsName (in packages/compiler/src/ml_parser/tags.ts), it evaluates if (elementName[0] != ':'), throwing a TypeError: Cannot read properties of null (reading '0') and crashing the compiler.

Failing Test Case

Here is a test case (e.g. for packages/core/test/acceptance/control_flow_if_spec.ts) that demonstrates the crash during compilation:

it('should not crash the compiler when an @if block root is an ng-template with a structural directive', () => {
  @Component({
    selector: 'test-cmp',
    template: 'Main: <ng-content></ng-content> Slot: <ng-content select="svg"></ng-content>',
  })
  class TestComponent {}

  @Component({
    imports: [TestComponent, NgIf],
    template: `
      <test-cmp>
        @if (true) {
          <ng-template *ngIf="true">
            <svg><text>foo</text></svg>
          </ng-template>
        }
      </test-cmp>
    `,
  })
  class App {}

  // This will throw TypeError: Cannot read properties of null (reading '0')
  expect(() => TestBed.createComponent(App)).not.toThrow();
});

Suggested Fix

You might want to handle the null case gracefully before passing it to splitNsName, similar to how the old code handled it:

const rawTagName = root instanceof t.Element ? root.name : root.tagName;
const tagName = rawTagName ? splitNsName(rawTagName)[1] : null;

// Don't pass along `ng-template` tag name since it enables directive matching.
return tagName === NG_TEMPLATE_TAG_NAME ? null : tagName;

Thanks for checking! I don't think it can get there though. A template only becomes root when its tagName isn't null (the check in the loop over node.children a few lines above), so for <ng-template *ngIf> the function returns null before splitNsName is called, same as before this change. I ran your test case against this branch to be sure and it passes, and the block renders the same as before.

aminesbdev force-pushed the fix/compiler-svg-control-flow-projection branch from 139c97c to cba7200 Compare October 3, 2026 20:54
…inst ng-content tag selectors

When a control flow block has a single root element, its tag name is copied onto
the block's template so the block can be projected into the matching
`ng-content` slot. The tag name was copied with its namespace prefix, so an
`<svg>` or `<math>` root ended up as `:svg:svg` or `:math:math` and never
matched a tag selector like `select="svg"`, falling back to the default slot.

Strip the namespace prefix like we already do for regular elements and
structural templates.

Fixes angular#71162
aminesbdev force-pushed the fix/compiler-svg-control-flow-projection branch from cba7200 to 43c8722 Compare October 3, 2026 21:17

Copy link
Copy Markdown
Contributor Author

I was looking at this change and noticed a potential bug that will cause the Angular compiler to crash with a TypeError on valid template syntax.
In packages/compiler/src/template/pipeline/src/ingest.ts, the new logic assumes root.tagName is always a string:

const [, tagName] = splitNsName(root instanceof t.Element ? root.name : root.tagName!);

However, if a developer places a structural directive directly on an <ng-template> at the root of a control flow block, root.tagName is null. As documented in packages/compiler/src/render3/r3_ast.ts, tagName is explicitly set to null to distinguish synthetic templates from actual <ng-template> elements.

The Crash

Because of the non-null assertion (!), splitNsName(null) gets called. Inside splitNsName (in packages/compiler/src/ml_parser/tags.ts), it evaluates if (elementName[0] != ':'), throwing a TypeError: Cannot read properties of null (reading '0') and crashing the compiler.

Failing Test Case

Here is a test case (e.g. for packages/core/test/acceptance/control_flow_if_spec.ts) that demonstrates the crash during compilation:

it('should not crash the compiler when an @if block root is an ng-template with a structural directive', () => {
  @Component({
    selector: 'test-cmp',
    template: 'Main: <ng-content></ng-content> Slot: <ng-content select="svg"></ng-content>',
  })
  class TestComponent {}

  @Component({
    imports: [TestComponent, NgIf],
    template: `
      <test-cmp>
        @if (true) {
          <ng-template *ngIf="true">
            <svg><text>foo</text></svg>
          </ng-template>
        }
      </test-cmp>
    `,
  })
  class App {}

  // This will throw TypeError: Cannot read properties of null (reading '0')
  expect(() => TestBed.createComponent(App)).not.toThrow();
});

Suggested Fix

You might want to handle the null case gracefully before passing it to splitNsName, similar to how the old code handled it:

const rawTagName = root instanceof t.Element ? root.name : root.tagName;
const tagName = rawTagName ? splitNsName(rawTagName)[1] : null;

// Don't pass along `ng-template` tag name since it enables directive matching.
return tagName === NG_TEMPLATE_TAG_NAME ? null : tagName;

Thanks for checking! I don't think it can get there though. A template only becomes root when its tagName isn't null (the check in the loop over node.children a few lines above), so for <ng-template *ngIf> the function returns null before splitNsName is called, same as before this change. I ran your test case against this branch to be sure and it passes, and the block renders the same as before.

Pushed your version anyway, it reads better without the non-null assertion and doesn't depend on the check in the loop.

JoostK added action: merge The PR is ready for merge by the caretaker target: patch This PR is targeted for the next patch release action: presubmit The PR is in need of a google3 presubmit labels Oct 4, 2026
JeanMeche removed the action: presubmit The PR is in need of a google3 presubmit label Oct 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

action: merge The PR is ready for merge by the caretaker area: compiler Issues related to `ngc`, Angular's template compiler target: patch This PR is targeted for the next patch release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Inline <svg> at the root of a control flow block is not matched by an ng-content tag selector

4 participants


Back | FazBrowse Home | New Git URL