Compare commits

...

180 Commits

Author SHA1 Message Date
Jessica Janiuk 147ea13d59 release: cut the v21.0.9 release 2026-01-14 11:30:17 -08:00
Matthieu Riegler 79273c18c0 Revert "docs(docs-infra): use Signals Forms focusBoundControl"
This reverts commit 79e6dfdef3.
2026-01-14 09:19:35 -08:00
SkyZeroZx d710ebee6d docs(docs-infra): Exempts animation-related symbols from linking
Exempts 'animation', 'transition', and 'trigger' symbols from automatic linking.

(cherry picked from commit 724e917f7a)
2026-01-14 16:24:52 +00:00
Shuaib Hasan Akib b0b341f440 docs: add IMPORTANT prefix to pipe usage warning and
Adds an IMPORTANT label to emphasize the guidance about avoiding impure
pipes due to potential performance impact.

(cherry picked from commit c9f584b918)
2026-01-14 16:11:03 +00:00
Jessica Janiuk a0361b3ae2 Revert "refactor(core): remove ng when invoking enableProdMode"
This reverts commit cb5879f566.

(cherry picked from commit e673eb6df2)
2026-01-14 16:09:02 +00:00
SkyZeroZx 79e6dfdef3 docs(docs-infra): use Signals Forms focusBoundControl
Removes direct `ViewChild` access from `TextField` components and relies on
Signals Forms via the `focusBoundControl`  to manage focus.

(cherry picked from commit 074666b2e8)
2026-01-14 16:00:49 +00:00
Alan Agius 16a23a5fe8 fix(docs-infra): remove trailing slash from sitemap URLs
Fixes an issue where a trailing slash was sometimes added to the end of URLs in the sitemap, causing unnecessary redirects.

Example of problematic URL:
```xml
<loc>https://angular.dev/</loc>
```

(cherry picked from commit 710a4bf02b)
2026-01-14 15:57:12 +00:00
Andrew Scott de9277b174 build: Update vscode release script
- Ensure there is a GITHUB_TOKEN environment variable at the start so we can push the release
- More robust handling for finding releaser's fork if it's not 'origin'

(cherry picked from commit 227acddd3a)
2026-01-13 21:08:29 +00:00
Matthieu Riegler 492b10686b refactor(core): remove ng when invoking enableProdMode
This was caught while investigating a leak.

(cherry picked from commit cb5879f566)
2026-01-13 19:39:52 +00:00
Andrew Scott 99b10e45d2 build: update cherrypick changelog to stash
there are often some formatting/newline changes caused by other steps. just stash anything

(cherry picked from commit 029685bd93)
2026-01-13 19:20:19 +00:00
Jeremy Elbourn 01e5babdf6 docs: update OnChanges API reference w/ signal input
We had some reports of LLMs claiming that `ngOnChanges` does not include
changes for signal-based inputs. This is wrong (and the LLMs just made
it up), but we can update the docs at least to demonstrate `ngOnChanges`
with signal-based inputs.

Previously, the API reference used a real test as a code example, but
updating the test to a signal-based input is more involved than this
change needs to call for.

(cherry picked from commit 871fa1b2dd)
2026-01-13 17:04:27 +00:00
Angular Robot 8f4cc8488c docs: update cross-repo adev docs
Updated Angular adev cross repo docs files.
2026-01-13 08:43:40 -08:00
lyutails 33de839fe8 docs: update hierarchical-dependency-injection.md
VillainsService highly likely misspelled and then written as VillainService with 's' missing

(cherry picked from commit 910768f5cc)
2026-01-13 16:31:45 +00:00
Alan Agius 30742c59ef ci: update Renovate configuration to replace Bazel module dependency command
This uses `ng-dev misc sync-module-bazel`, add `MODULE.bazel` to file filters, and remove specific dependency name matching.

(cherry picked from commit 6e82167481)
2026-01-13 16:28:47 +00:00
Andrew Scott 6e8f1421a6 build: improve changelog generation and tag fetching in release script
- Fetch tags explicitly to ensure local availability.
- Implement `getPreviousTag` to reliably determine the base for changelog generation, falling back to the latest `vsix-*` tag if the specific previous version tag is missing.
- Filter changelog commits by subject to exclude duplicates (e.g. cherry-picks) that are already present in the previous release history but have different hashes.

(cherry picked from commit 69da6390b1)
2026-01-13 00:06:38 +00:00
Andrew Scott d5caac6bba build: fix changelog generation for last rev
updates changelog generation to work when releasing the first final
version.

(cherry picked from commit 78755771e1)
2026-01-12 22:53:23 +00:00
Matthieu Riegler 82d556a8fb fix(forms): Ensure the control instruction comes after the other bindings
Prior to this change, binding to radio value was sensitive to the order in which `value` & `formField` where binding in the template.
The compiler change makes that order non-important.

fixes #66402

(cherry picked from commit 65fa5b5439)
2026-01-12 21:49:23 +00:00
cexbrayat 5cbb66fdf2 test(forms): improve test coverage for multiple pattern validators
Add comprehensive test cases to validate behavior when multiple
pattern validators are applied to the same field.

These tests clarify that multiple patterns operate with AND logic,
where each pattern is validated independently and produces its own
error when it doesn't match.

(cherry picked from commit 7fd076bdd3)
2026-01-12 21:48:19 +00:00
Shuaib Hasan Akib 6a9d4d5458 docs: replace old https://angular.io/license with https://angular.dev/license
(cherry picked from commit 1b4dcc01ff)
2026-01-12 21:41:34 +00:00
Mikhail Efanov 22d27ed792 build: update dev container configuration
build: update dev container configuration

build: update dev container configuration

Update dev container configuration files to support latest environment.
- Use customizations.vscode structure instead of deprecated extensions property.
- Update bazel extension to BazelBuild.vscode-bazel.
- Ignore .pnpm-store directory.
- Update ports list
- Use forwardPorts instead appPort

Document the usage of Dev Containers in building-and-testing-angular.md.

(cherry picked from commit 27276374ab)
2026-01-12 21:39:33 +00:00
Shuaib Hasan Akib b395a252cc fix(docs-infra): use margin instead of padding to prevent heading hover overflow
Replaced padding-block-start with margin-block-start on
.docs-reference-section-heading to prevent anchor hover and click areas from extending into empty space above the heading. This ensures hover behavior aligns with the visible text.

(cherry picked from commit f53f878e0f)
2026-01-12 21:38:50 +00:00
Shuaib Hasan Akib bb34ccb14d fix(docs-infra): prevent duplicate description rendering for block API entries
Block entries (@if, @defer, @for,@let, @switch) were falling back to the generic
DocsReference template, causing the description to appear twice - once in
the header section and once in the main content area.

This commit adds a dedicated rendering path for block entries:
- Creates BlockEntryRenderable type and associated transforms
- Adds BlockReference template that uses RawHtml directly
- Modifies HeaderApi to accept hideDescription prop
- Updates processing and rendering pipelines to handle blocks

The fix ensures block documentation displays only one description section
while preserving the existing behavior for all other API entry types.

Update adev/shared-docs/pipeline/api-gen/rendering/transforms/block-transforms.mts

Co-authored-by: Matthieu Riegler <kyro38@gmail.com>
(cherry picked from commit 8bdd98ef41)
2026-01-12 21:37:57 +00:00
Roshith dc9f59b9be docs: fix spelling mistake in documentation
correct typo where "youself" was written instead of "yourself"

(cherry picked from commit 933ca9e5dc)
2026-01-12 21:36:59 +00:00
Joey Perrott 5f935eee58 build: begin requring usage of release mode for releases
Start requiring the usage of the release merge-mode for allowing releases to be made from the repository

(cherry picked from commit 4d6a6aafee)
2026-01-12 21:36:22 +00:00
SkyZeroZx 79023440fb docs: remove Component suffix in animations, HTTP, and SSR guides
Removes the unnecessary `Component` suffix from examples.
Also fixes button styling in the animations examples, which previously did not render correctly in dark mode.

(cherry picked from commit 511d9ca9d2)
2026-01-12 18:07:09 +00:00
Shuaib Hasan Akib 007d790018 docs: correct line number highlighting in query examples
Updates incorrect highlighted line numbers so they match the intended
code being referenced in the documentation.

(cherry picked from commit c63bc58e0f)
2026-01-12 18:04:17 +00:00
SkyZeroZx a1dea3e736 refactor(http): remove redundant providedIn: 'root' in XSRF_HEADER_NAME
Removes unnecessary `providedIn: 'root'` declarations from injection token

(cherry picked from commit e38c1bf743)
2026-01-12 18:00:29 +00:00
SkyZeroZx e72e136096 refactor(core): Add missing OnDestroy to zone and zoneless change detection schedulers
Implements the OnDestroy interface in NgZoneChangeDetectionScheduler and ChangeDetectionSchedulerImpl

(cherry picked from commit fa6e82b569)
2026-01-12 18:00:28 +00:00
Matthieu Riegler 74e99fa389 docs(docs-infra): prevent from showing duplicate usage blocks in functions API docs
fixes #66398

(cherry picked from commit b22835579e)
2026-01-12 17:50:15 +00:00
Matthieu Riegler 08d0ac73b1 docs(docs-infra): Show API decorators docs
fixes #66129

(cherry picked from commit a056413520)
2026-01-12 17:41:28 +00:00
Matthieu Riegler 8655913dcc docs(docs-infra): Add community russian translation
fixes #65720

(cherry picked from commit 33b2d9905d)
2026-01-12 17:40:11 +00:00
Matthieu Riegler 77226dd585 docs: change language section to "community translations"
(cherry picked from commit ad699cebb5)
2026-01-12 17:30:22 +00:00
omar-almasry11 49e18e5582 fix(docs-infra): improve symbolic green contrast for WCAG compliance
Adjusts the symbolic green color to meet WCAG contrast requirements in documentation styles.

(cherry picked from commit 405b05607c)
2026-01-12 16:50:37 +00:00
Jeevan Mahesha dfd8c5220e docs: update testing guide to include Vitest and improve authGuard test setup
(cherry picked from commit 66b40d4fb3)
2026-01-12 16:32:14 +00:00
Matthieu Riegler 5b0e705eb4 docs: improve separation between enter and leave example
fixes #66359

(cherry picked from commit 7f1b790f84)
2026-01-12 16:18:19 +00:00
Kristiyan Kostadinov 89a51c8415 build: update license for vscode extension
The vscode extension still has the old license headers pointing pointing to AIO.

(cherry picked from commit f0dba6deb9)
2026-01-12 16:17:42 +00:00
aparziale 1efd5fa33f docs(docs-infra): Angular Aria listbox shows placeholder for "Selection modes"
Improved documentation for the aria listbox component. Added an example comparing selectionMode input.

