Skip to content

refactor(compiler): disallow unsupported bindings and attributes on ng-content and ng-template - #71887

Draft
JeanMeche wants to merge 1 commit into
angular:mainfrom
JeanMeche:compiler-virtual-element-bindings
Draft

JeanMeche wants to merge 1 commit into
angular:mainfrom
JeanMeche:compiler-virtual-element-bindings

Conversation

@JeanMeche

@JeanMeche JeanMeche commented Nov 8, 2026 •

Copy link
Copy Markdown
Member

This PR extends compiler and template type-checking validation for virtual elements to <ng-content> and <ng-template>:

  • Template transformation (r3_template_transform.ts):
    • <ng-template> does not support attribute bindings ([attr.foo]).
    • <ng-content> does not support property bindings ([id]), event bindings ((click)), or template references (#ref).
  • Type Check Block (TcbContentAttributesOp in ngtsc and ngp):
    • <ng-content> enforces an allowlist for static attributes:
      • select (case-insensitive)
      • ngProjectAs
      • ngSkipHydration
      • ngPreserveWhitespaces
    • Also allows static attributes on <ng-content> when re-projecting into a parent component if the attribute selects a specific content projection slot of that parent component.
    • If selection or selector is used on <ng-content> (and does not match a parent projection slot), a helpful suggestion is provided: 'selection'/'selector' is not supported on ng-content. Did you mean 'select'?.

Also removes legacy compliance tests in r3_view_compiler_template that asserted unsupported attribute and class bindings on <ng-template> degraded to property bindings, and fixes invalid attributes found in existing tests and examples where slot="foo", selector="...", or class="..." were mistakenly used on <ng-content>.

@angular-robot angular-robot Bot added the area: compiler Issues related to `ngc`, Angular's template compiler label Nov 8, 2026
@ngbot ngbot Bot added this to the Backlog milestone Nov 8, 2026
@JeanMeche
JeanMeche force-pushed the compiler-virtual-element-bindings branch from b7a8b82 to f3dfcfe Compare November 8, 2026 20:11
@JeanMeche
JeanMeche force-pushed the compiler-virtual-element-bindings branch from f3dfcfe to e99d42b Compare November 9, 2026 14:16
@JeanMeche JeanMeche changed the title refactor(compiler): disallow unsupported bindings on ng-content and ng-template refactor(compiler): disallow unsupported bindings and attributes on ng-content and ng-template Nov 9, 2026
@JeanMeche
JeanMeche force-pushed the compiler-virtual-element-bindings branch 6 times, most recently from fdcecd6 to af14208 Compare November 10, 2026 15:12
JeanMeche

This comment was marked as outdated.

@JeanMeche
JeanMeche force-pushed the compiler-virtual-element-bindings branch 2 times, most recently from dbf45c7 to 363c2ef Compare November 10, 2026 17:35

@JeanMeche JeanMeche left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Nice improvement! Splitting the validation between r3_template_transform.ts (for syntactic errors that don't depend on component metadata: [attr.foo] on <ng-template>, property/event/reference bindings and selection/selector typos on <ng-content>) and the TCB (TcbContentAttributesOp for static attributes on <ng-content> that don't match a parent component's content projection slot) fits cleanly into the existing architecture and works across both ngtsc and ngp.

Extracting validateNgContentElement and validateNoAttributeBindings also keeps visitElement much easier to read.

I left a few comments on edge cases around multi-class attributes / comma-separated slot selectors, namespaced attributes, legacy .d.ts metadata, and reporting multiple invalid bindings.

const attributes = new Set<TextAttribute>();
for (const attr of child.attributes) {
const withoutAttr = removeAttributeFromSelector(selector, attr.name);
if (findProjectionSlot(matcher, ngContentSelectors, withoutAttr) !== slot) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Two subtle edge cases with checking each attribute by removing it individually (withoutAttr = removeAttributeFromSelector(selector, attr.name)):

  1. Multiple classes in class="...": Because removeAttributeFromSelector strips the entire class attribute and all selector.classNames at once (lines 271–275), if a parent has <ng-content select=".used" /> and a child writes <ng-content class="used unused" />, removing class changes the matched slot so the whole TextAttribute is marked as used—allowing the redundant unused class token through without a diagnostic.
  2. Comma-separated / redundant selectors on the same slot: If a parent slot uses a selector list like <ng-content select="[slot], .foo" /> and a child writes <ng-content slot class="foo" />, removing slot alone still matches slot 0 via .foo, and removing class alone still matches slot 0 via [slot]. Because neither attribute changes slot when removed on its own, both attributes will be flagged as unsupported even though at least one is required to match the slot.

function createContentCssSelector(node: Content): CssSelector {
const selector = new CssSelector();
for (const attr of node.attributes) {
const name = splitNsName(attr.name)[1];

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

splitNsName(attr.name)[1] strips the namespace prefix and adds the unprefixed name to CssSelector. However, at runtime Angular's projection matcher (findAttrIndexInNode in packages/core/src/render3/node_selector_matcher.ts) explicitly skips AttributeMarker.NamespaceURI attributes altogether. If <ng-content> has a namespaced attribute (e.g. xlink:title), createContentCssSelector will match a parent's [title] slot even though it won't match at runtime. Consider skipping attributes where splitNsName(attr.name)[0] !== null.

if (
meta === undefined ||
meta.ngContentSelectors === null ||
meta.ngContentSelectors.length === 0

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

In ngtsc's DtsMetadataReader (packages/compiler-cli/src/ngtsc/metadata/src/dts.ts), a .d.ts component with no <ng-content> slots (NgContentSelectors = never) has ngContentSelectors: [], whereas ngContentSelectors: null indicates a legacy .d.ts declaration (typeArguments.length <= 6) where projection slots were not recorded in the type definition.

Returning early here when meta.ngContentSelectors === null treats unknown slots the same as zero slots, which could produce false-positive diagnostics if <ng-content someAttr> is projected into a component from an older .d.ts.

references: t.Reference[],
attributes: t.TextAttribute[],
): void {
if (boundEvents.length > 0) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Nit: boundEvents, parsedProperties, and references only report the first item ([0]), whereas attributes below (and validateNoAttributeBindings) loop over every item. Iterating over all elements in boundEvents, parsedProperties, and references would report all unsupported bindings on <ng-content> in one pass rather than requiring the user to fix them one at a time.

@JeanMeche
JeanMeche force-pushed the compiler-virtual-element-bindings branch 10 times, most recently from 12b8bb1 to 6999080 Compare November 10, 2026 22:50
…g-content and ng-template

Virtual elements such as <ng-content> and <ng-template> do not support
certain bindings and attributes:
- <ng-template> does not support attribute bindings ([attr.foo]).
- <ng-content> does not support property bindings ([id]), event bindings ((click)),
  or template references (#ref).
- <ng-content> only supports an allowlist of static attributes ('select',
  'ngProjectAs', 'ngSkipHydration', 'ngNonBindable', 'ngPreserveWhitespaces',
  and i18n attributes 'i18n', 'i18n-*'), as well as static attributes that
  select a content projection slot of a parent component when re-projecting
  <ng-content>. If 'selection' or 'selector' is encountered on <ng-content>,
  a helpful 'Did you mean select?' suggestion is provided.

Syntactic binding and reference checks are enforced during template
transformation (r3_template_transform.ts), while static attribute checks on
<ng-content> are performed during TCB generation (TcbContentAttributesOp) so
that parent component projection slots can be taken into account across both
ngtsc and ngp.

Also removes legacy compliance tests in r3_view_compiler_template that asserted
unsupported attribute and class bindings on ng-template degraded to property
bindings, and fixes tests and examples where invalid attributes were used on
<ng-content>.
@JeanMeche
JeanMeche force-pushed the compiler-virtual-element-bindings branch from 6999080 to 797374e Compare November 10, 2026 23:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: compiler Issues related to `ngc`, Angular's template compiler

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant