Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions adev/src/content/reference/errors/NG0319.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Binding on ng-container or ng-template has no effect

A template puts a binding on `<ng-container>` or `<ng-template>` that has no effect, because neither element renders a DOM element.

In development mode, Angular reports the following bindings when it creates a template:

- Class and style bindings that no directive input takes over. On `<ng-container>`, this covers `[class]`, `[style]`, and bindings such as `[class.active]` or `[style.width]`. On `<ng-template>`, it covers only `[class]` and `[style]`: Angular treats `[class.active]` and `[style.width]` on `<ng-template>` as property bindings, and reports them as unknown properties (NG0303).
- Event bindings, such as `(click)`, that no directive output claims and that have no global target such as `window:` or `document:`.

```angular-ts
import {Component, signal} from '@angular/core';

@Component({
selector: 'app-item',
// NG0319: neither binding has an effect, because <ng-container> doesn't render an element.
template: `
<ng-container [class.active]="active()" (click)="select()">
<span>{{ label }}</span>
</ng-container>
`,
})
export class Item {
label = 'Item';
active = signal(false);

select() {
this.active.set(true);
}
}
```

Angular logs this error with `console.error`. It throws instead when [`errorOnUnknownProperties`](api/core/testing/TestModuleMetadata#errorOnUnknownProperties) is enabled in tests. Host bindings of directives aren't reported, because a directive can also be applied to elements where those bindings work.

## Debugging the error

Use the binding and the component name in the error message to find the template. Then pick the fix that matches what the binding was meant to do:

- **Style or listen to the content**: move the binding to an element inside the `<ng-container>` or `<ng-template>`, or replace it with an element.
- **Set a directive input or listen to a directive output**: add the directive to the component's `imports`.
- **Nothing reads the binding**: remove it.

With [`strictTemplates`](tools/cli/template-typecheck#strict-mode), the compiler reports these bindings at build time as [NG8031](errors/NG8031), along with other bindings and attributes that `<ng-container>`, `<ng-template>`, and `<ng-content>` ignore.
83 changes: 83 additions & 0 deletions adev/src/content/reference/errors/NG8031.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Binding on ng-container, ng-template, or ng-content has no effect

A template puts a binding or an attribute on `<ng-container>`, `<ng-template>`, or `<ng-content>` that can never take effect.

None of these elements renders a DOM element. `<ng-container>` and `<ng-template>` leave at most a comment in the DOM, and Angular replaces `<ng-content>` with projected content. A class, style, attribute, or event binding on them has nothing to apply to, so Angular ignores it.

```angular-ts
import {Component, signal} from '@angular/core';

@Component({
selector: 'app-item',
// NG8031: neither binding has an effect, because <ng-container> doesn't render an element.
template: `
<ng-container [class.active]="active()" (click)="select()">
<span>{{ label }}</span>
</ng-container>
`,
})
export class Item {
label = 'Item';
active = signal(false);

select() {
this.active.set(true);
}
}
```

The compiler reports the following on these elements:

- Class, style, `attr.`, and `animate.` bindings that no directive input claims.
- Property bindings on `<ng-template>` that no directive input claims, such as `[ngIf]` when `NgIf` isn't imported.
- Property bindings on `<ng-container>` under `NO_ERRORS_SCHEMA`. Without that schema, the compiler reports them as unknown properties ([NG8002](errors/NG8002)).
- Event bindings, including `(animate.enter)` and `(animate.leave)`, that no directive output claims and that have no global target such as `window:` or `document:`.
- Static attributes that nothing reads: no directive or structural directive matched the element, no specific content projection slot selects it, and Angular doesn't read the attribute itself, as it does with `i18n`, `ngNonBindable`, `ngProjectAs`, `ngPreserveWhitespaces`, `ngSkipHydration`, and `select` on `<ng-content>`.
- Every binding on `<ng-content>`, which Angular never evaluates.

Bindings that a directive on the element claims aren't reported, and neither are host bindings of directives. A property or event binding named in the selector of a matched directive isn't reported either, because it makes the directive match.

## Debugging the error

Pick the fix that matches what the binding was meant to do:

- **Style or listen to the content**: move the binding to an element inside the `<ng-container>` or `<ng-template>`, or replace it with an element.

```angular-ts
@Component({
selector: 'app-item',
template: `<button type="button" [class.active]="active()" (click)="select()">
{{ label }}
</button>`,
})
export class Item {
label = 'Item';
active = signal(false);

select() {
this.active.set(true);
}
}
```

- **Set a directive input or listen to a directive output**: add the directive to the component's `imports`. For example, `[ngIf]` on `<ng-template>` needs `NgIf`.
- **Select a content projection slot**: check that the slot's `select` matches the attribute, and that the element is at the root of the component's content rather than inside another element. Slots match an element with `ngProjectAs` against that value instead of its attributes. For details, see [Content projection with ng-content](guide/components/content-projection).
- **Nothing reads the binding**: remove it.

## Configuring the diagnostic

NG8031 needs [`strictTemplates`](tools/cli/template-typecheck#strict-mode) and is an error by default. To report it as a warning, or to turn it off, set `ignoredBinding` in the `extendedDiagnostics` section of `tsconfig.json`:

```json
{
"angularCompilerOptions": {
"extendedDiagnostics": {
"checks": {
"ignoredBinding": "warning"
}
}
}
}
```

The value can be `"error"`, `"warning"`, or `"suppress"`.
46 changes: 46 additions & 0 deletions adev/src/content/reference/errors/NG8032.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Component on ng-container or ng-template

A component's selector matches an `<ng-container>` or an `<ng-template>` in a template.

A component renders its template inside its host element. `<ng-container>` and `<ng-template>` don't render an element, so they can't host a component. In development mode, Angular throws an error when it renders the template. In production mode, rendering fails with a `HierarchyRequestError` from the DOM.

```angular-ts
import {Component} from '@angular/core';

@Component({
selector: '[app-card]',
template: `<article><ng-content /></article>`,
})
export class Card {}

@Component({
selector: 'app-profile',
imports: [Card],
// NG8032: Card matches <ng-container>, which doesn't render an element.
template: `
<ng-container app-card>
<p>Profile details</p>
</ng-container>
`,
})
export class Profile {}
```

The compiler reports this for `<ng-container>` and `<ng-template>` written in the template. A selectorless component tag such as `<MyComp:ng-container>` is already a template parse error, because these tags can't be used as component tags.

## Debugging the error

Use an element that matches the component's selector:

```angular-ts
@Component({
selector: 'app-profile',
imports: [Card],
template: `
<section app-card>
<p>Profile details</p>
</section>
`,
})
export class Profile {}
```
29 changes: 16 additions & 13 deletions adev/src/content/reference/errors/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
| `NG0301` | [Export Not Found](errors/NG0301) |
| `NG0302` | [Pipe Not Found](errors/NG0302) |
| `NG0318` | [Invalid style property binding value](errors/NG0318) |
| `NG0319` | [Binding on ng-container or ng-template has no effect](errors/NG0319) |
| `NG0401` | [Missing platform](errors/NG0401) |
| `NG0403` | [Bootstrapped NgModule doesn't specify which component to initialize](errors/NG0403) |
| `NG0500` | [Hydration Node Mismatch](errors/NG0500) |
Expand Down Expand Up @@ -57,16 +58,18 @@

## Compiler errors

| Code | Name |
| :------- | :---------------------------------------------------------- |
| `NG1001` | [Argument Not Literal](errors/NG1001) |
| `NG2003` | [Missing Token](errors/NG2003) |
| `NG2009` | [Invalid Shadow DOM selector](errors/NG2009) |
| `NG3003` | [Import Cycle Detected](errors/NG3003) |
| `NG6100` | [NgModule.id Set to module.id anti-pattern](errors/NG6100) |
| `NG8001` | [Invalid Element](errors/NG8001) |
| `NG8002` | [Invalid Attribute](errors/NG8002) |
| `NG8003` | [Missing Reference Target](errors/NG8003) |
| `NG8011` | [Control Flow Preventing Content Projection](errors/NG8011) |
| `NG8023` | [Multiple Components Match Same Element](errors/NG8023) |
| `NG8024` | [Conflicting Host Directive Binding](errors/NG8024) |
| Code | Name |
| :------- | :--------------------------------------------------------------------------------- |
| `NG1001` | [Argument Not Literal](errors/NG1001) |
| `NG2003` | [Missing Token](errors/NG2003) |
| `NG2009` | [Invalid Shadow DOM selector](errors/NG2009) |
| `NG3003` | [Import Cycle Detected](errors/NG3003) |
| `NG6100` | [NgModule.id Set to module.id anti-pattern](errors/NG6100) |
| `NG8001` | [Invalid Element](errors/NG8001) |
| `NG8002` | [Invalid Attribute](errors/NG8002) |
| `NG8003` | [Missing Reference Target](errors/NG8003) |
| `NG8011` | [Control Flow Preventing Content Projection](errors/NG8011) |
| `NG8023` | [Multiple Components Match Same Element](errors/NG8023) |
| `NG8024` | [Conflicting Host Directive Binding](errors/NG8024) |
| `NG8031` | [Binding on ng-container, ng-template, or ng-content has no effect](errors/NG8031) |
| `NG8032` | [Component on ng-container or ng-template](errors/NG8032) |
2 changes: 2 additions & 0 deletions goldens/public-api/compiler-cli/error_code.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export enum ErrorCode {
// (undocumented)
COMPONENT_MISSING_TEMPLATE = 2001,
COMPONENT_NOT_STANDALONE = 2010,
COMPONENT_ON_NON_RENDERING_ELEMENT = -8032,
COMPONENT_RESOURCE_NOT_FOUND = 2008,
COMPONENT_UNKNOWN_DEFERRED_IMPORT = 2022,
COMPONENT_UNKNOWN_IMPORT = 2012,
Expand Down Expand Up @@ -70,6 +71,7 @@ export enum ErrorCode {
HOST_DIRECTIVE_MISSING_REQUIRED_BINDING = 2019,
HOST_DIRECTIVE_NOT_STANDALONE = 2014,
HOST_DIRECTIVE_UNDEFINED_BINDING = 2017,
IGNORED_BINDING = -8031,
ILLEGAL_FOR_LOOP_TRACK_ACCESS = 8009,
ILLEGAL_LET_WRITE = 8015,
IMPORT_CYCLE_DETECTED = -3003,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export enum ExtendedTemplateDiagnosticName {
// (undocumented)
DEFER_TRIGGER_MISCONFIGURATION = "deferTriggerMisconfiguration",
// (undocumented)
IGNORED_BINDING = "ignoredBinding",
// (undocumented)
INTERPOLATED_SIGNAL_NOT_INVOKED = "interpolatedSignalNotInvoked",
// (undocumented)
INVALID_BANANA_IN_BOX = "invalidBananaInBox",
Expand Down
2 changes: 2 additions & 0 deletions goldens/public-api/core/errors.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ export const enum RuntimeErrorCode {
// (undocumented)
HYDRATION_STABLE_TIMEDOUT = -506,
// (undocumented)
IGNORED_BINDING = -319,
// (undocumented)
IMAGE_PERFORMANCE_WARNING = -913,
// (undocumented)
IMPORT_PROVIDERS_FROM_STANDALONE = 800,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,22 @@ define_error_codes! {
/// This is only reported when `strictUnclaimedEventNames` is enabled.
UNCLAIMED_EVENT_BINDING = 8030,

/// A binding or attribute on an `<ng-container>`, an `<ng-template>`, or an `<ng-content>` has no
/// effect, because none of them renders an element. For example:
///
/// ```html
/// <ng-container [class.active]="isActive" (click)="select()"></ng-container>
/// ```
IGNORED_BINDING = -8031,

/// A component's selector matches an `<ng-container>` or an `<ng-template>`, which can't host
/// a component because neither renders an element. For example:
///
/// ```html
/// <ng-container user-card></ng-container>
/// ```
COMPONENT_ON_NON_RENDERING_ELEMENT = -8032,

/// A two way binding in a template has an incorrect syntax,
/// parentheses outside brackets. For example:
///
Expand Down
4 changes: 3 additions & 1 deletion packages/compiler-cli/preprocessor/src/compiler_options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const DIAGNOSTIC_CATEGORY_LABELS: readonly string[] = ['warning', 'error', 'supp
*/
export type ExtendedTemplateDiagnosticName =
| 'controlFlowPreventingContentProjection'
| 'ignoredBinding'
| 'unusedStandaloneImports'
| 'invalidBananaInBox'
| 'nullishCoalescingNotNullable'
Expand All @@ -45,11 +46,12 @@ export type ExtendedTemplateDiagnosticName =

/**
* Mirrors `SUPPORTED_DIAGNOSTIC_NAMES` in `ngtsc/typecheck/extended/index.ts`, in its iteration
* order (the two option-only checks, then `ALL_DIAGNOSTIC_FACTORIES`), since the diagnostic
* order (the three option-only checks, then `ALL_DIAGNOSTIC_FACTORIES`), since the diagnostic
* lists them. Keep in sync when a check is added upstream.
*/
export const SUPPORTED_DIAGNOSTIC_NAMES: ReadonlySet<ExtendedTemplateDiagnosticName> = new Set([
'controlFlowPreventingContentProjection',
'ignoredBinding',
'unusedStandaloneImports',
'invalidBananaInBox',
'nullishCoalescingNotNullable',
Expand Down
4 changes: 2 additions & 2 deletions packages/compiler-cli/preprocessor/src/hybrid_compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,8 +374,8 @@ export class HybridCompiler {
this.optionDiagnostics = [...configErrors];
// Mirrors `NgCompiler`, which doesn't construct the extended template checker, the template
// semantics checker or the source file validator when it has construction diagnostics. Note
// that `controlFlowPreventingContentProjection` is intentionally left alone: ngtsc reports
// it from the type check block regardless of option diagnostics.
// that `controlFlowPreventingContentProjection` and `ignoredBinding` are intentionally left
// alone: ngtsc reports them from the type check block regardless of option diagnostics.
if (this.optionDiagnostics.length > 0) {
this.tcbConfig = {...this.tcbConfig, disabledExtendedDiagnostics: true};
}
Expand Down
32 changes: 32 additions & 0 deletions packages/compiler-cli/preprocessor/src/oob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ import {
OutOfBandDiagnosticRecorder,
type TcbDirectiveMetadata,
type TypeCheckId,
type TmplAstContent,
getComponentOnNonRenderingElementMessage,
getIgnoredBindingMessage,
} from '@angular/compiler';
import {ErrorCode} from '@angular/compiler-cli/private/hybrid_preprocessor';
import {Diagnostic, DiagnosticRelatedInformation} from './diagnostic.js';
Expand Down Expand Up @@ -527,6 +530,35 @@ export class OutOfBandDiagnosticRecorderImpl implements OutOfBandDiagnosticRecor
);
}

ignoredBinding(
id: TypeCheckId,
category: OutOfBandDiagnosticCategory,
node: TmplAstElement | TmplAstTemplate | TmplAstContent,
binding: TmplAstBoundAttribute | TmplAstBoundEvent | TmplAstTextAttribute,
): void {
this.pushDiagnostic(
id,
getIgnoredBindingMessage(node, binding),
binding.sourceSpan,
category,
ErrorCode.IGNORED_BINDING,
);
}

componentOnNonRenderingElement(
id: TypeCheckId,
node: TmplAstElement | TmplAstTemplate,
componentName: string,
): void {
this.pushDiagnostic(
id,
getComponentOnNonRenderingElementMessage(node, componentName),
node.startSourceSpan,
OutOfBandDiagnosticCategory.Warning,
ErrorCode.COMPONENT_ON_NON_RENDERING_ELEMENT,
);
}

conflictingHostDirectiveBinding(
id: TypeCheckId,
node: TmplAstElement | TmplAstTemplate | TmplAstComponent | TmplAstDirective,
Expand Down
6 changes: 6 additions & 0 deletions packages/compiler-cli/preprocessor/src/tcb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,10 @@ export function buildTypeCheckingConfig(
options.extendedDiagnostics?.checks?.controlFlowPreventingContentProjection ??
options.extendedDiagnostics?.defaultCategory ??
'warning',
ignoredBinding:
options.extendedDiagnostics?.checks?.ignoredBinding ??
options.extendedDiagnostics?.defaultCategory ??
'error',
unusedStandaloneImports:
options.unusedStandaloneImports ??
options.extendedDiagnostics?.checks?.unusedStandaloneImports ??
Expand Down Expand Up @@ -253,6 +257,8 @@ export function buildTypeCheckingConfig(
options.extendedDiagnostics?.checks?.controlFlowPreventingContentProjection ??
options.extendedDiagnostics?.defaultCategory ??
'warning',
// Like extended diagnostics, this check needs `strictTemplates`.
ignoredBinding: 'suppress',
unusedStandaloneImports:
options.unusedStandaloneImports ??
options.extendedDiagnostics?.checks?.unusedStandaloneImports ??
Expand Down
7 changes: 7 additions & 0 deletions packages/compiler-cli/src/ngtsc/core/src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1130,6 +1130,8 @@ export class NgCompiler {
useInlineTypeConstructors,
controlFlowPreventingContentProjection:
this.options.extendedDiagnostics?.defaultCategory || DiagnosticCategoryLabel.Warning,
ignoredBinding:
this.options.extendedDiagnostics?.defaultCategory || DiagnosticCategoryLabel.Error,
unusedStandaloneImports:
this.options.extendedDiagnostics?.defaultCategory || DiagnosticCategoryLabel.Warning,
allowSignalsInTwoWayBindings,
Expand Down Expand Up @@ -1162,6 +1164,8 @@ export class NgCompiler {
useInlineTypeConstructors,
controlFlowPreventingContentProjection:
this.options.extendedDiagnostics?.defaultCategory || DiagnosticCategoryLabel.Warning,
// Like extended diagnostics, this check needs `strictTemplates`.
ignoredBinding: 'suppress',
unusedStandaloneImports:
this.options.extendedDiagnostics?.defaultCategory || DiagnosticCategoryLabel.Warning,
allowSignalsInTwoWayBindings,
Expand Down Expand Up @@ -1214,6 +1218,9 @@ export class NgCompiler {
typeCheckingConfig.controlFlowPreventingContentProjection =
this.options.extendedDiagnostics.checks.controlFlowPreventingContentProjection;
}
if (this.options.extendedDiagnostics?.checks?.ignoredBinding !== undefined) {
typeCheckingConfig.ignoredBinding = this.options.extendedDiagnostics.checks.ignoredBinding;
}
if (this.options.extendedDiagnostics?.checks?.unusedStandaloneImports !== undefined) {
typeCheckingConfig.unusedStandaloneImports =
this.options.extendedDiagnostics.checks.unusedStandaloneImports;
Expand Down
Loading
Loading