GeezerVison is showing github.com — feat(compiler-cli): add ignoredBinding check by Flarette · Pull Request #71882 · angular/angular
Conversation
Flarette
commented
Nov 8, 2026
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
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?
<ng-container>, <ng-template>, and <ng-content> don't render an element, so Angular ignores
class, style, attribute, and event bindings on them, and every binding on <ng-content>. These
templates compile and run without a word:
<ng-container class="card" [class.active]="isActive" (click)="select()">...</ng-container>
<ng-content selector="h1" />
Issue Number: #71881
See also: #69322, which this PR also covers: unclaimed property bindings on an explicit
<ng-template> get NG8031.
What is the new behavior?
This is step 1 and changes no behavior that works today. Two breaking PRs build on it, one per
diagnostic, so each can land on its own: #71883 makes NG8032 an error, and #71884 makes NG8031 an
error.
-
NG8031 ignoredBinding, a type-check diagnostic. It needs strictTemplates, is
a warning by default, and is configured through extendedDiagnostics.checks, like NG8011:
warning NG8031: The '[class.active]' binding has no effect on <ng-container>, because
<ng-container> doesn't render an element. Move the binding to an element.
This check can be disabled using the
`extendedDiagnostics.checks.ignoredBinding = "suppress"` compiler option.
It doesn't report bindings that a directive claims, bindings whose name a matched directive's
selector uses (they make the directive match), directive host bindings, global event targets, or
static attributes that a directive, a structural directive, a content projection
slot, or Angular itself (ngProjectAs, select, ngNonBindable, ngPreserveWhitespaces,
ngSkipHydration) reads.
-
NG8032, a template type-check warning for a component whose selector matches
<ng-container> or <ng-template>:
warning NG8032: Component 'Card' matches <ng-container>, but <ng-container> can't host a component
because it doesn't render an element. Use an element that matches the component's selector
instead.
Today this compiles and fails only when the template renders: dev mode throws an assertion
("tags cannot be used as component hosts"), and production mode throws a DOM
HierarchyRequestError when the component's view is attached to a comment node (measured in
domino, Chromium, and Firefox). It's a warning here because a template that never renders the
element, such as one behind an @if that is always false, runs today. feat(compiler-cli): make NG8032 an error #71883 makes it an error.
Selectorless <MyComp:ng-container> is already a parse error.
-
NG0319, a dev-mode console.error for class, style, and event bindings on
<ng-container> and <ng-template>, so JIT is covered too. It's tree-shaken in production.
-
<svg> fix: [attr.x] on an <ng-container> inside <svg> now gets its parse error
(NG5002), like everywhere else. It failed at runtime before, so nothing that worked breaks.
-
The language service no longer suggests DOM attributes on <ng-container>.
-
Error guides for NG8031, NG8032, and NG0319.
Does this PR introduce a breaking change?
Other information
- The codes are negative (
-8031, -8032, -319) because they have guides.
- NG8031 runs as a type-check-block op,
TcbIgnoredBindingsOp, next to NG8011's
TcbControlFlowContentProjectionOp, and is wired through the same config path. The preprocessor
backend reports it too.
- The message builder lives in
@angular/compiler and both backends' recorders use it.
- Known gaps, all on the under-reporting side:
- Legacy
[@trigger] bindings and (@trigger.done) callbacks aren't reported at compile time.
At runtime, (@trigger.done) on these elements logs NG0319.
- A root of a multi-root
@if or @for block inside a component's content counts as
projected when its attributes match a slot, although Angular projects the whole block into
the default slot. NG8011 already reports that mistake, so NG8031 doesn't add a second
report for the same attribute.
- An unclaimed event on
<ng-container> gets both NG8030 (when strictUnclaimedEventNames is
on) and NG8031. feat(compiler-cli): make NG8031 an error #71884 removes the double report.
- NG8031 found
<ng-content selector="h1"> (meant: select) in packages/examples/upgrade,
where it's been since 2017. This PR fixes it, because the repo's build fails on warnings. All
172 ng_project targets in the repo build with this change.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
<ng-container>, <ng-template>, and <ng-content> don't render an
element, so Angular ignores class, style, attribute, and event bindings
on them, and every binding on <ng-content>. These templates compile and
run without a word, although the bindings never take effect.
Add NG8031 (`ignoredBinding`), a type-check diagnostic that
reports these bindings and static attributes. It needs strictTemplates,
is a warning by default, and is configured through
`extendedDiagnostics.checks` like NG8011. It doesn't report:
- bindings that a directive input or output claims;
- directive host bindings, since a directive is reusable;
- static attributes when a directive or a structural directive matched
the element, a content projection slot selects it, or Angular reads
the attribute;
- global event targets such as `document:` and `window:`.
Add NG0319, a dev-mode runtime error that logs class, style, and event
bindings on <ng-container> and <ng-template>, so JIT is covered too.
Add NG8032, a template type-check warning for a component whose
selector matches <ng-container> or <ng-template>. Neither can host a
component: dev mode throws an assertion, and production mode throws a
DOM error when it attaches the component's view to a comment node.
It's a warning for now, because a template that never renders the
element, for example behind an `@if` that is always false, runs today.
The language service no longer suggests DOM attributes on
<ng-container>.
Also report `[attr.x]` on an <ng-container> inside <svg>. The parse
check missed it there, so the binding failed later with a misleading
assertion in dev mode. Templates that hit the new parse error never
worked.
Also fix `<ng-content selector="h1">` in the upgrade examples, which
meant `select`. NG8031 found it, and the repo's build fails on
warnings.
This was referenced Nov 8, 2026
This was referenced Nov 8, 2026
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Comment on lines
+107
to
+109
|
|
'bindings on ng-container, ng-template, and ng-content bindings that take effect should not report static attributes on ng-container when a selectorless directive applies',
|
|
|
'bindings on ng-container, ng-template, and ng-content bindings that take effect should not report static attributes on ng-template when a selectorless directive applies',
|
|
|
'bindings on ng-container, ng-template, and ng-content content projection should not report a static attribute that a selectorless component slot selects',
|
JeanMeche
Nov 8, 2026
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is actually an issue. It is possible that we won't be supporting extended diagnotics that relying on typechecking in the new compiler infrastructure.
Also those exclusing are not located in the right part of this file.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Flarette
Nov 8, 2026
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. NG8031 doesn’t use TypeScript types. It needs the template, the matched directives, and the component’s ng-content selectors, and it runs during TCB generation like NG8011 because that’s where those are available. The preprocessor backend reports it too (preprocessor/src/oob.ts), and the whole NG8031 spec passes on ngtsc_ngp except these three selectorless rows. I’ll move them to NGP_KNOWN_NGTSC_FAILURES as a selectorless gap. If the new infrastructure won’t keep TCB-time checks, I can move NG8031 to a template pass over the bound target, since it needs no type information. Where would you prefer it to live?
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
|
* <ng-container [class.active]="isActive" (click)="select()"></ng-container>
|
|
|
* ```
|
|
|
*/
|
|
|
IGNORED_BINDING = -8031,
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This reminds me also that we have #69640 in the pipe
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#69640 is a special case of what this PR implements in general: every binding that <ng-container>, <ng-template>, and <ng-content> ignore (#71881).
Covered here on <ng-template> but not in #69640:
should report a class binding on ng-template ([class.p])
should report an attribute binding on ng-template ([attr.title])
should not report a class binding on ng-template, … a style binding …, … an attribute binding …: on <ng-template> these compile to property bindings, so a directive input named p claims [class.p], [style.p], and [attr.p]
should not report ngPreserveWhitespaces on ng-template and should not report ngSkipHydration on ng-template
should not report static attributes on ng-template when a selectorless directive applies
should report a component on ng-template (NG8032)
The same rules apply to <ng-container> and <ng-content>; event, animation, and static-attribute bindings are tested on <ng-container>.
|
#69640 |
this PR |
| Code |
NG8002 |
NG8031 (ignoredBinding) |
| Turned on by |
new strictNgTemplateTypes |
strictTemplates |
| Severity |
error |
warning; error in #71884; configurable |
| Expression type-checked |
yes |
no |
| Bindings covered |
properties on <ng-template> |
also class, style, events, static attributes, <ng-container>, <ng-content> |
- Why not NG8002:
- "Can't bind to 'x' since it isn't a known property of 'ng-template'" is misleading:
<ng-template> has no DOM node, so it has no properties at all. NG8031 says the binding is ignored and suggests importing the directive that should claim it.
- NG8002 also suggests
NO_ERRORS_SCHEMA, which hides the error while the binding still does nothing.
- Why no type-check: the binding already gets NG8031, so a TS2339 would be a second error on a binding that has to be removed, or claimed by an imported directive that then type-checks it.
How would you like to handle <ng-template> given the overlap with #69640?
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
This overlaps with #70808.
Yes, this overlaps with #70808 / #71844, Those are special cases of what this PR implements in general: every binding that <ng-container>, <ng-template>, and <ng-content> ignore (#71881).
#70808 / #71844, class bindings on <ng-container>, covered in ignored_bindings_spec.ts:
should report a class binding on ng-container ([class.p])
should report a class map binding on ng-container ([class])
should report a class interpolation on ng-container (class="{{x}}")
should not report a class map binding that a directive input claims
Additionaly, this feature also reports, beyond class bindings on <ng-container>:
- Other bindings on
<ng-container>:
should report a style map binding on ng-container ([style])
should report an event binding on ng-container ((click))
should report an animation binding on ng-container ([animate.enter])
should report an animation event on ng-container ((animate.leave))
should report a class binding on ng-container inside svg
- Static attributes that nothing reads, while keeping the ones that work:
should report a static attribute on ng-container (class="s")
should report an attribute that i18n translates on ng-container
should not report a static class that a projection slot selects (select=".card-body")
should not report static attributes when a directive matches
should not report a static attribute that a directive can inject (HostAttributeToken)
should report a static attribute that no projection slot selects
<ng-template>:
should report a class binding on ng-template
should report an attribute binding on ng-template
should report an unclaimed property binding on ng-template
should report an unclaimed two-way binding on ng-template once
<ng-content>:
should report a binding on ng-content ([foo])
should report each static attribute on ng-content (class, title)
should report static attributes on ng-content projected into a catch-all slot
- Bindings that make a directive match:
should not report an event that a directive selector matches
- Components on these elements (NG8032):
should report a component on ng-container, should report a component on ng-template
- Under
NO_ERRORS_SCHEMA: should report bindings under NO_ERRORS_SCHEMA
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Don't report a property or event binding whose name appears in the
selector of a directive that matched by that selector. Directive
matching uses these binding names, so the binding makes the directive
match even when no input or output claims it, as in
`<ng-template [foobar]>` with a `[foobar]` selector and no input.
Host directives aren't matched by their selector, so their selectors
don't count.
Also add tests for `[class]` and class interpolation on <ng-container>,
an event binding on <ng-template>, a static class that a projection
slot selects, and <ng-template> inside <svg> and inside a namespaced
element.
Move the three selectorless NG8031 tests from the intentional ngp
differences to the known ngp failures, in their own group. They fail
on ngp with NG2024, because ngp doesn't resolve selectorless
dependencies yet.
This was referenced Nov 8, 2026
JeanMeche
commented
Nov 8, 2026
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
|
Ran some internal tests, this change is massively breaking. This is expected because internally warnings are treated as errors.
The few examples I saw seemed legit to me. (selection on ng-content, missing imports for directives on ng-container etc).
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
JeanMeche
commented
Nov 8, 2026
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
|
Okay, first false positive : ng-content should allow staticclass attributes (because of content projection).
cf the dedicated unit test:
|
'INNER(<innerinner><ng-content select=".left" class="left"></ng-content><ng-content></ng-content></innerinner>)', |
The same applies for ng-container.
|
'<selector-proj><ng-container class="x">Hello world!</ng-container></selector-proj>', |
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Ran some internal tests, this change is massively breaking.
Thanks for running it. The goal is to flag only code that does nothing at runtime, so builds break but working code doesn't. For example, selection (meant: select) is ignored, and a directive that isn't imported never runs. Did you find any case where the flagged code actually works?
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Okay, first false positive : ng-content should allow staticclass attributes (because of content projection).
Both linked tests compile without NG8031 on this PR. A static class isn't reported when it takes effect:
- on
<ng-content> inside a component that has a named slot, because it can be projected again
- on
<ng-container> when a slot or a directive selects it
It's reported only when nothing reads it, so it never reaches the DOM. Can you share the template that was flagged internally?
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Match an <ng-content> in a component's content against the
component's specific projection slots, as <ng-container> already is,
instead of keeping all of its static attributes. A slot sees only the
attributes and classes of a projected <ng-content>, because it has no
tag name at runtime. An attribute that no specific slot selects lands
the content in the default slot, so it has no effect and is reported.
Components with unknown slots still keep every attribute.
|
We can consider adding a migration that outright deletes all the dead code flagged since it was never running. However I wonder if leaving it as is would help teams to potentially discover bugs they missed.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Flarette
commented
Nov 9, 2026
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
|
Added tests for the class case in e55d644 and cebec12 (ignored_bindings_spec.ts):
should not report a static class on ng-content that a projection slot selects: <proj><ng-content class="card-body" /></proj>, where proj has a .card-body slot.
should report other static attributes on ng-content that a projection slot selects: title in <proj><ng-content class="card-body" title="t" /></proj>.
should report only the static attributes that change the projection slot: covers selector order, [c][d], and :not().
<ng-content> is now matched against the component's slots, as <ng-container> already is. On <ng-container>, <ng-template>, and <ng-content>, only the attributes that decide which slot selects the node are kept; the rest have no effect and are reported. Rendered elements and component hosts are never reported, whichever slot they land in.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Keep only the static attributes that decide which content projection
slot selects an <ng-container>, <ng-template>, or <ng-content>, rather
than every attribute of a node that a slot selects. These elements
don't render, so an attribute that doesn't change the slot has no
effect. An attribute decides the slot if the node lands in a different
slot without it, which also covers an attribute that keeps a node out
of a `:not()` slot.
Also add tests that rendered elements in a component's content are
never reported, whichever slot they land in.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for adding the extra tests and looking into this. However, I believe the current approach in TCB still has fundamental architectural issues and will continue to produce false positives:
-
Static class attributes should simply not be reported:
Classes on <ng-container> and <ng-content> are widely and legitimately used for content projection (e.g. <proj><ng-content class="card-body" /></proj>). Attempting to flag static classes when they "aren't read" is overly aggressive and causes massive breakages across existing codebases.
-
Simulating content projection in TCB is fragile:
The current heuristic (findProjectionSlot / removeAttributeFromSelector) tests whether mutating attributes changes the matched projection slot, but it only checks immediate root.children in the same template. In practice, content projection frequently crosses boundaries:
- Elements inside structural blocks (
@if, @for, *ngIf).
- Templates passed to components via inputs or
[ngTemplateOutlet] (e.g. <my-tab [header]="headerTpl">).
- Components imported from external libraries (
.d.ts).
In these common scenarios, the TCB cannot know what projection slot will capture the content at runtime, resulting in false positives. Re-implementing CSS selector mutation math inside TcbIgnoredBindingsOp introduces ongoing maintenance burden and edge cases.
-
Conflating static attributes with dynamic bindings:
There is a clear distinction between:
- Invalid dynamic bindings:
(click) or [id] on <ng-content>, or [attr.*] on <ng-template>. These have zero runtime effect, represent actual developer confusion, and can be cleanly rejected.
- Static attributes: Passive metadata (
class, data-testid, role) used by test harnesses (Playwright, Cypress, Karma), scrapers, or projection. Attempting to ensure every static attribute is read by a directive or projection slot is too restrictive.
-
Right layer for validation:
Validating virtual element bindings is a template-semantic concern, not a type-check concern. Validating this during template transformation (in r3_template_transform.ts, as in #71844 / #71887) catches the truly invalid dynamic bindings in a few dozen lines of code:
- Works regardless of
strictTemplates.
- Requires zero complex projection heuristics or TCB ops.
- Avoids adding runtime overhead (
NG0319) and diverging from ngp (which currently doesn't implement this).
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
I'll take over from there with #71887. It'll require a large clean-up on ourside before we can land it.
In any way, I think we'll only ship this in v23.
|
Flarette reacted with thumbs up emoji
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Reviewers
crisbeto
Awaiting requested review from crisbeto
Uh oh!
There was an error while loading. Please reload this page.
Assignees
No one assigned
Development
Successfully merging this pull request may close these issues.
Uh oh!
There was an error while loading. Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
|