Fixes: #65682
(cherry picked from commit e7f24014cf)
2026-01-12 16:17:16 +00:00
SkyZeroZx 615a77b685 refactor(compiler-cli): update error message links to versioned docs (#66374)
Error message links now point to the archived documentation site (v*.angular.dev)
so that referenced content matches the framework version in use.

See angular#44650

PR Close #66374
2026-01-09 22:33:52 +00:00
SkyZeroZx 450b4c1e90 refactor(core): update error message links to versioned docs (#66374)
Error message links now point to the archived documentation site (v*.angular.dev)
so that referenced content matches the framework version in use.

See angular#44650

PR Close #66374
2026-01-09 22:33:51 +00:00
Miles Malerba 0055f3cc79 fix(forms): Rename signal form [field] to [formField]
This completes the rename started in #66136. `[field]` is too generic of
a selector for the forms system to own, and likely to cause naming
collisions with existing components. Therefore it is being renamed to
`[formField]`

(cherry picked from commit 5671f2cc07)
2026-01-09 22:33:13 +00:00
tomer953 e4bfa5c9e7 fix(migrations): prevent duplicate imports in common-to-standalone migration
The common-to-standalone migration did not check for existing imports
when adding needed imports after removing CommonModule. This change adds deduplication
logic to filter out imports that already exist in the imports array
before adding them, ensuring each import appears only once.

(cherry picked from commit 008e20ca56)
2026-01-09 22:30:52 +00:00
Alan Agius 5bc072ea3e ci: add pnpm and typescript exclusions to a Renovate package rule.
This will be handled in dev-infra preset, see: https://github.com/angular/dev-infra/pull/3340

(cherry picked from commit 99db2e9065)
2026-01-09 18:41:58 +00:00
fisker 018edd5566 refactor(compiler): switch Binary.isAssignmentOperation to type guard function
Improve `Binary.isAssignmentOperation` types

(cherry picked from commit 4fb24cadee)
2026-01-09 17:59:32 +00:00
SkyZeroZx a6efb7752d docs(docs-infra): avoid persistent AbortSignal listener in wait debounce in search
Cleans up the abort listener once the timeout resolves to prevent it from
remaining attached longer than necessary.

(cherry picked from commit 94b707957a)
2026-01-09 17:38:11 +00:00
Yahya-Almubarak a63f6d0978 docs: replace UnambiguousRoleValidator in asyc-valdotors part form-validation.md
The async validators part of document uses uniqueRoleValidator and not UnambiguousRoleValidator.

(cherry picked from commit e3b3290998)
2026-01-09 17:26:22 +00:00
Devin Chasanoff 5b0c2b3272 docs: add ai tutor signal forms docs
(cherry picked from commit 09234753e1)
2026-01-09 17:25:43 +00:00
kirjs 24fa4a2460 docs(forms): Clarify returning errors from submit functions
Update outdate comment, and add a section to the docs

(cherry picked from commit 7d985ce08e)
2026-01-09 16:47:46 +00:00
fisker 3033568bb4 refactor(compiler): tighten Unary.operator type
Improve `Unary.operator` type

(cherry picked from commit a7e470a1b3)
2026-01-09 16:47:04 +00:00
fisker 418326839a refactor: _ParseAST.isAssignmentOperator to type guard
Update `_ParseAST.isAssignmentOperator` to type guard

(cherry picked from commit 4d19408e9b)
2026-01-09 16:45:39 +00:00
fisker 9b97a1dfa3 refactor(compiler): tighten Binary.operation type
Improve `Binary.operation` types

(cherry picked from commit 07a08ab9f8)
2026-01-09 16:45:39 +00:00
SkyZeroZx 491a1569ad docs(docs-infra): improve label fallback logic in search results
Improves label fallback in search results by providing a fallback to the top-level category when a more specific label is missing.

(cherry picked from commit 3a65814f53)
2026-01-09 15:58:36 +00:00
Matthieu Riegler baee12d944 Revert "docs: remove incorrectly listed operators in "Supported operators" table (Expression Syntax Guide) (#62092)"
This reverts commit ea71a1e8cd.

(cherry picked from commit 80a7419a16)
2026-01-09 15:47:52 +00:00
Andrew Scott 696adbca5f refactor(core): Use the provided Document value rather than global in FakeNavigation
This commit ensures the Document used by `FakeNavigation` is the one
passed in the constructor rather than the global `document`, which may
be different.

(cherry picked from commit 49295778bc)
2026-01-08 21:28:23 +00:00
Jessica Janiuk a0904c8a5a release: cut the v21.0.8 release 2026-01-08 12:42:42 -08:00
Andrew Scott 5239e471a1 fix(core): handle cancelled traversals in fake navigation
Update fake navigation implementation to correctly handle traversals that are cancelled (e.g. by a precommit handler).
Ensure prospective index is calculated correctly so that subsequent traversals target the correct entry.
Add regression test for cancelled traversals.

(cherry picked from commit 86dc1283e8)
2026-01-08 19:27:41 +00:00
omar-almasry11 8ff277612e fix(docs-infra): improve dark mode button hover contrast for WCAG compliance
The .docs-primary-btn hover state in dark mode had a contrast ratio of 2.18:1,
failing WCAG 2.1 SC 1.4.3. This change reuses the existing
--hot-pink-to-electric-violet-radial-gradient to achieve 4.5:1+ contrast.

(cherry picked from commit ad49d489ae)
2026-01-08 19:21:27 +00:00
Angular Robot 0dead0fbac build: update cross-repo angular dependencies
See associated pull request for more information.
2026-01-08 11:11:37 -08:00
zakaria-bali 2b830917d8 docs(forms): make onSubmit method async for form submission
Change onSubmit method to be asynchronous since we are using await inside it

(cherry picked from commit bd5997cfeb)
2026-01-08 18:03:28 +00:00
lyutails e0a8a4d4f9 docs: fix typo in effect.md
(cherry picked from commit 8fad6155b2)
2026-01-08 18:02:55 +00:00
aparziale 32ff1a0d12 docs(docs-infra): Fix toggle button in dark mode
Fix visibility toggle button in dark mode

Fix #66049

(cherry picked from commit 71149ef57a)
2026-01-08 17:07:59 +00:00
Angular Robot 8aab227960 build: update cross-repo angular dependencies
See associated pull request for more information.
2026-01-08 08:41:59 -08:00
Shuaib Hasan Akib 1488c2e30b docs: fix broken anchor link for Angular versioning section
Updates the release documentation link to point to the correct `#angular-versioning` anchor so readers are directed to the intended

(cherry picked from commit f727f8e655)
2026-01-08 16:35:34 +00:00
Matthieu Riegler 37dcc26727 docs: add release schedule
(cherry picked from commit 8bc688b56f)
2026-01-08 16:34:51 +00:00
Jaime Burgos f5bad8e417 refactor(core): Improve tree-shakable RuntimeError error for INVALID_FIELD_DIRECTIVE_HOST
Improves the tree-shakable runtime error handling for `INVALID_FIELD_DIRECTIVE_HOST`.

(cherry picked from commit 5c75d29fb4)
2026-01-07 22:32:29 +00:00
Andrew Scott ecc5c27b17 fix(vscode-extension): convert enum member kind in completions correctly
This converts enum member kinds to the correct completion kind for displa in the completions popup

(cherry picked from commit 50674f8c28)
2026-01-07 22:30:08 +00:00
Jessica Janiuk a6a2621bf9 fix(core): fix memory leak with event replay
This ensures event replay does not hold on to elements after an application has been destroyed.

fixes: #59261
(cherry picked from commit 30e9c62bdf)
2026-01-07 22:27:47 +00:00
kirjs 8fd585cc0b release: cut the v21.0.7 release 2026-01-07 16:45:50 -05:00
Leon Senft ff531752b0 refactor(forms): remove customError()
Remove the `customError` function and `CustomValidationError` type.

These were made obsolete by support for returning plain object literals
as custom errors.

This also catches few `field` properties that were missed in the
renaming to `fieldTree`.

(cherry picked from commit 46dbd18566)
2026-01-07 15:07:35 -05:00
arturovt 5e9e09aee0 fix(router): handle errors from view transition updateCallbackDone promise
Firefox-specific workaround: Firefox 144+ creates the `updateCallbackDone` promise
internally during error handling (e.g., duplicate view-transition-name), even before
the property is accessed. This can cause unhandled promise rejections to appear in
error tracking tools like Rollbar/Sentry. We must attach catch handlers to all three
promises immediately to prevent these unhandled rejections.
See: https://bugzilla.mozilla.org/show_bug.cgi?id=1999336 (if applicable)
Spec: https://drafts.csswg.org/css-view-transitions-1/#dom-viewtransition-updatecallbackdone

(cherry picked from commit 9e043decaf)
2026-01-07 15:06:02 -05:00
Kristiyan Kostadinov fbb37d8388 refactor(core): remove unused instruction parameter
The `thisArg` in pure functions isn't used so we can drop it. We still need to keep it on the underlying implementation, because pipe instructions rely on it.

(cherry picked from commit 4dc5ae54a9)
2026-01-07 15:05:24 -05:00
SkyZeroZx c3adc5a707 docs(docs-infra): Fix combobox dialog styles for dark theme
Fixes combobox dialog styles to ensure correct text color and contrast in dark theme.
This improves readability and visual consistency when using the docs in dark mode.

(cherry picked from commit 090484c853)
2026-01-07 14:19:35 -05:00
SkyZeroZx 8eb0cd51de docs: enhance programmatic rendering guide by covering ngComponentOutlet options
(cherry picked from commit f9e9fc4ffe)
2026-01-07 14:18:37 -05:00
Shuaib Hasan Akib 0b26947802 docs: standardize component decorator placeholders in markdown
Updates `@Component({ ... })` to `@Component({/*...*/})` across
documentation examples for consistency and clearer formatting.

(cherry picked from commit d129d201c1)
2026-01-07 14:17:42 -05:00
SkyZeroZx 35fe58f7da docs(vscode-extension): update angular.io links to angular.dev in messages
(cherry picked from commit 8c8c5b24a1)
2026-01-07 14:12:19 -05:00
SkyZeroZx 8aa77557f1 docs: update angular.io links to angular.dev in comments, TSDoc, and warnings
(cherry picked from commit b2f417585a)
2026-01-07 14:12:18 -05:00
Andrew Scott 41cd4a6af8 fix(router): Fix RouterLink href not updating with queryParamsHandling
There was a bug introduced in #60875. While RouterLink doesn't
necessarily depend on Router state, its link can change when navigations
cause the `ActivatedRoute` paths to change. It is difficult to determine
which segments the link depends on. Luckily, the commit had flawed
logic:

```
!this.queryParamsHandling && !dependsOnRouterState(this.options?.defaultQueryParamsHandling);
```

The subscription gets created whenever `queryParamsHandling` was not
defined, or it was non-default. This is pretty much all scenarios since
nobody is likely to set it explicitly to the default value. In addition,
the `click` handler recomputes the tree because `urlTree` is a getter
that does the computation.

This commit effectively rolls back #60875

(cherry picked from commit bcef77d950)
2026-01-07 14:08:17 -05:00
SkyZeroZx ae1dc16bb0 fix(forms): clean up abort listener after timeout
Removes the abort event listener once the debounce timeout completes.

This avoids lingering listeners, prevents potential memory leaks, and ensures
the abort logic runs at most once.

(cherry picked from commit e7d99f02cb)
2026-01-07 14:07:28 -05:00
Kristiyan Kostadinov 63b1cdcf70 fix(compiler): produce accurate span for typeof and void expressions
Fixes that the `typeof` and `void` expressions were starting their spans from the expression start, rather than the keyword.

Fixes #66174.

(cherry picked from commit 3a56c1367f)
2026-01-07 14:04:09 -05:00
SkyZeroZx c3f3ebec8f docs: remove Component suffix from examples in DI & Drag and Drop
Updates documentation examples to remove the `Component` suffix, aligning them with the current Angular Style Guide.

(cherry picked from commit 0d6ac92ccb)
2026-01-07 13:59:20 -05:00
Angular Robot 61370dc8c8 docs: update cross-repo adev docs
Updated Angular adev cross repo docs files.
2026-01-07 13:34:27 -05:00
Kristiyan Kostadinov 7c74674eb0 fix(core): avoid leaking view data in animations
The animations code currently tracks which views have running leave animations by adding them to a `Set`. This can leak memory if we don't clean something up on time.

These changes switch to tracking the views by their ID which doesn't risk retaining the view.

Fixes #66255.

(cherry picked from commit 80b0fbba1f)
2026-01-07 12:39:04 -05:00
Matthieu Riegler 7738eb94cb docs(docs-infra): Use category for signal forms guides
(cherry picked from commit b17f93b6d1)
2026-01-07 09:35:22 -05:00
hawkgs c1e9627f6d docs(core): fix Ivy view data docs tables
The current formatting prevents the tables to be rendered making it hard to read them.

(cherry picked from commit 51ed487fae)
2026-01-07 09:22:47 -05:00
SkyZeroZx d699eef2ca docs: add docs for transform property in built-in loaders
(cherry picked from commit 3dfdb71d65)
2026-01-07 09:18:03 -05:00
Paweł Kawula 0ff2d5af2d docs: fixed incorrect link in migrating-to-vitest.md
provided correct link to section about vitest fake timer

(cherry picked from commit f811c6c69d)
2026-01-07 09:10:33 -05:00
Andrew Scott 4161161320 Revert "fix(router): Ensure createUrlTree does not reuse segments of input"
This reverts commit 39efb62c0f.

(cherry picked from commit 174d2da29a)
2026-01-07 00:29:22 +00:00
Miles Malerba e3fba182f9 feat(forms): add [formField] directive
This will replace the `[field]` directive, since `[field]` is a very
generic name for signal forms to commandeer

refactor(forms): hook up `formField` directive in compiler

Hooks up the `formField` direcive to get the same treatment as the
`field` directive in the compiler.

apply updated formatting

(cherry picked from commit 2d85ae5811)
2026-01-06 17:21:09 -05:00
Angular Robot defc9246a5 build: update cross-repo angular dependencies
See associated pull request for more information.
2026-01-06 15:19:53 -05:00
Leon Senft af1d4acb9d refactor(forms): bind native properties on interop controls
The framework will now bind all field state properties to their
corresponding native properties (if any) on interop form controls (those
using `ControlValueAccessor`), excluding those handled explicitly by
`ControlValueAccessor` such as `disabled`.

(cherry picked from commit 82a2de201f)
2026-01-06 13:12:21 -05:00
Leon Senft 0e8b9d8cde refactor(forms): bind field properties to all directives on interop controls
The framework will now bind the field state properties to all matching
directive inputs on form controls using Reactive Forms'
`ControlValueAccessor`.

(cherry picked from commit 15bddbdcda)
2026-01-06 13:12:21 -05:00
Leon Senft ff37a13913 refactor(forms): bind field properties to all directives on custom controls
The framework will now bind the field state properties to all matching
directive inputs on custom form controls.

(cherry picked from commit 0c8f15d546)
2026-01-06 13:12:21 -05:00
Leon Senft a4ba820d36 refactor(forms): bind field properties to all directives on native controls
Since the `Field` directive manages binding `FieldState` properties to
the underlying form control automatically, the type checker prohibits
explicit bindings to the same properties to avoid conflicts. This proved
problematic in cases where developers wanted to bind these properties to
the inputs of other directives on form controls. Now the framework will
bind the field state properties to all matching directive inputs on
native controls.

Fix #65617

(cherry picked from commit c149f47ef6)
2026-01-06 13:12:21 -05:00
Andrew Scott 92c162e1b9 fix(router): Ensure createUrlTree does not reuse segments of input
Mutating the `UrlTree` that is returned by `createUrlTree` can cause the
input, which might even be an active route, to be mutated. This ensures
the `UrlSegment`s are recreated and do not mutate the input.

fixes #54624

(cherry picked from commit 39efb62c0f)
2026-01-06 13:11:58 -05:00
Kristiyan Kostadinov 8e808740c9 fix(compiler): better types for a few expression AST nodes
We had `any` types for `LiteralArray.expressions`, `Chain.expressions`, `BindingPipe.args`, `LiteralPrimitive.value` and `LiteralMap.values`. These changes add proper types to them.

(cherry picked from commit 10da2f9029)
2026-01-06 12:16:32 -05:00
Alan Agius f9c29572d2 fix(core): sanitize sensitive attributes on SVG script elements
This commit updates the DOM security schema and sanitization logic to properly recognize and sanitize `href` and `xlink:href` attributes on SVG `<script>` elements.
2026-01-06 12:16:03 -05:00
Shuaib Hasan Akib 45bfd824bf docs(forms): add documentation links to Signal Forms API
Add @see JSDoc tags to Signal Forms API functions and classes to link to the essentials guide and detailed documentation pages. This improves discoverability of Signal Forms documentation from the API reference.

(cherry picked from commit b93d5ec27a)
2026-01-06 10:34:49 -05:00
Angular Robot 9d33cf3793 build: lock file maintenance
See associated pull request for more information.
2026-01-06 10:20:35 -05:00
Angular Robot 4917befe59 build: update cross-repo angular dependencies
See associated pull request for more information.
2026-01-06 10:19:00 -05:00
Matthieu Riegler a119427834 docs: rewrite testing docs
Those rewrites focus on using Vitest instead of jasmine, drop usages of `fakeAsync`, present modern testing strategy that rely on `whenStable` more than explicit calls to `detectChanges`.

fixes #42748, #48510, #64962, #65987, #66029,  #66150

(cherry picked from commit 71cde39ff0)
2026-01-05 19:38:27 -05:00
Angular Robot b16c2f59ee build: update cross-repo angular dependencies
See associated pull request for more information.
2026-01-05 19:28:19 -05:00
Shuaib Hasan Akib e21076d068 docs(forms): fix duplicate validator reference titles in AbstractControl
The removeValidators and hasValidator methods both had identical "Reference to a ValidatorFn" section titles, causing duplicate entries in the API documentation table of contents.

(cherry picked from commit 456ca35906)
2026-01-05 19:26:34 -05:00
SkyZeroZx 8f26a52aa8 refactor(core): introduce tree-shakeable runtime error codes for NgModule handling and ViewContainerRef errors
Adds new tree-shakeable runtime error codes to improve error reporting for
NgModule resolution issues (duplicate or missing IDs) and invalid ViewContainerRef
operations involving destroyed views.

(cherry picked from commit 1532be9d00)
2026-01-05 16:52:27 -05:00
Matthieu Riegler 1748c8984c docs(docs-infra): Add package filter to the API list
This commit also introduces the usage of signal forms on adev

(cherry picked from commit 15308149db)
2026-01-05 15:21:37 -05:00
Angular Robot 834bae1331 docs: update cross-repo adev docs
Updated Angular adev cross repo docs files.
2026-01-05 14:43:08 -05:00
Shuaib Hasan Akib 9494b56cc3 docs(docs-infra): sync heading colors between docs and API pages
Aligns documentation heading colors across docs and API pages to ensure
visual consistency and a cohesive reading experience.

(cherry picked from commit e6631f0007)
2026-01-05 12:37:02 -05:00
SkyZeroZx cc067c2a0d feat(docs-infra): Extracts aliases from doc entries
Implements a mechanism to extract alias names from a directive's selector.

(cherry picked from commit ed7bfb3c88)
2026-01-05 12:33:19 -05:00
SkyZeroZx 6cfbc736b4 docs: update tsDoc code examples to use TypeScript syntax highlighting
(cherry picked from commit b735c45974)
2026-01-05 12:31:47 -05:00
Angular Robot 7e9dc56186 build: lock file maintenance
See associated pull request for more information.
2026-01-05 12:21:40 -05:00
SkyZeroZx c1f22a3fa4 docs: Add form control state management and event options
(cherry picked from commit b1a0d1d8e2)
2026-01-05 12:16:14 -05:00
SkyZeroZx e65c3cabe5 docs(docs-infra): add external link styles to API reference documentation
Adds the missing icon external link styling to the API reference documentation.

(cherry picked from commit c1d72af10d)
2026-01-05 12:14:34 -05:00
SkyZeroZx 06109f5ad3 docs: add @see links to API docs for better discoverability
(cherry picked from commit fa430bffb0)
2026-01-05 12:12:28 -05:00
SkyZeroZx 5e6a9ba2af refactor(router): Simplifies unsubscribe logic
Uses optional chaining to simplify unsubscribe handling.
Also improving tree-shaking by limiting injector names to route paths using `ngDevMode`

(cherry picked from commit 712d8f41e8)
2026-01-05 12:08:14 -05:00
Angular Robot b2d6d36458 build: update cross-repo angular dependencies
See associated pull request for more information.
2026-01-05 12:07:00 -05:00
SkyZeroZx 73a3fd5bf7 docs: add section on converting writable signals to readonly
(cherry picked from commit a9283ca93a)
2026-01-05 12:03:15 -05:00
Shuaib Hasan Akib 4227a4915e docs: fix incorrect Input import in example
Update the documentation example to import input instead of Input,
ensuring the code reflects the signal-based API and current Angular
recommendations.

(cherry picked from commit bcaa001cac)
2026-01-05 12:02:20 -05:00
Shuaib Hasan Akib aac05ff1d6 fix(docs-infra): stop auto-linking "state" and "group" to APIs
Prevents common words used in code examples from being incorrectly
linked to API references.

Fixes: #66292
(cherry picked from commit 8358e548ce)
2026-01-05 12:01:55 -05:00
SkyZeroZx d75cd8ed25 docs: prevent mobile overflow in the route behavior customization example
(cherry picked from commit 5d3708e5ed)
2026-01-05 12:01:02 -05:00
Logan Mzz 6c911adf05 docs: fix reference to parent from children resolvers
(cherry picked from commit ef48826dc7)
2026-01-05 11:55:56 -05:00
Kirill Cherkashin c98549d6de docs(forms): update migration guide (#66111)
PR Close #66111
2026-01-05 11:54:45 -05:00
kirjs 22399296f0 docs(forms): Address feedback (#66111)
Update `Automatic status class` section

PR Close #66111
2026-01-05 11:54:45 -05:00
kirjs 958d7f7909 docs(forms): Address feedback (#66111)
Update the Automatic status classes section to make NG_STATUS_CLASSES go first.

PR Close #66111
2026-01-05 11:54:45 -05:00
kirjs 6ee4834d35 docs(forms): Address feedback (#66111)
Change wording and formatting and clean up.

PR Close #66111
2026-01-05 11:54:45 -05:00
kirjs cf57c01ca6 docs(forms): Add a section for automatic form binding (#66111)
To avoid manually adding classes like `.ng-valid`, `.ng-dirty` etc to every field, you can provide a global configuration using `provideSignalFormsConfig`.

PR Close #66111
2026-01-05 11:54:45 -05:00
kirjs b0a4ecc50e docs(forms): add a Accessing values section (#66111)
This explains how to get the value of a compat form

PR Close #66111
2026-01-05 11:54:45 -05:00
kirjs 65a2a0da8f docs(forms): add migration guide for Signal Forms (#66111)
This commit introduces a new guide for migrating from legacy forms (Reactive and Template-driven) to the new Signal Forms.

PR Close #66111
2026-01-05 11:54:45 -05:00
SkyZeroZx caeea1ced9 docs(docs-infra): Fixes the visibility of the copy link button in CLI headings
Fixes the visibility of the copy link button in CLI documentation headings.

(cherry picked from commit a5ebdd9cd1)
2026-01-05 11:52:27 -05:00
Matthieu Riegler 70a4739113 docs: fix typos in Effects doc in Signals guide
(cherry picked from commit c225b03c91)
2026-01-05 11:51:04 -05:00
Matthieu Riegler d669763a31 docs(docs-infra): Modernize tests
Remove usages of `detectChanges` and rely on `whenStable`.
This commit also removed the usage of `provideZonelessChangeDetection` which is no longer necessary.

(cherry picked from commit 3a85031dc0)
2026-01-05 11:47:12 -05:00
Angular Robot 71a4373fbb build: update cross-repo angular dependencies
See associated pull request for more information.
2026-01-02 14:24:47 +01:00
Kristiyan Kostadinov f8b6a3fed1 Revert "refactor(core): Add ngDevMode guards and new sanitization error codes"
This reverts commit 4e7e38c591.

(cherry picked from commit 1765ebe79b)
2026-01-02 11:37:26 +01:00
Angular Robot d370c4ba44 build: update bazel dependencies
See associated pull request for more information.
2026-01-02 08:32:09 +01:00
Angular Robot 5725509e88 build: update pnpm to v10.27.0
See associated pull request for more information.
2026-01-02 08:30:14 +01:00
Kristiyan Kostadinov b4fc0dace5 test(compiler): switch compliance tests to es2022
Sets up the compliance tests to target es2022 since that's the default in the CLI. Also updates all of the tests, primarily because the generated output for static properties has changed.

(cherry picked from commit 41eacff363)
2026-01-02 08:29:16 +01:00
Shuaib Hasan Akib efaf84a168 docs: Incorrect Highlighting in Content Queries Example
Fixes: #66284
(cherry picked from commit 75fa37a0dd)
2026-01-02 08:28:48 +01:00
Shuaib Hasan Akib c0cdec5cee docs: fix incorrect FormArray example link
Update the typed forms guide link to point to the correct
FormArray section instead of the FormControl getting started example.

(cherry picked from commit e3781cd88a)
2026-01-02 08:28:21 +01:00
Kristiyan Kostadinov cd405685af fix(compiler-cli): fix up spelling of diagnostic
Fixes the spelling for the `component is missing a template` diagnostic and expands it a bit.

(cherry picked from commit 3a26244d61)
2026-01-02 08:27:30 +01:00
Adam Podroužek 263e7290bd docs: fix URL link to valibot
(cherry picked from commit 402ecaf85c)
2026-01-02 08:27:07 +01:00
aparziale 09ba268fc5 docs(docs-infra): LoadComponent along with loadChildren
Added example with loadComponent along with loadChildren

Fix #66257

(cherry picked from commit b54bc1b16e)
2026-01-02 08:26:34 +01:00
kirjs 6bd22df987 fix(forms): Support readonly arrays in signal forms
This would allow using `readonly Array<...>` in types

(cherry picked from commit 282220d032)
2026-01-02 08:26:09 +01:00
SkyZeroZx 86374b2c0e docs: use currentNavigation instead of deprecated getCurrentNavigation
(cherry picked from commit 004813dc37)
2026-01-02 08:25:47 +01:00
SkyZeroZx a45141383f docs(docs-infra): fixes copy-to-clipboard icon moves to a new line for long heading text
Fixes an issue where the copy-to-clipboard icon moves to a new line when heading text is long and wraps.
Applies improved text wrapping for headings to keep the icon visually aligned.

Fixes #66239

(cherry picked from commit e409757208)
2026-01-02 08:24:43 +01:00
SkyZeroZx 9babb85fed docs: update next steps in Signal Forms tutorial and fix broken link
(cherry picked from commit baa19e2cf0)
2026-01-02 08:23:47 +01:00
SkyZeroZx 4789653db0 docs(docs-infra): apply OnPush change detection and remove redundant root provider
Adds OnPush change detection and removes a redundant `providedIn: 'root'` injection token.

(cherry picked from commit 3b73eff0b4)
2026-01-02 08:23:23 +01:00
Matthieu Riegler 7cc2a7669f docs(docs-infra): user-select none for buttons
fixes #66214

(cherry picked from commit 80eb5c4d8a)
2026-01-02 08:22:20 +01:00
Kristiyan Kostadinov 523dbaf1c3 fix(compiler): stop ThisReceiver inheritance from ImplicitReceiver
Back in #39323, I added a new `ThisReceiver` node to represent accesses done through `this` and I ended up making it inherit from `ImplicitReceiver`. The logic was that in most cases accessing through `this` was the same as the implicit access.

Over the years this has proven to not be a great idea, because no other AST nodes do this and one has to keep it in mind whenever dealing with `ImplicitReceiver`.

These changes remove the inheritance and update all of the usage sites accordingly.

(cherry picked from commit fa7cb4b87a)
2026-01-02 08:21:53 +01:00
JoostK 778460fcca fix(compiler-cli): support qualified names in typeof type references
This commit expands the static interpreter to now understand qualified names in `typeof`
type queries.

Fixes #65686

(cherry picked from commit f12e160bc1)
2026-01-02 08:21:12 +01:00
JoostK 4d9c4567ed fix(compiler-cli): ensure component import diagnostics are reported within the imports expression
PR #60455 improved error reporting for `@Component.imports` by scoping the diagnostic to an individual
element within the `imports` array, but this may introduce hard to track diagnostics when it ends
up being reported (far) away from the component itself.

This can be even more problematic when the diagnostic would end up being reported in a declaration file,
as happened in issue #65686; the declaration files of an imported library contained syntax that the
static interpreter did not support, hence the `@Component.imports` was rejected with a diagnostic reported
in the library's declaration file. This diagnostic isn't guaranteed to be reported (e.g. the CLI only
gathers Angular-specific diagnostics for Angular-compiled files, which excludes declaration files).

This commit changes the diagnostic location to ensure it is being reported within the `@Component.imports`
expression, in most cases retaining the desirable effect of #60455 while avoiding out-of-band diagnostics.

(cherry picked from commit 106ba63650)
2026-01-02 08:21:12 +01:00
SkyZeroZx aa3b028e25 refactor(core): implement missing OnDestroy for schedulers
Implements the missing OnDestroy interface. Also removes an unused generic from a performance logging function.

(cherry picked from commit 02e80b4ea2)
2026-01-02 08:20:00 +01:00
aparziale 514abbfc82 docs(docs-infra): Fix incorrect code highlight in Creating an output based on an RxJS Observable
Fix highlighted line in output-interop.md documentation

Fixes: #66187
(cherry picked from commit 66637f1c3f)
2026-01-02 08:18:35 +01:00
Kristiyan Kostadinov 3c3ae0cb64 fix(compiler): provide location information for literal map keys
Adds spans for the keys of a `LiteralMap`.

Fixes #66175.

(cherry picked from commit 76fa180005)
2026-01-02 08:18:06 +01:00
SkyZeroZx b37f46b652 refactor(forms): add MANAGED_METADATA_LAZY_CREATION runtime error
Add the `MANAGED_METADATA_LAZY_CREATION` `RuntimeError`  in signal form,
making it tree-shakeable.

(cherry picked from commit 398078f456)
2026-01-02 08:17:43 +01:00
Alan Agius 31a2ac2a22 build: switch Node.js toolchain to derive version from .nvmrc.
Remove hardcoded node.js version

(cherry picked from commit 0d9e51e766)
2026-01-02 08:16:55 +01:00
SkyZeroZx 71998069bd docs: update localization docs and modernize examples
Improves the localization documentation by updating code highlight syntax issues
and removing outdated TODOs.

Additionally, the CDK examples have been updated to use the viewChild query signal API,
aligning the docs with current recommended patterns.

(cherry picked from commit a7762c79b8)
2026-01-02 08:16:28 +01:00
Matthieu Riegler f173acab69 ci: add Charles & Doug as optional adev reviewers
Since we also have tooling docs...

(cherry picked from commit c69b67147a)
2026-01-02 08:14:41 +01:00
Alan Agius 349b54351a docs: update flattened type definitions description to include rollup-plugin-dts.
(cherry picked from commit e11f02af81)
2026-01-02 08:14:09 +01:00
Shuaib Hasan Akib b9d7455d9c refactor(core): mark VERSION exports as pure for better tree-shaking
Adds a PURE annotation to VERSION constants, enabling tree-shaking
and keeping the implementation consistent with other pure exports.

(cherry picked from commit abb179466d)
2026-01-02 08:13:44 +01:00
Shuaib Hasan Akib d4799c1e82 docs: update Angular Package Format version references to latest
(cherry picked from commit 54a49ad9eb)
2026-01-02 08:12:50 +01:00
Alan Agius 901d66dbb7 build: correct markdown link syntax for commit hashes in changelog generation
The links were generated incorrectly

(cherry picked from commit 98bfd139ee)
2026-01-02 08:11:51 +01:00
SkyZeroZx 0aa5c7b815 refactor(core): remove unused Injector and Renderer2 dependencies from hydration functions
Removes the unsued `Injector` & `Renderer2` dependency from the hydration annotation process.

(cherry picked from commit 93fe833088)
2026-01-02 08:11:19 +01:00
SkyZeroZx 269799979f docs(docs-infra): adjust z-index to prevent adev tutorial nav overlapping sidebar
Updated the z-index hierarchy to avoid the adev tutorial navigation bar
overlapping the main sidebar.

(cherry picked from commit 59c1dcc662)
2026-01-02 08:10:57 +01:00
Leon Senft 85621f11e1 test(forms): add test coverage for binding errors to custon controls
This input was missing dedicated test coverage.

(cherry picked from commit e7745dc9dd)
2026-01-02 08:09:39 +01:00
Leon Senft 6d167bd64c test(forms): remove duplicate test case
We had two test cases that tested the `Field` directive synchronizes
with a custom checkbox control components.

(cherry picked from commit 8832fc01b2)
2026-01-02 08:09:39 +01:00
Leon Senft ec110f170b fix(forms): allow custom controls to require pending input
* Allow custom controls to make `pending` a required input
* Refactor test for `pending` input to be consistent with other control
  properties
* Test that `pending` inputs are reset when the field binding changes

(cherry picked from commit 1a4c3eb1d0)
2026-01-02 08:09:39 +01:00
Leon Senft 79d16919a5 test(forms): remove obsolete test case
There's no longer a need to test that inputs are set before
initialization on custom controls now that required inputs are
supported.

(cherry picked from commit 9ad603fa11)
2026-01-02 08:09:39 +01:00
Leon Senft 8c83266816 test(forms): refactor and improve test coverage for disabledReasons input
* Refactor test for `disabledReasons` input to be consistent with other control
  properties
* Test that `disabledReasons` inputs are reset when the field binding changes

(cherry picked from commit 4f73a350a5)
2026-01-02 08:09:39 +01:00
Leon Senft 561772b152 fix(forms): allow custom controls to require dirty input
* Allow custom controls to make `dirty` a required input
* Refactor test for `dirty` input to be consistent with other control
  properties
* Test that `dirty` inputs are reset when the field binding changes

(cherry picked from commit 89c37f1f7f)
2026-01-02 08:09:39 +01:00
Leon Senft 929306e658 test(forms): refactor and improve test coverage for invalid input
* Refactor test for `invalid` input to be consistent with other control
  properties
* Test that `invalid` inputs are reset when the field binding changes

(cherry picked from commit b563b5cfc2)
2026-01-02 08:09:39 +01:00
Leon Senft f0fb1d8581 fix(forms): allow custom controls to require hidden input
* Allow custom controls to make `hidden` a required input
* Refactor test for `hidden` input to be consistent with other control
  properties
* Test that `hidden` inputs are reset when the field binding changes

(cherry picked from commit 82edf18427)
2026-01-02 08:09:38 +01:00
Leon Senft 9748b0d5da fix(forms): support custom controls with non signal-based models
* Recognize directives with non signal-based models as valid custom controls
* Relax type checker to allow non signal-based models

The `FormValueControl` and `FormCheckboxControl` interfaces still
require a `model()`-input, however, a custom control need not implement
either interface to be bound by the `Field` directive.

All of the following examples can be used to define a custom control:

```ts
// Preferred: model()
class MyFormControl implements FormValueControl<string> {
  readonly value: model.required<string>();
}

// Supported: input() + output()
class MyFormControl {
  readonly value: input.required<string>();
  readonly valueChange: output<string>();
}

// Supported: @Input() + @Output()
class MyFormControl {
  @Input({required: true}) value!: string;
  @Output() valueChange: new EventEmitter<string>();
}
```

The latter two may still choose to implement `FormUiControl` for other
properties, but again it is not required.

Fix #65478

(cherry picked from commit cb09fb8308)
2026-01-02 08:09:07 +01:00
Leon Senft ab86c90c20 refactor(forms): always bind [field] to ControlValueAccessor if present
Change the order of precedence that `[field]` binds to from

  1. Custom control (`FormValueControl`, `FormCheckboxControl`)
  2. Interop control (`ControlValueAccessor`)
  3. Native control (`<input>`, `<select>`, `<textarea>`)

to

  1. Interop control (`ControlValueAccessor`)
  2. Custom control (`FormValueControl`, `FormCheckboxControl`)
  3. Native control (`<input>`, `<select>`, `<textarea>`)

This ensures that Reactive Forms controls authored to use
`ControlValueAccessor` work correctly with Signal Forms, even if they
happen to conform to the `FormValueControl` interface.

(cherry picked from commit 817e2b87a8)
2026-01-02 08:09:06 +01:00
SkyZeroZx ce7b1ab3b6 refactor(core): Add ngDevMode guards and new sanitization error codes
Adds new runtime sanitization error codes. Adds `ngDevMode` guards around
error message strings to ensure detailed diagnostics are included only
in development mode. This allows production builds to tree-shake verbose error descriptions, reducing bundle size.

(cherry picked from commit 4e7e38c591)
2026-01-02 08:08:25 +01:00
Angular Robot 069974a519 build: update dependency @angular/aria to v21.0.4
See associated pull request for more information.
2025-12-17 16:57:02 -08:00
Miles Malerba 59b3790031 refactor(forms): rename server errors to submission errors
This better reflects the intent to indicate an error during submission,
regardless of whether the error came from the server or not.

(cherry picked from commit 244b54c9bf)
2025-12-17 15:42:11 -08:00
Angular Robot 96dccc4e6f docs: update cross-repo adev docs
Updated Angular adev cross repo docs files.
2025-12-17 15:40:54 -08:00
Andrew Seguin 0edbee4550 fix(core): explicitly cast signal node value to String
If the computed's `toString` is called and `node.value` is a Symbol, the browser will throw an exception `ERROR TypeError: Cannot convert a Symbol value to a string`

Symbols cannot be implicitly changed to strings. This change changes the conversion to be explicit by wrapping `node.value` with `String()`

This can be reproduced if you create a computed with `createComputed(computation, equal)` and call `toString()` while `node.value` is something like `Symbol(UNSET)`

(cherry picked from commit e2a9938c51)
2025-12-17 14:38:41 -08:00
Angular Robot a8127273a6 build: update cross-repo angular dependencies
See associated pull request for more information.
2025-12-17 14:35:48 -08:00
Shuaib Hasan Akib 441d158302 docs(docs-infra): clean up TODO comment, unused imports, and helper function
(cherry picked from commit e9d1c6478a)
2025-12-17 14:34:02 -08:00
Shuaib Hasan Akib c8d14d75ba docs: fix incorrect line highlighting in tutorials
Ensures highlighted lines in tutorial examples match the
corresponding code shown to readers.

(cherry picked from commit ad88c72f24)
2025-12-17 14:26:34 -08:00
Miles Malerba ae0d707d44 docs: call out breaking changes as experimental
Explicitly calls out that recent breaking changes were only for
experimental features

(cherry picked from commit 5b06996fb2)
2025-12-17 14:25:36 -08:00
Andrew Scott eaa68c4cd7 release: bump VSCode extension version to 21.0.1 (#66153) 2025-12-17 13:51:34 -08:00
975 changed files with 35207 additions and 30065 deletions
+5 -12
View File
@@ -1,23 +1,16 @@
# Image metadata and config.
# Ideally, the Node.js version should match what we use on CI.
FROM cimg/node:18.13.0@sha256:45d0ee279f522c4562a464df71108104cc9b2187205333ffd0707d4f05589731
FROM cimg/node:22.21.1-browsers@sha256:eee8ed828d5a54196456a86ac6872c43325acad54e59632261e23b486c458992
LABEL name="Angular dev environment" \
description="This image can be used to create a dev environment for building Angular." \
vendor="angular" \
version="1.0"
EXPOSE 4000 4200 4433 5000 8080 9876
description="This image can be used to create a dev environment for building Angular." \
vendor="angular" \
version="1.0"
EXPOSE 4200 4201 4205 4206 9876
# Switch to `root`.
USER root
# Configure `Node.js`/`npm`.
RUN npm config --global set user root
# Go! (And keep going.)
CMD ["tail", "--follow", "/dev/null"]
+6 -5
View File
@@ -2,10 +2,11 @@
{
"name": "Angular dev container",
"dockerFile": "Dockerfile",
"appPort": [4000, 4200, 4433, 5000, 8080, 9876],
"forwardPorts": [4200, 4201, 4205, 4206, 9876],
"postCreateCommand": "pnpm install",
"extensions": [
"devondcarew.bazel-code",
"ms-vscode.vscode-typescript-tslint-plugin"
],
"customizations": {
"vscode": {
"extensions": ["BazelBuild.vscode-bazel", "ms-vscode.vscode-typescript-tslint-plugin"]
}
}
}
@@ -1,33 +1,53 @@
import {Deployment} from './deployments.mjs';
import {join} from 'path';
import {readFileSync, writeFileSync} from 'fs';
export async function generateSitemap(deployment: Deployment, distDir: string) {
const servingUrlWithoutEndingSlash = deployment.servingUrl.endsWith('/')
? deployment.servingUrl.slice(0, -1)
: deployment.servingUrl;
import {join} from 'node:path';
import {readFile, writeFile} from 'node:fs/promises';
export async function generateSitemap(deployment: Deployment, distDir: string): Promise<void> {
/** Timestamp string used to of the last file update. */
const lastModifiedTimestamp = new Date().toISOString();
/** An object containing all of the routes available within the application. */
const routes = JSON.parse(readFileSync(join(distDir, 'prerendered-routes.json'), 'utf-8'));
const {routes} = JSON.parse(await readFile(join(distDir, 'prerendered-routes.json'), 'utf-8'));
const routePaths = Object.keys(routes);
/** The generated sitemap string. */
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${Object.keys(routes.routes)
.map((route) => {
const routeWithoutLeadingSlash = route.startsWith('/') ? route.slice(1) : route;
return `
<url>
<loc>${servingUrlWithoutEndingSlash}/${routeWithoutLeadingSlash}</loc>
${routePaths
.map(
(route) => `<url>
<loc>${joinUrlParts(deployment.servingUrl, route)}</loc>
<lastmod>${lastModifiedTimestamp}</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
`;
})
</url>`,
)
.join('')}
</urlset>`;
writeFileSync(join(distDir, 'browser', 'sitemap.xml'), sitemap, 'utf-8');
console.log(`Generated sitemap with ${Object.keys(routes.routes).length} entries.`);
await writeFile(join(distDir, 'browser', 'sitemap.xml'), sitemap, 'utf-8');
console.log(`Generated sitemap with ${routePaths.length} entries.`);
}
function joinUrlParts(...parts: string[]): string {
const normalizeParts: string[] = [];
for (const part of parts) {
if (part === '') {
// Skip any empty parts
continue;
}
let normalizedPart = part;
if (part[0] === '/') {
normalizedPart = normalizedPart.slice(1);
}
if (part.at(-1) === '/') {
normalizedPart = normalizedPart.slice(0, -1);
}
if (normalizedPart !== '') {
normalizeParts.push(normalizedPart);
}
}
return normalizeParts.join('/');
}
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -5,9 +5,9 @@ runs:
using: 'composite'
steps:
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/bazel/setup@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Setup Saucelabs Variables
uses: angular/dev-infra/github-actions/saucelabs@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/saucelabs@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Starting Saucelabs tunnel service
shell: bash
run: ./tools/saucelabs/sauce-service.sh run &
+4 -4
View File
@@ -21,16 +21,16 @@ jobs:
(github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'adev: preview'))
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/bazel/setup@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/bazel/configure-remote@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Build adev
run: pnpm bazel build //adev:build.production --config=snapshot-build
- uses: angular/dev-infra/github-actions/previews/pack-and-upload-artifact@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
- uses: angular/dev-infra/github-actions/previews/pack-and-upload-artifact@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
with:
workflow-artifact-name: 'adev-preview'
pull-number: '${{github.event.pull_request.number}}'
+1 -1
View File
@@ -40,7 +40,7 @@ jobs:
npx -y firebase-tools@latest target:clear --config adev/firebase.json --project ${{env.PREVIEW_PROJECT}} hosting angular-docs
npx -y firebase-tools@latest target:apply --config adev/firebase.json --project ${{env.PREVIEW_PROJECT}} hosting angular-docs ${{env.PREVIEW_SITE}}
- uses: angular/dev-infra/github-actions/previews/upload-artifacts-to-firebase@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
- uses: angular/dev-infra/github-actions/previews/upload-artifacts-to-firebase@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
with:
github-token: '${{secrets.GITHUB_TOKEN}}'
workflow-artifact-name: 'adev-preview'
@@ -17,6 +17,6 @@ jobs:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
with:
persist-credentials: false
- uses: angular/dev-infra/github-actions/branch-manager@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
- uses: angular/dev-infra/github-actions/branch-manager@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
- run: pnpm install --frozen-lockfile
- uses: angular/dev-infra/github-actions/bazel/configure-remote@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
- uses: angular/dev-infra/github-actions/bazel/configure-remote@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
with:
bazelrc: ./.bazelrc.user
+25 -25
View File
@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Check code lint
@@ -39,13 +39,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
with:
disable-package-manager-cache: true
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/bazel/setup@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/bazel/configure-remote@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
with:
google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }}
- name: Install node modules
@@ -67,11 +67,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/bazel/setup@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Setup Bazel Remote Caching
uses: angular/dev-infra/github-actions/bazel/configure-remote@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/bazel/configure-remote@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
with:
google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }}
- name: Install node modules
@@ -83,11 +83,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/bazel/setup@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Setup Bazel Remote Caching
uses: angular/dev-infra/github-actions/bazel/configure-remote@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/bazel/configure-remote@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
with:
google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }}
- name: Install node modules
@@ -100,11 +100,11 @@ jobs:
labels: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/bazel/setup@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/bazel/configure-remote@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
with:
google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }}
- name: Install node modules
@@ -119,11 +119,11 @@ jobs:
labels: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/bazel/setup@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/bazel/configure-remote@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
with:
google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }}
- name: Install node modules
@@ -136,11 +136,11 @@ jobs:
labels: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/bazel/setup@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/bazel/configure-remote@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Install node modules
run: pnpm install --frozen-lockfile
- run: echo "https://${{secrets.SNAPSHOT_BUILDS_GITHUB_TOKEN}}:@github.com" > ${HOME}/.git_credentials
@@ -152,11 +152,11 @@ jobs:
labels: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/bazel/setup@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/bazel/configure-remote@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
with:
google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }}
- name: Install node modules
@@ -206,11 +206,11 @@ jobs:
runs-on: ubuntu-latest-8core
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/bazel/setup@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/bazel/configure-remote@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Build adev
+2 -2
View File
@@ -13,13 +13,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- uses: angular/dev-infra/github-actions/pull-request-labeling@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
- uses: angular/dev-infra/github-actions/pull-request-labeling@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
post_approval_changes:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- uses: angular/dev-infra/github-actions/post-approval-changes@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
- uses: angular/dev-infra/github-actions/post-approval-changes@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- uses: angular/dev-infra/github-actions/google-internal-tests@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
- uses: angular/dev-infra/github-actions/google-internal-tests@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
with:
run-tests-guide-url: http://go/angular-g3sync-start
github-token: ${{ secrets.GITHUB_TOKEN }}
+4 -4
View File
@@ -13,15 +13,15 @@ jobs:
JOBS: 2
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/bazel/setup@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Setup Bazel Remote Caching
uses: angular/dev-infra/github-actions/bazel/configure-remote@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/bazel/configure-remote@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Setup Saucelabs Variables
uses: angular/dev-infra/github-actions/saucelabs@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/saucelabs@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Set up Sauce Tunnel Daemon
run: pnpm bazel run //tools/saucelabs-daemon/background-service -- $JOBS &
env:
+1 -1
View File
@@ -9,6 +9,6 @@ jobs:
status:
runs-on: ubuntu-latest
steps:
- uses: angular/dev-infra/github-actions/unified-status-check@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
- uses: angular/dev-infra/github-actions/unified-status-check@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
+3 -3
View File
@@ -21,7 +21,7 @@ jobs:
workflows: ${{ steps.workflows.outputs.workflows }}
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Install node modules
run: pnpm install --frozen-lockfile
- id: workflows
@@ -36,9 +36,9 @@ jobs:
workflow: ${{ fromJSON(needs.list.outputs.workflows) }}
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/bazel/setup@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Install node modules
run: pnpm install --frozen-lockfile
# We utilize the google-github-actions/auth action to allow us to get an active credential using workflow
+20 -20
View File
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Check code lint
@@ -37,7 +37,7 @@ jobs:
- name: Check code format
run: pnpm ng-dev format changed --check ${{ github.event.pull_request.base.sha }}
- name: Check Package Licenses
uses: angular/dev-infra/github-actions/linting/licenses@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/linting/licenses@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
with:
allow-dependencies-licenses: 'pkg:npm/google-protobuf@'
@@ -45,13 +45,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
with:
disable-package-manager-cache: true
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/bazel/setup@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/bazel/configure-remote@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Run unit tests
@@ -71,11 +71,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/bazel/setup@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Setup Bazel Remote Caching
uses: angular/dev-infra/github-actions/bazel/configure-remote@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/bazel/configure-remote@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Run CI tests for framework
@@ -95,11 +95,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/bazel/setup@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Setup Bazel Remote Caching
uses: angular/dev-infra/github-actions/bazel/configure-remote@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/bazel/configure-remote@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Run integration CI tests for framework
@@ -110,11 +110,11 @@ jobs:
labels: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/bazel/setup@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/bazel/configure-remote@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Run tests
@@ -127,11 +127,11 @@ jobs:
labels: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/bazel/setup@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/bazel/configure-remote@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Run tests
@@ -142,11 +142,11 @@ jobs:
labels: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/bazel/setup@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d
uses: angular/dev-infra/github-actions/bazel/configure-remote@2ff164a0ca60e692f3d7e1a982bde9bf3379365e
- name: Install node modules
run: pnpm install --frozen-lockfile
- run: |
+3
View File
@@ -54,3 +54,6 @@ baseline.json
# Ignore cache created with the Angular CLI.
.angular/
# pnpm store
.pnpm-store
+1
View File
@@ -9,4 +9,5 @@ export const github = {
name: 'angular',
mainBranchName: 'main',
mergeMode: 'caretaker-only',
requireReleaseModeForRelease: true,
};
+2
View File
@@ -270,6 +270,8 @@ groups:
- ~amishne
- ~leonsenft
- ~mattrbeck
- ~clydin
- ~dgp1130
# =========================================================
# Angular DevTools
+64 -1
View File
@@ -1,6 +1,69 @@
<a name="21.0.9"></a>
# 21.0.9 (2026-01-14)
### forms
| Commit | Type | Description |
| -- | -- | -- |
| [82d556a8fb](https://github.com/angular/angular/commit/82d556a8fb38cd2024e3d098c55254305ba12b6b) | fix | Ensure the control instruction comes after the other bindings |
| [0055f3cc79](https://github.com/angular/angular/commit/0055f3cc79f387b8dec6ce5e1a33fad5486f9341) | fix | Rename signal form [field] to [formField] |
### migrations
| Commit | Type | Description |
| -- | -- | -- |
| [e4bfa5c9e7](https://github.com/angular/angular/commit/e4bfa5c9e7feec48d3c4e9425a21a2ccf6532bdb) | fix | prevent duplicate imports in common-to-standalone migration |
<!-- CHANGELOG SPLIT MARKER -->
<a name="21.0.8"></a>
# 21.0.8 (2026-01-08)
### core
| Commit | Type | Description |
| -- | -- | -- |
| [a6a2621bf9](https://github.com/angular/angular/commit/a6a2621bf9df02584e4079f4a804278fc2060a9c) | fix | fix memory leak with event replay |
| [5239e471a1](https://github.com/angular/angular/commit/5239e471a1f887574c6703c0497e5854304cce4e) | fix | handle cancelled traversals in fake navigation |
<!-- CHANGELOG SPLIT MARKER -->
<a name="21.0.7"></a>
# 21.0.7 (2026-01-07)
### compiler
| Commit | Type | Description |
| -- | -- | -- |
| [8e808740c9](https://github.com/angular/angular/commit/8e808740c9311daa0f1c9bab8596ed5e54bdcc6a) | fix | better types for a few expression AST nodes |
| [63b1cdcf70](https://github.com/angular/angular/commit/63b1cdcf70e6de448e8fa4ba1732d7bd7b5400d1) | fix | produce accurate span for typeof and void expressions |
| [3c3ae0cb64](https://github.com/angular/angular/commit/3c3ae0cb64bb112d7167fd9b0bf7739f0c9e6a39) | fix | provide location information for literal map keys |
| [523dbaf1c3](https://github.com/angular/angular/commit/523dbaf1c3646ce27f1cf2e4cfc84c730fea8da9) | fix | stop ThisReceiver inheritance from ImplicitReceiver |
### compiler-cli
| Commit | Type | Description |
| -- | -- | -- |
| [4d9c4567ed](https://github.com/angular/angular/commit/4d9c4567edfb8dd424a3336ef54ffdfc6ca7c15f) | fix | ensure component import diagnostics are reported within the `imports` expression |
| [cd405685af](https://github.com/angular/angular/commit/cd405685afbfad530de7fb841ad352d2b702a9a4) | fix | fix up spelling of diagnostic |
| [778460fcca](https://github.com/angular/angular/commit/778460fccac13d8667bb53fa24ba977a930c0253) | fix | support qualified names in `typeof` type references |
### core
| Commit | Type | Description |
| -- | -- | -- |
| [7c74674eb0](https://github.com/angular/angular/commit/7c74674eb07491f808f79976e3e21787a841aefb) | fix | avoid leaking view data in animations |
| [0edbee4550](https://github.com/angular/angular/commit/0edbee4550e85b933e9bd2ba3c5511ef6fbf7304) | fix | explicitly cast signal node value to String |
| [f9c29572d2](https://github.com/angular/angular/commit/f9c29572d28feef878c73edad562b3a6451825a6) | fix | sanitize sensitive attributes on SVG script elements |
### forms
| Commit | Type | Description |
| -- | -- | -- |
| [e3fba182f9](https://github.com/angular/angular/commit/e3fba182f90a2673040cf267a970c54c07d4840f) | feat | add `[formField]` directive |
| [561772b152](https://github.com/angular/angular/commit/561772b152458e1d91d4bf3ef45d9645a731f2b1) | fix | allow custom controls to require `dirty` input |
| [f0fb1d8581](https://github.com/angular/angular/commit/f0fb1d8581671ca499bcb4790b0549825eb36a91) | fix | allow custom controls to require `hidden` input |
| [ec110f170b](https://github.com/angular/angular/commit/ec110f170bbba95f023c8ae0e4429c35bfedc572) | fix | allow custom controls to require `pending` input |
| [ae1dc16bb0](https://github.com/angular/angular/commit/ae1dc16bb0d30b6e87b0f98b7989e6685d856e31) | fix | clean up abort listener after timeout |
| [9748b0d5da](https://github.com/angular/angular/commit/9748b0d5da6ffb1fd2498b23cc452240f46e0549) | fix | support custom controls with non signal-based models |
| [6bd22df987](https://github.com/angular/angular/commit/6bd22df987e433a9e3cb759e35eb6403991cf4b7) | fix | Support readonly arrays in signal forms |
### router
| Commit | Type | Description |
| -- | -- | -- |
| [41cd4a6af8](https://github.com/angular/angular/commit/41cd4a6af800cf7807c46862c99ae036457d8fa7) | fix | Fix RouterLink href not updating with `queryParamsHandling` |
| [5e9e09aee0](https://github.com/angular/angular/commit/5e9e09aee0c08901d2a4d48b60bd13692c73e76e) | fix | handle errors from view transition `updateCallbackDone` promise |
<!-- CHANGELOG SPLIT MARKER -->
<a name="21.0.6"></a>
# 21.0.6 (2025-12-17)
## Breaking Changes
## Breaking Changes (affecting only experimental features)
### forms
- The shape of `SignalFormsConfig.classes` has changed
+6 -6
View File
@@ -4,7 +4,7 @@ module(
name = "angular",
)
bazel_dep(name = "rules_pkg", version = "1.1.0")
bazel_dep(name = "rules_pkg", version = "1.2.0")
bazel_dep(name = "rules_nodejs", version = "6.6.2")
bazel_dep(name = "aspect_rules_ts", version = "3.8.1")
bazel_dep(name = "aspect_rules_js", version = "2.8.3")
@@ -12,9 +12,9 @@ bazel_dep(name = "aspect_rules_esbuild", version = "0.25.0")
bazel_dep(name = "aspect_rules_jasmine", version = "2.0.2")
bazel_dep(name = "aspect_rules_rollup", version = "2.0.1")
bazel_dep(name = "bazel_skylib", version = "1.9.0")
bazel_dep(name = "aspect_bazel_lib", version = "2.22.0")
bazel_dep(name = "aspect_bazel_lib", version = "2.22.2")
bazel_dep(name = "tar.bzl", version = "0.7.0")
bazel_dep(name = "yq.bzl", version = "0.3.2")
bazel_dep(name = "yq.bzl", version = "0.3.4")
bazel_dep(name = "rules_angular")
git_override(
module_name = "rules_angular",
@@ -25,7 +25,7 @@ git_override(
bazel_dep(name = "devinfra")
git_override(
module_name = "devinfra",
commit = "091b9a65ff074a3eb84c1fc1d0e17cc9f7e8868d",
commit = "2ff164a0ca60e692f3d7e1a982bde9bf3379365e",
remote = "https://github.com/angular/dev-infra.git",
)
@@ -39,7 +39,7 @@ git_override(
bazel_dep(name = "rules_browsers")
git_override(
module_name = "rules_browsers",
commit = "8ef3e996d5fc040a35770f860987d8b9cad8ef3d",
commit = "5f0e50d17d15c70e6ab5546f1659338ae94c4072",
remote = "https://github.com/devversion/rules_browsers.git",
)
@@ -47,7 +47,7 @@ yq = use_extension("@yq.bzl//yq:extensions.bzl", "yq")
use_repo(yq, "yq_toolchains")
node = use_extension("@rules_nodejs//nodejs:extensions.bzl", "node")
node.toolchain(node_version = "22.21.1")
node.toolchain(node_version_from_nvmrc = "//:.nvmrc")
use_repo(node, "nodejs_toolchains")
use_repo(node, "nodejs_darwin_amd64")
use_repo(node, "nodejs_darwin_arm64")
+77 -53
View File
@@ -15,12 +15,11 @@
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.14.0/MODULE.bazel": "2b31ffcc9bdc8295b2167e07a757dbbc9ac8906e7028e5170a3708cecaac119f",
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.17.1/MODULE.bazel": "9b027af55f619c7c444cead71061578fab6587e5e1303fa4ed61d49d2b1a7262",
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.19.3/MODULE.bazel": "253d739ba126f62a5767d832765b12b59e9f8d2bc88cc1572f4a73e46eb298ca",
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.22.0/MODULE.bazel": "7fe0191f047d4fe4a4a46c1107e2350cbb58a8fc2e10913aa4322d3190dec0bf",
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.22.0/source.json": "369df5b7f2eae82f200fff95cf1425f90dee90a0d0948122060b48150ff0e224",
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.22.2/MODULE.bazel": "6b735f3fdd64978e217c9725f4ff0d84bf606554c8e77d20e90977841d7ff2ed",
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.22.2/source.json": "58fffa2d722cff47cb8d921c8bbed7701c53f233009d9ca82beb4a0fb8fb9418",
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.7.7/MODULE.bazel": "491f8681205e31bb57892d67442ce448cda4f472a8e6b3dc062865e29a64f89c",
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.8.1/MODULE.bazel": "812d2dd42f65dca362152101fbec418029cc8fd34cbad1a2fde905383d705838",
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.9.3/MODULE.bazel": "66baf724dbae7aff4787bf2245cc188d50cb08e07789769730151c0943587c14",
"https://bcr.bazel.build/modules/aspect_rules_esbuild/0.24.0/MODULE.bazel": "15d19e46ec74e9e49ddf3c335e7a91b0657571654b0960bdcd10b771eeb4f7cb",
"https://bcr.bazel.build/modules/aspect_rules_esbuild/0.25.0/MODULE.bazel": "5fef5ec709c837312823f9bcf0f276661e2cb48ad52f17c4e01176bbf1e9bf58",
"https://bcr.bazel.build/modules/aspect_rules_esbuild/0.25.0/source.json": "5e42968c6d23ab8bd95c02634b16864d866334347827cb6a8425b86c11cc4363",
"https://bcr.bazel.build/modules/aspect_rules_jasmine/2.0.2/MODULE.bazel": "45f054400ff242c4433f6d7f20f6123a9a72739cb7a1f44247d738db1644f46c",
@@ -34,7 +33,6 @@
"https://bcr.bazel.build/modules/aspect_rules_rollup/2.0.1/source.json": "2fe8ac1ccb4de74bf884761e070010280b272d94e3997205b361b91c75409726",
"https://bcr.bazel.build/modules/aspect_rules_ts/3.6.3/MODULE.bazel": "d09db394970f076176ce7bab5b5fa7f0d560fd4f30b8432ea5e2c2570505b130",
"https://bcr.bazel.build/modules/aspect_rules_ts/3.7.0/MODULE.bazel": "5aace216caf88638950ef061245d23c36f57c8359e56e97f02a36f70bb09c50f",
"https://bcr.bazel.build/modules/aspect_rules_ts/3.8.0/MODULE.bazel": "b1ab5a46cc68c11192613035703606482b115516969788b8c313ad583395366d",
"https://bcr.bazel.build/modules/aspect_rules_ts/3.8.1/MODULE.bazel": "796622c65ae3008374fc2d77c32ddb4ef6da9fe891826ce648f70033a48b3667",
"https://bcr.bazel.build/modules/aspect_rules_ts/3.8.1/source.json": "a7c4f332f5c21f4e63d073f8dda40bf278d5307499fb307b35058dba558f417a",
"https://bcr.bazel.build/modules/aspect_tools_telemetry/0.2.3/MODULE.bazel": "20f53b145f40957a51077ae90b37b7ce83582a1daf9350349f0f86179e19dd0d",
@@ -165,8 +163,8 @@
"https://bcr.bazel.build/modules/rules_nodejs/6.6.2/source.json": "6e8c1ecc64ff8da147c1620f862ad77d7b19c5d1b52b3aa5e847d5b3d0de4cc3",
"https://bcr.bazel.build/modules/rules_pkg/0.7.0/MODULE.bazel": "df99f03fc7934a4737122518bb87e667e62d780b610910f0447665a7e2be62dc",
"https://bcr.bazel.build/modules/rules_pkg/1.0.1/MODULE.bazel": "5b1df97dbc29623bccdf2b0dcd0f5cb08e2f2c9050aab1092fd39a41e82686ff",
"https://bcr.bazel.build/modules/rules_pkg/1.1.0/MODULE.bazel": "9db8031e71b6ef32d1846106e10dd0ee2deac042bd9a2de22b4761b0c3036453",
"https://bcr.bazel.build/modules/rules_pkg/1.1.0/source.json": "fef768df13a92ce6067e1cd0cdc47560dace01354f1d921cfb1d632511f7d608",
"https://bcr.bazel.build/modules/rules_pkg/1.2.0/MODULE.bazel": "c7db3c2b407e673c7a39e3625dc05dc9f12d6682cbd82a3a5924a13b491eda7e",
"https://bcr.bazel.build/modules/rules_pkg/1.2.0/source.json": "9062e00845bf91a4247465d371baa837adf9b6ff44c542f73ba084f07667e1dc",
"https://bcr.bazel.build/modules/rules_proto/4.0.0/MODULE.bazel": "a7a7b6ce9bee418c1a760b3d84f83a299ad6952f9903c67f19e4edd964894e06",
"https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/MODULE.bazel": "e8dff86b0971688790ae75528fe1813f71809b5afd57facb44dad9e8eca631b7",
"https://bcr.bazel.build/modules/rules_proto/6.0.0/MODULE.bazel": "b531d7f09f58dce456cd61b4579ce8c86b38544da75184eadaf0a7cb7966453f",
@@ -202,7 +200,8 @@
"https://bcr.bazel.build/modules/yq.bzl/0.1.1/MODULE.bazel": "9039681f9bcb8958ee2c87ffc74bdafba9f4369096a2b5634b88abc0eaefa072",
"https://bcr.bazel.build/modules/yq.bzl/0.2.0/MODULE.bazel": "6f3a675677db8885be4d607fde14cc51829715e3a879fb016eb9bf336786ce6d",
"https://bcr.bazel.build/modules/yq.bzl/0.3.2/MODULE.bazel": "0384efa70e8033d842ea73aa4b7199fa099709e236a7264345c03937166670b6",
"https://bcr.bazel.build/modules/yq.bzl/0.3.2/source.json": "c4ec3e192477e154f08769e29d69e8fd36e8a4f0f623997f3e1f6f7d328f7d7d",
"https://bcr.bazel.build/modules/yq.bzl/0.3.4/MODULE.bazel": "d3a270662f5d766cd7229732d65a5a5bc485240c3007343dd279edfb60c9ae27",
"https://bcr.bazel.build/modules/yq.bzl/0.3.4/source.json": "786dafdc2843722da3416e4343ee1a05237227f068590779a6e8496a2064c0f9",
"https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0",
"https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/MODULE.bazel": "eec517b5bbe5492629466e11dae908d043364302283de25581e3eb944326c4ca",
"https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/source.json": "22bc55c47af97246cfc093d0acf683a7869377de362b5d1c552c2c2e16b7a806",
@@ -212,7 +211,7 @@
"moduleExtensions": {
"@@aspect_rules_esbuild+//esbuild:extensions.bzl%esbuild": {
"general": {
"bzlTransitiveDigest": "0aod3RK04ALA/OKTExzd7QqyeqcC4O2GWuDwUxhQHp4=",
"bzlTransitiveDigest": "9odiC0alKLq5PUYv/CZiw2yiMHsGpxvEhPsqt//fRRk=",
"usagesDigest": "ToTaCONCN/E05krnHXLM1kpV1zrHNxHrGpUip973II4=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
@@ -318,6 +317,11 @@
}
},
"recordedRepoMappingEntries": [
[
"aspect_bazel_lib+",
"bazel_lib",
"bazel_lib+"
],
[
"aspect_bazel_lib+",
"bazel_skylib",
@@ -408,7 +412,7 @@
},
"@@aspect_rules_js+//npm:extensions.bzl%pnpm": {
"general": {
"bzlTransitiveDigest": "tQ+7EwLfQwqi/T4v5/N3NNHTmP6Wu/FqXxRDndEB2OU=",
"bzlTransitiveDigest": "zVV86HvMwDBJ8IsFt27k/Sjq0vCMPr8b8X9OAuprQ6w=",
"usagesDigest": "3ECQVM1bdy/C4w7vxglaG1gt0S98v0T14gP5wNiDgwY=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
@@ -467,6 +471,11 @@
}
},
"recordedRepoMappingEntries": [
[
"aspect_bazel_lib+",
"bazel_lib",
"bazel_lib+"
],
[
"aspect_bazel_lib+",
"bazel_skylib",
@@ -718,7 +727,7 @@
},
"@@rules_browsers+//browsers:extensions.bzl%browsers": {
"general": {
"bzlTransitiveDigest": "ljZlVgWkQJnI6EvlHVfYit2EttUE52gDTbvmota5YO8=",
"bzlTransitiveDigest": "agkaLQ8wE1r/5IX6pkERzFxI/z0M42Em+ICNO6TXsVo=",
"usagesDigest": "FS7q5WaIwg3KirS3njhuPFkTIBYvDaTInVGrlzu0XL8=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
@@ -727,9 +736,9 @@
"rules_browsers_chrome_linux": {
"repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo",
"attributes": {
"sha256": "1419fa328bd7ea2697f26412ec693867516e4ef23c32eb13143a0b0b179b604b",
"sha256": "0a2ff0fc9eb5958b7b420f20e3968f424be7423fef89739e71565a48aa073a57",
"urls": [
"https://storage.googleapis.com/chrome-for-testing-public/144.0.7531.0/linux64/chrome-headless-shell-linux64.zip"
"https://storage.googleapis.com/chrome-for-testing-public/145.0.7586.0/linux64/chrome-headless-shell-linux64.zip"
],
"named_files": {
"CHROME-HEADLESS-SHELL": "chrome-headless-shell-linux64/chrome-headless-shell"
@@ -745,9 +754,9 @@
"rules_browsers_chrome_mac": {
"repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo",
"attributes": {
"sha256": "792cbf9b77219b4476e41c49647bcd15e55f0988002fa1e4e6a720eb430c7eda",
"sha256": "e6076b1201d86f74c5eab982a239d5af83e66b1aa4d780bcb792698790e01d87",
"urls": [
"https://storage.googleapis.com/chrome-for-testing-public/144.0.7531.0/mac-x64/chrome-headless-shell-mac-x64.zip"
"https://storage.googleapis.com/chrome-for-testing-public/145.0.7586.0/mac-x64/chrome-headless-shell-mac-x64.zip"
],
"named_files": {
"CHROME-HEADLESS-SHELL": "chrome-headless-shell-mac-x64/chrome-headless-shell"
@@ -763,9 +772,9 @@
"rules_browsers_chrome_mac_arm": {
"repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo",
"attributes": {
"sha256": "f0c1917769775e826dfa69936381d0d95b06fe67cf631ecd842380d5de0e4c7f",
"sha256": "b74dbcf5543d916b02d0a133e2e7c6a4de251f06733f72c2c15ea8c42213f63b",
"urls": [
"https://storage.googleapis.com/chrome-for-testing-public/144.0.7531.0/mac-arm64/chrome-headless-shell-mac-arm64.zip"
"https://storage.googleapis.com/chrome-for-testing-public/145.0.7586.0/mac-arm64/chrome-headless-shell-mac-arm64.zip"
],
"named_files": {
"CHROME-HEADLESS-SHELL": "chrome-headless-shell-mac-arm64/chrome-headless-shell"
@@ -781,9 +790,9 @@
"rules_browsers_chrome_win64": {
"repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo",
"attributes": {
"sha256": "6ce0f20dd743a804890f45f5349370e1aa7cd3ac3482c04686fcff5fafd01bb3",
"sha256": "df1e612dc3b1615e182a1f11821052995913c39df37caa52699de21a68d030d2",
"urls": [
"https://storage.googleapis.com/chrome-for-testing-public/144.0.7531.0/win64/chrome-headless-shell-win64.zip"
"https://storage.googleapis.com/chrome-for-testing-public/145.0.7586.0/win64/chrome-headless-shell-win64.zip"
],
"named_files": {
"CHROME-HEADLESS-SHELL": "chrome-headless-shell-win64/chrome-headless-shell.exe"
@@ -799,9 +808,9 @@
"rules_browsers_chromedriver_linux": {
"repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo",
"attributes": {
"sha256": "baf4bf9d22881265487732f17d35a49e9aadd0837aa5c1c1eea520c8aa24a97f",
"sha256": "69c504306399d979a2766fea603c3fb9d3d87d46c75bddc9f2a049b4f636d57c",
"urls": [
"https://storage.googleapis.com/chrome-for-testing-public/144.0.7531.0/linux64/chromedriver-linux64.zip"
"https://storage.googleapis.com/chrome-for-testing-public/145.0.7586.0/linux64/chromedriver-linux64.zip"
],
"named_files": {
"CHROMEDRIVER": "chromedriver-linux64/chromedriver"
@@ -815,9 +824,9 @@
"rules_browsers_chromedriver_mac": {
"repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo",
"attributes": {
"sha256": "87560768d5aa203b37c0a1b8459a35b05e4ece54afee2df530f3bc33de4f63c5",
"sha256": "5fc9d6f594fc5f2568a15145f25116dd8e9c9a60baa8da4bb21a17650fb00e7e",
"urls": [
"https://storage.googleapis.com/chrome-for-testing-public/144.0.7531.0/mac-x64/chromedriver-mac-x64.zip"
"https://storage.googleapis.com/chrome-for-testing-public/145.0.7586.0/mac-x64/chromedriver-mac-x64.zip"
],
"named_files": {
"CHROMEDRIVER": "chromedriver-mac-x64/chromedriver"
@@ -831,9 +840,9 @@
"rules_browsers_chromedriver_mac_arm": {
"repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo",
"attributes": {
"sha256": "99821795fa7c87eb92fb15248e23b237c83f397486d22ad9a10771622c36a5a0",
"sha256": "14e92294c2c3639ca4e7d27e850588b619d698e2f8905cee368f07db2e1bf1e9",
"urls": [
"https://storage.googleapis.com/chrome-for-testing-public/144.0.7531.0/mac-arm64/chromedriver-mac-arm64.zip"
"https://storage.googleapis.com/chrome-for-testing-public/145.0.7586.0/mac-arm64/chromedriver-mac-arm64.zip"
],
"named_files": {
"CHROMEDRIVER": "chromedriver-mac-arm64/chromedriver"
@@ -847,9 +856,9 @@
"rules_browsers_chromedriver_win64": {
"repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo",
"attributes": {
"sha256": "6e180e234a710c3cbf69566f64a662ed85473db6ae82275fd359f80ab288df99",
"sha256": "cf641d2e176db95bcc158cd90eafd347ad4928fa0458a5f3bfd56c6d983e70db",
"urls": [
"https://storage.googleapis.com/chrome-for-testing-public/144.0.7531.0/win64/chromedriver-win64.zip"
"https://storage.googleapis.com/chrome-for-testing-public/145.0.7586.0/win64/chromedriver-win64.zip"
],
"named_files": {
"CHROMEDRIVER": "chromedriver-win64/chromedriver.exe"
@@ -863,9 +872,9 @@
"rules_browsers_firefox_linux": {
"repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo",
"attributes": {
"sha256": "00fb922cda6bab971e02bcbfb77923b0a234388ed7d77c23506ca0a1a61d4a86",
"sha256": "8d56f479cc398a537a60a3fa20dca92d8a41925113d3a67f534881a4e4d7e344",
"urls": [
"https://archive.mozilla.org/pub/firefox/releases/145.0/linux-x86_64/en-US/firefox-145.0.tar.xz"
"https://archive.mozilla.org/pub/firefox/releases/146.0/linux-x86_64/en-US/firefox-146.0.tar.xz"
],
"named_files": {
"FIREFOX": "firefox/firefox"
@@ -879,9 +888,9 @@
"rules_browsers_firefox_mac": {
"repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo",
"attributes": {
"sha256": "1c4556480deac8424049f3081a6de1e2c6de619bab3e8ce53e5a497b8d6d919e",
"sha256": "4b1645313887972d466cd82166ea571485c2c40a167f84624e3f3ca739993cc9",
"urls": [
"https://archive.mozilla.org/pub/firefox/releases/145.0/mac/en-US/Firefox%20145.0.dmg"
"https://archive.mozilla.org/pub/firefox/releases/146.0/mac/en-US/Firefox%20146.0.dmg"
],
"named_files": {
"FIREFOX": "Firefox.app/Contents/MacOS/firefox"
@@ -895,9 +904,9 @@
"rules_browsers_firefox_mac_arm": {
"repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo",
"attributes": {
"sha256": "1c4556480deac8424049f3081a6de1e2c6de619bab3e8ce53e5a497b8d6d919e",
"sha256": "4b1645313887972d466cd82166ea571485c2c40a167f84624e3f3ca739993cc9",
"urls": [
"https://archive.mozilla.org/pub/firefox/releases/145.0/mac/en-US/Firefox%20145.0.dmg"
"https://archive.mozilla.org/pub/firefox/releases/146.0/mac/en-US/Firefox%20146.0.dmg"
],
"named_files": {
"FIREFOX": "Firefox.app/Contents/MacOS/firefox"
@@ -911,9 +920,9 @@
"rules_browsers_firefox_win64": {
"repoRuleId": "@@rules_browsers+//browsers/private:browser_repo.bzl%browser_repo",
"attributes": {
"sha256": "4b0345c113242653d923b369fcbd48e3089c57658f8c1542f887c8a375d50306",
"sha256": "216870c89648f32450cfefb5cec417fcd66d480d5dc83f894bf99f5fd7f38dbb",
"urls": [
"https://archive.mozilla.org/pub/firefox/releases/145.0/win64/en-US/Firefox%20Setup%20145.0.exe"
"https://archive.mozilla.org/pub/firefox/releases/146.0/win64/en-US/Firefox%20Setup%20146.0.exe"
],
"named_files": {
"FIREFOX": "core/firefox.exe"
@@ -1078,7 +1087,7 @@
"@@rules_nodejs+//nodejs:extensions.bzl%node": {
"general": {
"bzlTransitiveDigest": "NwcLXHrbh2hoorA/Ybmcpjxsn/6avQmewDglodkDrgo=",
"usagesDigest": "eycVhpa7dyjfsYiyP69uXfJ7I60zQlL3Bd0gcVPq8+8=",
"usagesDigest": "QbHTNODxV2P0w/Zs/oELuqv6ovXgi81f6SUYmX7dNhc=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
@@ -1091,7 +1100,8 @@
"node_urls": [
"https://nodejs.org/dist/v{version}/{filename}"
],
"node_version": "22.21.1",
"node_version": "20.19.5",
"node_version_from_nvmrc": "@@//:.nvmrc",
"include_headers": false,
"platform": "linux_amd64"
}
@@ -1104,7 +1114,8 @@
"node_urls": [
"https://nodejs.org/dist/v{version}/{filename}"
],
"node_version": "22.21.1",
"node_version": "20.19.5",
"node_version_from_nvmrc": "@@//:.nvmrc",
"include_headers": false,
"platform": "linux_arm64"
}
@@ -1117,7 +1128,8 @@
"node_urls": [
"https://nodejs.org/dist/v{version}/{filename}"
],
"node_version": "22.21.1",
"node_version": "20.19.5",
"node_version_from_nvmrc": "@@//:.nvmrc",
"include_headers": false,
"platform": "linux_s390x"
}
@@ -1130,7 +1142,8 @@
"node_urls": [
"https://nodejs.org/dist/v{version}/{filename}"
],
"node_version": "22.21.1",
"node_version": "20.19.5",
"node_version_from_nvmrc": "@@//:.nvmrc",
"include_headers": false,
"platform": "linux_ppc64le"
}
@@ -1143,7 +1156,8 @@
"node_urls": [
"https://nodejs.org/dist/v{version}/{filename}"
],
"node_version": "22.21.1",
"node_version": "20.19.5",
"node_version_from_nvmrc": "@@//:.nvmrc",
"include_headers": false,
"platform": "darwin_amd64"
}
@@ -1156,7 +1170,8 @@
"node_urls": [
"https://nodejs.org/dist/v{version}/{filename}"
],
"node_version": "22.21.1",
"node_version": "20.19.5",
"node_version_from_nvmrc": "@@//:.nvmrc",
"include_headers": false,
"platform": "darwin_arm64"
}
@@ -1169,7 +1184,8 @@
"node_urls": [
"https://nodejs.org/dist/v{version}/{filename}"
],
"node_version": "22.21.1",
"node_version": "20.19.5",
"node_version_from_nvmrc": "@@//:.nvmrc",
"include_headers": false,
"platform": "windows_amd64"
}
@@ -1182,7 +1198,8 @@
"node_urls": [
"https://nodejs.org/dist/v{version}/{filename}"
],
"node_version": "22.21.1",
"node_version": "20.19.5",
"node_version_from_nvmrc": "@@//:.nvmrc",
"include_headers": false,
"platform": "windows_arm64"
}
@@ -4005,8 +4022,8 @@
},
"@@yq.bzl+//yq:extensions.bzl%yq": {
"general": {
"bzlTransitiveDigest": "61Uz+o5PnlY0jJfPZEUNqsKxnM/UCLeWsn5VVCc8u5Y=",
"usagesDigest": "RMl/keg0xfMeM4V6asCKGMUDCuo+wyQ3styJIK7uDok=",
"bzlTransitiveDigest": "tDqk+ntWTdxNAWPDjRY1uITgHbti2jcXR5ZdinltBs0=",
"usagesDigest": "fKeGl7m4X6ZwRQS4Up/5Bzx+9laXzZyKEn6s9ODJQTc=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
@@ -4015,56 +4032,63 @@
"repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo",
"attributes": {
"platform": "darwin_amd64",
"version": "4.45.1"
"version": "4.45.2"
}
},
"yq_darwin_arm64": {
"repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo",
"attributes": {
"platform": "darwin_arm64",
"version": "4.45.1"
"version": "4.45.2"
}
},
"yq_linux_amd64": {
"repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo",
"attributes": {
"platform": "linux_amd64",
"version": "4.45.1"
"version": "4.45.2"
}
},
"yq_linux_arm64": {
"repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo",
"attributes": {
"platform": "linux_arm64",
"version": "4.45.1"
"version": "4.45.2"
}
},
"yq_linux_s390x": {
"repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo",
"attributes": {
"platform": "linux_s390x",
"version": "4.45.1"
"version": "4.45.2"
}
},
"yq_linux_riscv64": {
"repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo",
"attributes": {
"platform": "linux_riscv64",
"version": "4.45.1"
"version": "4.45.2"
}
},
"yq_linux_ppc64le": {
"repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo",
"attributes": {
"platform": "linux_ppc64le",
"version": "4.45.1"
"version": "4.45.2"
}
},
"yq_windows_amd64": {
"repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo",
"attributes": {
"platform": "windows_amd64",
"version": "4.45.1"
"version": "4.45.2"
}
},
"yq_windows_arm64": {
"repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo",
"attributes": {
"platform": "windows_arm64",
"version": "4.45.2"
}
},
"yq_toolchains": {
+6 -6
View File
@@ -5,21 +5,21 @@
"@algolia/requester-browser-xhr": "5.41.0",
"@algolia/requester-node-http": "5.41.0",
"@angular/animations": "workspace:*",
"@angular/aria": "21.0.3",
"@angular/build": "21.0.3",
"@angular/cdk": "21.0.3",
"@angular/cli": "21.0.3",
"@angular/aria": "21.0.5",
"@angular/build": "21.0.5",
"@angular/cdk": "21.0.5",
"@angular/cli": "21.0.5",
"@angular/common": "workspace:*",
"@angular/compiler": "workspace:*",
"@angular/compiler-cli": "workspace:*",
"@angular/core": "workspace:*",
"@angular/docs": "workspace:*",
"@angular/forms": "workspace:*",
"@angular/material": "21.0.3",
"@angular/material": "21.0.5",
"@angular/platform-browser": "workspace:*",
"@angular/platform-server": "workspace:*",
"@angular/router": "workspace:*",
"@angular/ssr": "21.0.3",
"@angular/ssr": "21.0.5",
"@codemirror/autocomplete": "6.19.1",
"@codemirror/commands": "6.10.0",
"@codemirror/lang-angular": "0.1.4",
@@ -13,10 +13,8 @@ import {NavigationState} from '../../services';
import {NavigationItem} from '../../interfaces';
import {By} from '@angular/platform-browser';
import {provideRouter} from '@angular/router';
import {provideZonelessChangeDetection} from '@angular/core';
describe('Breadcrumb', () => {
let fixture: ComponentFixture<Breadcrumb>;
let navigationStateSpy: jasmine.SpyObj<NavigationState>;
beforeEach(() => {
@@ -26,20 +24,19 @@ describe('Breadcrumb', () => {
imports: [Breadcrumb],
providers: [
provideRouter([]),
provideZonelessChangeDetection(),
{
provide: NavigationState,
useValue: navigationStateSpy,
},
],
});
fixture = TestBed.createComponent(Breadcrumb);
});
it('should display proper breadcrumb structure based on navigation state', () => {
it('should display proper breadcrumb structure based on navigation state', async () => {
navigationStateSpy.activeNavigationItem.and.returnValue(item);
const fixture = TestBed.createComponent(Breadcrumb);
await fixture.whenStable();
fixture.detectChanges();
const breadcrumbs = fixture.debugElement.queryAll(By.css('.docs-breadcrumb span'));
expect(breadcrumbs.length).toBe(2);
@@ -47,10 +44,11 @@ describe('Breadcrumb', () => {
expect(breadcrumbs[1].nativeElement.innerText).toEqual('Parent');
});
it('should display breadcrumb links when navigation item has got path', () => {
it('should display breadcrumb links when navigation item has got path', async () => {
navigationStateSpy.activeNavigationItem.and.returnValue(exampleItemWithPath);
fixture.detectChanges();
const fixture = TestBed.createComponent(Breadcrumb);
await fixture.whenStable();
const breadcrumbs = fixture.debugElement.queryAll(By.css('.docs-breadcrumb a'));
expect(breadcrumbs.length).toBe(1);
@@ -11,7 +11,6 @@ import {ComponentFixture, TestBed} from '@angular/core/testing';
import {CookiePopup, STORAGE_KEY} from './cookie-popup.component';
import {LOCAL_STORAGE} from '../../providers/index';
import {MockLocalStorage} from '../../testing/index';
import {provideZonelessChangeDetection} from '@angular/core';
describe('CookiePopup', () => {
let fixture: ComponentFixture<CookiePopup>;
@@ -21,7 +20,6 @@ describe('CookiePopup', () => {
TestBed.configureTestingModule({
imports: [CookiePopup],
providers: [
provideZonelessChangeDetection(),
{
provide: LOCAL_STORAGE,
useValue: mockLocalStorage,
@@ -30,34 +28,34 @@ describe('CookiePopup', () => {
});
});
it('should make the popup visible by default', () => {
initComponent(false);
it('should make the popup visible by default', async () => {
await initComponent(false);
expect(getCookiesPopup()).not.toBeNull();
});
it('should hide the cookies popup if the user has already accepted cookies', () => {
initComponent(true);
it('should hide the cookies popup if the user has already accepted cookies', async () => {
await initComponent(true);
expect(getCookiesPopup()).toBeNull();
});
it('should hide the cookies popup', () => {
initComponent(false);
it('should hide the cookies popup', async () => {
await initComponent(false);
accept();
fixture.detectChanges();
await fixture.whenStable();
expect(getCookiesPopup()).toBeNull();
});
it('should store the user confirmation', () => {
initComponent(false);
it('should store the user confirmation', async () => {
await initComponent(false);
expect(mockLocalStorage.getItem(STORAGE_KEY)).toBeNull();
accept();
await fixture.whenStable();
expect(mockLocalStorage.getItem(STORAGE_KEY)).toBe('true');
});
@@ -73,10 +71,10 @@ describe('CookiePopup', () => {
?.click();
}
function initComponent(cookiesAccepted: boolean) {
async function initComponent(cookiesAccepted: boolean) {
mockLocalStorage.setItem(STORAGE_KEY, cookiesAccepted ? 'true' : null);
fixture = TestBed.createComponent(CookiePopup);
fixture.detectChanges();
await fixture.whenStable();
}
});
@@ -12,12 +12,7 @@ import {
CONFIRMATION_DISPLAY_TIME_MS,
CopySourceCodeButton,
} from './copy-source-code-button.component';
import {
ChangeDetectionStrategy,
Component,
provideZonelessChangeDetection,
signal,
} from '@angular/core';
import {ChangeDetectionStrategy, Component, signal} from '@angular/core';
import {By} from '@angular/platform-browser';
import {Clipboard} from '@angular/cdk/clipboard';
@@ -32,7 +27,6 @@ describe('CopySourceCodeButton', () => {
beforeEach(async () => {
TestBed.configureTestingModule({
imports: [CodeSnippetWrapper],
providers: [provideZonelessChangeDetection()],
});
fixture = TestBed.createComponent(CodeSnippetWrapper);
component = fixture.componentInstance;
@@ -56,20 +50,20 @@ describe('CopySourceCodeButton', () => {
expect(copySpy.calls.argsFor(0)[0].trim()).toBe(expectedCodeToBeCopied);
});
it(`should set ${SUCCESSFULLY_COPY_CLASS_NAME} for ${CONFIRMATION_DISPLAY_TIME_MS} ms when copy was executed properly`, () => {
it(`should set ${SUCCESSFULLY_COPY_CLASS_NAME} for ${CONFIRMATION_DISPLAY_TIME_MS} ms when copy was executed properly`, async () => {
const clock = jasmine.clock().install();
component.code.set('example');
fixture.detectChanges();
await fixture.whenStable();
const button = fixture.debugElement.query(By.directive(CopySourceCodeButton)).nativeElement;
button.click();
fixture.detectChanges();
await fixture.whenStable();
expect(button).toHaveClass(SUCCESSFULLY_COPY_CLASS_NAME);
clock.tick(CONFIRMATION_DISPLAY_TIME_MS);
fixture.detectChanges();
await fixture.whenStable();
expect(button).not.toHaveClass(SUCCESSFULLY_COPY_CLASS_NAME);
clock.uninstall();
@@ -80,17 +74,17 @@ describe('CopySourceCodeButton', () => {
component.code.set('example');
copySpy.and.throwError('Fake copy error');
fixture.detectChanges();
await fixture.whenStable();
const button = fixture.debugElement.query(By.directive(CopySourceCodeButton)).nativeElement;
button.click();
fixture.detectChanges();
await fixture.whenStable();
expect(button).toHaveClass(FAILED_COPY_CLASS_NAME);
clock.tick(CONFIRMATION_DISPLAY_TIME_MS);
fixture.detectChanges();
await fixture.whenStable();
expect(button).not.toHaveClass(FAILED_COPY_CLASS_NAME);
clock.uninstall();
@@ -1,16 +1,25 @@
<ng-template #navigationList let-navigationItems>
<ng-template
#navigationList
let-navigationItems
let-preserveOtherCategoryOrder="preserveOtherCategoryOrder"
>
<ul
class="docs-navigation-list docs-faceted-list"
[class.docs-navigation-list-dropdown]="isDropdownView()"
>
@for (itemGroup of groupItems(navigationItems); track $index) {
@for (itemGroup of groupItems(navigationItems, preserveOtherCategoryOrder); track $index) {
@let groupLabel = itemGroup[0];
@let items = itemGroup[1];
@if (groupLabel) {
<li class="docs-navigation-group">{{ groupLabel | titlecase }}</li>
@let firstItem = items[0];
@if (groupLabel && collapsableLevel() !== firstItem.level && items.length > 1) {
<li class="docs-navigation-group">{{ groupLabel }}</li>
}
@for (item of items; track $index) {
<li class="docs-faceted-list-item">
<li
class="docs-faceted-list-item"
[class.single-entry]="items.length === 1 && item.level !== 1"
>
@let itemLabel = item.label!;
@if (item.path) {
@if (item.isExternal) {
@@ -28,7 +37,6 @@
class="docs-faceted-list-item-text"
>
{{ item.label }}
<ng-container *ngTemplateOutlet="itemStatus; context: {$implicit: item}" />
</span>
@if (item.children && item.level! > 1 && !item.isExpanded) {
@@ -122,7 +130,11 @@
<ng-container
*ngTemplateOutlet="
navigationList;
context: {$implicit: item.children, level: item.level}
context: {
$implicit: item.children,
level: item.level,
preserveOtherCategoryOrder: item.preserveOtherCategoryOrder,
}
"
/>
}
@@ -132,7 +144,15 @@
</ul>
</ng-template>
<ng-container *ngTemplateOutlet="navigationList; context: {$implicit: navigationItems()}" />
<ng-container
*ngTemplateOutlet="
navigationList;
context: {
$implicit: navigationItems(),
preserveOtherCategoryOrder: preserveOtherCategoryOrder(),
}
"
/>
<ng-template let-item #itemStatus>
@if (item.status === 'new') {
@@ -194,3 +194,7 @@ a,
margin-top: 2rem;
}
}
.single-entry:not(:first-child) {
margin-top: 2rem;
}
@@ -8,12 +8,12 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {NavigationList} from './navigation-list.component';
import {signal} from '@angular/core';
import {By} from '@angular/platform-browser';
import {NavigationItem} from '../../interfaces';
import {provideRouter} from '@angular/router';
import {provideZonelessChangeDetection, signal} from '@angular/core';
import {NavigationItem} from '../../interfaces';
import {NavigationState} from '../../services';
import {NavigationList} from './navigation-list.component';
const navigationItems: NavigationItem[] = [
{
@@ -38,21 +38,18 @@ describe('NavigationList', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [NavigationList],
providers: [
provideRouter([]),
{provide: NavigationState, useClass: FakeNavigationListState},
provideZonelessChangeDetection(),
],
providers: [provideRouter([]), {provide: NavigationState, useClass: FakeNavigationListState}],
});
fixture = TestBed.createComponent(NavigationList);
fixture.componentRef.setInput('navigationItems', []);
fixture.componentRef.setInput('preserveOtherCategoryOrder', false);
component = fixture.componentInstance;
});
it('should display provided navigation structure', () => {
it('should display provided navigation structure', async () => {
fixture.componentRef.setInput('navigationItems', [...navigationItems]);
fixture.detectChanges();
await fixture.whenStable();
const links = fixture.debugElement.queryAll(By.css('a'));
const nonClickableItem = fixture.debugElement.queryAll(By.css('.docs-secondary-nav-header'));
@@ -61,28 +58,28 @@ describe('NavigationList', () => {
expect(nonClickableItem.length).toBe(1);
});
it('should append `docs-navigation-list-dropdown` when isDropdownView is true', () => {
it('should append `docs-navigation-list-dropdown` when isDropdownView is true', async () => {
fixture.componentRef.setInput('isDropdownView', true);
fixture.detectChanges();
await fixture.whenStable();
const ulElement = fixture.debugElement.query(By.css('ul.docs-navigation-list-dropdown'));
expect(ulElement).toBeTruthy();
});
it('should not append `docs-navigation-list-dropdown` when isDropdownView is false', () => {
it('should not append `docs-navigation-list-dropdown` when isDropdownView is false', async () => {
fixture.componentRef.setInput('isDropdownView', false);
fixture.detectChanges();
await fixture.whenStable();
const ulElement = fixture.debugElement.query(By.css('ul.docs-navigation-list-dropdown'));
expect(ulElement).toBeFalsy();
});
it('should emit linkClicked when user clicked on link', () => {
it('should emit linkClicked when user clicked on link', async () => {
const emitClickOnLinkSpy = spyOn(component, 'emitClickOnLink');
fixture.componentRef.setInput('navigationItems', [...navigationItems]);
fixture.detectChanges(true);
await fixture.whenStable();
const guideLink = fixture.debugElement.query(By.css('a[href="/guide"]'));
guideLink.nativeElement.click();
@@ -122,10 +119,10 @@ describe('NavigationList', () => {
expect(toggleItemSpy).toHaveBeenCalledOnceWith(itemToToggle);
});
it('should display only items to provided level (Level 1)', () => {
it('should display only items to provided level (Level 1)', async () => {
fixture.componentRef.setInput('navigationItems', [...navigationItems]);
fixture.componentRef.setInput('displayItemsToLevel', 1);
fixture.detectChanges(true);
await fixture.whenStable();
const items = fixture.debugElement.queryAll(By.css('li.docs-faceted-list-item'));
@@ -134,10 +131,10 @@ describe('NavigationList', () => {
expect(items[1].nativeElement.innerText).toBe(navigationItems[1].label);
});
it('should display all items (Level 2)', () => {
it('should display all items (Level 2)', async () => {
fixture.componentRef.setInput('navigationItems', [...navigationItems]);
fixture.componentRef.setInput('displayItemsToLevel', 2);
fixture.detectChanges(true);
await fixture.whenStable();
const items = fixture.debugElement.queryAll(By.css('li.docs-faceted-list-item'));
@@ -6,15 +6,14 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {NgTemplateOutlet} from '@angular/common';
import {ChangeDetectionStrategy, Component, inject, input, output} from '@angular/core';
import {NavigationItem} from '../../interfaces/index';
import {NavigationState} from '../../services/index';
import {RouterLink, RouterLinkActive} from '@angular/router';
import {IconComponent} from '../icon/icon.component';
import {IsActiveNavigationItem} from '../../pipes';
import {NgTemplateOutlet, TitleCasePipe} from '@angular/common';
import {MatTooltipModule} from '@angular/material/tooltip';
import {ReactiveFormsModule} from '@angular/forms';
import {RouterLink, RouterLinkActive} from '@angular/router';
import {NavigationItem} from '../../interfaces/index';
import {IsActiveNavigationItem} from '../../pipes';
import {NavigationState} from '../../services/index';
import {IconComponent} from '../icon/icon.component';
@Component({
selector: 'docs-navigation-list',
@@ -25,8 +24,6 @@ import {ReactiveFormsModule} from '@angular/forms';
IsActiveNavigationItem,
NgTemplateOutlet,
MatTooltipModule,
ReactiveFormsModule,
TitleCasePipe,
],
templateUrl: './navigation-list.component.html',
styleUrls: ['./navigation-list.component.scss'],
@@ -34,6 +31,7 @@ import {ReactiveFormsModule} from '@angular/forms';
})
export class NavigationList {
readonly navigationItems = input.required<NavigationItem[]>();
readonly preserveOtherCategoryOrder = input.required<boolean>();
readonly displayItemsToLevel = input(2);
readonly collapsableLevel = input<number | undefined>();
readonly expandableLevel = input(2);
@@ -64,14 +62,17 @@ export class NavigationList {
return items.some((item) => !!item.category);
}
protected groupItems(items: NavigationItem[]): Map<string, NavigationItem[]> {
protected groupItems(
items: NavigationItem[],
preserveOtherCategoryOrder: boolean,
): Map<string, NavigationItem[]> {
const hasCategories = this.hasCategories(items);
if (hasCategories) {
const others: NavigationItem[] = [];
const categorizedItems = new Map<string, NavigationItem[]>();
for (const item of items) {
const category = item.category || 'Other';
if (category === 'Other') {
if (!preserveOtherCategoryOrder && category === 'Other') {
others.push(item);
continue;
}
@@ -3,7 +3,7 @@
<docs-text-field
[autofocus]="true"
[hideIcon]="true"
[formControl]="searchControl"
[formField]="searchForm"
[resetLabel]="'Clear the search'"
class="docs-search-input"
placeholder="Search docs"
@@ -59,7 +59,7 @@
</li>
}
</ul>
} @else if (history.hasItems() && !this.searchControl.value.length) {
} @else if (history.hasItems() && !this.searchForm().value().length) {
<docs-search-history />
} @else {
<div class="docs-search-results docs-mini-scroll-track">
@@ -6,16 +6,16 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {ApplicationRef} from '@angular/core';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {Router, provideRouter} from '@angular/router';
import {SearchDialog} from './search-dialog.component';
import {ENVIRONMENT, WINDOW} from '../../providers';
import {ALGOLIA_CLIENT, Search} from '../../services';
import {FakeEventTarget} from '../../testing/index';
import {By} from '@angular/platform-browser';
import {AlgoliaIcon} from '../algolia-icon/algolia-icon.component';
import {Router, provideRouter} from '@angular/router';
import {ApplicationRef, provideZonelessChangeDetection, ResourceStatus} from '@angular/core';
import {SearchResult} from '../../interfaces';
describe('SearchDialog', () => {
@@ -34,7 +34,6 @@ describe('SearchDialog', () => {
imports: [SearchDialog],
providers: [
provideRouter([]),
provideZonelessChangeDetection(),
{provide: ENVIRONMENT, useValue: {algolia: {index: 'fakeIndex'}}},
{provide: ALGOLIA_CLIENT, useValue: {search: searchResults}},
{provide: WINDOW, useValue: fakeWindow},
@@ -42,8 +41,8 @@ describe('SearchDialog', () => {
});
fixture = TestBed.createComponent(SearchDialog);
fixture.detectChanges();
search = TestBed.inject(Search);
await fixture.whenStable();
});
it('should navigate to active item when user pressed Enter', async () => {
@@ -98,7 +97,6 @@ describe('SearchDialog', () => {
it('should display `Start typing to see results` message when there are no provided query', () => {
searchResults.and.returnValue(undefined);
fixture.detectChanges();
const startTypingContainer = fixture.debugElement.query(
By.css('.docs-search-results__start-typing'),
@@ -7,32 +7,32 @@
*/
import {
afterNextRender,
ChangeDetectionStrategy,
Component,
DestroyRef,
ElementRef,
Injector,
afterNextRender,
effect,
ElementRef,
inject,
Injector,
output,
viewChild,
viewChildren,
} from '@angular/core';
import {WINDOW} from '../../providers';
import {ClickOutside, SearchItem} from '../../directives';
import {WINDOW} from '../../providers';
import {Search, SearchHistory} from '../../services';
import {TextField} from '../text-field/text-field.component';
import {FormControl, ReactiveFormsModule} from '@angular/forms';
import {ActiveDescendantKeyManager} from '@angular/cdk/a11y';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {form, FormField} from '@angular/forms/signals';
import {Router, RouterLink} from '@angular/router';
import {fromEvent} from 'rxjs';
import {AlgoliaIcon} from '../algolia-icon/algolia-icon.component';
import {RelativeLink} from '../../pipes';
import {AlgoliaIcon} from '../algolia-icon/algolia-icon.component';
import {SearchHistoryComponent} from '../search-history/search-history.component';
import {TextField} from '../text-field/text-field.component';
@Component({
selector: 'docs-search-dialog',
@@ -40,7 +40,7 @@ import {SearchHistoryComponent} from '../search-history/search-history.component
imports: [
ClickOutside,
TextField,
ReactiveFormsModule,
FormField,
SearchItem,
AlgoliaIcon,
RelativeLink,
@@ -67,22 +67,14 @@ export class SearchDialog {
this.injector,
).withWrap();
readonly searchQuery = this.search.searchQuery;
readonly resultsResource = this.search.resultsResource;
readonly searchResults = this.search.searchResults;
// We use a FormControl instead of relying on NgModel+signal to avoid
// the issue https://github.com/angular/angular/issues/13568
// TODO: Use signal forms when available
searchControl = new FormControl(this.searchQuery(), {nonNullable: true});
searchForm = form(this.search.searchQuery);
constructor() {
inject(DestroyRef).onDestroy(() => this.keyManager.destroy());
this.searchControl.valueChanges.pipe(takeUntilDestroyed()).subscribe((value) => {
this.searchQuery.set(value);
});
// Thinking about refactoring this to a single afterRenderEffect ?
// Answer: It won't have the same behavior
effect(() => {
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {provideZonelessChangeDetection, ApplicationRef} from '@angular/core';
import {ApplicationRef} from '@angular/core';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {provideRouter} from '@angular/router';
@@ -75,11 +75,7 @@ describe('SearchHistoryComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
providers: [
provideZonelessChangeDetection(),
{provide: LOCAL_STORAGE, useClass: MockLocalStorage},
provideRouter([]),
],
providers: [{provide: LOCAL_STORAGE, useClass: MockLocalStorage}, provideRouter([])],
});
fixture = TestBed.createComponent(SearchHistoryComponent);
@@ -1,10 +1,10 @@
<select
[attr.id]="id()"
[attr.name]="name()"
[ngModel]="selectedOption()"
(ngModelChange)="setOption($event)"
[value]="value()"
(change)="setOption($any($event.target).value)"
>
@for (item of options(); track item) {
<option [value]="item.value">{{ item.label }}</option>
<option [value]="item.value" [selected]="value() === item.value">{{ item.label }}</option>
}
</select>
@@ -9,17 +9,12 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {Select} from './select.component';
import {provideZonelessChangeDetection} from '@angular/core';
describe('Select', () => {
let component: Select;
let fixture: ComponentFixture<Select>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [Select],
providers: [provideZonelessChangeDetection()],
});
beforeEach(async () => {
fixture = TestBed.createComponent(Select);
component = fixture.componentInstance;
@@ -28,7 +23,7 @@ describe('Select', () => {
fixture.componentRef.setInput('name', 'name');
fixture.componentRef.setInput('options', []);
fixture.detectChanges();
await fixture.whenStable();
});
it('should create', () => {
@@ -6,8 +6,8 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {ControlValueAccessor, NG_VALUE_ACCESSOR, FormsModule} from '@angular/forms';
import {ChangeDetectionStrategy, Component, model, forwardRef, input, signal} from '@angular/core';
import {ChangeDetectionStrategy, Component, input, model} from '@angular/core';
import {FormValueControl} from '@angular/forms/signals';
type SelectOptionValue = string | number | boolean;
@@ -19,59 +19,25 @@ export interface SelectOption {
@Component({
selector: 'docs-select',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [FormsModule],
templateUrl: './select.component.html',
styleUrls: ['./select.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => Select),
multi: true,
},
],
host: {
class: 'docs-form-element',
},
})
export class Select implements ControlValueAccessor {
export class Select implements FormValueControl<string | null> {
readonly value = model<string | null>(null);
readonly id = input.required<string>({alias: 'selectId'});
readonly name = input.required<string>();
readonly options = input.required<SelectOption[]>();
readonly disabled = model(false);
// Implemented as part of ControlValueAccessor.
private onChange: (value: SelectOptionValue) => void = (_: SelectOptionValue) => {};
private onTouched: () => void = () => {};
protected readonly selectedOption = signal<SelectOptionValue | null>(null);
// Implemented as part of ControlValueAccessor.
writeValue(value: SelectOptionValue): void {
this.selectedOption.set(value);
}
// Implemented as part of ControlValueAccessor.
registerOnChange(fn: any): void {
this.onChange = fn;
}
// Implemented as part of ControlValueAccessor.
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
// Implemented as part of ControlValueAccessor.
setDisabledState?(isDisabled: boolean): void {
this.disabled.set(isDisabled);
}
readonly disabled = input(false);
setOption($event: SelectOptionValue): void {
if (this.disabled()) {
return;
}
this.selectedOption.set($event);
this.onChange($event);
this.onTouched();
this.value.set($event as string);
}
}
@@ -9,22 +9,17 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {SlideToggle} from './slide-toggle.component';
import {provideZonelessChangeDetection} from '@angular/core';
describe('SlideToggle', () => {
let component: SlideToggle;
let fixture: ComponentFixture<SlideToggle>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [SlideToggle],
providers: [provideZonelessChangeDetection()],
});
beforeEach(async () => {
fixture = TestBed.createComponent(SlideToggle);
fixture.componentRef.setInput('buttonId', 'id');
fixture.componentRef.setInput('label', 'foo');
component = fixture.componentInstance;
fixture.detectChanges();
await fixture.whenStable();
});
it('should toggle the value when clicked', () => {
@@ -36,26 +31,13 @@ describe('SlideToggle', () => {
expect(component['checked']()).toBeTrue();
});
it('should call onChange and onTouched when toggled', () => {
const onChangeSpy = jasmine.createSpy('onChangeSpy');
const onTouchedSpy = jasmine.createSpy('onTouchedSpy');
component.registerOnChange(onChangeSpy);
component.registerOnTouched(onTouchedSpy);
component.toggle();
expect(onChangeSpy).toHaveBeenCalled();
expect(onChangeSpy).toHaveBeenCalledWith(true);
expect(onTouchedSpy).toHaveBeenCalled();
});
it('should set active class for button when is checked', () => {
component.writeValue(true);
component.checked.set(true);
fixture.detectChanges();
const buttonElement: HTMLButtonElement = fixture.nativeElement.querySelector('input');
expect(buttonElement.classList.contains('docs-toggle-active')).toBeTrue();
component.writeValue(false);
component.checked.set(false);
fixture.detectChanges();
expect(buttonElement.classList.contains('docs-toggle-active')).toBeFalse();
});
@@ -6,52 +6,21 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {ChangeDetectionStrategy, Component, forwardRef, model, input, signal} from '@angular/core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
import {ChangeDetectionStrategy, Component, input, model} from '@angular/core';
import {FormCheckboxControl} from '@angular/forms/signals';
@Component({
selector: 'docs-slide-toggle',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './slide-toggle.component.html',
styleUrls: ['./slide-toggle.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SlideToggle),
multi: true,
},
],
})
export class SlideToggle implements ControlValueAccessor {
export class SlideToggle implements FormCheckboxControl {
readonly buttonId = input.required<string>();
readonly label = input.required<string>();
readonly disabled = model(false);
// Implemented as part of ControlValueAccessor.
private onChange: (value: boolean) => void = (_: boolean) => {};
private onTouched: () => void = () => {};
protected readonly checked = signal(false);
// Implemented as part of ControlValueAccessor.
writeValue(value: boolean): void {
this.checked.set(value);
}
// Implemented as part of ControlValueAccessor.
registerOnChange(fn: any): void {
this.onChange = fn;
}
// Implemented as part of ControlValueAccessor.
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
// Implemented as part of ControlValueAccessor.
setDisabledState(isDisabled: boolean): void {
this.disabled.set(isDisabled);
}
readonly checked = model(false);
// Toggles the checked state of the slide-toggle.
toggle(): void {
@@ -60,7 +29,5 @@ export class SlideToggle implements ControlValueAccessor {
}
this.checked.update((checked) => !checked);
this.onChange(this.checked());
this.onTouched();
}
}
@@ -9,21 +9,20 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {TabGroup} from './tab-group.component';
import {provideZonelessChangeDetection} from '@angular/core';
describe('TabGroup', () => {
let fixture: ComponentFixture<TabGroup>;
beforeEach(() => {
beforeEach(async () => {
fixture = TestBed.createComponent(TabGroup);
fixture.detectChanges();
await fixture.whenStable();
});
it('should update tabs and tabpanels', () => {
it('should update tabs and tabpanels', async () => {
const testPanel = document.createElement('div');
testPanel.textContent = 'panel 1';
fixture.componentRef.setInput('tabs', [{label: 'tab 1', panel: testPanel}]);
fixture.detectChanges();
await fixture.whenStable();
const tabs = fixture.nativeElement.querySelectorAll('.docs-tab');
const tabpanels = fixture.nativeElement.querySelectorAll('.docs-tab-panel');
@@ -16,6 +16,7 @@ import {
afterRenderEffect,
Renderer2,
ElementRef,
ChangeDetectionStrategy,
} from '@angular/core';
import {_IdGenerator} from '@angular/cdk/a11y';
@@ -31,6 +32,7 @@ import {_IdGenerator} from '@angular/cdk/a11y';
host: {
class: 'docs-tab-group',
},
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TabGroup {
private readonly _renderer = inject(Renderer2);
@@ -12,7 +12,6 @@ import {provideRouter} from '@angular/router';
import {TableOfContentsItem, TableOfContentsLevel} from '../../interfaces/index';
import {TableOfContentsLoader} from '../../services/index';
import {WINDOW} from '../../providers/index';
import {provideZonelessChangeDetection, signal} from '@angular/core';
describe('TableOfContents', () => {
let component: TableOfContents;
@@ -40,11 +39,10 @@ describe('TableOfContents', () => {
};
beforeEach(async () => {
await TestBed.configureTestingModule({
TestBed.configureTestingModule({
imports: [TableOfContents],
providers: [
provideRouter([]),
provideZonelessChangeDetection(),
{
provide: WINDOW,
useValue: fakeWindow,
@@ -59,7 +57,7 @@ describe('TableOfContents', () => {
fixture.componentRef.setInput('contentSourceElement', document.createElement('div'));
component = fixture.componentInstance;
fixture.detectChanges();
await fixture.whenStable();
});
it('should create', () => {
@@ -69,8 +67,6 @@ describe('TableOfContents', () => {
it('should call scrollToTop when user click on Back to the top button', () => {
const spy = spyOn(component, 'scrollToTop');
fixture.detectChanges();
const button: HTMLButtonElement = fixture.nativeElement.querySelector('button');
button.click();
@@ -78,8 +74,6 @@ describe('TableOfContents', () => {
});
it('should render items when tableOfContentItems has value', () => {
fixture.detectChanges();
const renderedItems = fixture.nativeElement.querySelectorAll('li');
expect(renderedItems.length).toBe(3);
@@ -87,8 +81,6 @@ describe('TableOfContents', () => {
});
it('should append level class to element', () => {
fixture.detectChanges();
const h2Items = fixture.nativeElement.querySelectorAll('li.docs-toc-item-h2');
const h3Items = fixture.nativeElement.querySelectorAll('li.docs-toc-item-h3');
@@ -97,8 +89,6 @@ describe('TableOfContents', () => {
});
it('should append active class when item is active', () => {
fixture.detectChanges();
const activeItem = fixture.nativeElement.querySelector('.docs-faceted-list-item-active');
expect(activeItem).toBeDefined();
});
@@ -9,29 +9,24 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {TextField} from './text-field.component';
import {provideZonelessChangeDetection} from '@angular/core';
describe('TextField', () => {
let component: TextField;
let fixture: ComponentFixture<TextField>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [TextField],
providers: [provideZonelessChangeDetection()],
});
beforeEach(async () => {
fixture = TestBed.createComponent(TextField);
component = fixture.componentInstance;
fixture.detectChanges();
await fixture.whenStable();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should update DOM when setting the value via the CVA', () => {
it('should update DOM when setting the value via the CVA', async () => {
component.setValue('test');
fixture.detectChanges();
await fixture.whenStable();
expect(fixture.nativeElement.querySelector('input').value).toBe('test');
// If we were using ngModel instead of the value binding, we would get an empty string
@@ -7,91 +7,58 @@
*/
import {
afterNextRender,
ChangeDetectionStrategy,
Component,
ElementRef,
input,
afterNextRender,
forwardRef,
signal,
model,
viewChild,
} from '@angular/core';
import {ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR} from '@angular/forms';
import {FormValueControl} from '@angular/forms/signals';
import {IconComponent} from '../icon/icon.component';
@Component({
selector: 'docs-text-field',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [FormsModule, IconComponent],
imports: [IconComponent],
templateUrl: './text-field.component.html',
styleUrls: ['./text-field.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TextField),
multi: true,
},
],
host: {
class: 'docs-form-element',
},
})
export class TextField implements ControlValueAccessor {
export class TextField implements FormValueControl<string | null> {
readonly input = viewChild.required<ElementRef<HTMLInputElement>>('inputRef');
readonly name = input<string | null>(null);
readonly name = input<string>('');
readonly value = model<string | null>(null);
readonly placeholder = input<string | null>(null);
readonly disabled = model<boolean>(false);
readonly hideIcon = input<boolean>(false);
readonly autofocus = input<boolean>(false);
readonly resetLabel = input<string | null>(null);
// Implemented as part of ControlValueAccessor.
private onChange: (value: string) => void = (_: string) => {};
private onTouched: () => void = () => {};
protected readonly value = signal<string | null>(null);
constructor() {
afterNextRender(() => {
if (this.autofocus()) {
this.input().nativeElement.focus();
this.focus();
}
});
}
// Implemented as part of ControlValueAccessor.
writeValue(value: unknown): void {
this.value.set(typeof value === 'string' ? value : null);
}
// Implemented as part of ControlValueAccessor.
registerOnChange(fn: any): void {
this.onChange = fn;
}
// Implemented as part of ControlValueAccessor.
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
// Implemented as part of ControlValueAccessor.
setDisabledState?(isDisabled: boolean): void {
this.disabled.set(isDisabled);
}
setValue(value: string): void {
if (this.disabled()) {
return;
}
this.value.set(value);
this.onChange(value);
this.onTouched();
}
clearTextField() {
this.setValue('');
}
focus() {
this.input().nativeElement.focus();
}
}
@@ -34,13 +34,13 @@ describe('TopLevelBannerComponent', () => {
fixture.componentRef.setInput('id', EXAMPLE_ID);
component = fixture.componentInstance;
fixture.detectChanges();
await fixture.whenStable();
});
it('should render an anchor element when link is provided', () => {
it('should render an anchor element when link is provided', async () => {
fixture.componentRef.setInput('text', EXAMPLE_TEXT);
fixture.componentRef.setInput('link', EXAMPLE_LINK);
fixture.detectChanges();
await fixture.whenStable();
const bannerElement = fixture.nativeElement.querySelector('a.docs-top-level-banner');
expect(bannerElement).toBeTruthy();
@@ -48,49 +48,49 @@ describe('TopLevelBannerComponent', () => {
expect(bannerElement.textContent).toContain(EXAMPLE_TEXT);
});
it('should render a div element when link is not provided', () => {
it('should render a div element when link is not provided', async () => {
const EXAMPLE_TEXT = 'No Link Available';
fixture.componentRef.setInput('text', EXAMPLE_TEXT);
fixture.detectChanges();
await fixture.whenStable();
const bannerElement = fixture.nativeElement.querySelector('div.docs-top-level-banner');
expect(bannerElement).toBeTruthy();
expect(bannerElement.textContent).toContain(EXAMPLE_TEXT);
});
it('should correctly render the text input', () => {
it('should correctly render the text input', async () => {
const EXAMPLE_TEXT = 'Lorem ipsum dolor...';
fixture.componentRef.setInput('text', EXAMPLE_TEXT);
fixture.detectChanges();
await fixture.whenStable();
const bannerElement = fixture.nativeElement.querySelector('.docs-top-level-banner-cta');
expect(bannerElement).toBeTruthy();
expect(bannerElement.textContent).toBe(EXAMPLE_TEXT);
});
it('should set hasClosed to true if the banner was closed before', () => {
it('should set hasClosed to true if the banner was closed before', async () => {
mockLocalStorage.getItem.and.returnValue('true');
fixture = TestBed.createComponent(TopLevelBannerComponent);
fixture.componentRef.setInput('text', EXAMPLE_TEXT);
fixture.componentRef.setInput('id', EXAMPLE_ID);
component = fixture.componentInstance;
fixture.detectChanges();
await fixture.whenStable();
expect(component.hasClosed()).toBeTrue();
expect(mockLocalStorage.getItem).toHaveBeenCalledWith(`${STORAGE_KEY_PREFIX}${EXAMPLE_ID}`);
});
it('should set hasClosed to false if the banner was not closed before', () => {
it('should set hasClosed to false if the banner was not closed before', async () => {
mockLocalStorage.getItem.and.returnValue('false');
fixture = TestBed.createComponent(TopLevelBannerComponent);
fixture.componentRef.setInput('text', EXAMPLE_TEXT);
fixture.componentRef.setInput('id', EXAMPLE_ID);
component = fixture.componentInstance;
fixture.detectChanges();
await fixture.whenStable();
expect(component.hasClosed()).toBeFalse();
expect(mockLocalStorage.getItem).toHaveBeenCalledWith(`${STORAGE_KEY_PREFIX}${EXAMPLE_ID}`);
@@ -54,6 +54,7 @@
display: inline-block;
color: inherit;
max-width: 100%;
text-wrap: pretty;
code {
// This can be useful on mobile if the heading contains a long code block
@@ -19,11 +19,9 @@ import {NavigationState} from '../../../services';
import {CopySourceCodeButton} from '../../copy-source-code-button/copy-source-code-button.component';
import {CopyLinkButton} from '../../copy-link-anchor/copy-link-anchor.component';
import {TableOfContents} from '../../table-of-contents/table-of-contents.component';
import {provideZonelessChangeDetection} from '@angular/core';
import {Clipboard} from '@angular/cdk/clipboard';
describe('DocViewer', () => {
let fixture: ComponentFixture<DocViewer>;
let exampleContentSpy: jasmine.SpyObj<ExampleViewerContentLoader>;
let navigationStateSpy: jasmine.SpyObj<NavigationState>;
@@ -83,18 +81,15 @@ describe('DocViewer', () => {
navigationStateSpy = jasmine.createSpyObj(NavigationState, ['activeNavigationItem']);
});
beforeEach(async () => {
await TestBed.configureTestingModule({
beforeEach(() => {
TestBed.configureTestingModule({
imports: [DocViewer],
providers: [
provideRouter([]),
provideZonelessChangeDetection(),
{provide: EXAMPLE_VIEWER_CONTENT_LOADER, useValue: exampleContentSpy},
{provide: NavigationState, useValue: navigationStateSpy},
],
});
fixture = TestBed.createComponent(DocViewer);
});
it('should load doc into innerHTML', async () => {
@@ -10,7 +10,7 @@ import {ComponentFixture, TestBed} from '@angular/core/testing';
import {ExampleViewer} from './example-viewer.component';
import {ExampleMetadata, ExampleViewerContentLoader} from '../../../interfaces';
import {EXAMPLE_VIEWER_CONTENT_LOADER} from '../../../providers';
import {Component, provideZonelessChangeDetection, ComponentRef, signal} from '@angular/core';
import {Component, ComponentRef, signal} from '@angular/core';
import {HarnessLoader} from '@angular/cdk/testing';
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
import {Clipboard} from '@angular/cdk/clipboard';
@@ -32,10 +32,9 @@ describe('ExampleViewer', () => {
});
beforeEach(async () => {
await TestBed.configureTestingModule({
TestBed.configureTestingModule({
imports: [ExampleViewer],
providers: [
provideZonelessChangeDetection(),
{provide: EXAMPLE_VIEWER_CONTENT_LOADER, useValue: exampleContentSpy},
{provide: ActivatedRoute, useValue: {snapshot: {fragment: 'fragment'}}},
],
@@ -44,7 +43,7 @@ describe('ExampleViewer', () => {
component = fixture.componentInstance;
componentRef = fixture.componentRef;
loader = TestbedHarnessEnvironment.loader(fixture);
fixture.detectChanges();
await fixture.whenStable();
});
it('should set file extensions as tab names when all files have different extension', async () => {
@@ -131,7 +130,7 @@ describe('ExampleViewer', () => {
);
await component.renderExample();
fixture.detectChanges();
await fixture.whenStable();
const hiddenLine = fixture.debugElement.query(By.css('div[class="line hidden"]'));
expect(hiddenLine).toBeTruthy();
@@ -155,13 +154,13 @@ describe('ExampleViewer', () => {
);
await component.renderExample();
fixture.detectChanges();
await fixture.whenStable();
const expandButton = fixture.debugElement.query(
By.css('button[aria-label="Expand code example"]'),
);
expandButton.nativeElement.click();
fixture.detectChanges();
await fixture.whenStable();
const hiddenLine = fixture.debugElement.query(By.css('div[class="line hidden"]'));
expect(hiddenLine).toBeNull();
@@ -191,7 +190,7 @@ describe('ExampleViewer', () => {
);
componentRef.setInput('githubUrl', 'https://github.com/');
await component.renderExample();
fixture.detectChanges();
await fixture.whenStable();
const githubButton = fixture.debugElement.query(
By.css('a[aria-label="Open example on GitHub"]'),
@@ -212,7 +211,7 @@ describe('ExampleViewer', () => {
componentRef.setInput('stackblitzUrl', 'https://stackblitz.com/');
await component.renderExample();
fixture.detectChanges();
await fixture.whenStable();
const stackblitzButton = fixture.debugElement.query(
By.css('a[aria-label="Edit example in StackBlitz"]'),
@@ -261,7 +260,7 @@ describe('ExampleViewer', () => {
it('should call clipboard service when clicked on copy example link', async () => {
componentRef.setInput('metadata', getMetadata());
component.expanded.set(true);
fixture.detectChanges();
await fixture.whenStable();
const clipboardService = TestBed.inject(Clipboard);
const spy = spyOn(clipboardService, 'copy');
@@ -283,7 +282,7 @@ describe('ExampleViewer', () => {
);
await component.renderExample();
fixture.detectChanges();
await fixture.whenStable();
// Initially, the code should be hidden.
expect(component.showCode()).toBeFalse();
@@ -295,7 +294,7 @@ describe('ExampleViewer', () => {
componentRef.setInput('metadata', getMetadata());
await component.renderExample();
fixture.detectChanges();
await fixture.whenStable();
// Initially, the code should be visible.
expect(component.showCode()).toBeTrue();
@@ -304,14 +303,14 @@ describe('ExampleViewer', () => {
const codeToggleButton = fixture.debugElement.query(By.css('.docs-example-code-toggle'));
codeToggleButton.nativeElement.click();
fixture.detectChanges();
await fixture.whenStable();
expect(component.showCode()).toBeFalse();
codeContainer = fixture.debugElement.query(By.css('.docs-example-viewer-code-wrapper'));
expect(codeContainer).toBeNull();
codeToggleButton.nativeElement.click();
fixture.detectChanges();
await fixture.whenStable();
expect(component.showCode()).toBeTrue();
codeContainer = fixture.debugElement.query(By.css('.docs-example-viewer-code-wrapper'));
@@ -328,7 +327,7 @@ describe('ExampleViewer', () => {
}),
);
await component.renderExample();
fixture.detectChanges();
await fixture.whenStable();
expect(component.exampleComponent).toBeDefined();
const previewContainer = fixture.debugElement.query(By.css('.docs-example-viewer-preview'));
@@ -16,14 +16,14 @@ describe('ClickOutside', () => {
let component: ExampleComponent;
let fixture: ComponentFixture<ExampleComponent>;
beforeEach(() => {
beforeEach(async () => {
TestBed.configureTestingModule({
imports: [ExampleComponent],
providers: [provideRouter([])],
});
fixture = TestBed.createComponent(ExampleComponent);
component = fixture.componentInstance;
fixture.detectChanges();
await fixture.whenStable();
});
it('should docsClickOutside be emitted when user click outside `content` element', () => {
@@ -21,7 +21,7 @@ describe('ExternalLink', () => {
},
};
beforeEach(() => {
beforeEach(async () => {
TestBed.configureTestingModule({
imports: [ExampleComponentWithLinks],
providers: [
@@ -33,7 +33,7 @@ describe('ExternalLink', () => {
],
});
fixture = TestBed.createComponent(ExampleComponentWithLinks);
fixture.detectChanges();
await fixture.whenStable();
});
it('should external link have target=_blank attribute', () => {
@@ -17,4 +17,5 @@ export interface NavigationItem {
contentPath?: string;
status?: 'new' | 'updated';
category?: string;
preserveOtherCategoryOrder?: boolean; // true by default
}
@@ -126,11 +126,11 @@ export interface EnumEntry extends DocEntry {
/** Documentation entity for an Angular decorator. */
export interface DecoratorEntry extends DocEntry {
decoratorType: DecoratorType;
members: PropertyEntry[] | null;
signatures?: {
parameters: ParameterEntry[];
jsdocTags: JsDocTagEntry[];
}[];
members: MemberEntry[];
}
/** Documentation entity for an Angular directives and components. */
@@ -161,6 +161,9 @@ export type FunctionEntry = FunctionDefinitionEntry &
implementation: FunctionSignatureMetadata;
};
/** Documentation entity for a block. */
export interface BlockEntry extends DocEntry {}
/** Interface describing a function with overload signatures. */
export interface FunctionDefinitionEntry {
name: string;
@@ -7,6 +7,7 @@
*/
import {
BlockEntry,
ClassEntry,
ConstantEntry,
DecoratorEntry,
@@ -27,6 +28,7 @@ import {
import {CliCommand} from '../cli-entities.mjs';
import {
BlockEntryRenderable,
ClassEntryRenderable,
ConstantEntryRenderable,
DecoratorEntryRenderable,
@@ -107,6 +109,13 @@ export function isFunctionEntry(entry: DocEntry): entry is FunctionEntry {
return entry.entryType === EntryType.Function;
}
/** Gets whether the given entry represents a block */
export function isBlockEntry(entry: DocEntryRenderable): entry is BlockEntryRenderable;
export function isBlockEntry(entry: DocEntry): entry is BlockEntry;
export function isBlockEntry(entry: DocEntry): entry is BlockEntry {
return entry.entryType === EntryType.Block;
}
export function isInitializerApiFunctionEntry(
entry: DocEntryRenderable,
): entry is InitializerApiFunctionRenderable;
@@ -21,6 +21,7 @@ import {
ParameterEntry,
PipeEntry,
TypeAliasEntry,
EntryType,
} from '../entities.mjs';
import {CliCommand, CliOption} from '../cli-entities.mjs';
@@ -75,7 +76,11 @@ export type PipeEntryRenderable = PipeEntry &
members: MemberEntryRenderable[];
};
export type DecoratorEntryRenderable = DecoratorEntry & DocEntryRenderable & HasRenderableToc;
export type DecoratorEntryRenderable = Omit<DecoratorEntry, 'members'> &
DocEntryRenderable &
HasRenderableToc & {
members: PropertyEntryRenderable[];
};
/** Documentation entity for a TypeScript enum augmented transformed content for rendering. */
export type EnumEntryRenderable = EnumEntry &
@@ -102,6 +107,9 @@ export type FunctionSignatureMetadataRenderable = FunctionSignatureMetadata &
params: ParameterEntryRenderable[];
};
/** Documentation entity for a block augmented with transformed content for rendering. */
export type BlockEntryRenderable = DocEntry & DocEntryRenderable;
/** Sub-entry for a single class or enum member augmented with transformed content for rendering. */
export interface MemberEntryRenderable extends MemberEntry {
htmlDescription: string;
@@ -116,6 +124,10 @@ export interface MemberEntryRenderable extends MemberEntry {
experimental: {version: string | undefined} | undefined;
}
export interface PropertyEntryRenderable extends MemberEntryRenderable {
type: string;
}
/** Sub-entry for a class method augmented transformed content for rendering. */
export type MethodEntryRenderable = MemberEntryRenderable &
FunctionEntryRenderable & {
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {JsDocTagEntry, MemberEntry, ParameterEntry} from '../entities.mjs';
import {JsDocTagEntry, MemberEntry, ParameterEntry, PropertyEntry} from '../entities.mjs';
import {
CodeLineRenderable,
@@ -14,6 +14,7 @@ import {
LinkEntryRenderable,
MemberEntryRenderable,
ParameterEntryRenderable,
PropertyEntryRenderable,
} from './renderables.mjs';
/** A doc entry that has jsdoc tags. */
@@ -56,6 +57,10 @@ export interface HasMembers {
members: MemberEntry[];
}
export interface HasPropertyMembers {
members: PropertyEntry[];
}
/** A doc entry that has members groups transformed for rendering. */
export interface HasRenderableMembersGroups {
membersGroups: Map<string, MemberEntryRenderable[]>;
@@ -66,6 +71,10 @@ export interface HasRenderableMembers {
members: MemberEntryRenderable[];
}
export interface HasRenderablePropertyMembers {
members: PropertyEntryRenderable[];
}
/** A doc entry that has an associated JS module name. */
export interface HasModuleName {
moduleName: string;
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {existsSync, mkdirSync, readFileSync, writeFileSync} from 'fs';
import {mkdirSync, readFileSync, writeFileSync} from 'fs';
import path from 'path';
import {CliCommand} from './cli-entities.mjs';
import {DocEntry} from './entities.mjs';
@@ -19,6 +19,7 @@ import {
isInitializerApiFunctionEntry,
isInterfaceEntry,
isTypeAliasEntry,
isBlockEntry,
} from './entities/categorization.mjs';
import {CliCommandRenderable, DocEntryRenderable} from './entities/renderables.mjs';
import {getClassRenderable} from './transforms/class-transforms.mjs';
@@ -39,6 +40,7 @@ import {
import {addModuleName} from './transforms/module-name.mjs';
import {addRepo} from './transforms/repo.mjs';
import {getTypeAliasRenderable} from './transforms/type-alias-transforms.mjs';
import {getBlockRenderable} from './transforms/block-transforms.mjs';
export async function getRenderable(
entry: DocEntry | CliCommand,
@@ -73,6 +75,9 @@ export async function getRenderable(
if (isInitializerApiFunctionEntry(entry)) {
return getInitializerApiFunctionRenderable(entry, moduleName, repo);
}
if (isBlockEntry(entry)) {
return getBlockRenderable(entry, moduleName, repo);
}
// Fallback to an uncategorized renderable.
return setEntryFlags(
@@ -18,6 +18,7 @@ import {
isInitializerApiFunctionEntry,
isInterfaceEntry,
isTypeAliasEntry,
isBlockEntry,
} from './entities/categorization.mjs';
import {CliCommandRenderable, DocEntryRenderable} from './entities/renderables.mjs';
import {ClassReference} from './templates/class-reference';
@@ -30,6 +31,7 @@ import {InitializerApiFunction} from './templates/initializer-api-function';
import {TypeAliasReference} from './templates/type-alias-reference';
import {DecoratorReference} from './templates/decorator-reference';
import {setCurrentSymbol} from './symbol-context.mjs';
import {BlockReference} from './templates/block-reference';
/** Given a doc entry, get the transformed version of the entry for rendering. */
export function renderEntry(renderable: DocEntryRenderable | CliCommandRenderable): string {
@@ -59,6 +61,9 @@ export function renderEntry(renderable: DocEntryRenderable | CliCommandRenderabl
if (isInitializerApiFunctionEntry(renderable)) {
return render(InitializerApiFunction(renderable));
}
if (isBlockEntry(renderable)) {
return render(BlockReference(renderable));
}
// Fall back rendering nothing while in development.
return render(DocsReference(renderable));
@@ -6,8 +6,6 @@
* found in the LICENSE file at https://angular.dev/license
*/
// TODO(jelbourn): all of these CSS classes should use the `docs-` prefix.
export const API_REFERENCE_CONTAINER = 'docs-api';
export const PARAM_KEYWORD_CLASS_NAME = 'docs-param-keyword';
@@ -0,0 +1,23 @@
/*!
* @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 {h} from 'preact';
import {BlockEntryRenderable} from '../entities/renderables.mjs';
import {HeaderApi} from './header-api';
import {RawHtml} from './raw-html';
import {API_REFERENCE_CONTAINER} from '../styling/css-classes.mjs';
/** Component to render a block API reference document. */
export function BlockReference(entry: BlockEntryRenderable) {
return (
<div className={API_REFERENCE_CONTAINER}>
<HeaderApi entry={entry} hideDescription={true} />
<RawHtml value={entry.htmlDescription} />
</div>
);
}
@@ -17,7 +17,6 @@ import {
} from '../entities/categorization.mjs';
import {MemberEntryRenderable, MethodEntryRenderable} from '../entities/renderables.mjs';
import {
PARAM_KEYWORD_CLASS_NAME,
REFERENCE_MEMBER_CARD,
REFERENCE_MEMBER_CARD_BODY,
REFERENCE_MEMBER_CARD_HEADER,
@@ -12,15 +12,18 @@ import {
ParameterEntryRenderable,
} from '../entities/renderables.mjs';
import {PARAM_KEYWORD_CLASS_NAME, REFERENCE_MEMBER_CARD_ITEM} from '../styling/css-classes.mjs';
import {CodeSymbol} from './code-symbols';
import {DeprecatedLabel} from './deprecated-label';
import {Parameter} from './parameter';
import {RawHtml} from './raw-html';
import {CodeSymbol} from './code-symbols';
/**
* Component to render the method-specific parts of a class's API reference.
*/
export function ClassMethodInfo(props: {entry: FunctionSignatureMetadataRenderable}) {
export function ClassMethodInfo(props: {
entry: FunctionSignatureMetadataRenderable;
hideUsageNotes?: boolean;
}) {
const entry = props.entry;
return (
@@ -43,7 +46,7 @@ export function ClassMethodInfo(props: {entry: FunctionSignatureMetadataRenderab
<span className={PARAM_KEYWORD_CLASS_NAME}>@returns</span>
<CodeSymbol code={entry.returnType} />
</div>
{entry.htmlUsageNotes ? (
{entry.htmlUsageNotes && !props.hideUsageNotes ? (
<div className={'docs-usage-notes'}>
<span className={'docs-usage-notes-heading'}>Usage notes</span>
<RawHtml value={entry.htmlUsageNotes} />
@@ -7,12 +7,45 @@
*/
import {h} from 'preact';
import {DecoratorEntryRenderable, PropertyEntryRenderable} from '../entities/renderables.mjs';
import {
API_REFERENCE_CONTAINER,
REFERENCE_MEMBER_CARD,
REFERENCE_MEMBER_CARD_BODY,
REFERENCE_MEMBER_CARD_HEADER,
REFERENCE_MEMBER_CARD_ITEM,
REFERENCE_MEMBERS,
} from '../styling/css-classes.mjs';
import {CodeSymbol} from './code-symbols';
import {HeaderApi} from './header-api';
import {RawHtml} from './raw-html';
import {SectionApi} from './section-api';
import {SectionDescription} from './section-description';
import {SectionUsageNotes} from './section-usage-notes';
import {SectionApi} from './section-api';
import {API_REFERENCE_CONTAINER} from '../styling/css-classes.mjs';
import {DecoratorEntryRenderable} from '../entities/renderables.mjs';
export const signatureCard = (
name: string,
member: PropertyEntryRenderable,
opts: {id: string},
) => {
return (
<div id={opts.id} class={REFERENCE_MEMBER_CARD}>
<header class={REFERENCE_MEMBER_CARD_HEADER}>
<h3>{name}</h3>
<div>
<CodeSymbol code={member.type} />
</div>
</header>
<div class={REFERENCE_MEMBER_CARD_BODY}>
<div className={`${REFERENCE_MEMBER_CARD_ITEM}`}>
<p>
<RawHtml value={member.htmlDescription} />
</p>
</div>
</div>
</div>
);
};
/** Component to render a decorator API reference document. */
export function DecoratorReference(entry: DecoratorEntryRenderable) {
@@ -20,6 +53,13 @@ export function DecoratorReference(entry: DecoratorEntryRenderable) {
<div className={API_REFERENCE_CONTAINER}>
<HeaderApi entry={entry} />
<SectionApi entry={entry} />
<div className={REFERENCE_MEMBERS}>
{entry.members.map((member, index) =>
signatureCard(member.name, member, {
id: `${member.name}_${index}`,
}),
)}
</div>
<SectionDescription entry={entry} />
<SectionUsageNotes entry={entry} />
</div>
@@ -22,23 +22,22 @@ import {printInitializerFunctionSignatureLine} from '../transforms/code-transfor
import {getFunctionMetadataRenderable} from '../transforms/function-transforms.mjs';
import {ClassMethodInfo} from './class-method-info';
import {CodeSymbol} from './code-symbols';
import {DeprecationWarning} from './deprecation-warning';
import {HeaderApi} from './header-api';
import {HighlightTypeScript} from './highlight-ts';
import {SectionApi} from './section-api';
import {SectionDescription} from './section-description';
import {SectionUsageNotes} from './section-usage-notes';
import {DeprecationWarning} from './deprecation-warning';
export const signatureCard = (
name: string,
signature: FunctionSignatureMetadataRenderable,
opts: {id: string},
printSignaturesAsHeader: boolean,
opts: {id: string; printSignaturesAsHeader: boolean; hideUsageNotes?: boolean},
) => {
return (
<div id={opts.id} class={REFERENCE_MEMBER_CARD}>
<header class={REFERENCE_MEMBER_CARD_HEADER}>
{printSignaturesAsHeader ? (
{opts.printSignaturesAsHeader ? (
<HighlightTypeScript
code={printInitializerFunctionSignatureLine(
name,
@@ -57,7 +56,7 @@ export const signatureCard = (
)}
</header>
<div class={REFERENCE_MEMBER_CARD_BODY}>
<ClassMethodInfo entry={signature} />
<ClassMethodInfo entry={signature} hideUsageNotes={opts.hideUsageNotes} />
</div>
</div>
);
@@ -75,14 +74,11 @@ export function FunctionReference(entry: FunctionEntryRenderable) {
<SectionApi entry={entry} />
<div className={REFERENCE_MEMBERS}>
{entry.signatures.map((s, i) =>
signatureCard(
s.name,
getFunctionMetadataRenderable(s, entry.moduleName, entry.repo),
{
id: `${s.name}_${i}`,
},
signatureCard(s.name, getFunctionMetadataRenderable(s, entry.moduleName, entry.repo), {
id: `${s.name}_${i}`,
printSignaturesAsHeader,
),
hideUsageNotes: true,
}),
)}
</div>
@@ -21,6 +21,7 @@ import {DocsPillRow} from './docs-pill-row';
export function HeaderApi(props: {
entry: DocEntryRenderable | PipeEntryRenderable;
showFullDescription?: boolean;
hideDescription?: boolean;
}) {
const entry = props.entry;
@@ -60,12 +61,14 @@ export function HeaderApi(props: {
)}
</div>
<section
className={'docs-reference-description'}
dangerouslySetInnerHTML={{
__html: props.showFullDescription ? entry.htmlDescription : entry.shortHtmlDescription,
}}
></section>
{!props.hideDescription && (
<section
className={'docs-reference-description'}
dangerouslySetInnerHTML={{
__html: props.showFullDescription ? entry.htmlDescription : entry.shortHtmlDescription,
}}
></section>
)}
<DocsPillRow links={entry.additionalLinks} />
</header>
@@ -123,13 +126,6 @@ function tagInVersionString(label: string, tag: {version: string | undefined} |
return <>{label}</>;
}
function tagInVersionTooltip(
label: string,
tag: {version: string | undefined} | undefined,
): string {
return tag?.version ? `${label} since ${tag.version}` : label;
}
function getEntryTypeDisplayName(entryType: EntryType | string): string {
switch (entryType) {
case EntryType.NgModule:
@@ -40,14 +40,10 @@ export function InitializerApiFunction(entry: InitializerApiFunctionRenderable)
<div class={REFERENCE_MEMBERS}>
{entry.callFunction.signatures.map((s, i) =>
signatureCard(
s.name,
getFunctionMetadataRenderable(s, entry.moduleName, entry.repo),
{
id: `${s.name}_${i}`,
},
signatureCard(s.name, getFunctionMetadataRenderable(s, entry.moduleName, entry.repo), {
id: `${s.name}_${i}`,
printSignaturesAsHeader,
),
}),
)}
{entry.subFunctions.reduce(
@@ -59,8 +55,8 @@ export function InitializerApiFunction(entry: InitializerApiFunctionRenderable)
getFunctionMetadataRenderable(s, entry.moduleName, entry.repo),
{
id: `${entry.name}_${s.name}_${i}`,
printSignaturesAsHeader,
},
printSignaturesAsHeader,
),
),
],
@@ -0,0 +1,36 @@
/*!
* @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 {DocEntry} from '../entities.mjs';
import {BlockEntryRenderable} from '../entities/renderables.mjs';
import {
addHtmlAdditionalLinks,
addHtmlDescription,
addHtmlJsDocTagComments,
addHtmlUsageNotes,
setEntryFlags,
} from './jsdoc-transforms.mjs';
import {addModuleName} from './module-name.mjs';
import {addRepo} from './repo.mjs';
/** Given an unprocessed block entry, get the fully renderable block entry. */
export function getBlockRenderable(
blockEntry: DocEntry,
moduleName: string,
repo: string,
): BlockEntryRenderable {
return setEntryFlags(
addHtmlAdditionalLinks(
addHtmlDescription(
addHtmlUsageNotes(
addHtmlJsDocTagComments(addRepo(addModuleName(blockEntry, moduleName), repo)),
),
),
),
);
}
@@ -16,6 +16,7 @@ import {
addHtmlUsageNotes,
setEntryFlags,
} from './jsdoc-transforms.mjs';
import {addRenderablePropertyMembers} from './member-transforms.mjs';
import {addModuleName} from './module-name.mjs';
import {addRepo} from './repo.mjs';
@@ -25,14 +26,24 @@ export async function getDecoratorRenderable(
moduleName: string,
repo: string,
): Promise<DecoratorEntryRenderable> {
const decoratorEntryWithMembers = {
...decoratorEntry,
members: decoratorEntry.members ?? [],
};
return setEntryFlags(
await addRenderableCodeToc(
addHtmlAdditionalLinks(
addHtmlUsageNotes(
addHtmlJsDocTagComments(
addHtmlDescription(addRepo(addModuleName(decoratorEntry, moduleName), repo)),
await addRenderablePropertyMembers(
addHtmlAdditionalLinks(
addHtmlUsageNotes(
addHtmlJsDocTagComments(
addHtmlDescription(
addRepo(addModuleName(decoratorEntryWithMembers, moduleName), repo),
),
),
),
),
decoratorEntry.name,
),
),
) as DecoratorEntryRenderable;
@@ -10,7 +10,14 @@ import {MemberEntry, MemberTags, MemberType, type DocEntry} from '../entities.mj
import {isHiddenEntry} from '../entities/categorization.mjs';
import type {MemberEntryRenderable} from '../entities/renderables.mjs';
import {HasMembers, HasModuleName, HasRenderableMembers, HasRepo} from '../entities/traits.mjs';
import {
HasMembers,
HasModuleName,
HasPropertyMembers,
HasRenderableMembers,
HasRenderablePropertyMembers,
HasRepo,
} from '../entities/traits.mjs';
import {addRenderableCodeToc} from './code-transforms.mjs';
import {
@@ -85,8 +92,7 @@ export async function addRenderableMembers<T extends HasMembers & HasModuleName
): Promise<T & HasRenderableMembers> {
const members = (
await Promise.all(
// TODO: remove `?? []` when components repo is updated to include members array on type aliases.
(entry.members ?? [])
entry.members
.filter((member) => !isHiddenEntry(member))
.map((member) => {
if (member.memberType === MemberType.Interface) {
@@ -112,3 +118,11 @@ export async function addRenderableMembers<T extends HasMembers & HasModuleName
members,
};
}
export async function addRenderablePropertyMembers<
T extends HasPropertyMembers & HasModuleName & HasRepo,
>(entry: T, parentName: string): Promise<T & HasRenderablePropertyMembers> {
return {
...((await addRenderableMembers(entry, parentName)) as T & HasRenderablePropertyMembers),
};
}
@@ -83,6 +83,7 @@ async function getFileAndContent(workingDir: string, path: string): Promise<File
return {content, path};
}
// Use once we restore the StackBlitz feature
async function createPostData(
exampleDir: string,
config: ZipConfig,
@@ -18,6 +18,11 @@ const LINK_EXEMPT = new Set([
'readonly',
'disabled',
'hidden',
'state',
'group',
'animation',
'transition',
'trigger',
]);
export function shouldLinkSymbol(symbol: string): boolean {
@@ -6,3 +6,9 @@ this is code
<docs-code path="./example-with-region.ts" />
<docs-code header="src/locale/messages.fr.xlf (<trans-unit>)" path="./messages.fr.xlf" />
<docs-code header="Property names should not be linked" language="ts">
const form = {
state: ['']
};
</docs-code>
@@ -47,4 +47,9 @@ describe('markdown to html', () => {
const codeBlock = markdownDocument.querySelectorAll('code')[3];
expect(codeBlock).toBeTruthy();
});
it('should not link property names in object literals', () => {
const codeBlock = markdownDocument.querySelectorAll('code')[4];
expect(codeBlock?.innerHTML).not.toContain('<a href="/api/animations/state">state</a>');
});
});
@@ -9,6 +9,5 @@
import {InjectionToken, signal} from '@angular/core';
export const IS_SEARCH_DIALOG_OPEN = new InjectionToken('', {
providedIn: 'root',
factory: () => signal(false),
});
@@ -10,7 +10,6 @@ import {isPlatformBrowser} from '@angular/common';
import {InjectionToken, PLATFORM_ID, inject} from '@angular/core';
export const LOCAL_STORAGE = new InjectionToken<Storage | null>('LOCAL_STORAGE', {
providedIn: 'root',
factory: () => getStorage(inject(PLATFORM_ID)),
});
@@ -101,7 +101,7 @@ export class NavigationState {
private expand(item: NavigationItem): void {
// Add item to the expanded items list
this._expandedItems.update((expandedItems) => {
this._expandedItems.update(() => {
return [...(this.actualExpandedItems() ?? []), {...item, isExpanded: true}];
});
+23 -14
View File
@@ -105,17 +105,23 @@ export class Search {
const content = hitItem._snippetResult.content;
const hierarchy = hitItem._snippetResult.hierarchy;
const category = hitItem.hierarchy?.lvl0 ?? null;
const hasSubLabel = hierarchy?.lvl2 || hierarchy?.lvl3 || hierarchy?.lvl4;
const lvl1Value = hierarchy?.lvl1?.value || hitItem.hierarchy?.lvl1;
const sublabelSnippet = this.getBestSnippetForMatch(hitItem);
// If no lvl1, promote sublabel to label to avoid empty titles
const label = lvl1Value || sublabelSnippet || '';
const hasSubLabel = lvl1Value && (hierarchy?.lvl2 || hierarchy?.lvl3 || hierarchy?.lvl4);
return {
id: hitItem.objectID,
type: hitItem.hierarchy.lvl0 === 'Tutorials' ? 'code' : 'doc',
url: hitItem.url,
labelHtml: this.parseLabelToHtml(hitItem._snippetResult.hierarchy?.lvl1?.value ?? ''),
subLabelHtml: this.parseLabelToHtml(
hasSubLabel ? this.getBestSnippetForMatch(hitItem) : null,
),
labelHtml: this.parseLabelToHtml(label),
subLabelHtml: this.parseLabelToHtml(hasSubLabel ? sublabelSnippet : null),
contentHtml: content ? this.parseLabelToHtml(content.value) : null,
package: category === 'Reference' ? extractPackageNameFromUrl(hitItem.url) : null,
@@ -222,16 +228,19 @@ function matched(snippet: SnippetResult | undefined): boolean {
*/
function wait(ms: number, signal: AbortSignal): Promise<void> {
return new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => resolve(), ms);
let timeout: ReturnType<typeof setTimeout> | undefined;
signal.addEventListener(
'abort',
() => {
clearTimeout(timeout);
reject(new Error('Operation aborted'));
},
{once: true},
);
const onAbort = () => {
clearTimeout(timeout);
reject(new Error('Operation aborted'));
};
timeout = setTimeout(() => {
signal.removeEventListener('abort', onAbort);
resolve();
}, ms);
signal.addEventListener('abort', onAbort, {once: true});
});
}
+7 -2
View File
@@ -41,6 +41,7 @@
padding: 0.75rem 1.5rem;
width: max-content;
color: transparent;
user-select: none;
// border gradient / background
--angle: 90deg;
@@ -66,7 +67,9 @@
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.3s ease, background 0.3s ease;
transition:
opacity 0.3s ease,
background 0.3s ease;
color: var(--primary-contrast);
}
@@ -80,7 +83,9 @@
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.3s ease, background 0.3s ease;
transition:
opacity 0.3s ease,
background 0.3s ease;
color: var(--primary-contrast);
}
+15 -11
View File
@@ -40,7 +40,7 @@
--symbolic-pink: oklch(63.67% 0.254 13.47); // #ff025c
--symbolic-orange: oklch(64.73% 0.23769984683784018 33.18328352127882); // #fe3700
--symbolic-yellow: oklch(78.09% 0.163 65.69); // #fd9f28
--symbolic-green: oklch(67.83% 0.229 142.73); // #00b80a
--symbolic-green: oklch(48% 0.16 145); // #008B38
--symbolic-cyan: oklch(67.05% 0.1205924489987394 181.34025902203868); // #00ad9a
--symbolic-magenta: oklch(51.74% 0.25453048882711515 315.26261625862725); // #9c00c8
--symbolic-teal: oklch(57.59% 0.083 230.58); // #3f82a1
@@ -179,7 +179,8 @@
--white-to-light-blue-diagonal-gradient: linear-gradient(
132.83deg,
hsl(from color-mix(in srgb, var(--bright-blue), white 60%) h s l / 0) 45.38%,
hsl(from color-mix(in srgb, var(--bright-blue), white 60%) h s l / 0.3) 159.03%);
hsl(from color-mix(in srgb, var(--bright-blue), white 60%) h s l / 0.3) 159.03%
);
--white-to-light-pink-diagonal-gradient: linear-gradient(
135deg,
@@ -230,8 +231,7 @@
// for the "unfilled" portion of the word that hasn't
// been highlighted by the gradient
--gray-unfilled: var(--gray-400);
--webgl-page-background: var(var(--page-background))
--webgl-gray-unfilled: var(--gray-400);
--webgl-page-background: var(var(--page-background)) --webgl-gray-unfilled: var(--gray-400);
}
@mixin dark-mode-definitions() {
@@ -261,9 +261,16 @@
--super-green: color-mix(in srgb, oklch(79.12% 0.257 155.13), var(--full-contrast) 70%);
--light-vivid-pink-to-light-electric-violet-horizontal-gradient:
linear-gradient(hsl(from var(--gray-1000) h s l / 0.9), hsl(from var(--gray-1000) h s l / 0.9)),
linear-gradient(hsl(from var(--full-contrast) h s l / 0.2), hsl(from var(--full-contrast) h s l / 0.2)),
linear-gradient(
hsl(from var(--full-contrast) h s l / 0.2),
hsl(from var(--full-contrast) h s l / 0.2)
),
var(--pink-to-purple-horizontal-gradient);
--white-to-light-pink-diagonal-gradient: color-mix(in srgb, var(--french-violet), var(--full-contrast) 60%);
--white-to-light-pink-diagonal-gradient: color-mix(
in srgb,
var(--french-violet),
var(--full-contrast) 60%
);
--deprecated-dark: oklch(30% 0.02 304.04); // Base color
--deprecated-docs-bg: var(--deprecated-dark); // Alias specifically for deprecated docs
@@ -281,7 +288,7 @@
var(--full-contrast) 65%
);
--symbolic-yellow: color-mix(in srgb, oklch(78.09% 0.163 65.69), var(--full-contrast) 65%);
--symbolic-green: color-mix(in srgb, oklch(67.83% 0.229 142.73), var(--full-contrast) 65%);
--symbolic-green: color-mix(in srgb, oklch(48% 0.16 145), var(--full-contrast) 65%);
--symbolic-cyan: color-mix(
in srgb,
oklch(67.05% 0.1205924489987394 181.34025902203868),
@@ -301,10 +308,7 @@
);
--page-bg-radial-gradient: radial-gradient(circle, black 0%, black 100%);
--soft-pink-radial-gradient:
linear-gradient(119.77deg,
hsl(from color-mix(in srgb, var(--french-violet), white 60%) h s l / 0) 24.32%,
hsl(from color-mix(in srgb, var(--french-violet), white 60%) h s l / 0.3) 116.25%);
--soft-pink-radial-gradient: var(--hot-pink-to-electric-violet-radial-gradient);
// Use light values hot pink and electric violet since this gradient used for
// the decorative header which looks the same in both modes
+17 -3
View File
@@ -1,3 +1,5 @@
@use './links' as links;
/* Common styles for the API & CLI references */
@mixin reference-common() {
.docs-code {
@@ -62,10 +64,10 @@
}
.docs-reference-section-heading {
padding-block-start: 3rem;
margin-block-start: 3rem;
&--sub {
padding-block-start: 1rem;
margin-block-start: 1rem;
}
& > a {
@@ -73,18 +75,30 @@
}
}
.docs-api {
.docs-api,
.docs-cli {
h1,
h2,
h3,
h4,
h5,
h6 {
.docs-anchor {
color: inherit;
}
// Show copy link button when hovering the heading
&:hover docs-copy-link-button {
opacity: 1;
}
}
a:not(.docs-github-links):not(.docs-pill):not(.docs-example-github-link) {
&[href^='http:'],
&[href^='https:'] {
@include links.external-link-with-icon();
}
}
}
.docs-reference-members {
+1
View File
@@ -3,6 +3,7 @@
--z-index-top-level-banner: 150;
--z-index-nav: 100;
--z-index-cookie-consent: 200;
--z-index-nav-tutorial: 75;
--z-index-content: 50;
--z-index-icon: 10;
}
@@ -6,7 +6,6 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {ChangeDetectorRef} from '@angular/core';
import {
DirEnt,
FSWatchCallback,
@@ -65,14 +64,6 @@ export class MockLocalStorage implements Pick<Storage, 'getItem' | 'setItem'> {
}
}
export class FakeChangeDetectorRef implements ChangeDetectorRef {
markForCheck(): void {}
detach(): void {}
checkNoChanges(): void {}
reattach(): void {}
detectChanges(): void {}
}
export class FakeWebContainer extends WebContainer {
fakeSpawn: FakeWebContainerProcess | undefined = undefined;
@@ -93,7 +93,7 @@
</ul>
</div>
<div>
<h2>Languages</h2>
<h2>Community translations</h2>
<ul>
<li>
<a href="https://angular.cn/" title="简体中文版">简体中文版</a>
@@ -107,6 +107,9 @@
<li>
<a href="https://angular.kr/" title="한국어">한국어</a>
</li>
<li>
<a href="https://angular-docs.ru/" title="Русский">Русский</a>
</li>
</ul>
</div>
</div>
@@ -21,7 +21,7 @@ describe('Footer', () => {
},
};
beforeEach(() => {
beforeEach(async () => {
TestBed.configureTestingModule({
imports: [Footer],
providers: [
@@ -34,7 +34,7 @@ describe('Footer', () => {
});
fixture = TestBed.createComponent(Footer);
component = fixture.componentInstance;
fixture.detectChanges();
await fixture.whenStable();
});
it('should create', () => {
@@ -38,7 +38,7 @@ describe('Navigation', () => {
const fakeSearch = {};
beforeEach(async () => {
await TestBed.configureTestingModule({
TestBed.configureTestingModule({
imports: [Navigation],
providers: [
provideRouter([]),
@@ -58,23 +58,23 @@ describe('Navigation', () => {
fixture = TestBed.createComponent(Navigation);
component = fixture.componentInstance;
fixture.detectChanges();
await fixture.whenStable();
});
it('should append active class to DOCS_ROUTE when DOCS_ROUTE is active', () => {
it('should append active class to DOCS_ROUTE when DOCS_ROUTE is active', async () => {
component.activeRouteItem.set(PAGE_PREFIX.DOCS);
fixture.detectChanges();
await fixture.whenStable();
const docsLink = fixture.debugElement.query(By.css('a[href="/docs"]')).parent?.nativeElement;
expect(docsLink).toHaveClass('adev-nav-item--active');
});
it('should not have active class when activeRouteItem is null', () => {
it('should not have active class when activeRouteItem is null', async () => {
component.activeRouteItem.set(null);
fixture.detectChanges();
await fixture.whenStable();
const docsLink = fixture.debugElement.query(By.css('a[href="/docs"]')).nativeElement;
const referenceLink = fixture.debugElement.query(By.css('a[href="/reference"]')).nativeElement;
@@ -83,14 +83,14 @@ describe('Navigation', () => {
expect(referenceLink).not.toHaveClass('adev-nav-item--active');
});
it('should call themeManager.setTheme(dark) when user tries to set dark theme', () => {
it('should call themeManager.setTheme(dark) when user tries to set dark theme', async () => {
const openThemePickerButton = fixture.debugElement.query(
By.css('button[aria-label^="Change theme. Current theme:"]'),
).nativeElement;
const setThemeSpy = spyOn(fakeThemeManager, 'setTheme');
openThemePickerButton.click();
fixture.detectChanges();
await fixture.whenStable();
const setDarkModeButton = fixture.debugElement.query(
By.css('button[aria-label="Set dark theme"]'),
@@ -17,14 +17,14 @@ describe('ProgressBarComponent', () => {
let fixture: ComponentFixture<ProgressBarComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
TestBed.configureTestingModule({
imports: [ProgressBarComponent],
providers: [provideRouter([])],
});
fixture = TestBed.createComponent(ProgressBarComponent);
component = fixture.componentInstance;
fixture.detectChanges();
await fixture.whenStable();
});
// This test often timeouts
@@ -16,6 +16,7 @@
[navigationItems]="navigationItems"
[displayItemsToLevel]="maxVisibleLevelsOnSecondaryNav()"
[expandableLevel]="maxVisibleLevelsOnSecondaryNav()"
[preserveOtherCategoryOrder]="true"
(linkClicked)="close()"
/>
}
@@ -26,6 +27,7 @@
[collapsableLevel]="level + maxVisibleLevelsOnSecondaryNav()"
[expandableLevel]="level + maxVisibleLevelsOnSecondaryNav() + 1"
[navigationItems]="[item]"
[preserveOtherCategoryOrder]="item.preserveOtherCategoryOrder ?? false"
[displayItemsToLevel]="level + maxVisibleLevelsOnSecondaryNav() + 1"
(linkClicked)="close()"
/>
@@ -21,7 +21,7 @@ describe('SecondaryNavigation', () => {
},
};
beforeEach(() => {
beforeEach(async () => {
TestBed.configureTestingModule({
imports: [SecondaryNavigation],
providers: [
@@ -33,7 +33,7 @@ describe('SecondaryNavigation', () => {
});
fixture = TestBed.createComponent(SecondaryNavigation);
component = fixture.componentInstance;
fixture.detectChanges();
await fixture.whenStable();
});
it('should create', () => {
@@ -19,7 +19,7 @@ import {EmbeddedTutorialManager} from '../embedded-tutorial-manager.service';
import {CodeEditor, REQUIRED_FILES} from './code-editor.component';
import {CodeMirrorEditor} from './code-mirror-editor.service';
import {FakeChangeDetectorRef, TutorialType} from '@angular/docs';
import {TutorialType} from '@angular/docs';
import {MatTooltipHarness} from '@angular/material/tooltip/testing';
const files = [
@@ -45,7 +45,6 @@ class FakeCodeMirrorEditor implements Partial<CodeMirrorEditor> {
openFiles = this.files;
}
const codeMirrorEditorService = new FakeCodeMirrorEditor();
const fakeChangeDetectorRef = new FakeChangeDetectorRef();
describe('CodeEditor', () => {
let component: CodeEditor;
@@ -63,10 +62,6 @@ describe('CodeEditor', () => {
provide: CodeMirrorEditor,
useValue: codeMirrorEditorService,
},
{
provide: ChangeDetectorRef,
useValue: fakeChangeDetectorRef,
},
{
provide: EmbeddedTutorialManager,
useValue: {
@@ -93,7 +88,7 @@ describe('CodeEditor', () => {
loader = TestbedHarnessEnvironment.loader(fixture);
component = fixture.componentInstance;
fixture.detectChanges();
await fixture.whenStable();
});
it('should create', () => {
@@ -105,7 +100,7 @@ describe('CodeEditor', () => {
const codeMirrorEditorInitSpy = spyOn(codeMirrorEditorService, 'init');
fixture = TestBed.createComponent(CodeEditor);
fixture.detectChanges();
await fixture.whenStable();
component = fixture.componentInstance;
expect(component.codeEditorWrapperRef()).toBeDefined();
@@ -50,8 +50,6 @@ describe('Preview', () => {
const component = fixture.componentInstance;
const iframeElement = fixture.debugElement.query(By.css('iframe'));
fixture.detectChanges();
return {
fixture,
component,
@@ -64,27 +62,28 @@ describe('Preview', () => {
};
};
it('should set iframe src', () => {
it('should set iframe src', async () => {
const {fixture, PREVIEW_URL} = beforeEach();
await fixture.whenStable();
const iframeElement = fixture.debugElement.query(By.css('iframe'));
expect(iframeElement?.nativeElement?.src).toBe(PREVIEW_URL);
});
it('should not render loading elements if the loadingStep is READY or ERROR', () => {
it('should not render loading elements if the loadingStep is READY or ERROR', async () => {
const {fixture, fakeNodeRuntimeState, getLoadingElementsWrapper} = beforeEach();
fakeNodeRuntimeState.loadingStep.set(LoadingStep.READY);
fixture.detectChanges();
await fixture.whenStable();
expect(getLoadingElementsWrapper()).toBeNull();
fakeNodeRuntimeState.loadingStep.set(LoadingStep.ERROR);
fixture.detectChanges();
await fixture.whenStable();
expect(getLoadingElementsWrapper()).toBeNull();
});
it('should render the correct loading element based on loadingStep', () => {
it('should render the correct loading element based on loadingStep', async () => {
const {fixture, fakeNodeRuntimeState} = beforeEach();
function getDebugElements(): Record<number, DebugElement | null> {
@@ -113,7 +112,7 @@ describe('Preview', () => {
componentLoadingStep++
) {
fakeNodeRuntimeState.loadingStep.set(componentLoadingStep);
fixture.detectChanges();
await fixture.whenStable();
const loadingElements = getDebugElements();
@@ -136,8 +135,6 @@ describe('Preview', () => {
fakeNodeRuntimeState.loadingStep.set(LoadingStep.ERROR);
fakeNodeRuntimeState.error.set({message: 'Error message', type: undefined});
fixture.detectChanges();
await fixture.whenStable();
expect(fixture.debugElement.query(By.directive(PreviewError))).toBeDefined();
@@ -23,7 +23,7 @@ describe('DocsComponent', () => {
getContent: (id: string) => undefined,
};
beforeEach(() => {
beforeEach(async () => {
TestBed.configureTestingModule({
imports: [DocsComponent],
providers: [
@@ -40,7 +40,7 @@ describe('DocsComponent', () => {
});
fixture = TestBed.createComponent(DocsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
await fixture.whenStable();
});
it('should create', () => {
@@ -81,13 +81,9 @@ describe('Animation', () => {
const layerObjects = new Map<string, HTMLElement>();
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AnimationHost],
});
fixture = TestBed.createComponent(AnimationHost);
component = fixture.componentInstance;
fixture.detectChanges();
await fixture.whenStable();
animation = component.animation;
// Store all layer objects in a map for easier access.
+2 -1
View File
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {Component, inject, signal} from '@angular/core';
import {ChangeDetectionStrategy, Component, inject, signal} from '@angular/core';
import {ActivatedRoute, RouterLink, UrlSegment} from '@angular/router';
import {Search, SearchItem, RelativeLink, SearchResultItem} from '@angular/docs';
@@ -14,6 +14,7 @@ import {Search, SearchItem, RelativeLink, SearchResultItem} from '@angular/docs'
imports: [SearchItem, RouterLink, RelativeLink],
templateUrl: './not-found.html',
styleUrl: './not-found.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class NotFound {
private readonly search = inject(Search);
@@ -26,7 +26,7 @@ describe('TutorialPlayground', () => {
},
};
beforeEach(() => {
beforeEach(async () => {
class FakeEmbeddedTutorialManager {
fetchAndSetTutorialFiles() {}
}
@@ -50,7 +50,7 @@ describe('TutorialPlayground', () => {
fixture = TestBed.createComponent(TutorialPlayground);
component = fixture.componentInstance;
fixture.detectChanges();
await fixture.whenStable();
});
it('should create', () => {
@@ -15,24 +15,21 @@ describe('ApiItemLabel', () => {
let fixture: ComponentFixture<ApiItemLabel>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ApiItemLabel],
});
fixture = TestBed.createComponent(ApiItemLabel);
});
it('should by default display short label for Class', () => {
it('should by default display short label for Class', async () => {
fixture.componentRef.setInput('type', ApiItemType.CLASS);
fixture.detectChanges();
await fixture.whenStable();
const label = fixture.nativeElement.innerText;
expect(label).toBe('C');
});
it('should display short label for Class', () => {
it('should display short label for Class', async () => {
fixture.componentRef.setInput('type', ApiItemType.CLASS);
fixture.detectChanges();
await fixture.whenStable();
const label = fixture.nativeElement.innerText;
@@ -54,18 +54,18 @@ describe('ApiItemsSection', () => {
component = fixture.componentInstance;
});
it('should render list of all APIs of provided group', () => {
it('should render list of all APIs of provided group', async () => {
fixture.componentRef.setInput('group', fakeGroup);
fixture.detectChanges();
await fixture.whenStable();
const apis = fixture.debugElement.queryAll(By.css('.adev-api-items-section-grid li'));
expect(apis.length).toBe(2);
});
it('should display deprecated icon for deprecated API', () => {
it('should display deprecated icon for deprecated API', async () => {
fixture.componentRef.setInput('group', fakeGroup);
fixture.detectChanges();
await fixture.whenStable();
const deprecatedApiIcons = fixture.debugElement.queryAll(
By.css('.adev-api-items-section-grid li .adev-item-attribute'),
@@ -54,7 +54,6 @@ describe('ApiReferenceDetailsPage', () => {
const harness = await RouterTestingHarness.create();
fixture = harness.fixture;
component = await harness.navigateByUrl('/', ApiReferenceDetailsPage);
fixture.detectChanges();
});
it('should create', () => {
@@ -4,7 +4,13 @@
<div class="adev-reference-list-filter">
<div class="adev-reference-list-query-filter">
<docs-text-field name="query" placeholder="Filter" [(ngModel)]="query" />
<docs-text-field placeholder="Filter" [formField]="form.query" />
<docs-select
selectId="package-entry-filter"
[options]="packageOptions()"
[formField]="form.selectedPackage"
/>
</div>
<div class="adev-reference-list-status">
@@ -23,7 +29,7 @@
@for (itemType of itemTypes; track itemType) {
<li
class="adev-reference-list-type-filter-item"
[class.adev-reference-list-type-filter-item-active]="type() === itemType"
[class.adev-reference-list-type-filter-item-active]="form.type().value() === itemType"
(click)="setItemType(itemType)"
>
<docs-api-item-label [type]="itemType" mode="short" class="docs-api-item-label" />
@@ -8,16 +8,16 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {Location} from '@angular/common';
import {signal} from '@angular/core';
import {TextField} from '@angular/docs';
import {By} from '@angular/platform-browser';
import {provideRouter} from '@angular/router';
import {RouterTestingHarness} from '@angular/router/testing';
import {ApiItem} from '../interfaces/api-item';
import {ApiItemType} from '../interfaces/api-item-type';
import ApiReferenceList, {ALL_TYPES_KEY, STATUSES} from './api-reference-list.component';
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';
import {Location} from '@angular/common';
import {By} from '@angular/platform-browser';
import {TextField} from '@angular/docs';
import {ApiItem} from '../interfaces/api-item';
describe('ApiReferenceList', () => {
let component: ApiReferenceList;
@@ -93,7 +93,7 @@ describe('ApiReferenceList', () => {
}),
};
beforeEach(() => {
beforeEach(async () => {
TestBed.configureTestingModule({
imports: [ApiReferenceList],
providers: [
@@ -103,52 +103,52 @@ describe('ApiReferenceList', () => {
});
fixture = TestBed.createComponent(ApiReferenceList);
component = fixture.componentInstance;
fixture.detectChanges();
await fixture.whenStable();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should display only items which contains provided query when query is not empty', () => {
it('should display only items which contains provided query when query is not empty', async () => {
fixture.componentRef.setInput('query', 'Item1');
fixture.detectChanges();
await fixture.whenStable();
expect(component.filteredGroups()![0].items).toEqual([fakeItem1]);
expect(component.filteredGroups()[0].items).toEqual([fakeItem1]);
});
it('should display items which match the query by group title', () => {
it('should display items which match the query by group title', async () => {
fixture.componentRef.setInput('query', 'Fake Group');
fixture.detectChanges();
await fixture.whenStable();
// Should find all items whose group title contains the query
expect(component.filteredGroups()![0].items.length).toBeGreaterThan(0);
expect(component.filteredGroups()![0].items).toContain(fakeItem1);
expect(component.filteredGroups()[0].items.length).toBeGreaterThan(0);
expect(component.filteredGroups()[0].items).toContain(fakeItem1);
});
it('should display only class items when user selects Class in the Type select', () => {
it('should display only class items when user selects Class in the Type select', async () => {
fixture.componentRef.setInput('type', ApiItemType.CLASS);
fixture.detectChanges();
await fixture.whenStable();
expect(component.type()).toEqual(ApiItemType.CLASS);
expect(component.filteredGroups()![0].items).toEqual([fakeItem2]);
expect(component.form.type().value()).toEqual(ApiItemType.CLASS);
expect(component.filteredGroups()[0].items).toEqual([fakeItem2]);
});
it('should set selected type when provided type is different than selected', async () => {
expect(component.type()).toBe(ALL_TYPES_KEY);
expect(component.form.type().value()).toBe(ALL_TYPES_KEY);
component.setItemType(ApiItemType.BLOCK);
await RouterTestingHarness.create(`/api?type=${ApiItemType.BLOCK}`);
expect(component.type()).toBe(ApiItemType.BLOCK);
expect(component.form.type().value()).toBe(ApiItemType.BLOCK);
});
it('should reset selected type when provided type is equal to selected', async () => {
component.setItemType(ApiItemType.BLOCK);
const harness = await RouterTestingHarness.create(`/api?type=${ApiItemType.BLOCK}`);
expect(component.type()).toBe(ApiItemType.BLOCK);
expect(component.form.type().value()).toBe(ApiItemType.BLOCK);
component.setItemType(ApiItemType.BLOCK);
harness.navigateByUrl(`/api`);
expect(component.type()).toBe(ALL_TYPES_KEY);
expect(component.form.type().value()).toBe(ALL_TYPES_KEY);
});
it('should set the value of the queryParam equal to the query text field', async () => {
@@ -164,15 +164,15 @@ describe('ApiReferenceList', () => {
const location = TestBed.inject(Location);
const textField = fixture.debugElement.query(By.directive(TextField));
(textField.componentInstance as TextField).setValue('item1');
fixture.componentInstance.form.query().value.set('item1');
await fixture.whenStable();
expect(location.path()).toBe(`?query=item1`);
component.setItemType(ApiItemType.BLOCK);
fixture.componentInstance.form.type().value.set(ApiItemType.BLOCK);
await fixture.whenStable();
expect(location.path()).toBe(`?query=item1&type=${ApiItemType.BLOCK}`);
fixture.componentRef.setInput('status', STATUSES.experimental);
fixture.componentInstance.form.status().value.set(STATUSES.experimental);
await fixture.whenStable();
expect(location.path()).toBe(
`?query=item1&type=${ApiItemType.BLOCK}&status=${STATUSES.experimental}`,
@@ -192,30 +192,30 @@ describe('ApiReferenceList', () => {
]);
});
it('should not display deprecated and developer-preview and experimental items when status is set to stable', () => {
it('should not display deprecated and developer-preview and experimental items when status is set to stable', async () => {
fixture.componentRef.setInput('status', STATUSES.stable);
fixture.detectChanges();
await fixture.whenStable();
expect(component.filteredGroups()![0].items).toEqual([fakeItem1, fakeItem2]);
});
it('should only display deprecated items when status is set to deprecated', () => {
it('should only display deprecated items when status is set to deprecated', async () => {
fixture.componentRef.setInput('status', STATUSES.deprecated);
fixture.detectChanges();
await fixture.whenStable();
expect(component.filteredGroups()![0].items).toEqual([fakeDeprecatedFeaturedItem]);
});
it('should only display developer-preview items when status is set to developer-preview', () => {
it('should only display developer-preview items when status is set to developer-preview', async () => {
fixture.componentRef.setInput('status', STATUSES.developerPreview);
fixture.detectChanges();
await fixture.whenStable();
expect(component.filteredGroups()![0].items).toEqual([fakeDeveloperPreviewItem]);
});
it('should only display experimental items when status is set to experimental', () => {
it('should only display experimental items when status is set to experimental', async () => {
fixture.componentRef.setInput('status', STATUSES.experimental);
fixture.detectChanges();
await fixture.whenStable();
expect(component.filteredGroups()![0].items).toEqual([fakeExperimentalItem]);
});
@@ -6,33 +6,33 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {CdkMenuModule} from '@angular/cdk/menu';
import {KeyValuePipe} from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
ElementRef,
EnvironmentInjector,
afterNextRender,
computed,
effect,
inject,
input,
linkedSignal,
viewChild,
afterNextRender,
EnvironmentInjector,
effect,
input,
} from '@angular/core';
import ApiItemsSection from '../api-items-section/api-items-section.component';
import {FormsModule} from '@angular/forms';
import {TextField} from '@angular/docs';
import {KeyValuePipe} from '@angular/common';
import {Params, Router} from '@angular/router';
import {ApiItemType} from '../interfaces/api-item-type';
import {ApiReferenceManager} from './api-reference-manager.service';
import ApiItemLabel from '../api-item-label/api-item-label.component';
import {ApiLabel} from '../pipes/api-label.pipe';
import {ApiItemsGroup} from '../interfaces/api-items-group';
import {CdkMenuModule} from '@angular/cdk/menu';
import {Select, SelectOption, TextField} from '@angular/docs';
import {FormField, form} from '@angular/forms/signals';
import {MatChipsModule} from '@angular/material/chips';
import {Params, Router} from '@angular/router';
import ApiItemLabel from '../api-item-label/api-item-label.component';
import ApiItemsSection from '../api-items-section/api-items-section.component';
import {ApiItemType} from '../interfaces/api-item-type';
import {ApiItemsGroup} from '../interfaces/api-items-group';
import {ApiLabel} from '../pipes/api-label.pipe';
import {ApiReferenceManager} from './api-reference-manager.service';
export const ALL_TYPES_KEY = 'All';
export const ALL_PACKAGES = 'All';
export const STATUSES = {
stable: 1,
developerPreview: 2,
@@ -46,12 +46,13 @@ export const DEFAULT_STATUS = STATUSES.stable | STATUSES.developerPreview | STAT
imports: [
ApiItemsSection,
ApiItemLabel,
FormsModule,
TextField,
ApiLabel,
CdkMenuModule,
MatChipsModule,
KeyValuePipe,
Select,
FormField,
],
templateUrl: './api-reference-list.component.html',
styleUrls: ['./api-reference-list.component.scss'],
@@ -61,19 +62,33 @@ export default class ApiReferenceList {
// services
private readonly apiReferenceManager = inject(ApiReferenceManager);
private readonly router = inject(Router);
private readonly filterInput = viewChild.required(TextField, {read: ElementRef});
private readonly filterInput = viewChild.required(TextField);
private readonly injector = inject(EnvironmentInjector);
// inputs
readonly queryInput = input<string | undefined>('', {alias: 'query'});
readonly typeInput = input<string | undefined>(ALL_TYPES_KEY, {alias: 'type'});
readonly statusInput = input<number | undefined>(DEFAULT_STATUS, {alias: 'status'});
protected selectedPackageInput = input<string | undefined>(ALL_PACKAGES, {alias: 'package'});
// inputs are route binded, they can reset to undefined
// also we want a writable state, so we use a linked signal
protected query = linkedSignal(() => this.queryInput() ?? '');
public type = linkedSignal(() => this.typeInput() ?? ALL_TYPES_KEY);
private status = linkedSignal(() => this.statusInput() ?? DEFAULT_STATUS);
public form = form(
linkedSignal(() => ({
query: this.queryInput() ?? '',
status: this.statusInput() ?? DEFAULT_STATUS,
type: this.typeInput() ?? ALL_TYPES_KEY,
selectedPackage: this.selectedPackageInput() ?? ALL_PACKAGES,
})),
);
protected packageOptions = computed<SelectOption[]>(() => [
{label: 'All Packages', value: ALL_PACKAGES},
...this.apiReferenceManager.apiGroups().map((group) => ({
label: group.title,
value: group.id,
})),
]);
// const state
protected readonly itemTypes = Object.values(ApiItemType);
@@ -87,9 +102,9 @@ export default class ApiReferenceList {
};
readonly filteredGroups = computed((): ApiItemsGroup[] => {
const query = this.query().toLocaleLowerCase();
const status = this.status();
const type = this.type();
const query = this.form.query().value().toLocaleLowerCase();
const status = this.form.status().value();
const type = this.form.type().value();
return this.apiReferenceManager
.apiGroups()
.map((group) => ({
@@ -112,7 +127,12 @@ export default class ApiReferenceList {
);
}),
}))
.filter((group) => group.items.length > 0);
.filter(
(group) =>
group.items.length > 0 &&
(this.form.selectedPackage().value() === ALL_PACKAGES ||
group.id === this.form.selectedPackage().value()),
);
});
constructor() {
@@ -121,10 +141,9 @@ export default class ApiReferenceList {
afterNextRender(
{
write: () => {
// Lord forgive me for I have sinned
// Use the CVA to focus when https://github.com/angular/angular/issues/31133 is implemented
// Why not improve this once https://github.com/orgs/angular/projects/60?pane=issue&itemId=143488813 is done
if (matchMedia('(hover: hover) and (pointer:fine)').matches) {
scheduleOnIdle(() => filterInput.nativeElement.querySelector('input').focus());
scheduleOnIdle(() => filterInput.focus());
}
},
},
@@ -135,9 +154,13 @@ export default class ApiReferenceList {
effect(() => {
// We'll only set the params if we deviate from the default values
const params: Params = {
'query': this.query() || null,
'type': this.type() === ALL_TYPES_KEY ? null : this.type(),
'status': this.status() === DEFAULT_STATUS ? null : this.status(),
'query': this.form.query().value() || null,
'type': this.form.type().value() === ALL_TYPES_KEY ? null : this.form.type().value(),
'status': this.form.status().value() === DEFAULT_STATUS ? null : this.form.status().value(),
'package':
this.form.selectedPackage().value() === ALL_PACKAGES
? null
: this.form.selectedPackage().value(),
};
this.router.navigate([], {
@@ -152,11 +175,11 @@ export default class ApiReferenceList {
}
setItemType(itemType: ApiItemType): void {
this.type.update((type) => (type === itemType ? ALL_TYPES_KEY : itemType));
this.form.type().value.update((type) => (type === itemType ? ALL_TYPES_KEY : itemType));
}
setStatus(status: number): void {
this.status.update((previousStatus) => {
this.form.status().value.update((previousStatus) => {
if (this.isStatusSelected(status)) {
return previousStatus & ~status; // Clear the bit
} else {
@@ -166,7 +189,7 @@ export default class ApiReferenceList {
}
isStatusSelected(status: number): boolean {
return (this.status() & status) === status;
return (this.form.status().value() & status) === status;
}
}

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