mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 096fdd0f59 | |||
| 9998bee5f4 | |||
| 95387047fd | |||
| 899e51ad8e | |||
| 0ba2b5e5c3 | |||
| 9f490da7e2 | |||
| b95b5b55d8 | |||
| 26781fd0fe | |||
| f79fd35042 | |||
| dfa772d686 | |||
| c7c0723c7f | |||
| e01549b26b | |||
| cf62063174 | |||
| c0c6d1d892 | |||
| d1fa6cd7d9 | |||
| 1472515d68 |
@@ -1124,6 +1124,7 @@ groups:
|
||||
users:
|
||||
- AleksanderBodurri
|
||||
- devversion
|
||||
- dgp1130
|
||||
- josephperrott
|
||||
- mgechev
|
||||
- twerske
|
||||
|
||||
+33
-89
@@ -1,3 +1,36 @@
|
||||
<a name="16.2.0"></a>
|
||||
# 16.2.0 (2023-08-09)
|
||||
### benchpress
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [dd850b2ab7](https://github.com/angular/angular/commit/dd850b2ab781f24065550f8a948ced498e0f1e99) | fix | correctly report GC memory amounts ([#50760](https://github.com/angular/angular/pull/50760)) |
|
||||
### common
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [29d358170b](https://github.com/angular/angular/commit/29d358170b046f4a6773dfdfbbd1050f54deb301) | feat | add component input binding support for NgComponentOutlet ([#51148](https://github.com/angular/angular/pull/51148)) |
|
||||
| [1837efb9da](https://github.com/angular/angular/commit/1837efb9daf5c8e86a99a06ecc77bb42bc60dbb0) | feat | Allow ngSrc to be changed post-init ([#50683](https://github.com/angular/angular/pull/50683)) |
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [c27a1e61d6](https://github.com/angular/angular/commit/c27a1e61d64a67aa169086f7db11bcfd5bb7d2fc) | feat | scope selectors in @scope queries ([#50747](https://github.com/angular/angular/pull/50747)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [12bad6576d](https://github.com/angular/angular/commit/12bad6576d2ffe4667118b214d9c7598ed3d8edb) | fix | libraries compiled with v16.1+ breaking with Angular framework v16.0.x ([#50714](https://github.com/angular/angular/pull/50714)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [e53d4ecf4c](https://github.com/angular/angular/commit/e53d4ecf4cfd9e64d6ba8c8b19adbb7df9cfc047) | feat | add afterRender and afterNextRender ([#50607](https://github.com/angular/angular/pull/50607)) |
|
||||
| [98d262fd27](https://github.com/angular/angular/commit/98d262fd27795014ee3988b08d3c48a0dfb63c40) | feat | create injector debugging APIs ([#48639](https://github.com/angular/angular/pull/48639)) |
|
||||
| [cdaa2a8a9e](https://github.com/angular/angular/commit/cdaa2a8a9eab490b55bbb841ede4f54a2656df30) | feat | support Provider type in Injector.create ([#49587](https://github.com/angular/angular/pull/49587)) |
|
||||
| [9f490da7e2](https://github.com/angular/angular/commit/9f490da7e27e495cb45d2064af9091731422a6b1) | fix | handle hydration of view containers for root components ([#51247](https://github.com/angular/angular/pull/51247)) |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [0b14e4ef74](https://github.com/angular/angular/commit/0b14e4ef742b1c0f73d873e2c337683b60f46845) | feat | exposes the `fixture` of the `RouterTestingHarness` ([#50280](https://github.com/angular/angular/pull/50280)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="16.1.8"></a>
|
||||
# 16.1.8 (2023-08-02)
|
||||
### compiler
|
||||
@@ -7,19 +40,6 @@
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="16.2.0-next.4"></a>
|
||||
# 16.2.0-next.4 (2023-07-26)
|
||||
### common
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [29d358170b](https://github.com/angular/angular/commit/29d358170b046f4a6773dfdfbbd1050f54deb301) | feat | add component input binding support for NgComponentOutlet ([#51148](https://github.com/angular/angular/pull/51148)) |
|
||||
### http
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [57e8412e53](https://github.com/angular/angular/commit/57e8412e53229b0671df02c55be52e88b66a6865) | fix | check whether `Zone` is defined ([#51119](https://github.com/angular/angular/pull/51119)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="16.1.7"></a>
|
||||
# 16.1.7 (2023-07-26)
|
||||
### http
|
||||
@@ -29,15 +49,6 @@
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="16.2.0-next.3"></a>
|
||||
# 16.2.0-next.3 (2023-07-19)
|
||||
### http
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [c5608e5ca9](https://github.com/angular/angular/commit/c5608e5ca99805af1a3a7caf4ce28a35f3a13ebf) | fix | Run fetch request out the angular zone ([#50981](https://github.com/angular/angular/pull/50981)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="16.1.6"></a>
|
||||
# 16.1.6 (2023-07-19)
|
||||
### http
|
||||
@@ -47,38 +58,6 @@
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="16.2.0-next.2"></a>
|
||||
# 16.2.0-next.2 (2023-07-13)
|
||||
### animations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [a14bdfe859](https://github.com/angular/angular/commit/a14bdfe8591a33d359bf4940f4efa828499a6373) | fix | Ensure elements are removed from the cache after leave animation. ([#50929](https://github.com/angular/angular/pull/50929)) |
|
||||
### common
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [1837efb9da](https://github.com/angular/angular/commit/1837efb9daf5c8e86a99a06ecc77bb42bc60dbb0) | feat | Allow ngSrc to be changed post-init ([#50683](https://github.com/angular/angular/pull/50683)) |
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [c27a1e61d6](https://github.com/angular/angular/commit/c27a1e61d64a67aa169086f7db11bcfd5bb7d2fc) | feat | scope selectors in @scope queries ([#50747](https://github.com/angular/angular/pull/50747)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [031b599a55](https://github.com/angular/angular/commit/031b599a5528e1df5779695eb75b738a5a3076fe) | fix | ensure that standalone components get correct injector instances ([#50954](https://github.com/angular/angular/pull/50954)) |
|
||||
| [5d6ec0336b](https://github.com/angular/angular/commit/5d6ec0336bdea22735d0ca2bbd7cfad958efbdda) | fix | handle `deref` returning `null` on `RefactiveNode`. ([#50992](https://github.com/angular/angular/pull/50992)) |
|
||||
| [b66a16ec4c](https://github.com/angular/angular/commit/b66a16ec4cf42f47efeafa711ec301efeda272be) | fix | use `setTimeout` when coalescing tasks in Node.js ([#50820](https://github.com/angular/angular/pull/50820)) |
|
||||
### platform-browser
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [f1b629961f](https://github.com/angular/angular/commit/f1b629961f75d535982a6196e70b82f177587ebf) | perf | do not remove renderer from cache when `REMOVE_STYLES_ON_COMPONENT_DESTROY` is enabled. ([#51005](https://github.com/angular/angular/pull/51005)) |
|
||||
### upgrade
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [a19a87df6c](https://github.com/angular/angular/commit/a19a87df6c30a60ca997083b979a8e1e185e5b43) | fix | allow for downgraded components to work with component-router ([#50871](https://github.com/angular/angular/pull/50871)) |
|
||||
| [253d756464](https://github.com/angular/angular/commit/253d7564647607804d45404152d1253993177aea) | fix | Use `takeUntil` on leaky subscription. ([#50901](https://github.com/angular/angular/pull/50901)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="16.1.5"></a>
|
||||
# 16.1.5 (2023-07-13)
|
||||
### animations
|
||||
@@ -114,28 +93,6 @@
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="16.2.0-next.1"></a>
|
||||
# 16.2.0-next.1 (2023-06-28)
|
||||
### benchpress
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [dd850b2ab7](https://github.com/angular/angular/commit/dd850b2ab781f24065550f8a948ced498e0f1e99) | fix | correctly report GC memory amounts ([#50760](https://github.com/angular/angular/pull/50760)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [29340a0678](https://github.com/angular/angular/commit/29340a06789652e359e61b32f1814dcd20d9bd26) | fix | expose input transform function on ComponentFactory and ComponentMirror ([#50713](https://github.com/angular/angular/pull/50713)) |
|
||||
### elements
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [d64864e95e](https://github.com/angular/angular/commit/d64864e95e193e46180aeaf0d634152327650871) | fix | support input transform functions ([#50713](https://github.com/angular/angular/pull/50713)) |
|
||||
### platform-browser
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [2b55103e94](https://github.com/angular/angular/commit/2b55103e94578ab1cb765147077e82e1228b0dbb) | fix | wait until animation completion before destroying renderer ([#50677](https://github.com/angular/angular/pull/50677)) |
|
||||
| [0380564f85](https://github.com/angular/angular/commit/0380564f8562f5971cff671319439ad0f2b40a7e) | fix | wait until animation completion before destroying renderer ([#50860](https://github.com/angular/angular/pull/50860)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="16.1.3"></a>
|
||||
# 16.1.3 (2023-06-28)
|
||||
### core
|
||||
@@ -154,19 +111,6 @@
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="16.2.0-next.0"></a>
|
||||
# 16.2.0-next.0 (2023-06-21)
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [12bad6576d](https://github.com/angular/angular/commit/12bad6576d2ffe4667118b214d9c7598ed3d8edb) | fix | libraries compiled with v16.1+ breaking with Angular framework v16.0.x ([#50714](https://github.com/angular/angular/pull/50714)) |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [0b14e4ef74](https://github.com/angular/angular/commit/0b14e4ef742b1c0f73d873e2c337683b60f46845) | feat | exposes the `fixture` of the `RouterTestingHarness` ([#50280](https://github.com/angular/angular/pull/50280)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="16.1.2"></a>
|
||||
# 16.1.2 (2023-06-21)
|
||||
### http
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Index</title>
|
||||
<base href="/">
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular.js"></script>
|
||||
<script src="app/ajs-bootstrap/app.module.js"></script>
|
||||
|
||||
@@ -124,6 +124,13 @@ v16 - v19
|
||||
| `@angular/platform-browser` | [`BrowserModule.withServerTransition`](api/platform-browser/BrowserModule#withservertransition) | v16 | v18 |
|
||||
| `@angular/platform-browser` | [`makeStateKey`, `StateKey` and `TransferState`](#platform-browser), symbols were moved to `@angular/core` | v16 | v18 |
|
||||
|
||||
|
||||
### Deprecated features that can be removed in v19 or later
|
||||
|
||||
| Area | API or Feature | Deprecated in | May be removed in |
|
||||
|:--- |:--- |:--- |:--- |
|
||||
| `@angular/core` | `PACKAGE_ROOT_URL` | v17 | v19 |
|
||||
|
||||
### Deprecated features with no planned removal version
|
||||
|
||||
| Area | API or Feature | Deprecated in | May be removed in |
|
||||
|
||||
@@ -58,7 +58,7 @@ The `<ng-template>` element is a good choice for dynamic components because it d
|
||||
Take a closer look at the methods in `ad-banner.component.ts`.
|
||||
|
||||
`AdBannerComponent` takes an array of `AdItem` objects as input, which ultimately comes from `AdService`.
|
||||
`AdItem` objects specify the type of component to load and any data to bind to the component.`AdService` returns the actual ads making up the ad campaign.
|
||||
`AdItem` objects specify the type of component to load and any data to bind to the component. `AdService` returns the actual ads making up the ad campaign.
|
||||
|
||||
Passing an array of components to `AdBannerComponent` allows for a dynamic list of ads without static elements in the template.
|
||||
|
||||
|
||||
@@ -138,6 +138,97 @@ data$ = http.get('...').pipe(takeUntilDestroyed());
|
||||
|
||||
By default, `takeUntilDestroyed` must be called in an [injection context](/guide/dependency-injection-context) so that it can access `DestroyRef`. If an injection context isn't available, you can explicitly provide a `DestroyRef`.
|
||||
|
||||
## Reading and writing the DOM
|
||||
|
||||
Sometimes it's necessary to use browser-only APIs to manually read or write the DOM. This can be challenging to do with the [lifecycle events](#lifecycle-event-sequence) above, as they will also run during [server-side rendering and pre-rendering](guide/glossary#server-side-rendering). For this purpose, Angular provides `afterRender` and `afterNextRender`. These functions can be used unconditionally, but will only have an effect on the browser. Both functions accept a callback that will run after the next [change detection](/guide/glossary#change-detection) cycle (including any nested cycles) has completed.
|
||||
|
||||
<div class="alert is-important">
|
||||
|
||||
`afterRender` and `afterNextRender` are available for [developer preview](/guide/releases#developer-preview). They are ready for you to try, but they might change before they are stable.
|
||||
|
||||
</div>
|
||||
|
||||
| Function | Purpose | Timing |
|
||||
| ------ | ------- | ------ |
|
||||
| `afterNextRender` | Perform one-time initialization, or observe a single, specific change to the DOM. <br /> <div class="alert is-helpful">As a rule of thumb, you should use `afterRender` instead if you need to manually read or write any layout data such as size or location.</div> See details in [One-time initialization](#one-time-initialization) in this document. | _Once_ after the next change detection cycle. |
|
||||
| `afterRender` | Synchronize state with the DOM. See details in [Handling synchronization](#handling-synchronization) in this document. | After _every_ change detection cycle that follows. |
|
||||
|
||||
### One-time initialization
|
||||
|
||||
Generally, you will want to use `afterNextRender` to perform any one-time initialization, such as for a third-party library, or for browser-only APIs.
|
||||
|
||||
```ts
|
||||
@Component({
|
||||
selector: 'my-chart-cmp',
|
||||
template: `<div #chart>{{ ... }}</div>`,
|
||||
})
|
||||
export class MyChartCmp {
|
||||
@ViewChild('chart') chartRef: ElementRef;
|
||||
chart: MyChart|null;
|
||||
|
||||
constructor() {
|
||||
afterNextRender(() => {
|
||||
this.chart = new MyChart(this.chartRef.nativeElement);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Instead of attempting to recreate their behaviors with `afterRender`, you should prefer to use built-in browser APIs like `ResizeObserver` and `IntersectionObserver` wherever possible. You can use `afterNextRender` to safely initialize such APIs on the browser only.
|
||||
|
||||
```ts
|
||||
@Component({
|
||||
selector: 'my-cmp',
|
||||
template: `<span #content>{{ ... }}</span>`,
|
||||
})
|
||||
export class MyComponent {
|
||||
resizeObserver: ResizeObserver|null = null;
|
||||
@ViewChild('content') contentRef: ElementRef;
|
||||
|
||||
constructor() {
|
||||
afterNextRender(() => {
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
console.log('Content was resized');
|
||||
});
|
||||
|
||||
this.resizeObserver.observe(this.contentRef.nativeElement);
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.resizeObserver?.disconnect();
|
||||
this.resizeObserver = null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<div class="alert is-important">
|
||||
|
||||
As a rule of thumb, `afterNextRender` should be used to observe _discrete_ changes to the DOM, such as element creation or deletion. For manually reading or writing data that tends to change frequently, such as size or location, you should generally prefer to use `afterRender` instead.
|
||||
|
||||
</div>
|
||||
|
||||
### Handling synchronization
|
||||
|
||||
As an escape hatch for when the browser does not provide a better API to do so, you can use `afterRender` to perform any additional read or writes to the DOM every time Angular finishes mutating it.
|
||||
|
||||
```ts
|
||||
@Component({
|
||||
selector: 'my-cmp',
|
||||
template: `<span #content>{{ ... }}</span>`,
|
||||
})
|
||||
export class MyComponent {
|
||||
@ViewChild('content') contentRef: ElementRef;
|
||||
|
||||
constructor() {
|
||||
afterRender(() => {
|
||||
const elem = this.contentRef.nativeElement;
|
||||
console.log(`content position: (${elem.offsetLeft}, ${elem.offsetTop})`);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## General examples
|
||||
|
||||
The following examples demonstrate the call sequence and relative frequency of the various lifecycle events, and how the hooks can be used separately or together for components and directives.
|
||||
|
||||
@@ -85,6 +85,8 @@ export const enum RuntimeErrorCode {
|
||||
// (undocumented)
|
||||
MISSING_REQUIRED_INJECTABLE_IN_BOOTSTRAP = 402,
|
||||
// (undocumented)
|
||||
MISSING_SSR_CONTENT_INTEGRITY_MARKER = 507,
|
||||
// (undocumented)
|
||||
MISSING_ZONEJS = 908,
|
||||
// (undocumented)
|
||||
MULTIPLE_COMPONENTS_MATCH = -300,
|
||||
|
||||
@@ -1126,7 +1126,7 @@ export interface OutputDecorator {
|
||||
new (alias?: string): any;
|
||||
}
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export const PACKAGE_ROOT_URL: InjectionToken<string>;
|
||||
|
||||
// @public
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "angular-srcs",
|
||||
"version": "16.2.0-next.4",
|
||||
"version": "16.2.0",
|
||||
"private": true,
|
||||
"description": "Angular - a web framework for modern web apps",
|
||||
"homepage": "https://github.com/angular/angular",
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
/**
|
||||
* DOMAnimation represents the Animation Web API.
|
||||
*
|
||||
* It is an external API by the browser, and must thus use "declare interface",
|
||||
* to prevent renaming by Closure Compiler.
|
||||
*
|
||||
* @see https://developer.mozilla.org/de/docs/Web/API/Animation
|
||||
*/
|
||||
export declare interface DOMAnimation {
|
||||
cancel(): void;
|
||||
play(): void;
|
||||
pause(): void;
|
||||
finish(): void;
|
||||
onfinish: Function;
|
||||
position: number;
|
||||
currentTime: number;
|
||||
addEventListener(eventName: string, handler: (event: any) => any): any;
|
||||
dispatchEvent(eventName: string): any;
|
||||
}
|
||||
@@ -10,8 +10,6 @@ import {AnimationPlayer, ɵStyleDataMap} from '@angular/animations';
|
||||
import {computeStyle} from '../../util';
|
||||
import {SpecialCasedStyles} from '../special_cased_styles';
|
||||
|
||||
import {DOMAnimation} from './dom_animation';
|
||||
|
||||
export class WebAnimationsPlayer implements AnimationPlayer {
|
||||
private _onDoneFns: Function[] = [];
|
||||
private _onStartFns: Function[] = [];
|
||||
@@ -31,7 +29,7 @@ export class WebAnimationsPlayer implements AnimationPlayer {
|
||||
private _originalOnStartFns: Function[] = [];
|
||||
|
||||
// using non-null assertion because it's re(set) by init();
|
||||
public readonly domPlayer!: DOMAnimation;
|
||||
public readonly domPlayer!: Animation;
|
||||
public time = 0;
|
||||
|
||||
public parentPlayer: AnimationPlayer|null = null;
|
||||
@@ -88,10 +86,9 @@ export class WebAnimationsPlayer implements AnimationPlayer {
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_triggerWebAnimation(element: any, keyframes: Array<ɵStyleDataMap>, options: any): DOMAnimation {
|
||||
// jscompiler doesn't seem to know animate is a native property because it's not fully
|
||||
// supported yet across common browsers (we polyfill it for Edge/Safari) [CL #143630929]
|
||||
return element['animate'](this._convertKeyframesToObject(keyframes), options) as DOMAnimation;
|
||||
_triggerWebAnimation(element: HTMLElement, keyframes: Array<ɵStyleDataMap>, options: any):
|
||||
Animation {
|
||||
return element.animate(this._convertKeyframesToObject(keyframes), options);
|
||||
}
|
||||
|
||||
onStart(fn: () => void): void {
|
||||
@@ -180,7 +177,8 @@ export class WebAnimationsPlayer implements AnimationPlayer {
|
||||
}
|
||||
|
||||
getPosition(): number {
|
||||
return this.domPlayer.currentTime / this.time;
|
||||
// tsc is complaining with TS2362 without the conversion to number
|
||||
return +(this.domPlayer.currentTime ?? 0) / this.time;
|
||||
}
|
||||
|
||||
get totalTime(): number {
|
||||
|
||||
+48
-5
@@ -5,7 +5,6 @@
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {DOMAnimation} from '../../../src/render/web_animations/dom_animation';
|
||||
import {WebAnimationsPlayer} from '../../../src/render/web_animations/web_animations_player';
|
||||
|
||||
{
|
||||
@@ -91,7 +90,7 @@ import {WebAnimationsPlayer} from '../../../src/render/web_animations/web_animat
|
||||
});
|
||||
}
|
||||
|
||||
class MockDomAnimation implements DOMAnimation {
|
||||
class MockDomAnimation implements Animation {
|
||||
log: string[] = [];
|
||||
cancel(): void {
|
||||
this.log.push('cancel');
|
||||
@@ -105,9 +104,53 @@ class MockDomAnimation implements DOMAnimation {
|
||||
finish(): void {
|
||||
this.log.push('finish');
|
||||
}
|
||||
onfinish: Function = () => {};
|
||||
position: number = 0;
|
||||
currentTime: number = 0;
|
||||
|
||||
// Other properties to ensure conformnce to interface
|
||||
effect: AnimationEffect|null = null;
|
||||
finished: Promise<Animation> = Promise.resolve({} as any);
|
||||
id: string = '';
|
||||
oncancel: ((this: Animation, ev: AnimationPlaybackEvent) => any)|null = null;
|
||||
onfinish: ((this: Animation, ev: AnimationPlaybackEvent) => any)|null = null;
|
||||
onremove: ((this: Animation, ev: Event) => any)|null = null;
|
||||
pending: boolean = false;
|
||||
playState: AnimationPlayState = 'running';
|
||||
playbackRate: number = 0;
|
||||
ready: Promise<Animation> = Promise.resolve({} as any);
|
||||
replaceState: AnimationReplaceState = 'active';
|
||||
startTime: number|null = null;
|
||||
timeline: AnimationTimeline|null = null;
|
||||
commitStyles(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
persist(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
reverse(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
updatePlaybackRate(playbackRate: number): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
removeEventListener<K extends keyof AnimationEventMap>(
|
||||
type: K, listener: (this: Animation, ev: AnimationEventMap[K]) => any,
|
||||
options?: boolean|EventListenerOptions|undefined): void;
|
||||
removeEventListener(
|
||||
type: string, listener: EventListenerOrEventListenerObject,
|
||||
options?: boolean|EventListenerOptions|undefined): void;
|
||||
removeEventListener(type: unknown, listener: unknown, options?: unknown): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
dispatchEvent(event: Event): boolean;
|
||||
dispatchEvent(event: Event): boolean;
|
||||
dispatchEvent(event: unknown): boolean {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
removeAllListeners?(eventName?: string|undefined): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
eventListeners?(eventName?: string|undefined): EventListenerOrEventListenerObject[] {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
addEventListener(eventName: string, handler: (event: any) => any): any {}
|
||||
dispatchEvent(eventName: string): any {}
|
||||
}
|
||||
|
||||
@@ -14,10 +14,22 @@ export type CompileFactoryFn = (metadata: R3FactoryMetadata) => CompileResult;
|
||||
|
||||
export function compileNgFactoryDefField(metadata: R3FactoryMetadata): CompileResult {
|
||||
const res = compileFactoryFunction(metadata);
|
||||
return {name: 'ɵfac', initializer: res.expression, statements: res.statements, type: res.type};
|
||||
return {
|
||||
name: 'ɵfac',
|
||||
initializer: res.expression,
|
||||
statements: res.statements,
|
||||
type: res.type,
|
||||
deferrableImports: null
|
||||
};
|
||||
}
|
||||
|
||||
export function compileDeclareFactory(metadata: R3FactoryMetadata): CompileResult {
|
||||
const res = compileDeclareFactoryFunction(metadata);
|
||||
return {name: 'ɵfac', initializer: res.expression, statements: res.statements, type: res.type};
|
||||
return {
|
||||
name: 'ɵfac',
|
||||
initializer: res.expression,
|
||||
statements: res.statements,
|
||||
type: res.type,
|
||||
deferrableImports: null
|
||||
};
|
||||
}
|
||||
|
||||
@@ -22,7 +22,8 @@ export function compileInputTransformFields(inputs: ClassPropertyMapping<InputMa
|
||||
name: `ngAcceptInputType_${input.classPropertyName}`,
|
||||
type: outputAst.transplantedType(input.transform.type),
|
||||
statements: [],
|
||||
initializer: null
|
||||
initializer: null,
|
||||
deferrableImports: null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,7 +331,8 @@ export function createSourceSpan(node: ts.Node): ParseSourceSpan {
|
||||
*/
|
||||
export function compileResults(
|
||||
fac: CompileResult, def: R3CompiledExpression, metadataStmt: Statement|null, propName: string,
|
||||
additionalFields: CompileResult[]|null): CompileResult[] {
|
||||
additionalFields: CompileResult[]|null,
|
||||
deferrableImports: Set<ts.ImportDeclaration>|null): CompileResult[] {
|
||||
const statements = def.statements;
|
||||
if (metadataStmt !== null) {
|
||||
statements.push(metadataStmt);
|
||||
@@ -344,6 +345,7 @@ export function compileResults(
|
||||
initializer: def.expression,
|
||||
statements: def.statements,
|
||||
type: def.type,
|
||||
deferrableImports,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -1006,7 +1006,8 @@ export class ComponentDecoratorHandler implements
|
||||
const classMetadata = analysis.classMetadata !== null ?
|
||||
compileClassMetadata(analysis.classMetadata).toStmt() :
|
||||
null;
|
||||
return compileResults(fac, def, classMetadata, 'ɵcmp', inputTransformFields);
|
||||
const deferrableImports = this.deferredSymbolTracker.getDeferrableImportDecls();
|
||||
return compileResults(fac, def, classMetadata, 'ɵcmp', inputTransformFields, deferrableImports);
|
||||
}
|
||||
|
||||
compilePartial(
|
||||
@@ -1031,7 +1032,8 @@ export class ComponentDecoratorHandler implements
|
||||
const classMetadata = analysis.classMetadata !== null ?
|
||||
compileDeclareClassMetadata(analysis.classMetadata).toStmt() :
|
||||
null;
|
||||
return compileResults(fac, def, classMetadata, 'ɵcmp', inputTransformFields);
|
||||
return compileResults(
|
||||
fac, def, classMetadata, 'ɵcmp', inputTransformFields, null /* deferrableImports */);
|
||||
}
|
||||
|
||||
compileLocal(
|
||||
@@ -1054,7 +1056,8 @@ export class ComponentDecoratorHandler implements
|
||||
const classMetadata = analysis.classMetadata !== null ?
|
||||
compileClassMetadata(analysis.classMetadata).toStmt() :
|
||||
null;
|
||||
return compileResults(fac, def, classMetadata, 'ɵcmp', inputTransformFields);
|
||||
return compileResults(
|
||||
fac, def, classMetadata, 'ɵcmp', inputTransformFields, null /* deferrableImports */);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -214,7 +214,8 @@ export class DirectiveDecoratorHandler implements
|
||||
const classMetadata = analysis.classMetadata !== null ?
|
||||
compileClassMetadata(analysis.classMetadata).toStmt() :
|
||||
null;
|
||||
return compileResults(fac, def, classMetadata, 'ɵdir', inputTransformFields);
|
||||
return compileResults(
|
||||
fac, def, classMetadata, 'ɵdir', inputTransformFields, null /* deferrableImports */);
|
||||
}
|
||||
|
||||
compilePartial(
|
||||
@@ -227,7 +228,8 @@ export class DirectiveDecoratorHandler implements
|
||||
compileDeclareClassMetadata(analysis.classMetadata).toStmt() :
|
||||
null;
|
||||
|
||||
return compileResults(fac, def, classMetadata, 'ɵdir', inputTransformFields);
|
||||
return compileResults(
|
||||
fac, def, classMetadata, 'ɵdir', inputTransformFields, null /* deferrableImports */);
|
||||
}
|
||||
|
||||
compileLocal(
|
||||
@@ -239,7 +241,8 @@ export class DirectiveDecoratorHandler implements
|
||||
const classMetadata = analysis.classMetadata !== null ?
|
||||
compileClassMetadata(analysis.classMetadata).toStmt() :
|
||||
null;
|
||||
return compileResults(fac, def, classMetadata, 'ɵdir', inputTransformFields);
|
||||
return compileResults(
|
||||
fac, def, classMetadata, 'ɵdir', inputTransformFields, null /* deferrableImports */);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -779,12 +779,14 @@ export class NgModuleDecoratorHandler implements
|
||||
initializer: ngModuleDef.expression,
|
||||
statements: ngModuleDef.statements,
|
||||
type: ngModuleDef.type,
|
||||
deferrableImports: null,
|
||||
},
|
||||
{
|
||||
name: 'ɵinj',
|
||||
initializer: injectorDef.expression,
|
||||
statements: injectorDef.statements,
|
||||
type: injectorDef.type,
|
||||
deferrableImports: null,
|
||||
},
|
||||
];
|
||||
return res;
|
||||
|
||||
@@ -156,8 +156,13 @@ export class InjectableDecoratorHandler implements
|
||||
if (ɵprov === undefined) {
|
||||
// Only add a new ɵprov if there is not one already
|
||||
const res = compileInjectableFn(analysis.meta);
|
||||
results.push(
|
||||
{name: 'ɵprov', initializer: res.expression, statements: res.statements, type: res.type});
|
||||
results.push({
|
||||
name: 'ɵprov',
|
||||
initializer: res.expression,
|
||||
statements: res.statements,
|
||||
type: res.type,
|
||||
deferrableImports: null
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
|
||||
@@ -185,7 +185,7 @@ export class PipeDecoratorHandler implements
|
||||
const classMetadata = analysis.classMetadata !== null ?
|
||||
compileClassMetadata(analysis.classMetadata).toStmt() :
|
||||
null;
|
||||
return compileResults(fac, def, classMetadata, 'ɵpipe', null);
|
||||
return compileResults(fac, def, classMetadata, 'ɵpipe', null, null /* deferrableImports */);
|
||||
}
|
||||
|
||||
compilePartial(node: ClassDeclaration, analysis: Readonly<PipeHandlerData>): CompileResult[] {
|
||||
@@ -194,7 +194,7 @@ export class PipeDecoratorHandler implements
|
||||
const classMetadata = analysis.classMetadata !== null ?
|
||||
compileDeclareClassMetadata(analysis.classMetadata).toStmt() :
|
||||
null;
|
||||
return compileResults(fac, def, classMetadata, 'ɵpipe', null);
|
||||
return compileResults(fac, def, classMetadata, 'ɵpipe', null, null /* deferrableImports */);
|
||||
}
|
||||
|
||||
compileLocal(node: ClassDeclaration, analysis: Readonly<PipeHandlerData>): CompileResult[] {
|
||||
@@ -203,6 +203,6 @@ export class PipeDecoratorHandler implements
|
||||
const classMetadata = analysis.classMetadata !== null ?
|
||||
compileClassMetadata(analysis.classMetadata).toStmt() :
|
||||
null;
|
||||
return compileResults(fac, def, classMetadata, 'ɵpipe', null);
|
||||
return compileResults(fac, def, classMetadata, 'ɵpipe', null, null /* deferrableImports */);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,6 +248,7 @@ export interface CompileResult {
|
||||
initializer: Expression|null;
|
||||
statements: Statement[];
|
||||
type: Type;
|
||||
deferrableImports: Set<ts.ImportDeclaration>|null;
|
||||
}
|
||||
|
||||
export interface ResolveResult<R> {
|
||||
|
||||
@@ -56,6 +56,7 @@ export function ivyTransformFactory(
|
||||
*/
|
||||
class IvyCompilationVisitor extends Visitor {
|
||||
public classCompilationMap = new Map<ts.ClassDeclaration, CompileResult[]>();
|
||||
public deferrableImports = new Set<ts.ImportDeclaration>();
|
||||
|
||||
constructor(private compilation: TraitCompiler, private constantPool: ConstantPool) {
|
||||
super();
|
||||
@@ -68,6 +69,16 @@ class IvyCompilationVisitor extends Visitor {
|
||||
const result = this.compilation.compile(node, this.constantPool);
|
||||
if (result !== null) {
|
||||
this.classCompilationMap.set(node, result);
|
||||
|
||||
// Collect all deferrable imports declarations into a single set,
|
||||
// so that we can pass it to the transform visitor that will drop
|
||||
// corresponding regular import declarations.
|
||||
for (const classResult of result) {
|
||||
if (classResult.deferrableImports !== null && classResult.deferrableImports.size > 0) {
|
||||
classResult.deferrableImports.forEach(
|
||||
importDecl => this.deferrableImports.add(importDecl));
|
||||
}
|
||||
}
|
||||
}
|
||||
return {node};
|
||||
}
|
||||
@@ -83,7 +94,8 @@ class IvyTransformationVisitor extends Visitor {
|
||||
private classCompilationMap: Map<ts.ClassDeclaration, CompileResult[]>,
|
||||
private reflector: ReflectionHost, private importManager: ImportManager,
|
||||
private recordWrappedNodeExpr: RecordWrappedNodeFn<ts.Expression>,
|
||||
private isClosureCompilerEnabled: boolean, private isCore: boolean) {
|
||||
private isClosureCompilerEnabled: boolean, private isCore: boolean,
|
||||
private deferrableImports: Set<ts.ImportDeclaration>) {
|
||||
super();
|
||||
}
|
||||
|
||||
@@ -153,6 +165,16 @@ class IvyTransformationVisitor extends Visitor {
|
||||
return {node, after: statements};
|
||||
}
|
||||
|
||||
override visitOtherNode<T extends ts.Node>(node: T): T {
|
||||
if (ts.isImportDeclaration(node) && this.deferrableImports.has(node)) {
|
||||
// Return `null` as an indication that this node should not be present
|
||||
// in the final AST. Symbols from this import would be imported via
|
||||
// dynamic imports.
|
||||
return null!;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all decorators on a `Declaration` which are from @angular/core, or an empty set if none
|
||||
* are.
|
||||
@@ -281,7 +303,7 @@ function transformIvySourceFile(
|
||||
// results obtained at Step 1.
|
||||
const transformationVisitor = new IvyTransformationVisitor(
|
||||
compilation, compilationVisitor.classCompilationMap, reflector, importManager,
|
||||
recordWrappedNode, isClosureCompilerEnabled, isCore);
|
||||
recordWrappedNode, isClosureCompilerEnabled, isCore, compilationVisitor.deferrableImports);
|
||||
let sf = visit(file, transformationVisitor, context);
|
||||
|
||||
// Generate the constant statements first, as they may involve adding additional imports
|
||||
|
||||
@@ -102,7 +102,8 @@ runInEachFileSystem(() => {
|
||||
name: 'compileFull',
|
||||
initializer: o.literal(true),
|
||||
statements: [],
|
||||
type: o.BOOL_TYPE
|
||||
type: o.BOOL_TYPE,
|
||||
deferrableImports: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -111,7 +112,8 @@ runInEachFileSystem(() => {
|
||||
name: 'compileLocal',
|
||||
initializer: o.literal(true),
|
||||
statements: [],
|
||||
type: o.BOOL_TYPE
|
||||
type: o.BOOL_TYPE,
|
||||
deferrableImports: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -140,7 +142,8 @@ runInEachFileSystem(() => {
|
||||
name: 'compileFull',
|
||||
initializer: o.literal(true),
|
||||
statements: [],
|
||||
type: o.BOOL_TYPE
|
||||
type: o.BOOL_TYPE,
|
||||
deferrableImports: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -149,7 +152,8 @@ runInEachFileSystem(() => {
|
||||
name: 'compilePartial',
|
||||
initializer: o.literal(true),
|
||||
statements: [],
|
||||
type: o.BOOL_TYPE
|
||||
type: o.BOOL_TYPE,
|
||||
deferrableImports: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -158,7 +162,8 @@ runInEachFileSystem(() => {
|
||||
name: 'compileLocal',
|
||||
initializer: o.literal(true),
|
||||
statements: [],
|
||||
type: o.BOOL_TYPE
|
||||
type: o.BOOL_TYPE,
|
||||
deferrableImports: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -187,7 +192,8 @@ runInEachFileSystem(() => {
|
||||
name: 'compileFull',
|
||||
initializer: o.literal(true),
|
||||
statements: [],
|
||||
type: o.BOOL_TYPE
|
||||
type: o.BOOL_TYPE,
|
||||
deferrableImports: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -196,7 +202,8 @@ runInEachFileSystem(() => {
|
||||
name: 'compileLocal',
|
||||
initializer: o.literal(true),
|
||||
statements: [],
|
||||
type: o.BOOL_TYPE
|
||||
type: o.BOOL_TYPE,
|
||||
deferrableImports: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -317,7 +324,8 @@ runInEachFileSystem(() => {
|
||||
name: 'compileFull',
|
||||
initializer: o.literal(true),
|
||||
statements: [],
|
||||
type: o.BOOL_TYPE
|
||||
type: o.BOOL_TYPE,
|
||||
deferrableImports: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -326,7 +334,8 @@ runInEachFileSystem(() => {
|
||||
name: 'compileLocal',
|
||||
initializer: o.literal(true),
|
||||
statements: [],
|
||||
type: o.BOOL_TYPE
|
||||
type: o.BOOL_TYPE,
|
||||
deferrableImports: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ export abstract class Visitor {
|
||||
// is completed.
|
||||
let visitedNode: T|null = null;
|
||||
|
||||
node = ts.visitEachChild(node, child => this._visit(child, context), context) as T;
|
||||
node = ts.visitEachChild(node, child => child && this._visit(child, context), context) as T;
|
||||
|
||||
if (ts.isClassDeclaration(node)) {
|
||||
visitedNode =
|
||||
@@ -90,7 +90,7 @@ export abstract class Visitor {
|
||||
|
||||
// If the visited node has a `statements` array then process them, maybe replacing the visited
|
||||
// node and adding additional statements.
|
||||
if (ts.isBlock(visitedNode) || ts.isSourceFile(visitedNode)) {
|
||||
if (visitedNode && (ts.isBlock(visitedNode) || ts.isSourceFile(visitedNode))) {
|
||||
visitedNode = this._maybeProcessStatements(visitedNode);
|
||||
}
|
||||
|
||||
|
||||
@@ -8873,10 +8873,10 @@ function allTests(os: string) {
|
||||
});
|
||||
|
||||
describe('deferred blocks', () => {
|
||||
it('should not error for deferred blocks', () => {
|
||||
it('should handle deferred blocks', () => {
|
||||
env.tsconfig({_enabledBlockTypes: ['defer']});
|
||||
env.write('cmp-a.ts', `
|
||||
import {Component} from '@angular/core';
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
@@ -8887,8 +8887,8 @@ function allTests(os: string) {
|
||||
`);
|
||||
|
||||
env.write('/test.ts', `
|
||||
import {Component} from '@angular/core';
|
||||
import {CmpA} from './cmp-a';
|
||||
import { Component } from '@angular/core';
|
||||
import { CmpA } from './cmp-a';
|
||||
|
||||
@Component({
|
||||
selector: 'local-dep',
|
||||
@@ -8914,10 +8914,177 @@ function allTests(os: string) {
|
||||
env.driveMain();
|
||||
|
||||
const jsContents = env.getContents('test.js');
|
||||
|
||||
expect(jsContents).toContain('ɵɵdefer(0, TestCmp_Defer_0_DepsFn)');
|
||||
expect(jsContents)
|
||||
.toContain(
|
||||
'function TestCmp_Defer_0_DepsFn() { return [import("./cmp-a").then(function (m) { return m.CmpA; }), LocalDep]; }');
|
||||
|
||||
// The `CmpA` symbol wasn't referenced elsewhere, so it can be defer-loaded
|
||||
// via dynamic imports and an original import can be removed.
|
||||
expect(jsContents).not.toContain('import { CmpA }');
|
||||
});
|
||||
|
||||
describe('imports', () => {
|
||||
it('should retain regular imports when symbol is eagerly referenced', () => {
|
||||
env.tsconfig({_enabledBlockTypes: ['defer']});
|
||||
env.write('cmp-a.ts', `
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'cmp-a',
|
||||
template: 'CmpA!'
|
||||
})
|
||||
export class CmpA {}
|
||||
`);
|
||||
|
||||
env.write('/test.ts', `
|
||||
import { Component } from '@angular/core';
|
||||
import { CmpA } from './cmp-a';
|
||||
|
||||
@Component({
|
||||
selector: 'test-cmp',
|
||||
standalone: true,
|
||||
imports: [CmpA],
|
||||
template: \`
|
||||
{#defer}
|
||||
<cmp-a />
|
||||
{/defer}
|
||||
\`,
|
||||
})
|
||||
export class TestCmp {
|
||||
constructor() {
|
||||
// This line retains the regular import of CmpA,
|
||||
// since it's eagerly referenced in the code.
|
||||
console.log(CmpA);
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
env.driveMain();
|
||||
|
||||
const jsContents = env.getContents('test.js');
|
||||
|
||||
expect(jsContents).toContain('ɵɵdefer(0, TestCmp_Defer_0_DepsFn)');
|
||||
|
||||
// The dependency function doesn't have a dynamic import, because `CmpA`
|
||||
// was eagerly referenced in component's code, thus regular import can not be removed.
|
||||
expect(jsContents).toContain('function TestCmp_Defer_0_DepsFn() { return [CmpA]; }');
|
||||
expect(jsContents).toContain('import { CmpA }');
|
||||
});
|
||||
|
||||
it('should retain regular imports when one of the symbols is eagerly referenced', () => {
|
||||
env.tsconfig({_enabledBlockTypes: ['defer']});
|
||||
env.write('cmp-a.ts', `
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'cmp-a',
|
||||
template: 'CmpA!'
|
||||
})
|
||||
export class CmpA {}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'cmp-b',
|
||||
template: 'CmpB!'
|
||||
})
|
||||
export class CmpB {}
|
||||
`);
|
||||
|
||||
env.write('/test.ts', `
|
||||
import { Component } from '@angular/core';
|
||||
import { CmpA, CmpB } from './cmp-a';
|
||||
|
||||
@Component({
|
||||
selector: 'test-cmp',
|
||||
standalone: true,
|
||||
imports: [CmpA, CmpB],
|
||||
template: \`
|
||||
{#defer}
|
||||
<cmp-a />
|
||||
<cmp-b />
|
||||
{/defer}
|
||||
\`,
|
||||
})
|
||||
export class TestCmp {
|
||||
constructor() {
|
||||
// This line retains the regular import of CmpA,
|
||||
// since it's eagerly referenced in the code.
|
||||
console.log(CmpA);
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
env.driveMain();
|
||||
|
||||
const jsContents = env.getContents('test.js');
|
||||
|
||||
expect(jsContents).toContain('ɵɵdefer(0, TestCmp_Defer_0_DepsFn)');
|
||||
|
||||
// The dependency function doesn't have a dynamic import, because `CmpA`
|
||||
// was eagerly referenced in component's code, thus regular import can not be removed.
|
||||
// This also affects `CmpB`, since it was extracted from the same import.
|
||||
expect(jsContents)
|
||||
.toContain('function TestCmp_Defer_0_DepsFn() { return [CmpA, CmpB]; }');
|
||||
expect(jsContents).toContain('import { CmpA, CmpB }');
|
||||
});
|
||||
|
||||
it('should drop regular imports when none of the symbols are eagerly referenced', () => {
|
||||
env.tsconfig({_enabledBlockTypes: ['defer']});
|
||||
env.write('cmp-a.ts', `
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'cmp-a',
|
||||
template: 'CmpA!'
|
||||
})
|
||||
export class CmpA {}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'cmp-b',
|
||||
template: 'CmpB!'
|
||||
})
|
||||
export class CmpB {}
|
||||
`);
|
||||
|
||||
env.write('/test.ts', `
|
||||
import { Component } from '@angular/core';
|
||||
import { CmpA, CmpB } from './cmp-a';
|
||||
|
||||
@Component({
|
||||
selector: 'test-cmp',
|
||||
standalone: true,
|
||||
imports: [CmpA, CmpB],
|
||||
template: \`
|
||||
{#defer}
|
||||
<cmp-a />
|
||||
<cmp-b />
|
||||
{/defer}
|
||||
\`,
|
||||
})
|
||||
export class TestCmp {}
|
||||
`);
|
||||
|
||||
env.driveMain();
|
||||
|
||||
const jsContents = env.getContents('test.js');
|
||||
|
||||
expect(jsContents).toContain('ɵɵdefer(0, TestCmp_Defer_0_DepsFn)');
|
||||
|
||||
// Both `CmpA` and `CmpB` were used inside the `{#defer}` and were not
|
||||
// referenced elsewhere, so we generate dynamic imports and drop a regular one.
|
||||
expect(jsContents)
|
||||
.toContain(
|
||||
'function TestCmp_Defer_0_DepsFn() { return [' +
|
||||
'import("./cmp-a").then(function (m) { return m.CmpA; }), ' +
|
||||
'import("./cmp-a").then(function (m) { return m.CmpB; })]; }');
|
||||
expect(jsContents).not.toContain('import { CmpA, CmpB }');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -66,6 +66,7 @@ export const PLATFORM_ID = new InjectionToken<Object>('Platform ID', {
|
||||
* A [DI token](guide/glossary#di-token "DI token definition") that indicates the root directory of
|
||||
* the application
|
||||
* @publicApi
|
||||
* @deprecated
|
||||
*/
|
||||
export const PACKAGE_ROOT_URL = new InjectionToken<string>('Application Packages Root URL');
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ export {formatRuntimeError as ɵformatRuntimeError, RuntimeError as ɵRuntimeErr
|
||||
export {annotateForHydration as ɵannotateForHydration} from './hydration/annotate';
|
||||
export {withDomHydration as ɵwithDomHydration} from './hydration/api';
|
||||
export {IS_HYDRATION_DOM_REUSE_ENABLED as ɵIS_HYDRATION_DOM_REUSE_ENABLED} from './hydration/tokens';
|
||||
export {SSR_CONTENT_INTEGRITY_MARKER as ɵSSR_CONTENT_INTEGRITY_MARKER} from './hydration/utils';
|
||||
export {CurrencyIndex as ɵCurrencyIndex, ExtraLocaleDataIndex as ɵExtraLocaleDataIndex, findLocaleData as ɵfindLocaleData, getLocaleCurrencyCode as ɵgetLocaleCurrencyCode, getLocalePluralCase as ɵgetLocalePluralCase, LocaleDataIndex as ɵLocaleDataIndex, registerLocaleData as ɵregisterLocaleData, unregisterAllLocaleData as ɵunregisterLocaleData} from './i18n/locale_data_api';
|
||||
export {DEFAULT_LOCALE_ID as ɵDEFAULT_LOCALE_ID} from './i18n/localization';
|
||||
export {InitialRenderPendingTasks as ɵInitialRenderPendingTasks} from './initial_render_pending_tasks';
|
||||
|
||||
@@ -78,6 +78,7 @@ export const enum RuntimeErrorCode {
|
||||
INVALID_SKIP_HYDRATION_HOST = -504,
|
||||
MISSING_HYDRATION_ANNOTATIONS = -505,
|
||||
HYDRATION_STABLE_TIMEDOUT = -506,
|
||||
MISSING_SSR_CONTENT_INTEGRITY_MARKER = 507,
|
||||
|
||||
// Signal Errors
|
||||
SIGNAL_WRITE_FROM_ILLEGAL_CONTEXT = 600,
|
||||
|
||||
@@ -8,21 +8,22 @@
|
||||
|
||||
import {ApplicationRef} from '../application_ref';
|
||||
import {ViewEncapsulation} from '../metadata';
|
||||
import {Renderer2} from '../render';
|
||||
import {collectNativeNodes} from '../render3/collect_native_nodes';
|
||||
import {getComponentDef} from '../render3/definition';
|
||||
import {CONTAINER_HEADER_OFFSET, LContainer} from '../render3/interfaces/container';
|
||||
import {TNode, TNodeType} from '../render3/interfaces/node';
|
||||
import {RElement} from '../render3/interfaces/renderer_dom';
|
||||
import {hasI18n, isComponentHost, isLContainer, isProjectionTNode, isRootView} from '../render3/interfaces/type_checks';
|
||||
import {CONTEXT, FLAGS, HEADER_OFFSET, HOST, LView, LViewFlags, RENDERER, TView, TVIEW, TViewType} from '../render3/interfaces/view';
|
||||
import {CONTEXT, HEADER_OFFSET, HOST, LView, PARENT, RENDERER, TView, TVIEW, TViewType} from '../render3/interfaces/view';
|
||||
import {unwrapRNode} from '../render3/util/view_utils';
|
||||
import {TransferState} from '../transfer_state';
|
||||
|
||||
import {unsupportedProjectionOfDomNodes} from './error_handling';
|
||||
import {CONTAINERS, DISCONNECTED_NODES, ELEMENT_CONTAINERS, MULTIPLIER, NODES, NUM_ROOT_NODES, SerializedContainerView, SerializedView, TEMPLATE_ID, TEMPLATES} from './interfaces';
|
||||
import {calcPathForNode} from './node_lookup_utils';
|
||||
import {hasInSkipHydrationBlockFlag, isInSkipHydrationBlock, SKIP_HYDRATION_ATTR_NAME} from './skip_hydration';
|
||||
import {getComponentLViewForHydration, NGH_ATTR_NAME, NGH_DATA_KEY, TextNodeMarker} from './utils';
|
||||
import {isInSkipHydrationBlock, SKIP_HYDRATION_ATTR_NAME} from './skip_hydration';
|
||||
import {getLNodeForHydration, NGH_ATTR_NAME, NGH_DATA_KEY, TextNodeMarker} from './utils';
|
||||
|
||||
/**
|
||||
* A collection that tracks all serialized views (`ngh` DOM annotations)
|
||||
@@ -91,6 +92,54 @@ function calcNumRootNodes(tView: TView, lView: LView, tNode: TNode|null): number
|
||||
return rootNodes.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Annotates root level component's LView for hydration,
|
||||
* see `annotateHostElementForHydration` for additional information.
|
||||
*/
|
||||
function annotateComponentLViewForHydration(lView: LView, context: HydrationContext): number|null {
|
||||
const hostElement = lView[HOST];
|
||||
// Root elements might also be annotated with the `ngSkipHydration` attribute,
|
||||
// check if it's present before starting the serialization process.
|
||||
if (hostElement && !(hostElement as HTMLElement).hasAttribute(SKIP_HYDRATION_ATTR_NAME)) {
|
||||
return annotateHostElementForHydration(hostElement as HTMLElement, lView, context);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Annotates root level LContainer for hydration. This happens when a root component
|
||||
* injects ViewContainerRef, thus making the component an anchor for a view container.
|
||||
* This function serializes the component itself as well as all views from the view
|
||||
* container.
|
||||
*/
|
||||
function annotateLContainerForHydration(lContainer: LContainer, context: HydrationContext) {
|
||||
const componentLView = lContainer[HOST] as LView<unknown>;
|
||||
|
||||
// Serialize the root component itself.
|
||||
const componentLViewNghIndex = annotateComponentLViewForHydration(componentLView, context);
|
||||
|
||||
const hostElement = unwrapRNode(componentLView[HOST]!) as HTMLElement;
|
||||
|
||||
// Serialize all views within this view container.
|
||||
const rootLView = lContainer[PARENT];
|
||||
const rootLViewNghIndex = annotateHostElementForHydration(hostElement, rootLView, context);
|
||||
|
||||
const renderer = componentLView[RENDERER] as Renderer2;
|
||||
|
||||
// For cases when a root component also acts as an anchor node for a ViewContainerRef
|
||||
// (for example, when ViewContainerRef is injected in a root component), there is a need
|
||||
// to serialize information about the component itself, as well as an LContainer that
|
||||
// represents this ViewContainerRef. Effectively, we need to serialize 2 pieces of info:
|
||||
// (1) hydration info for the root component itself and (2) hydration info for the
|
||||
// ViewContainerRef instance (an LContainer). Each piece of information is included into
|
||||
// the hydration data (in the TransferState object) separately, thus we end up with 2 ids.
|
||||
// Since we only have 1 root element, we encode both bits of info into a single string:
|
||||
// ids are separated by the `|` char (e.g. `10|25`, where `10` is the ngh for a component view
|
||||
// and 25 is the `ngh` for a root view which holds LContainer).
|
||||
const finalIndex = `${componentLViewNghIndex}|${rootLViewNghIndex}`;
|
||||
renderer.setAttribute(hostElement, NGH_ATTR_NAME, finalIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Annotates all components bootstrapped in a given ApplicationRef
|
||||
* with info needed for hydration.
|
||||
@@ -103,21 +152,21 @@ export function annotateForHydration(appRef: ApplicationRef, doc: Document) {
|
||||
const corruptedTextNodes = new Map<HTMLElement, TextNodeMarker>();
|
||||
const viewRefs = appRef._views;
|
||||
for (const viewRef of viewRefs) {
|
||||
const lView = getComponentLViewForHydration(viewRef);
|
||||
const lNode = getLNodeForHydration(viewRef);
|
||||
|
||||
// An `lView` might be `null` if a `ViewRef` represents
|
||||
// an embedded view (not a component view).
|
||||
if (lView !== null) {
|
||||
const hostElement = lView[HOST];
|
||||
// Root elements might also be annotated with the `ngSkipHydration` attribute,
|
||||
// check if it's present before starting the serialization process.
|
||||
if (hostElement && !(hostElement as HTMLElement).hasAttribute(SKIP_HYDRATION_ATTR_NAME)) {
|
||||
const context: HydrationContext = {
|
||||
serializedViewCollection,
|
||||
corruptedTextNodes,
|
||||
};
|
||||
annotateHostElementForHydration(hostElement as HTMLElement, lView, context);
|
||||
insertCorruptedTextNodeMarkers(corruptedTextNodes, doc);
|
||||
if (lNode !== null) {
|
||||
const context: HydrationContext = {
|
||||
serializedViewCollection,
|
||||
corruptedTextNodes,
|
||||
};
|
||||
if (isLContainer(lNode)) {
|
||||
annotateLContainerForHydration(lNode, context);
|
||||
} else {
|
||||
annotateComponentLViewForHydration(lNode, context);
|
||||
}
|
||||
insertCorruptedTextNodeMarkers(corruptedTextNodes, doc);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,9 +457,11 @@ function componentUsesShadowDomEncapsulation(lView: LView): boolean {
|
||||
* @param element The Host element to be annotated
|
||||
* @param lView The associated LView
|
||||
* @param context The hydration context
|
||||
* @returns An index of serialized view from the transfer state object
|
||||
* or `null` when a given component can not be serialized.
|
||||
*/
|
||||
function annotateHostElementForHydration(
|
||||
element: RElement, lView: LView, context: HydrationContext): void {
|
||||
element: RElement, lView: LView, context: HydrationContext): number|null {
|
||||
const renderer = lView[RENDERER];
|
||||
if (hasI18n(lView) || componentUsesShadowDomEncapsulation(lView)) {
|
||||
// Attach the skip hydration attribute if this component:
|
||||
@@ -419,10 +470,12 @@ function annotateHostElementForHydration(
|
||||
// shadow DOM, so we can not guarantee that client and server representations
|
||||
// would exactly match
|
||||
renderer.setAttribute(element, SKIP_HYDRATION_ATTR_NAME, '');
|
||||
return null;
|
||||
} else {
|
||||
const ngh = serializeLView(lView, context);
|
||||
const index = context.serializedViewCollection.add(ngh);
|
||||
renderer.setAttribute(element, NGH_ATTR_NAME, index.toString());
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,27 +9,27 @@
|
||||
import {first} from 'rxjs/operators';
|
||||
|
||||
import {APP_BOOTSTRAP_LISTENER, ApplicationRef} from '../application_ref';
|
||||
import {ENABLED_SSR_FEATURES, PLATFORM_ID} from '../application_tokens';
|
||||
import {ENABLED_SSR_FEATURES} from '../application_tokens';
|
||||
import {Console} from '../console';
|
||||
import {ENVIRONMENT_INITIALIZER, EnvironmentProviders, Injector, makeEnvironmentProviders} from '../di';
|
||||
import {inject} from '../di/injector_compatibility';
|
||||
import {formatRuntimeError, RuntimeErrorCode} from '../errors';
|
||||
import {formatRuntimeError, RuntimeError, RuntimeErrorCode} from '../errors';
|
||||
import {enableLocateOrCreateContainerRefImpl} from '../linker/view_container_ref';
|
||||
import {enableLocateOrCreateElementNodeImpl} from '../render3/instructions/element';
|
||||
import {enableLocateOrCreateElementContainerNodeImpl} from '../render3/instructions/element_container';
|
||||
import {enableApplyRootElementTransformImpl} from '../render3/instructions/shared';
|
||||
import {enableLocateOrCreateContainerAnchorImpl} from '../render3/instructions/template';
|
||||
import {enableLocateOrCreateTextNodeImpl} from '../render3/instructions/text';
|
||||
import {getDocument} from '../render3/interfaces/document';
|
||||
import {isPlatformBrowser} from '../render3/util/misc_utils';
|
||||
import {TransferState} from '../transfer_state';
|
||||
import {NgZone} from '../zone';
|
||||
|
||||
import {cleanupDehydratedViews} from './cleanup';
|
||||
import {IS_HYDRATION_DOM_REUSE_ENABLED, PRESERVE_HOST_CONTENT} from './tokens';
|
||||
import {enableRetrieveHydrationInfoImpl, NGH_DATA_KEY} from './utils';
|
||||
import {enableRetrieveHydrationInfoImpl, NGH_DATA_KEY, SSR_CONTENT_INTEGRITY_MARKER} from './utils';
|
||||
import {enableFindMatchingDehydratedViewImpl} from './views';
|
||||
|
||||
|
||||
/**
|
||||
* Indicates whether the hydration-related code was added,
|
||||
* prevents adding it multiple times.
|
||||
@@ -150,10 +150,11 @@ export function withDomHydration(): EnvironmentProviders {
|
||||
useValue: () => {
|
||||
// Since this function is used across both server and client,
|
||||
// make sure that the runtime code is only added when invoked
|
||||
// on the client. Moving forward, the `isBrowser` check should
|
||||
// on the client. Moving forward, the `isPlatformBrowser` check should
|
||||
// be replaced with a tree-shakable alternative (e.g. `isServer`
|
||||
// flag).
|
||||
if (isPlatformBrowser() && inject(IS_HYDRATION_DOM_REUSE_ENABLED)) {
|
||||
verifySsrContentsIntegrity();
|
||||
enableHydrationRuntimeSupport();
|
||||
}
|
||||
},
|
||||
@@ -209,3 +210,34 @@ function logWarningOnStableTimedout(time: number, console: Console): void {
|
||||
|
||||
console.warn(formatRuntimeError(RuntimeErrorCode.HYDRATION_STABLE_TIMEDOUT, message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies whether the DOM contains a special marker added during SSR time to make sure
|
||||
* there is no SSR'ed contents transformations happen after SSR is completed. Typically that
|
||||
* happens either by CDN or during the build process as an optimization to remove comment nodes.
|
||||
* Hydration process requires comment nodes produced by Angular to locate correct DOM segments.
|
||||
* When this special marker is *not* present - throw an error and do not proceed with hydration,
|
||||
* since it will not be able to function correctly.
|
||||
*
|
||||
* Note: this function is invoked only on the client, so it's safe to use DOM APIs.
|
||||
*/
|
||||
function verifySsrContentsIntegrity(): void {
|
||||
const doc = getDocument();
|
||||
let hydrationMarker: Node|undefined;
|
||||
for (const node of doc.body.childNodes) {
|
||||
if (node.nodeType === Node.COMMENT_NODE &&
|
||||
node.textContent?.trim() === SSR_CONTENT_INTEGRITY_MARKER) {
|
||||
hydrationMarker = node;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hydrationMarker) {
|
||||
throw new RuntimeError(
|
||||
RuntimeErrorCode.MISSING_SSR_CONTENT_INTEGRITY_MARKER,
|
||||
typeof ngDevMode !== 'undefined' && ngDevMode &&
|
||||
'Angular hydration logic detected that HTML content of this page was modified after it ' +
|
||||
'was produced during server side rendering. Make sure that there are no optimizations ' +
|
||||
'that remove comment nodes from HTML are enabled on your CDN. Angular hydration ' +
|
||||
'relies on HTML produced by the server, including whitespaces and comment nodes.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,14 +10,14 @@ import {ApplicationRef} from '../application_ref';
|
||||
import {CONTAINER_HEADER_OFFSET, DEHYDRATED_VIEWS, LContainer} from '../render3/interfaces/container';
|
||||
import {Renderer} from '../render3/interfaces/renderer';
|
||||
import {RNode} from '../render3/interfaces/renderer_dom';
|
||||
import {isLContainer} from '../render3/interfaces/type_checks';
|
||||
import {isLContainer, isLView} from '../render3/interfaces/type_checks';
|
||||
import {HEADER_OFFSET, HOST, LView, PARENT, RENDERER, TVIEW} from '../render3/interfaces/view';
|
||||
import {nativeRemoveNode} from '../render3/node_manipulation';
|
||||
import {EMPTY_ARRAY} from '../util/empty';
|
||||
|
||||
import {validateSiblingNodeExists} from './error_handling';
|
||||
import {DehydratedContainerView, NUM_ROOT_NODES} from './interfaces';
|
||||
import {getComponentLViewForHydration} from './utils';
|
||||
import {getLNodeForHydration} from './utils';
|
||||
|
||||
/**
|
||||
* Removes all dehydrated views from a given LContainer:
|
||||
@@ -92,11 +92,20 @@ function cleanupLView(lView: LView) {
|
||||
export function cleanupDehydratedViews(appRef: ApplicationRef) {
|
||||
const viewRefs = appRef._views;
|
||||
for (const viewRef of viewRefs) {
|
||||
const lView = getComponentLViewForHydration(viewRef);
|
||||
const lNode = getLNodeForHydration(viewRef);
|
||||
// An `lView` might be `null` if a `ViewRef` represents
|
||||
// an embedded view (not a component view).
|
||||
if (lView !== null && lView[HOST] !== null) {
|
||||
cleanupLView(lView);
|
||||
if (lNode !== null && lNode[HOST] !== null) {
|
||||
if (isLView(lNode)) {
|
||||
cleanupLView(lNode);
|
||||
} else {
|
||||
// Cleanup in the root component view
|
||||
const componentLView = lNode[HOST] as LView<unknown>;
|
||||
cleanupLView(componentLView);
|
||||
|
||||
// Cleanup in all views within this view container
|
||||
cleanupLContainer(lNode);
|
||||
}
|
||||
ngDevMode && ngDevMode.dehydratedViewsCleanupRuns++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,11 @@
|
||||
|
||||
import {Injector} from '../di/injector';
|
||||
import {ViewRef} from '../linker/view_ref';
|
||||
import {LContainer} from '../render3/interfaces/container';
|
||||
import {getDocument} from '../render3/interfaces/document';
|
||||
import {RElement, RNode} from '../render3/interfaces/renderer_dom';
|
||||
import {isLContainer, isRootView} from '../render3/interfaces/type_checks';
|
||||
import {HEADER_OFFSET, HOST, LView, TVIEW, TViewType} from '../render3/interfaces/view';
|
||||
import {isRootView} from '../render3/interfaces/type_checks';
|
||||
import {HEADER_OFFSET, LView, TVIEW, TViewType} from '../render3/interfaces/view';
|
||||
import {makeStateKey, TransferState} from '../transfer_state';
|
||||
import {assertDefined} from '../util/assert';
|
||||
|
||||
@@ -36,6 +37,11 @@ export const NGH_DATA_KEY = makeStateKey<Array<SerializedView>>(TRANSFER_STATE_T
|
||||
*/
|
||||
export const NGH_ATTR_NAME = 'ngh';
|
||||
|
||||
/**
|
||||
* Marker used in a comment node to ensure hydration content integrity
|
||||
*/
|
||||
export const SSR_CONTENT_INTEGRITY_MARKER = 'nghm';
|
||||
|
||||
export const enum TextNodeMarker {
|
||||
|
||||
/**
|
||||
@@ -64,15 +70,34 @@ export const enum TextNodeMarker {
|
||||
*
|
||||
* @param rNode Component's host element.
|
||||
* @param injector Injector that this component has access to.
|
||||
* @param isRootView Specifies whether we trying to read hydration info for the root view.
|
||||
*/
|
||||
let _retrieveHydrationInfoImpl: typeof retrieveHydrationInfoImpl =
|
||||
(rNode: RElement, injector: Injector) => null;
|
||||
(rNode: RElement, injector: Injector, isRootView?: boolean) => null;
|
||||
|
||||
export function retrieveHydrationInfoImpl(rNode: RElement, injector: Injector): DehydratedView|
|
||||
null {
|
||||
const nghAttrValue = rNode.getAttribute(NGH_ATTR_NAME);
|
||||
export function retrieveHydrationInfoImpl(
|
||||
rNode: RElement, injector: Injector, isRootView = false): DehydratedView|null {
|
||||
let nghAttrValue = rNode.getAttribute(NGH_ATTR_NAME);
|
||||
if (nghAttrValue == null) return null;
|
||||
|
||||
// For cases when a root component also acts as an anchor node for a ViewContainerRef
|
||||
// (for example, when ViewContainerRef is injected in a root component), there is a need
|
||||
// to serialize information about the component itself, as well as an LContainer that
|
||||
// represents this ViewContainerRef. Effectively, we need to serialize 2 pieces of info:
|
||||
// (1) hydration info for the root component itself and (2) hydration info for the
|
||||
// ViewContainerRef instance (an LContainer). Each piece of information is included into
|
||||
// the hydration data (in the TransferState object) separately, thus we end up with 2 ids.
|
||||
// Since we only have 1 root element, we encode both bits of info into a single string:
|
||||
// ids are separated by the `|` char (e.g. `10|25`, where `10` is the ngh for a component view
|
||||
// and 25 is the `ngh` for a root view which holds LContainer).
|
||||
const [componentViewNgh, rootViewNgh] = nghAttrValue.split('|');
|
||||
nghAttrValue = isRootView ? rootViewNgh : componentViewNgh;
|
||||
if (!nghAttrValue) return null;
|
||||
|
||||
// We've read one of the ngh ids, keep the remaining one, so that
|
||||
// we can set it back on the DOM element.
|
||||
const remainingNgh = isRootView ? componentViewNgh : (rootViewNgh ? `|${rootViewNgh}` : '');
|
||||
|
||||
let data: SerializedView = {};
|
||||
// An element might have an empty `ngh` attribute value (e.g. `<comp ngh="" />`),
|
||||
// which means that no special annotations are required. Do not attempt to read
|
||||
@@ -96,9 +121,31 @@ export function retrieveHydrationInfoImpl(rNode: RElement, injector: Injector):
|
||||
data,
|
||||
firstChild: rNode.firstChild ?? null,
|
||||
};
|
||||
// The `ngh` attribute is cleared from the DOM node now
|
||||
// that the data has been retrieved.
|
||||
rNode.removeAttribute(NGH_ATTR_NAME);
|
||||
|
||||
if (isRootView) {
|
||||
// If there is hydration info present for the root view, it means that there was
|
||||
// a ViewContainerRef injected in the root component. The root component host element
|
||||
// acted as an anchor node in this scenario. As a result, the DOM nodes that represent
|
||||
// embedded views in this ViewContainerRef are located as siblings to the host node,
|
||||
// i.e. `<app-root /><#VIEW1><#VIEW2>...<!--container-->`. In this case, the current
|
||||
// node becomes the first child of this root view and the next sibling is the first
|
||||
// element in the DOM segment.
|
||||
dehydratedView.firstChild = rNode;
|
||||
|
||||
// We use `0` here, since this is the slot (right after the HEADER_OFFSET)
|
||||
// where a component LView or an LContainer is located in a root LView.
|
||||
setSegmentHead(dehydratedView, 0, rNode.nextSibling);
|
||||
}
|
||||
|
||||
if (remainingNgh) {
|
||||
// If we have only used one of the ngh ids, store the remaining one
|
||||
// back on this RNode.
|
||||
rNode.setAttribute(NGH_ATTR_NAME, remainingNgh);
|
||||
} else {
|
||||
// The `ngh` attribute is cleared from the DOM node now
|
||||
// that the data has been retrieved for all indices.
|
||||
rNode.removeAttribute(NGH_ATTR_NAME);
|
||||
}
|
||||
|
||||
// Note: don't check whether this node was claimed for hydration,
|
||||
// because this node might've been previously claimed while processing
|
||||
@@ -120,15 +167,18 @@ export function enableRetrieveHydrationInfoImpl() {
|
||||
* Retrieves hydration info by reading the value from the `ngh` attribute
|
||||
* and accessing a corresponding slot in TransferState storage.
|
||||
*/
|
||||
export function retrieveHydrationInfo(rNode: RElement, injector: Injector): DehydratedView|null {
|
||||
return _retrieveHydrationInfoImpl(rNode, injector);
|
||||
export function retrieveHydrationInfo(
|
||||
rNode: RElement, injector: Injector, isRootView = false): DehydratedView|null {
|
||||
return _retrieveHydrationInfoImpl(rNode, injector, isRootView);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an instance of a component LView from a given ViewRef.
|
||||
* Returns an instance of a component LView or `null` in case of an embedded view.
|
||||
* Retrieves the necessary object from a given ViewRef to serialize:
|
||||
* - an LView for component views
|
||||
* - an LContainer for cases when component acts as a ViewContainerRef anchor
|
||||
* - `null` in case of an embedded view
|
||||
*/
|
||||
export function getComponentLViewForHydration(viewRef: ViewRef): LView|null {
|
||||
export function getLNodeForHydration(viewRef: ViewRef): LView|LContainer|null {
|
||||
// Reading an internal field from `ViewRef` instance.
|
||||
let lView = (viewRef as any)._lView as LView;
|
||||
const tView = lView[TVIEW];
|
||||
@@ -143,12 +193,6 @@ export function getComponentLViewForHydration(viewRef: ViewRef): LView|null {
|
||||
lView = lView[HEADER_OFFSET];
|
||||
}
|
||||
|
||||
// If a `ViewContainerRef` was injected in a component class, this resulted
|
||||
// in an LContainer creation at that location. In this case, the component
|
||||
// LView is in the LContainer's `HOST` slot.
|
||||
if (isLContainer(lView)) {
|
||||
lView = lView[HOST];
|
||||
}
|
||||
return lView;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,12 +8,10 @@
|
||||
|
||||
import {Injector} from '../di/injector';
|
||||
import {DehydratedContainerView} from '../hydration/interfaces';
|
||||
import {assertLContainer} from '../render3/assert';
|
||||
import {renderView} from '../render3/instructions/render';
|
||||
import {createLView} from '../render3/instructions/shared';
|
||||
import {TContainerNode, TNode, TNodeType} from '../render3/interfaces/node';
|
||||
import {DECLARATION_LCONTAINER, FLAGS, LView, LViewFlags, QUERIES, TView} from '../render3/interfaces/view';
|
||||
import {LView} from '../render3/interfaces/view';
|
||||
import {getCurrentTNode, getLView} from '../render3/state';
|
||||
import {createAndRenderEmbeddedLView} from '../render3/view_manipulation';
|
||||
import {ViewRef as R3_ViewRef} from '../render3/view_ref';
|
||||
import {assertDefined} from '../util/assert';
|
||||
|
||||
@@ -112,7 +110,7 @@ const R3TemplateRef = class TemplateRef<T> extends ViewEngineTemplateRef<T> {
|
||||
}
|
||||
|
||||
override createEmbeddedView(context: T, injector?: Injector): EmbeddedViewRef<T> {
|
||||
return this.createEmbeddedViewImpl(context, injector, null);
|
||||
return this.createEmbeddedViewImpl(context, injector);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,26 +118,9 @@ const R3TemplateRef = class TemplateRef<T> extends ViewEngineTemplateRef<T> {
|
||||
*/
|
||||
override createEmbeddedViewImpl(
|
||||
context: T, injector?: Injector,
|
||||
hydrationInfo?: DehydratedContainerView|null): EmbeddedViewRef<T> {
|
||||
// Embedded views follow the change detection strategy of the view they're declared in.
|
||||
const isSignalView = this._declarationLView[FLAGS] & LViewFlags.SignalView;
|
||||
const viewFlags = isSignalView ? LViewFlags.SignalView : LViewFlags.CheckAlways;
|
||||
const embeddedTView = this._declarationTContainer.tView as TView;
|
||||
const embeddedLView = createLView(
|
||||
this._declarationLView, embeddedTView, context, viewFlags, null, embeddedTView.declTNode,
|
||||
null, null, null, injector || null, hydrationInfo || null);
|
||||
|
||||
const declarationLContainer = this._declarationLView[this._declarationTContainer.index];
|
||||
ngDevMode && assertLContainer(declarationLContainer);
|
||||
embeddedLView[DECLARATION_LCONTAINER] = declarationLContainer;
|
||||
|
||||
const declarationViewLQueries = this._declarationLView[QUERIES];
|
||||
if (declarationViewLQueries !== null) {
|
||||
embeddedLView[QUERIES] = declarationViewLQueries.createEmbeddedView(embeddedTView);
|
||||
}
|
||||
|
||||
renderView(embeddedTView, embeddedLView, context);
|
||||
|
||||
hydrationInfo?: DehydratedContainerView): EmbeddedViewRef<T> {
|
||||
const embeddedLView = createAndRenderEmbeddedLView(
|
||||
this._declarationLView, this._declarationTContainer, context, {injector, hydrationInfo});
|
||||
return new R3_ViewRef<T>(embeddedLView);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -22,14 +22,15 @@ import {addToViewTree, createLContainer} from '../render3/instructions/shared';
|
||||
import {CONTAINER_HEADER_OFFSET, DEHYDRATED_VIEWS, LContainer, NATIVE, VIEW_REFS} from '../render3/interfaces/container';
|
||||
import {NodeInjectorOffset} from '../render3/interfaces/injector';
|
||||
import {TContainerNode, TDirectiveHostNode, TElementContainerNode, TElementNode, TNode, TNodeType} from '../render3/interfaces/node';
|
||||
import {RComment, RElement, RNode} from '../render3/interfaces/renderer_dom';
|
||||
import {RComment, RNode} from '../render3/interfaces/renderer_dom';
|
||||
import {isLContainer} from '../render3/interfaces/type_checks';
|
||||
import {HEADER_OFFSET, HYDRATION, LView, PARENT, RENDERER, T_HOST, TVIEW} from '../render3/interfaces/view';
|
||||
import {assertTNodeType} from '../render3/node_assert';
|
||||
import {addViewToContainer, destroyLView, detachView, getBeforeNodeForView, insertView, nativeInsertBefore, nativeNextSibling, nativeParentNode} from '../render3/node_manipulation';
|
||||
import {destroyLView, detachView, nativeInsertBefore, nativeNextSibling, nativeParentNode} from '../render3/node_manipulation';
|
||||
import {getCurrentTNode, getLView} from '../render3/state';
|
||||
import {getParentInjectorIndex, getParentInjectorView, hasParentInjector} from '../render3/util/injector_utils';
|
||||
import {getNativeByTNode, unwrapRNode, viewAttachedToContainer} from '../render3/util/view_utils';
|
||||
import {addLViewToLContainer} from '../render3/view_manipulation';
|
||||
import {ViewRef as R3ViewRef} from '../render3/view_ref';
|
||||
import {addToArray, removeFromArray} from '../util/array_utils';
|
||||
import {assertDefined, assertEqual, assertGreaterThan, assertLessThan, throwError} from '../util/assert';
|
||||
@@ -483,17 +484,8 @@ const R3ViewContainerRef = class ViewContainerRef extends VE_ViewContainerRef {
|
||||
// Logical operation of adding `LView` to `LContainer`
|
||||
const adjustedIdx = this._adjustIndex(index);
|
||||
const lContainer = this._lContainer;
|
||||
insertView(tView, lView, lContainer, adjustedIdx);
|
||||
|
||||
// Physical operation of adding the DOM nodes.
|
||||
if (!skipDomInsertion) {
|
||||
const beforeNode = getBeforeNodeForView(adjustedIdx, lContainer);
|
||||
const renderer = lView[RENDERER];
|
||||
const parentRNode = nativeParentNode(renderer, lContainer[NATIVE] as RElement | RComment);
|
||||
if (parentRNode !== null) {
|
||||
addViewToContainer(tView, lContainer[T_HOST], renderer, lView, parentRNode, beforeNode);
|
||||
}
|
||||
}
|
||||
addLViewToLContainer(lContainer, lView, adjustedIdx, !skipDomInsertion);
|
||||
|
||||
(viewRef as R3ViewRef<any>).attachToViewContainerRef();
|
||||
addToArray(getOrCreateViewRefs(lContainer), adjustedIdx, viewRef);
|
||||
|
||||
@@ -215,12 +215,17 @@ export class ComponentFactory<T> extends AbstractComponentFactory<T> {
|
||||
LViewFlags.CheckAlways | LViewFlags.IsRoot;
|
||||
const rootFlags = this.componentDef.signals ? signalFlags : nonSignalFlags;
|
||||
|
||||
let hydrationInfo: DehydratedView|null = null;
|
||||
if (hostRNode !== null) {
|
||||
hydrationInfo = retrieveHydrationInfo(hostRNode, rootViewInjector, true /* isRootView */);
|
||||
}
|
||||
|
||||
// Create the root view. Uses empty TView and ContentTemplate.
|
||||
const rootTView =
|
||||
createTView(TViewType.Root, null, null, 1, 0, null, null, null, null, null, null);
|
||||
const rootLView = createLView(
|
||||
null, rootTView, null, rootFlags, null, null, environment, hostRenderer, rootViewInjector,
|
||||
null, null);
|
||||
null, hydrationInfo);
|
||||
|
||||
// rootView is the parent when bootstrapping
|
||||
// TODO(misko): it looks like we are entering view here but we don't really need to as
|
||||
@@ -382,7 +387,7 @@ function createRootComponentView(
|
||||
const tView = rootView[TVIEW];
|
||||
applyRootComponentStyling(rootDirectives, tNode, hostRNode, hostRenderer);
|
||||
|
||||
// Hydration info is on the host element and needs to be retreived
|
||||
// Hydration info is on the host element and needs to be retrieved
|
||||
// and passed to the component LView.
|
||||
let hydrationInfo: DehydratedView|null = null;
|
||||
if (hostRNode !== null) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import {Injector} from '../../di/injector';
|
||||
import {ErrorHandler} from '../../error_handler';
|
||||
import {RuntimeError, RuntimeErrorCode} from '../../errors';
|
||||
import {DehydratedView} from '../../hydration/interfaces';
|
||||
import {hasInSkipHydrationBlockFlag, hasSkipHydrationAttrOnRElement, SKIP_HYDRATION_ATTR_NAME} from '../../hydration/skip_hydration';
|
||||
import {hasSkipHydrationAttrOnRElement} from '../../hydration/skip_hydration';
|
||||
import {PRESERVE_HOST_CONTENT, PRESERVE_HOST_CONTENT_DEFAULT} from '../../hydration/tokens';
|
||||
import {processTextNodeMarkersBeforeHydration} from '../../hydration/utils';
|
||||
import {DoCheck, OnChanges, OnInit} from '../../interface/lifecycle_hooks';
|
||||
@@ -42,7 +42,7 @@ import {clearElementContents, updateTextNode} from '../node_manipulation';
|
||||
import {isInlineTemplate, isNodeMatchingSelectorList} from '../node_selector_matcher';
|
||||
import {profiler, ProfilerEvent} from '../profiler';
|
||||
import {commitLViewConsumerIfHasProducers, getReactiveLViewConsumer} from '../reactive_lview_consumer';
|
||||
import {getBindingsEnabled, getCurrentDirectiveIndex, getCurrentParentTNode, getCurrentTNodePlaceholderOk, getSelectedIndex, isCurrentTNodeParent, isInCheckNoChangesMode, isInI18nBlock, isInSkipHydrationBlock, leaveView, setBindingRootForHostBindings, setCurrentDirectiveIndex, setCurrentQueryIndex, setCurrentTNode, setSelectedIndex} from '../state';
|
||||
import {getBindingsEnabled, getCurrentDirectiveIndex, getCurrentParentTNode, getCurrentTNodePlaceholderOk, getSelectedIndex, isCurrentTNodeParent, isInCheckNoChangesMode, isInI18nBlock, isInSkipHydrationBlock, setBindingRootForHostBindings, setCurrentDirectiveIndex, setCurrentQueryIndex, setCurrentTNode, setSelectedIndex} from '../state';
|
||||
import {NO_CHANGE} from '../tokens';
|
||||
import {mergeHostAttrs} from '../util/attrs_utils';
|
||||
import {INTERPOLATION_DELIMITER} from '../util/misc_utils';
|
||||
@@ -94,7 +94,7 @@ export function createLView<T>(
|
||||
parentLView: LView|null, tView: TView, context: T|null, flags: LViewFlags, host: RElement|null,
|
||||
tHostNode: TNode|null, environment: LViewEnvironment|null, renderer: Renderer|null,
|
||||
injector: Injector|null, embeddedViewInjector: Injector|null,
|
||||
hydrationInfo: DehydratedView|null): LView {
|
||||
hydrationInfo: DehydratedView|null): LView<T> {
|
||||
const lView = tView.blueprint.slice() as LView;
|
||||
lView[HOST] = host;
|
||||
lView[FLAGS] = flags | LViewFlags.CreationMode | LViewFlags.Attached | LViewFlags.FirstLViewPass;
|
||||
@@ -122,7 +122,7 @@ export function createLView<T>(
|
||||
'Embedded views must have parentLView');
|
||||
lView[DECLARATION_COMPONENT_VIEW] =
|
||||
tView.type == TViewType.Embedded ? parentLView![DECLARATION_COMPONENT_VIEW] : lView;
|
||||
return lView;
|
||||
return lView as LView<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -136,7 +136,7 @@ export function createElementNode(
|
||||
* @param tView The `TView' of the `LView` from which elements should be added or removed
|
||||
* @param lView The view from which elements should be added or removed
|
||||
*/
|
||||
export function removeViewFromContainer(tView: TView, lView: LView): void {
|
||||
export function removeViewFromDOM(tView: TView, lView: LView): void {
|
||||
const renderer = lView[RENDERER];
|
||||
applyView(tView, lView, renderer, WalkTNodeTreeAction.Detach, null, null);
|
||||
lView[HOST] = null;
|
||||
@@ -157,7 +157,7 @@ export function removeViewFromContainer(tView: TView, lView: LView): void {
|
||||
* @param parentNativeNode The parent `RElement` where it should be inserted into.
|
||||
* @param beforeNode The node before which elements should be added, if insert mode
|
||||
*/
|
||||
export function addViewToContainer(
|
||||
export function addViewToDOM(
|
||||
tView: TView, parentTNode: TNode, renderer: Renderer, lView: LView, parentNativeNode: RElement,
|
||||
beforeNode: RNode|null): void {
|
||||
lView[HOST] = parentNativeNode;
|
||||
@@ -172,7 +172,7 @@ export function addViewToContainer(
|
||||
* @param tView The `TView' of the `LView` to be detached
|
||||
* @param lView the `LView` to be detached.
|
||||
*/
|
||||
export function renderDetachView(tView: TView, lView: LView) {
|
||||
export function detachViewFromDOM(tView: TView, lView: LView) {
|
||||
applyView(tView, lView, lView[RENDERER], WalkTNodeTreeAction.Detach, null, null);
|
||||
}
|
||||
|
||||
@@ -349,7 +349,7 @@ export function detachView(lContainer: LContainer, removeIndex: number): LView|u
|
||||
lContainer[indexInContainer - 1][NEXT] = viewToDetach[NEXT] as LView;
|
||||
}
|
||||
const removedLView = removeFromArray(lContainer, CONTAINER_HEADER_OFFSET + removeIndex);
|
||||
removeViewFromContainer(viewToDetach[TVIEW], viewToDetach);
|
||||
removeViewFromDOM(viewToDetach[TVIEW], viewToDetach);
|
||||
|
||||
// notify query that a view has been removed
|
||||
const lQueries = removedLView[QUERIES];
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Injector} from '../di/injector';
|
||||
import {DehydratedContainerView} from '../hydration/interfaces';
|
||||
import {assertDefined} from '../util/assert';
|
||||
|
||||
import {assertLContainer, assertLView, assertTNodeForLView} from './assert';
|
||||
import {renderView} from './instructions/render';
|
||||
import {createLView} from './instructions/shared';
|
||||
import {CONTAINER_HEADER_OFFSET, LContainer, NATIVE} from './interfaces/container';
|
||||
import {TNode} from './interfaces/node';
|
||||
import {RComment, RElement} from './interfaces/renderer_dom';
|
||||
import {DECLARATION_LCONTAINER, FLAGS, LView, LViewFlags, QUERIES, RENDERER, T_HOST, TVIEW} from './interfaces/view';
|
||||
import {addViewToDOM, destroyLView, detachView, getBeforeNodeForView, insertView, nativeParentNode} from './node_manipulation';
|
||||
|
||||
export function createAndRenderEmbeddedLView<T>(
|
||||
declarationLView: LView<unknown>, templateTNode: TNode, context: T,
|
||||
options?: {injector?: Injector, hydrationInfo?: DehydratedContainerView}): LView<T> {
|
||||
const embeddedTView = templateTNode.tView!;
|
||||
ngDevMode && assertDefined(embeddedTView, 'TView must be defined for a template node.');
|
||||
ngDevMode && assertTNodeForLView(templateTNode, declarationLView);
|
||||
|
||||
// Embedded views follow the change detection strategy of the view they're declared in.
|
||||
const isSignalView = declarationLView[FLAGS] & LViewFlags.SignalView;
|
||||
const viewFlags = isSignalView ? LViewFlags.SignalView : LViewFlags.CheckAlways;
|
||||
const embeddedLView = createLView<T>(
|
||||
declarationLView, embeddedTView, context, viewFlags, null, templateTNode, null, null, null,
|
||||
options?.injector ?? null, options?.hydrationInfo ?? null);
|
||||
|
||||
const declarationLContainer = declarationLView[templateTNode.index];
|
||||
ngDevMode && assertLContainer(declarationLContainer);
|
||||
embeddedLView[DECLARATION_LCONTAINER] = declarationLContainer;
|
||||
|
||||
const declarationViewLQueries = declarationLView[QUERIES];
|
||||
if (declarationViewLQueries !== null) {
|
||||
embeddedLView[QUERIES] = declarationViewLQueries.createEmbeddedView(embeddedTView);
|
||||
}
|
||||
|
||||
// execute creation mode of a view
|
||||
renderView(embeddedTView, embeddedLView, context);
|
||||
|
||||
return embeddedLView;
|
||||
}
|
||||
|
||||
export function getLViewFromLContainer<T>(lContainer: LContainer, index: number): LView<T>|
|
||||
undefined {
|
||||
const adjustedIndex = CONTAINER_HEADER_OFFSET + index;
|
||||
// avoid reading past the array boundaries
|
||||
if (adjustedIndex < lContainer.length) {
|
||||
const lView = lContainer[adjustedIndex];
|
||||
ngDevMode && assertLView(lView);
|
||||
return lView as LView<T>;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function addLViewToLContainer(
|
||||
lContainer: LContainer, lView: LView<unknown>, index: number, addToDOM = true): void {
|
||||
const tView = lView[TVIEW];
|
||||
|
||||
// insert to the view tree so the new view can be change-detected
|
||||
insertView(tView, lView, lContainer, index);
|
||||
|
||||
// insert to the view to the DOM tree
|
||||
if (addToDOM) {
|
||||
const beforeNode = getBeforeNodeForView(index, lContainer);
|
||||
const renderer = lView[RENDERER];
|
||||
const parentRNode = nativeParentNode(renderer, lContainer[NATIVE] as RElement | RComment);
|
||||
if (parentRNode !== null) {
|
||||
addViewToDOM(tView, lContainer[T_HOST], renderer, lView, parentRNode, beforeNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function removeLViewFromLContainer(lContainer: LContainer, index: number): LView<unknown>|
|
||||
undefined {
|
||||
const lView = detachView(lContainer, index);
|
||||
if (lView !== undefined) {
|
||||
destroyLView(lView[TVIEW], lView);
|
||||
}
|
||||
return lView;
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import {markViewDirty} from './instructions/mark_view_dirty';
|
||||
import {CONTAINER_HEADER_OFFSET, VIEW_REFS} from './interfaces/container';
|
||||
import {isLContainer} from './interfaces/type_checks';
|
||||
import {CONTEXT, FLAGS, LView, LViewFlags, PARENT, TVIEW} from './interfaces/view';
|
||||
import {destroyLView, detachView, renderDetachView} from './node_manipulation';
|
||||
import {destroyLView, detachView, detachViewFromDOM} from './node_manipulation';
|
||||
import {storeLViewOnDestroy} from './util/view_utils';
|
||||
|
||||
|
||||
@@ -297,7 +297,7 @@ export class ViewRef<T> implements EmbeddedViewRef<T>, InternalViewRef, ChangeDe
|
||||
|
||||
detachFromAppRef() {
|
||||
this._appRef = null;
|
||||
renderDetachView(this._lView[TVIEW], this._lView);
|
||||
detachViewFromDOM(this._lView[TVIEW], this._lView);
|
||||
}
|
||||
|
||||
attachToAppRef(appRef: ViewRefTracker) {
|
||||
|
||||
@@ -10,7 +10,6 @@ import {CommonModule} from '@angular/common';
|
||||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DoCheck, Input, TemplateRef, Type, ViewChild, ViewContainerRef} from '@angular/core';
|
||||
import {AfterViewChecked} from '@angular/core/src/core';
|
||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
||||
|
||||
describe('change detection for transplanted views', () => {
|
||||
describe('when declaration appears before insertion', () => {
|
||||
|
||||
@@ -20,7 +20,6 @@ import {getLView} from '@angular/core/src/render3/state';
|
||||
import {ngDevModeResetPerfCounters} from '@angular/core/src/util/ng_dev_mode';
|
||||
import {fakeAsync, flushMicrotasks, TestBed} from '@angular/core/testing';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
||||
import {expectPerfCounters} from '@angular/private/testing';
|
||||
|
||||
describe('acceptance integration tests', () => {
|
||||
|
||||
@@ -12,7 +12,6 @@ import {KNOWN_CONTROL_FLOW_DIRECTIVES} from '@angular/core/src/render3/instructi
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {BrowserModule} from '@angular/platform-browser';
|
||||
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
|
||||
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
||||
import {withBody} from '@angular/private/testing';
|
||||
|
||||
describe('NgModule', () => {
|
||||
|
||||
@@ -10,7 +10,6 @@ import {CommonModule} from '@angular/common';
|
||||
import {AfterViewInit, Component, ContentChild, ContentChildren, Directive, ElementRef, EventEmitter, forwardRef, InjectionToken, Input, QueryList, TemplateRef, Type, ViewChild, ViewChildren, ViewContainerRef, ViewRef} from '@angular/core';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
||||
|
||||
describe('query logic', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -12,7 +12,6 @@ import {ngDevModeResetPerfCounters} from '@angular/core/src/util/ng_dev_mode';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {getElementClasses, getElementStyles, getSortedClassName, getSortedStyle} from '@angular/core/testing/src/styling';
|
||||
import {By, DomSanitizer, SafeStyle} from '@angular/platform-browser';
|
||||
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
||||
import {expectPerfCounters} from '@angular/private/testing';
|
||||
|
||||
describe('styling', () => {
|
||||
@@ -2476,10 +2475,10 @@ describe('styling', () => {
|
||||
}
|
||||
|
||||
const header = fixture.nativeElement.querySelector('header');
|
||||
expect(header.classList.contains('header'));
|
||||
expect(header.classList.contains('header')).toBeTrue();
|
||||
|
||||
const footer = fixture.nativeElement.querySelector('footer');
|
||||
expect(footer.classList.contains('footer'));
|
||||
expect(footer.classList.contains('footer')).toBeTrue();
|
||||
|
||||
expect(getItemElements().length).toEqual(3);
|
||||
expect(getItemClasses()).toEqual([
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
import {Component, Injector, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
||||
|
||||
describe('TemplateRef', () => {
|
||||
describe('rootNodes', () => {
|
||||
|
||||
@@ -695,6 +695,9 @@
|
||||
{
|
||||
"name": "activeConsumer"
|
||||
},
|
||||
{
|
||||
"name": "addLViewToLContainer"
|
||||
},
|
||||
{
|
||||
"name": "addPropertyAlias"
|
||||
},
|
||||
|
||||
@@ -683,6 +683,9 @@
|
||||
{
|
||||
"name": "activeConsumer"
|
||||
},
|
||||
{
|
||||
"name": "addLViewToLContainer"
|
||||
},
|
||||
{
|
||||
"name": "addPropertyAlias"
|
||||
},
|
||||
|
||||
@@ -788,9 +788,6 @@
|
||||
{
|
||||
"name": "getComponentLViewByIndex"
|
||||
},
|
||||
{
|
||||
"name": "getComponentLViewForHydration"
|
||||
},
|
||||
{
|
||||
"name": "getCurrentTNode"
|
||||
},
|
||||
@@ -827,6 +824,9 @@
|
||||
{
|
||||
"name": "getInjectorIndex"
|
||||
},
|
||||
{
|
||||
"name": "getLNodeForHydration"
|
||||
},
|
||||
{
|
||||
"name": "getLView"
|
||||
},
|
||||
@@ -1235,6 +1235,9 @@
|
||||
{
|
||||
"name": "setInjectImplementation"
|
||||
},
|
||||
{
|
||||
"name": "setSegmentHead"
|
||||
},
|
||||
{
|
||||
"name": "setSelectedIndex"
|
||||
},
|
||||
|
||||
@@ -851,6 +851,9 @@
|
||||
{
|
||||
"name": "addEmptyPathsToChildrenIfNeeded"
|
||||
},
|
||||
{
|
||||
"name": "addLViewToLContainer"
|
||||
},
|
||||
{
|
||||
"name": "addPropertyAlias"
|
||||
},
|
||||
|
||||
@@ -608,6 +608,9 @@
|
||||
{
|
||||
"name": "activeConsumer"
|
||||
},
|
||||
{
|
||||
"name": "addLViewToLContainer"
|
||||
},
|
||||
{
|
||||
"name": "addPropertyAlias"
|
||||
},
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
import {InjectFlags, InjectionToken, INJECTOR, Injector, Optional, ɵɵdefineInjectable, ɵɵdefineInjector, ɵɵinject} from '@angular/core';
|
||||
import {createInjector} from '@angular/core/src/di/create_injector';
|
||||
import {R3Injector} from '@angular/core/src/di/r3_injector';
|
||||
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
||||
|
||||
describe('InjectorDef-based createInjector()', () => {
|
||||
class CircularA {
|
||||
@@ -352,19 +351,19 @@ describe('InjectorDef-based createInjector()', () => {
|
||||
|
||||
it('injects a service with dependencies', () => {
|
||||
const instance = injector.get(ServiceWithDep);
|
||||
expect(instance instanceof ServiceWithDep);
|
||||
expect(instance instanceof ServiceWithDep).toBeTrue();
|
||||
expect(instance.service).toBe(injector.get(Service));
|
||||
});
|
||||
|
||||
it('injects a service with optional dependencies', () => {
|
||||
const instance = injector.get(ServiceWithOptionalDep);
|
||||
expect(instance instanceof ServiceWithOptionalDep);
|
||||
expect(instance instanceof ServiceWithOptionalDep).toBeTrue();
|
||||
expect(instance.service).toBe(null);
|
||||
});
|
||||
|
||||
it('injects a service with dependencies on multi-providers', () => {
|
||||
const instance = injector.get(ServiceWithMultiDep);
|
||||
expect(instance instanceof ServiceWithMultiDep);
|
||||
expect(instance instanceof ServiceWithMultiDep).toBeTrue();
|
||||
expect(instance.locale).toEqual(['en', 'es']);
|
||||
});
|
||||
|
||||
@@ -375,7 +374,7 @@ describe('InjectorDef-based createInjector()', () => {
|
||||
|
||||
it('injects an injector with dependencies', () => {
|
||||
const instance = injector.get(InjectorWithDep);
|
||||
expect(instance instanceof InjectorWithDep);
|
||||
expect(instance instanceof InjectorWithDep).toBeTrue();
|
||||
expect(instance.service).toBe(injector.get(Service));
|
||||
});
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
*/
|
||||
|
||||
import {forwardRef, Inject, InjectFlags, Injector, Self, SkipSelf} from '@angular/core';
|
||||
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
||||
|
||||
import {stringify} from '../../src/util/stringify';
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
import {discardPeriodicTasks, fakeAsync, flush, flushMicrotasks, inject, tick} from '@angular/core/testing';
|
||||
import {Log} from '@angular/core/testing/src/testing_internal';
|
||||
import {EventManager} from '@angular/platform-browser';
|
||||
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
||||
|
||||
const resolvedPromise = Promise.resolve(null);
|
||||
const ProxyZoneSpec: {assertPresent: () => void} = (Zone as any)['ProxyZoneSpec'];
|
||||
|
||||
@@ -11,7 +11,6 @@ import {ɵɵdefineInjectable} from '@angular/core/src/di/interface/defs';
|
||||
import {NgModuleType} from '@angular/core/src/render3';
|
||||
import {getNgModuleDef} from '@angular/core/src/render3/definition';
|
||||
import {ComponentFixture, inject} from '@angular/core/testing';
|
||||
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
||||
|
||||
import {InternalNgModuleRef, NgModuleFactory} from '../../src/linker/ng_module_factory';
|
||||
import {clearModulesForTest, setAllowDuplicateNgModuleIdsForTest} from '../../src/linker/ng_module_registration';
|
||||
|
||||
@@ -7,17 +7,7 @@
|
||||
*/
|
||||
|
||||
import {CompilerConfig} from '@angular/compiler';
|
||||
import {Compiler, CompilerFactory, CompilerOptions, InjectionToken, Injector, MissingTranslationStrategy, PACKAGE_ROOT_URL, StaticProvider, ViewEncapsulation} from '@angular/core';
|
||||
|
||||
export const ERROR_COLLECTOR_TOKEN = new InjectionToken('ErrorCollector');
|
||||
|
||||
/**
|
||||
* A default provider for {@link PACKAGE_ROOT_URL} that maps to '/'.
|
||||
*/
|
||||
export const DEFAULT_PACKAGE_URL_PROVIDER = {
|
||||
provide: PACKAGE_ROOT_URL,
|
||||
useValue: '/'
|
||||
};
|
||||
import {Compiler, CompilerFactory, CompilerOptions, Injector, MissingTranslationStrategy, StaticProvider, ViewEncapsulation} from '@angular/core';
|
||||
|
||||
export const COMPILER_PROVIDERS =
|
||||
<StaticProvider[]>[{provide: Compiler, useFactory: () => new Compiler()}];
|
||||
|
||||
@@ -169,7 +169,6 @@ export function provideClientHydration(...features: HydrationFeature<HydrationFe
|
||||
providers.push(ɵproviders);
|
||||
}
|
||||
}
|
||||
|
||||
return makeEnvironmentProviders([
|
||||
(typeof ngDevMode !== 'undefined' && ngDevMode) ? provideZoneJsCompatibilityDetector() : [],
|
||||
(featuresKind.has(HydrationFeatureKind.NoDomReuseFeature) ? [] : withDomHydration()),
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import {DOCUMENT} from '@angular/common';
|
||||
import {HttpClient, provideHttpClient} from '@angular/common/http';
|
||||
import {HttpTestingController, provideHttpClientTesting} from '@angular/common/http/testing';
|
||||
import {ApplicationRef, Component, Injectable} from '@angular/core';
|
||||
import {ApplicationRef, Component, Injectable, ɵSSR_CONTENT_INTEGRITY_MARKER as SSR_CONTENT_INTEGRITY_MARKER} from '@angular/core';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {withBody} from '@angular/private/testing';
|
||||
import {BehaviorSubject} from 'rxjs';
|
||||
@@ -37,23 +37,24 @@ describe('provideClientHydration', () => {
|
||||
}
|
||||
|
||||
describe('default', () => {
|
||||
beforeEach(withBody('<test-hydrate-app></test-hydrate-app>', () => {
|
||||
TestBed.resetTestingModule();
|
||||
beforeEach(withBody(
|
||||
`<!--${SSR_CONTENT_INTEGRITY_MARKER}--><test-hydrate-app></test-hydrate-app>`, () => {
|
||||
TestBed.resetTestingModule();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [SomeComponent],
|
||||
providers: [
|
||||
{provide: DOCUMENT, useFactory: () => document},
|
||||
{provide: ApplicationRef, useClass: ApplicationRefPatched},
|
||||
provideClientHydration(),
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
});
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [SomeComponent],
|
||||
providers: [
|
||||
{provide: DOCUMENT, useFactory: () => document},
|
||||
{provide: ApplicationRef, useClass: ApplicationRefPatched},
|
||||
provideClientHydration(),
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
});
|
||||
|
||||
const appRef = TestBed.inject(ApplicationRef);
|
||||
appRef.bootstrap(SomeComponent);
|
||||
}));
|
||||
const appRef = TestBed.inject(ApplicationRef);
|
||||
appRef.bootstrap(SomeComponent);
|
||||
}));
|
||||
|
||||
it(`should use cached HTTP calls`, () => {
|
||||
makeRequestAndExpectOne('/test-1', 'foo');
|
||||
@@ -63,23 +64,24 @@ describe('provideClientHydration', () => {
|
||||
});
|
||||
|
||||
describe('withNoHttpTransferCache', () => {
|
||||
beforeEach(withBody('<test-hydrate-app></test-hydrate-app>', () => {
|
||||
TestBed.resetTestingModule();
|
||||
beforeEach(withBody(
|
||||
`<!--${SSR_CONTENT_INTEGRITY_MARKER}--><test-hydrate-app></test-hydrate-app>`, () => {
|
||||
TestBed.resetTestingModule();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [SomeComponent],
|
||||
providers: [
|
||||
{provide: DOCUMENT, useFactory: () => document},
|
||||
{provide: ApplicationRef, useClass: ApplicationRefPatched},
|
||||
provideClientHydration(withNoHttpTransferCache()),
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
});
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [SomeComponent],
|
||||
providers: [
|
||||
{provide: DOCUMENT, useFactory: () => document},
|
||||
{provide: ApplicationRef, useClass: ApplicationRefPatched},
|
||||
provideClientHydration(withNoHttpTransferCache()),
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
});
|
||||
|
||||
const appRef = TestBed.inject(ApplicationRef);
|
||||
appRef.bootstrap(SomeComponent);
|
||||
}));
|
||||
const appRef = TestBed.inject(ApplicationRef);
|
||||
appRef.bootstrap(SomeComponent);
|
||||
}));
|
||||
|
||||
it(`should not cached HTTP calls`, () => {
|
||||
makeRequestAndExpectOne('/test-1', 'foo');
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ApplicationRef, InjectionToken, PlatformRef, Provider, Renderer2, StaticProvider, Type, ɵannotateForHydration as annotateForHydration, ɵENABLED_SSR_FEATURES as ENABLED_SSR_FEATURES, ɵInitialRenderPendingTasks as InitialRenderPendingTasks, ɵIS_HYDRATION_DOM_REUSE_ENABLED as IS_HYDRATION_DOM_REUSE_ENABLED} from '@angular/core';
|
||||
import {ApplicationRef, InjectionToken, PlatformRef, Provider, Renderer2, StaticProvider, Type, ɵannotateForHydration as annotateForHydration, ɵENABLED_SSR_FEATURES as ENABLED_SSR_FEATURES, ɵIS_HYDRATION_DOM_REUSE_ENABLED as IS_HYDRATION_DOM_REUSE_ENABLED, ɵSSR_CONTENT_INTEGRITY_MARKER as SSR_CONTENT_INTEGRITY_MARKER} from '@angular/core';
|
||||
import {first} from 'rxjs/operators';
|
||||
|
||||
import {PlatformState} from './platform_state';
|
||||
@@ -31,6 +31,19 @@ function createServerPlatform(options: PlatformOptions): PlatformRef {
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a marker comment node and append it into the `<body>`.
|
||||
* Some CDNs have mechanisms to remove all comment node from HTML.
|
||||
* This behaviour breaks hydration, so we'll detect on the client side if this
|
||||
* marker comment is still available or else throw an error
|
||||
*/
|
||||
function appendSsrContentIntegrityMarker(doc: Document) {
|
||||
// Adding a ng hydration marken comment
|
||||
const comment = doc.createComment(SSR_CONTENT_INTEGRITY_MARKER);
|
||||
doc.body.firstChild ? doc.body.insertBefore(comment, doc.body.firstChild) :
|
||||
doc.body.append(comment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the `ng-server-context` attribute to host elements of all bootstrapped components
|
||||
* within a given application.
|
||||
@@ -60,7 +73,9 @@ async function _render(platformRef: PlatformRef, applicationRef: ApplicationRef)
|
||||
|
||||
const platformState = platformRef.injector.get(PlatformState);
|
||||
if (applicationRef.injector.get(IS_HYDRATION_DOM_REUSE_ENABLED, false)) {
|
||||
annotateForHydration(applicationRef, platformState.getDocument());
|
||||
const doc = platformState.getDocument();
|
||||
appendSsrContentIntegrityMarker(doc);
|
||||
annotateForHydration(applicationRef, doc);
|
||||
}
|
||||
|
||||
// Run any BEFORE_APP_SERIALIZED callbacks just before rendering to string.
|
||||
|
||||
@@ -12,6 +12,7 @@ import {CommonModule, DOCUMENT, isPlatformServer, NgComponentOutlet, NgFor, NgIf
|
||||
import {MockPlatformLocation} from '@angular/common/testing';
|
||||
import {afterRender, ApplicationRef, Component, ComponentRef, createComponent, destroyPlatform, Directive, ElementRef, EnvironmentInjector, ErrorHandler, getPlatform, inject, Injectable, Input, NgZone, PLATFORM_ID, Provider, TemplateRef, Type, ViewChild, ViewContainerRef, ViewEncapsulation, ɵsetDocument} from '@angular/core';
|
||||
import {Console} from '@angular/core/src/console';
|
||||
import {SSR_CONTENT_INTEGRITY_MARKER} from '@angular/core/src/hydration/utils';
|
||||
import {getComponentDef} from '@angular/core/src/render3/definition';
|
||||
import {NoopNgZone} from '@angular/core/src/zone/ng_zone';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
@@ -69,10 +70,7 @@ function getComponentRef<T>(appRef: ApplicationRef): ComponentRef<T> {
|
||||
*/
|
||||
function getAppContents(html: string): string {
|
||||
const result = stripUtilAttributes(html, true).match(/<body>(.*?)<\/body>/s);
|
||||
if (!result) {
|
||||
throw new Error('Invalid HTML structure is provided.');
|
||||
}
|
||||
return result[1];
|
||||
return result ? result[1] : html;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,6 +87,10 @@ function convertHtmlToDom(html: string, doc: Document): HTMLElement {
|
||||
return container;
|
||||
}
|
||||
|
||||
function stripSsrIntegrityMarker(input: string): string {
|
||||
return input.replace(`<!--${SSR_CONTENT_INTEGRITY_MARKER}-->`, '');
|
||||
}
|
||||
|
||||
function stripTransferDataScript(input: string): string {
|
||||
return input.replace(/<script (.*?)<\/script>/s, '');
|
||||
}
|
||||
@@ -103,10 +105,22 @@ function whenStable(appRef: ApplicationRef): Promise<void> {
|
||||
}
|
||||
|
||||
function verifyClientAndSSRContentsMatch(ssrContents: string, clientAppRootElement: HTMLElement) {
|
||||
const clientContents =
|
||||
stripTransferDataScript(stripUtilAttributes(clientAppRootElement.outerHTML, false));
|
||||
ssrContents = stripTransferDataScript(stripUtilAttributes(ssrContents, false));
|
||||
expect(clientContents).toBe(ssrContents, 'Client and server contents mismatch');
|
||||
const clientContents = stripSsrIntegrityMarker(
|
||||
stripTransferDataScript(stripUtilAttributes(clientAppRootElement.outerHTML, false)));
|
||||
ssrContents =
|
||||
stripSsrIntegrityMarker(stripTransferDataScript(stripUtilAttributes(ssrContents, false)));
|
||||
expect(getAppContents(clientContents)).toBe(ssrContents, 'Client and server contents mismatch');
|
||||
}
|
||||
|
||||
/** Checks whether a given element is a <script> that contains transfer state data. */
|
||||
function isTransferStateScript(el: HTMLElement): boolean {
|
||||
return el.nodeType === Node.ELEMENT_NODE && el.tagName.toLowerCase() === 'script' &&
|
||||
el.getAttribute('id') === 'ng-state';
|
||||
}
|
||||
|
||||
function isSsrContentsIntegrityMarker(el: Node): boolean {
|
||||
return el.nodeType === Node.COMMENT_NODE &&
|
||||
el.textContent?.trim() === SSR_CONTENT_INTEGRITY_MARKER;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,12 +131,17 @@ function verifyClientAndSSRContentsMatch(ssrContents: string, clientAppRootEleme
|
||||
*/
|
||||
function verifyAllNodesClaimedForHydration(el: HTMLElement, exceptions: HTMLElement[] = []) {
|
||||
if ((el.nodeType === Node.ELEMENT_NODE && el.hasAttribute(SKIP_HYDRATION_ATTR_NAME_LOWER_CASE)) ||
|
||||
exceptions.includes(el))
|
||||
exceptions.includes(el) || isTransferStateScript(el) || isSsrContentsIntegrityMarker(el)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(el as any).__claimed) {
|
||||
fail('Hydration error: the node is *not* hydrated: ' + el.outerHTML);
|
||||
}
|
||||
verifyAllChildNodesClaimedForHydration(el, exceptions);
|
||||
}
|
||||
|
||||
function verifyAllChildNodesClaimedForHydration(el: HTMLElement, exceptions: HTMLElement[] = []) {
|
||||
let current = el.firstChild;
|
||||
while (current) {
|
||||
verifyAllNodesClaimedForHydration(current as HTMLElement, exceptions);
|
||||
@@ -280,7 +299,7 @@ describe('platform-server hydration integration', () => {
|
||||
hydrationFeatures: HydrationFeature<HydrationFeatureKind>[] = []): Promise<ApplicationRef> {
|
||||
// Get HTML contents of the `<app>`, create a DOM element and append it into the body.
|
||||
const container = convertHtmlToDom(html, doc);
|
||||
Array.from(container.children).forEach(node => doc.body.appendChild(node));
|
||||
Array.from(container.childNodes).forEach(node => doc.body.appendChild(node));
|
||||
|
||||
function _document(): any {
|
||||
ɵsetDocument(doc);
|
||||
@@ -1447,6 +1466,214 @@ describe('platform-server hydration integration', () => {
|
||||
verifyClientAndSSRContentsMatch(ssrContents, clientRootNode);
|
||||
});
|
||||
|
||||
it('should hydrate dynamically created components using root component as an anchor',
|
||||
async () => {
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
selector: 'dynamic',
|
||||
template: `
|
||||
<span>This is a content of a dynamic component.</span>
|
||||
`,
|
||||
})
|
||||
class DynamicComponent {
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app',
|
||||
template: `
|
||||
<main>Hi! This is the main content.</main>
|
||||
`,
|
||||
})
|
||||
class SimpleComponent {
|
||||
vcr = inject(ViewContainerRef);
|
||||
|
||||
ngAfterViewInit() {
|
||||
const compRef = this.vcr.createComponent(DynamicComponent);
|
||||
compRef.changeDetectorRef.detectChanges();
|
||||
}
|
||||
}
|
||||
|
||||
const html = await ssr(SimpleComponent);
|
||||
const ssrContents = getAppContents(html);
|
||||
|
||||
expect(ssrContents).toContain('<app ngh');
|
||||
|
||||
resetTViewsFor(SimpleComponent, DynamicComponent);
|
||||
|
||||
const appRef = await hydrate(html, SimpleComponent);
|
||||
const compRef = getComponentRef<SimpleComponent>(appRef);
|
||||
appRef.tick();
|
||||
|
||||
// Compare output starting from the parent node above the component node,
|
||||
// because component host node also acted as a ViewContainerRef anchor,
|
||||
// thus there are elements after this node (as next siblings).
|
||||
const clientRootNode = compRef.location.nativeElement.parentNode;
|
||||
await whenStable(appRef);
|
||||
|
||||
verifyAllChildNodesClaimedForHydration(clientRootNode);
|
||||
verifyClientAndSSRContentsMatch(ssrContents, clientRootNode);
|
||||
});
|
||||
|
||||
it('should hydrate embedded views when using root component as an anchor', async () => {
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app',
|
||||
template: `
|
||||
<ng-template #tmpl>
|
||||
<h1>Content of embedded view</h1>
|
||||
</ng-template>
|
||||
<main>Hi! This is the main content.</main>
|
||||
`,
|
||||
})
|
||||
class SimpleComponent {
|
||||
@ViewChild('tmpl', {read: TemplateRef}) tmpl!: TemplateRef<unknown>;
|
||||
|
||||
vcr = inject(ViewContainerRef);
|
||||
|
||||
ngAfterViewInit() {
|
||||
const viewRef = this.vcr.createEmbeddedView(this.tmpl);
|
||||
viewRef.detectChanges();
|
||||
}
|
||||
}
|
||||
|
||||
const html = await ssr(SimpleComponent);
|
||||
const ssrContents = getAppContents(html);
|
||||
|
||||
expect(ssrContents).toContain('<app ngh');
|
||||
|
||||
resetTViewsFor(SimpleComponent);
|
||||
|
||||
const appRef = await hydrate(html, SimpleComponent);
|
||||
const compRef = getComponentRef<SimpleComponent>(appRef);
|
||||
appRef.tick();
|
||||
|
||||
// Compare output starting from the parent node above the component node,
|
||||
// because component host node also acted as a ViewContainerRef anchor,
|
||||
// thus there are elements after this node (as next siblings).
|
||||
const clientRootNode = compRef.location.nativeElement.parentNode;
|
||||
await whenStable(appRef);
|
||||
|
||||
verifyAllChildNodesClaimedForHydration(clientRootNode);
|
||||
verifyClientAndSSRContentsMatch(ssrContents, clientRootNode);
|
||||
});
|
||||
|
||||
it('should hydrate dynamically created components using root component as an anchor',
|
||||
async () => {
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
selector: 'nested-dynamic-a',
|
||||
template: `
|
||||
<p>NestedDynamicComponentA</p>
|
||||
`,
|
||||
})
|
||||
class NestedDynamicComponentA {
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
selector: 'nested-dynamic-b',
|
||||
template: `
|
||||
<p>NestedDynamicComponentB</p>
|
||||
`,
|
||||
})
|
||||
class NestedDynamicComponentB {
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
selector: 'dynamic',
|
||||
template: `
|
||||
<span>This is a content of a dynamic component.</span>
|
||||
`,
|
||||
})
|
||||
class DynamicComponent {
|
||||
vcr = inject(ViewContainerRef);
|
||||
|
||||
ngAfterViewInit() {
|
||||
const compRef = this.vcr.createComponent(NestedDynamicComponentB);
|
||||
compRef.changeDetectorRef.detectChanges();
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app',
|
||||
template: `
|
||||
<main>Hi! This is the main content.</main>
|
||||
`,
|
||||
})
|
||||
class SimpleComponent {
|
||||
doc = inject(DOCUMENT);
|
||||
appRef = inject(ApplicationRef);
|
||||
elementRef = inject(ElementRef);
|
||||
viewContainerRef = inject(ViewContainerRef);
|
||||
environmentInjector = inject(EnvironmentInjector);
|
||||
|
||||
createOuterDynamicComponent() {
|
||||
const hostElement = this.doc.body.querySelector('[id=dynamic-cmp-target]')!;
|
||||
const compRef = createComponent(DynamicComponent, {
|
||||
hostElement,
|
||||
environmentInjector: this.environmentInjector,
|
||||
});
|
||||
compRef.changeDetectorRef.detectChanges();
|
||||
this.appRef.attachView(compRef.hostView);
|
||||
}
|
||||
|
||||
createInnerDynamicComponent() {
|
||||
const compRef = this.viewContainerRef.createComponent(NestedDynamicComponentA);
|
||||
compRef.changeDetectorRef.detectChanges();
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.createInnerDynamicComponent();
|
||||
this.createOuterDynamicComponent();
|
||||
}
|
||||
}
|
||||
|
||||
// In this test we expect to have the following structure,
|
||||
// where both root component nodes also act as ViewContainerRef
|
||||
// anchors, i.e.:
|
||||
// ```
|
||||
// <app />
|
||||
// <nested-dynamic-b />
|
||||
// <!--container-->
|
||||
// <div></div> // Host element for DynamicComponent
|
||||
// <nested-dynamic-a/>
|
||||
// <!--container-->
|
||||
// ```
|
||||
// The test verifies that 2 root components acting as ViewContainerRef
|
||||
// do not have overlaps in DOM elements that represent views and all
|
||||
// DOM nodes are able to hydrate correctly.
|
||||
const indexHtml = '<html><head></head><body>' +
|
||||
'<app></app>' +
|
||||
'<div id="dynamic-cmp-target"></div>' +
|
||||
'</body></html>';
|
||||
const html = await ssr(SimpleComponent, indexHtml);
|
||||
const ssrContents = getAppContents(html);
|
||||
|
||||
expect(ssrContents).toContain('<app ngh');
|
||||
|
||||
resetTViewsFor(SimpleComponent, DynamicComponent);
|
||||
|
||||
const appRef = await hydrate(html, SimpleComponent);
|
||||
const compRef = getComponentRef<SimpleComponent>(appRef);
|
||||
appRef.tick();
|
||||
|
||||
// Compare output starting from the parent node above the component node,
|
||||
// because component host node also acted as a ViewContainerRef anchor,
|
||||
// thus there are elements after this node (as next siblings).
|
||||
const clientRootNode = compRef.location.nativeElement.parentNode;
|
||||
await whenStable(appRef);
|
||||
|
||||
verifyAllChildNodesClaimedForHydration(clientRootNode);
|
||||
verifyClientAndSSRContentsMatch(ssrContents, clientRootNode);
|
||||
});
|
||||
|
||||
it('should re-create the views from the ViewContainerRef ' +
|
||||
'if there is a mismatch in template ids between the current view ' +
|
||||
'(that is being created) and the first dehydrated view in the list',
|
||||
@@ -3080,6 +3307,80 @@ describe('platform-server hydration integration', () => {
|
||||
expect(clientContents).not.toContain('This is a SERVER-ONLY content<!--ng-container-->');
|
||||
});
|
||||
|
||||
it('should cleanup unclaimed views in a view container when ' +
|
||||
'root component is used as an anchor for ViewContainerRef',
|
||||
async () => {
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app',
|
||||
imports: [NgIf],
|
||||
template: `
|
||||
<ng-template #tmpl>
|
||||
<span *ngIf="isServer">This is a SERVER-ONLY content (embedded view)</span>
|
||||
<div *ngIf="!isServer">This is a CLIENT-ONLY content (embedded view)</div>
|
||||
</ng-template>
|
||||
<b *ngIf="isServer">This is a SERVER-ONLY content (root component)</b>
|
||||
<i *ngIf="!isServer">This is a CLIENT-ONLY content (root component)</i>
|
||||
`,
|
||||
})
|
||||
class SimpleComponent {
|
||||
// This flag is intentionally different between the client
|
||||
// and the server: we use it to test the logic to cleanup
|
||||
// dehydrated views.
|
||||
isServer = isPlatformServer(inject(PLATFORM_ID));
|
||||
|
||||
@ViewChild('tmpl', {read: TemplateRef}) tmpl!: TemplateRef<unknown>;
|
||||
|
||||
vcr = inject(ViewContainerRef);
|
||||
|
||||
ngAfterViewInit() {
|
||||
const viewRef = this.vcr.createEmbeddedView(this.tmpl);
|
||||
viewRef.detectChanges();
|
||||
}
|
||||
}
|
||||
|
||||
const html = await ssr(SimpleComponent);
|
||||
let ssrContents = getAppContents(html);
|
||||
|
||||
expect(ssrContents).toContain('<app ngh');
|
||||
|
||||
resetTViewsFor(SimpleComponent);
|
||||
|
||||
const appRef = await hydrate(html, SimpleComponent);
|
||||
const compRef = getComponentRef<SimpleComponent>(appRef);
|
||||
appRef.tick();
|
||||
|
||||
ssrContents = stripExcessiveSpaces(stripUtilAttributes(ssrContents, false));
|
||||
|
||||
// In the SSR output we expect to see SERVER content, but not CLIENT.
|
||||
expect(ssrContents)
|
||||
.not.toContain('<i>This is a CLIENT-ONLY content (root component)</i>');
|
||||
expect(ssrContents)
|
||||
.not.toContain('<div>This is a CLIENT-ONLY content (embedded view)</div>');
|
||||
|
||||
expect(ssrContents).toContain('<b>This is a SERVER-ONLY content (root component)</b>');
|
||||
expect(ssrContents)
|
||||
.toContain('<span>This is a SERVER-ONLY content (embedded view)</span>');
|
||||
|
||||
const clientRootNode = compRef.location.nativeElement;
|
||||
|
||||
await whenStable(appRef);
|
||||
|
||||
const clientContents = stripExcessiveSpaces(
|
||||
stripUtilAttributes(clientRootNode.parentNode.outerHTML, false));
|
||||
|
||||
// After the cleanup, we expect to see CLIENT content, but not SERVER.
|
||||
expect(clientContents)
|
||||
.toContain('<i>This is a CLIENT-ONLY content (root component)</i>');
|
||||
expect(clientContents)
|
||||
.toContain('<div>This is a CLIENT-ONLY content (embedded view)</div>');
|
||||
|
||||
expect(clientContents)
|
||||
.not.toContain('<b>This is a SERVER-ONLY content (root component)</b>');
|
||||
expect(clientContents)
|
||||
.not.toContain('<span>This is a SERVER-ONLY content (embedded view)</span>');
|
||||
});
|
||||
|
||||
it('should cleanup within inner containers', async () => {
|
||||
@Component({
|
||||
standalone: true,
|
||||
@@ -4122,14 +4423,15 @@ describe('platform-server hydration integration', () => {
|
||||
appRef.tick();
|
||||
|
||||
const clientRootNode = compRef.location.nativeElement;
|
||||
const portalRootNode = clientRootNode.ownerDocument.body.firstChild;
|
||||
const portalRootNode = clientRootNode.ownerDocument.querySelector('portal-app');
|
||||
verifyAllNodesClaimedForHydration(clientRootNode);
|
||||
verifyAllNodesClaimedForHydration(portalRootNode.firstChild);
|
||||
const clientContents = stripUtilAttributes(portalRootNode.outerHTML, false) +
|
||||
stripUtilAttributes(clientRootNode.outerHTML, false);
|
||||
expect(clientContents)
|
||||
.toBe(
|
||||
stripUtilAttributes(stripTransferDataScript(ssrContents), false),
|
||||
stripSsrIntegrityMarker(
|
||||
stripUtilAttributes(stripTransferDataScript(ssrContents), false)),
|
||||
'Client and server contents mismatch');
|
||||
});
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {DOCUMENT, isPlatformServer, PlatformLocation, ɵgetDOM as getDOM} from '
|
||||
import {HTTP_INTERCEPTORS, HttpClient, HttpClientModule, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http';
|
||||
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
|
||||
import {ApplicationConfig, ApplicationRef, Component, destroyPlatform, EnvironmentProviders, getPlatform, HostListener, Inject, inject as coreInject, Injectable, Input, makeStateKey, mergeApplicationConfig, NgModule, NgZone, PLATFORM_ID, Provider, TransferState, Type, ViewEncapsulation} from '@angular/core';
|
||||
import {SSR_CONTENT_INTEGRITY_MARKER} from '@angular/core/src/hydration/utils';
|
||||
import {InitialRenderPendingTasks} from '@angular/core/src/initial_render_pending_tasks';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {bootstrapApplication, BrowserModule, provideClientHydration, Title, withNoDomReuse, withNoHttpTransferCache} from '@angular/platform-browser';
|
||||
@@ -870,6 +871,22 @@ describe('platform-server integration', () => {
|
||||
expect(output).toMatch(/ng-server-context="other"/);
|
||||
});
|
||||
|
||||
it('appends SSR integrity marker comment when hydration is enabled', async () => {
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app',
|
||||
template: ``,
|
||||
})
|
||||
class SimpleApp {
|
||||
}
|
||||
|
||||
const bootstrap = renderApplication(
|
||||
getStandaloneBoostrapFn(SimpleApp, [provideClientHydration()]), {document: doc});
|
||||
// HttpClient cache and DOM hydration are enabled by default.
|
||||
const output = await bootstrap;
|
||||
expect(output).toContain(`<body><!--${SSR_CONTENT_INTEGRITY_MARKER}-->`);
|
||||
});
|
||||
|
||||
it('includes a set of features into `ng-server-context` attribute', async () => {
|
||||
const options = {
|
||||
document: doc,
|
||||
|
||||
@@ -716,7 +716,7 @@ export interface CanActivate {
|
||||
* navigation is cancelled. If any guard returns a `UrlTree`, the current navigation
|
||||
* is cancelled and a new navigation begins to the `UrlTree` returned from the guard.
|
||||
*
|
||||
* The following example implements and uses a `CanActivateChildFn` that checks whether the
|
||||
* The following example implements and uses a `CanActivateFn` that checks whether the
|
||||
* current user has permission to activate the requested route.
|
||||
*
|
||||
* {@example router/route_functional_guards.ts region="CanActivateFn"}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"target": "es2022",
|
||||
// Keep the below in sync with ng_module.bzl
|
||||
"useDefineForClassFields": false,
|
||||
"lib": ["es2020", "dom"],
|
||||
"lib": ["es2020", "dom", "dom.iterable"],
|
||||
"skipLibCheck": true,
|
||||
// don't auto-discover @types/node, it results in a ///<reference in the .d.ts output
|
||||
"types": [],
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"extends": "./tsconfig-build.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"lib": ["es2020", "dom"],
|
||||
"lib": ["es2020", "dom", "dom.iterable"],
|
||||
"plugins": [{"name": "tsec", "exemptionConfig": "./tsec-exemption.json"}]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
},
|
||||
"rootDir": ".",
|
||||
"inlineSourceMap": true,
|
||||
"lib": ["es2020", "dom"],
|
||||
"lib": ["es2020", "dom", "dom.iterable"],
|
||||
"skipDefaultLibCheck": true,
|
||||
"skipLibCheck": true,
|
||||
"types": ["angular"]
|
||||
|
||||
Reference in New Issue
Block a user