diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 7f9f0ab9bd00..fa12e442c085 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,7 +1,7 @@
blank_issues_enabled: false
contact_links:
- name: Security issue disclosure
- url: https://angular.io/guide/security#report-issues
+ url: https://angular.dev/best-practices/security#report-issues
about: Report a security issue in Angular Framework, CDK, Material, or CLI
- name: Angular CLI
url: https://github.com/angular/angular-cli/issues/new/choose
diff --git a/.github/actions/yarn-install/action.yml b/.github/actions/yarn-install/action.yml
index 26b3efe2283a..7455b8479a9c 100644
--- a/.github/actions/yarn-install/action.yml
+++ b/.github/actions/yarn-install/action.yml
@@ -4,7 +4,7 @@ description: 'Installs the dependencies using Yarn'
runs:
using: 'composite'
steps:
- - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4
+ - uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4
with:
path: |
./node_modules/
diff --git a/.github/workflows/adev-preview-deploy.yml b/.github/workflows/adev-preview-deploy.yml
index 93d651bec279..5399fd16d883 100644
--- a/.github/workflows/adev-preview-deploy.yml
+++ b/.github/workflows/adev-preview-deploy.yml
@@ -29,7 +29,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
+ - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
token: '${{secrets.GITHUB_TOKEN}}'
diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml
index c7314a73ef12..518aabb13114 100644
--- a/.github/workflows/scorecard.yml
+++ b/.github/workflows/scorecard.yml
@@ -25,7 +25,7 @@ jobs:
steps:
- name: 'Checkout code'
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
+ uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
with:
persist-credentials: false
@@ -39,7 +39,7 @@ jobs:
# Upload the results as artifacts.
- name: 'Upload artifact'
- uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
+ uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4.4.1
with:
name: SARIF file
path: results.sarif
@@ -47,6 +47,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: 'Upload to code-scanning'
- uses: github/codeql-action/upload-sarif@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.26.10
+ uses: github/codeql-action/upload-sarif@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12
with:
sarif_file: results.sarif
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cba27e104773..5339cb1c353c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,30 @@
+
+# 18.2.8 (2024-10-10)
+### compiler
+| Commit | Type | Description |
+| -- | -- | -- |
+| [11692c8dab](https://github.com/angular/angular/commit/11692c8dab2a78dc8780ceed301242d51dee7c9c) | fix | add multiple :host and nested selectors support ([#57796](https://github.com/angular/angular/pull/57796)) |
+| [66dcc691f5](https://github.com/angular/angular/commit/66dcc691f55eafc9de9a233b9bab53284fc13e1b) | fix | allow combinators inside pseudo selectors ([#57796](https://github.com/angular/angular/pull/57796)) |
+| [48a1437e77](https://github.com/angular/angular/commit/48a1437e77be5c3b29b8bbcd1b5d7784fbb67e68) | fix | fix comment typo ([#57796](https://github.com/angular/angular/pull/57796)) |
+| [d325f9b55f](https://github.com/angular/angular/commit/d325f9b55f248e5bd059645be901f210018f8fa2) | fix | fix parsing of the :host-context with pseudo selectors ([#57796](https://github.com/angular/angular/pull/57796)) |
+| [aea747ab3b](https://github.com/angular/angular/commit/aea747ab3bcbca79dbbc7ddfc41e11b9e43952eb) | fix | preserve attributes attached to :host selector ([#57796](https://github.com/angular/angular/pull/57796)) |
+| [21be258be6](https://github.com/angular/angular/commit/21be258be687a300ca22daad823e0b931029db35) | fix | scope :host-context inside pseudo selectors, do not decrease specificity ([#57796](https://github.com/angular/angular/pull/57796)) |
+| [7a6fd427d5](https://github.com/angular/angular/commit/7a6fd427d5ad70ad4c50693f54a6e77bf51eea86) | fix | transform pseudo selectors correctly for the encapsulated view ([#57796](https://github.com/angular/angular/pull/57796)) |
+### compiler-cli
+| Commit | Type | Description |
+| -- | -- | -- |
+| [f187c3abf8](https://github.com/angular/angular/commit/f187c3abf8b9547b2692995f344cd7dcb9f32ebc) | fix | defer symbols only used in types ([#58104](https://github.com/angular/angular/pull/58104)) |
+### core
+| Commit | Type | Description |
+| -- | -- | -- |
+| [46bafb0b0a](https://github.com/angular/angular/commit/46bafb0b0a952d8e9c2a0099f0607354697bbeaa) | fix | clean up afterRender after it is executed ([#58119](https://github.com/angular/angular/pull/58119)) |
+### platform-server
+| Commit | Type | Description |
+| -- | -- | -- |
+| [b40875a2cc](https://github.com/angular/angular/commit/b40875a2cc28a94015e6392044a03b30c2559999) | fix | destroy `PlatformRef` when error happens during the `bootstrap()` phase ([#58112](https://github.com/angular/angular/pull/58112)) ([#58135](https://github.com/angular/angular/pull/58135)) |
+
+
+
# 18.2.7 (2024-10-02)
### common
diff --git a/adev/shared-docs/pipeline/api-gen/rendering/templates/tab-description.tsx b/adev/shared-docs/pipeline/api-gen/rendering/templates/tab-description.tsx
index 0e1ee1faf7ef..4637a6b62fa0 100644
--- a/adev/shared-docs/pipeline/api-gen/rendering/templates/tab-description.tsx
+++ b/adev/shared-docs/pipeline/api-gen/rendering/templates/tab-description.tsx
@@ -10,18 +10,41 @@ import {Fragment, h} from 'preact';
import {DocEntryRenderable} from '../entities/renderables';
import {normalizeTabUrl} from '../transforms/url-transforms';
import {RawHtml} from './raw-html';
+import {CodeSymbol} from './code-symbols';
const DESCRIPTION_TAB_NAME = 'Description';
/** Component to render the description tab. */
export function TabDescription(props: {entry: DocEntryRenderable}) {
- if (!props.entry.htmlDescription || props.entry.htmlDescription === props.entry.shortHtmlDescription) {
- return (<>>);
+ const exportedBy = props.entry.jsdocTags.filter((t) => t.name === 'ngModule');
+ if (
+ (!props.entry.htmlDescription ||
+ props.entry.htmlDescription === props.entry.shortHtmlDescription) &&
+ !exportedBy.length
+ ) {
+ return <>>;
}
return (
+
+ {exportedBy.length ? (
+ <>
+
+
Exported by
+
+
+ {exportedBy.map((tag) => (
+ -
+
+
+ ))}
+
+ >
+ ) : (
+ <>>
+ )}
);
}
diff --git a/adev/shared-docs/styles/global-styles.scss b/adev/shared-docs/styles/global-styles.scss
index 8093ddb1450b..5c073d2f32ec 100644
--- a/adev/shared-docs/styles/global-styles.scss
+++ b/adev/shared-docs/styles/global-styles.scss
@@ -64,6 +64,7 @@ $theme: mat.m2-define-light-theme(
@include mat.core();
@include mat.tabs-theme($theme);
@include mat.button-toggle-theme($theme);
+@include mat.tooltip-theme($theme);
// Include custom docs styles
@include alert.docs-alert();
@@ -116,7 +117,7 @@ $theme: mat.m2-define-light-theme(
&.cli {
padding-inline-start: 1rem;
}
-
+
a {
color: inherit;
&:hover {
diff --git a/adev/src/app/editor/code-editor/code-editor.component.html b/adev/src/app/editor/code-editor/code-editor.component.html
index 06b87384d0b0..93d50cde385f 100644
--- a/adev/src/app/editor/code-editor/code-editor.component.html
+++ b/adev/src/app/editor/code-editor/code-editor.component.html
@@ -89,7 +89,9 @@
class="adev-editor-download-button"
type="button"
(click)="downloadCurrentCodeEditorState()"
- aria-label="Download current code in editor"
+ aria-label="Download current source code"
+ matTooltip="Download current source code"
+ matTooltipPosition="above"
>
download
diff --git a/adev/src/app/editor/code-editor/code-editor.component.scss b/adev/src/app/editor/code-editor/code-editor.component.scss
index 7606aa56c5bf..621fd810d8f5 100644
--- a/adev/src/app/editor/code-editor/code-editor.component.scss
+++ b/adev/src/app/editor/code-editor/code-editor.component.scss
@@ -122,10 +122,21 @@
// adjust height for terminal tabs
// so that scroll bar & content do not render under tabs
height: calc(100% - 49px);
+ transition: height 0.3s ease;
&-hidden {
display: none;
}
+
+ // If error box are displayed, move inline-errors-box
+ // to bottom of the editor and adjust height
+ // so that scroll bar & content do not render under tabs
+ &:has(~ .adev-inline-errors-box) ~ .adev-inline-errors-box {
+ bottom: auto;
+ }
+ &:has(~ .adev-inline-errors-box) {
+ height: min(75% - 49px);
+ }
}
.adev-inline-errors-box {
@@ -137,6 +148,8 @@
background-color: color-mix(in srgb, var(--bright-blue), var(--page-background) 90%);
border: 1px solid color-mix(in srgb, var(--bright-blue), var(--page-background) 70%);
border-radius: 0.25rem;
+ overflow: auto;
+ max-height: 25%;
button {
position: absolute;
@@ -162,7 +175,6 @@
margin: 0;
margin-inline: 1.25rem;
color: var(--tertiary-contrast);
- max-height: 200px;
overflow: auto;
}
}
diff --git a/adev/src/app/editor/code-editor/code-editor.component.spec.ts b/adev/src/app/editor/code-editor/code-editor.component.spec.ts
index 0e9099cdef42..65c0d9ba7e6c 100644
--- a/adev/src/app/editor/code-editor/code-editor.component.spec.ts
+++ b/adev/src/app/editor/code-editor/code-editor.component.spec.ts
@@ -21,6 +21,8 @@ import {CodeEditor, REQUIRED_FILES} from './code-editor.component';
import {CodeMirrorEditor} from './code-mirror-editor.service';
import {FakeChangeDetectorRef} from '@angular/docs';
import {TutorialType} from '@angular/docs';
+import {MatTooltip} from '@angular/material/tooltip';
+import {MatTooltipHarness} from '@angular/material/tooltip/testing';
const files = [
{filename: 'a', content: '', language: {} as any},
@@ -51,7 +53,7 @@ describe('CodeEditor', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
- imports: [CodeEditor, NoopAnimationsModule],
+ imports: [CodeEditor, NoopAnimationsModule, MatTooltip],
providers: [
{
provide: CodeMirrorEditor,
@@ -200,4 +202,28 @@ describe('CodeEditor', () => {
expect(fixture.debugElement.query(By.css('[aria-label="Delete file"]'))).toBeNull();
}
});
+
+ it('should be able to display the tooltip on the download button', async () => {
+ const tooltip = await loader.getHarness(
+ MatTooltipHarness.with({selector: '.adev-editor-download-button'}),
+ );
+ expect(await tooltip.isOpen()).toBeFalse();
+ await tooltip.show();
+ expect(await tooltip.isOpen()).toBeTrue();
+ });
+
+ it('should be able to get the tooltip message on the download button', async () => {
+ const tooltip = await loader.getHarness(
+ MatTooltipHarness.with({selector: '.adev-editor-download-button'}),
+ );
+ await tooltip.show();
+ expect(await tooltip.getTooltipText()).toBe('Download current source code');
+ });
+
+ it('should not be able to get the tooltip message on the download button when the tooltip is not shown', async () => {
+ const tooltip = await loader.getHarness(
+ MatTooltipHarness.with({selector: '.adev-editor-download-button'}),
+ );
+ expect(await tooltip.getTooltipText()).toBe('');
+ });
});
diff --git a/adev/src/app/editor/code-editor/code-editor.component.ts b/adev/src/app/editor/code-editor/code-editor.component.ts
index 19a6f0510c38..99fa1ddcc79e 100644
--- a/adev/src/app/editor/code-editor/code-editor.component.ts
+++ b/adev/src/app/editor/code-editor/code-editor.component.ts
@@ -33,6 +33,7 @@ import {StackBlitzOpener} from '../stackblitz-opener.service';
import {ClickOutside, IconComponent} from '@angular/docs';
import {CdkMenu, CdkMenuItem, CdkMenuTrigger} from '@angular/cdk/menu';
import {IDXLauncher} from '../idx-launcher.service';
+import {MatTooltip} from '@angular/material/tooltip';
export const REQUIRED_FILES = new Set([
'src/main.ts',
@@ -48,7 +49,15 @@ const ANGULAR_DEV = 'https://angular.dev';
templateUrl: './code-editor.component.html',
styleUrls: ['./code-editor.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
- imports: [MatTabsModule, IconComponent, ClickOutside, CdkMenu, CdkMenuItem, CdkMenuTrigger],
+ imports: [
+ MatTabsModule,
+ MatTooltip,
+ IconComponent,
+ ClickOutside,
+ CdkMenu,
+ CdkMenuItem,
+ CdkMenuTrigger,
+ ],
})
export class CodeEditor implements AfterViewInit, OnDestroy {
@ViewChild('codeEditorWrapper') private codeEditorWrapperRef!: ElementRef;
diff --git a/adev/src/app/editor/embedded-editor.component.scss b/adev/src/app/editor/embedded-editor.component.scss
index 7b54416901b1..30db00077121 100644
--- a/adev/src/app/editor/embedded-editor.component.scss
+++ b/adev/src/app/editor/embedded-editor.component.scss
@@ -32,7 +32,7 @@ $width-breakpoint: 950px;
}
}
- // If files are displayed, shpare the space
+ // If files are displayed, share the space
&:has(.docs-editor-tabs) {
.adev-tutorial-code-editor {
display: block;
diff --git a/adev/src/app/features/references/api-reference-list/api-reference-list.component.ts b/adev/src/app/features/references/api-reference-list/api-reference-list.component.ts
index 5afdd196d190..891b780ae713 100644
--- a/adev/src/app/features/references/api-reference-list/api-reference-list.component.ts
+++ b/adev/src/app/features/references/api-reference-list/api-reference-list.component.ts
@@ -10,7 +10,7 @@ import {
ChangeDetectionStrategy,
Component,
ElementRef,
- EnvironmentInjector,
+ Injector,
afterNextRender,
computed,
effect,
@@ -43,7 +43,7 @@ export default class ApiReferenceList {
private readonly apiReferenceManager = inject(ApiReferenceManager);
private readonly router = inject(Router);
filterInput = viewChild.required(TextField, {read: ElementRef});
- private readonly injector = inject(EnvironmentInjector);
+ private readonly injector = inject(Injector);
private readonly allGroups = this.apiReferenceManager.apiGroups;
diff --git a/adev/src/content/guide/di/dependency-injection-providers.md b/adev/src/content/guide/di/dependency-injection-providers.md
index e8065ff70b40..afd9575b6749 100644
--- a/adev/src/content/guide/di/dependency-injection-providers.md
+++ b/adev/src/content/guide/di/dependency-injection-providers.md
@@ -161,6 +161,10 @@ The following example defines a token, `APP_CONFIG`. of the type `InjectionToken
import { InjectionToken } from '@angular/core';
+export interface AppConfig {
+ title: string;
+}
+
export const APP_CONFIG = new InjectionToken('app.config description');
@@ -169,6 +173,10 @@ The optional type parameter, ``, and the token description, `app.conf
Next, register the dependency provider in the component using the `InjectionToken` object of `APP_CONFIG`:
+const MY_APP_CONFIG_VARIABLE: AppConfig = {
+ title: 'Hello',
+};
+
providers: [{ provide: APP_CONFIG, useValue: MY_APP_CONFIG_VARIABLE }]
diff --git a/adev/src/content/guide/routing/common-router-tasks.md b/adev/src/content/guide/routing/common-router-tasks.md
index 0d5b4fcf60dc..f12949eb3b72 100644
--- a/adev/src/content/guide/routing/common-router-tasks.md
+++ b/adev/src/content/guide/routing/common-router-tasks.md
@@ -474,6 +474,20 @@ gotoItems(hero: Hero) {
You can configure your routes to lazy load modules, which means that Angular only loads modules as needed, rather than loading all modules when the application launches.
Additionally, preload parts of your application in the background to improve the user experience.
+Any route can lazily load its routed, standalone component by using `loadComponent:`
+
+
+
+const routes: Routes = [
+ {
+ path: 'lazy',
+ loadComponent: () => import('./lazy.component').then(c => c.LazyComponent)
+ }
+];
+
+This works as long as the loaded component is standalone.
+
+
For more information on lazy loading and preloading see the dedicated guide [Lazy loading](guide/ngmodules/lazy-loading).
## Preventing unauthorized access
diff --git a/adev/src/content/guide/security.md b/adev/src/content/guide/security.md
index c3b5aaaeae63..e49f904e147c 100644
--- a/adev/src/content/guide/security.md
+++ b/adev/src/content/guide/security.md
@@ -5,6 +5,8 @@ It doesn't cover application-level security, such as authentication and authoriz
For more information about the attacks and mitigations described below, see the [Open Web Application Security Project (OWASP) Guide](https://www.owasp.org/index.php/Category:OWASP_Guide_Project).
+
+
Angular is part of Google [Open Source Software Vulnerability Reward Program](https://bughunters.google.com/about/rules/6521337925468160/google-open-source-software-vulnerability-reward-program-rules). [For vulnerabilities in Angular, please submit your report at https://bughunters.google.com](https://bughunters.google.com/report).
diff --git a/adev/src/content/guide/signals/model.md b/adev/src/content/guide/signals/model.md
index 10a8558e343d..142d53b08e39 100644
--- a/adev/src/content/guide/signals/model.md
+++ b/adev/src/content/guide/signals/model.md
@@ -130,9 +130,7 @@ through the template.
## When to use model inputs
-Use model inputs in components that exist to modify a value based on user interaction.
-Custom form controls, such as a date picker or combobox, should use model inputs for their
-primary value.
-
-Avoid using model inputs as a convenience to avoid introducing an additional class property for
-containing local state.
+Use model inputs when you want a component to support two-way binding. This is typically
+appropriate when a component exists to modify a value based on user interaction. Most commonly,
+custom form controls such as a date picker or combobox should use model inputs for their primary
+value.
diff --git a/adev/src/content/guide/templates/whitespace.md b/adev/src/content/guide/templates/whitespace.md
index 8830b5f6b4b6..a0ff607b2a33 100644
--- a/adev/src/content/guide/templates/whitespace.md
+++ b/adev/src/content/guide/templates/whitespace.md
@@ -44,7 +44,9 @@ In this example, the browser displays only a single space between "Hello" and "w
See [How whitespace is handled by HTML, CSS, and in the DOM](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace) for more context on how this works.
Angular avoids sending these unnecessary whitespace characters to the browser in the first place by collapsing them to a single character when it compiles the template.
-Preserving whitespace
+
+## Preserving whitespace
+
You can tell Angular to preserve whitespace in a template by specifying `preserveWhitespaces: true` in the `@Component` decorator for a template.
```angular-ts
diff --git a/adev/src/content/tutorials/first-app/steps/11-details-page/README.md b/adev/src/content/tutorials/first-app/steps/11-details-page/README.md
index 23453127f748..be84c8be009c 100644
--- a/adev/src/content/tutorials/first-app/steps/11-details-page/README.md
+++ b/adev/src/content/tutorials/first-app/steps/11-details-page/README.md
@@ -18,7 +18,7 @@ Route parameters enable you to include dynamic information as a part of your rou
-
+
In lesson 10, you added a second route to `src/app/routes.ts` which includes a special segment that identifies the route parameter, `id`:
diff --git a/goldens/public-api/common/index.api.md b/goldens/public-api/common/index.api.md
index cfc2e72fd7d5..f23757633029 100644
--- a/goldens/public-api/common/index.api.md
+++ b/goldens/public-api/common/index.api.md
@@ -536,20 +536,20 @@ export { NgForOf }
// @public (undocumented)
export class NgForOfContext = NgIterable> {
- // (undocumented)
$implicit: T;
- constructor($implicit: T, ngForOf: U, index: number, count: number);
- // (undocumented)
+ constructor(
+ $implicit: T,
+ ngForOf: U,
+ index: number,
+ count: number);
count: number;
// (undocumented)
get even(): boolean;
// (undocumented)
get first(): boolean;
- // (undocumented)
index: number;
// (undocumented)
get last(): boolean;
- // (undocumented)
ngForOf: U;
// (undocumented)
get odd(): boolean;
diff --git a/integration/animations-async/tsconfig.json b/integration/animations-async/tsconfig.json
index 274fbe2affa2..195ad48ae3c4 100644
--- a/integration/animations-async/tsconfig.json
+++ b/integration/animations-async/tsconfig.json
@@ -7,7 +7,7 @@
"esModuleInterop": true,
"declaration": false,
"experimentalDecorators": true,
- "module": "esnext",
+ "module": "es2022",
"moduleResolution": "node",
"importHelpers": true,
"target": "es2022",
diff --git a/integration/animations/tsconfig.json b/integration/animations/tsconfig.json
index 1dc925081af1..3210a9ea4250 100644
--- a/integration/animations/tsconfig.json
+++ b/integration/animations/tsconfig.json
@@ -7,10 +7,10 @@
"esModuleInterop": true,
"declaration": false,
"experimentalDecorators": true,
- "module": "esnext",
+ "module": "es2022",
"moduleResolution": "node",
"importHelpers": true,
- "target": "es2015",
+ "target": "es2022",
"typeRoots": [
"node_modules/@types"
],
diff --git a/integration/cli-elements-universal/tsconfig.json b/integration/cli-elements-universal/tsconfig.json
index 6df1071edd7e..f431b70173fb 100644
--- a/integration/cli-elements-universal/tsconfig.json
+++ b/integration/cli-elements-universal/tsconfig.json
@@ -14,8 +14,8 @@
"experimentalDecorators": true,
"moduleResolution": "node",
"importHelpers": true,
- "target": "es2015",
- "module": "es2020",
+ "target": "es2022",
+ "module": "es2022",
"lib": [
"es2018",
"dom"
diff --git a/integration/cli-hello-world-ivy-i18n/tsconfig.json b/integration/cli-hello-world-ivy-i18n/tsconfig.json
index 1dc925081af1..3210a9ea4250 100644
--- a/integration/cli-hello-world-ivy-i18n/tsconfig.json
+++ b/integration/cli-hello-world-ivy-i18n/tsconfig.json
@@ -7,10 +7,10 @@
"esModuleInterop": true,
"declaration": false,
"experimentalDecorators": true,
- "module": "esnext",
+ "module": "es2022",
"moduleResolution": "node",
"importHelpers": true,
- "target": "es2015",
+ "target": "es2022",
"typeRoots": [
"node_modules/@types"
],
diff --git a/integration/cli-hello-world-lazy/tsconfig.json b/integration/cli-hello-world-lazy/tsconfig.json
index 1dc925081af1..3210a9ea4250 100644
--- a/integration/cli-hello-world-lazy/tsconfig.json
+++ b/integration/cli-hello-world-lazy/tsconfig.json
@@ -7,10 +7,10 @@
"esModuleInterop": true,
"declaration": false,
"experimentalDecorators": true,
- "module": "esnext",
+ "module": "es2022",
"moduleResolution": "node",
"importHelpers": true,
- "target": "es2015",
+ "target": "es2022",
"typeRoots": [
"node_modules/@types"
],
diff --git a/integration/cli-hello-world-mocha/tsconfig.json b/integration/cli-hello-world-mocha/tsconfig.json
index 1dc925081af1..3210a9ea4250 100644
--- a/integration/cli-hello-world-mocha/tsconfig.json
+++ b/integration/cli-hello-world-mocha/tsconfig.json
@@ -7,10 +7,10 @@
"esModuleInterop": true,
"declaration": false,
"experimentalDecorators": true,
- "module": "esnext",
+ "module": "es2022",
"moduleResolution": "node",
"importHelpers": true,
- "target": "es2015",
+ "target": "es2022",
"typeRoots": [
"node_modules/@types"
],
diff --git a/integration/cli-hello-world/tsconfig.json b/integration/cli-hello-world/tsconfig.json
index 0d22beecb727..6d01924436cb 100644
--- a/integration/cli-hello-world/tsconfig.json
+++ b/integration/cli-hello-world/tsconfig.json
@@ -7,10 +7,10 @@
"esModuleInterop": true,
"declaration": false,
"experimentalDecorators": true,
- "module": "esnext",
+ "module": "es2022",
"moduleResolution": "node",
"importHelpers": true,
- "target": "es2015",
+ "target": "es2022",
"typeRoots": ["node_modules/@types"],
"lib": ["es2018", "dom"]
},
diff --git a/integration/cli-signal-inputs/tsconfig.json b/integration/cli-signal-inputs/tsconfig.json
index 0b56495c0943..d07f2a627156 100644
--- a/integration/cli-signal-inputs/tsconfig.json
+++ b/integration/cli-signal-inputs/tsconfig.json
@@ -8,10 +8,10 @@
"strict": true,
"declaration": false,
"experimentalDecorators": true,
- "module": "esnext",
+ "module": "es2022",
"moduleResolution": "node",
"importHelpers": true,
- "target": "es2015",
+ "target": "es2022",
"typeRoots": ["node_modules/@types"],
"lib": ["es2018", "dom"]
},
diff --git a/integration/defer/tsconfig.json b/integration/defer/tsconfig.json
index 1dc925081af1..3210a9ea4250 100644
--- a/integration/defer/tsconfig.json
+++ b/integration/defer/tsconfig.json
@@ -7,10 +7,10 @@
"esModuleInterop": true,
"declaration": false,
"experimentalDecorators": true,
- "module": "esnext",
+ "module": "es2022",
"moduleResolution": "node",
"importHelpers": true,
- "target": "es2015",
+ "target": "es2022",
"typeRoots": [
"node_modules/@types"
],
diff --git a/integration/dynamic-compiler/tsconfig.json b/integration/dynamic-compiler/tsconfig.json
index e6c8da5c89c5..571967b7d41c 100644
--- a/integration/dynamic-compiler/tsconfig.json
+++ b/integration/dynamic-compiler/tsconfig.json
@@ -1,7 +1,7 @@
{
"compilerOptions": {
- "target": "es2020",
- "module": "es2020",
+ "target": "es2022",
+ "module": "es2022",
"moduleResolution": "node",
"declaration": false,
"removeComments": true,
diff --git a/integration/forms/tsconfig.json b/integration/forms/tsconfig.json
index 1dc925081af1..3210a9ea4250 100644
--- a/integration/forms/tsconfig.json
+++ b/integration/forms/tsconfig.json
@@ -7,10 +7,10 @@
"esModuleInterop": true,
"declaration": false,
"experimentalDecorators": true,
- "module": "esnext",
+ "module": "es2022",
"moduleResolution": "node",
"importHelpers": true,
- "target": "es2015",
+ "target": "es2022",
"typeRoots": [
"node_modules/@types"
],
diff --git a/integration/ivy-i18n/tsconfig.json b/integration/ivy-i18n/tsconfig.json
index 1dc925081af1..3210a9ea4250 100644
--- a/integration/ivy-i18n/tsconfig.json
+++ b/integration/ivy-i18n/tsconfig.json
@@ -7,10 +7,10 @@
"esModuleInterop": true,
"declaration": false,
"experimentalDecorators": true,
- "module": "esnext",
+ "module": "es2022",
"moduleResolution": "node",
"importHelpers": true,
- "target": "es2015",
+ "target": "es2022",
"typeRoots": [
"node_modules/@types"
],
diff --git a/integration/ng-add-localize/tsconfig.json b/integration/ng-add-localize/tsconfig.json
index d85741f5e04d..dd149a779268 100644
--- a/integration/ng-add-localize/tsconfig.json
+++ b/integration/ng-add-localize/tsconfig.json
@@ -17,7 +17,7 @@
"moduleResolution": "node",
"importHelpers": true,
"target": "es2017",
- "module": "es2020",
+ "module": "es2022",
"lib": [
"es2018",
"dom"
diff --git a/integration/ng_elements/tsconfig.json b/integration/ng_elements/tsconfig.json
index e6a7a10295c5..a6773a564417 100644
--- a/integration/ng_elements/tsconfig.json
+++ b/integration/ng_elements/tsconfig.json
@@ -7,7 +7,7 @@
"module": "es2015",
"moduleResolution": "node",
"strictNullChecks": true,
- "target": "es2015",
+ "target": "es2022",
"sourceMap": false,
"experimentalDecorators": true,
"outDir": "built",
diff --git a/integration/ng_update_migrations/tsconfig.json b/integration/ng_update_migrations/tsconfig.json
index 6329b405f6b7..f9b4d004128c 100644
--- a/integration/ng_update_migrations/tsconfig.json
+++ b/integration/ng_update_migrations/tsconfig.json
@@ -6,12 +6,12 @@
"sourceMap": true,
"esModuleInterop": true,
"declaration": false,
- "module": "esnext",
+ "module": "es2022",
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"importHelpers": true,
- "target": "es2015",
+ "target": "es2022",
"typeRoots": [
"node_modules/@types"
],
diff --git a/integration/standalone-bootstrap/tsconfig.json b/integration/standalone-bootstrap/tsconfig.json
index 1dc925081af1..3210a9ea4250 100644
--- a/integration/standalone-bootstrap/tsconfig.json
+++ b/integration/standalone-bootstrap/tsconfig.json
@@ -7,10 +7,10 @@
"esModuleInterop": true,
"declaration": false,
"experimentalDecorators": true,
- "module": "esnext",
+ "module": "es2022",
"moduleResolution": "node",
"importHelpers": true,
- "target": "es2015",
+ "target": "es2022",
"typeRoots": [
"node_modules/@types"
],
diff --git a/integration/trusted-types/tsconfig.json b/integration/trusted-types/tsconfig.json
index 6df1071edd7e..f431b70173fb 100644
--- a/integration/trusted-types/tsconfig.json
+++ b/integration/trusted-types/tsconfig.json
@@ -14,8 +14,8 @@
"experimentalDecorators": true,
"moduleResolution": "node",
"importHelpers": true,
- "target": "es2015",
- "module": "es2020",
+ "target": "es2022",
+ "module": "es2022",
"lib": [
"es2018",
"dom"
diff --git a/package.json b/package.json
index e2978df611f3..c6821627c949 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "angular-srcs",
- "version": "18.2.7",
+ "version": "18.2.8",
"private": true,
"description": "Angular - a web framework for modern web apps",
"homepage": "https://github.com/angular/angular",
@@ -106,7 +106,7 @@
"bluebird": "^3.7.2",
"canonical-path": "1.0.0",
"chalk": "^4.1.0",
- "chokidar": "^3.5.1",
+ "chokidar": "^4.0.0",
"convert-source-map": "^1.5.1",
"d3": "^7.0.0",
"diff": "^5.0.0",
diff --git a/packages/common/src/directives/ng_for_of.ts b/packages/common/src/directives/ng_for_of.ts
index f7ca5760c5b8..389006145b98 100644
--- a/packages/common/src/directives/ng_for_of.ts
+++ b/packages/common/src/directives/ng_for_of.ts
@@ -29,24 +29,39 @@ import {RuntimeErrorCode} from '../errors';
*/
export class NgForOfContext = NgIterable> {
constructor(
+ /** Reference to the current item from the collection. */
public $implicit: T,
+
+ /**
+ * The value of the iterable expression. Useful when the expression is
+ * more complex then a property access, for example when using the async pipe
+ * (`userStreams | async`).
+ */
public ngForOf: U,
+
+ /** Returns an index of the current item in the collection. */
public index: number,
+
+ /** Returns total amount of items in the collection. */
public count: number,
) {}
+ // Indicates whether this is the first item in the collection.
get first(): boolean {
return this.index === 0;
}
+ // Indicates whether this is the last item in the collection.
get last(): boolean {
return this.index === this.count - 1;
}
+ // Indicates whether an index of this item in the collection is even.
get even(): boolean {
return this.index % 2 === 0;
}
+ // Indicates whether an index of this item in the collection is odd.
get odd(): boolean {
return !this.even;
}
diff --git a/packages/common/src/directives/ng_optimized_image/ng_optimized_image.ts b/packages/common/src/directives/ng_optimized_image/ng_optimized_image.ts
index bbfcd809d14d..dcc82ed5d6c7 100644
--- a/packages/common/src/directives/ng_optimized_image/ng_optimized_image.ts
+++ b/packages/common/src/directives/ng_optimized_image/ng_optimized_image.ts
@@ -258,7 +258,7 @@ export interface ImagePlaceholderConfig {
* {
* provide: IMAGE_LOADER,
* useValue: (config: ImageLoaderConfig) => {
- * return `https://example.com/${config.src}-${config.width}.jpg}`;
+ * return `https://example.com/${config.src}-${config.width}.jpg`;
* }
* },
* ],
diff --git a/packages/common/src/pipes/number_pipe.ts b/packages/common/src/pipes/number_pipe.ts
index cc8a128d3c3d..1edc0ba62a38 100644
--- a/packages/common/src/pipes/number_pipe.ts
+++ b/packages/common/src/pipes/number_pipe.ts
@@ -83,13 +83,6 @@ import {invalidPipeArgumentError} from './invalid_pipe_argument_error';
export class DecimalPipe implements PipeTransform {
constructor(@Inject(LOCALE_ID) private _locale: string) {}
- transform(value: number | string, digitsInfo?: string, locale?: string): string | null;
- transform(value: null | undefined, digitsInfo?: string, locale?: string): null;
- transform(
- value: number | string | null | undefined,
- digitsInfo?: string,
- locale?: string,
- ): string | null;
/**
* @param value The value to be formatted.
* @param digitsInfo Sets digit and decimal representation.
@@ -97,6 +90,13 @@ export class DecimalPipe implements PipeTransform {
* @param locale Specifies what locale format rules to use.
* [See more](#locale).
*/
+ transform(value: number | string, digitsInfo?: string, locale?: string): string | null;
+ transform(value: null | undefined, digitsInfo?: string, locale?: string): null;
+ transform(
+ value: number | string | null | undefined,
+ digitsInfo?: string,
+ locale?: string,
+ ): string | null;
transform(
value: number | string | null | undefined,
digitsInfo?: string,
diff --git a/packages/compiler-cli/package.json b/packages/compiler-cli/package.json
index a93989e10bdf..f7dc9b3e1f52 100644
--- a/packages/compiler-cli/package.json
+++ b/packages/compiler-cli/package.json
@@ -46,7 +46,7 @@
"@babel/core": "7.25.2",
"@jridgewell/sourcemap-codec": "^1.4.14",
"reflect-metadata": "^0.2.0",
- "chokidar": "^3.0.0",
+ "chokidar": "^4.0.0",
"convert-source-map": "^1.5.1",
"semver": "^7.0.0",
"tslib": "^2.3.0",
diff --git a/packages/compiler-cli/src/ngtsc/docs/src/class_extractor.ts b/packages/compiler-cli/src/ngtsc/docs/src/class_extractor.ts
index 6cf0aa24210e..fc179950db40 100644
--- a/packages/compiler-cli/src/ngtsc/docs/src/class_extractor.ts
+++ b/packages/compiler-cli/src/ngtsc/docs/src/class_extractor.ts
@@ -110,7 +110,10 @@ class ClassExtractor {
protected extractClassMember(memberDeclaration: MemberElement): MemberEntry | undefined {
if (this.isMethod(memberDeclaration)) {
return this.extractMethod(memberDeclaration);
- } else if (this.isProperty(memberDeclaration)) {
+ } else if (
+ this.isProperty(memberDeclaration) &&
+ !this.hasPrivateComputedProperty(memberDeclaration)
+ ) {
return this.extractClassProperty(memberDeclaration);
} else if (ts.isAccessor(memberDeclaration)) {
return this.extractGetterSetter(memberDeclaration);
@@ -375,6 +378,17 @@ class ClassExtractor {
const modifiers = this.declaration.modifiers ?? [];
return modifiers.some((mod) => mod.kind === ts.SyntaxKind.AbstractKeyword);
}
+
+ /**
+ * Check wether a member has a private computed property name like [ɵWRITABLE_SIGNAL]
+ *
+ * This will prevent exposing private computed properties in the docs.
+ */
+ private hasPrivateComputedProperty(property: PropertyLike) {
+ return (
+ ts.isComputedPropertyName(property.name) && property.name.expression.getText().startsWith('ɵ')
+ );
+ }
}
/** Extractor to pull info for API reference documentation for an Angular directive. */
diff --git a/packages/compiler-cli/src/ngtsc/imports/src/deferred_symbol_tracker.ts b/packages/compiler-cli/src/ngtsc/imports/src/deferred_symbol_tracker.ts
index 0375c5162e7d..a45f60e67446 100644
--- a/packages/compiler-cli/src/ngtsc/imports/src/deferred_symbol_tracker.ts
+++ b/packages/compiler-cli/src/ngtsc/imports/src/deferred_symbol_tracker.ts
@@ -171,7 +171,7 @@ export class DeferredSymbolTracker {
}
const symbolsMap = this.imports.get(importDecl)!;
- for (const [symbol, refs] of symbolsMap) {
+ for (const refs of symbolsMap.values()) {
if (refs === AssumeEager || refs.size > 0) {
// There may be still eager references to this symbol.
return false;
@@ -201,8 +201,9 @@ export class DeferredSymbolTracker {
): Set {
const results = new Set();
const visit = (node: ts.Node): void => {
- if (node === importDecl) {
- // Don't record references from the declaration itself.
+ // Don't record references from the declaration itself or inside
+ // type nodes which will be stripped from the JS output.
+ if (node === importDecl || ts.isTypeNode(node)) {
return;
}
diff --git a/packages/compiler-cli/src/perform_watch.ts b/packages/compiler-cli/src/perform_watch.ts
index 16087e97196f..ebb4595a61b9 100644
--- a/packages/compiler-cli/src/perform_watch.ts
+++ b/packages/compiler-cli/src/perform_watch.ts
@@ -88,7 +88,8 @@ export function createPerformWatchHost
+ /((^[\/\\])\..)|(\.js$)|(\.map$)|(\.metadata\.json|node_modules)/.test(path),
ignoreInitial: true,
persistent: true,
});
diff --git a/packages/compiler-cli/test/ngtsc/defer_spec.ts b/packages/compiler-cli/test/ngtsc/defer_spec.ts
index 0742c944c941..84a2d21cf692 100644
--- a/packages/compiler-cli/test/ngtsc/defer_spec.ts
+++ b/packages/compiler-cli/test/ngtsc/defer_spec.ts
@@ -79,12 +79,10 @@ runInEachFileSystem(() => {
expect(jsContents).not.toContain('import { CmpA }');
});
- it(
- 'should include timer scheduler function when ' + '`after` or `minimum` parameters are used',
- () => {
- env.write(
- 'cmp-a.ts',
- `
+ it('should include timer scheduler function when `after` or `minimum` parameters are used', () => {
+ env.write(
+ 'cmp-a.ts',
+ `
import { Component } from '@angular/core';
@Component({
@@ -94,38 +92,37 @@ runInEachFileSystem(() => {
})
export class CmpA {}
`,
- );
+ );
- env.write(
- '/test.ts',
- `
- import { Component } from '@angular/core';
- import { CmpA } from './cmp-a';
+ env.write(
+ '/test.ts',
+ `
+ import { Component } from '@angular/core';
+ import { CmpA } from './cmp-a';
- @Component({
- selector: 'test-cmp',
- standalone: true,
- imports: [CmpA],
- template: \`
- @defer {
-
- } @loading (after 500ms; minimum 300ms) {
- Loading...
- }
- \`,
- })
- export class TestCmp {}
- `,
- );
+ @Component({
+ selector: 'test-cmp',
+ standalone: true,
+ imports: [CmpA],
+ template: \`
+ @defer {
+
+ } @loading (after 500ms; minimum 300ms) {
+ Loading...
+ }
+ \`,
+ })
+ export class TestCmp {}
+ `,
+ );
- env.driveMain();
+ env.driveMain();
- const jsContents = env.getContents('test.js');
- expect(jsContents).toContain(
- 'ɵɵdefer(2, 0, TestCmp_Defer_2_DepsFn, 1, null, null, 0, null, i0.ɵɵdeferEnableTimerScheduling)',
- );
- },
- );
+ const jsContents = env.getContents('test.js');
+ expect(jsContents).toContain(
+ 'ɵɵdefer(2, 0, TestCmp_Defer_2_DepsFn, 1, null, null, 0, null, i0.ɵɵdeferEnableTimerScheduling)',
+ );
+ });
describe('imports', () => {
it('should retain regular imports when symbol is eagerly referenced', () => {
@@ -652,6 +649,112 @@ runInEachFileSystem(() => {
// via dynamic imports and an original import can be removed.
expect(jsContents).not.toContain('import CmpA');
});
+
+ it('should defer symbol that is used only in types', () => {
+ env.write(
+ 'cmp.ts',
+ `
+ import { Component } from '@angular/core';
+
+ @Component({
+ standalone: true,
+ selector: 'cmp',
+ template: 'Cmp!'
+ })
+ export class Cmp {}
+ `,
+ );
+
+ env.write(
+ '/test.ts',
+ `
+ import { Component, viewChild } from '@angular/core';
+ import { Cmp } from './cmp';
+
+ const topLevelConst: Cmp = null!;
+
+ @Component({
+ standalone: true,
+ imports: [Cmp],
+ template: \`
+ @defer {
+
+ }
+ \`,
+ })
+ export class TestCmp {
+ query = viewChild('ref');
+ asType: Cmp;
+ inlineType: {foo: Cmp};
+ unionType: string | Cmp | number;
+ constructor(param: Cmp) {}
+ inMethod(param: Cmp): Cmp {
+ let localVar: Cmp | null = null;
+ return localVar!;
+ }
+ }
+
+ function inFunction(param: Cmp): Cmp {
+ return null!;
+ }
+ `,
+ );
+
+ env.driveMain();
+
+ const jsContents = env.getContents('test.js');
+ expect(jsContents).toContain('ɵɵdefer(1, 0, TestCmp_Defer_1_DepsFn)');
+ expect(jsContents).toContain('() => [import("./cmp").then(m => m.Cmp)]');
+ expect(jsContents).not.toContain('import { Cmp }');
+ });
+
+ it('should retain symbols used in types and eagerly', () => {
+ env.write(
+ 'cmp.ts',
+ `
+ import { Component } from '@angular/core';
+
+ @Component({
+ standalone: true,
+ selector: 'cmp',
+ template: 'Cmp!'
+ })
+ export class Cmp {}
+ `,
+ );
+
+ env.write(
+ '/test.ts',
+ `
+ import { Component, viewChild } from '@angular/core';
+ import { Cmp } from './cmp';
+
+ @Component({
+ standalone: true,
+ imports: [Cmp],
+ template: \`
+ @defer {
+
+ }
+ \`,
+ })
+ export class TestCmp {
+ // Type-only reference
+ query = viewChild('ref');
+
+ // Directy reference
+ otherQuery = viewChild(Cmp);
+ }
+ `,
+ );
+
+ env.driveMain();
+
+ const jsContents = env.getContents('test.js');
+ expect(jsContents).toContain('ɵɵdefer(1, 0, TestCmp_Defer_1_DepsFn)');
+ expect(jsContents).toContain('() => [Cmp]');
+ expect(jsContents).toContain('import { Cmp }');
+ });
});
it('should detect pipe used in the `when` trigger as an eager dependency', () => {
diff --git a/packages/compiler/src/shadow_css.ts b/packages/compiler/src/shadow_css.ts
index f21dc0027022..bbaad8b43cf4 100644
--- a/packages/compiler/src/shadow_css.ts
+++ b/packages/compiler/src/shadow_css.ts
@@ -338,7 +338,7 @@ export class ShadowCss {
* captures how many (if any) leading whitespaces are present or a comma
* - (?:(?:(['"])((?:\\\\|\\\2|(?!\2).)+)\2)|(-?[A-Za-z][\w\-]*))
* captures two different possible keyframes, ones which are quoted or ones which are valid css
- * idents (custom properties excluded)
+ * indents (custom properties excluded)
* - (?=[,\s;]|$)
* simply matches the end of the possible keyframe, valid endings are: a comma, a space, a
* semicolon or the end of the string
@@ -459,7 +459,7 @@ export class ShadowCss {
*/
private _scopeCssText(cssText: string, scopeSelector: string, hostSelector: string): string {
const unscopedRules = this._extractUnscopedRulesFromCssText(cssText);
- // replace :host and :host-context -shadowcsshost and -shadowcsshost respectively
+ // replace :host and :host-context with -shadowcsshost and -shadowcsshostcontext respectively
cssText = this._insertPolyfillHostInCssText(cssText);
cssText = this._convertColonHost(cssText);
cssText = this._convertColonHostContext(cssText);
@@ -539,7 +539,7 @@ export class ShadowCss {
* .foo .bar { ... }
*/
private _convertColonHostContext(cssText: string): string {
- return cssText.replace(_cssColonHostContextReGlobal, (selectorText) => {
+ return cssText.replace(_cssColonHostContextReGlobal, (selectorText, pseudoPrefix) => {
// We have captured a selector that contains a `:host-context` rule.
// For backward compatibility `:host-context` may contain a comma separated list of selectors.
@@ -594,10 +594,12 @@ export class ShadowCss {
}
// The context selectors now must be combined with each other to capture all the possible
- // selectors that `:host-context` can match. See `combineHostContextSelectors()` for more
+ // selectors that `:host-context` can match. See `_combineHostContextSelectors()` for more
// info about how this is done.
return contextSelectorGroups
- .map((contextSelectors) => combineHostContextSelectors(contextSelectors, selectorText))
+ .map((contextSelectors) =>
+ _combineHostContextSelectors(contextSelectors, selectorText, pseudoPrefix),
+ )
.join(', ');
});
}
@@ -616,7 +618,12 @@ export class ShadowCss {
let selector = rule.selector;
let content = rule.content;
if (rule.selector[0] !== '@') {
- selector = this._scopeSelector(rule.selector, scopeSelector, hostSelector);
+ selector = this._scopeSelector({
+ selector,
+ scopeSelector,
+ hostSelector,
+ isParentSelector: true,
+ });
} else if (scopedAtRuleIdentifiers.some((atRule) => rule.selector.startsWith(atRule))) {
content = this._scopeSelectors(rule.content, scopeSelector, hostSelector);
} else if (rule.selector.startsWith('@font-face') || rule.selector.startsWith('@page')) {
@@ -656,15 +663,44 @@ export class ShadowCss {
});
}
- private _scopeSelector(selector: string, scopeSelector: string, hostSelector: string): string {
+ private _safeSelector: SafeSelector | undefined;
+ private _shouldScopeIndicator: boolean | undefined;
+
+ // `isParentSelector` is used to distinguish the selectors which are coming from
+ // the initial selector string and any nested selectors, parsed recursively,
+ // for example `selector = 'a:where(.one)'` could be the parent, while recursive call
+ // would have `selector = '.one'`.
+ private _scopeSelector({
+ selector,
+ scopeSelector,
+ hostSelector,
+ isParentSelector = false,
+ }: {
+ selector: string;
+ scopeSelector: string;
+ hostSelector: string;
+ isParentSelector?: boolean;
+ }): string {
+ // Split the selector into independent parts by `,` (comma) unless
+ // comma is within parenthesis, for example `:is(.one, two)`.
+ // Negative lookup after comma allows not splitting inside nested parenthesis,
+ // up to three levels (((,))).
+ const selectorSplitRe =
+ / ?,(?!(?:[^)(]*(?:\([^)(]*(?:\([^)(]*(?:\([^)(]*\)[^)(]*)*\)[^)(]*)*\)[^)(]*)*\))) ?/;
+
return selector
- .split(/ ?, ?/)
+ .split(selectorSplitRe)
.map((part) => part.split(_shadowDeepSelectors))
.map((deepParts) => {
const [shallowPart, ...otherParts] = deepParts;
const applyScope = (shallowPart: string) => {
if (this._selectorNeedsScoping(shallowPart, scopeSelector)) {
- return this._applySelectorScope(shallowPart, scopeSelector, hostSelector);
+ return this._applySelectorScope({
+ selector: shallowPart,
+ scopeSelector,
+ hostSelector,
+ isParentSelector,
+ });
} else {
return shallowPart;
}
@@ -697,9 +733,9 @@ export class ShadowCss {
if (_polyfillHostRe.test(selector)) {
const replaceBy = `[${hostSelector}]`;
return selector
- .replace(_polyfillHostNoCombinatorRe, (hnc, selector) => {
+ .replace(_polyfillHostNoCombinatorReGlobal, (_hnc, selector) => {
return selector.replace(
- /([^:]*)(:*)(.*)/,
+ /([^:\)]*)(:*)(.*)/,
(_: string, before: string, colon: string, after: string) => {
return before + replaceBy + colon + after;
},
@@ -713,11 +749,17 @@ export class ShadowCss {
// return a selector with [name] suffix on each simple selector
// e.g. .foo.bar > .zot becomes .foo[name].bar[name] > .zot[name] /** @internal */
- private _applySelectorScope(
- selector: string,
- scopeSelector: string,
- hostSelector: string,
- ): string {
+ private _applySelectorScope({
+ selector,
+ scopeSelector,
+ hostSelector,
+ isParentSelector,
+ }: {
+ selector: string;
+ scopeSelector: string;
+ hostSelector: string;
+ isParentSelector?: boolean;
+ }): string {
const isRe = /\[is=([^\]]*)\]/g;
scopeSelector = scopeSelector.replace(isRe, (_: string, ...parts: string[]) => parts[0]);
@@ -732,6 +774,10 @@ export class ShadowCss {
if (p.includes(_polyfillHostNoCombinator)) {
scopedP = this._applySimpleSelectorScope(p, scopeSelector, hostSelector);
+ if (_polyfillHostNoCombinatorWithinPseudoFunction.test(p)) {
+ const [_, before, colon, after] = scopedP.match(/([^:]*)(:*)(.*)/)!;
+ scopedP = before + attrName + colon + after;
+ }
} else {
// remove :host since it should be unnecessary
const t = p.replace(_polyfillHostRe, '');
@@ -746,13 +792,60 @@ export class ShadowCss {
return scopedP;
};
- const safeContent = new SafeSelector(selector);
- selector = safeContent.content();
+ // Wraps `_scopeSelectorPart()` to not use it directly on selectors with
+ // pseudo selector functions like `:where()`. Selectors within pseudo selector
+ // functions are recursively sent to `_scopeSelector()`.
+ const _pseudoFunctionAwareScopeSelectorPart = (selectorPart: string) => {
+ let scopedPart = '';
+
+ const cssPrefixWithPseudoSelectorFunctionMatch = selectorPart.match(
+ _cssPrefixWithPseudoSelectorFunction,
+ );
+ if (cssPrefixWithPseudoSelectorFunctionMatch) {
+ const [cssPseudoSelectorFunction] = cssPrefixWithPseudoSelectorFunctionMatch;
+
+ // Unwrap the pseudo selector to scope its contents.
+ // For example,
+ // - `:where(selectorToScope)` -> `selectorToScope`;
+ // - `:is(.foo, .bar)` -> `.foo, .bar`.
+ const selectorToScope = selectorPart.slice(cssPseudoSelectorFunction.length, -1);
+
+ if (selectorToScope.includes(_polyfillHostNoCombinator)) {
+ this._shouldScopeIndicator = true;
+ }
+
+ const scopedInnerPart = this._scopeSelector({
+ selector: selectorToScope,
+ scopeSelector,
+ hostSelector,
+ });
+
+ // Put the result back into the pseudo selector function.
+ scopedPart = `${cssPseudoSelectorFunction}${scopedInnerPart})`;
+ } else {
+ this._shouldScopeIndicator =
+ this._shouldScopeIndicator || selectorPart.includes(_polyfillHostNoCombinator);
+ scopedPart = this._shouldScopeIndicator ? _scopeSelectorPart(selectorPart) : selectorPart;
+ }
+
+ return scopedPart;
+ };
+
+ if (isParentSelector) {
+ this._safeSelector = new SafeSelector(selector);
+ selector = this._safeSelector.content();
+ }
let scopedSelector = '';
let startIndex = 0;
let res: RegExpExecArray | null;
- const sep = /( |>|\+|~(?!=))\s*/g;
+ // Combinators aren't used as a delimiter if they are within parenthesis,
+ // for example `:where(.one .two)` stays intact.
+ // Similarly to selector separation by comma initially, negative lookahead
+ // is used here to not break selectors within nested parenthesis up to three
+ // nested layers.
+ const sep =
+ /( |>|\+|~(?!=))(?!([^)(]*(?:\([^)(]*(?:\([^)(]*(?:\([^)(]*\)[^)(]*)*\)[^)(]*)*\)[^)(]*)*\)))\s*/g;
// If a selector appears before :host it should not be shimmed as it
// matches on ancestor elements and not on elements in the host's shadow
@@ -766,8 +859,13 @@ export class ShadowCss {
// - `tag :host` -> `tag [h]` (`tag` is not scoped because it's considered part of a
// `:host-context(tag)`)
const hasHost = selector.includes(_polyfillHostNoCombinator);
- // Only scope parts after the first `-shadowcsshost-no-combinator` when it is present
- let shouldScope = !hasHost;
+ // Only scope parts after or on the same level as the first `-shadowcsshost-no-combinator`
+ // when it is present. The selector has the same level when it is a part of a pseudo
+ // selector, like `:where()`, for example `:where(:host, .foo)` would result in `.foo`
+ // being scoped.
+ if (isParentSelector || this._shouldScopeIndicator) {
+ this._shouldScopeIndicator = !hasHost;
+ }
while ((res = sep.exec(selector)) !== null) {
const separator = res[1];
@@ -786,18 +884,17 @@ export class ShadowCss {
continue;
}
- shouldScope = shouldScope || part.includes(_polyfillHostNoCombinator);
- const scopedPart = shouldScope ? _scopeSelectorPart(part) : part;
+ const scopedPart = _pseudoFunctionAwareScopeSelectorPart(part);
scopedSelector += `${scopedPart} ${separator} `;
startIndex = sep.lastIndex;
}
const part = selector.substring(startIndex);
- shouldScope = shouldScope || part.includes(_polyfillHostNoCombinator);
- scopedSelector += shouldScope ? _scopeSelectorPart(part) : part;
+ scopedSelector += _pseudoFunctionAwareScopeSelectorPart(part);
// replace the placeholders with their original values
- return safeContent.restore(scopedSelector);
+ // using values stored inside the `safeSelector` instance.
+ return this._safeSelector!.restore(scopedSelector);
}
private _insertPolyfillHostInCssText(selector: string): string {
@@ -862,6 +959,8 @@ class SafeSelector {
}
}
+const _cssScopedPseudoFunctionPrefix = '(:(where|is)\\()?';
+const _cssPrefixWithPseudoSelectorFunction = /^:(where|is)\(/i;
const _cssContentNextSelectorRe =
/polyfill-next-selector[^}]*content:[\s]*?(['"])(.*?)\1[;\s]*}([^{]*?){/gim;
const _cssContentRuleRe = /(polyfill-rule)[^}]*(content:[\s]*(['"])(.*?)\3)[;\s]*[^}]*}/gim;
@@ -872,10 +971,17 @@ const _polyfillHost = '-shadowcsshost';
const _polyfillHostContext = '-shadowcsscontext';
const _parenSuffix = '(?:\\((' + '(?:\\([^)(]*\\)|[^)(]*)+?' + ')\\))?([^,{]*)';
const _cssColonHostRe = new RegExp(_polyfillHost + _parenSuffix, 'gim');
-const _cssColonHostContextReGlobal = new RegExp(_polyfillHostContext + _parenSuffix, 'gim');
+const _cssColonHostContextReGlobal = new RegExp(
+ _cssScopedPseudoFunctionPrefix + '(' + _polyfillHostContext + _parenSuffix + ')',
+ 'gim',
+);
const _cssColonHostContextRe = new RegExp(_polyfillHostContext + _parenSuffix, 'im');
const _polyfillHostNoCombinator = _polyfillHost + '-no-combinator';
+const _polyfillHostNoCombinatorWithinPseudoFunction = new RegExp(
+ `:.*\\(.*${_polyfillHostNoCombinator}.*\\)`,
+);
const _polyfillHostNoCombinatorRe = /-shadowcsshost-no-combinator([^\s]*)/;
+const _polyfillHostNoCombinatorReGlobal = new RegExp(_polyfillHostNoCombinatorRe, 'g');
const _shadowDOMSelectorsRe = [
/::shadow/g,
/::content/g,
@@ -1126,7 +1232,11 @@ function unescapeQuotes(str: string, isQuoted: boolean): string {
* @param contextSelectors an array of context selectors that will be combined.
* @param otherSelectors the rest of the selectors that are not context selectors.
*/
-function combineHostContextSelectors(contextSelectors: string[], otherSelectors: string): string {
+function _combineHostContextSelectors(
+ contextSelectors: string[],
+ otherSelectors: string,
+ pseudoPrefix = '',
+): string {
const hostMarker = _polyfillHostNoCombinator;
_polyfillHostRe.lastIndex = 0; // reset the regex to ensure we get an accurate test
const otherSelectorsHasHost = _polyfillHostRe.test(otherSelectors);
@@ -1155,8 +1265,8 @@ function combineHostContextSelectors(contextSelectors: string[], otherSelectors:
return combined
.map((s) =>
otherSelectorsHasHost
- ? `${s}${otherSelectors}`
- : `${s}${hostMarker}${otherSelectors}, ${s} ${hostMarker}${otherSelectors}`,
+ ? `${pseudoPrefix}${s}${otherSelectors}`
+ : `${pseudoPrefix}${s}${hostMarker}${otherSelectors}, ${pseudoPrefix}${s} ${hostMarker}${otherSelectors}`,
)
.join(',');
}
diff --git a/packages/compiler/test/shadow_css/host_and_host_context_spec.ts b/packages/compiler/test/shadow_css/host_and_host_context_spec.ts
index c4dc29c372b4..5b5689feb2be 100644
--- a/packages/compiler/test/shadow_css/host_and_host_context_spec.ts
+++ b/packages/compiler/test/shadow_css/host_and_host_context_spec.ts
@@ -107,6 +107,42 @@ describe('ShadowCss, :host and :host-context', () => {
});
describe(':host-context', () => {
+ it('should transform :host-context with pseudo selectors', () => {
+ expect(
+ shim(':host-context(backdrop:not(.borderless)) .backdrop {}', 'contenta', 'hosta'),
+ ).toEqualCss(
+ 'backdrop:not(.borderless)[hosta] .backdrop[contenta], backdrop:not(.borderless) [hosta] .backdrop[contenta] {}',
+ );
+ expect(shim(':where(:host-context(backdrop)) {}', 'contenta', 'hosta')).toEqualCss(
+ ':where(backdrop[hosta]), :where(backdrop [hosta]) {}',
+ );
+ expect(shim(':where(:host-context(outer1)) :host(bar) {}', 'contenta', 'hosta')).toEqualCss(
+ ':where(outer1) bar[hosta] {}',
+ );
+ expect(
+ shim(':where(:host-context(.one)) :where(:host-context(.two)) {}', 'contenta', 'a-host'),
+ ).toEqualCss(
+ ':where(.one.two[a-host]), ' + // `one` and `two` both on the host
+ ':where(.one.two [a-host]), ' + // `one` and `two` are both on the same ancestor
+ ':where(.one .two[a-host]), ' + // `one` is an ancestor and `two` is on the host
+ ':where(.one .two [a-host]), ' + // `one` and `two` are both ancestors (in that order)
+ ':where(.two .one[a-host]), ' + // `two` is an ancestor and `one` is on the host
+ ':where(.two .one [a-host])' + // `two` and `one` are both ancestors (in that order)
+ ' {}',
+ );
+ expect(
+ shim(':where(:host-context(backdrop)) .foo ~ .bar {}', 'contenta', 'hosta'),
+ ).toEqualCss(
+ ':where(backdrop[hosta]) .foo[contenta] ~ .bar[contenta], :where(backdrop [hosta]) .foo[contenta] ~ .bar[contenta] {}',
+ );
+ expect(shim(':where(:host-context(backdrop)) :host {}', 'contenta', 'hosta')).toEqualCss(
+ ':where(backdrop) [hosta] {}',
+ );
+ expect(shim('div:where(:host-context(backdrop)) :host {}', 'contenta', 'hosta')).toEqualCss(
+ 'div:where(backdrop) [hosta] {}',
+ );
+ });
+
it('should handle tag selector', () => {
expect(shim(':host-context(div) {}', 'contenta', 'a-host')).toEqualCss(
'div[a-host], div [a-host] {}',
diff --git a/packages/compiler/test/shadow_css/shadow_css_spec.ts b/packages/compiler/test/shadow_css/shadow_css_spec.ts
index e7e038d1b52e..77a0a361a319 100644
--- a/packages/compiler/test/shadow_css/shadow_css_spec.ts
+++ b/packages/compiler/test/shadow_css/shadow_css_spec.ts
@@ -66,6 +66,18 @@ describe('ShadowCss', () => {
expect(shim('one[attr="va lue"] {}', 'contenta')).toEqualCss('one[attr="va lue"][contenta] {}');
expect(shim('one[attr] {}', 'contenta')).toEqualCss('one[attr][contenta] {}');
expect(shim('[is="one"] {}', 'contenta')).toEqualCss('[is="one"][contenta] {}');
+ expect(shim('[attr] {}', 'contenta')).toEqualCss('[attr][contenta] {}');
+ });
+
+ it('should transform :host with attributes', () => {
+ expect(shim(':host [attr] {}', 'contenta', 'hosta')).toEqualCss('[hosta] [attr][contenta] {}');
+ expect(shim(':host(create-first-project) {}', 'contenta', 'hosta')).toEqualCss(
+ 'create-first-project[hosta] {}',
+ );
+ expect(shim(':host[attr] {}', 'contenta', 'hosta')).toEqualCss('[attr][hosta] {}');
+ expect(shim(':host[attr]:where(:not(.cm-button)) {}', 'contenta', 'hosta')).toEqualCss(
+ '[attr][hosta]:where(:not(.cm-button)) {}',
+ );
});
it('should handle escaped sequences in selectors', () => {
@@ -77,6 +89,171 @@ describe('ShadowCss', () => {
expect(shim('.one\\:two .three\\:four {}', 'contenta')).toEqualCss(
'.one\\:two[contenta] .three\\:four[contenta] {}',
);
+ expect(shim('div:where(.one) {}', 'contenta', 'hosta')).toEqualCss(
+ 'div[contenta]:where(.one) {}',
+ );
+ expect(shim('div:where() {}', 'contenta', 'hosta')).toEqualCss('div[contenta]:where() {}');
+ // See `xit('should parse concatenated pseudo selectors'`
+ expect(shim(':where(a):where(b) {}', 'contenta', 'hosta')).toEqualCss(
+ ':where(a)[contenta]:where(b) {}',
+ );
+ expect(shim('*:where(.one) {}', 'contenta', 'hosta')).toEqualCss('*[contenta]:where(.one) {}');
+ expect(shim('*:where(.one) ::ng-deep .foo {}', 'contenta', 'hosta')).toEqualCss(
+ '*[contenta]:where(.one) .foo {}',
+ );
+ });
+
+ xit('should parse concatenated pseudo selectors', () => {
+ // Current logic leads to a result with an outer scope
+ // It could be changed, to not increase specificity
+ // Requires a more complex parsing
+ expect(shim(':where(a):where(b) {}', 'contenta', 'hosta')).toEqualCss(
+ ':where(a[contenta]):where(b[contenta]) {}',
+ );
+ });
+
+ it('should handle pseudo functions correctly', () => {
+ // :where()
+ expect(shim(':where(.one) {}', 'contenta', 'hosta')).toEqualCss(':where(.one[contenta]) {}');
+ expect(shim(':where(div.one span.two) {}', 'contenta', 'hosta')).toEqualCss(
+ ':where(div.one[contenta] span.two[contenta]) {}',
+ );
+ expect(shim(':where(.one) .two {}', 'contenta', 'hosta')).toEqualCss(
+ ':where(.one[contenta]) .two[contenta] {}',
+ );
+ expect(shim(':where(:host) {}', 'contenta', 'hosta')).toEqualCss(':where([hosta]) {}');
+ expect(shim(':where(:host) .one {}', 'contenta', 'hosta')).toEqualCss(
+ ':where([hosta]) .one[contenta] {}',
+ );
+ expect(shim(':where(.one) :where(:host) {}', 'contenta', 'hosta')).toEqualCss(
+ ':where(.one) :where([hosta]) {}',
+ );
+ expect(shim(':where(.one :host) {}', 'contenta', 'hosta')).toEqualCss(
+ ':where(.one [hosta]) {}',
+ );
+ expect(shim('div :where(.one) {}', 'contenta', 'hosta')).toEqualCss(
+ 'div[contenta] :where(.one[contenta]) {}',
+ );
+ expect(shim(':host :where(.one .two) {}', 'contenta', 'hosta')).toEqualCss(
+ '[hosta] :where(.one[contenta] .two[contenta]) {}',
+ );
+ expect(shim(':where(.one, .two) {}', 'contenta', 'hosta')).toEqualCss(
+ ':where(.one[contenta], .two[contenta]) {}',
+ );
+ expect(shim(':where(.one > .two) {}', 'contenta', 'hosta')).toEqualCss(
+ ':where(.one[contenta] > .two[contenta]) {}',
+ );
+ expect(shim(':where(> .one) {}', 'contenta', 'hosta')).toEqualCss(
+ ':where( > .one[contenta]) {}',
+ );
+ expect(shim(':where(:not(.one) ~ .two) {}', 'contenta', 'hosta')).toEqualCss(
+ ':where([contenta]:not(.one) ~ .two[contenta]) {}',
+ );
+ expect(shim(':where([foo]) {}', 'contenta', 'hosta')).toEqualCss(':where([foo][contenta]) {}');
+
+ // :is()
+ expect(shim('div:is(.foo) {}', 'contenta', 'a-host')).toEqualCss('div[contenta]:is(.foo) {}');
+ expect(shim(':is(.dark :host) {}', 'contenta', 'a-host')).toEqualCss(':is(.dark [a-host]) {}');
+ expect(shim(':is(.dark) :is(:host) {}', 'contenta', 'a-host')).toEqualCss(
+ ':is(.dark) :is([a-host]) {}',
+ );
+ expect(shim(':host:is(.foo) {}', 'contenta', 'a-host')).toEqualCss('[a-host]:is(.foo) {}');
+ expect(shim(':is(.foo) {}', 'contenta', 'a-host')).toEqualCss(':is(.foo[contenta]) {}');
+ expect(shim(':is(.foo, .bar, .baz) {}', 'contenta', 'a-host')).toEqualCss(
+ ':is(.foo[contenta], .bar[contenta], .baz[contenta]) {}',
+ );
+ expect(shim(':is(.foo, .bar) :host {}', 'contenta', 'a-host')).toEqualCss(
+ ':is(.foo, .bar) [a-host] {}',
+ );
+
+ // :is() and :where()
+ expect(
+ shim(
+ ':is(.foo, .bar) :is(.baz) :where(.one, .two) :host :where(.three:first-child) {}',
+ 'contenta',
+ 'a-host',
+ ),
+ ).toEqualCss(
+ ':is(.foo, .bar) :is(.baz) :where(.one, .two) [a-host] :where(.three[contenta]:first-child) {}',
+ );
+ expect(shim(':where(:is(a)) {}', 'contenta', 'hosta')).toEqualCss(
+ ':where(:is(a[contenta])) {}',
+ );
+ expect(shim(':where(:is(a, b)) {}', 'contenta', 'hosta')).toEqualCss(
+ ':where(:is(a[contenta], b[contenta])) {}',
+ );
+ expect(shim(':where(:host:is(.one, .two)) {}', 'contenta', 'hosta')).toEqualCss(
+ ':where([hosta]:is(.one, .two)) {}',
+ );
+ expect(shim(':where(:host :is(.one, .two)) {}', 'contenta', 'hosta')).toEqualCss(
+ ':where([hosta] :is(.one[contenta], .two[contenta])) {}',
+ );
+ expect(shim(':where(:is(a, b) :is(.one, .two)) {}', 'contenta', 'hosta')).toEqualCss(
+ ':where(:is(a[contenta], b[contenta]) :is(.one[contenta], .two[contenta])) {}',
+ );
+ expect(
+ shim(
+ ':where(:where(a:has(.foo), b) :is(.one, .two:where(.foo > .bar))) {}',
+ 'contenta',
+ 'hosta',
+ ),
+ ).toEqualCss(
+ ':where(:where(a[contenta]:has(.foo), b[contenta]) :is(.one[contenta], .two[contenta]:where(.foo > .bar))) {}',
+ );
+
+ // complex selectors
+ expect(shim(':host:is([foo],[foo-2])>div.example-2 {}', 'contenta', 'a-host')).toEqualCss(
+ '[a-host]:is([foo],[foo-2]) > div.example-2[contenta] {}',
+ );
+ expect(shim(':host:is([foo], [foo-2]) > div.example-2 {}', 'contenta', 'a-host')).toEqualCss(
+ '[a-host]:is([foo], [foo-2]) > div.example-2[contenta] {}',
+ );
+ expect(shim(':host:has([foo],[foo-2])>div.example-2 {}', 'contenta', 'a-host')).toEqualCss(
+ '[a-host]:has([foo],[foo-2]) > div.example-2[contenta] {}',
+ );
+
+ // :has()
+ expect(shim('div:has(a) {}', 'contenta', 'hosta')).toEqualCss('div[contenta]:has(a) {}');
+ expect(shim('div:has(a) :host {}', 'contenta', 'hosta')).toEqualCss('div:has(a) [hosta] {}');
+ expect(shim(':has(a) :host :has(b) {}', 'contenta', 'hosta')).toEqualCss(
+ ':has(a) [hosta] [contenta]:has(b) {}',
+ );
+ expect(shim('div:has(~ .one) {}', 'contenta', 'hosta')).toEqualCss(
+ 'div[contenta]:has(~ .one) {}',
+ );
+ // Unlike `:is()` or `:where()` the attribute selector isn't placed inside
+ // of `:has()`. That is deliberate, `[contenta]:has(a)` would select all
+ // `[contenta]` with `a` inside, while `:has(a[contenta])` would select
+ // everything that contains `a[contenta]`, targeting elements outside of
+ // encapsulated scope.
+ expect(shim(':has(a) :has(b) {}', 'contenta', 'hosta')).toEqualCss(
+ '[contenta]:has(a) [contenta]:has(b) {}',
+ );
+ });
+
+ it('should handle :host inclusions inside pseudo-selectors selectors', () => {
+ expect(shim('.header:not(.admin) {}', 'contenta', 'hosta')).toEqualCss(
+ '.header[contenta]:not(.admin) {}',
+ );
+ expect(shim('.header:is(:host > .toolbar, :host ~ .panel) {}', 'contenta', 'hosta')).toEqualCss(
+ '.header[contenta]:is([hosta] > .toolbar, [hosta] ~ .panel) {}',
+ );
+ expect(
+ shim('.header:where(:host > .toolbar, :host ~ .panel) {}', 'contenta', 'hosta'),
+ ).toEqualCss('.header[contenta]:where([hosta] > .toolbar, [hosta] ~ .panel) {}');
+ expect(shim('.header:not(.admin, :host.super .header) {}', 'contenta', 'hosta')).toEqualCss(
+ '.header[contenta]:not(.admin, .super[hosta] .header) {}',
+ );
+ expect(
+ shim('.header:not(.admin, :host.super .header, :host.mega .header) {}', 'contenta', 'hosta'),
+ ).toEqualCss('.header[contenta]:not(.admin, .super[hosta] .header, .mega[hosta] .header) {}');
+
+ expect(shim('.one :where(.two, :host) {}', 'contenta', 'hosta')).toEqualCss(
+ '.one :where(.two[contenta], [hosta]) {}',
+ );
+ expect(shim('.one :where(:host, .two) {}', 'contenta', 'hosta')).toEqualCss(
+ '.one :where([hosta], .two[contenta]) {}',
+ );
});
it('should handle escaped selector with space (if followed by a hex char)', () => {
diff --git a/packages/core/src/linker/view_ref.ts b/packages/core/src/linker/view_ref.ts
index 55e4b7cdbb76..ef684ba61cdc 100644
--- a/packages/core/src/linker/view_ref.ts
+++ b/packages/core/src/linker/view_ref.ts
@@ -100,13 +100,3 @@ export abstract class EmbeddedViewRef extends ViewRef {
*/
abstract get rootNodes(): any[];
}
-
-/**
- * Interface for tracking root `ViewRef`s in `ApplicationRef`.
- *
- * NOTE: Importing `ApplicationRef` here directly creates circular dependency, which is why we have
- * a subset of the `ApplicationRef` interface `ViewRefTracker` here.
- */
-export interface ViewRefTracker {
- detachView(viewRef: ViewRef): void;
-}
diff --git a/packages/core/src/platform/bootstrap.ts b/packages/core/src/platform/bootstrap.ts
index 88ef785f2306..c9d802754770 100644
--- a/packages/core/src/platform/bootstrap.ts
+++ b/packages/core/src/platform/bootstrap.ts
@@ -26,21 +26,24 @@ import {Injector} from '../di';
import {InternalNgModuleRef, NgModuleRef} from '../linker/ng_module_factory';
import {stringify} from '../util/stringify';
-export interface ModuleBootstrapConfig {
+export interface BootstrapConfig {
+ platformInjector: Injector;
+}
+
+export interface ModuleBootstrapConfig extends BootstrapConfig {
moduleRef: InternalNgModuleRef;
allPlatformModules: NgModuleRef[];
}
-export interface ApplicationBootstrapConfig {
+export interface ApplicationBootstrapConfig extends BootstrapConfig {
r3Injector: R3Injector;
- platformInjector: Injector;
rootComponent: Type | undefined;
}
function isApplicationBootstrapConfig(
config: ApplicationBootstrapConfig | ModuleBootstrapConfig,
): config is ApplicationBootstrapConfig {
- return !!(config as ApplicationBootstrapConfig).platformInjector;
+ return !(config as ModuleBootstrapConfig).moduleRef;
}
export function bootstrap(
@@ -91,9 +94,9 @@ export function bootstrap(
});
});
+ // If the whole platform is destroyed, invoke the `destroy` method
+ // for all bootstrapped applications as well.
if (isApplicationBootstrapConfig(config)) {
- // If the whole platform is destroyed, invoke the `destroy` method
- // for all bootstrapped applications as well.
const destroyListener = () => envInjector.destroy();
const onPlatformDestroyListeners = config.platformInjector.get(PLATFORM_DESTROY_LISTENERS);
onPlatformDestroyListeners.add(destroyListener);
@@ -103,9 +106,14 @@ export function bootstrap(
onPlatformDestroyListeners.delete(destroyListener);
});
} else {
+ const destroyListener = () => config.moduleRef.destroy();
+ const onPlatformDestroyListeners = config.platformInjector.get(PLATFORM_DESTROY_LISTENERS);
+ onPlatformDestroyListeners.add(destroyListener);
+
config.moduleRef.onDestroy(() => {
remove(config.allPlatformModules, config.moduleRef);
onErrorSubscription.unsubscribe();
+ onPlatformDestroyListeners.delete(destroyListener);
});
}
diff --git a/packages/core/src/platform/platform_ref.ts b/packages/core/src/platform/platform_ref.ts
index 600d00cf84c8..69d4eeef2035 100644
--- a/packages/core/src/platform/platform_ref.ts
+++ b/packages/core/src/platform/platform_ref.ts
@@ -79,7 +79,11 @@ export class PlatformRef {
allAppProviders,
);
- return bootstrap({moduleRef, allPlatformModules: this._modules});
+ return bootstrap({
+ moduleRef,
+ allPlatformModules: this._modules,
+ platformInjector: this.injector,
+ });
}
/**
diff --git a/packages/core/src/render3/after_render/manager.ts b/packages/core/src/render3/after_render/manager.ts
index 6c876bbcdd70..3077ea920055 100644
--- a/packages/core/src/render3/after_render/manager.ts
+++ b/packages/core/src/render3/after_render/manager.ts
@@ -82,6 +82,9 @@ export class AfterRenderImpl {
sequence.afterRun();
if (sequence.once) {
this.sequences.delete(sequence);
+ // Destroy the sequence so its on destroy callbacks can be cleaned up
+ // immediately, instead of waiting until the injector is destroyed.
+ sequence.destroy();
}
}
diff --git a/packages/core/src/render3/view_ref.ts b/packages/core/src/render3/view_ref.ts
index 98db89e1db17..fdabd856d959 100644
--- a/packages/core/src/render3/view_ref.ts
+++ b/packages/core/src/render3/view_ref.ts
@@ -8,8 +8,9 @@
import {ChangeDetectorRef} from '../change_detection/change_detector_ref';
import {NotificationSource} from '../change_detection/scheduling/zoneless_scheduling';
+import type {ApplicationRef} from '../core';
import {RuntimeError, RuntimeErrorCode} from '../errors';
-import {EmbeddedViewRef, ViewRefTracker} from '../linker/view_ref';
+import {EmbeddedViewRef} from '../linker/view_ref';
import {removeFromArray} from '../util/array_utils';
import {assertEqual} from '../util/assert';
@@ -43,7 +44,7 @@ import {storeLViewOnDestroy, updateAncestorTraversalFlagsOnAttach} from './util/
interface ChangeDetectorRefInterface extends ChangeDetectorRef {}
export class ViewRef implements EmbeddedViewRef, ChangeDetectorRefInterface {
- private _appRef: ViewRefTracker | null = null;
+ private _appRef: ApplicationRef | null = null;
private _attachedToViewContainer = false;
get rootNodes(): any[] {
@@ -349,7 +350,7 @@ export class ViewRef implements EmbeddedViewRef, ChangeDetectorRefInterfac
detachViewFromDOM(this._lView[TVIEW], this._lView);
}
- attachToAppRef(appRef: ViewRefTracker) {
+ attachToAppRef(appRef: ApplicationRef) {
if (this._attachedToViewContainer) {
throw new RuntimeError(
RuntimeErrorCode.VIEW_ALREADY_ATTACHED,
diff --git a/packages/core/test/acceptance/after_render_hook_spec.ts b/packages/core/test/acceptance/after_render_hook_spec.ts
index 1a6cca09ae23..00ec5b8299b3 100644
--- a/packages/core/test/acceptance/after_render_hook_spec.ts
+++ b/packages/core/test/acceptance/after_render_hook_spec.ts
@@ -1394,6 +1394,40 @@ describe('after render hooks', () => {
appRef.tick();
}).toThrowError(/NG0103.*(Infinite change detection while refreshing application views)/);
});
+
+ it('should destroy after the hook has run', () => {
+ let hookRef: AfterRenderRef | null = null;
+ let afterRenderCount = 0;
+
+ @Component({selector: 'comp'})
+ class Comp {
+ constructor() {
+ hookRef = afterNextRender(() => {
+ afterRenderCount++;
+ });
+ }
+ }
+
+ TestBed.configureTestingModule({
+ declarations: [Comp],
+ ...COMMON_CONFIGURATION,
+ });
+ createAndAttachComponent(Comp);
+ const appRef = TestBed.inject(ApplicationRef);
+ const destroySpy = spyOn(hookRef!, 'destroy').and.callThrough();
+ expect(afterRenderCount).toBe(0);
+ expect(destroySpy).not.toHaveBeenCalled();
+
+ // Run once and ensure that it was called and then cleaned up.
+ appRef.tick();
+ expect(afterRenderCount).toBe(1);
+ expect(destroySpy).toHaveBeenCalledTimes(1);
+
+ // Make sure we're not retaining it.
+ appRef.tick();
+ expect(afterRenderCount).toBe(1);
+ expect(destroySpy).toHaveBeenCalledTimes(1);
+ });
});
describe('server', () => {
diff --git a/packages/core/testing/src/component_fixture.ts b/packages/core/testing/src/component_fixture.ts
index a9f7ee6ee357..8793bcfce4fc 100644
--- a/packages/core/testing/src/component_fixture.ts
+++ b/packages/core/testing/src/component_fixture.ts
@@ -111,6 +111,8 @@ export abstract class ComponentFixture {
* Set whether the fixture should autodetect changes.
*
* Also runs detectChanges once so that any existing change is detected.
+ *
+ * @param autoDetect Whether to autodetect changes. By default, `true`.
*/
abstract autoDetectChanges(autoDetect?: boolean): void;
diff --git a/packages/forms/src/model/abstract_model.ts b/packages/forms/src/model/abstract_model.ts
index 75cf9f6d1f79..8b03afc6d94f 100644
--- a/packages/forms/src/model/abstract_model.ts
+++ b/packages/forms/src/model/abstract_model.ts
@@ -91,6 +91,8 @@ export type FormControlStatus = 'VALID' | 'INVALID' | 'PENDING' | 'DISABLED';
export abstract class ControlEvent {
/**
* Form control from which this event is originated.
+ *
+ * Note: the type of the control can't be infered from T as the event can be emitted by any of child controls
*/
public abstract readonly source: AbstractControl;
}
diff --git a/packages/platform-server/src/utils.ts b/packages/platform-server/src/utils.ts
index 3a3d87edf4b6..4ee30ac9210f 100644
--- a/packages/platform-server/src/utils.ts
+++ b/packages/platform-server/src/utils.ts
@@ -194,18 +194,21 @@ async function _render(platformRef: PlatformRef, applicationRef: ApplicationRef)
}
appendServerContextInfo(applicationRef);
- const output = platformState.renderToString();
- // Destroy the application in a macrotask, this allows pending promises to be settled and errors
- // to be surfaced to the users.
- await new Promise((resolve) => {
+ return platformState.renderToString();
+}
+
+/**
+ * Destroy the application in a macrotask, this allows pending promises to be settled and errors
+ * to be surfaced to the users.
+ */
+function asyncDestroyPlatform(platformRef: PlatformRef): Promise {
+ return new Promise((resolve) => {
setTimeout(() => {
platformRef.destroy();
resolve();
}, 0);
});
-
- return output;
}
/**
@@ -248,9 +251,13 @@ export async function renderModule(
): Promise {
const {document, url, extraProviders: platformProviders} = options;
const platformRef = createServerPlatform({document, url, platformProviders});
- const moduleRef = await platformRef.bootstrapModule(moduleType);
- const applicationRef = moduleRef.injector.get(ApplicationRef);
- return _render(platformRef, applicationRef);
+ try {
+ const moduleRef = await platformRef.bootstrapModule(moduleType);
+ const applicationRef = moduleRef.injector.get(ApplicationRef);
+ return await _render(platformRef, applicationRef);
+ } finally {
+ await asyncDestroyPlatform(platformRef);
+ }
}
/**
@@ -280,7 +287,11 @@ export async function renderApplication(
return runAndMeasurePerf('renderApplication', async () => {
const platformRef = createServerPlatform(options);
- const applicationRef = await bootstrap();
- return _render(platformRef, applicationRef);
+ try {
+ const applicationRef = await bootstrap();
+ return await _render(platformRef, applicationRef);
+ } finally {
+ await asyncDestroyPlatform(platformRef);
+ }
});
}
diff --git a/packages/platform-server/test/integration_spec.ts b/packages/platform-server/test/integration_spec.ts
index dcfead07f56d..b2facece402c 100644
--- a/packages/platform-server/test/integration_spec.ts
+++ b/packages/platform-server/test/integration_spec.ts
@@ -42,6 +42,9 @@ import {
ViewEncapsulation,
ɵPendingTasks as PendingTasks,
ɵwhenStable as whenStable,
+ APP_INITIALIZER,
+ inject,
+ getPlatform,
} from '@angular/core';
import {SSR_CONTENT_INTEGRITY_MARKER} from '@angular/core/src/hydration/utils';
import {TestBed} from '@angular/core/testing';
@@ -1076,6 +1079,153 @@ class HiddenModule {}
);
},
);
+
+ it(
+ `should call onOnDestroy of a service after a successful render` +
+ `(standalone: ${isStandalone})`,
+ async () => {
+ let wasServiceNgOnDestroyCalled = false;
+
+ @Injectable({providedIn: 'root'})
+ class DestroyableService {
+ ngOnDestroy() {
+ wasServiceNgOnDestroyCalled = true;
+ }
+ }
+
+ const SuccessfulAppInitializerProviders = [
+ {
+ provide: APP_INITIALIZER,
+ useFactory: () => {
+ inject(DestroyableService);
+ return () => Promise.resolve(); // Success in APP_INITIALIZER
+ },
+ multi: true,
+ },
+ ];
+
+ @NgModule({
+ providers: SuccessfulAppInitializerProviders,
+ imports: [MyServerAppModule, ServerModule],
+ bootstrap: [MyServerApp],
+ })
+ class ServerSuccessfulAppInitializerModule {}
+
+ const ServerSuccessfulAppInitializerAppStandalone = getStandaloneBootstrapFn(
+ createMyServerApp(true),
+ SuccessfulAppInitializerProviders,
+ );
+
+ const options = {document: doc};
+ const bootstrap = isStandalone
+ ? renderApplication(ServerSuccessfulAppInitializerAppStandalone, options)
+ : renderModule(ServerSuccessfulAppInitializerModule, options);
+ await bootstrap;
+
+ expect(getPlatform()).withContext('PlatformRef should be destroyed').toBeNull();
+ expect(wasServiceNgOnDestroyCalled)
+ .withContext('DestroyableService.ngOnDestroy() should be called')
+ .toBeTrue();
+ },
+ );
+
+ it(
+ `should call onOnDestroy of a service after some APP_INITIALIZER fails ` +
+ `(standalone: ${isStandalone})`,
+ async () => {
+ let wasServiceNgOnDestroyCalled = false;
+
+ @Injectable({providedIn: 'root'})
+ class DestroyableService {
+ ngOnDestroy() {
+ wasServiceNgOnDestroyCalled = true;
+ }
+ }
+
+ const FailingAppInitializerProviders = [
+ {
+ provide: APP_INITIALIZER,
+ useFactory: () => {
+ inject(DestroyableService);
+ return () => Promise.reject('Error in APP_INITIALIZER');
+ },
+ multi: true,
+ },
+ ];
+
+ @NgModule({
+ providers: FailingAppInitializerProviders,
+ imports: [MyServerAppModule, ServerModule],
+ bootstrap: [MyServerApp],
+ })
+ class ServerFailingAppInitializerModule {}
+
+ const ServerFailingAppInitializerAppStandalone = getStandaloneBootstrapFn(
+ createMyServerApp(true),
+ FailingAppInitializerProviders,
+ );
+
+ const options = {document: doc};
+ const bootstrap = isStandalone
+ ? renderApplication(ServerFailingAppInitializerAppStandalone, options)
+ : renderModule(ServerFailingAppInitializerModule, options);
+ await expectAsync(bootstrap).toBeRejectedWith('Error in APP_INITIALIZER');
+
+ expect(getPlatform()).withContext('PlatformRef should be destroyed').toBeNull();
+ expect(wasServiceNgOnDestroyCalled)
+ .withContext('DestroyableService.ngOnDestroy() should be called')
+ .toBeTrue();
+ },
+ );
+
+ it(
+ `should call onOnDestroy of a service after an error happens in a root component's constructor ` +
+ `(standalone: ${isStandalone})`,
+ async () => {
+ let wasServiceNgOnDestroyCalled = false;
+
+ @Injectable({providedIn: 'root'})
+ class DestroyableService {
+ ngOnDestroy() {
+ wasServiceNgOnDestroyCalled = true;
+ }
+ }
+
+ @Component({
+ standalone: isStandalone,
+ selector: 'app',
+ template: `Works!`,
+ })
+ class MyServerFailingConstructorApp {
+ constructor() {
+ inject(DestroyableService);
+ throw 'Error in constructor of the root component';
+ }
+ }
+
+ @NgModule({
+ declarations: [MyServerFailingConstructorApp],
+ imports: [MyServerAppModule, ServerModule],
+ bootstrap: [MyServerFailingConstructorApp],
+ })
+ class MyServerFailingConstructorAppModule {}
+
+ const MyServerFailingConstructorAppStandalone = getStandaloneBootstrapFn(
+ MyServerFailingConstructorApp,
+ );
+ const options = {document: doc};
+ const bootstrap = isStandalone
+ ? renderApplication(MyServerFailingConstructorAppStandalone, options)
+ : renderModule(MyServerFailingConstructorAppModule, options);
+ await expectAsync(bootstrap).toBeRejectedWith(
+ 'Error in constructor of the root component',
+ );
+ expect(getPlatform()).withContext('PlatformRef should be destroyed').toBeNull();
+ expect(wasServiceNgOnDestroyCalled)
+ .withContext('DestroyableService.ngOnDestroy() should be called')
+ .toBeTrue();
+ },
+ );
});
});
diff --git a/packages/zone.js/MODULE.md b/packages/zone.js/MODULE.md
index a07cb8827489..1d0954514b49 100644
--- a/packages/zone.js/MODULE.md
+++ b/packages/zone.js/MODULE.md
@@ -4,145 +4,147 @@ Starting from zone.js v0.8.9, you can choose which web API modules you want to p
the below samples show how to disable some modules. You just need to define a few global variables
before loading zone.js.
-```
-
-
+```html
+
+
```
Below is the full list of currently supported modules.
-- Common
-
-|Module Name|Behavior with zone.js patch|How to disable|
-|--|--|--|
-|Error|stack frames will have the Zone's name information, (By default, Error patch will not be loaded by zone.js)|__Zone_disable_Error = true|
-|toString|Function.toString will be patched to return native version of toString|__Zone_disable_toString = true|
-|ZoneAwarePromise|Promise.then will be patched as Zone aware MicroTask|__Zone_disable_ZoneAwarePromise = true|
-|bluebird|Bluebird will use Zone.scheduleMicroTask as async scheduler. (By default, bluebird patch will not be loaded by zone.js)|__Zone_disable_bluebird = true|
-
-- Browser
-
-|Module Name|Behavior with zone.js patch|How to disable|
-|--|--|--|
-|on_property|target.onProp will become zone aware target.addEventListener(prop)|__Zone_disable_on_property = true|
-|timers|setTimeout/setInterval/setImmediate will be patched as Zone MacroTask|__Zone_disable_timers = true|
-|requestAnimationFrame|requestAnimationFrame will be patched as Zone MacroTask|__Zone_disable_requestAnimationFrame = true|
-|blocking|alert/prompt/confirm will be patched as Zone.run|__Zone_disable_blocking = true|
-|EventTarget|target.addEventListener will be patched as Zone aware EventTask|__Zone_disable_EventTarget = true|
-|MutationObserver|MutationObserver will be patched as Zone aware operation|__Zone_disable_MutationObserver = true|
-|IntersectionObserver|Intersection will be patched as Zone aware operation|__Zone_disable_IntersectionObserver = true|
-|FileReader|FileReader will be patched as Zone aware operation|__Zone_disable_FileReader = true|
-|canvas|HTMLCanvasElement.toBlob will be patched as Zone aware operation|__Zone_disable_canvas = true|
-|IE BrowserTools check|in IE, browser tool will not use zone patched eventListener|__Zone_disable_IE_check = true|
-|CrossContext check|in webdriver, enable check event listener is cross context|__Zone_enable_cross_context_check = true|
-|XHR|XMLHttpRequest will be patched as Zone aware MacroTask|__Zone_disable_XHR = true|
-|geolocation|navigator.geolocation's prototype will be patched as Zone.run|__Zone_disable_geolocation = true|
-|PromiseRejectionEvent|PromiseRejectEvent will fire when ZoneAwarePromise has unhandled error|__Zone_disable_PromiseRejectionEvent = true|
-|mediaQuery|mediaQuery addListener API will be patched as Zone aware EventTask. (By default, mediaQuery patch will not be loaded by zone.js) |__Zone_disable_mediaQuery = true|
-|notification|notification onProperties API will be patched as Zone aware EventTask. (By default, notification patch will not be loaded by zone.js) |__Zone_disable_notification = true|
-|MessagePort|MessagePort onProperties APIs will be patched as Zone aware EventTask. (By default, MessagePort patch will not be loaded by zone.js) |__Zone_disable_MessagePort = true|
-
-- NodeJS
-
-|Module Name|Behavior with zone.js patch|How to disable|
-|--|--|--|
-|node_timers|NodeJS patch timer|__Zone_disable_node_timers = true|
-|fs|NodeJS patch fs function as macroTask|__Zone_disable_fs = true|
-|EventEmitter|NodeJS patch EventEmitter as Zone aware EventTask|__Zone_disable_EventEmitter = true|
-|nextTick|NodeJS patch process.nextTick as microTask|__Zone_disable_nextTick = true|
-|handleUnhandledPromiseRejection|NodeJS handle unhandledPromiseRejection from ZoneAwarePromise|__Zone_disable_handleUnhandledPromiseRejection = true|
-|crypto|NodeJS patch crypto function as macroTask|__Zone_disable_crypto = true|
-
-- Test Framework
-
-|Module Name|Behavior with zone.js patch|How to disable|
-|--|--|--|
-|Jasmine|Jasmine APIs patch|__Zone_disable_jasmine = true|
-|Mocha|Mocha APIs patch|__Zone_disable_mocha = true|
-
-- on_property
-
-You can also disable specific on_properties by setting `__Zone_ignore_on_properties` as follows: for example,
-if you want to disable `window.onmessage` and `HTMLElement.prototype.onclick` from zone.js patching,
-you can do like this.
-
+### Common
+
+| Module Name | Behavior with zone.js patch | How to disable |
+| ---------------- | ----------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- |
+| Error | stack frames will have the Zone's name information, (By default, Error patch will not be loaded by zone.js) | \_\_Zone_disable_Error = true |
+| toString | Function.toString will be patched to return native version of toString | \_\_Zone_disable_toString = true |
+| ZoneAwarePromise | Promise.then will be patched as Zone aware MicroTask | \_\_Zone_disable_ZoneAwarePromise = true |
+| bluebird | Bluebird will use Zone.scheduleMicroTask as async scheduler. (By default, bluebird patch will not be loaded by zone.js) | \_\_Zone_disable_bluebird = true |
+
+### Browser
+
+| Module Name | Behavior with zone.js patch | How to disable |
+| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- |
+| on_property | target.onProp will become zone aware target.addEventListener(prop) | \_\_Zone_disable_on_property = true |
+| timers | setTimeout/setInterval/setImmediate will be patched as Zone MacroTask | \_\_Zone_disable_timers = true |
+| requestAnimationFrame | requestAnimationFrame will be patched as Zone MacroTask | \_\_Zone_disable_requestAnimationFrame = true |
+| blocking | alert/prompt/confirm will be patched as Zone.run | \_\_Zone_disable_blocking = true |
+| EventTarget | target.addEventListener will be patched as Zone aware EventTask | \_\_Zone_disable_EventTarget = true |
+| MutationObserver | MutationObserver will be patched as Zone aware operation | \_\_Zone_disable_MutationObserver = true |
+| IntersectionObserver | Intersection will be patched as Zone aware operation | \_\_Zone_disable_IntersectionObserver = true |
+| FileReader | FileReader will be patched as Zone aware operation | \_\_Zone_disable_FileReader = true |
+| canvas | HTMLCanvasElement.toBlob will be patched as Zone aware operation | \_\_Zone_disable_canvas = true |
+| IE BrowserTools check | in IE, browser tool will not use zone patched eventListener | \_\_Zone_disable_IE_check = true |
+| CrossContext check | in webdriver, enable check event listener is cross context | \_\_Zone_enable_cross_context_check = true |
+| `beforeunload` | enable the default `beforeunload` handling behavior, where event handlers return strings to prompt the user | **zone_symbol**enable_beforeunload = true |
+| XHR | XMLHttpRequest will be patched as Zone aware MacroTask | \_\_Zone_disable_XHR = true |
+| geolocation | navigator.geolocation's prototype will be patched as Zone.run | \_\_Zone_disable_geolocation = true |
+| PromiseRejectionEvent | PromiseRejectEvent will fire when ZoneAwarePromise has unhandled error | \_\_Zone_disable_PromiseRejectionEvent = true |
+| mediaQuery | mediaQuery addListener API will be patched as Zone aware EventTask. (By default, mediaQuery patch will not be loaded by zone.js) | \_\_Zone_disable_mediaQuery = true |
+| notification | notification onProperties API will be patched as Zone aware EventTask. (By default, notification patch will not be loaded by zone.js) | \_\_Zone_disable_notification = true |
+| MessagePort | MessagePort onProperties APIs will be patched as Zone aware EventTask. (By default, MessagePort patch will not be loaded by zone.js) | \_\_Zone_disable_MessagePort = true |
+
+### Node.js
+
+| Module Name | Behavior with zone.js patch | How to disable |
+| ------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------- |
+| node_timers | NodeJS patch timer | \_\_Zone_disable_node_timers = true |
+| fs | NodeJS patch fs function as macroTask | \_\_Zone_disable_fs = true |
+| EventEmitter | NodeJS patch EventEmitter as Zone aware EventTask | \_\_Zone_disable_EventEmitter = true |
+| nextTick | NodeJS patch process.nextTick as microTask | \_\_Zone_disable_nextTick = true |
+| handleUnhandledPromiseRejection | NodeJS handle unhandledPromiseRejection from ZoneAwarePromise | \_\_Zone_disable_handleUnhandledPromiseRejection = true |
+| crypto | NodeJS patch crypto function as macroTask | \_\_Zone_disable_crypto = true |
+
+### Test Framework
+
+| Module Name | Behavior with zone.js patch | How to disable |
+| ----------- | --------------------------- | ------------------------------- |
+| Jasmine | Jasmine APIs patch | \_\_Zone_disable_jasmine = true |
+| Mocha | Mocha APIs patch | \_\_Zone_disable_mocha = true |
+
+### `on` properties
+
+You can also disable specific `on` properties by setting `__Zone_ignore_on_properties` as follows. For example, if you want to disable `window.onmessage` and `HTMLElement.prototype.onclick` from zone.js patching, you can do so like this:
+
+```html
+
+
```
-
-
+
+Excluding `on` properties from being patched means that callbacks will always be invoked within the root context, regardless of where the `on` callback has been set. Even if `onclick` is set within a child zone, the callback will be called inside the root zone:
+
+```ts
+Zone.current.fork({ name: 'child' }).run(() => {
+ document.body.onclick = () => {
+ console.log(Zone.current); //
+ };
+});
```
-- Error
+You can find more information on adding unpatched events via `addEventListener`, please refer to [UnpatchedEvents](./STANDARD-APIS.md#unpatchedevents).
+
+### Error
-By default, `zone.js/plugins/zone-error` will not be loaded for performance concern.
-This package will provide following functionality.
+By default, `zone.js/plugins/zone-error` will not be loaded for performance reasons.
+This package provides the following functionality:
- 1. Error inherit: handle `extend Error` issue.
- ```
- class MyError extends Error {}
- const myError = new MyError();
- console.log('is MyError instanceof Error', (myError instanceof Error));
- ```
+1. **Error Inheritance:** Handle the `extend Error` issue:
- without `zone-error` patch, the example above will output `false`, with the patch, the result will be `true`.
+ ```ts
+ class MyError extends Error {}
+ const myError = new MyError();
+ console.log('is MyError instanceof Error', (myError instanceof Error));
+ ```
- 2. ZoneJsInternalStackFrames: remove zone.js stack from `stackTrace`, and add `zone` information. Without this patch, a lot of `zone.js` invocation stack will be shown
- in stack frames.
+ Without the `zone-error` patch, the example above will output `false`. With the patch, the result will be `true`.
- ```
- at zone.run (polyfill.bundle.js: 3424)
- at zoneDelegate.invokeTask (polyfill.bundle.js: 3424)
- at zoneDelegate.runTask (polyfill.bundle.js: 3424)
- at zone.drainMicroTaskQueue (polyfill.bundle.js: 3424)
- at a.b.c (vendor.bundle.js: 12345 )
- at d.e.f (main.bundle.js: 23456)
- ```
+2. **ZoneJsInternalStackFrames:** Remove the zone.js stack from `stackTrace` and add `zone` information. Without this patch, many `zone.js` invocation stacks will be displayed in the stack frames.
- with this patch, those zone frames will be removed,
- and the zone information `/` will be added
+ ```
+ at zone.run (polyfill.bundle.js: 3424)
+ at zoneDelegate.invokeTask (polyfill.bundle.js: 3424)
+ at zoneDelegate.runTask (polyfill.bundle.js: 3424)
+ at zone.drainMicroTaskQueue (polyfill.bundle.js: 3424)
+ at a.b.c (vendor.bundle.js: 12345 )
+ at d.e.f (main.bundle.js: 23456)
+ ```
- ```
- at a.b.c (vendor.bundle.js: 12345 )
- at d.e.f (main.bundle.js: 23456 )
- ```
+ With this patch, those zone frames will be removed, and the zone information `/` will be added.
- The second feature will slow down the `Error` performance, so `zone.js` provide a flag to let you be able to control the behavior.
- The flag is `__Zone_Error_ZoneJsInternalStackFrames_policy`. And the available options is:
+ ```
+ at a.b.c (vendor.bundle.js: 12345 )
+ at d.e.f (main.bundle.js: 23456 )
+ ```
- 1. default: this is the default one, if you load `zone.js/plugins/zone-error` without
- setting the flag, `default` will be used, and `ZoneJsInternalStackFrames` will be available
- when `new Error()`, you can get a `error.stack` which is `zone stack free`. But this
- will slow down `new Error()` a little bit.
+The second feature may slow down `Error` performance, so `zone.js` provides a flag that allows you to control this behavior.
+The flag is `__Zone_Error_ZoneJsInternalStackFrames_policy`. The available options are:
- 2. disable: this will disable `ZoneJsInternalStackFrames` feature, and if you load
- `zone.js/plugins/zone-error`, you will only get a `wrapped Error` which can handle
- `Error inherit` issue.
+1. **default:** This is the default setting. If you load `zone.js/plugins/zone-error` without setting the flag, `default` will be used. In this case, `ZoneJsInternalStackFrames` will be available when using `new Error()`, allowing you to obtain an `error.stack` that is zone-stack-free. However, this may slightly slow down the performance of new `Error()`.
- 3. lazy: this is a feature to let you be able to get `ZoneJsInternalStackFrames` feature,
- but not impact performance. But as a trade off, you can't get the `zone free stack
- frames` by access `error.stack`. You can only get it by access `error.zoneAwareStack`.
+2. **disable:** This option will disable the `ZoneJsInternalStackFrames` feature. If you load `zone.js/plugins/zone-error`, you will only receive a wrapped `Error`, which can handle the `Error` inheritance issue.
+3. **lazy:** This feature allows you to access `ZoneJsInternalStackFrames` without impacting performance. However, as a trade-off, you won't be able to obtain the zone-free stack frames via `error.stack`. You can only access them through `error.zoneAwareStack`.
-- Angular(2+)
+### Angular
-Angular uses zone.js to manage async operations and decide when to perform change detection. Thus, in Angular,
-the following APIs should be patched, otherwise Angular may not work as expected.
+Angular uses zone.js to manage asynchronous operations and determine when to perform change detection. Therefore, in Angular, the following APIs should be patched; otherwise, Angular may not work as expected:
1. ZoneAwarePromise
2. timer
diff --git a/packages/zone.js/lib/common/fetch.ts b/packages/zone.js/lib/common/fetch.ts
index c22edea28fb7..3b8d1a2a5b80 100644
--- a/packages/zone.js/lib/common/fetch.ts
+++ b/packages/zone.js/lib/common/fetch.ts
@@ -99,22 +99,31 @@ export function patchFetch(Zone: ZoneType): void {
options.signal = fetchSignal;
args[1] = options;
+ let onAbort: () => void;
if (signal) {
const nativeAddEventListener =
signal[Zone.__symbol__('addEventListener') as 'addEventListener'] ||
signal.addEventListener;
- nativeAddEventListener.call(
- signal,
- 'abort',
- function () {
- ac!.abort();
- },
- {once: true},
- );
+ onAbort = () => ac!.abort();
+ nativeAddEventListener.call(signal, 'abort', onAbort, {once: true});
}
- return createFetchTask('fetch', {fetchArgs: args} as FetchTaskData, fetch, this, args, ac);
+ return createFetchTask(
+ 'fetch',
+ {fetchArgs: args} as FetchTaskData,
+ fetch,
+ this,
+ args,
+ ac,
+ ).finally(() => {
+ // We need to be good citizens and remove the `abort` listener once
+ // the fetch is settled. The `abort` listener may not be called at all,
+ // which means the event listener closure would retain a reference to
+ // the `ac` object even if it goes out of scope. Since browser's garbage
+ // collectors work differently, some may not be smart enough to collect a signal.
+ signal?.removeEventListener('abort', onAbort);
+ });
};
if (OriginalResponse?.prototype) {
diff --git a/packages/zone.js/test/common/fetch.spec.ts b/packages/zone.js/test/common/fetch.spec.ts
index 7e6c642972c9..ca8b60761ab8 100644
--- a/packages/zone.js/test/common/fetch.spec.ts
+++ b/packages/zone.js/test/common/fetch.spec.ts
@@ -169,6 +169,10 @@ describe(
'invokeTask:fetch:macroTask',
'scheduleTask:Promise.then:microTask',
'invokeTask:Promise.then:microTask',
+
+ // This is the `finally` task, which is used for cleanup.
+ 'scheduleTask:Promise.then:microTask',
+ 'invokeTask:Promise.then:microTask',
]);
done();
},
@@ -194,6 +198,11 @@ describe(
'invokeTask:fetch:macroTask',
'scheduleTask:Promise.then:microTask',
'invokeTask:Promise.then:microTask',
+
+ // This is the `finally` task, which is used for cleanup.
+ 'scheduleTask:Promise.then:microTask',
+ 'invokeTask:Promise.then:microTask',
+
// Please refer to the issue link above. Previously, `Response` methods were not
// patched by zone.js, and their return values were considered only as
// microtasks (not macrotasks). The Angular zone stabilized prematurely,
diff --git a/renovate.json b/renovate.json
index 0fb3d514cd05..4aed7e39a8ff 100644
--- a/renovate.json
+++ b/renovate.json
@@ -45,7 +45,9 @@
"unist-util-visit",
"unist-util-visit-parents",
"rules_pkg",
- "aspect_bazel_lib"
+ "aspect_bazel_lib",
+ "@bazel/runfiles",
+ "build_bazel_rules_nodejs"
],
"packageRules": [
{