Compare commits

...

109 Commits

Author SHA1 Message Date
Andrew Scott 58ee039d5e release: cut the v18.1.2 release 2024-07-24 10:53:25 -07:00
Matthieu Riegler 3d0ee73892 docs(docs-infra): Use shiki for code highlighting (#57059)
PR Close #57059
2024-07-24 10:24:52 -07:00
Matthieu Riegler 8bb5937e71 docs(docs-infra): disable experimental decorators (#56257)
Also enable useDefineForClassFields and disable downlevelIteration

PR Close #56257
2024-07-24 10:18:22 -07:00
Andrew Scott c969422da0 Revert "fix(router): Scroller should scroll as soon as change detection completes (#55105)" (#57115)
This reverts commit 66ffeca2de.

It looks like nuxt encountered the same issue with scrolling when
waiting for Vue's `nextTick`, which is a microtask. This would have
similar timing to ZoneJS's rendering in the microtask queue. This
reverts to a `setTimeout` alone, though recreates the problem in #53985.
This was also mentioned in one of the comments in the Nuxt issue and the
solution would be `rAF`.

In order to address #53985, we'd likely want to use the `race(rAF, setTimeout)`
that we use in the zoneless and coalescing schedulers. This would have effectively the same timing as
the `afterNextRender` implementation here with zoneless, but quite
different timing to `afterNextRender` with ZoneJS.

fixes #57109

PR Close #57115
2024-07-24 10:04:38 -07:00
Tom Wilkinson 1a606b0bfc refactor(core): Switch to using concat rather than spread operator for events. (#57103)
Internally, the JSCompiler does not dead code eliminate this code when its using spread.

PR Close #57103
2024-07-23 15:54:23 -07:00
Andrew Scott 288a4fac01 test(docs-infra): fix test with slight adjustment to error handling (#57104)
The test was broken because multiple errors happen and the last error would be the displayed message.
However, in this case, those errors happen because the initial one already terminated the container
so the follow-up errors are a result of the service being in an unexpected state

PR Close #57104
2024-07-23 15:45:55 -07:00
Andrew Scott 889a02b1c3 test(docs-infra): update adev tests to be zoneless (#57100)
Updates adev tests to use zoneless and remove
fakeAsync/waitForAsync/tick

PR Close #57100
2024-07-23 14:50:32 -07:00
Angular Robot 23fa43d044 build: update all non-major dependencies (#57092)
See associated pull request for more information.

PR Close #57092
2024-07-23 12:18:34 -07:00
Angular Robot c979dfd462 build: update github/codeql-action action to v3.25.13 (#57091)
See associated pull request for more information.

PR Close #57091
2024-07-23 10:29:17 -07:00
Andrew Scott 915b0e0b16 Revert "fix(compiler-cli): add warning for unused let declarations (#57033)" (#57088)
This reverts commit c76b440ac0.

PR Close #57088
2024-07-22 15:28:04 -07:00
JoostK 463945003d fix(compiler): limit the number of chained instructions (#57069)
Some Angular template instructions that follow each other may be chained
together in a single expressions statement, containing a deeply nested
AST of call expressions. The number of chained instructions wasn't previously
limited, so this could result in very deep ASTs that cause stack overflow
errors during TypeScript emit.

This commit introduces a limit to the number of chained instructions to
avoid these problems.

Closes #57066

PR Close #57069
2024-07-22 11:50:12 -07:00
Rushikesh Kavar 06ae22680e docs: Update packages/router/src/router.ts (#57053)
Co-authored-by: Andrew Scott <atscott01@gmail.com>
PR Close #57053
2024-07-22 10:52:36 -07:00
Rushikesh Kavar f917b6e4d0 docs: Update packages/router/src/router.ts (#57053)
Co-authored-by: Andrew Scott <atscott01@gmail.com>
PR Close #57053
2024-07-22 10:52:36 -07:00
Rushikesh Kavar 87d042ba8a docs: adding reference to RouterModule.forroot method in Router service for better understanding for beginner (#57053)
PR Close #57053
2024-07-22 10:52:36 -07:00
Rushikesh Kavar 4b561f8f35 docs: Adding Router service hierarchical injection level (#57053)
PR Close #57053
2024-07-22 10:52:36 -07:00
Kristiyan Kostadinov b9fb98c67c fix(core): tree shake dev mode error message (#57035)
Adds an `ngDevMode` check before a runtime error message.

Fixes #57034.

PR Close #57035
2024-07-22 10:49:18 -07:00
xpl0siv 8a005eada5 docs: fix push-notifications.md (#57065)
Added missing comma at actions notifications example.
SyntaxError:
Expected ',' or ']' after array element in JSON at position 255

PR Close #57065
2024-07-22 10:46:56 -07:00
Salanoid ae15dd94ef docs: fixing dead link (#57077)
Fixing guide dead link for issue #57067.

PR Close #57077
2024-07-22 09:58:42 -07:00
Angular Robot 7792aad8da build: update babel dependencies (#57045)
See associated pull request for more information.

Closes #56996 as a pr takeover

PR Close #57045
2024-07-19 13:46:00 -07:00
Thomas Nguyen 142c2dffb0 refactor(core): Allow manual renderer listens to contribute to event delegation as well. (#56799)
There are existing usages that inject the renderer to manualy listen (often for event
delegation purposes). These should contribute as well.

PR Close #56799
2024-07-19 13:42:26 -07:00
Ashish Deshpande 56f0fc1f9f docs: update broken link to version updates in versionEvent (#56870) (#56870)
PR Close #56870
2024-07-19 13:41:54 -07:00
Matthieu Riegler 9e52c1c840 fix(core): afterNextRender hooks return that callback value. (#57031)
`afterRender` was working fine but `afterNextRender` wasn't

PR Close #57031
2024-07-19 13:41:00 -07:00
Kristiyan Kostadinov e904f34020 fix(compiler-cli): add warning for unused let declarations (#57033)
Adds a new extended diagnostic that will flag `@let` declarations that aren't used within the template. The diagnostic can be turned off through the `extendedDiagnostics` compiler option.

PR Close #57033
2024-07-19 11:50:32 -07:00
Matthieu Riegler be16d7fcc3 docs: fix filtering on API reference (#57057)
With the input binding, the `type` can become `undefined`

PR Close #57057
2024-07-19 11:49:05 -07:00
Matthieu Riegler 04c504ab40 docs: fix pipes link (#52979)
fixes #52968

PR Close #52979
2024-07-18 18:05:07 -07:00
Matthieu Riegler 91483ab5b3 docs(docs-infra): filter types (#52979)
PR Close #52979
2024-07-18 18:05:07 -07:00
Matthieu Riegler e088c80854 docs(docs-infra): update tutorial deps to 18.1 (#57024)
lockfile were still loading 18.0 but TS was already at 5.5 which 18.0 doesn't support

PR Close #57024
2024-07-18 15:27:24 -07:00
Sheik Althaf 14d442a6c3 refactor(devtools): use signal api in property view (#56995)
Refactor the property view components to use signal apis, in future we
can make the components onPush and zoneless

PR Close #56995
2024-07-18 15:23:32 -07:00
Matthieu Riegler 1999e31851 docs: Add @let block entry to the API (#57027)
PR Close #57027
2024-07-18 15:22:42 -07:00
qshurick f1349e1d0f docs: Update service code snippet (#57036)
Snippet was missing import for the function and failed when copied to the test
project

PR Close #57036
2024-07-18 15:21:32 -07:00
Tom Wilkinson f3014c5392 refactor(core): Move logic into early event contract files. (#56994)
Also remove some dead a11y code, now that a11y is in `ActionResolver`.

PR Close #56994
2024-07-18 12:05:18 -07:00
Fred Sauer bbabf5451a docs: fix headings and code font in @for docs (#57007)
- Fix "Syntax" and "Description" h2 headings
- Fix code font in h3 headings

PR Close #57007
2024-07-18 11:07:43 -07:00
Tomek Sułkowski edbb403171 docs(docs-infra): add open in StackBlitz button to code editor (#57018)
PR Close #57018
2024-07-18 08:41:35 -07:00
Matthieu Riegler d3e14b35d3 docs(docs-infra): no prefix for the current major (#56882)
To allow navigation back from next.angular.dev, the current major uses no prefix.

fixes #56868

PR Close #56882
2024-07-18 08:40:09 -07:00
Andrew Scott 4bfe707ac2 release: cut the v18.1.1 release 2024-07-17 13:05:52 -07:00
Alan Agius 4f11d6eee2 fix(zone.js): support Timeout.refresh in Node.js (#56852)
The `Timeout` object in Node.js has a `refresh` method, used to restart `setTimeout`/`setInterval` timers. Before this commit, `Timeout.refresh` was not handled, leading to memory leaks when using `fetch` in Node.js. This issue arose because `undici` (the Node.js fetch implementation) uses a refreshed `setTimeout` for cleanup operations.

For reference, see: https://github.com/nodejs/undici/blob/1dff4fd9b1b2cee97c5f8cf44041521a62d3f133/lib/util/timers.js#L45

Fixes: #56586

PR Close #56852
2024-07-16 12:46:52 -07:00
Matthieu Riegler a1cb9dfc0d fix(common): Don't run preconnect assertion on the server. (#56213)
The `window` global is patched by domino on the server but the value of `window.location.href` isn't a valid base.

Before this change `getUrl()` would throw when running in devmode on the server.

Fixes #56207

PR Close #56213
2024-07-16 09:53:33 -07:00
Matthieu Riegler f02f351cda docs: Expose $localize in the @angular/localize/init entry. (#55345)
fixes #54388

PR Close #55345
2024-07-16 08:43:53 -07:00
homerlsy e9837c94d0 docs: update structural-directives.md select to [select] (#56955)
this custom directive's selector should be [select]
PR Close #56955
2024-07-16 08:42:59 -07:00
Kristiyan Kostadinov 5682527d94 fix(core): not all callbacks running when registered at the same time (#56981)
Fixes that only the first callback was firing when multiple are registered in the same call to `afterNextRender`, e.g. `afterNextRender({earlyRead: fn, read: fn});`

Fixes #56979.

PR Close #56981
2024-07-16 08:42:26 -07:00
Angular Robot 4c1f3cf843 build: update github/codeql-action action to v3.25.12 (#56997)
See associated pull request for more information.

PR Close #56997
2024-07-16 08:41:58 -07:00
Kristiyan Kostadinov daf0317bdc fix(compiler): JIT mode incorrectly interpreting host directive configuration in partial compilation (#57002)
Fixes that the runtime implementation of `ɵɵngDeclareDirective` was interpreting the `hostDirectives` mapping incorrectly. Instead of treating the inputs/outputs as `['binding', 'alias']` arrays, it was parsing them as `['binding: alias']`. This was leading to runtime errors if a user is consuming a partially-compiled library in JIT mode.

Fixes #54096.

PR Close #57002
2024-07-16 08:37:26 -07:00
sihyunjojo 6cfc4d8c85 docs: change code formatting for better readability (#56974)
This change improves the consistency and visibility of the documentation by using backticks for code formatting in the instructions.

PR Close #56974
2024-07-15 12:05:44 -07:00
Angular Robot da9149ea10 build: update dependency inquirer to v10 (#56883)
See associated pull request for more information.

PR Close #56883
2024-07-15 11:49:27 -07:00
Matthieu Riegler bbe655ff14 refactor(compiler): include public constructor paramters to class properties. (#56315)
Public properties declared in the constructor are part of the public API and we should extract them.

Fixes #56310

PR Close #56315
2024-07-15 11:39:22 -07:00
Ben Hong 7c0a7a02c0 docs: add quick installation page (#56816)
PR Close #56816
2024-07-15 11:37:57 -07:00
Angular Robot 35b2728994 build: update actions/upload-artifact action to v4.3.4 (#56905)
See associated pull request for more information.

PR Close #56905
2024-07-15 11:35:00 -07:00
Angular Robot 653e898f78 build: update io_bazel_rules_sass digest to 83022b9 (#56954)
See associated pull request for more information.

PR Close #56954
2024-07-15 11:34:26 -07:00
Matthieu Riegler c0855f0e25 build: update material / cdk (#56960)
Material 18.1 includes a fix we were waiting for adev.

Fixes #56786

PR Close #56960
2024-07-15 11:23:40 -07:00
Kristiyan Kostadinov c94a897248 fix(compiler-cli): avoid emitting references to typecheck files in TS 5.4 (#56961)
In #56358 we removed most of the places that untag the references to typecheck files, because it was causing the compiler to throw error when it produces diagnostics. This appears to have caused a regression in TS 5.4 which now emits the synthetic references.

These changes add tagging right before the program emits.

Fixes #56945.

PR Close #56961
2024-07-15 11:22:00 -07:00
Jessica Janiuk b666d2c20f fix(migrations): fix common module removal (#56968)
This fixes the case that common module is removed on a second run of the migration. We were not looking at block parameters for common module usage.

PR Close #56968
2024-07-15 11:17:46 -07:00
Matthieu Riegler 634d55a4f6 refactor(compiler-cli): Extract call signatures from interfaces. (#56973)
This commit adds support for extracting call signals from interfaces.

fixes #56969

PR Close #56973
2024-07-15 11:08:00 -07:00
Matthieu Riegler 5c08ae68ad ci: add an entry for aio in the pullapprove config (#56952)
This would allow me & ben to review some of the changes on the 17.3 branch, for example the redirections.

PR Close #56952
2024-07-11 17:03:26 -07:00
Matthieu Riegler 60df722d29 docs(docs-infra): focus api search filter on non-touch devices (#56294)
PR Close #56294
2024-07-11 12:05:34 -07:00
Paul Gschwendtner a531106578 build: fix rate limits when deploying angular.dev site (#56929)
This commit fixes that the angular.dev deployment is subject
to GitHub API rate limiting due to lack of an access token.

This commit fixes this, similar to how we fixed it in
`angular/components`. The token is pure read-only.

PR Close #56929
2024-07-11 08:54:51 -07:00
Tomer953 d7dca6dbb6 fix(compiler): use strict equality for 'code' comparison (#56944)
Replace loose equality (==) with strict equality (===) for the 'code' variable.
This change ensures type safety and prevents unintended type coercion.
PR Close #56944
2024-07-11 08:52:58 -07:00
Matthieu Riegler f6eec4813f build: update deps (#56946)
Updating `build-tooling` and `docs`.

PR Close #56946
2024-07-11 08:52:28 -07:00
Andrew Kushnir 94c6ec0221 docs: fixing links to guides on the Performance Overview page (#56942)
PR Close #56942
2024-07-10 17:06:08 -07:00
Tom Wilkinson a7b774c4a2 refactor(core): Refactor EarlyEventContract to prepare for using it as a container. (#56900)
This is the first step towards combining `EarlyEventContract` and `EventContract`. It contains a few refactors, such as making names more consistent.

The goal of this refactor is to remove the `EarlyEventContract` class altogether, as well as `EventContract`.

To install the early event contract with the default events in early script tag, users will call:

`bootstrapGlobalEarlyEventContract()`

And for boostraping:

`registerGlobalDispatcher(dispatcher)`

PR Close #56900
2024-07-10 13:53:04 -07:00
Michael van der Luit 5673f6b04d docs: move public folder to workspace configuration section (#56931)
move the public folder entry from the application project files section to the workspace configuration files section

PR Close #56931
2024-07-10 13:52:06 -07:00
Ben Hong aedf8322b0 docs: add overview page for performance section (#56828)
PR Close #56828
2024-07-10 18:17:05 +02:00
Angular Robot e076eb2ae6 build: update actions/upload-artifact action to v4.3.4 (#56906)
See associated pull request for more information.

PR Close #56906
2024-07-10 17:28:33 +02:00
Angular Robot aa08ecc538 build: update io_bazel_rules_sass digest to ea6b195 (#56917)
See associated pull request for more information.

PR Close #56917
2024-07-10 17:24:58 +02:00
Tom Wilkinson 4b82b9815d refactor(core): Remove unused A11Y_CLICK_SUPPORT define. (#56901)
This behavior was moved to be configured by a runtime boolean, rather
than a define.

PR Close #56901
2024-07-10 15:50:39 +02:00
Matthieu Riegler bbbaf7a1b8 docs(docs-infra): fix z-index issue on mobile (#56874)
fixes #56871

PR Close #56874
2024-07-10 15:48:42 +02:00
Matthieu Riegler 729bf5b828 docs: fix testing angular url (#56847)
PR Close #56847
2024-07-10 15:18:12 +02:00
Pawel Kozlowski 07a0b87a4c release: cut the v18.1.0 release 2024-07-10 15:00:47 +02:00
Harish Krishnan 551ffbf9f2 docs: Update components-scenarios.md (#56921)
Fix grammatical issues

PR Close #56921
2024-07-10 11:29:17 +02:00
cexbrayat 4d87b9e899 fix(core): rename the equality function option in toSignal (#56769) (#56922)
The option introduced in 5df3e78c99 has been named `equals` whereas the existing option in `signal` is named `equal`.
This commit renames the new option to `equal` as well to keep the naming coherent across these APIs.

PR Close #56769

PR Close #56922
2024-07-10 11:09:07 +02:00
Matthieu Riegler 0da9eae24c docs: update version table for 18.1 (#56920)
PR Close #56920
2024-07-10 09:34:48 +02:00
Andrew Scott 15d2e8d54c docs: Update component testing scenarios (#56388)
This update goes through half of the component-scenarios guide and makes
relevant updates to examples.

Updates include:

* Use signals in examples
* Advise and use `await fixture.whenStable` more frequently than
  `detectChanges`
* Use and advise mocks and spies more sparingly
* Remove `waitForAsync` and jasmine `done` guidance - The ecosystem has evolved
  and these aren't needed
* Remove marble testing for rxjs - this belongs in rxjs documentation,
  not in Angular
* Remove class-only component testing. This approach is not advisable
  for components.

related to #48510

PR Close #56388
2024-07-10 09:11:21 +02:00
Marcos Paulo dos Santos 3a453d6040 docs: update compat table with 13.4.x (#56915)
PR Close #56915
2024-07-10 09:10:29 +02:00
Andrew Scott 8bd4c074af fix(core): toSignal equal option should be passed to inner computed (#56903)
The user-defined equality function needs to be passed to the inner
computed it will still use `Object.is` and prevent notifications.

PR Close #56903
2024-07-09 19:52:18 +02:00
Jessica Janiuk 5f97d6aec2 fix(migrations): Fix cf migration let condition semicolon order (#56913)
In rare cases people may put let statements before else statements and omit semicolons, causing migration issues.

PR Close #56913
2024-07-09 18:17:52 +02:00
Matthieu Riegler 2f8bf933f4 refactor(compiler-cli): escape missing decorators. (#56794)
Escaping decorators is required as else they are considered as new JSDocs tags.

fixes #56569

PR Close #56794
2024-07-09 13:13:53 +02:00
Ofer Segev ae9505b702 docs: update dependency-injection-context.md (#56736)
HeroService is not part of this example, and is only introduced in the next example. Use type Service1 for this.service1, as its type
PR Close #56736
2024-07-09 13:05:57 +02:00
Matthieu Riegler 0f4219cf0c refactor(platform-server): Add private profiler. (#56274)
The commit adds a private profiler to investigate SSR performance.

PR Close #56274
2024-07-09 12:47:46 +02:00
Kristiyan Kostadinov f689214178 docs: fix wrong link in template overview (#56911)
One of the links added in #56886 was incomplete.

PR Close #56911
2024-07-09 12:13:12 +02:00
Daniel Puckowski aef166f763 fix(compiler): fix CSS animation rule scope (#56800)
It is valid CSS to list keyframe names in an animation declaration only
separating the names with a comma and no whitespace. This is typical of
production builds. Updated a couple of regexes and added a couple of
tests to account for this scenario.

Fixes #53038

PR Close #56800
2024-07-09 09:44:56 +02:00
Kristiyan Kostadinov 02f5e73a8d docs: add documentation page for let declarations (#56886)
Adds a docs page for the new `@let` syntax in Angular 18.1.

PR Close #56886
2024-07-09 09:38:23 +02:00
Matthieu Riegler 20f5e1291e docs: add a FAQ on how to find the LCP image (#56779)
PR Close #56779
2024-07-09 09:37:20 +02:00
Andrew Kushnir 8adc6b9425 test(core): simplify @defer-based test to make CI more stable (#56904)
This commit updates one of the `@defer`-based test to use the `on immediate` trigger to make a test mor
e stable without the need to add mocks for the `on idle` (default) condition.

PR Close #56904
2024-07-09 09:36:12 +02:00
vladboisa 35fa9b0f77 docs(docs-infra): add missing context with link (#56896)
Added missing link documenting ng global features.

Fixes #56488

PR Close #56896
2024-07-08 18:04:00 +02:00
Paul Gschwendtner 56ec97d2e1 refactor(compiler-cli): do not truncate/reduce types in API docs (#56572)
Fixes that e.g. signal input APIs docs were removing `undefined` from
the shorthand `input<T>()` documentation.

PR Close #56572
2024-07-08 16:26:40 +02:00
Matthieu Riegler ba325c59c0 docs: remove private symbol from docs (#56851)
fixes #56850

PR Close #56851
2024-07-08 09:54:14 +02:00
Charles Lyding 447a36ef95 refactor(localize): reduce deep imports in ng add schematic (#56839)
The `@angular/schematic` package has a `utility` export path which can be
used to access common utility rules and helpers. The previous deep imports
into the `@angular/schematic` package have been switched to the actual
export path where possible. This lowers the potential for breakage from
internal package changes.

PR Close #56839
2024-07-08 09:51:53 +02:00
Sasidharan SD 8326e62313 docs: fix previous configuration link (#56880)
PR Close #56880
2024-07-08 09:48:02 +02:00
cexbrayat 9b35726e42 fix(common): typo in warning for NgOptimizedDirective (#56817)
The warning message for the `PLACEHOLDER_DIMENSION_LIMIT_EXCEEDED` check has a repetition of `of`.

PR Close #56817
2024-07-05 13:03:56 +02:00
Kristiyan Kostadinov cdebf751e4 fix(compiler-cli): used before declared diagnostic not firing for control flow blocks (#56843)
When we process `@if` and `@for` blocks, we create a scope around their expressions in order to encapsulate the aliases to them. The problem is that this doesn't represent the actual structure since the expression is part of the outer scope. This surfaces by not raising the "used before declared" diagnostic for `@let` declarations.

These changes resolve the issue by processing the expression as a part of the parent scope.

Fixes #56842.

PR Close #56843
2024-07-05 13:02:58 +02:00
Kristiyan Kostadinov 66e582551e fix(compiler-cli): avoid duplicate diagnostics for let declarations read before definition (#56843)
Fixes that in some cases `@let` declarations that are read before they're defined were producing multiple diagnostics.

PR Close #56843
2024-07-05 13:02:58 +02:00
Dan Nisenson 1efe907881 docs: fix broken link (#56848)
fix link @ /guide/forms/reactive-forms#reactive-forms-api-summary
prev: /api/forms
current: /api#forms

PR Close #56848
2024-07-05 12:27:35 +02:00
Corbin Crutchley 1074134d4b docs: add window title to reference pages (#56840)
PR Close #56840
2024-07-04 14:29:29 +02:00
Alan Agius ecc56447b9 perf(http): execute fetch outside of Angular zone (#56820)
In this update, the fetch backend now executes fetch operations outside of the Angular zone. This adjustment primarily aims to decrease Continuous Delivery (CD) cycles on Node.js. The decision was influenced by Undici, the Node.js fetch implementation, which relies on `setTimeouts` to manage response timeouts.

PR Close #56820
2024-07-04 12:10:35 +02:00
Michael van der Luit 85da4b3b2a docs: update app-shell docs to reflect v17 (#56836)
Fixes the build commands as previously mentioned here: https://github.com/angular/angular/pull/52294

PR Close #56836
2024-07-04 11:32:14 +02:00
Harish Krishnan 3fa21cbbc5 docs: Update image-optimization.md (#56819)
Fix the mistyped attribute rel
PR Close #56819
2024-07-04 09:56:18 +02:00
Matthieu Riegler 1ee64a5c45 refactor(common): Fire priority check on stable. (#56776)
To support routing on app init, the directive will now fire the priority check when the apps become stable.

fixes #56757

PR Close #56776
2024-07-03 14:40:48 +00:00
Andrew Kushnir 3235d6d6dd fix(core): establish proper defer injector hierarchy for components attached to ApplicationRef (#56763)
This commit updates the logic that create an injector for defer blocks (when it's needed) to account for a situation when a component is instantiated without a connection to the current component tree. This can happen if a component is created using its factory function or via `createComponent()` call.

Resolves #56372.

PR Close #56763
2024-07-03 14:08:44 +00:00
Krishna Sahu 6ad2fe6ab0 docs: add toSignal import inside the demo example (#56804)
PR Close #56804
2024-07-03 14:07:51 +00:00
komura-c 0e0fb7bb61 docs: fix missing image and baseUrl in tutorials/first-app (#56766)
PR Close #56766
2024-07-02 20:21:41 +00:00
Andrew Kushnir 8a0a00e212 docs: add Event Replay section to the Hydration guide (#56770)
PR Close #56770
2024-07-02 20:19:53 +00:00
Matthieu Riegler 092bfe95cc fix(core): support injection of object with null constructor. (#56553)
This is debug only code, it shouldn't have any consequences on prod build.

fixes #56552

PR Close #56553
2024-07-02 17:33:32 +00:00
Andrew Scott c4384bf269 fix(router): routes should not get stale providers (#56798)
This fixes a bug with RouterOutlet and its context where it would reuse
providers from a previously activated route.

fixes #56774

PR Close #56798
2024-07-02 17:10:52 +00:00
Angular Robot 52852abb56 build: update github/codeql-action action to v3.25.11 (#56803)
See associated pull request for more information.

PR Close #56803
2024-07-02 13:04:27 -04:00
Ben Hong 0f612dcaf8 docs: update playground app to use hello world for easier onboarding (#56813)
PR Close #56813
2024-07-02 12:57:31 -04:00
Ben Elliott 9acccb11ff docs: Update interceptors.md to replace public class with export class in DI section (#56807)
PR Close #56807
2024-07-02 16:44:12 +00:00
Ben Hong e914767efb docs: add documentation aroun build error for docs (#56814)
PR Close #56814
2024-07-02 16:43:07 +00:00
Angular Robot 58be8e6bf8 build: update all non-major dependencies (#56801)
See associated pull request for more information.

PR Close #56801
2024-07-02 15:26:28 +00:00
Pawel Kozlowski 9cc4dbeadd refactor(common): remove remaining code for NgSwitch equality patch (#56806)
This change is the final cleanup after removing NgSwitch's equality patch
that was used in G3 to allow == matching for a certain period of time. This
code is no longer needed as both the external ecosystem and G3 are using
=== equality for matching NgSwitch cases.

PR Close #56806
2024-07-02 14:16:24 +00:00
Jessica Janiuk 7f5d70a74a release: cut the v18.1.0-rc.0 release 2024-07-01 14:01:57 -07:00
252 changed files with 11072 additions and 7718 deletions
@@ -5,6 +5,9 @@ inputs:
serviceKey:
description: 'The service key used for firebase deployments.'
required: true
githubReleaseTrainReadToken:
description: 'GitHub access token for reading release trains without rate limits.'
required: true
configPath:
description: 'The path to the firebase config file.'
required: true
@@ -13,3 +13,8 @@ export function getCredentialFilePath(): string {
}
return credentialFilePath;
}
/** Github access token. Used for querying the active release trains. */
export const githubReleaseTrainReadToken: string = getInput('githubReleaseTrainReadToken', {
required: true,
});
@@ -1,6 +1,6 @@
import {fetchLongTermSupportBranchesFromNpm, ActiveReleaseTrains} from '@angular/ng-dev';
import {ReleaseConfig} from '@angular/ng-dev';
import {GitClient} from '@angular/ng-dev';
import {AuthenticatedGitClient} from '@angular/ng-dev';
export interface Deployment {
branch: string;
@@ -14,7 +14,7 @@ export interface Deployment {
export type Deployments = Map<string, Deployment>;
export async function getDeployments(): Promise<Deployments> {
const {github} = await GitClient.get();
const {github} = await AuthenticatedGitClient.get();
const releaseTrains = await ActiveReleaseTrains.fetch({
api: github,
name: 'angular',
+5 -1
View File
@@ -2,7 +2,8 @@ import {getInput, setFailed} from '@actions/core';
import {context} from '@actions/github';
import {deployToFirebase, setupRedirect} from './deploy';
import {getDeployments} from './deployments';
import {GithubConfig, setConfig} from '@angular/ng-dev';
import {AuthenticatedGitClient, GithubConfig, setConfig} from '@angular/ng-dev';
import {githubReleaseTrainReadToken} from './credential';
const refMatcher = /refs\/heads\/(.*)/;
@@ -14,6 +15,9 @@ async function deployDocs() {
owner: 'angular',
},
});
AuthenticatedGitClient.configure(githubReleaseTrainReadToken);
if (context.eventName !== 'push') {
throw Error();
}
+5 -1
View File
@@ -11344,6 +11344,9 @@ function getCredentialFilePath() {
}
return credentialFilePath;
}
var githubReleaseTrainReadToken = (0, import_core.getInput)("githubReleaseTrainReadToken", {
required: true
});
//
async function deployToFirebase(deployment, configPath, distDirPath) {
@@ -12977,7 +12980,7 @@ AuthenticatedGitClient._authenticatedInstance = null;
//
async function getDeployments() {
const { github } = await GitClient.get();
const { github } = await AuthenticatedGitClient.get();
const releaseTrains = await ActiveReleaseTrains.fetch({
api: github,
name: "angular",
@@ -13033,6 +13036,7 @@ async function deployDocs() {
owner: "angular"
}
});
AuthenticatedGitClient.configure(githubReleaseTrainReadToken);
if (import_github3.context.eventName !== "push") {
throw Error();
}
+1
View File
@@ -197,5 +197,6 @@ jobs:
uses: ./.github/actions/deploy-docs-site
with:
serviceKey: ${{ secrets.ANGULAR_DEV_SITE_DEPLOY }}
githubReleaseTrainReadToken: ${{ secrets.DOCS_DEPLOY_GITHUB_RELEASE_TRAIN_TOKEN }}
configPath: 'adev/firebase.json'
distDir: 'dist/bin/adev/build/browser'
+1 -1
View File
@@ -93,7 +93,7 @@ jobs:
- name: Run CI tests for framework
run: yarn tsx ./scripts/build/build-packages-dist.mts
- name: Archive build artifacts
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
with:
name: pr-artifacts-${{ github.event.number }}
path: dist/packages-dist/
+2 -2
View File
@@ -39,7 +39,7 @@ jobs:
# Upload the results as artifacts.
- name: 'Upload artifact'
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
with:
name: SARIF file
path: results.sarif
@@ -47,6 +47,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: 'Upload to code-scanning'
uses: github/codeql-action/upload-sarif@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3.25.10
uses: github/codeql-action/upload-sarif@2d790406f505036ef40ecba973cc774a50395aac # v3.25.13
with:
sarif_file: results.sarif
+2 -1
View File
@@ -597,7 +597,8 @@ groups:
conditions:
- >
contains_any_globs(files, [
'adev/**/{.*,*}'
'adev/**/{.*,*}',
'aio/**/{.*,*}'
])
reviewers:
users:
+101 -108
View File
@@ -1,3 +1,104 @@
<a name="18.1.2"></a>
# 18.1.2 (2024-07-24)
### compiler
| Commit | Type | Description |
| -- | -- | -- |
| [463945003d](https://github.com/angular/angular/commit/463945003dcf253c64809ffdcddabedb87e78e06) | fix | limit the number of chained instructions ([#57069](https://github.com/angular/angular/pull/57069)) |
### compiler-cli
| Commit | Type | Description |
| -- | -- | -- |
| [e904f34020](https://github.com/angular/angular/commit/e904f3402053ce9098854a31f2652020dd79e8f9) | fix | add warning for unused let declarations ([#57033](https://github.com/angular/angular/pull/57033)) |
### core
| Commit | Type | Description |
| -- | -- | -- |
| [9e52c1c840](https://github.com/angular/angular/commit/9e52c1c8402dd5a54cf73caafff4a5b81d73fabd) | fix | `afterNextRender` hooks return that callback value. ([#57031](https://github.com/angular/angular/pull/57031)) |
| [b9fb98c67c](https://github.com/angular/angular/commit/b9fb98c67c29d8e5697b72788f09f1263e8130f4) | fix | tree shake dev mode error message ([#57035](https://github.com/angular/angular/pull/57035)) |
<!-- CHANGELOG SPLIT MARKER -->
<a name="18.1.1"></a>
# 18.1.1 (2024-07-17)
### common
| Commit | Type | Description |
| -- | -- | -- |
| [a1cb9dfc0d](https://github.com/angular/angular/commit/a1cb9dfc0d3539d16020a53dd4c32311240a6265) | fix | Don't run preconnect assertion on the server. ([#56213](https://github.com/angular/angular/pull/56213)) |
### compiler
| Commit | Type | Description |
| -- | -- | -- |
| [daf0317bdc](https://github.com/angular/angular/commit/daf0317bdcef79445eead4a3e0e1be75671f43fb) | fix | JIT mode incorrectly interpreting host directive configuration in partial compilation ([#57002](https://github.com/angular/angular/pull/57002)) |
| [d7dca6dbb6](https://github.com/angular/angular/commit/d7dca6dbb6d8afc77a988de0b7471ac4e078762e) | fix | use strict equality for 'code' comparison ([#56944](https://github.com/angular/angular/pull/56944)) |
### compiler-cli
| Commit | Type | Description |
| -- | -- | -- |
| [c94a897248](https://github.com/angular/angular/commit/c94a8972488f62656f5d0368b8763776e2cd39c6) | fix | avoid emitting references to typecheck files in TS 5.4 ([#56961](https://github.com/angular/angular/pull/56961)) |
### core
| Commit | Type | Description |
| -- | -- | -- |
| [5682527d94](https://github.com/angular/angular/commit/5682527d949b078e92ff7588c7e25dad37fffc52) | fix | not all callbacks running when registered at the same time ([#56981](https://github.com/angular/angular/pull/56981)) |
### migrations
| Commit | Type | Description |
| -- | -- | -- |
| [b666d2c20f](https://github.com/angular/angular/commit/b666d2c20f932f435fa6c51e1d74d7bca53381f2) | fix | fix common module removal ([#56968](https://github.com/angular/angular/pull/56968)) |
<!-- CHANGELOG SPLIT MARKER -->
<a name="18.1.0"></a>
# 18.1.0 (2024-07-10)
### common
| Commit | Type | Description |
| -- | -- | -- |
| [f25653e231](https://github.com/angular/angular/commit/f25653e2311152d30b14d25acb0dccb4e2b5ea56) | fix | typo in NgOptimizedImage warning ([#56756](https://github.com/angular/angular/pull/56756)) |
| [9b35726e42](https://github.com/angular/angular/commit/9b35726e42ebdeed138a25581e0a7eefff466206) | fix | typo in warning for NgOptimizedDirective ([#56817](https://github.com/angular/angular/pull/56817)) |
### compiler
| Commit | Type | Description |
| -- | -- | -- |
| [fd6cd0422d](https://github.com/angular/angular/commit/fd6cd0422d2d761d2c6cc0cd41838fbba8a3f010) | feat | Add extended diagnostic to warn when there are uncalled functions in event bindings ([#56295](https://github.com/angular/angular/pull/56295)) |
| [341a116d61](https://github.com/angular/angular/commit/341a116d611c095ed414c82612adb529e7be310c) | fix | allow more characters in let declaration name ([#56764](https://github.com/angular/angular/pull/56764)) |
| [2a1291e942](https://github.com/angular/angular/commit/2a1291e942a3cd645ee635e72e7d83722383d39b) | fix | give precedence to local let declarations over parent ones ([#56752](https://github.com/angular/angular/pull/56752)) |
### compiler-cli
| Commit | Type | Description |
| -- | -- | -- |
| [66e582551e](https://github.com/angular/angular/commit/66e582551eb081e422e0df41badce1821c46dc62) | fix | avoid duplicate diagnostics for let declarations read before definition ([#56843](https://github.com/angular/angular/pull/56843)) |
| [4d18c5bfd5](https://github.com/angular/angular/commit/4d18c5bfd54c53655955c8cd90472081ade40b34) | fix | flag all conflicts between let declarations and local symbols ([#56752](https://github.com/angular/angular/pull/56752)) |
| [9e21582456](https://github.com/angular/angular/commit/9e215824565f0d30da7edb20087c4460069a6660) | fix | Show template syntax errors in local compilation modified ([#55855](https://github.com/angular/angular/pull/55855)) |
| [5996502921](https://github.com/angular/angular/commit/599650292107f8856c7cd41791bd0856f9d14eb1) | fix | type check let declarations nested inside nodes ([#56752](https://github.com/angular/angular/pull/56752)) |
| [cdebf751e4](https://github.com/angular/angular/commit/cdebf751e4949048b01acc92de2517f46fcd5d37) | fix | used before declared diagnostic not firing for control flow blocks ([#56843](https://github.com/angular/angular/pull/56843)) |
### core
| Commit | Type | Description |
| -- | -- | -- |
| [ea3c802056](https://github.com/angular/angular/commit/ea3c80205653af109c688a4d4808143b34591d54) | feat | Add a schematic to migrate afterRender phase flag ([#55648](https://github.com/angular/angular/pull/55648)) |
| [5df3e78c99](https://github.com/angular/angular/commit/5df3e78c9907f522f2f96c087b10ca12d57f7028) | feat | add equality function to rxjs-interop `toSignal` ([#56447](https://github.com/angular/angular/pull/56447)) |
| [0a48d584f2](https://github.com/angular/angular/commit/0a48d584f2ffeebb9402032182d4fc13a260c5cf) | feat | add support for let syntax ([#56715](https://github.com/angular/angular/pull/56715)) |
| [352e0782ec](https://github.com/angular/angular/commit/352e0782ec37d2adcc662cfc69c83d38058a34bf) | feat | expose signal input metadata in `ComponentMirror` ([#56402](https://github.com/angular/angular/pull/56402)) |
| [a655e46447](https://github.com/angular/angular/commit/a655e46447962bf56bf0184e3104328b9f7c1531) | feat | Redesign the `afterRender` & `afterNextRender` phases API ([#55648](https://github.com/angular/angular/pull/55648)) |
| [e5a6f91722](https://github.com/angular/angular/commit/e5a6f917225aafa7c5c860f280d2aafe3615727e) | feat | support TypeScript 5.5 ([#56096](https://github.com/angular/angular/pull/56096)) |
| [38effcc63e](https://github.com/angular/angular/commit/38effcc63eea360e948dc22860add72d3aa02288) | fix | Add back phase flag option as a deprecated API ([#55648](https://github.com/angular/angular/pull/55648)) |
| [86bcfd3e49](https://github.com/angular/angular/commit/86bcfd3e498b8ec1de1a2a1ad0847fe567f7e9d4) | fix | improve docs on afterRender hooks ([#56522](https://github.com/angular/angular/pull/56522)) |
| [b2445a0953](https://github.com/angular/angular/commit/b2445a095314aa66da038d3093e6a1b18fe5768b) | fix | link errors to ADEV ([#55554](https://github.com/angular/angular/pull/55554)) ([#56038](https://github.com/angular/angular/pull/56038)) |
| [03a2acd2a3](https://github.com/angular/angular/commit/03a2acd2a3bdc87aaeb6b835a7c1016f800b31cb) | fix | properly remove imports in the afterRender phase migration ([#56524](https://github.com/angular/angular/pull/56524)) |
| [4d87b9e899](https://github.com/angular/angular/commit/4d87b9e899381894a1de90f251da58613a96eed0) | fix | rename the equality function option in toSignal ([#56769](https://github.com/angular/angular/pull/56769)) ([#56922](https://github.com/angular/angular/pull/56922)) |
| [8bd4c074af](https://github.com/angular/angular/commit/8bd4c074afe85b739dff4d3c4dcc19384c42b85e) | fix | toSignal equal option should be passed to inner computed ([#56903](https://github.com/angular/angular/pull/56903)) |
### forms
| Commit | Type | Description |
| -- | -- | -- |
| [00bde8b1c2](https://github.com/angular/angular/commit/00bde8b1c2d1511da40526a374d4e94d31e0d575) | fix | Make `NgControlStatus` host bindings `OnPush` compatible ([#55720](https://github.com/angular/angular/pull/55720)) |
### http
| Commit | Type | Description |
| -- | -- | -- |
| [cc21989132](https://github.com/angular/angular/commit/cc21989132bc64b981df83cb6ff6e1506b42a1d0) | fix | Make `Content-Type` header case insensitive ([#56541](https://github.com/angular/angular/pull/56541)) |
### language-service
| Commit | Type | Description |
| -- | -- | -- |
| [b400e2e4d4](https://github.com/angular/angular/commit/b400e2e4d4c27a9c8d8e91b52852ef7b64f7591a) | feat | autocompletion for the component not imported ([#55595](https://github.com/angular/angular/pull/55595)) |
| [67b2c336bc](https://github.com/angular/angular/commit/67b2c336bc0bdce3f7ae054c094990a9831f5b20) | fix | import the default exported component correctly ([#56432](https://github.com/angular/angular/pull/56432)) |
### router
| Commit | Type | Description |
| -- | -- | -- |
| [a13f5da773](https://github.com/angular/angular/commit/a13f5da77303f4ab2f1543df1de1f416216b5a9c) | feat | Allow `UrlTree` as an input to `routerLink` ([#56265](https://github.com/angular/angular/pull/56265)) |
| [1d3a7529b4](https://github.com/angular/angular/commit/1d3a7529b4fa3617a5d6a97e742cb13818253a14) | feat | Set a different browser URL from the one for route matching ([#53318](https://github.com/angular/angular/pull/53318)) |
<!-- CHANGELOG SPLIT MARKER -->
<a name="18.0.5"></a>
# 18.0.5 (2024-06-26)
### core
@@ -9,51 +110,6 @@
<!-- CHANGELOG SPLIT MARKER -->
<a name="18.1.0-next.4"></a>
# 18.1.0-next.4 (2024-06-26)
### core
| Commit | Type | Description |
| -- | -- | -- |
| [0a48d584f2](https://github.com/angular/angular/commit/0a48d584f2ffeebb9402032182d4fc13a260c5cf) | feat | add support for let syntax ([#56715](https://github.com/angular/angular/pull/56715)) |
| [29ca6d10cc](https://github.com/angular/angular/commit/29ca6d10cc3cd75ebdf64658dafcb3ce579af343) | fix | improve support for i18n hydration of projected content ([#56192](https://github.com/angular/angular/pull/56192)) |
| [5be16d06bd](https://github.com/angular/angular/commit/5be16d06bd504b1a72b7fb6f8d6e0c1b93a8188b) | fix | prevent calling devMode only function on `@defer` error. ([#56559](https://github.com/angular/angular/pull/56559)) |
### http
| Commit | Type | Description |
| -- | -- | -- |
| [cc21989132](https://github.com/angular/angular/commit/cc21989132bc64b981df83cb6ff6e1506b42a1d0) | fix | Make `Content-Type` header case insensitive ([#56541](https://github.com/angular/angular/pull/56541)) |
<!-- CHANGELOG SPLIT MARKER -->
<a name="18.1.0-next.3"></a>
# 18.1.0-next.3 (2024-06-20)
### compiler-cli
| Commit | Type | Description |
| -- | -- | -- |
| [0b867e83b6](https://github.com/angular/angular/commit/0b867e83b624cbfe449ed3b7daf87642337824f0) | fix | insert constant statements after the first group of imports ([#56431](https://github.com/angular/angular/pull/56431)) |
### core
| Commit | Type | Description |
| -- | -- | -- |
| [5df3e78c99](https://github.com/angular/angular/commit/5df3e78c9907f522f2f96c087b10ca12d57f7028) | feat | add equality function to rxjs-interop `toSignal` ([#56447](https://github.com/angular/angular/pull/56447)) |
| [352e0782ec](https://github.com/angular/angular/commit/352e0782ec37d2adcc662cfc69c83d38058a34bf) | feat | expose signal input metadata in `ComponentMirror` ([#56402](https://github.com/angular/angular/pull/56402)) |
| [bf6df6f186](https://github.com/angular/angular/commit/bf6df6f18658dd0d477271f7eb969317ce1df024) | fix | do not activate event replay when no events are registered ([#56509](https://github.com/angular/angular/pull/56509)) |
| [86bcfd3e49](https://github.com/angular/angular/commit/86bcfd3e498b8ec1de1a2a1ad0847fe567f7e9d4) | fix | improve docs on afterRender hooks ([#56522](https://github.com/angular/angular/pull/56522)) |
| [03a2acd2a3](https://github.com/angular/angular/commit/03a2acd2a3bdc87aaeb6b835a7c1016f800b31cb) | fix | properly remove imports in the afterRender phase migration ([#56524](https://github.com/angular/angular/pull/56524)) |
### forms
| Commit | Type | Description |
| -- | -- | -- |
| [00bde8b1c2](https://github.com/angular/angular/commit/00bde8b1c2d1511da40526a374d4e94d31e0d575) | fix | Make `NgControlStatus` host bindings `OnPush` compatible ([#55720](https://github.com/angular/angular/pull/55720)) |
### language-service
| Commit | Type | Description |
| -- | -- | -- |
| [67b2c336bc](https://github.com/angular/angular/commit/67b2c336bc0bdce3f7ae054c094990a9831f5b20) | fix | import the default exported component correctly ([#56432](https://github.com/angular/angular/pull/56432)) |
### router
| Commit | Type | Description |
| -- | -- | -- |
| [1d3a7529b4](https://github.com/angular/angular/commit/1d3a7529b4fa3617a5d6a97e742cb13818253a14) | feat | Set a different browser URL from the one for route matching ([#53318](https://github.com/angular/angular/pull/53318)) |
| [0d52c6b182](https://github.com/angular/angular/commit/0d52c6b18275433923a5fe94f3fc24323b73d0be) | fix | Delay the view transition to ensure renders in microtasks complete ([#56494](https://github.com/angular/angular/pull/56494)) |
<!-- CHANGELOG SPLIT MARKER -->
<a name="18.0.4"></a>
# 18.0.4 (2024-06-20)
### compiler-cli
@@ -71,39 +127,6 @@
<!-- CHANGELOG SPLIT MARKER -->
<a name="18.1.0-next.2"></a>
# 18.1.0-next.2 (2024-06-12)
### benchpress
| Commit | Type | Description |
| -- | -- | -- |
| [31d9fa40bdb](https://github.com/angular/angular/commit/31d9fa40bdbd1b815a819527312994b6ab1b532f) | fix | adjust supported browser names for headless chrome ([#56360](https://github.com/angular/angular/pull/56360)) |
### core
| Commit | Type | Description |
| -- | -- | -- |
| [ea3c8020565](https://github.com/angular/angular/commit/ea3c80205653af109c688a4d4808143b34591d54) | feat | Add a schematic to migrate afterRender phase flag ([#55648](https://github.com/angular/angular/pull/55648)) |
| [a655e464479](https://github.com/angular/angular/commit/a655e46447962bf56bf0184e3104328b9f7c1531) | feat | Redesign the `afterRender` & `afterNextRender` phases API ([#55648](https://github.com/angular/angular/pull/55648)) |
| [38effcc63ee](https://github.com/angular/angular/commit/38effcc63eea360e948dc22860add72d3aa02288) | fix | Add back phase flag option as a deprecated API ([#55648](https://github.com/angular/angular/pull/55648)) |
| [d5c6ee432fc](https://github.com/angular/angular/commit/d5c6ee432fcd467c09b4d5d5366e731f5c91e8d4) | fix | async EventEmitter should contribute to app stability ([#56308](https://github.com/angular/angular/pull/56308)) |
| [4c7d5d8acd8](https://github.com/angular/angular/commit/4c7d5d8acd8a714fe89366f76dc69f91356f0a06) | fix | signals should be tracked when embeddedViewRef.detectChanges is called ([#55719](https://github.com/angular/angular/pull/55719)) |
### language-service
| Commit | Type | Description |
| -- | -- | -- |
| [b400e2e4d4c](https://github.com/angular/angular/commit/b400e2e4d4c27a9c8d8e91b52852ef7b64f7591a) | feat | autocompletion for the component not imported ([#55595](https://github.com/angular/angular/pull/55595)) |
### localize
| Commit | Type | Description |
| -- | -- | -- |
| [81486c2f477](https://github.com/angular/angular/commit/81486c2f4770361b4aecdc891107f70446e43bc3) | fix | add `@angular/localize/init` as polyfill in `angular.json` ([#56300](https://github.com/angular/angular/pull/56300)) |
### migrations
| Commit | Type | Description |
| -- | -- | -- |
| [96aa5c8a2c9](https://github.com/angular/angular/commit/96aa5c8a2c9dd607dbcffa5d7923df4c21636510) | fix | resolve error in standalone migration ([#56302](https://github.com/angular/angular/pull/56302)) |
### router
| Commit | Type | Description |
| -- | -- | -- |
| [a13f5da7730](https://github.com/angular/angular/commit/a13f5da77303f4ab2f1543df1de1f416216b5a9c) | feat | Allow `UrlTree` as an input to `routerLink` ([#56265](https://github.com/angular/angular/pull/56265)) |
<!-- CHANGELOG SPLIT MARKER -->
<a name="18.0.3"></a>
# 18.0.3 (2024-06-12)
### benchpress
@@ -126,26 +149,6 @@
<!-- CHANGELOG SPLIT MARKER -->
<a name="18.1.0-next.1"></a>
# 18.1.0-next.1 (2024-06-05)
### compiler-cli
| Commit | Type | Description |
| -- | -- | -- |
| [9e21582456](https://github.com/angular/angular/commit/9e215824565f0d30da7edb20087c4460069a6660) | fix | Show template syntax errors in local compilation modified ([#55855](https://github.com/angular/angular/pull/55855)) |
### core
| Commit | Type | Description |
| -- | -- | -- |
| [8d75627ddd](https://github.com/angular/angular/commit/8d75627dddb82380dc8b3455a13f128e88309aa6) | fix | Do not migrate `HttpClientModule` imports on components. ([#56067](https://github.com/angular/angular/pull/56067)) |
| [1081c8d623](https://github.com/angular/angular/commit/1081c8d6233ba1ff09187b95a09b0644e130cdf8) | fix | don't coerce all producers to consumers on liveness change ([#56140](https://github.com/angular/angular/pull/56140)) |
| [b6fb53cdc7](https://github.com/angular/angular/commit/b6fb53cdc7c9f9dad5a113a68e72a2b723626bef) | fix | Fix shouldPreventDefaultBeforeDispatching bug ([#56188](https://github.com/angular/angular/pull/56188)) |
| [31f3975e4b](https://github.com/angular/angular/commit/31f3975e4b1f418c1a6b2516618f44093ba20175) | fix | handle missing `withI18nSupport()` call for components that use i18n blocks ([#56175](https://github.com/angular/angular/pull/56175)) |
### migrations
| Commit | Type | Description |
| -- | -- | -- |
| [75e811c18b](https://github.com/angular/angular/commit/75e811c18b16f42c6f72ac32e44c4bc4d37fc3b1) | fix | do not generate aliased variables with the same name ([#56154](https://github.com/angular/angular/pull/56154)) |
<!-- CHANGELOG SPLIT MARKER -->
<a name="18.0.2"></a>
# 18.0.2 (2024-06-05)
### core
@@ -162,16 +165,6 @@
<!-- CHANGELOG SPLIT MARKER -->
<a name="18.1.0-next.0"></a>
# 18.1.0-next.0 (2024-05-29)
### core
| Commit | Type | Description |
| -- | -- | -- |
| [e5a6f91722](https://github.com/angular/angular/commit/e5a6f917225aafa7c5c860f280d2aafe3615727e) | feat | support TypeScript 5.5 ([#56096](https://github.com/angular/angular/pull/56096)) |
| [b2445a0953](https://github.com/angular/angular/commit/b2445a095314aa66da038d3093e6a1b18fe5768b) | fix | link errors to ADEV ([#55554](https://github.com/angular/angular/pull/55554)) ([#56038](https://github.com/angular/angular/pull/56038)) |
<!-- CHANGELOG SPLIT MARKER -->
<a name="18.0.1"></a>
# 18.0.1 (2024-05-29)
### compiler
+2 -2
View File
@@ -353,9 +353,9 @@ DEPRECATED: <what is deprecated>
Closes #<pr number>
```
Breaking Change section should start with the phrase "BREAKING CHANGE: " followed by a summary of the breaking change, a blank line, and a detailed description of the breaking change that also includes migration instructions.
Breaking Change section should start with the phrase `BREAKING CHANGE: ` followed by a summary of the breaking change, a blank line, and a detailed description of the breaking change that also includes migration instructions.
Similarly, a Deprecation section should start with "DEPRECATED: " followed by a short description of what is deprecated, a blank line, and a detailed description of the deprecation that also mentions the recommended update path.
Similarly, a Deprecation section should start with `DEPRECATED: ` followed by a short description of what is deprecated, a blank line, and a detailed description of the deprecation that also mentions the recommended update path.
### Revert commits
+4 -4
View File
@@ -75,7 +75,7 @@ yarn_install(
"//:.yarnrc",
"//:tools/npm-patches/@bazel+jasmine+5.8.1.patch",
"//tools:postinstall-patches.js",
"//tools/esm-interop:patches/npm/@angular+build-tooling+0.0.0-239d56b71911f9fa1eeefb6e4505dbe7b0cd81a7.patch",
"//tools/esm-interop:patches/npm/@angular+build-tooling+0.0.0-d30a56c19bafaac67cf44e605ed8c2c0e45b0a51.patch",
"//tools/esm-interop:patches/npm/@bazel+concatjs+5.8.1.patch",
"//tools/esm-interop:patches/npm/@bazel+esbuild+5.7.1.patch",
"//tools/esm-interop:patches/npm/@bazel+protractor+5.7.1.patch",
@@ -143,10 +143,10 @@ cldr_xml_data_repository(
# sass rules
http_archive(
name = "io_bazel_rules_sass",
sha256 = "47c50aa960ddf875a2a2dd7efd1c839e5f8598725c00014680122b0e48d0ec7f",
strip_prefix = "rules_sass-b222c61b3d3879ec45b66062b2c706a72f3d80bb",
sha256 = "cd83736ea65d0df064283aea5922dbaf132dd2b3aa54e7151aae7edaa9572c3e",
strip_prefix = "rules_sass-83022b98114c07e9588089c7fe8f76bc0262c7e7",
urls = [
"https://github.com/bazelbuild/rules_sass/archive/b222c61b3d3879ec45b66062b2c706a72f3d80bb.zip",
"https://github.com/bazelbuild/rules_sass/archive/83022b98114c07e9588089c7fe8f76bc0262c7e7.zip",
],
)
+2
View File
@@ -26,6 +26,7 @@ APPLICATION_FILES = [
TEST_FILES = APPLICATION_FILES + [
"karma.conf.js",
"test-main.ts",
"tsconfig.spec.json",
] + glob(
["**/*.spec.ts"],
@@ -78,6 +79,7 @@ APPLICATION_DEPS = [
"@npm//@lezer/highlight",
"@npm//@lezer/javascript",
"@npm//@lezer/common",
"@npm//@stackblitz/sdk",
"@npm//@xterm/xterm",
"@npm//@xterm/addon-fit",
"@npm//algoliasearch",
+16
View File
@@ -24,6 +24,8 @@ yarn
yarn docs
```
If you are having issues with the docs building, see the [FAQs](#faqs) section.
## Contributing
Want to report a bug, contribute some code, or improve the documentation? Excellent!
@@ -35,3 +37,17 @@ And if you're new, check out one of our issues labeled as <kbd>[help wanted](htt
### Code of Conduct
Help us keep Angular open and inclusive. Please read and follow our [Code of Conduct](CODE_OF_CONDUCT.md).
## FAQs
### The build is failing and I'm seeing `bazel:bazel failed: missing input file` messages.
This is most likely due to a bazel dependency / caching issue. To resolve this, run the following command:
```
# Try this first
yarn bazel clean
# If that doesn't work, try it with the expunge flag
yarn bazel clean --expunge
```
+1 -1
View File
@@ -84,10 +84,10 @@
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": ["zone.js", "zone.js/testing"],
"tsConfig": "tsconfig.spec.json",
"include": ["src/app"],
"karmaConfig": "karma.conf.js",
"main": "test-main.ts",
"inlineStyleLanguage": "scss",
"assets": ["src/favicon.ico", "src/assets"],
"styles": ["@angular/docs/styles/global-styles.scss"],
+1 -1
View File
@@ -5,7 +5,7 @@
"ignore": ["**/.*"],
"headers": [
{
"source": "assets/tutorials/common/*.jpg",
"source": "assets/images/tutorials/common/*.jpg",
"headers": [
{
"key": "Cross-Origin-Resource-Policy",
+2
View File
@@ -32,6 +32,7 @@ import {
TitleStrategy,
createUrlTreeFromSnapshot,
provideRouter,
withComponentInputBinding,
withInMemoryScrolling,
withViewTransitions,
} from '@angular/router';
@@ -70,6 +71,7 @@ export const appConfig: ApplicationConfig = {
}
},
}),
withComponentInputBinding(),
),
provideExperimentalZonelessChangeDetection(),
provideClientHydration(),
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {ComponentFixture, TestBed, fakeAsync, tick} from '@angular/core/testing';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {PROGRESS_BAR_DELAY, ProgressBarComponent} from './progress-bar.component';
import {RouterTestingHarness, RouterTestingModule} from '@angular/router/testing';
@@ -25,13 +25,13 @@ describe('ProgressBarComponent', () => {
fixture.detectChanges();
});
it('should call progressBar.complete() on route change', fakeAsync(async () => {
it('should call progressBar.complete() on route change', async () => {
const progressBarCompleteSpy = spyOn(component.progressBar, 'complete');
const harness = await RouterTestingHarness.create();
await harness.navigateByUrl('/');
tick(PROGRESS_BAR_DELAY);
await new Promise((resolve) => setTimeout(resolve, PROGRESS_BAR_DELAY));
expect(progressBarCompleteSpy).toHaveBeenCalled();
}));
});
});
@@ -1,22 +1,19 @@
@use '@angular/docs/styles/media-queries' as mq;
:host {
z-index: 100;
@include mq.for-tablet-landscape-up {
position: sticky;
top: 0;
// Render secondary nav below primary nav
z-index: 99;
}
@include mq.for-tablet-landscape-down {
position: fixed;
// Render secondary nav above mobile menu
z-index: 100;
}
@include mq.for-phone-only {
transform: translateX(0);
z-index: 99;
transition: transform 0.3s ease;
&:has(.docs-nav-secondary--open) {
transform: translateX(82px);
@@ -40,7 +37,10 @@
transition: transform 0.45s ease;
@media (prefers-reduced-motion: no-preference) {
transition: transform 0.45s ease, background-color 0.3s ease, border-color 0.3s ease;
transition:
transform 0.45s ease,
background-color 0.3s ease,
border-color 0.3s ease;
}
@include mq.for-tablet-landscape-down {
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {ChangeDetectionStrategy, Component, Inject} from '@angular/core';
import {ChangeDetectionStrategy, Component, Inject, inject} from '@angular/core';
import {MAT_SNACK_BAR_DATA, MatSnackBarAction, MatSnackBarRef} from '@angular/material/snack-bar';
export interface ErrorSnackBarData {
@@ -37,10 +37,8 @@ export class ErrorSnackBar {
protected message: string;
protected actionText?: string;
constructor(
protected snackBarRef: MatSnackBarRef<ErrorSnackBar>,
@Inject(MAT_SNACK_BAR_DATA) public data: ErrorSnackBarData,
) {
constructor(protected snackBarRef: MatSnackBarRef<ErrorSnackBar>) {
const data = inject(MAT_SNACK_BAR_DATA) as ErrorSnackBarData;
this.message = data.message;
this.actionText = data.actionText;
}
@@ -68,7 +68,7 @@ export class VersionManager {
// version: 'rc',
// },
{
url: this.getAdevDocsUrl(this.currentMajorVersion),
url: 'https://angular.dev/',
displayName: this.getVersion(this.currentMajorVersion),
version: this.currentVersionMode,
},
@@ -65,6 +65,14 @@
</button>
</div>
<button
class="adev-editor-download-button"
type="button"
(click)="openCurrentCodeInStackBlitz()"
aria-label="Open current code in editor in StackBlitz"
>
<docs-icon>launch</docs-icon>
</button>
<button
class="adev-editor-download-button"
type="button"
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {NgFor, NgIf} from '@angular/common';
import {Location, NgFor, NgIf} from '@angular/common';
import {
AfterViewInit,
ChangeDetectionStrategy,
@@ -20,6 +20,7 @@ import {
} from '@angular/core';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {MatTabGroup, MatTabsModule} from '@angular/material/tabs';
import {Title} from '@angular/platform-browser';
import {debounceTime, map} from 'rxjs';
import {TerminalType} from '../terminal/terminal-handler.service';
@@ -28,6 +29,7 @@ import {EmbeddedTutorialManager} from '../embedded-tutorial-manager.service';
import {CodeMirrorEditor} from './code-mirror-editor.service';
import {DiagnosticWithLocation, DiagnosticsState} from './services/diagnostics-state.service';
import {DownloadManager} from '../download-manager.service';
import {StackBlitzOpener} from '../stackblitz-opener.service';
import {ClickOutside, IconComponent} from '@angular/docs';
export const REQUIRED_FILES = new Set([
@@ -36,6 +38,8 @@ export const REQUIRED_FILES = new Set([
'src/app/app.component.ts',
]);
const ANGULAR_DEV = 'https://angular.dev';
@Component({
selector: 'docs-tutorial-code-editor',
standalone: true,
@@ -73,6 +77,9 @@ export class CodeEditor implements AfterViewInit, OnDestroy {
private readonly codeMirrorEditor = inject(CodeMirrorEditor);
private readonly diagnosticsState = inject(DiagnosticsState);
private readonly downloadManager = inject(DownloadManager);
private readonly stackblitzOpener = inject(StackBlitzOpener);
private readonly title = inject(Title);
private readonly location = inject(Location);
private readonly embeddedTutorialManager = inject(EmbeddedTutorialManager);
private readonly errors$ = this.diagnosticsState.diagnostics$.pipe(
@@ -110,6 +117,16 @@ export class CodeEditor implements AfterViewInit, OnDestroy {
this.codeMirrorEditor.disable();
}
async openCurrentCodeInStackBlitz(): Promise<void> {
const title = this.title.getTitle();
const path = this.location.path();
const editorUrl = `${ANGULAR_DEV}${path}`;
const description = `Angular.dev example generated from [${editorUrl}](${editorUrl})`;
await this.stackblitzOpener.openCurrentSolutionInStackBlitz({title, description});
}
async downloadCurrentCodeEditorState(): Promise<void> {
const name = this.embeddedTutorialManager.tutorialId();
await this.downloadManager.downloadCurrentStateOfTheSolution(name);
@@ -7,7 +7,7 @@
*/
import {signal} from '@angular/core';
import {TestBed, fakeAsync, tick} from '@angular/core/testing';
import {TestBed} from '@angular/core/testing';
import {BehaviorSubject, Subject} from 'rxjs';
import {EditorState} from '@codemirror/state';
import type {FileSystemTree} from '@webcontainer/api';
@@ -142,17 +142,19 @@ describe('CodeMirrorEditor', () => {
expect(service.currentFile().content).toBe(newContent);
});
it('should write the changed file content to the sandbox filesystem', fakeAsync(() => {
it('should write the changed file content to the sandbox filesystem', () => {
jasmine.clock().install();
jasmine.clock().mockDate();
const newContent = 'new content';
const nodeRuntimeSandboxSpy = spyOn(fakeNodeRuntimeSandbox, 'writeFile');
dispatchDocumentChange(newContent);
tick(EDITOR_CONTENT_CHANGE_DELAY_MILLIES);
jasmine.clock().tick(EDITOR_CONTENT_CHANGE_DELAY_MILLIES);
expect(nodeRuntimeSandboxSpy).toHaveBeenCalledWith(service.currentFile().filename, newContent);
}));
jasmine.clock().uninstall();
});
it('should add created file to code editor', async () => {
const newFile = 'new-component.component.ts';
@@ -107,7 +107,10 @@ export class NodeRuntimeSandbox {
console.timeEnd('Load time');
} catch (error: any) {
this.setErrorState(error.message);
// If we're already in an error state, throw away the most recent error which may have happened because
// we were in the error state already and tried to do more things after terminating.
const message = this.nodeRuntimeState.error()?.message ?? error.message;
this.setErrorState(message);
}
}
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {TestBed, fakeAsync, tick} from '@angular/core/testing';
import {TestBed} from '@angular/core/testing';
import {DebugElement, signal} from '@angular/core';
import {By} from '@angular/platform-browser';
import {of} from 'rxjs';
@@ -64,16 +64,16 @@ describe('Preview', () => {
};
};
it('should set iframe src on init', fakeAsync(() => {
it('should set iframe src on init', async () => {
const {component, PREVIEW_URL} = beforeEach();
component.ngAfterViewInit();
tick(100);
await new Promise((resolve) => setTimeout(resolve, 100));
expect(component.previewIframe?.nativeElement).toBeTruthy();
expect(component.previewIframe?.nativeElement?.src).toBe(PREVIEW_URL);
}));
});
it('should not render loading elements if the loadingStep is READY or ERROR', () => {
const {fixture, fakeNodeRuntimeState, getLoadingElementsWrapper} = beforeEach();
@@ -0,0 +1,24 @@
/*!
* @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.dev/license
*/
import {TestBed} from '@angular/core/testing';
import {StackBlitzOpener} from './stackblitz-opener.service';
describe('StackBlitzOpener', () => {
let service: StackBlitzOpener;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(StackBlitzOpener);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
@@ -0,0 +1,46 @@
/*!
* @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.dev/license
*/
import {EnvironmentInjector, Injectable, inject} from '@angular/core';
import sdk, {Project, ProjectFiles} from '@stackblitz/sdk';
import {injectAsync} from '../core/services/inject-async';
@Injectable({
providedIn: 'root',
})
export class StackBlitzOpener {
private readonly environmentInjector = inject(EnvironmentInjector);
/**
* Generate a StackBlitz project from the current state of the solution in the EmbeddedEditor
*/
async openCurrentSolutionInStackBlitz(
projectMetadata: Pick<Project, 'title' | 'description'>,
): Promise<void> {
const nodeRuntimeSandbox = await injectAsync(this.environmentInjector, () =>
import('./node-runtime-sandbox.service').then((c) => c.NodeRuntimeSandbox),
);
const runtimeFiles = await nodeRuntimeSandbox.getSolutionFiles();
const stackblitzProjectFiles: ProjectFiles = {};
runtimeFiles.forEach((file) => {
// Leading slashes are incompatible with StackBlitz SDK they are removed
const path = file.path.replace(/^\//, '');
stackblitzProjectFiles[path] =
typeof file.content !== 'string' ? new TextDecoder().decode(file.content) : file.content;
});
sdk.openProject({
...projectMetadata,
template: 'node',
files: stackblitzProjectFiles,
});
}
}
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {ComponentFixture, TestBed, fakeAsync, tick} from '@angular/core/testing';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {Terminal} from './terminal.component';
@@ -61,12 +61,12 @@ describe('Terminal', () => {
);
});
it('should call resizeToFitParent on window resize', fakeAsync(() => {
it('should call resizeToFitParent on window resize', async () => {
fakeWindow.dispatchEvent(new Event('resize'));
// debounce time
tick(50);
await new Promise((resolve) => setTimeout(resolve, 50));
expect(terminalHandlerSpy.resizeToFitParent).toHaveBeenCalled();
}));
});
});
@@ -70,7 +70,6 @@
.docs-code {
pre {
margin-block: 0;
padding-block: 1rem;
}
}
@@ -120,13 +119,13 @@
transition: background-color 0.3s ease;
font-family: monospace;
&.hljs-ln-line {
&.line {
font-weight: 400;
text-align: left;
padding-block: 0.25rem;
}
&.hljs-ln-line-highlighted {
&.shiki-ln-line-highlighted {
background-color: var(--senary-contrast);
}
&:hover {
@@ -8,7 +8,7 @@
import {HarnessLoader} from '@angular/cdk/testing';
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
import {TestBed, waitForAsync} from '@angular/core/testing';
import {TestBed} from '@angular/core/testing';
import {MatTabGroupHarness} from '@angular/material/tabs/testing';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {ReferenceScrollHandler} from '../services/reference-scroll-handler.service';
@@ -68,15 +68,15 @@ describe('ApiReferenceDetailsPage', () => {
expect(component).toBeTruthy();
});
it('should render tabs for all elements with tab attribute', waitForAsync(async () => {
it('should render tabs for all elements with tab attribute', async () => {
const matTabGroup = await loader.getHarness(MatTabGroupHarness);
const tabs = await matTabGroup.getTabs();
expect(tabs.length).toBe(4);
}));
});
it('should display members cards when API tab is active', waitForAsync(async () => {
it('should display members cards when API tab is active', async () => {
const matTabGroup = await loader.getHarness(MatTabGroupHarness);
const tabs = await matTabGroup.getTabs();
@@ -94,7 +94,7 @@ describe('ApiReferenceDetailsPage', () => {
membersCard = harness.fixture.debugElement.query(By.css('.docs-reference-members-container'));
expect(membersCard).toBeTruthy();
}));
});
it('should setup scroll listeners when API members are loaded', () => {
const setupListenersSpy = spyOn(fakeApiReferenceScrollHandler, 'setupListeners');
@@ -1,30 +1,13 @@
<div class="adev-reference-list-page">
<header>
<h6>Getting Started</h6>
<h1 tabindex="-1">API Reference</h1>
<h1>API Reference</h1>
</header>
@if (featuredGroup().items.length) {
<adev-api-items-section [group]="featuredGroup()" class="adev-featured-list" />
<adev-api-items-section [group]="featuredGroup()" class="adev-featured-list" />
}
<form class="adev-reference-list-form">
<docs-text-field
name="query"
placeholder="Filter"
[ngModel]="query()"
(ngModelChange)="query.set($event)"
/>
<div class="adev-reference-list-form-part-two">
<docs-slide-toggle
buttonId="includeDeprecated"
label="Show @deprecated"
name="includeDeprecated"
[ngModel]="includeDeprecated()"
(ngModelChange)="includeDeprecated.set($event)"
/>
</div>
<ul class="adev-reference-list-legend">
@for (itemType of itemTypes; track itemType) {
<li
@@ -37,10 +20,24 @@
</li>
}
</ul>
<docs-text-field name="query" placeholder="Filter" [(ngModel)]="query" />
<div class="adev-reference-list-form-part-two">
<docs-slide-toggle
buttonId="includeDeprecated"
label="Show @deprecated"
name="includeDeprecated"
[(ngModel)]="includeDeprecated"
/>
</div>
@for (group of filteredGroups(); track group.id) {
<adev-api-items-section [group]="group" />
} @empty {
<div class="adev-reference-list-empty">
<p>No API items found.</p>
</div>
}
</form>
@for (group of filteredGroups(); track group.id) {
<adev-api-items-section [group]="group" />
}
</div>
@@ -25,7 +25,6 @@
.adev-reference-list-legend {
display: grid;
grid-template-columns: repeat(6, 1fr);
padding-block-end: 0.5rem;
margin-block-start: 0;
padding-inline: 0;
cursor: pointer;
@@ -57,7 +56,10 @@
border-radius: 0.25rem;
margin-inline-end: 0.5rem;
margin-block-end: 0.5rem;
transition: color 0.3s ease, background 0.3s ease, border 0.3s ease;
transition:
color 0.3s ease,
background 0.3s ease,
border 0.3s ease;
&:hover {
color: var(--primary-contrast);
@@ -74,8 +76,8 @@
.adev-reference-list-form {
display: flex;
justify-content: flex-start;
flex-wrap: wrap-reverse;
justify-content: space-between;
flex-wrap: wrap;
gap: 1.5rem;
padding-block-start: 1.5rem;
padding-block-end: 2rem;
@@ -88,6 +90,13 @@
flex-wrap: wrap;
}
.adev-reference-list-empty {
text-align: center;
p {
font-size: 1rem;
}
}
.adev-featured-list {
display: block;
padding-block-end: 1rem;
@@ -9,10 +9,11 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import ApiReferenceList, {ALL_STATUSES_KEY} from './api-reference-list.component';
import {RouterTestingModule} from '@angular/router/testing';
import {ApiReferenceManager} from './api-reference-manager.service';
import {signal} from '@angular/core';
import {ApiItemType} from '../interfaces/api-item-type';
import {RouterTestingHarness} from '@angular/router/testing';
import {provideRouter} from '@angular/router';
describe('ApiReferenceList', () => {
let component: ApiReferenceList;
@@ -52,8 +53,11 @@ describe('ApiReferenceList', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ApiReferenceList, RouterTestingModule],
providers: [{provide: ApiReferenceManager, useValue: fakeApiReferenceManager}],
imports: [ApiReferenceList],
providers: [
{provide: ApiReferenceManager, useValue: fakeApiReferenceManager},
provideRouter([{path: 'api', component: ApiReferenceList}]),
],
});
fixture = TestBed.createComponent(ApiReferenceList);
component = fixture.componentInstance;
@@ -90,25 +94,27 @@ describe('ApiReferenceList', () => {
});
it('should display only class items when user selects Class in the Type select', () => {
component.type.set(ApiItemType.CLASS);
fixture.componentInstance.type.set(ApiItemType.CLASS);
fixture.detectChanges();
expect(component.type()).toEqual(ApiItemType.CLASS);
expect(component.filteredGroups()![0].items).toEqual([fakeItem2]);
});
it('should set selected type when provided type is different than selected', () => {
it('should set selected type when provided type is different than selected', async () => {
expect(component.type()).toBe(ALL_STATUSES_KEY);
component.filterByItemType(ApiItemType.BLOCK);
await RouterTestingHarness.create(`/api?type=${ApiItemType.BLOCK}`);
expect(component.type()).toBe(ApiItemType.BLOCK);
});
it('should reset selected type when provided type is equal to selected', () => {
it('should reset selected type when provided type is equal to selected', async () => {
component.filterByItemType(ApiItemType.BLOCK);
const harness = await RouterTestingHarness.create(`/api?type=${ApiItemType.BLOCK}`);
expect(component.type()).toBe(ApiItemType.BLOCK);
component.filterByItemType(ApiItemType.BLOCK);
harness.navigateByUrl(`/api`);
expect(component.type()).toBe(ALL_STATUSES_KEY);
});
});
@@ -6,7 +6,17 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {ChangeDetectionStrategy, Component, computed, inject, signal} from '@angular/core';
import {
ChangeDetectionStrategy,
Component,
computed,
effect,
ElementRef,
inject,
model,
signal,
viewChild,
} from '@angular/core';
import ApiItemsSection from '../api-items-section/api-items-section.component';
import {FormsModule} from '@angular/forms';
import {SlideToggle, TextField} from '@angular/docs';
@@ -38,12 +48,22 @@ export const ALL_STATUSES_KEY = 'All';
})
export default class ApiReferenceList {
private readonly apiReferenceManager = inject(ApiReferenceManager);
filterInput = viewChild.required(TextField, {read: ElementRef});
private readonly allGroups = this.apiReferenceManager.apiGroups;
private filterEffect = effect(() => {
if (matchMedia('(hover: hover) and (pointer:fine)').matches) {
// Lord forgive me for I have sinned
// Use the CVA to focus when https://github.com/angular/angular/issues/31133 is implemented
this.filterInput().nativeElement.querySelector('input').focus();
}
});
query = signal('');
includeDeprecated = signal(false);
type = signal(ALL_STATUSES_KEY);
type = model<string | undefined>(ALL_STATUSES_KEY);
featuredGroup = this.apiReferenceManager.featuredGroup;
filteredGroups = computed((): ApiItemsGroup[] => {
@@ -58,7 +78,9 @@ export default class ApiReferenceList {
? apiItem.title.toLocaleLowerCase().includes(this.query().toLocaleLowerCase())
: true) &&
(this.includeDeprecated() ? true : apiItem.isDeprecated === this.includeDeprecated()) &&
(this.type() === ALL_STATUSES_KEY || apiItem.itemType === this.type())
(this.type() === undefined ||
this.type() === ALL_STATUSES_KEY ||
apiItem.itemType === this.type())
);
}),
}))
@@ -67,6 +89,6 @@ export default class ApiReferenceList {
itemTypes = Object.values(ApiItemType);
filterByItemType(itemType: ApiItemType): void {
this.type.set(this.type() === itemType ? ALL_STATUSES_KEY : itemType);
this.type.update((currentType) => (currentType === itemType ? ALL_STATUSES_KEY : itemType));
}
}
@@ -81,7 +81,7 @@
z-index: 0;
}
&:has(.hljs-ln-line-highlighted) {
&:has(.shiki-ln-line-highlighted) {
&::before {
background: var(--red-to-pink-to-purple-horizontal-gradient);
}
@@ -95,8 +95,8 @@
}
}
.hljs-ln-line-argument,
.hljs-ln-line-option {
.shiki-ln-line-argument,
.shiki-ln-line-option {
padding: 0.1rem 0.2rem 0.2rem;
margin-inline: 0.1rem;
color: var(--quaternary-contrast);
@@ -110,12 +110,12 @@
background: var(--septenary-contrast);
}
&.hljs-ln-line-highlighted {
&.shiki-ln-line-highlighted {
color: var(--primary-contrast);
background: var(--senary-contrast);
}
}
.hljs-ln-line-argument {
.shiki-ln-line-argument {
margin-inline-start: 0.2rem;
}
}
@@ -16,6 +16,6 @@ export const API_REFERENCE_TAB_API_LABEL = 'API';
export const API_REFERENCE_TAB_QUERY_PARAM = 'tab';
export const API_TAB_CLASS_NAME = '.docs-reference-api-tab';
export const API_REFERENCE_MEMBER_CARD_CLASS_NAME = '.docs-reference-member-card';
export const API_TAB_ACTIVE_CODE_LINE = 'hljs-ln-line-highlighted';
export const HIGHLIGHT_JS_CODE_LINE_CLASS_NAME = 'hljs-ln-line';
export const API_TAB_ACTIVE_CODE_LINE = 'shiki-ln-line-highlighted';
export const HIGHLIGHT_JS_CODE_LINE_CLASS_NAME = 'shiki-ln-line';
export const MEMBER_ID_ATTRIBUTE = 'member-id';
@@ -35,6 +35,7 @@ export function mapApiManifestToRoutes(): Route[] {
),
},
data: {
label: api.name,
displaySecondaryNav: true,
},
});
@@ -18,7 +18,7 @@ import {
} from '@angular/core';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {fromEvent} from 'rxjs';
import {auditTime} from 'rxjs/operators';
import {auditTime, skipWhile} from 'rxjs/operators';
import {
API_REFERENCE_DETAILS_PAGE_MEMBERS_CLASS_NAME,
API_REFERENCE_MEMBER_CARD_CLASS_NAME,
@@ -114,7 +114,10 @@ export class ReferenceScrollHandler implements OnDestroy, ReferenceScrollHandler
this.getAllMemberCards().forEach((card) => {
this.cardOffsetTop.set(card.id, card.offsetTop);
fromEvent(card, 'click')
.pipe(takeUntilDestroyed(this.destroyRef))
.pipe(
skipWhile((event) => event.target instanceof HTMLAnchorElement),
takeUntilDestroyed(this.destroyRef),
)
.subscribe(() => {
this.router.navigate([], {fragment: card.id, replaceUrl: true});
});
+15
View File
@@ -31,6 +31,11 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
path: 'overview',
contentPath: 'introduction/what-is-angular',
},
{
label: 'Installation',
path: 'installation',
contentPath: 'introduction/installation',
},
{
label: 'Essentials',
children: [
@@ -228,6 +233,11 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
path: 'guide/templates/control-flow',
contentPath: 'guide/templates/control-flow',
},
{
label: 'Local template variables with @let',
path: 'guide/templates/let-template-variables',
contentPath: 'guide/templates/let-template-variables',
},
{
label: 'Pipes',
children: [
@@ -473,6 +483,11 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
{
label: 'Performance',
children: [
{
label: 'Overview',
path: 'guide/performance',
contentPath: 'guide/performance/overview',
},
{
label: 'Deferrable views',
path: 'guide/defer',
+2 -1
View File
@@ -20,6 +20,7 @@ copy_to_directory(
"//adev/src/content/guide/http",
"//adev/src/content/guide/i18n",
"//adev/src/content/guide/ngmodules",
"//adev/src/content/guide/performance",
"//adev/src/content/guide/pipes",
"//adev/src/content/guide/routing",
"//adev/src/content/guide/signals",
@@ -54,7 +55,7 @@ copy_to_directory(
"//packages/elements:elements_docs",
"//packages/forms:forms_docs",
"//packages/localize:localize_docs",
"//packages/localize/init:localize_docs",
"//packages/localize/src/localize:localize_init_docs",
"//packages/platform-browser:platform-browser_docs",
"//packages/platform-browser-dynamic:platform-browser_dynamic_docs",
"//packages/platform-browser-dynamic/testing:platform-browser_dynamic_testing_docs",
Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

+1 -1
View File
@@ -127,7 +127,7 @@ The `RouterLinkActive` directive provides the `ariaCurrentWhenActive` input whic
The following example shows how to apply the `active-page` class to active links as well as setting their `aria-current` attribute to `"page"` when they are active:
```html
```angular-html
<nav>
<a routerLink="home"
routerLinkActive="active-page"
@@ -48,7 +48,7 @@ src
<docs-code language="shell">
ng run my-app:app-shell:development
ng build --configuration=development
</docs-code>
@@ -56,7 +56,7 @@ Or to use the production configuration.
<docs-code language="shell">
ng run my-app:app-shell:production
ng build
</docs-code>
@@ -69,7 +69,7 @@ In addition, using the `onActionClick` property on the `data` object, you can ti
{"action": "foo", "title": "Open new tab"},
{"action": "bar", "title": "Focus last"},
{"action": "baz", "title": "Navigate last"},
{"action": "qux", "title": "Send request in the background"}
{"action": "qux", "title": "Send request in the background"},
{"action": "other", "title": "Just notify existing clients"}
],
"data": {
@@ -15,7 +15,6 @@ describe('BannerComponent (AutoChangeDetect)', () => {
beforeEach(() => {
// #docregion auto-detect
TestBed.configureTestingModule({
imports: [BannerComponent],
providers: [{provide: ComponentFixtureAutoDetect, useValue: true}],
});
// #enddocregion auto-detect
@@ -30,15 +29,18 @@ describe('BannerComponent (AutoChangeDetect)', () => {
expect(h1.textContent).toContain(comp.title);
});
it('should still see original title after comp.title change', () => {
it('should still see original title after comp.title change', async () => {
const oldTitle = comp.title;
comp.title = 'Test Title';
// Displayed title is old because Angular didn't hear the change :(
const newTitle = 'Test Title';
comp.title.set(newTitle);
// Displayed title is old because Angular didn't yet run change detection
expect(h1.textContent).toContain(oldTitle);
await fixture.whenStable();
expect(h1.textContent).toContain(newTitle);
});
it('should display updated title after detectChanges', () => {
comp.title = 'Test Title';
comp.title.set('Test Title');
fixture.detectChanges(); // detect changes explicitly
expect(h1.textContent).toContain(comp.title);
});
@@ -1,13 +1,13 @@
import {Component} from '@angular/core';
import {Component, signal} from '@angular/core';
// #docregion component
@Component({
standalone: true,
selector: 'app-banner',
template: '<h1>{{title}}</h1>',
template: '<h1>{{title()}}</h1>',
styles: ['h1 { color: green; font-size: 350%}'],
})
export class BannerComponent {
title = 'Test Tour of Heroes';
title = signal('Test Tour of Heroes');
}
// #enddocregion component
@@ -12,19 +12,6 @@ import {DashboardHeroComponent} from './dashboard-hero.component';
beforeEach(addMatchers);
describe('DashboardHeroComponent class only', () => {
// #docregion class-only
it('raises the selected event when clicked', () => {
const comp = new DashboardHeroComponent();
const hero: Hero = {id: 42, name: 'Test'};
comp.hero = hero;
comp.selected.pipe(first()).subscribe((selectedHero: Hero) => expect(selectedHero).toBe(hero));
comp.click();
});
// #enddocregion class-only
});
describe('DashboardHeroComponent when tested directly', () => {
let comp: DashboardHeroComponent;
let expectedHero: Hero;
@@ -32,19 +19,18 @@ describe('DashboardHeroComponent when tested directly', () => {
let heroDe: DebugElement;
let heroEl: HTMLElement;
beforeEach(waitForAsync(() => {
beforeEach(() => {
// #docregion setup, config-testbed
TestBed.configureTestingModule({
providers: appProviders,
imports: [DashboardHeroComponent],
})
// #enddocregion setup, config-testbed
.compileComponents();
}));
});
// #enddocregion setup, config-testbed
});
beforeEach(() => {
beforeEach(async () => {
// #docregion setup
fixture = TestBed.createComponent(DashboardHeroComponent);
fixture.autoDetectChanges();
comp = fixture.componentInstance;
// find the hero's DebugElement and element
@@ -55,10 +41,10 @@ describe('DashboardHeroComponent when tested directly', () => {
expectedHero = {id: 42, name: 'Test Name'};
// simulate the parent setting the input property with that hero
comp.hero = expectedHero;
fixture.componentRef.setInput('hero', expectedHero);
// trigger initial data binding
fixture.detectChanges();
// wait for initial data binding
await fixture.whenStable();
// #enddocregion setup
});
@@ -72,7 +58,7 @@ describe('DashboardHeroComponent when tested directly', () => {
// #docregion click-test
it('should raise selected event when clicked (triggerEventHandler)', () => {
let selectedHero: Hero | undefined;
comp.selected.pipe(first()).subscribe((hero: Hero) => (selectedHero = hero));
comp.selected.subscribe((hero: Hero) => (selectedHero = hero));
// #docregion trigger-event-handler
heroDe.triggerEventHandler('click');
@@ -84,7 +70,7 @@ describe('DashboardHeroComponent when tested directly', () => {
// #docregion click-test-2
it('should raise selected event when clicked (element.click)', () => {
let selectedHero: Hero | undefined;
comp.selected.pipe(first()).subscribe((hero: Hero) => (selectedHero = hero));
comp.selected.subscribe((hero: Hero) => (selectedHero = hero));
heroEl.click();
expect(selectedHero).toBe(expectedHero);
@@ -94,7 +80,7 @@ describe('DashboardHeroComponent when tested directly', () => {
// #docregion click-test-3
it('should raise selected event when clicked (click helper with DebugElement)', () => {
let selectedHero: Hero | undefined;
comp.selected.pipe(first()).subscribe((hero: Hero) => (selectedHero = hero));
comp.selected.subscribe((hero: Hero) => (selectedHero = hero));
click(heroDe); // click helper with DebugElement
@@ -104,7 +90,7 @@ describe('DashboardHeroComponent when tested directly', () => {
it('should raise selected event when clicked (click helper with native element)', () => {
let selectedHero: Hero | undefined;
comp.selected.pipe(first()).subscribe((hero: Hero) => (selectedHero = hero));
comp.selected.subscribe((hero: Hero) => (selectedHero = hero));
click(heroEl); // click helper with native element
@@ -1,5 +1,5 @@
// #docregion
import {Component, EventEmitter, Input, Output} from '@angular/core';
import {Component, input, output} from '@angular/core';
import {UpperCasePipe} from '@angular/common';
import {Hero} from '../model/hero';
@@ -10,7 +10,7 @@ import {Hero} from '../model/hero';
selector: 'dashboard-hero',
template: `
<button type="button" (click)="click()" class="hero">
{{ hero.name | uppercase }}
{{ hero().name | uppercase }}
</button>
`,
styleUrls: ['./dashboard-hero.component.css'],
@@ -18,10 +18,10 @@ import {Hero} from '../model/hero';
})
// #docregion class
export class DashboardHeroComponent {
@Input() hero!: Hero;
@Output() selected = new EventEmitter<Hero>();
hero = input.required<Hero>();
selected = output<Hero>();
click() {
this.selected.emit(this.hero);
this.selected.emit(this.hero());
}
}
// #enddocregion component, class
@@ -172,7 +172,8 @@ function heroModuleSetup() {
}));
// #docregion title-case-pipe
it('should convert hero name to Title Case', () => {
it('should convert hero name to Title Case', async () => {
harness.fixture.autoDetectChanges();
// get the name's input and display elements from the DOM
const hostElement: HTMLElement = harness.routeNativeElement!;
const nameInput: HTMLInputElement = hostElement.querySelector('input')!;
@@ -184,8 +185,8 @@ function heroModuleSetup() {
// Dispatch a DOM event so that Angular learns of input value change.
nameInput.dispatchEvent(new Event('input'));
// Tell Angular to update the display binding through the title pipe
harness.detectChanges();
// Wait for Angular to update the display binding through the title pipe
await harness.fixture.whenStable();
expect(nameDisplay.textContent).toBe('Quick Brown Fox');
});
@@ -1,7 +1,7 @@
import {Injectable} from '@angular/core';
import {Injectable, signal} from '@angular/core';
@Injectable()
@Injectable({providedIn: 'root'})
export class UserService {
isLoggedIn = true;
user = {name: 'Sam Spade'};
isLoggedIn = signal(true);
user = signal({name: 'Sam Spade'});
}
@@ -3,7 +3,7 @@ import {fakeAsync, ComponentFixture, TestBed, tick, waitForAsync} from '@angular
import {asyncData, asyncError} from '../../testing';
import {of, throwError} from 'rxjs';
import {Subject, defer, of, throwError} from 'rxjs';
import {last} from 'rxjs/operators';
import {TwainComponent} from './twain.component';
@@ -25,21 +25,21 @@ describe('TwainComponent', () => {
// #docregion setup
beforeEach(() => {
TestBed.configureTestingModule({
imports: [TwainComponent],
providers: [TwainService],
});
testQuote = 'Test Quote';
// #docregion spy
// Create a fake TwainService object with a `getQuote()` spy
const twainService = jasmine.createSpyObj('TwainService', ['getQuote']);
const twainService = TestBed.inject(TwainService);
// Make the spy return a synchronous Observable with the test data
getQuoteSpy = twainService.getQuote.and.returnValue(of(testQuote));
getQuoteSpy = spyOn(twainService, 'getQuote').and.returnValue(of(testQuote));
// #enddocregion spy
TestBed.configureTestingModule({
imports: [TwainComponent],
providers: [{provide: TwainService, useValue: twainService}],
});
fixture = TestBed.createComponent(TwainComponent);
fixture.autoDetectChanges();
component = fixture.componentInstance;
quoteEl = fixture.nativeElement.querySelector('.twain');
});
@@ -54,8 +54,8 @@ describe('TwainComponent', () => {
// The quote would not be immediately available if the service were truly async.
// #docregion sync-test
it('should show quote after component initialized', () => {
fixture.detectChanges(); // onInit()
it('should show quote after component initialized', async () => {
await fixture.whenStable(); // onInit()
// sync spy result shows testQuote immediately after init
expect(quoteEl.textContent).toBe(testQuote);
@@ -67,12 +67,20 @@ describe('TwainComponent', () => {
// Use `fakeAsync` because the component error calls `setTimeout`
// #docregion error-test
it('should display error when TwainService fails', fakeAsync(() => {
// tell spy to return an error observable
getQuoteSpy.and.returnValue(throwError(() => new Error('TwainService test failure')));
// tell spy to return an error observable after a timeout
getQuoteSpy.and.returnValue(
defer(() => {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('TwainService test failure');
});
});
}),
);
fixture.detectChanges(); // onInit()
// sync spy errors immediately after init
tick(); // flush the component's setTimeout()
tick(); // flush the setTimeout()
fixture.detectChanges(); // update errorMessage within setTimeout()
@@ -120,46 +128,18 @@ describe('TwainComponent', () => {
}));
// #enddocregion fake-async-test
// #docregion waitForAsync-test
it('should show quote after getQuote (waitForAsync)', waitForAsync(() => {
// #docregion async-test
it('should show quote after getQuote (async)', async () => {
fixture.detectChanges(); // ngOnInit()
expect(quoteEl.textContent).withContext('should show placeholder').toBe('...');
fixture.whenStable().then(() => {
// wait for async getQuote
fixture.detectChanges(); // update view with quote
expect(quoteEl.textContent).toBe(testQuote);
expect(errorMessage()).withContext('should not show error').toBeNull();
});
}));
// #enddocregion waitForAsync-test
// #docregion quote-done-test
it('should show last quote (quote done)', (done: DoneFn) => {
fixture.detectChanges();
component.quote.pipe(last()).subscribe(() => {
fixture.detectChanges(); // update view with quote
expect(quoteEl.textContent).toBe(testQuote);
expect(errorMessage()).withContext('should not show error').toBeNull();
done();
});
await fixture.whenStable();
// wait for async getQuote
fixture.detectChanges(); // update view with quote
expect(quoteEl.textContent).toBe(testQuote);
expect(errorMessage()).withContext('should not show error').toBeNull();
});
// #enddocregion quote-done-test
// #docregion spy-done-test
it('should show quote after getQuote (spy done)', (done: DoneFn) => {
fixture.detectChanges();
// the spy's most recent call returns the observable with the test quote
getQuoteSpy.calls.mostRecent().returnValue.subscribe(() => {
fixture.detectChanges(); // update view with quote
expect(quoteEl.textContent).toBe(testQuote);
expect(errorMessage()).withContext('should not show error').toBeNull();
done();
});
});
// #enddocregion spy-done-test
// #enddocregion async-test
it('should display error when TwainService fails', fakeAsync(() => {
// tell spy to return an async error observable
@@ -1,5 +1,5 @@
// #docregion
import {Component, OnInit} from '@angular/core';
import {Component, OnInit, signal} from '@angular/core';
import {AsyncPipe} from '@angular/common';
import {sharedImports} from '../shared/shared';
@@ -16,16 +16,16 @@ import {TwainService} from './twain.service';
<i>{{ quote | async }}</i>
</p>
<button type="button" (click)="getQuote()">Next quote</button>
@if (errorMessage) {
<p class="error">{{ errorMessage }}</p>
@if (errorMessage()) {
<p class="error">{{ errorMessage() }}</p>
}`,
// #enddocregion template
styles: ['.twain { font-style: italic; } .error { color: red; }'],
imports: [AsyncPipe, sharedImports],
})
export class TwainComponent implements OnInit {
errorMessage!: string;
quote!: Observable<string>;
errorMessage = signal('');
quote?: Observable<string>;
constructor(private twainService: TwainService) {}
@@ -35,12 +35,11 @@ export class TwainComponent implements OnInit {
// #docregion get-quote
getQuote() {
this.errorMessage = '';
this.errorMessage.set('');
this.quote = this.twainService.getQuote().pipe(
startWith('...'),
catchError((err: any) => {
// Wait a turn because errorMessage already set once this turn
setTimeout(() => (this.errorMessage = err.message || err.toString()));
this.errorMessage.set(err.message || err.toString());
return of('...'); // reset message to placeholder
}),
);
@@ -11,41 +11,6 @@ class MockUserService {
}
// #enddocregion mock-user-service
describe('WelcomeComponent (class only)', () => {
let comp: WelcomeComponent;
let userService: UserService;
// #docregion class-only-before-each
beforeEach(() => {
TestBed.configureTestingModule({
// provide the component-under-test and dependent service
providers: [WelcomeComponent, {provide: UserService, useClass: MockUserService}],
});
// inject both the component and the dependent service.
comp = TestBed.inject(WelcomeComponent);
userService = TestBed.inject(UserService);
});
// #enddocregion class-only-before-each
// #docregion class-only-tests
it('should not have welcome message after construction', () => {
expect(comp.welcome).toBe('');
});
it('should welcome logged in user after Angular calls ngOnInit', () => {
comp.ngOnInit();
expect(comp.welcome).toContain(userService.user.name);
});
it('should ask user to log in if not logged in after ngOnInit', () => {
userService.isLoggedIn = false;
comp.ngOnInit();
expect(comp.welcome).not.toContain(userService.user.name);
expect(comp.welcome).toContain('log in');
});
// #enddocregion class-only-tests
});
describe('WelcomeComponent', () => {
let comp: WelcomeComponent;
let fixture: ComponentFixture<WelcomeComponent>;
@@ -53,40 +18,17 @@ describe('WelcomeComponent', () => {
let userService: UserService; // the TestBed injected service
let el: HTMLElement; // the DOM element with the welcome message
// #docregion setup, user-service-stub
let userServiceStub: Partial<UserService>;
// #enddocregion user-service-stub
// #docregion setup
beforeEach(() => {
// stub UserService for test purposes
// #docregion user-service-stub
userServiceStub = {
isLoggedIn: true,
user: {name: 'Test User'},
};
// #enddocregion user-service-stub
// #docregion config-test-module
TestBed.configureTestingModule({
imports: [WelcomeComponent],
// #enddocregion setup
// providers: [ UserService ], // NO! Don't provide the real service!
// Provide a test-double instead
// #docregion setup
providers: [{provide: UserService, useValue: userServiceStub}],
});
// #enddocregion config-test-module
fixture = TestBed.createComponent(WelcomeComponent);
fixture.autoDetectChanges();
comp = fixture.componentInstance;
// #enddocregion setup
// #docregion injected-service
// UserService actually injected into the component
userService = fixture.debugElement.injector.get(UserService);
// #enddocregion injected-service
componentUserService = userService;
// #docregion setup
// #docregion inject-from-testbed
// UserService from the root injector
userService = TestBed.inject(UserService);
@@ -98,22 +40,22 @@ describe('WelcomeComponent', () => {
// #enddocregion setup
// #docregion tests
it('should welcome the user', () => {
fixture.detectChanges();
it('should welcome the user', async () => {
await fixture.whenStable();
const content = el.textContent;
expect(content).withContext('"Welcome ..."').toContain('Welcome');
expect(content).withContext('expected name').toContain('Test User');
});
it('should welcome "Bubba"', () => {
userService.user.name = 'Bubba'; // welcome message hasn't been shown yet
fixture.detectChanges();
it('should welcome "Bubba"', async () => {
userService.user.set({name: 'Bubba'}); // welcome message hasn't been shown yet
await fixture.whenStable();
expect(el.textContent).toContain('Bubba');
});
it('should request login if not logged in', () => {
userService.isLoggedIn = false; // welcome message hasn't been shown yet
fixture.detectChanges();
it('should request login if not logged in', async () => {
userService.isLoggedIn.set(false); // welcome message hasn't been shown yet
await fixture.whenStable();
const content = el.textContent;
expect(content).withContext('not welcomed').not.toContain('Welcome');
expect(content)
@@ -1,21 +1,21 @@
// #docregion
import {Component, OnInit} from '@angular/core';
import {Component, OnInit, signal} from '@angular/core';
import {UserService} from '../model/user.service';
@Component({
standalone: true,
selector: 'app-welcome',
template: '<h3 class="welcome"><i>{{welcome}}</i></h3>',
template: '<h3 class="welcome"><i>{{welcome()}}</i></h3>',
})
// #docregion class
export class WelcomeComponent implements OnInit {
welcome = '';
welcome = signal('');
constructor(private userService: UserService) {}
ngOnInit(): void {
this.welcome = this.userService.isLoggedIn
? 'Welcome, ' + this.userService.user.name
: 'Please log in.';
this.welcome.set(
this.userService.isLoggedIn() ? 'Welcome, ' + this.userService.user().name : 'Please log in.',
);
}
}
// #enddocregion class
@@ -89,7 +89,7 @@ The heroes list gradually re-enters the page as you delete each letter in the fi
The HTML template contains a trigger called `filterAnimation`.
<docs-code header="src/app/hero-list-page.component.html" path="adev/src/content/examples/animations/src/app/hero-list-page.component.html" visibleRegion="filter-animations"/>
<docs-code header="src/app/hero-list-page.component.html" path="adev/src/content/examples/animations/src/app/hero-list-page.component.html" visibleRegion="filter-animations" language="angular-html"/>
The `filterAnimation` in the component's decorator contains three transitions.
@@ -43,14 +43,14 @@ rendered, or **projected**, at the location of that `<ng-content>`:
export class CustomCard {/* ... */}
```
```html
```angular-html
<!-- Using the component -->
<custom-card>
<p>This is the projected content</p>
</custom-card>
```
```html
```angular-html
<!-- The rendered DOM -->
<custom-card>
<div class="card-shadow">
@@ -78,7 +78,7 @@ Angular supports projecting multiple different elements into different `<ng-cont
based on CSS selector. Expanding the card example from above, you could create two placeholders for
a card title and a card body by using the `select` attribute:
```html
```angular-html
<!-- Component template -->
<div class="card-shadow">
<ng-content select="card-title"></ng-content>
@@ -87,7 +87,7 @@ a card title and a card body by using the `select` attribute:
</div>
```
```html
```angular-html
<!-- Using the component -->
<custom-card>
<card-title>Hello</card-title>
@@ -95,7 +95,7 @@ a card title and a card body by using the `select` attribute:
</custom-card>
```
```html
```angular-html
<!-- Rendered DOM -->
<custom-card>
<div class="card-shadow">
@@ -113,7 +113,7 @@ If you include one or more `<ng-content>` placeholders with a `select` attribute
one `<ng-content>` placeholder without a `select` attribute, the latter captures all elements that
did not match a `select` attribute:
```html
```angular-html
<!-- Component template -->
<div class="card-shadow">
<ng-content select="card-title"></ng-content>
@@ -123,7 +123,7 @@ did not match a `select` attribute:
</div>
```
```html
```angular-html
<!-- Using the component -->
<custom-card>
<card-title>Hello</card-title>
@@ -132,7 +132,7 @@ did not match a `select` attribute:
</custom-card>
```
```html
```angular-html
<!-- Rendered DOM -->
<custom-card>
<div class="card-shadow">
@@ -153,7 +153,7 @@ Angular supports a special attribute, `ngProjectAs`, that allows you to specify
any element. Whenever an element with `ngProjectAs` is checked against an `<ng-content>`
placeholder, Angular compares against the `ngProjectAs` value instead of the element's identity:
```html
```angular-html
<!-- Component template -->
<div class="card-shadow">
<ng-content select="card-title"></ng-content>
@@ -162,7 +162,7 @@ placeholder, Angular compares against the `ngProjectAs` value instead of the ele
</div>
```
```html
```angular-html
<!-- Using the component -->
<custom-card>
<h3 ngProjectAs="card-title">Hello</h3>
@@ -171,7 +171,7 @@ placeholder, Angular compares against the `ngProjectAs` value instead of the ele
</custom-card>
```
```html
```angular-html
<!-- Rendered DOM -->
<custom-card>
<div class="card-shadow">
@@ -17,14 +17,14 @@ The contents of a component's template are rendered inside its host element.
export class ProfilePhoto {}
```
```html
```angular-html
<!-- Using the component -->
<h3>Your profile photo</h3>
<profile-photo />
<button>Upload a new profile photo</button>
```
```html
```angular-html
<!-- Rendered DOM -->
<h3>Your profile photo</h3>
<profile-photo>
@@ -116,7 +116,7 @@ The component may _also_ define host bindings for the same properties or attribu
export class ProfilePhoto { /* ... */ }
```
```html
```angular-html
<profile-photo role="group" [id]="otherId" />
```
+3 -3
View File
@@ -15,7 +15,7 @@ export class CustomSlider {
This lets you bind to the property in a template:
```html
```angular-html
<custom-slider [value]="50" />
```
@@ -62,7 +62,7 @@ function trimString(value: string | undefined) {
}
</docs-code>
```html
```angular-html
<custom-slider [label]="systemVolume" />
```
@@ -121,7 +121,7 @@ export class CustomSlider {
}
</docs-code>
```html
```angular-html
<custom-slider [sliderValue]="50" />
```
@@ -21,7 +21,7 @@ export class MyComp {
An output is automatically recognized by Angular whenever you use the `output` function as an initializer of a class member.
Parent components can listen to outputs in templates by using the event binding syntax.
```html
```angular-html
<my-comp (onNameChange)="showNewName($event)" />
```
+3 -3
View File
@@ -11,7 +11,7 @@ export class ExpandablePanel {
}
</docs-code>
```html
```angular-html
<expandable-panel (panelClosed)="savePanelState()" />
```
@@ -46,7 +46,7 @@ this.thumbDropped.emit({
When defining an event listener in a template, you can access the event data from the `$event` variable:
```html
```angular-html
<custom-slider (valueChanged)="logValue($event)" />
```
@@ -61,7 +61,7 @@ export class CustomSlider {
}
</docs-code>
```html
```angular-html
<custom-slider (valueChanged)="saveVolume()" />
```
@@ -82,7 +82,7 @@ export class InnerItem {
In the example above, clicking the "Load content" button results in the following DOM structure
```html
```angular-html
<outer-container>
<p>This is the start of the outer container</p>
<inner-item>
+16 -16
View File
@@ -6,7 +6,7 @@ Deferrable views can be used in component template to defer the loading of selec
Deferrable views support a series of [triggers](guide/defer#triggers), [prefetching](guide/defer#prefetching), and several sub blocks used for [placeholder](guide/defer#placeholder), [loading](guide/defer#loading), and [error](guide/defer#error) state management. You can also create custom conditions with [`when`](guide/defer#when) and [`prefetch when`](guide/defer#prefetching).
```html
```angular-html
@defer {
<large-component />
}
@@ -44,7 +44,7 @@ Note: For the best user experience, you should always specify a `@placeholder` b
The `@placeholder` block accepts an optional parameter to specify the `minimum` amount of time that this placeholder should be shown. This `minimum` parameter is specified in time increments of milliseconds (ms) or seconds (s). This parameter exists to prevent fast flickering of placeholder content in the case that the deferred dependencies are fetched quickly. The `minimum` timer for the `@placeholder` block begins after the initial render of this `@placeholder` block completes.
```html
```angular-html
@defer {
<large-component />
} @placeholder (minimum 500ms) {
@@ -62,7 +62,7 @@ For example, you could show a loading spinner. Once loading has been triggered,
The `@loading` block accepts two optional parameters to specify the `minimum` amount of time that this placeholder should be shown and amount of time to wait `after` loading begins before showing the loading template. `minimum` and `after` parameters are specified in time increments of milliseconds (ms) or seconds (s). Just like `@placeholder`, these parameters exist to prevent fast flickering of content in the case that the deferred dependencies are fetched quickly. Both the `minimum` and `after` timers for the `@loading` block begins immediately after the loading has been triggered.
```html
```angular-html
@defer {
<large-component />
} @loading (after 100ms; minimum 1s) {
@@ -74,7 +74,7 @@ The `@loading` block accepts two optional parameters to specify the `minimum` am
The `@error` block allows you to declare content that will be shown if deferred loading fails. Similar to `@placeholder` and `@loading`, the dependencies of the `@error` block are eagerly loaded. The `@error` block is optional.
```html
```angular-html
@defer {
<calendar-cmp />
} @error {
@@ -93,7 +93,7 @@ Multiple event triggers can be defined at once. For example: `on interaction; on
Note: Multiple `on` triggers are always OR conditions. Similarly, `on` mixed with `when` conditions are also OR conditions.
```html
```angular-html
@defer (on viewport; on timer(5s)) {
<calendar-cmp />
} @placeholder {
@@ -106,7 +106,7 @@ Note: Multiple `on` triggers are always OR conditions. Similarly, `on` mixed wit
Note: if the `when` condition switches back to `false`, the defer block is not reverted back to the placeholder. The swap is a one-time operation. If the content within the block should be conditionally rendered, an `if` condition can be used within the block itself.
```html
```angular-html
@defer (when cond) {
<calendar-cmp />
}
@@ -114,7 +114,7 @@ Note: if the `when` condition switches back to `false`, the defer block is not r
You could also use both `when` and `on` together in one statement, and the swap will be triggered if either condition is met.
```html
```angular-html
@defer (on viewport; when cond) {
<calendar-cmp />
} @placeholder {
@@ -132,7 +132,7 @@ You could also use both `when` and `on` together in one statement, and the swap
By default, the placeholder will act as the element watched for entering viewport as long as it is a single root element node.
```html
```angular-html
@defer (on viewport) {
<calendar-cmp />
} @placeholder {
@@ -142,7 +142,7 @@ By default, the placeholder will act as the element watched for entering viewpor
Alternatively, you can specify a [template reference variable](guide/templates/reference-variables) in the same template as the `@defer` block as the element that is watched to enter the viewport. This variable is passed in as a parameter on the viewport trigger.
```html
```angular-html
<div #greeting>Hello!</div>
@defer (on viewport(greeting)) {
@@ -156,7 +156,7 @@ Alternatively, you can specify a [template reference variable](guide/templates/r
By default, the placeholder will act as the interaction element as long as it is a single root element node.
```html
```angular-html
@defer (on interaction) {
<calendar-cmp />
} @placeholder {
@@ -166,7 +166,7 @@ By default, the placeholder will act as the interaction element as long as it is
Alternatively, you can specify a [template reference variable](guide/templates/reference-variables) as the element that triggers interaction. This variable is passed in as a parameter on the interaction trigger.
```html
```angular-html
<button type="button" #greeting>Hello!</button>
@defer (on interaction(greeting)) {
@@ -182,7 +182,7 @@ Alternatively, you can specify a [template reference variable](guide/templates/r
By default, the placeholder will act as the hover element as long as it is a single root element node.
```html
```angular-html
@defer (on hover) {
<calendar-cmp />
} @placeholder {
@@ -192,7 +192,7 @@ By default, the placeholder will act as the hover element as long as it is a sin
Alternatively, you can specify a [template reference variable](guide/templates/reference-variables) as the hover element. This variable is passed in as a parameter on the hover trigger.
```html
```angular-html
<div #greeting>Hello!</div>
@defer (on hover(greeting)) {
@@ -206,7 +206,7 @@ Alternatively, you can specify a [template reference variable](guide/templates/r
`immediate` triggers the deferred load immediately, meaning once the client has finished rendering, the defer chunk would then start fetching right away.
```html
```angular-html
@defer (on immediate) {
<calendar-cmp />
} @placeholder {
@@ -218,7 +218,7 @@ Alternatively, you can specify a [template reference variable](guide/templates/r
`timer(x)` would trigger after a specified duration. The duration is required and can be specified in `ms` or `s`.
```html
```angular-html
@defer (on timer(500ms)) {
<calendar-cmp />
}
@@ -232,7 +232,7 @@ In this case, `when` and `on` associated with defer controls when to render, and
In the example below, the prefetching starts when a browser becomes idle and the contents of the block is rendered on interaction.
```html
```angular-html
@defer (on interaction; prefetch on idle) {
<calendar-cmp />
} @placeholder {
@@ -23,7 +23,7 @@ class MyComponent {
private service2: Service2 = inject(Service2); // In context
constructor() {
this.service1 = inject(HeroService) // In context
this.service1 = inject(Service1) // In context
}
}
</docs-code>
@@ -59,7 +59,7 @@ export class AdminMenu { }
By explicitly specifying the inputs and outputs, consumers of the component with `hostDirective` can
bind them in a template:
```html
```angular-html
<admin-menu menuId="top-menu" (menuClosed)="logMenuClosed()">
```
@@ -81,7 +81,7 @@ component:
export class AdminMenu { }
```
```html
```angular-html
<admin-menu id="top-menu" (closed)="logMenuClosed()">
```
@@ -10,7 +10,7 @@ In this guide you'll build a structural directive which fetches data from a give
The following is an example of using this directive directly on an `<ng-template>` would look like:
```html
```angular-html
<ng-template select let-data [selectFrom]="source">
<p>The data is: {{ data }}</p>
</ng-template>
@@ -30,7 +30,7 @@ Structural directives can be applied directly on an element by prefixing the dir
You can use this with `SelectDirective` as follows:
```html
```angular-html
<p *select="let data from source">The data is: {{data}}</p>
```
@@ -38,7 +38,7 @@ This example shows the flexibility of structural directive shorthand syntax, whi
When used in this way, only the structural directive and its bindings are applied to the `<ng-template>`. Any other attributes or bindings on the `<p>` tag are left alone. For example, these two forms are equivalent:
```html
```angular-html
<!-- Shorthand syntax: -->
<p class="data-view" *select="let data from source">The data is: {{data}}</p>
@@ -80,7 +80,7 @@ import {Directive, TemplateRef, ViewContainerRef} from '@angular/core';
@Directive({
standalone: true,
selector: 'select',
selector: '[select]',
})
export class SelectDirective {
constructor(private templateRef: TemplateRef, private ViewContainerRef: ViewContainerRef) {}
@@ -395,7 +395,7 @@ Initially, the form contains one `Alias` field. To add another field, click the
## Reactive forms API summary
The following table lists the base classes and services used to create and manage reactive form controls.
For complete syntax details, see the API reference documentation for the [Forms package](api/forms "API reference").
For complete syntax details, see the API reference documentation for the [Forms package](api#forms "API reference").
### Classes
+1 -1
View File
@@ -172,7 +172,7 @@ A DI-based interceptor is an injectable class which implements the `HttpIntercep
<docs-code language="ts">
@Injectable()
public class LoggingInterceptor implements HttpInterceptor {
export class LoggingInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, handler: HttpHandler): Observable<HttpEvent<any>> {
console.log('Request URL: ' + req.url);
return handler.handle(req);
+31 -5
View File
@@ -12,7 +12,15 @@ Without hydration enabled, server-side rendered Angular applications will destro
## How do you enable hydration in Angular
Before you can get started with hydration, you must have a server-side rendered (SSR) application. Follow the [Angular SSR Guide](guide/ssr) to enable server-side rendering first. Once you have SSR working with your application, you can enable hydration by visiting your main app component or module and importing `provideClientHydration` from `@angular/platform-browser`. You'll then add that provider to your app's bootstrapping providers list.
Hydration can be enabled for server-side rendered (SSR) applications only. Follow the [Angular SSR Guide](guide/ssr) to enable server-side rendering first.
### Using Angular CLI
If you've used Angular CLI to enable SSR (either by enabling it during application creation or later via `ng add @angular/ssr`), the code that enables hydration should already be included into your application.
### Manual setup
If you have a custom setup and didn't use Angular CLI to enable SSR, you can enable hydration manually by visiting your main application component or module and importing `provideClientHydration` from `@angular/platform-browser`. You'll then add that provider to your app's bootstrapping providers list.
```typescript
import {
@@ -43,13 +51,31 @@ export class AppModule {}
IMPORTANT: Make sure that the `provideClientHydration()` call is also included into a set of providers that is used to bootstrap an application on the **server**. In applications with the default project structure (generated by the `ng new` command), adding a call to the root `AppModule` should be sufficient, since this module is imported by the server module. If you use a custom setup, add the `provideClientHydration()` call to the providers list in the server bootstrap configuration.
After you've followed these steps and have started up your server, load your application in the browser.
### Verify that hydration is enabled
After you've configured hydration and have started up your server, load your application in the browser.
HELPFUL: You will likely need to fix instances of Direct DOM Manipulation before hydration will fully work either by switching to Angular constructs or by using `ngSkipHydration`. See [Constraints](#constraints), [Direct DOM Manipulation](#direct-dom-manipulation), and [How to skip hydration for particular components](#how-to-skip-hydration-for-particular-components) for more details.
While running an application in dev mode, you can confirm hydration is enabled by opening the Developer Tools in your browser and viewing the console. You should see a message that includes hydration-related stats, such as the number of components and nodes hydrated.
While running an application in dev mode, you can confirm hydration is enabled by opening the Developer Tools in your browser and viewing the console. You should see a message that includes hydration-related stats, such as the number of components and nodes hydrated. Angular calculates the stats based on all components rendered on a page, including those that come from third-party libraries.
HELPFUL: Angular calculates the stats based on all components rendered on a page, including those that come from third-party libraries.
You can also use [Angular DevTools browser extension](tools/devtools) to see hydration status of components on a page. Angular DevTools also allows to enable an overlay to indicate which parts of the page were hydrated. If there is a hydration mismatch error - DevTools would also highlight a component that caused the error.
## Capturing and replaying events
When an application is rendered on the server, it is visible in a browser as soon as produced HTML loads. Users may assume that they can interact with the page, but event listeners are not attached until hydration completes. Starting from v18, you can enable the Event Replay feature that allows to capture all events that happen before hydration and replay those events once hydration has completed. You can enable it using the `withEventReplay()` function, for example:
```typescript
import {provideClientHydration, withEventReplay} from '@angular/platform-browser';
bootstrapApplication(App, {
providers: [
provideClientHydration(withEventReplay())
]
});
```
IMPORTANT: the Event Replay feature is currently in [Developer Preview](/reference/releases#developer-preview).
## Constraints
@@ -104,7 +130,7 @@ For a full reference on hydration related errors, visit the [Errors Reference Gu
Some components may not work properly with hydration enabled due to some of the aforementioned issues, like [Direct DOM Manipulation](#direct-dom-manipulation). As a workaround, you can add the `ngSkipHydration` attribute to a component's tag in order to skip hydrating the entire component.
```html
```angular-html
<app-example ngSkipHydration />
```
+16 -2
View File
@@ -194,7 +194,7 @@ NgOptimizedImage includes a number of features designed to improve loading perfo
A [`preconnect` resource hint](https://web.dev/preconnect-and-dns-prefetch) for your image origin ensures that the LCP image loads as quickly as possible.
Preconnect links are automatically generated for domains provided as an argument to a [loader](#optional-set-up-a-loader). If an image origin cannot be automatically identified, and no preconnect link is detected for the LCP image, `NgOptimizedImage` will warn during development. In that case, you should manually add a resource hint to `index.html`. Within the `<head>` of the document, add a `link` tag with `rel="preload"`, as shown below:
Preconnect links are automatically generated for domains provided as an argument to a [loader](#optional-set-up-a-loader). If an image origin cannot be automatically identified, and no preconnect link is detected for the LCP image, `NgOptimizedImage` will warn during development. In that case, you should manually add a resource hint to `index.html`. Within the `<head>` of the document, add a `link` tag with `rel="preconnect"`, as shown below:
<docs-code language="html">
@@ -432,4 +432,18 @@ For maintenance reasons, we don't currently plan to support additional built-in
No, but this is on our roadmap, so stay tuned.
If you're waiting on this feature, please upvote the Github issue [here](https://github.com/angular/angular/issues/56594).
If you're waiting on this feature, please upvote the Github issue [here](https://github.com/angular/angular/issues/56594).
### How do I find my LCP image with Chrome DevTools?
1. Using the performance tab of the Chrome DevTools, click on the "start profiling and reload page" button on the top left. It looks like a page refresh icon.
2. This will trigger a profiling snapshot of your Angular application.
3. Once the profiling result is available, select "LCP" in the timings section.
4. A summary entry should appear in the panel at the bottom. You can find the LCP element in the row for "related node". Clicking on it will reveal the element in the Elements panel.
<img alt="LCP in the Chrome DevTools" src="assets/images/guide/image-optimization/devtools-lcp.png">
NOTE: This only identifies the LCP element within the viewport of the page you are testing. It is also recommended to use mobile emulation to identify the LCP element for smaller screens.
@@ -0,0 +1,12 @@
load("@npm//@angular/docs:index.bzl", "generate_guides")
generate_guides(
name = "performance",
srcs = glob([
"*.md",
]),
data = [
"//adev/src/assets/images:overview.svg",
],
visibility = ["//adev:__subpackages__"],
)
@@ -0,0 +1,15 @@
<docs-decorative-header title="Performance" imgSrc="adev/src/assets/images/overview.svg"> <!-- markdownlint-disable-line -->
Learn about different ways you can optimize the performance of your application.
</docs-decorative-header>
One of the top priorities of any developer is ensuring that their application is as performant as possible. These guides are here to help you follow best practices for building performant applications.
That said, please note that these best practices will only take the performance of your application so far. At the end of the day, we encourage you to measure performance in order to best understand what custom optimizations are best for your application.
| Guides Types | Description |
| :---------------------------------------- | :--------------------------------------------------------------------------------------------------------- |
| [Deferrable views](/guide/defer) | Defer loading of select dependencies within a template by wrapping corresponding parts in a `@defer` block. |
| [Image optimization](/guide/image-optimization) | Use the `NgOptimizedImage` directive to adopt best practices for loading images. |
| [Server-side rendering](/guide/ssr) | Learn how to leverage rendering pages on the server to improve load times. |
| [Build-time prerendering](/guide/prerendering) | Also known as static-side generation (SSG), is an alternate rendering method to improve load times. |
| [Hydration](/guide/hydration) | A process to improve application performance by restoring its state after server-side rendering and reusing existing DOM structure as much as possible. |
+1 -1
View File
@@ -23,5 +23,5 @@ The following are commonly used built-in pipes for data formatting:
- [`AsyncPipe`](api/common/AsyncPipe): Subscribe and unsubscribe to an asynchronous source such as an observable.
- [`JsonPipe`](api/common/JsonPipe): Display a component object property to the screen as JSON for debugging.
Note: For a complete list of built-in pipes, see the [pipes API documentation](/api/common#pipes "Pipes API reference summary").
Note: For a complete list of built-in pipes, see the [pipes API documentation](/api?type=pipe "Pipes API reference summary").
To learn more about using pipes for internationalization (i18n) efforts, see [formatting data based on locale](guide/i18n/format-data-locale).
@@ -93,7 +93,7 @@ const routes: Routes = [
Now that you have defined your routes, add them to your application. First, add links to the two components. Assign the anchor tag that you want to add the route to the `routerLink` attribute. Set the value of the attribute to the component to show when a user clicks on each link. Next, update your component template to include `<router-outlet>`. This element informs Angular to update the application view with the component for the selected route.
```html
```angular-html
<h1>Angular Router App</h1>
<nav>
<ul>
@@ -260,7 +260,7 @@ This means you're adding a second `<router-outlet>` to your app, because it is i
In this example, there are two additional child components, `child-a`, and `child-b`.
Here, `FirstComponent` has its own `<nav>` and a second `<router-outlet>` in addition to the one in `AppComponent`.
```html
```angular-html
<h2>First Component</h2>
<nav>
@@ -358,7 +358,7 @@ The following example shows a relative route to another component, `second-compo
`FirstComponent` and `SecondComponent` are at the same level in the tree, however, the link to `SecondComponent` is situated within the `FirstComponent`, meaning that the router has to go up a level and then into the second directory to find the `SecondComponent`.
Rather than writing out the whole path to get to `SecondComponent`, use the `../` notation to go up a level.
```html
```angular-html
<h2>First Component</h2>
<nav>
@@ -516,13 +516,13 @@ A link parameters array holds the following ingredients for router navigation:
Bind the `RouterLink` directive to such an array like this:
```html
```angular-html
<a [routerLink]="['/heroes']">Heroes</a>
```
The following is a two-element array when specifying a route parameter:
```html
```angular-html
<a [routerLink]="['/hero', hero.id]">
<span class="badge">{{ hero.id }}</span>{{ hero.name }}
</a>
@@ -530,7 +530,7 @@ The following is a two-element array when specifying a route parameter:
Provide optional route parameters in an object, as in `{ foo: 'foo' }`:
```html
```angular-html
<a [routerLink]="['/crisis-center', { foo: 'foo' }]">Crisis Center</a>
```
@@ -539,7 +539,7 @@ However, with a child router, such as in the crisis center, you create new link
The following minimal `RouterLink` example builds upon a specified default child route for the crisis center.
```html
```angular-html
<a [routerLink]="['/crisis-center']">Crisis Center</a>
```
@@ -552,7 +552,7 @@ Review the following:
Consider the following router link that navigates from the root of the application down to the Dragon Crisis:
```html
```angular-html
<a [routerLink]="['/crisis-center', 1]">Dragon Crisis</a>
```
@@ -589,7 +589,7 @@ The router can compose a "natural" URL that is indistinguishable from one that w
Here's the Crisis Center URL in this "HTML5 pushState" style:
```http
```text
localhost:3002/crisis-center
```
@@ -597,7 +597,7 @@ Older browsers send page requests to the server when the location URL changes un
Routers can take advantage of this exception by composing in-application route URLs with hashes.
Here's a "hash URL" that routes to the Crisis Center.
```http
```text
localhost:3002/src/#/crisis-center
```
@@ -660,7 +660,7 @@ While the router uses the [HTML5 pushState](https://developer.mozilla.org/docs/W
The preferred way to configure the strategy is to add a [`<base href>` element](https://developer.mozilla.org/docs/Web/HTML/Element/base 'base href') tag in the `<head>` of the `index.html`.
```html
```angular-html
<base href="/">
```
@@ -153,7 +153,7 @@ You should see only the application title.
To view the `crisis-list` component, add `crisis-list` to the end of the path in your browser's address bar.
For example:
<docs-code language="https">
<docs-code language="text">
http://localhost:4200/crisis-list
</docs-code>
@@ -161,7 +161,7 @@ Notice that the `crisis-list` component displays.
Angular is using the route you defined to dynamically load the component.
You can load the `heroes-list` component the same way:
<docs-code language="https">
<docs-code language="text">
http://localhost:4200/heroes-list
</docs-code>
+2 -2
View File
@@ -49,7 +49,7 @@ This allows users to bind to your input using `[studentAge]`, while inside your
Signal inputs are read-only signals.
As with signals declared via `signal()`, you access the current value of the input by calling the input signal.
```html
```angular-html
<p>First name: {{firstName()}}</p>
<p>Last name: {{lastName()}}</p>
```
@@ -126,7 +126,7 @@ These values are then parsed to a `boolean` with the transform, resulting in boo
That way, you are only dealing with `boolean` inside your component when calling `this.disabled()`, while users of your component can pass an empty string as a shorthand to mark your component as disabled.
```html
```angular-html
<my-custom-comp disabled>
```
@@ -12,6 +12,7 @@ Use the `toSignal` function to create a signal which tracks the value of an Obse
import { Component } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { interval } from 'rxjs';
import { toSignal } from '@angular/core/rxjs-interop';
@Component({
template: `{{ counter() }}`,
@@ -6,7 +6,7 @@ Angular templates support _control flow blocks_ that let you conditionally show,
The `@if` block conditionally displays its content when its condition expression is truthy:
```html
```angular-html
@if (a > b) {
{{a}} is greater than {{b}}
}
@@ -15,7 +15,7 @@ The `@if` block conditionally displays its content when its condition expression
The `@if` block might have one or more associated `@else` blocks. Immediately after an `@if` block, you can optionally
specify any number of `@else if` blocks and one `@else` block:
```html
```angular-html
@if (a > b) {
{{a}} is greater than {{b}}
} @else if (b > a) {
@@ -30,7 +30,7 @@ specify any number of `@else if` blocks and one `@else` block:
The new built-in `@if` conditional supports referencing of expression results to keep a solution for common coding
patterns:
```html
```angular-html
@if (users$ | async; as users) {
{{ users.length }}
}
@@ -42,7 +42,7 @@ The `@for` repeatedly renders content of a block for each item in a collection.
JavaScript [iterable](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Iteration_protocols) but there
are performance advantages of using a regular `Array`. A basic `@for` loop looks like:
```html
```angular-html
@for (item of items; track item.id) {
{{ item.name }}
}
@@ -79,7 +79,7 @@ Inside `@for` contents, several implicit variables are always available:
These variables are always available with these names, but can be aliased via a `let` segment:
```html
```angular-html
@for (item of items; track item.id; let idx = $index, e = $even) {
Item #{{ idx }}: {{ item.name }}
}
@@ -92,7 +92,7 @@ The aliasing is especially useful in case of using nested `@for` blocks where co
You can optionally include an `@empty` section immediately after the `@for` block content. The content of the `@empty`
block displays when there are no items:
```html
```angular-html
@for (item of items; track item.name) {
<li> {{ item.name }}</li>
} @empty {
@@ -104,7 +104,7 @@ block displays when there are no items:
The syntax for `switch` is very similar to `if`, and is inspired by the JavaScript `switch` statement:
```html
```angular-html
@switch (condition) {
@case (caseA) {
Case A.
@@ -147,7 +147,7 @@ The `track` setting replaces `NgFor`'s concept of a `trackBy` function. Because
better experience than passing a `trackBy` function, and directly use an expression representing the key instead.
Migrating from `trackBy` to `track` is possible by invoking the `trackBy` function:
```html
```angular-html
@for (item of items; track itemId($index, item)) {
{{ item.name }}
}
@@ -35,13 +35,13 @@ You can bind to keyboard events using Angular's binding syntax. You can specify
Combinations of keys can be separated by a `.` (period). For example, `keydown.enter` will allow you to bind events to the `enter` key. You can also use modifier keys, such as `shift`, `alt`, `control`, and the `command` keys from Mac. The following example shows how to bind a keyboard event to `keydown.shift.t`.
```html
```angular-html
<input (keydown.shift.t)="onKeydown($event)" />
```
Depending on the operating system, some key combinations might create special characters instead of the key combination that you expect. MacOS, for example, creates special characters when you use the option and shift keys together. If you bind to `keydown.shift.alt.t`, on macOS, that combination produces a `ˇ` character instead of a `t`, which doesn't match the binding and won't trigger your event handler. To bind to `keydown.shift.alt.t` on macOS, use the `code` keyboard event field to get the correct behavior, such as `keydown.code.shiftleft.altleft.keyt` shown in this example.
```html
```angular-html
<input (keydown.code.shiftleft.altleft.keyt)="onKeydown($event)" />
```
@@ -57,7 +57,7 @@ This is an advanced technique that is not necessary for most applications. You m
For example, to make a scroll event passive, it may be necessary to set the special zone.js flag in `src/index.html`:
```html
```angular-html
<!doctype html>
<html>
<head>
@@ -0,0 +1,105 @@
# Local template variables
Angular's `@let` syntax allows you to define a local variable and re-use it across the template.
IMPORTANT: the `@let` syntax is currently in [Developer Preview](/reference/releases#developer-preview).
## Syntax
`@let` declarations are similar to [JavaScript's `let`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let) and
their values can be any valid Angular expression. The expressions will be re-evaluated any time the
template is executed.
```angular-html
@let name = user.name;
@let greeting = 'Hello, ' + name;
@let data = data$ | async;
@let pi = 3.1459;
@let coordinates = {x: 50, y: 100};
@let longExpression = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit ' +
'sed do eiusmod tempor incididunt ut labore et dolore magna ' +
'Ut enim ad minim veniam...';
```
### Referencing the value of `@let`
Once you've declared the `@let`, you can reuse it anywhere in the template:
```angular-html
@let user = user$ | async;
@if (user) {
<h1>Hello, {{user.name}}</h1>
<user-avatar [photo]="user.photo"/>
<ul>
@for (snack of user.favoriteSnacks; track snack.id) {
<li>{{snack.name}}</li>
}
</ul>
<button (click)="update(user)">Update profile</button>
}
```
## Assignability
A key difference between `@let` and JavaScript's `let` is that `@let` cannot be re-assigned
within the template, however its value will be recomputed when Angular runs change detection.
```angular-html
@let value = 1;
<!-- Invalid -->
<button (click)="value = value + 1">Increment the value</button>
```
## Scope
`@let` declarations are scoped to the current view and its descendants. Since they are not
hoisted, they **cannot** be accessed by parent views or siblings:
```angular-html
@let topLevel = value;
<div>
@let insideDiv = value;
</div>
{{topLevel}} <!-- Valid -->
{{insideDiv}} <!-- Valid -->
@if (condition) {
{{topLevel + insideDiv}} <!-- Valid -->
@let nested = value;
@if (condition) {
{{topLevel + insideDiv + nested}} <!-- Valid -->
}
}
<div *ngIf="condition">
{{topLevel + insideDiv}} <!-- Valid -->
@let nestedNgIf = value;
<div *ngIf="condition">
{{topLevel + insideDiv + nestedNgIf}} <!-- Valid -->
</div>
</div>
{{nested}} <!-- Error, not hoisted from @if -->
{{nestedNgIf}} <!-- Error, not hoisted from *ngIf -->
```
## Syntax definition
The `@let` syntax is formally defined as:
* The `@let` keyword.
* Followed by one or more whitespaces, not including new lines.
* Followed by a valid JavaScript name and zero or more whitespaces.
* Followed by the = symbol and zero or more whitespaces.
* Followed by an Angular expression which can be multi-line.
* Terminated by the `;` symbol.
+16 -14
View File
@@ -32,17 +32,19 @@ For more information, see the [Security](best-practices/security) page.
You might also be interested in the following:
| Topics | Details |
| :------------------------------------------------------------------------ | :-------------------------------------------------------------------- |
| [Interpolation](guide/templates/interpolation) | Learn how to use interpolation and expressions in HTML. |
| [Template statements](guide/templates/template-statements) | Respond to events in your templates. |
| [Binding syntax](guide/templates/binding) | Use binding to coordinate values in your application. |
| [Property binding](guide/templates/property-binding) | Set properties of target elements or directive `@Input()` decorators. |
| [Attribute, class, and style bindings](guide/templates/attribute-binding) | Set the value of attributes, classes, and styles. |
| [Event binding](guide/templates/event-binding) | Listen for events and your HTML. |
| [Two-way binding](guide/templates/two-way-binding) | Share data between a class and its template. |
| [Built-in directives](guide/directives) | Listen to and modify the behavior and layout of HTML. |
| [Template reference variables](guide/templates/reference-variables) | Use special variables to reference a DOM element within a template. |
| [Inputs](guide/components/inputs) | Accepting data with input properties |
| [Outputs](guide/components/outputs) | Custom events with outputs |
| [SVG in templates](guide/templates/svg-in-templates) | Dynamically generate interactive graphics. |
| Topics | Details |
| :------------------------------------------------------------------------ | :---------------------------------------------------------------------------- |
| [Interpolation](guide/templates/interpolation) | Learn how to use interpolation and expressions in HTML. |
| [Template statements](guide/templates/template-statements) | Respond to events in your templates. |
| [Binding syntax](guide/templates/binding) | Use binding to coordinate values in your application. |
| [Property binding](guide/templates/property-binding) | Set properties of target elements or directive `@Input()` decorators. |
| [Attribute, class, and style bindings](guide/templates/attribute-binding) | Set the value of attributes, classes, and styles. |
| [Event binding](guide/templates/event-binding) | Listen for events and your HTML. |
| [Two-way binding](guide/templates/two-way-binding) | Share data between a class and its template. |
| [Control flow](guide/templates/control-flow) | Angular's syntax for conditionally showing, hiding, and repeating elements. |
| [Local template variables](guide/templates/let-template-variables) | Define and reuse variables in your template. |
| [Built-in directives](guide/directives) | Listen to and modify the behavior and layout of HTML. |
| [Template reference variables](guide/templates/reference-variables) | Use special variables to reference a DOM element within a template. |
| [Inputs](guide/components/inputs) | Accepting data with input properties |
| [Outputs](guide/components/outputs) | Custom events with outputs |
| [SVG in templates](guide/templates/svg-in-templates) | Dynamically generate interactive graphics. |
@@ -72,7 +72,7 @@ In this case, the `*ngIf` on `<span>` creates a new template scope, which includ
However, accessing a template variable from a child scope in the parent template doesn't work:
```html
```angular-html
<input *ngIf="true" #ref2 type="text" [(ngModel)]="secondExample" />
<span>Value: {{ ref2?.value }}</span> <!-- doesn't work -->
```
@@ -85,7 +85,7 @@ A _template input variable_ is a variable with a value that is set when an insta
Template input variables can be seen in action in the long-form usage of `NgFor`:
```html
```angular-html
<ul>
<ng-template ngFor let-hero [ngForOf]="heroes">
<li>{{hero.name}}
@@ -99,7 +99,7 @@ When an `<ng-template>` is instantiated, multiple named values can be passed whi
`NgFor` for example also provides access to the `index` of each hero in the array:
```html
```angular-html
<ul>
<ng-template ngFor let-hero let-i="index" [ngForOf]="heroes">
<li>Hero number {{i}}: {{hero.name}}
@@ -15,7 +15,7 @@ Two-way binding combines [property binding](guide/templates/property-binding) wi
Angular's two-way binding syntax is a combination of square brackets and parentheses, `[()]`.
The `[()]` syntax combines the brackets of property binding, `[]`, with the parentheses of event binding, `()`, as follows.
<docs-code header="src/app/app.component.html" path="adev/src/content/examples/two-way-binding/src/app/app.component.html" visibleRegion="two-way-syntax"/>
<docs-code header="src/app/app.component.html" path="adev/src/content/examples/two-way-binding/src/app/app.component.html" visibleRegion="two-way-syntax" language="angular-html"/>
## How two-way binding works
@@ -29,7 +29,7 @@ The `sizeChange` event is an `@Output()`, which lets data flow out of the `sizer
Next, there are two methods, `dec()` to decrease the font size and `inc()` to increase the font size.
These two methods use `resize()` to change the value of the `size` property within min/max value constraints, and to emit an event that conveys the new `size` value.
<docs-code header="src/app/sizer.component.ts" path="adev/src/content/examples/two-way-binding/src/app/sizer/sizer.component.ts" visibleRegion="sizer-component"/>
<docs-code header="src/app/sizer.component.ts" path="adev/src/content/examples/two-way-binding/src/app/sizer/sizer.component.ts" visibleRegion="sizer-component" language="angular-ts"/>
The `sizerComponent` template has two buttons that each bind the click event to the `inc()` and `dec()` methods.
When the user clicks one of the buttons, the `sizerComponent` calls the corresponding method.
@@ -9,69 +9,11 @@ Such tests require creating the component's host element in the browser DOM, as
The Angular `TestBed` facilitates this kind of testing as you'll see in the following sections.
But in many cases, *testing the component class alone*, without DOM involvement, can validate much of the component's behavior in a straightforward, more obvious way.
## Component class testing
Test a component class on its own as you would test a service class.
Component class testing should be kept very clean and simple.
It should test only a single unit.
At first glance, you should be able to understand what the test is testing.
Consider this `LightswitchComponent` which toggles a light on and off (represented by an on-screen message) when the user clicks the button.
<docs-code header="app/demo/demo.ts (LightswitchComp)" path="adev/src/content/examples/testing/src/app/demo/demo.ts" visibleRegion="LightswitchComp"/>
You might decide only to test that the `clicked()` method toggles the light's *on/off* state and sets the message appropriately.
This component class has no dependencies.
To test these types of classes, follow the same steps as you would for a service that has no dependencies:
1. Create a component using the new keyword.
1. Poke at its API.
1. Assert expectations on its public state.
<docs-code header="app/demo/demo.spec.ts (Lightswitch tests)" path="adev/src/content/examples/testing/src/app/demo/demo.spec.ts" visibleRegion="Lightswitch"/>
Here is the `DashboardHeroComponent` from the *Tour of Heroes* tutorial.
<docs-code header="app/dashboard/dashboard-hero.component.ts (component)" path="adev/src/content/examples/testing/src/app/dashboard/dashboard-hero.component.ts" visibleRegion="class"/>
It appears within the template of a parent component, which binds a *hero* to the `@Input` property and listens for an event raised through the *selected* `@Output` property.
You can test that the class code works without creating the `DashboardHeroComponent` or its parent component.
<docs-code header="app/dashboard/dashboard-hero.component.spec.ts (class tests)" path="adev/src/content/examples/testing/src/app/dashboard/dashboard-hero.component.spec.ts" visibleRegion="class-only"/>
When a component has dependencies, you might want to use the `TestBed` to both create the component and its dependencies.
The following `WelcomeComponent` depends on the `UserService` to know the name of the user to greet.
IMPORTANT: Remember to either *import* or *provide* each standalone component you want to test.
<docs-code header="app/welcome/welcome.component.ts" path="adev/src/content/examples/testing/src/app/welcome/welcome.component.ts" visibleRegion="class"/>
You might start by creating a mock of the `UserService` that meets the minimum needs of this component.
<docs-code header="app/welcome/welcome.component.spec.ts (MockUserService)" path="adev/src/content/examples/testing/src/app/welcome/welcome.component.spec.ts" visibleRegion="mock-user-service"/>
Then provide and inject *both the* **component** *and the service* in the `TestBed` configuration.
<docs-code header="app/welcome/welcome.component.spec.ts (class-only setup)" path="adev/src/content/examples/testing/src/app/welcome/welcome.component.spec.ts" visibleRegion="class-only-before-each"/>
Then exercise the component class, remembering to call the [lifecycle hook methods](guide/components/lifecycle) as Angular does when running the application.
<docs-code header="app/welcome/welcome.component.spec.ts (class-only tests)" path="adev/src/content/examples/testing/src/app/welcome/welcome.component.spec.ts" visibleRegion="class-only-tests"/>
## Component DOM testing
Testing the component *class* is as straightforward as [testing a service](guide/testing/services).
But a component is more than just its class.
A component is more than just its class.
A component interacts with the DOM and with other components.
The *class-only* tests can tell you about class behavior.
They cannot tell you if the component is going to render properly, respond to user input and gestures, or integrate with its parent and child components.
None of the preceding *class-only* tests can answer key questions about how the components actually behave on screen.
Classes alone cannot tell you if the component is going to render properly, respond to user input and gestures, or integrate with its parent and child components.
* Is `Lightswitch.clicked()` bound to anything such that the user can invoke it?
* Is the `Lightswitch.message` displayed?
@@ -37,15 +37,15 @@ expected '' to contain 'Test Tour of Heroes'.
Binding happens when Angular performs **change detection**.
In production, change detection kicks in automatically when Angular creates a component or the user enters a keystroke or an asynchronous activity \(for example, AJAX\) completes.
In production, change detection kicks in automatically when Angular creates a component or the user enters a keystroke, for example.
The `TestBed.createComponent` does *not* trigger change detection; a fact confirmed in the revised test:
The `TestBed.createComponent` does not trigger change detection by default; a fact confirmed in the revised test:
<docs-code path="adev/src/content/examples/testing/src/app/banner/banner.component.spec.ts" visibleRegion="test-w-o-detect-changes"/>
### `detectChanges()`
You must tell the `TestBed` to perform data binding by calling `fixture.detectChanges()`.
You can tell the `TestBed` to perform data binding by calling `fixture.detectChanges()`.
Only then does the `<h1>` have the expected title.
<docs-code path="adev/src/content/examples/testing/src/app/banner/banner.component.spec.ts" visibleRegion="expect-h1-default"/>
@@ -60,7 +60,7 @@ Here's another test that changes the component's `title` property *before* calli
### Automatic change detection
The `BannerComponent` tests frequently call `detectChanges`.
Some testers prefer that the Angular test environment run change detection automatically.
Many testers prefer that the Angular test environment run change detection automatically like it does in production.
That's possible by configuring the `TestBed` with the `ComponentFixtureAutoDetect` provider.
First import it from the testing utility library:
@@ -71,6 +71,10 @@ Then add it to the `providers` array of the testing module configuration:
<docs-code header="app/banner/banner.component.detect-changes.spec.ts (AutoDetect)" path="adev/src/content/examples/testing/src/app/banner/banner.component.detect-changes.spec.ts" visibleRegion="auto-detect"/>
HELPFUL: You can also use the `fixture.autoDetectChanges()` function instead if you only want to enable automatic change detection
after making updates to the state of the fixture's component. In addition, automatic change detection is on by default
when using `provideExperimentalZonelessChangeDetection` and turning it off is not recommended.
Here are three tests that illustrate how automatic change detection works.
<docs-code header="app/banner/banner.component.detect-changes.spec.ts (AutoDetect Tests)" path="adev/src/content/examples/testing/src/app/banner/banner.component.detect-changes.spec.ts" visibleRegion="auto-detect-tests"/>
@@ -78,24 +82,20 @@ Here are three tests that illustrate how automatic change detection works.
The first test shows the benefit of automatic change detection.
The second and third test reveal an important limitation.
The Angular testing environment does *not* know that the test changed the component's `title`.
The `ComponentFixtureAutoDetect` service responds to *asynchronous activities* such as promise resolution, timers, and DOM events.
But a direct, synchronous update of the component property is invisible.
The test must call `fixture.detectChanges()` manually to trigger another cycle of change detection.
The Angular testing environment does not run change detection synchronously when updates happen inside the test case that changed the component's `title`.
The test must call `await fixture.whenStable` to wait for another of change detection.
HELPFUL: Rather than wonder when the test fixture will or won't perform change detection, the samples in this guide *always call* `detectChanges()` *explicitly*.
There is no harm in calling `detectChanges()` more often than is strictly necessary.
HELPFUL: Angular does not know about direct updates to values that are not signals. The easiest way to ensure that
change detection will be scheduled is to use signals for values read in the template.
### Change an input value with `dispatchEvent()`
To simulate user input, find the input element and set its `value` property.
You will call `fixture.detectChanges()` to trigger Angular's change detection.
But there is an essential, intermediate step.
Angular doesn't know that you set the input element's `value` property.
It won't read that property until you raise the element's `input` event by calling `dispatchEvent()`.
*Then* you call `detectChanges()`.
The following example demonstrates the proper sequence.
@@ -138,32 +138,18 @@ It knows who the user is based on a property of the injected `UserService`:
<docs-code header="app/welcome/welcome.component.ts" path="adev/src/content/examples/testing/src/app/welcome/welcome.component.ts"/>
The `WelcomeComponent` has decision logic that interacts with the service, logic that makes this component worth testing.
Here's the testing module configuration for the spec file:
<docs-code header="app/welcome/welcome.component.spec.ts" path="adev/src/content/examples/testing/src/app/welcome/welcome.component.spec.ts" visibleRegion="config-test-module"/>
This time, in addition to declaring the *component-under-test*,
the configuration adds a `UserService` provider to the `providers` list.
But not the real `UserService`.
### Provide service test doubles
A *component-under-test* doesn't have to be injected with real services.
In fact, it is usually better if they are test doubles such as, stubs, fakes, spies, or mocks.
The purpose of the spec is to test the component, not the service, and real services can be trouble.
A *component-under-test* doesn't have to be provided with real services.
Injecting the real `UserService` could be a nightmare.
Injecting the real `UserService` could be difficult.
The real service might ask the user for login credentials and attempt to reach an authentication server.
These behaviors can be hard to intercept.
It is far easier and safer to create and register a test double in place of the real `UserService`.
This particular test suite supplies a minimal mock of the `UserService` that satisfies the needs of the `WelcomeComponent` and its tests:
<docs-code header="app/welcome/welcome.component.spec.ts" path="adev/src/content/examples/testing/src/app/welcome/welcome.component.spec.ts" visibleRegion="user-service-stub"/>
These behaviors can be hard to intercept. Be aware that using test doubles makes the test behave differently from production so use them sparingly.
### Get injected services
The tests need access to the stub `UserService` injected into the `WelcomeComponent`.
The tests need access to the `UserService` injected into the `WelcomeComponent`.
Angular has a hierarchical injection system.
There can be injectors at multiple levels, from the root injector created by the `TestBed` down through the component tree.
@@ -174,11 +160,11 @@ The component injector is a property of the fixture's `DebugElement`.
<docs-code header="WelcomeComponent's injector" path="adev/src/content/examples/testing/src/app/welcome/welcome.component.spec.ts" visibleRegion="injected-service"/>
HELPFUL: This is _usually_ not necessary. Services are often provided in the root or the TestBed overrides and can be retrieved more easily with `TestBed.inject()` (see below).
### `TestBed.inject()`
You *might* also be able to get the service from the root injector using `TestBed.inject()`.
This is easier to remember and less verbose.
But it only works when Angular injects the component with the service instance in the test's root injector.
This is easier to remember and less verbose than retrieving a service using the fixture's `DebugElement`.
In this test suite, the *only* provider of `UserService` is the root testing module, so it is safe to call `TestBed.inject()` as follows:
@@ -196,9 +182,9 @@ And here are some tests:
<docs-code header="app/welcome/welcome.component.spec.ts" path="adev/src/content/examples/testing/src/app/welcome/welcome.component.spec.ts" visibleRegion="tests"/>
The first is a sanity test; it confirms that the stubbed `UserService` is called and working.
The first is a sanity test; it confirms that the `UserService` is called and working.
HELPFUL: The second parameter to the Jasmine matcher \(for example, `'expected name'`\) is an optional failure label.
HELPFUL: The withContext function \(for example, `'expected name'`\) is an optional failure label.
If the expectation fails, Jasmine appends this label to the expectation failure message.
In a spec with multiple expectations, it can help clarify what went wrong and which expectation failed.
@@ -224,7 +210,6 @@ The `TwainComponent` gets quotes from an injected `TwainService`.
The component starts the returned `Observable` with a placeholder value \(`'...'`\), before the service can return its first quote.
The `catchError` intercepts service errors, prepares an error message, and returns the placeholder value on the success channel.
It must wait a tick to set the `errorMessage` in order to avoid updating that message twice in the same change detection cycle.
These are all features you'll want to test.
@@ -246,18 +231,8 @@ Unlike the real `getQuote()` method, this spy bypasses the server and returns a
You can write many useful tests with this spy, even though its `Observable` is synchronous.
### Synchronous tests
HELPFUL: It is best to limit the usage of spies to only what is necessary for the test. Creating mocks or spies for more than what's necessary can be brittle. As the component and injectable evolves, the unrelated tests can fail because they no longer mock enough behaviors that would otherwise not affect the test.
A key advantage of a synchronous `Observable` is that you can often turn asynchronous processes into synchronous tests.
<docs-code path="adev/src/content/examples/testing/src/app/twain/twain.component.spec.ts" visibleRegion="sync-test"/>
Because the spy result returns synchronously, the `getQuote()` method updates the message on screen immediately *after* the first change detection cycle during which Angular calls `ngOnInit`.
You're not so lucky when testing the error path.
Although the service spy will return an error synchronously, the component method calls `setTimeout()`.
The test must wait at least one full turn of the JavaScript engine before the value becomes available.
The test must become *asynchronous*.
### Async test with `fakeAsync()`
@@ -283,12 +258,16 @@ There is no nested syntax \(like a `Promise.then()`\) to disrupt the flow of con
HELPFUL: Limitation: The `fakeAsync()` function won't work if the test body makes an `XMLHttpRequest` \(XHR\) call.
XHR calls within a test are rare, but if you need to call XHR, see the [`waitForAsync()`](#waitForAsync) section.
IMPORTANT: Be aware that asynchronous tasks that happen inside the `fakeAsync` zone need to be manually executed with `flush` or `tick`. If you attempt to
wait for them to complete (i.e. using `fixture.whenStable`) without using the
`fakeAsync` test helpers to advance time, your test will likely fail. See below for more information.
### The `tick()` function
You do have to call [tick()](api/core/testing/tick) to advance the virtual clock.
Calling [tick()](api/core/testing/tick) simulates the passage of time until all pending asynchronous activities finish.
In this case, it waits for the error handler's `setTimeout()`.
In this case, it waits for the observable's `setTimeout()`.
The [tick()](api/core/testing/tick) function accepts `millis` and `tickOptions` as parameters. The `millis` parameter specifies how much the virtual clock advances and defaults to `0` if not provided.
For example, if you have a `setTimeout(fn, 100)` in a `fakeAsync()` test, you need to use `tick(100)` to trigger the fn callback.
@@ -415,21 +394,11 @@ Then call `detectChanges()` to tell Angular to update the screen.
Then you can assert that the quote element displays the expected text.
### Async test with `waitForAsync()`
### Async test without `fakeAsync()`
To use `waitForAsync()` functionality, you must import `zone.js/testing` in your test setup file.
If you created your project with the Angular CLI, `zone-testing` is already imported in `src/test.ts`.
Here's the previous `fakeAsync()` test, re-written with the `async`.
Here's the previous `fakeAsync()` test, re-written with the `waitForAsync()` utility.
<docs-code path="adev/src/content/examples/testing/src/app/twain/twain.component.spec.ts" visibleRegion="waitForAsync-test"/>
The `waitForAsync()` utility hides some asynchronous boilerplate by arranging for the tester's code to run in a special *async test zone*.
You don't need to pass Jasmine's `done()` into the test and call `done()` because it is `undefined` in promise or observable callbacks.
But the test's asynchronous nature is revealed by the call to `fixture.whenStable()`, which breaks the linear flow of control.
When using an `intervalTimer()` such as `setInterval()` in `waitForAsync()`, remember to cancel the timer with `clearInterval()` after the test, otherwise the `waitForAsync()` never ends.
<docs-code path="adev/src/content/examples/testing/src/app/twain/twain.component.spec.ts" visibleRegion="async-test"/>
### `whenStable`
@@ -439,104 +408,6 @@ Instead of calling [tick()](api/core/testing/tick), it calls `fixture.whenStable
The `fixture.whenStable()` returns a promise that resolves when the JavaScript engine's task queue becomes empty.
In this example, the task queue becomes empty when the observable emits the first quote.
The test resumes within the promise callback, which calls `detectChanges()` to update the quote element with the expected text.
### Jasmine `done()`
While the `waitForAsync()` and `fakeAsync()` functions greatly simplify Angular asynchronous testing, you can still fall back to the traditional technique and pass `it` a function that takes a [`done` callback](https://jasmine.github.io/2.0/introduction.html#section-Asynchronous_Support).
You can't call `done()` in `waitForAsync()` or `fakeAsync()` functions, because the `done parameter` is `undefined`.
Now you are responsible for chaining promises, handling errors, and calling `done()` at the appropriate moments.
Writing test functions with `done()`, is more cumbersome than `waitForAsync()`and `fakeAsync()`, but it is occasionally necessary when code involves the `intervalTimer()` like `setInterval`.
Here are two more versions of the previous test, written with `done()`.
The first one subscribes to the `Observable` exposed to the template by the component's `quote` property.
<docs-code path="adev/src/content/examples/testing/src/app/twain/twain.component.spec.ts" visibleRegion="quote-done-test"/>
The RxJS `last()` operator emits the observable's last value before completing, which will be the test quote.
The `subscribe` callback calls `detectChanges()` to update the quote element with the test quote, in the same manner as the earlier tests.
In some tests, you're more interested in how an injected service method was called and what values it returned, than what appears on screen.
A service spy, such as the `qetQuote()` spy of the fake `TwainService`, can give you that information and make assertions about the state of the view.
<docs-code path="adev/src/content/examples/testing/src/app/twain/twain.component.spec.ts" visibleRegion="spy-done-test"/>
## Component marble tests
The previous `TwainComponent` tests simulated an asynchronous observable response from the `TwainService` with the `asyncData` and `asyncError` utilities.
These are short, simple functions that you can write yourself.
Unfortunately, they're too simple for many common scenarios.
An observable often emits multiple times, perhaps after a significant delay.
A component might coordinate multiple observables with overlapping sequences of values and errors.
**RxJS marble testing** is a great way to test observable scenarios, both simple and complex.
You've likely seen the [marble diagrams](https://rxmarbles.com) that illustrate how observables work.
Marble testing uses a similar marble language to specify the observable streams and expectations in your tests.
The following examples revisit two of the `TwainComponent` tests with marble testing.
Start by installing the `jasmine-marbles` npm package.
Then import the symbols you need.
<docs-code header="app/twain/twain.component.marbles.spec.ts (import marbles)" path="adev/src/content/examples/testing/src/app/twain/twain.component.marbles.spec.ts" visibleRegion="import-marbles"/>
Here's the complete test for getting a quote:
<docs-code path="adev/src/content/examples/testing/src/app/twain/twain.component.marbles.spec.ts" visibleRegion="get-quote-test"/>
Notice that the Jasmine test is synchronous.
There's no `fakeAsync()`.
Marble testing uses a test scheduler to simulate the passage of time in a synchronous test.
The beauty of marble testing is in the visual definition of the observable streams.
This test defines a [*cold* observable](#learn-about-marble-testing) that waits three [frames](#learn-about-marble-testing) \(`---`\), emits a value \(`x`\), and completes \(`|`\).
In the second argument you map the value marker \(`x`\) to the emitted value \(`testQuote`\).
<docs-code path="adev/src/content/examples/testing/src/app/twain/twain.component.marbles.spec.ts" visibleRegion="test-quote-marbles"/>
The marble library constructs the corresponding observable, which the test sets as the `getQuote` spy's return value.
When you're ready to activate the marble observables, you tell the `TestScheduler` to *flush* its queue of prepared tasks like this.
<docs-code path="adev/src/content/examples/testing/src/app/twain/twain.component.marbles.spec.ts" visibleRegion="test-scheduler-flush"/>
This step serves a purpose analogous to [tick()](api/core/testing/tick) and `whenStable()` in the earlier `fakeAsync()` and `waitForAsync()` examples.
The balance of the test is the same as those examples.
### Marble error testing
Here's the marble testing version of the `getQuote()` error test.
<docs-code path="adev/src/content/examples/testing/src/app/twain/twain.component.marbles.spec.ts" visibleRegion="error-test"/>
It's still an async test, calling `fakeAsync()` and [tick()](api/core/testing/tick), because the component itself calls `setTimeout()` when processing errors.
Look at the marble observable definition.
<docs-code path="adev/src/content/examples/testing/src/app/twain/twain.component.marbles.spec.ts" visibleRegion="error-marbles"/>
This is a *cold* observable that waits three frames and then emits an error, the hash \(`#`\) character indicates the timing of the error that is specified in the third argument.
The second argument is null because the observable never emits a value.
### Learn about marble testing
A *marble frame* is a virtual unit of testing time.
Each symbol \(`-`, `x`, `|`, `#`\) marks the passing of one frame.
A *cold* observable doesn't produce values until you subscribe to it.
Most of your application observables are cold.
All [*HttpClient*](guide/http) methods return cold observables.
A *hot* observable is already producing values *before* you subscribe to it.
The [`Router.events`](api/router/Router#events) observable, which reports router activity, is a *hot* observable.
RxJS marble testing is a rich subject, beyond the scope of this guide.
Learn about it on the web, starting with the [official documentation](https://rxjs.dev/guide/testing/marble-testing).
## Component with inputs and outputs
@@ -564,22 +435,12 @@ While testing a component this simple has little intrinsic value, it's worth kno
Use one of these approaches:
* Test it as used by `DashboardComponent`
* Test it as a stand-alone component
* Test it as a standalone component
* Test it as used by a substitute for `DashboardComponent`
A quick look at the `DashboardComponent` constructor discourages the first approach:
<docs-code header="app/dashboard/dashboard.component.ts (constructor)" path="adev/src/content/examples/testing/src/app/dashboard/dashboard.component.ts" visibleRegion="ctor"/>
The `DashboardComponent` depends on the Angular router and the `HeroService`.
You'd probably have to replace them both with test doubles, which is a lot of work.
The router seems particularly challenging.
HELPFUL: The [following discussion](#routing-component) covers testing components that require the router.
The immediate goal is to test the `DashboardHeroComponent`, not the `DashboardComponent`, so, try the second and third options.
### Test `DashboardHeroComponent` stand-alone
### Test `DashboardHeroComponent` standalone
Here's the meat of the spec file setup.
@@ -593,8 +454,6 @@ The following test verifies that the hero name is propagated to the template usi
Because the [template](#dashboard-hero-component) passes the hero name through the Angular `UpperCasePipe`, the test must match the element value with the upper-cased name.
HELPFUL: This small test demonstrates how Angular tests can verify a component's visual representation —something not possible with [component class tests](guide/testing/components-basics#component-class-testing)— at low cost and without resorting to much slower and more complicated end-to-end tests.
### Clicking
Clicking the hero should raise a `selected` event that the host component \(`DashboardComponent` presumably\) can hear:
@@ -656,6 +515,7 @@ Here's the previous test, rewritten using the click helper.
<docs-code header="app/dashboard/dashboard-hero.component.spec.ts (test with click helper)" path="adev/src/content/examples/testing/src/app/dashboard/dashboard-hero.component.spec.ts" visibleRegion="click-test-3"/>
<!-- TODO(atscott): Guide above this line updated on 06/11/2024. Continue updating sections below. -->
## Component inside a test host
The previous tests played the role of the host `DashboardComponent` themselves.
@@ -1066,7 +926,7 @@ Fortunately, the `HeroDetailService` delegates responsibility for remote data ac
<docs-code header="app/hero/hero-detail.service.ts (prototype)" path="adev/src/content/examples/testing/src/app/hero/hero-detail.service.ts" visibleRegion="prototype"/>
The [previous test configuration](#feature-module-import) replaces the real `HeroService` with a `TestHeroService` that intercepts server requests and fakes their responses.
The [previous test configuration](#import-a-feature-module) replaces the real `HeroService` with a `TestHeroService` that intercepts server requests and fakes their responses.
What if you aren't so lucky.
What if faking the `HeroService` is hard?
+1 -1
View File
@@ -42,7 +42,7 @@ These notifications include:
### `OnPush`-compatible components
One way to ensure that a component is using the correct notification mechanisms from above is to
use [ChangeDetectionStrategy.OnPush](../best-practices/skipping-subtrees#using-onpush).
use [ChangeDetectionStrategy.OnPush](/adev/src/content/best-practices/runtime-performance/skipping-subtrees#using-onpush).
The `OnPush` change detection strategy is not required, but it is a recommended step towards zoneless compatibility for application components. It is not always possible for library components to use `ChangeDetectionStrategy.OnPush`.
When a library component is a host for user-components which might use `ChangeDetectionStratey.Default`, it cannot use `OnPush` because that would prevent the child component from being refreshed if it is not `OnPush` compatible and relies on ZoneJS to trigger change detection. Components can use the `Default` strategy as long as they notify Angular when change detection needs to run (calling `markForCheck`, using signals, `AsyncPipe`, etc.).
@@ -87,7 +87,7 @@ export class TodoListItem {
}
```
```html
```angular-html
<!-- todo-list-item.component.html -->
<li>(TODO) Read Angular Essentials Guide</li>
```
@@ -68,7 +68,7 @@ Another common scenario developers encounter is the need to render a list of ite
Similar to JavaScript’s `for...of` loops, Angular provides the `@for` block for rendering repeated elements.
```html
```angular-html
<!-- ingredient-list.component.html -->
<ul>
@for (ingredient of ingredientList; track ingredient.name) {
@@ -11,7 +11,7 @@ You can add an event handler to an element by:
1. Adding an attribute with the events name inside of parentheses
2. Specify what JavaScript statement you want to run when it fires
```html
```angular-html
<button (click)="save()">Save</button>
```
@@ -45,7 +45,7 @@ Other common examples of event listeners include:
If you need to access the [event](https://developer.mozilla.org/docs/Web/API/Event) object, Angular provides an implicit `$event` variable that you can pass to a function:
```html
```angular-html
<button (click)="createUser($event)">Submit</button>
```

Some files were not shown because too many files have changed in this diff Show More