Repository navigation
Conversation
<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 overlaps with #70808. |
| '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', |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
| * <ng-container [class.active]="isActive" (click)="select()"></ng-container> | ||
| * ``` | ||
| */ | ||
| IGNORED_BINDING = -8031, |
There was a problem hiding this comment.
This reminds me also that we have #69640 in the pipe
There was a problem hiding this comment.
#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 namedpclaims[class.p],[style.p], and[attr.p]should not report ngPreserveWhitespaces on ng-templateandshould not report ngSkipHydration on ng-templateshould not report static attributes on ng-template when a selectorless directive appliesshould 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.
- "Can't bind to 'x' since it isn't a known property of 'ng-template'" is misleading:
- 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?
Yes, this overlaps with #70808 / #71844, Those are special cases of what this PR implements in general: every binding that #70808 / #71844, class bindings on
Additionaly, this feature also reports, beyond class bindings on
|
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.
|
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. ( |
|
Okay, first false positive : cf the dedicated unit test: The same applies for |
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, |
Both linked tests compile without NG8031 on this PR. A static
It's reported only when nothing reads it, so it never reaches the DOM. Can you share the template that was flagged internally? |
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. |
|
Added tests for the
|
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.
JeanMeche
left a comment
There was a problem hiding this comment.
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
classattributes 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 immediateroot.childrenin 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 insideTcbIgnoredBindingsOpintroduces ongoing maintenance burden and edge cases.
- Elements inside structural blocks (
-
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.
- Invalid dynamic bindings:
-
Right layer for validation:
Validating virtual element bindings is a template-semantic concern, not a type-check concern. Validating this during template transformation (inr3_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 fromngp(which currently doesn't implement this).
- Works regardless of
|
I'll take over from there with #71887. It'll require a large clean-up on ourside before we can land it. |
PR Checklist
Please check if your PR fulfills the following requirements:
PR Type
What kind of change does this PR introduce?
What is the current behavior?
<ng-container>,<ng-template>, and<ng-content>don't render an element, so Angular ignoresclass, style, attribute, and event bindings on them, and every binding on
<ng-content>. Thesetemplates compile and run without a word:
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 needsstrictTemplates, isa warning by default, and is configured through
extendedDiagnostics.checks, like NG8011: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>: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
HierarchyRequestErrorwhen the component's view is attached to a comment node (measured indomino, Chromium, and Firefox). It's a warning here because a template that never renders the
element, such as one behind an
@ifthat 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.errorfor 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
-8031,-8032,-319) because they have guides.TcbIgnoredBindingsOp, next to NG8011'sTcbControlFlowContentProjectionOp, and is wired through the same config path. The preprocessorbackend reports it too.
@angular/compilerand both backends' recorders use it.[@trigger]bindings and(@trigger.done)callbacks aren't reported at compile time.At runtime,
(@trigger.done)on these elements logs NG0319.@ifor@forblock inside a component's content counts asprojected 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.
<ng-container>gets both NG8030 (whenstrictUnclaimedEventNamesison) and NG8031. feat(compiler-cli): make NG8031 an error #71884 removes the double report.
<ng-content selector="h1">(meant:select) inpackages/examples/upgrade,where it's been since 2017. This PR fixes it, because the repo's build fails on warnings. All
172
ng_projecttargets in the repo build with this change.