Compare commits

...

39 Commits

Author SHA1 Message Date
Pawel Kozlowski 4b46819ed1 release: cut the v16.2.1 release 2023-08-16 10:19:50 -07:00
Gerald Monaco 7a090eee96 refactor(core): Ensure hydration cleanup runs in the Angular zone (#51321)
Hydration cleanup needs to run in the Angular zone so that change detection will run.

PR Close #51321
2023-08-15 16:02:06 -07:00
Andrew Scott da68bead56 docs(router): Clarify how to clear secondary routes (#51376)
The example for clearing secondary outlets currently only works because
the named outlet appears at the root of the application's route config.
If developers follow this example with an outlet that is not at the
root level, they will not be able to close the outlet.
This commit updates the example to provide a more robust way
of clearing the outlet from the activated outlet component
as well as providing a warning about how the commands are applied.

Lastly, there is a small bit of guidance provided for developers who
might want to explore the ability to close an outlet from any location
in the app, without needing to be aware of the activated route.
An example of this can be found here:
https://stackblitz.com/edit/close-outlet-from-anywhere

resolves #51373
resolves #13523

PR Close #51376
2023-08-15 15:43:18 -07:00
Zoltán Szőgyényi 543b98e1f8 docs: add new UI library resource from the Tailwind CSS ecosystem (#50976)
PR Close #50976
2023-08-15 11:29:40 -07:00
Luis Castro 03619ada72 docs(common): revert format date_pipe (#51359)
PR Close #51359
2023-08-15 10:06:47 -07:00
Luis Castro d8e5316eca docs(common): add new date format example to documentation (#51359)
PR Close #51359
2023-08-15 10:06:47 -07:00
Matthieu Riegler 77efeb5933 docs: Add usage information to HttpInterceptorFn (#51236)
Simple doc addendum

PR Close #51236
2023-08-15 10:02:43 -07:00
Matthieu Riegler 252b111d1e docs: Improve host binding docs (#51326)
This commit adds the mention of the support of styles/classes/attributes in host bindings.

fixes # 44296

PR Close #51326
2023-08-15 09:54:14 -07:00
aanchal 1c41633483 docs: add the doc type attribute (#51332)
PR Close #51332
2023-08-15 09:53:06 -07:00
codestart 9f977e0f34 docs(docs-infra): fix minor typo in standalone-migration.md (#51370)
PR Close #51370
2023-08-15 09:37:47 -07:00
Quentin Dunand 3e19890967 docs: Fix Promise.then wrong parameter (#51322)
Removed the second wrong parameter from the `Promise.then()` method in the doc.
PR Close #51322
2023-08-14 15:27:37 -07:00
Andrew Scott 074fd4a5ec build: update pullapprove (#51360)
update pullapprove to remove self from unavailable list

PR Close #51360
2023-08-14 15:12:27 -07:00
Matthieu Riegler 094d733654 docs: Add Missing SSR integrity marker error doc page (#51340)
This commit adds an error doc page for error 507 : Missing SSR content integrity marker.

PR Close #51340
2023-08-14 14:39:25 -07:00
Jessica Janiuk 2fff344cd6 docs: Add CDN Optimization section to hydration guide (#51355)
This adds a section to cover the case of CDNs that have optimizations that will break both Angular and Hydration by stripping comment nodes.

PR Close #51355
2023-08-14 14:31:09 -07:00
Jeff Schiller 951ef93aa6 docs(core): Fix minor typo in comment of injector_compatibility.ts (#51324)
PR Close #51324
2023-08-14 08:56:29 -07:00
Jeff Schiller f30a49e3de docs(docs-infra): Fix minor typos in dependency-injection-context.md (#51324)
PR Close #51324
2023-08-14 08:56:29 -07:00
Joey Perrott c48e1cc4ed ci: migrate aio jobs to GHA (#51071)
Migrate aio presubmit and deploy jobs to use Github Actions

PR Close #51071
2023-08-14 08:55:31 -07:00
Sinan997 198834b731 docs: update @self() decorator explanation (#51342)
PR Close #51342
2023-08-13 16:33:16 -07:00
Matthieu Riegler 6a8c030ab1 docs(docs-infra): Hide decorator option row if empty. (#51327)
In case where the decorator option has no description nor usageNotes, we can hide that row.

PR Close #51327
2023-08-11 06:07:38 -07:00
Andrew Scott 232a8c1b8d fix(router): Apply named outlets to children empty paths not appearing in the URL (#51292)
Empty path routes are effectively 'passthrough' routes that do not
appear in the URL. When these exist in the route tree, we do not want to
apply named outlet commands to that tree location. Instead, we skip past
this location in the tree, effectively squashing/removing this
passthrough route from the tree.

fixes #50356

PR Close #51292
2023-08-10 09:18:30 -07:00
Joey Perrott 8f944e0752 ci: migrate framework tests to GHA (#51101)
Migrate framework presubmit test job to use Github Actions

PR Close #51101
2023-08-09 17:36:24 -07:00
aanchal af0c8b8fa8 docs: add legend tag to fieldset (#51259)
PR Close #51259
2023-08-09 17:18:39 -07:00
Swami ee19395672 docs: Correct statement about app.module.ts generation with --standalone flag (#51307)
The documentation incorrectly stated that the app.module.ts file is generated
when the --standalone flag is used. This is not the case, and the documentation
should accurately reflect the behavior.

Closes #51306

PR Close #51307
2023-08-09 17:16:49 -07:00
Dylan Hunn 096fdd0f59 release: cut the v16.2.0 release 2023-08-09 14:03:37 -07:00
Douglas Parker 9998bee5f4 refactor: add dgp1130 to DevTools PullApprove config (#51301)
DevTools falls under Angular Tooling and I'll probably be reviewing future PRs for the extension.
PR Close #51301
2023-08-09 10:19:27 -07:00
Gerald Monaco 95387047fd docs: Add after*Render to Component Lifecycle guide (#51291)
PR Close #51291
2023-08-09 10:18:28 -07:00
Paweł Rawicki 899e51ad8e docs: Instead of CanActivateFn, CanActivateChildFn appears in CanActivateFn docs (#51283)
A minor error is present within the documentation. Specifically, in the documentation for the CanActivateFn function, a reference is made to the CanActivateChildFn function. However, it appears that the CanActivateChildFn function is not utilized or referenced elsewhere in the documentation of CanActivateFn.
PR Close #51283
2023-08-08 10:40:54 -07:00
Matthieu Riegler 0ba2b5e5c3 refactor(animations): remove unecessary interface (#50662)
`Animation` is provided by `lib.dom`.

PR Close #50662
2023-08-07 09:26:26 -07:00
Andrew Kushnir 9f490da7e2 fix(core): handle hydration of view containers for root components (#51247)
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).

Previously, we were only including component-related information, thus all the views in the view container remained dehydrated and duplicated (client-rendered from scratch) on the client.

Resolves #51157.

PR Close #51247
2023-08-07 09:24:26 -07:00
Matthieu Riegler b95b5b55d8 refactor(core): throw an error when hydration marker is missing from DOM (#51170) (#51276)
non-destructive hydration expects the DOM tree to have the same structure in both places.
With this commit, the app will throw an error if comments are stripped out by the http server (eg by some CDNs).

PR Close #51276
2023-08-07 09:22:51 -07:00
Matthieu Riegler 26781fd0fe refactor(core): remove unnecessary import for custom expect. (#51216)
Also adding some missing assertion after `expect()`

PR Close #51216
2023-08-07 09:22:20 -07:00
Matthieu Riegler f79fd35042 docs: add deprecation of PACKAGE_ROOT_URL (#51222)
PR Close #51222
2023-08-07 09:21:47 -07:00
Matthieu Riegler dfa772d686 refactor(core): remove unused private provider (#51222)
Both `DEFAULT_PACKAGE_URL_PROVIDER` and `ERROR_COLLECTOR_TOKEN` are unused across the project

PR Close #51222
2023-08-07 09:21:46 -07:00
Matthieu Riegler c7c0723c7f refactor(core): deprecate PACKAGE_ROOT_URL token (#51222)
Usage was removed in #43884

PR Close #51222
2023-08-07 09:21:46 -07:00
Pawel Kozlowski e01549b26b refactor(core): introduce LView and LContainer utility functions (#51191)
This refactoring adds utility functions to add / remove LView from LContainer.
Those utils are preparation for the control flow and defer work.
Existing code was refactored to use the new utility functions and avoid
any code duplication.

PR Close #51191
2023-08-04 16:00:46 -04:00
aanchal cf62063174 docs: add title tag to index file (#51266)
PR Close #51266
2023-08-04 11:29:52 -04:00
Matthieu Riegler c0c6d1d892 docs: fix missing space after full stop (#51252)
Fixes #51250

PR Close #51252
2023-08-04 11:28:36 -04:00
Andrew Kushnir d1fa6cd7d9 refactor(compiler): drop regular imports when symbols can be defer-loaded (#51171)
This commit updates the logic to drop regular imports when all symbols that it brings can be defer-loaded.
The change ensures that there is no mix of regular and dynamic imports present in a source file.

PR Close #51171
2023-08-04 11:28:07 -04:00
Alex Rickabaugh 1472515d68 release: cut the v16.2.0-rc.0 release 2023-08-02 13:20:39 -07:00
87 changed files with 1424 additions and 426 deletions
+1 -53
View File
@@ -249,6 +249,7 @@ jobs:
- ./ng
- ./bazel_repository_cache
# TODO: Remove test job after confirmation that the GHA version is stable.
test:
executor:
name: test-browser-executor
@@ -312,48 +313,6 @@ jobs:
- notify_webhook_on_fail:
webhook_url_env_var: SLACK_DEV_INFRA_CI_FAILURES_WEBHOOK_URL
test_aio:
executor:
name: test-browser-executor
resource_class: 2xlarge+
steps:
- custom_attach_workspace
- init_environment
# Run all aio tests
- run:
command: yarn --cwd aio test:ci
no_output_timeout: 20m
# Check the bundle sizes.
- run: yarn --cwd aio payload-size
deploy_aio:
executor:
name: test-browser-executor
resource_class: 2xlarge+
steps:
- custom_attach_workspace
- init_environment
# Deploy angular.io to production (if necessary)
- run: setPublicVar_CI_STABLE_BRANCH
- run: yarn --cwd aio deploy-production
test_aio_local:
executor:
name: test-browser-executor
resource_class: 2xlarge+
steps:
- custom_attach_workspace
- init_environment
# Run all aio tests (with local Angular packages)
- run:
command: yarn --cwd aio test-local:ci
no_output_timeout: 20m
# Check the bundle sizes.
- run: yarn --cwd aio payload-size aio-local
# The `build-npm-packages` tasks exist for backwards-compatibility with old scripts and
# tests that rely on the pre-Bazel `dist/packages-dist` output structure (build.sh).
# Having multiple jobs that independently build in this manner duplicates some work; we build
@@ -593,15 +552,6 @@ workflows:
- legacy-unit-tests-saucelabs:
requires:
- setup
- test_aio:
requires:
- setup
- deploy_aio:
requires:
- test_aio
- test_aio_local:
requires:
- setup
- publish_packages_as_artifacts:
requires:
- build-npm-packages
@@ -610,8 +560,6 @@ workflows:
requires:
# Only publish if tests and integration tests pass
- test
# Only publish if `aio`/`docs` tests using the locally built Angular packages pass
- test_aio_local
# Get the artifacts to publish from the build-packages-dist job
# since the publishing script expects the legacy outputs layout.
- build-npm-packages
+80
View File
@@ -89,3 +89,83 @@ jobs:
run: yarn install --frozen-lockfile --network-timeout 100000
- name: Test all windows CI targets
run: bazel test --test_tag_filters="-browser:chromium-local" //packages/compiler-cli/...
test:
runs-on: ubuntu-latest-4core
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@45de46d9ba0e0689b7a846fd31ec9e241807ca71
with:
cache-node-modules: true
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@45de46d9ba0e0689b7a846fd31ec9e241807ca71
- name: Setup Bazel Remote Caching
uses: angular/dev-infra/github-actions/bazel/configure-remote@45de46d9ba0e0689b7a846fd31ec9e241807ca71
- name: Install node modules
run: yarn install --frozen-lockfile --network-timeout 100000
- name: Run CI tests for framework
run: yarn test:ci
aio:
runs-on: ubuntu-latest-4core
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@45de46d9ba0e0689b7a846fd31ec9e241807ca71
with:
cache-node-modules: true
node-module-directories: |
./aio/node_modules
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@45de46d9ba0e0689b7a846fd31ec9e241807ca71
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@45de46d9ba0e0689b7a846fd31ec9e241807ca71
- name: Install node modules for aio
run: yarn install --cwd aio --frozen-lockfile
- name: Run AIO tests with upstream packages
run: yarn --cwd aio test:ci
- name: Check generated bundle sizes
run: yarn --cwd aio payload-size
aio-local:
runs-on:
labels: ubuntu-latest-4core
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@45de46d9ba0e0689b7a846fd31ec9e241807ca71
with:
cache-node-modules: true
node-module-directories: |
./aio/node_modules
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@45de46d9ba0e0689b7a846fd31ec9e241807ca71
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@45de46d9ba0e0689b7a846fd31ec9e241807ca71
- name: Install node modules
run: yarn install --cwd aio --frozen-lockfile
- name: Run AIO tests with local packages
run: yarn --cwd aio test-local:ci
- name: Check generated bundle sizes
run: yarn --cwd aio payload-size aio-local
aio-deploy:
needs: [aio]
if: needs.aio.result == success() && github.event_name == 'push'
runs-on:
labels: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@45de46d9ba0e0689b7a846fd31ec9e241807ca71
with:
cache-node-modules: true
node-module-directories: |
./aio/node_modules
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@45de46d9ba0e0689b7a846fd31ec9e241807ca71
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@45de46d9ba0e0689b7a846fd31ec9e241807ca71
- name: Install node modules for aio
run: yarn install --cwd aio --frozen-lockfile
- name: Set the stable branch environment variable
run: export CI_STABLE_BRANCH=$(npm info @angular/core dist-tags.latest | sed -r 's/^\\s*([0-9]+\\.[0-9]+)\\.[0-9]+.*$/\\1.x/')
- name: Deploy aio to production
run: yarn --cwd aio deploy-production
+1 -1
View File
@@ -58,7 +58,6 @@ version: 3
availability:
users_unavailable:
- atscott
- devversion
# Meta field that goes unused by PullApprove to allow for defining aliases to be
@@ -1124,6 +1123,7 @@ groups:
users:
- AleksanderBodurri
- devversion
- dgp1130
- josephperrott
- mgechev
- twerske
+42 -89
View File
@@ -1,3 +1,45 @@
<a name="16.2.1"></a>
# 16.2.1 (2023-08-16)
### router
| Commit | Type | Description |
| -- | -- | -- |
| [232a8c1b8d](https://github.com/angular/angular/commit/232a8c1b8dadf3f886b4bd0142613d116c865759) | fix | Apply named outlets to children empty paths not appearing in the URL ([#51292](https://github.com/angular/angular/pull/51292)) |
<!-- CHANGELOG SPLIT MARKER -->
<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 +49,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 +58,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 +67,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 +102,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 +120,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 -1
View File
@@ -1,4 +1,5 @@
<html>
<!DOCTYPE html>
<html lang="en">
<head>
<title>Your first Angular app</title>
</head>
+12
View File
@@ -0,0 +1,12 @@
@name HTML content was altered after server-side rendering
@category runtime
@shortDescription HTML content was altered after server-side rendering
@description
Angular throws this error when it detects that the content generated by server-side rendering (SSR) was altered after the rendering. The process of hydration relies on the content to be untouched after SSR, which also includes whitespaces and comment nodes. Those whitespaces and comment nodes must be retained in the HTML generated by the SSR process. Learn more in the [Hydration guide](guide/hydration#constraints).
@debugging
Typically this happens in the following cases:
* Some CDN providers have a built-in feature to remove whitespaces and comment nodes from HTML as an optimization. Please verify if there is such an option in CDN configuration and turn it off.
* If you use custom post-processing of HTML generated by SSR (as a build step), make sure that this process doesn't remove whitespaces and comment nodes.
@@ -1,18 +1,22 @@
<div class="tax-return">
<div class="msg" [class.canceled]="message==='Canceled'">{{message}}</div>
<fieldset>
<legend>Tax Return</legend>
<span id=name>{{taxReturn.name}}</span>
<span id=tid>TID: {{taxReturn.tid}}</span>
</fieldset>
<fieldset>
<legend>Tax Return</legend>
<label for="income">
Income: <input id="income" [(ngModel)]="taxReturn.income" class="num">
</label>
</fieldset>
<fieldset>
<legend>Tax Return</legend>
<span>Tax: {{taxReturn.tax}}</span>
</fieldset>
<fieldset>
<legend>Tax Return</legend>
<button type="button" (click)="onSaved()">Save</button>
<button type="button" (click)="onCanceled()">Cancel</button>
<button type="button" (click)="onClose()">Close</button>
@@ -1,6 +1,6 @@
// #docregion
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { ActivatedRoute, Router } from '@angular/router';
@Component({
selector: 'app-compose-message',
@@ -12,7 +12,7 @@ export class ComposeMessageComponent {
message = '';
sending = false;
constructor(private router: Router) {}
constructor(private router: Router, private route: ActivatedRoute) {}
send() {
this.sending = true;
@@ -32,7 +32,7 @@ export class ComposeMessageComponent {
closePopup() {
// Providing a `null` value to the named outlet
// clears the contents of the named outlet
this.router.navigate([{ outlets: { popup: null }}]);
this.router.navigate([{outlets: {popup: null}}], {relativeTo: this.route.parent});
}
// #enddocregion closePopup
}
@@ -1,4 +1,5 @@
<html>
<!DOCTYPE html>
<html lang="en">
<head>
<base href="/">
<title>Angular App</title>
@@ -6,4 +7,4 @@
<body>
<app-root>loading</app-root>
</body>
</html>
</html>
@@ -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>
@@ -1,6 +1,6 @@
# Injection context
The dependency injection (DI) system relies internaly on a runtime context where the current injector is available.
The dependency injection (DI) system relies internally on a runtime context where the current injector is available.
This means that injectors can only work when code is executed in this context.
The injection context is available in these situations:
@@ -9,7 +9,7 @@ The injection context is available in these situations:
* In the initializer for fields of such classes.
* In the factory function specified for `useFactory` of a `Provider` or an `@Injectable`.
* In the `factory` function specified for an `InjectionToken`.
* Within a stack frame that is run in a injection context.
* Within a stack frame that is run in an injection context.
Knowing when your are in an injection context, will allow you to use the [`inject`](api/core/inject) function to inject instances.
@@ -60,4 +60,4 @@ Calling [`inject`](api/core/inject) or calling `assertInInjectionContext` outsid
@reviewed 2023-04-11
@reviewed 2023-04-11
+7
View File
@@ -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.
+1 -1
View File
@@ -97,7 +97,7 @@ Angular components, templates, and styles go here.
| `app/app.component.html` | Defines the HTML template associated with the root `AppComponent`. |
| `app/app.component.css` | Defines the base CSS stylesheet for the root `AppComponent`. |
| `app/app.component.spec.ts` | Defines a unit test for the root `AppComponent`. |
| `app/app.module.ts` | Defines the root module, named `AppModule`, that tells Angular how to assemble the application. Initially declares only the `AppComponent`. As you add more components to the app, they must be declared here.<br><br>_Only generated when using the `--standalone` option._ |
| `app/app.module.ts` | Defines the root module, named `AppModule`, that tells Angular how to assemble the application. Initially declares only the `AppComponent`. As you add more components to the app, they must be declared here.<br><br>_This file is not generated when using the `--standalone` option._ |
### Application configuration files
@@ -232,7 +232,7 @@ For example, in the following `SelfComponent`, notice the injected `LeafService`
<code-example header="src/app/self-no-data/self-no-data.component.ts" path="resolution-modifiers/src/app/self-no-data/self-no-data.component.ts" region="self-no-data-component"></code-example>
In this example, there is a parent provider and injecting the service will return the value, however, injecting the service with `@Self()` and `@Optional()` will return `null` because `@Self()` tells the injector to stop searching in the current host element.
In this example, there is a parent provider and injecting the service will return the value, however, injecting the service with `@Self()` and `@Optional()` will return `null` because `@Self()` tells the injector to only search in the current host element.
Another example shows the component class with a provider for `FlowerService`.
In this case, the injector looks no further than the current `ElementInjector` because it finds the `FlowerService` and returns the tulip <code>&#x1F337;</code>.
+8 -1
View File
@@ -125,6 +125,13 @@ If you choose to set this setting in your tsconfig, we recommend to set it only
</div>
<a id="cdn-configuration"></a>
### CDN Optimizations
Many CDNs offer a feature that will try to optimize your rendered application by stripping all nodes from the rendered DOM that it thinks are unnecessary, which includes comment nodes. Comment nodes are an essential part of Angular's functioning and are critical for hydration to work. You will need to disable this CDN feature in order to ensure your application loads and hydrates.
If CDN optimization is enabled and you have hydration enabled, when you attempt to load the page you will encounter error [NG0507](https://angular.io/errors/NG0507). If you see this error, you should go and disable the CDN optimization.
### Custom or Noop Zone.js are not yet supported
Hydration relies on a signal from Zone.js when it becomes stable inside an application, so that Angular can start the serialization process on the server or post-hydration cleanup on the client to remove DOM nodes that remained unclaimed.
@@ -184,4 +191,4 @@ re-rendering those components from scratch.
There are a number of third party libraries that depend on DOM manipulation to be able to render. D3 charts is a prime example. These libraries worked without hydration, but they may cause DOM mismatch errors when hydration is enabled. For now, if you encounter DOM mismatch errors using one of these libraries, you can add the `ngSkipHydration` attribute to the component that renders using that library.
@reviewed 2023-06-21
@reviewed 2023-08-14
+91
View File
@@ -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.
+23
View File
@@ -1623,6 +1623,29 @@ This time, the value of `'popup'` is `null`.
That's not a route, but it is a legitimate value.
Setting the popup `RouterOutlet` to `null` clears the outlet and removes the secondary popup route from the current URL.
<div class="alert is-critical">
**Note:** All commands in the array passed to `Router.navigate()` target a _specific segment_ in the `UrlTree`.
We specify the parent of the `ActivatedRoute` as the `relativeTo` option because we want to remove `'popup'` from the segment which holds its reference.
It's important to always be aware of which segments the commands will be applied to.
</div>
<div class="alert is-helpful">
When `relativeTo` is not provided to the `Router.navigate()` method, the commands are processed starting at the root.
We could omit the `relativeTo` option in this particular example because the `'popup'` outlet appears at the root level of the configuration.
</div>
<div class="alert is-helpful">
If you want to close an outlet which appears at any segment depth, you could accomplish
this by creating a `UrlTree` from the current URL, recursively clearing segment `children` matching the outlet name, and finally
calling `Router.navigateByUrl()` with the `root` segment of the current `UrlTree`.
</div>
<a id="guards"></a>
<a id="milestone-5-route-guards"></a>
+1 -1
View File
@@ -47,7 +47,7 @@ Congratulations, your application has been converted to standalone 🎉. These a
* Find and remove any remaining `NgModule` declarations: since the ["Remove unnecessary NgModules" step](#remove-unnecessary-ngmodules) cannot remove all modules automatically, you may have to remove the remaining declarations manually.
* Run the project's unit tests and fix any failures.
* Run any code formatters, if the project uses automatic formatting.
* Run any linters in your project and fix new warnings. Some linters support a `--fix` flag that may resolve so warnings automatically.
* Run any linters in your project and fix new warnings. Some linters support a `--fix` flag that may resolve some of your warnings automatically.
## Migration modes
The migration has the following modes:
+1 -1
View File
@@ -86,7 +86,7 @@ To clarify how changes are detected and values updated, consider the following c
value = v;
// call detectChange manually
detectChange();
}, 100);
});
// Example 5: some other asynchronous APIs
document.getElementById('canvas').toBlob(blob =&gt; {
+5
View File
@@ -574,6 +574,11 @@
"desc": "NGX Cookie Service is an Angular Library that can be used to read, set and delete browser cookies",
"title": "NGX Cookie Service",
"url": "https://github.com/stevermeister/ngx-cookie-service"
},
"flowbite": {
"desc": "Open-source UI component library built with the utility-classes from Tailwind CSS and with official support for Angular.",
"title": "Flowbite",
"url": "https://flowbite.com/docs/getting-started/angular/"
}
}
}
+1
View File
@@ -5,6 +5,7 @@ set -eu -o pipefail
readonly thisDir=$(cd $(dirname $0); pwd)
readonly parentDir=$(dirname $thisDir)
readonly target=${1:-aio}
readonly PROJECT_ROOT=$(realpath "$(dirname ${thisDir})/..")
# Track payload size functions
source ../scripts/ci/payload-size.sh
@@ -42,6 +42,7 @@
</code-example>
</td>
</tr>
{%- if option.description or option.usageNotes %}
<tr>
<td>
{%- if option.description %}
@@ -53,6 +54,7 @@
{% endif %}
</td>
</tr>
{% endif %}
</tbody>
</table>
{% endfor %}
+2
View File
@@ -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,
+1 -1
View File
@@ -1126,7 +1126,7 @@ export interface OutputDecorator {
new (alias?: string): any;
}
// @public
// @public @deprecated
export const PACKAGE_ROOT_URL: InjectionToken<string>;
// @public
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "angular-srcs",
"version": "16.2.0-next.4",
"version": "16.2.1",
"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 {
@@ -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 {}
}
+30 -2
View File
@@ -33,6 +33,7 @@ import {HttpEvent} from './response';
* @publicApi
*
* @see [HTTP Guide](guide/http-intercept-requests-and-responses)
* @see {@link HttpInterceptorFn}
*
* @usageNotes
*
@@ -85,8 +86,35 @@ export type HttpHandlerFn = (req: HttpRequest<unknown>) => Observable<HttpEvent<
* request) is provided. Most interceptors will delegate to this function, but that is not required
* (see `HttpHandlerFn` for more details).
*
* `HttpInterceptorFn`s have access to `inject()` via the `EnvironmentInjector` from which they were
* configured.
* `HttpInterceptorFn`s are executed in an [injection context](/guide/dependency-injection-context).
* They have access to `inject()` via the `EnvironmentInjector` from which they were configured.
*
* @see [HTTP Guide](guide/http-intercept-requests-and-responses)
* @see {@link withInterceptors}
*
* @usageNotes
* Here is a noop interceptor that passes the request through without modifying it:
* ```typescript
* export const noopInterceptor: HttpInterceptorFn = (req: HttpRequest<unknown>, next:
* HttpHandlerFn) => {
* return next(modifiedReq);
* };
* ```
*
* If you want to alter a request, clone it first and modify the clone before passing it to the
* `next()` handler function.
*
* Here is a basic interceptor that adds a bearer token to the headers
* ```typescript
* export const authenticationInterceptor: HttpInterceptorFn = (req: HttpRequest<unknown>, next:
* HttpHandlerFn) => {
* const userToken = 'MY_TOKEN'; const modifiedReq = req.clone({
* headers: req.headers.set('Authorization', `Bearer ${userToken}`),
* });
*
* return next(modifiedReq);
* };
* ```
*/
export type HttpInterceptorFn = (req: HttpRequest<unknown>, next: HttpHandlerFn) =>
Observable<HttpEvent<unknown>>;
+1
View File
@@ -190,6 +190,7 @@ export const DATE_PIPE_DEFAULT_OPTIONS =
* {{ dateObj | date:'medium' }} // output is 'Jun 15, 2015, 9:43:11 PM'
* {{ dateObj | date:'shortTime' }} // output is '9:43 PM'
* {{ dateObj | date:'mm:ss' }} // output is '43:11'
* {{ dateObj | date:"MMM dd, yyyy 'at' hh:mm a" }} // output is 'Jun 15, 2015 at 09:43 PM'
* ```
*
* ### Usage example
@@ -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);
}
+171 -4
View File
@@ -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 }');
});
});
});
});
+1
View File
@@ -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');
+1
View File
@@ -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';
@@ -155,7 +155,7 @@ export function inject<T>(token: ProviderToken<T>, options: InjectOptions&{optio
export function inject<T>(token: ProviderToken<T>, options: InjectOptions): T|null;
/**
* Injects a token from the currently active injector.
* `inject` is only supported in a [injection context](/guide/dependency-injection-context). It can
* `inject` is only supported in an [injection context](/guide/dependency-injection-context). It can
* be used during:
* - Construction (via the `constructor`) of a class being instantiated by the DI system, such
* as an `@Injectable` or `@Component`.
+1
View File
@@ -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,
+69 -16
View File
@@ -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;
}
}
+45 -9
View File
@@ -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();
}
},
@@ -176,11 +177,15 @@ export function withDomHydration(): EnvironmentProviders {
const appRef = inject(ApplicationRef);
const injector = inject(Injector);
return () => {
// Wait until an app becomes stable and cleanup all views that
// were not claimed during the application bootstrap process.
// The timing is similar to when we start the serialization process
// on the server.
//
// Note: the cleanup task *MUST* be scheduled within the Angular zone
// to ensure that change detection is properly run afterward.
whenStable(appRef, injector).then(() => {
// Wait until an app becomes stable and cleanup all views that
// were not claimed during the application bootstrap process.
// The timing is similar to when we start the serialization process
// on the server.
NgZone.assertInAngularZone();
cleanupDehydratedViews(appRef);
if (typeof ngDevMode !== 'undefined' && ngDevMode) {
@@ -209,3 +214,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 enabled on your CDN. Angular hydration ' +
'relies on HTML produced by the server, including whitespaces and comment nodes.');
}
}
+14 -5
View File
@@ -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++;
}
}
+64 -20
View File
@@ -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;
}
+6 -25
View File
@@ -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);
}
};
+4 -12
View File
@@ -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);
+22 -6
View File
@@ -900,23 +900,35 @@ export const Output: OutputDecorator = makePropDecorator('Output', (alias?: stri
*/
export interface HostBindingDecorator {
/**
* Decorator that marks a DOM property as a host-binding property and supplies configuration
* metadata.
* Angular automatically checks host property bindings during change detection, and
* if a binding changes it updates the host element of the directive.
* Decorator that marks a DOM property or an element class, style or attribute as a host-binding
* property and supplies configuration metadata. Angular automatically checks host bindings during
* change detection, and if a binding changes it updates the host element of the directive.
*
* @usageNotes
*
* The following example creates a directive that sets the `valid` and `invalid`
* properties on the DOM element that has an `ngModel` directive on it.
* class, a style color, and an id on the DOM element that has an `ngModel` directive on it.
*
* ```typescript
* @Directive({selector: '[ngModel]'})
* class NgModelStatus {
* constructor(public control: NgModel) {}
* // class bindings
* @HostBinding('class.valid') get valid() { return this.control.valid; }
* @HostBinding('class.invalid') get invalid() { return this.control.invalid; }
* }
*
* // style binding
* @HostBinding('style.color') get color() { return this.control.valid ? 'green': 'red'; }
*
* // style binding also supports a style unit extension
* @HostBinding('style.width.px') @Input() width: number = 500;
*
* // attribute binding
* @HostBinding('attr.aria-required')
* @Input() required: boolean = false;
*
* // property binding
* @HostBinding('id') get id() { return this.control.value?.length ? 'odd': 'even'; }
*
* @Component({
* selector: 'app',
@@ -940,6 +952,10 @@ export interface HostBindingDecorator {
export interface HostBinding {
/**
* The DOM property that is bound to a data property.
* This field also accepts:
* * classes, prefixed by `class.`
* * styles, prefixed by `style.`
* * attributes, prefixed by `attr.`
*/
hostPropertyName?: string;
}
+7 -2
View File
@@ -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;
}
+2 -2
View File
@@ -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"
},
+4 -5
View File
@@ -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';
-1
View File
@@ -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');
+17 -2
View File
@@ -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.
+314 -12
View File
@@ -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,
+6 -6
View File
@@ -338,10 +338,10 @@ function updateSegmentGroupChildren(
} else {
const outlets = getOutlets(commands);
const children: {[key: string]: UrlSegmentGroup} = {};
// If the set of commands does not apply anything to the primary outlet and the child segment is
// an empty path primary segment on its own, we want to apply the commands to the empty child
// path rather than here. The outcome is that the empty primary child is effectively removed
// from the final output UrlTree. Imagine the following config:
// If the set of commands applies to anything other than the primary outlet and the child
// segment is an empty path primary segment on its own, we want to apply the commands to the
// empty child path rather than here. The outcome is that the empty primary child is effectively
// removed from the final output UrlTree. Imagine the following config:
//
// {path: '', children: [{path: '**', outlet: 'popup'}]}.
//
@@ -359,8 +359,8 @@ function updateSegmentGroupChildren(
// `UrlSegmentGroup` that is created from an "unsquashed"/expanded `ActivatedRoute` tree.
// This code effectively "squashes" empty path primary routes when they have no siblings on
// the same level of the tree.
if (!outlets[PRIMARY_OUTLET] && segmentGroup.children[PRIMARY_OUTLET] &&
segmentGroup.numberOfChildren === 1 &&
if (Object.keys(outlets).some(o => o !== PRIMARY_OUTLET) &&
segmentGroup.children[PRIMARY_OUTLET] && segmentGroup.numberOfChildren === 1 &&
segmentGroup.children[PRIMARY_OUTLET].segments.length === 0) {
const childrenOfEmptyChild =
updateSegmentGroupChildren(segmentGroup.children[PRIMARY_OUTLET], startIndex, commands);
+1 -1
View File
@@ -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"}
@@ -231,6 +231,39 @@ describe('createUrlTree', async () => {
.toEqual('/case/(foo:foo)');
});
it('can change both primary and named outlets under an empty path', async () => {
router.resetConfig([{
path: 'foo',
children: [
{
path: '',
component: class {},
children: [
{path: 'bar', component: class {}},
{path: 'baz', component: class {}, outlet: 'other'},
],
},
]
}]);
await router.navigateByUrl('/foo/(bar//other:baz)');
expect(router.url).toEqual('/foo/(bar//other:baz)');
expect(router
.createUrlTree(
[
{
outlets: {
other: null,
primary: ['bar'],
},
},
],
// relative to the root '' route
{relativeTo: router.routerState.root.firstChild})
.toString())
.toEqual('/foo/bar');
});
describe('absolute navigations', () => {
it('with and pathless root', async () => {
router.resetConfig([
+1 -1
View File
@@ -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": [],
+1 -1
View File
@@ -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"}]
}
}
+1 -1
View File
@@ -27,7 +27,7 @@
},
"rootDir": ".",
"inlineSourceMap": true,
"lib": ["es2020", "dom"],
"lib": ["es2020", "dom", "dom.iterable"],
"skipDefaultLibCheck": true,
"skipLibCheck": true,
"types": ["angular"]