Compare commits

...

176 Commits

Author SHA1 Message Date
Andrew Kushnir ecb44d3d65 release: cut the v18.1.5 release 2024-08-14 09:29:57 -07:00
Andrew Kushnir bf3235cdce Revert "docs(docs-infra): fix markdown link rendering (#57377)" (#57380)
This reverts commit 95af661400.

Reason for revert: failign CI on 18.1.x branch.

PR Close #57380
2024-08-13 18:23:08 -07:00
Matthieu Riegler 95af661400 docs(docs-infra): fix markdown link rendering (#57377)
fixes #57376

PR Close #57377
2024-08-13 15:53:56 -07:00
Thomas Nguyen f99bb20a10 refactor(core): Add experimental support to have one event contract when there are multiple apps on the page. (#57355)
This may be removed if this turns out not to work out so well...

PR Close #57355
2024-08-13 12:10:35 -07:00
Thomas Nguyen 97e5dcb421 refactor(core): Call stopPropagation and preventDefault unconditionally within the patched methods. (#57354)
This fixes a few tests in g3 and is a bug fix for the event dispatcher. Otherwise, bubbling might
continue.

PR Close #57354
2024-08-13 12:09:56 -07:00
Thomas Nguyen db65bc25ca fix(core): Account for addEventListener to be passed a Window or Document. (#57354)
This happened to work for other event listeners since both had a
addEventListener method.

PR Close #57354
2024-08-13 12:09:56 -07:00
Andrew Scott 37cf37e3c3 refactor(docs-infra): Update search results to display content when it is matched (#57298)
This commit updates the search results to query for the content as well
as a snippet of the content for display when it's the content that
matches the query rather than any of the headers.

PR Close #57298
2024-08-13 12:07:49 -07:00
Andrew Kushnir 822db64b93 fix(core): skip hydration for i18n nodes that were not projected (#57356)
This commit fixes an issue that happens when an i18n block is defined as a projectable content, but a parent component doesn't project it. With an extra check added in this commit, the code will be taking a regular "creation" pass instead of attempting hydration.

Resolves #57301.

PR Close #57356
2024-08-13 09:42:42 -07:00
Angular Robot 9814e9bd80 build: update scorecard action dependencies (#57358)
See associated pull request for more information.

PR Close #57358
2024-08-13 09:30:16 -07:00
Angular Robot 93991b4ce2 build: update dependency @babel/core to v7.25.2 (#57191)
See associated pull request for more information.

PR Close #57191
2024-08-13 09:29:35 -07:00
Andrew Kushnir 810f76f574 fix(core): take skip hydration flag into account while hydrating i18n blocks (#57299)
This commit updates serialization and hydration i18n logic to take into account situations when i18n blocks are located within "skip hydration" blocks.

Resolves #57105.

PR Close #57299
2024-08-09 08:07:48 -07:00
Andrew Kushnir 0e024ecc27 fix(core): complete post-hydration cleanup in components that use ViewContainerRef (#57300)
Previously, if a component injects a `ViewContainerRef`, the post-hydration cleanup process doesn't visit inner views to cleanup dehydrated views in nested LContainers. This commit updates the logic to recognize this situation and enter host LView to complete cleanup.

Resolves #56989.

PR Close #57300
2024-08-09 08:07:12 -07:00
Angular Robot cf245ea0af build: update dependency marked to v14 (#57320)
See associated pull request for more information.

Closes #57283 as a pr takeover

PR Close #57320
2024-08-09 08:04:37 -07:00
Thomas Nguyen e39b22a932 fix(core): Account for addEventListener to be passed a Window or Document. (#57282)
This happened to work for other event listeners since both had a
addEventListener method.

PR Close #57282
2024-08-08 08:32:11 -07:00
Kristiyan Kostadinov 5401332b0e fix(compiler-cli): generate valid TS 5.6 type checking code (#57303)
Currently in some scenarios the compiler generates code like `null as any ? foo : bar` which will be invalid with [an upcoming TypeScript change](https://devblogs.microsoft.com/typescript/announcing-typescript-5-6-beta/#disallowed-nullish-and-truthy-checks). These changes switch to generating `0 as any` which is exempt from the change.

**Note:** I'm not starting the work to fully get us on TS 5.6 until the 18.2 release comes out, but this change is necessary to unblock an internal team.

PR Close #57303
2024-08-08 08:30:12 -07:00
Jessica Janiuk bdf8450b7a release: cut the v18.1.4 release 2024-08-07 11:52:09 -07:00
Matthieu Riegler 69540d32ef fix(zone.js): Add support for addition jest functions. (#57280)
This commit add support for `test.failing` and `it.failing`.

Fixes #57277

PR Close #57280
2024-08-07 18:13:52 +00:00
Kristiyan Kostadinov 6a99f83659 fix(compiler): reduce chance of conflicts between generated factory and local variables (#57181)
Currently we use some short variable names like `t` and `r` in the generated factory functions. They can conflict with local symbols with the same names, if they're used for DI.

These changes rename the parameters to reduce the change for conflicts.

Fixes #57168.

PR Close #57181
2024-08-07 17:25:06 +00:00
JoostK bae54a1621 perf(compiler-cli): improve performance of interpolatedSignalNotInvoked extended diagnostic (#57291)
This commit addresses a performance bottleneck in the `interpolatedSignalNotInvoked` extended
diagnostic by querying directive metadata instead of consulting the type-checker to determine if
a property binding corresponds with an input.

Fixes #57287

PR Close #57291
2024-08-07 16:47:24 +00:00
Kristiyan Kostadinov e366473109 refactor(core): replace usages of removeChild (#57203)
These changes replace most usages of `removeChild` with `remove`. The latter has the advantage of not having to look up the `parentNode` and ensure that the child being removed actually belongs to the specific parent.

The refactor should be fairly safe since all the browsers we cover support `remove`. [Something similar was done in Components](https://github.com/angular/components/pull/23592) some time ago and there haven't been any bug reports as a result.

PR Close #57203
2024-08-07 16:46:09 +00:00
David LJ 0c12d8e733 docs(router): update initial navigation SSR note (#57245)
With rephrasing by @AndrewKushnir

PR Close #57245
2024-08-07 16:45:30 +00:00
Angular Robot 8c029c6402 build: update actions/upload-artifact action to v4.3.5 (#57271)
See associated pull request for more information.

PR Close #57271
2024-08-06 13:50:53 +00:00
Angular Robot 4cbb2793c2 build: update actions/upload-artifact action to v4.3.5 (#57270)
See associated pull request for more information.

PR Close #57270
2024-08-06 13:50:12 +00:00
Andrew Scott 596c25157c refactor(docs-infra): Use new APIs in search dialog (#57269)
This reverts commit 9109f31cb7.

PR Close #57269
2024-08-06 13:49:41 +00:00
Andrew Scott 6b2a4c083b Revert "refactor(docs-infra): Use new APIs in search dialog (#57215)" (#57268)
This reverts commit f5960d0f94.

PR Close #57268
2024-08-05 21:12:00 +00:00
Joey Perrott 6eac1b3f22 refactor(docs-infra): migrate api-gen from dev-infra into the repo (#57241)
Move the api-gen pipeline into the shared-docs directory.

PR Close #57241
2024-08-05 17:06:30 +00:00
Paul Gschwendtner afb05ff1cb fix(compiler-cli): support JIT transforms before other transforms modifying classes (#57262)
Similar to a previous fix that intended to make the JIT transforms
compatible with pre-transforms like e.g. Tsickle, we need to solve
an additional issue where the class properties are synthetic and result
in an `getSourceFile() => undefined` invocation that breaks the import
insertion, causing errors like:

```
TypeError: Cannot read properties of undefined (reading 'fileName')
```

PR Close #57262
2024-08-05 14:45:19 +00:00
Angular Robot 183715aedd build: lock file maintenance (#57256)
See associated pull request for more information.

PR Close #57256
2024-08-05 14:42:25 +00:00
Doug Parker 523f639b76 release: bump Angular DevTools to 1.0.17 (#57221)
PR Close #57221
2024-08-02 16:53:14 +00:00
Doug Parker 168eed23b4 docs(devtools): remove duplicate text at end of release.md doc (#57222)
Somehow the last few lines got duplicated at the end of this doc. Definitely not needed.

PR Close #57222
2024-08-02 16:52:41 +00:00
Charles Lyding 6ac209c24f fix(language-service): avoid generating TS suggestion diagnostics for templates (#56241)
Angular's template files are not valid TypeScript. Attempting to get suggestion
diagnostics from the underlying TypeScript language service will result in
a large amount of false positives. Only actual TypeScript files should
be analyzed by the underlying TypeScript language service for suggestions.

PR Close #56241
2024-08-02 15:51:35 +00:00
Thomas Nguyen 0c729765a1 refactor(core): Remove clickmod support from Angular. (#57201)
This was an old feature that mapped shift + click (et al) to "clickmod". This doesn't really make sense to add to Angular, so let's remove it.

PR Close #57201
2024-08-02 14:20:54 +00:00
Jessica Janiuk ad29621d30 Revert "fix(compiler): reduce chance of conflicts between generated factory and local variables (#57181)" (#57230)
This reverts commit 67e09404db.

PR Close #57230
2024-08-01 19:33:05 +00:00
Joey Perrott e8794dd1f0 fix(docs-infra): replace the uses of replaceAll with replace using regex with g flag (#57232)
Use the g flag in replace to perform a replaceAll

PR Close #57232
2024-08-01 19:17:25 +00:00
Andrew Scott de7f5ac9f1 refactor(docs-infra): Use new APIs in search dialog (#57215)
This updates the search dialog component to use signal APIs

PR Close #57215
2024-07-31 18:56:31 +00:00
Dylan Hunn be6c246115 release: cut the v18.1.3 release 2024-07-31 10:17:09 -07:00
Andres Olivares 69b5a317ec refactor(devtools): Use Chrome DevTools Performance extension API (#55805)
This change is a proof of concept of how the new Chrome DevTools
Performance extension API (https://bit.ly/rpp-e11y) can be used to
surface Angular runtime data directly in the Chrome DevTools Performance
panel.

Specifically, it implements the following changes:

1. Use the profiling status notification API to toggle the Timing API:
The notification API is implemented under the
chrome.devtools.performance extension namespace and consits of two
events: ProfilingStarted and ProfilingStopped, dispatched when the
Performance panel has started and stopped recording, respectively. This
API is used to enable the Timings API when the recording has started in
the Performance panel and disable it when recording has stopped.

2. Use the User Timings `detail` field format specification of the
Performance extension API
(https://developer.mozilla.org/en-US/docs/Web/API/Performance_API/User_timing)
to inject data collected by the Angular Profiler into the
Performance panel timeline. Angular Profiler uses several hooks to
measure framework tasks like change detection. With this change, this
measurements are visible in the same context as the runtime data
collected by the browser in the Performance Panel timeline.

Note: to enable the user timings to be collected in the first place, one
needs to open the Angular DevTools panel so that the related artifacts
are loaded in the page. This shortcoming can be fixed in a follow up so
that the extra step isn't necessary.

PR Close #55805
2024-07-31 16:04:53 +00:00
Paul Gschwendtner 7459d09a23 ci: update license checking to latest version (#57211)
This may help with detecting the license of google-protobuf- which
is a transitive dependency of `@bazel/worker`

PR Close #57211
2024-07-31 16:04:13 +00:00
Paul Gschwendtner 2ece9509a0 test: update symbol golden after lock file maintenance (#57211)
Lock file maintenance updated Terser, which impacts the bundle
optimizations being tested via the symbol golden tests.

There was a small noticable change in the symbol golden where
`withDomHydration` is now preserved, and the underlying function
that was previously detected is gone. Seemingly Terser now inlines
this function and had to preserve `withDomHydration` as the entry point.

PR Close #57211
2024-07-31 16:04:13 +00:00
Paul Gschwendtner 722a4669b9 build: lock file maintenance (#57211)
See associated pull request for more information.

PR Close #57211
2024-07-31 16:04:13 +00:00
Paul Gschwendtner b3cc9e9c3b build: replace deprecated inquirer with new npm package (#57211)
The new package comes with types by default, and also has a nicer
API.

The old package had broken types and couldn't be used anymore; and
it's deprecated/unmaintained. We did a similar switch in
angular/dev-infra.

PR Close #57211
2024-07-31 16:04:13 +00:00
Matthieu Riegler 103a4dccdc docs(docs-infra): extract captured group from heading custom id regexp (#57206)
Backport of angular/dev-infra#2202

PR Close #57206
2024-07-31 16:03:28 +00:00
Angular Robot eadfa39767 build: update scorecard action dependencies (#57150)
See associated pull request for more information.

PR Close #57150
2024-07-31 14:00:40 +00:00
Jeremy Elbourn ec4bac0a3a docs: add docs authoring guide (#56505)
This new contributor guide includes information for authoring Angular
documentation.

PR Close #56505
2024-07-31 13:58:53 +00:00
Badshah d16071a5a0 docs: fix appRoutes constant was not passed correctly to provideRouter (#57070)
PR Close #57070
2024-07-31 13:58:21 +00:00
Kristiyan Kostadinov 1f9e090910 fix(compiler-cli): emitting references to ngtypecheck files (#57138) (#57202)
Follow-up to #56961 which doesn't appear to have caught all the cases. This change moves the pre-emit untagging to `NgCompiler.prepareEmit` which seems to cover a bit more comared to `NgtscProgram.emit`.

Fixes #57135.

PR Close #57138

PR Close #57202
2024-07-31 13:57:37 +00:00
Andrew Scott 62ade76cf8 refactor(core): Update callback schedulers to cancel pending timers (#57186)
Rather than leaving the timers around as no-ops, this commit updates the
logic to also attempt to clear or cancel the timers. This is helpful for
the eventual goal of running the scheduler in the `fakeAsync` zone (if
the test is running in `fakeAsync`) rather than scheduling in the root
zone and making it impossible to flush.

PR Close #57186
2024-07-30 18:05:09 +00:00
Andrew Scott 0def76f3b3 refactor(core): Private option to rethrow ApplicationRef.tick errors in tests (#57153)
This creates a private option that can be used internally while we
migrate this to the default and only behavior. ~200 tests in TGP have errors
that are being swallowed (console.log) and not causing the test to fail.
We can first explicitly opt those out, flip the default internally, then
"fix" them by adding expect...toThrow.

PR Close #57153
2024-07-30 18:04:27 +00:00
Bouguima, Walid 0fbcd6ed28 docs: broken HammerLoader reference fix (#55836)
*Fixing broken documentation reference to HammerLoader in HammerLoaderInjectionToken page

PR Close #55836
2024-07-30 16:56:34 +00:00
David LJ 0016849ca1 docs: update app shell model link (#57162)
PR Close #57162
2024-07-30 16:55:43 +00:00
Paweł Kubiak e5e5fedf54 fix(docs-infra): skip navigation to card if user clicks on anchor (#57081)
fix(docs-infra): skip navigation to card if user clicks on anchor

If user clicks on anchor inside member card (Reference) then skip navigation to card section and allow to navigate to anchor link.

Use correct RxJs operator.
Fix DI: destroy ReferenceScrollHandler when navigate outside of reference pages.

fix: scroll to the of the card only after click on card header
fix: remove import of filter operator

PR Close #57081
2024-07-30 16:52:43 +00:00
Enea Jahollari 352fa3d8ad docs: fix formatting and highlighting issues (#57144)
PR Close #57144
2024-07-30 16:52:10 +00:00
Andrew Scott b0657bcce9 docs: More updates to components scenarios guide (#57052)
PR Close #57052
2024-07-30 16:51:11 +00:00
Angular Robot 130ff94400 build: update all non-major dependencies (#57178)
See associated pull request for more information.

PR Close #57178
2024-07-30 16:44:42 +00:00
Kristiyan Kostadinov eaa83f9d27 fix(core): hydration error in some let declaration setups (#57173)
Fixes that we were throwing an assertion error during hydration if a `@let` declaration is used before and immediately inside of a container.

Fixes #57160.

PR Close #57173
2024-07-30 16:43:46 +00:00
Sheik Althaf 6489397b99 refactor(devtools): use signal apis in injection and router tree (#57047)
Refactor the injection and router tree components to use signal apis, in future we can make the components onPush and zoneless

PR Close #57047
2024-07-30 16:42:15 +00:00
David LJ 15e0214037 docs: update ng generate command references (#57161)
PR Close #57161
2024-07-30 16:41:39 +00:00
Joey Perrott 81b30dcef0 refactor(docs-infra): migrate @angular/docs from dev-infra into adev directory (#57132)
To increase the ease of development we are moving @angular/docs into the adev directory within this repo. While
we are doing this to improve our development experience in the short term, efforts are also in place
to maintain a division between this @angular/docs (shared) code and adev itself, so that it can be extracted
back out in the future when components is ready to leverage it as well.

PR Close #57132
2024-07-30 15:51:27 +00:00
David LJ 9304c45313 docs: fix invalid link formatting in sw config (#57170)
PR Close #57170
2024-07-29 13:52:24 -07:00
Angular Robot f2a97f4f95 build: update dependency @babel/generator to v7.25.0 (#57152)
See associated pull request for more information.

PR Close #57152
2024-07-29 13:50:15 -07:00
Andrew Scott f7ab04018e fix(core): errors during ApplicationRef.tick should be rethrown for zoneless tests (#56993)
The behavior of `ComponentFixture` for zoneless tests was decided somewhat through guesswork, trial, and error. In addition, working on the zoneless fixture revealed oddities in the behavior of the zone-based fixture, or behaviors that we felt were counterintuitive. The most consequential difference is how change detection works: `detectChanges` goes through ApplicationRef.tick in zoneless while it is `changeDetectorRef.detectChanges` in the zone fixture.

We felt that running change detection through `ApplicationRef.tick` was important for several reasons:
* Aligning application behavior more closely with the test behavior (almost all views are attached to application ref in reality)
* Ensuring that afterRender* hooks are executed when calling `fixture.detectChanges`
* Ensuring that the change detection runs again if render hooks update state

This change, however, has some noticeable consequences that will break some tests, mostly around how errors are handled. `ApplicationRef.tick` catches errors that happen during change detection and reports them to the ErrorHandler from DI. The default error handler only logs the error to the console. This will break tests which have `expect(() => fixture.detectChanges()).toThrow()`. In addition, it allows tests to pass when there are real errors encountered during change detection.

This change ensures that errors from `ApplicationRef.tick` are rethrown
and will fail the test. We should also do a follow-up investigation to
determine whether we can/should also do this for the zone-based
`ComponentFixture`.

fixes #56977

PR Close #56993
2024-07-29 13:49:01 -07:00
Aristeidis Bampakos 61367000e4 docs: use new blog in links of new documentation (#56052)
PR Close #56052
2024-07-29 13:48:37 -07:00
Tyler Hendrickson 6f15006648 docs: improve spelling and grammar for error NG0956 (#57164)
PR Close #57164
2024-07-29 13:48:06 -07:00
Matthieu Riegler b9a46bcd64 docs(docs-infra): fix visual glitch, (#57172)
When on a certain width, the tab-bar width glitch back-and-forth and forcing the 2 other buttons to reduce their width. This glitches appeared when we introduced that 2nd button.

fixes #57143

PR Close #57172
2024-07-29 13:47:38 -07:00
Kristiyan Kostadinov 31dea066d6 fix(compiler): reduce chance of conflicts between generated factory and local variables (#57181)
Currently we use some short variable names like `t` and `r` in the generated factory functions. They can conflict with local symbols with the same names, if they're used for DI.

These changes add a `ɵ` to the generated variables to reduce the chance of conflicts.

Fixes #57168.

PR Close #57181
2024-07-29 13:46:48 -07:00
marktechson 481092da4d docs: add open in IDX support (#57099)
This change adds a menu to the in-browser code editor on adev and adds the option for open in IDX.

PR Close #57099
2024-07-26 10:02:57 -07:00
Andrew Scott 8df60d4713 fix(docs-infra): only run matchMedia on client (#57121)
Use afterNextRender to avoid running matchMedia on the server

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

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

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

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

fixes #57109

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

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

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

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

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

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

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

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

Closes #57066

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

Fixes #57034.

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

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

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

Closes #56996 as a pr takeover

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

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

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

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

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

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

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

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

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

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

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

fixes #56868

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

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

Fixes: #56586

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

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

Fixes #56207

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

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

Fixes #56979.

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

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

Fixes #54096.

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

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

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

Fixes #56310

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

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

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

Fixes #56786

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

These changes add tagging right before the program emits.

Fixes #56945.

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

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

fixes #56969

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

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

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

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

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

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

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

`bootstrapGlobalEarlyEventContract()`

And for boostraping:

`registerGlobalDispatcher(dispatcher)`

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

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

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

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

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

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

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

PR Close #56769

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

Updates include:

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

related to #48510

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

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

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

fixes #56569

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

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

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

Fixes #53038

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

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

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

Fixes #56488

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

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

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

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

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

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

Fixes #56842.

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

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

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

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

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

fixes #56757

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

Resolves #56372.

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

fixes #56552

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

fixes #56774

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

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

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

PR Close #56806
2024-07-02 14:16:24 +00:00
Jessica Janiuk 7f5d70a74a release: cut the v18.1.0-rc.0 release 2024-07-01 14:01:57 -07:00
897 changed files with 49094 additions and 11075 deletions
@@ -5,6 +5,9 @@ inputs:
serviceKey:
description: 'The service key used for firebase deployments.'
required: true
githubReleaseTrainReadToken:
description: 'GitHub access token for reading release trains without rate limits.'
required: true
configPath:
description: 'The path to the firebase config file.'
required: true
@@ -13,3 +13,8 @@ export function getCredentialFilePath(): string {
}
return credentialFilePath;
}
/** Github access token. Used for querying the active release trains. */
export const githubReleaseTrainReadToken: string = getInput('githubReleaseTrainReadToken', {
required: true,
});
@@ -1,6 +1,6 @@
import {fetchLongTermSupportBranchesFromNpm, ActiveReleaseTrains} from '@angular/ng-dev';
import {ReleaseConfig} from '@angular/ng-dev';
import {GitClient} from '@angular/ng-dev';
import {AuthenticatedGitClient} from '@angular/ng-dev';
export interface Deployment {
branch: string;
@@ -14,7 +14,7 @@ export interface Deployment {
export type Deployments = Map<string, Deployment>;
export async function getDeployments(): Promise<Deployments> {
const {github} = await GitClient.get();
const {github} = await AuthenticatedGitClient.get();
const releaseTrains = await ActiveReleaseTrains.fetch({
api: github,
name: 'angular',
+5 -1
View File
@@ -2,7 +2,8 @@ import {getInput, setFailed} from '@actions/core';
import {context} from '@actions/github';
import {deployToFirebase, setupRedirect} from './deploy';
import {getDeployments} from './deployments';
import {GithubConfig, setConfig} from '@angular/ng-dev';
import {AuthenticatedGitClient, GithubConfig, setConfig} from '@angular/ng-dev';
import {githubReleaseTrainReadToken} from './credential';
const refMatcher = /refs\/heads\/(.*)/;
@@ -14,6 +15,9 @@ async function deployDocs() {
owner: 'angular',
},
});
AuthenticatedGitClient.configure(githubReleaseTrainReadToken);
if (context.eventName !== 'push') {
throw Error();
}
+27 -5
View File
@@ -7433,6 +7433,7 @@ var require_lrucache = __commonJS({
//
var require_range = __commonJS({
""(exports, module) {
var SPACE_CHARACTERS = /\s+/g;
var Range = class {
constructor(range, options) {
options = parseOptions(options);
@@ -7446,13 +7447,13 @@ var require_range = __commonJS({
if (range instanceof Comparator) {
this.raw = range.value;
this.set = [[range]];
this.format();
this.formatted = void 0;
return this;
}
this.options = options;
this.loose = !!options.loose;
this.includePrerelease = !!options.includePrerelease;
this.raw = range.trim().split(/\s+/).join(" ");
this.raw = range.trim().replace(SPACE_CHARACTERS, " ");
this.set = this.raw.split("||").map((r) => this.parseRange(r.trim())).filter((c) => c.length);
if (!this.set.length) {
throw new TypeError(`Invalid SemVer Range: ${this.raw}`);
@@ -7471,10 +7472,27 @@ var require_range = __commonJS({
}
}
}
this.format();
this.formatted = void 0;
}
get range() {
if (this.formatted === void 0) {
this.formatted = "";
for (let i = 0; i < this.set.length; i++) {
if (i > 0) {
this.formatted += "||";
}
const comps = this.set[i];
for (let k = 0; k < comps.length; k++) {
if (k > 0) {
this.formatted += " ";
}
this.formatted += comps[k].toString().trim();
}
}
}
return this.formatted;
}
format() {
this.range = this.set.map((comps) => comps.join(" ").trim()).join("||").trim();
return this.range;
}
toString() {
@@ -11344,6 +11362,9 @@ function getCredentialFilePath() {
}
return credentialFilePath;
}
var githubReleaseTrainReadToken = (0, import_core.getInput)("githubReleaseTrainReadToken", {
required: true
});
//
async function deployToFirebase(deployment, configPath, distDirPath) {
@@ -12977,7 +12998,7 @@ AuthenticatedGitClient._authenticatedInstance = null;
//
async function getDeployments() {
const { github } = await GitClient.get();
const { github } = await AuthenticatedGitClient.get();
const releaseTrains = await ActiveReleaseTrains.fetch({
api: github,
name: "angular",
@@ -13033,6 +13054,7 @@ async function deployDocs() {
owner: "angular"
}
});
AuthenticatedGitClient.configure(githubReleaseTrainReadToken);
if (import_github3.context.eventName !== "push") {
throw Error();
}
+1
View File
@@ -197,5 +197,6 @@ jobs:
uses: ./.github/actions/deploy-docs-site
with:
serviceKey: ${{ secrets.ANGULAR_DEV_SITE_DEPLOY }}
githubReleaseTrainReadToken: ${{ secrets.DOCS_DEPLOY_GITHUB_RELEASE_TRAIN_TOKEN }}
configPath: 'adev/firebase.json'
distDir: 'dist/bin/adev/build/browser'
+4 -2
View File
@@ -39,7 +39,9 @@ jobs:
- name: Check code format
run: yarn ng-dev format changed --check ${{ github.event.pull_request.base.sha }}
- name: Check Package Licenses
uses: angular/dev-infra/github-actions/linting/licenses@515b575bd6650a12353b6039b618cdb808280ad7
uses: angular/dev-infra/github-actions/linting/licenses@56966c765e97f463c4795fac85d46543d1e6b342
with:
allow-dependencies-licenses: 'pkg:npm/google-protobuf@'
devtools:
runs-on: ubuntu-latest
@@ -93,7 +95,7 @@ jobs:
- name: Run CI tests for framework
run: yarn tsx ./scripts/build/build-packages-dist.mts
- name: Archive build artifacts
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
with:
name: pr-artifacts-${{ github.event.number }}
path: dist/packages-dist/
+3 -3
View File
@@ -30,7 +30,7 @@ jobs:
persist-credentials: false
- name: 'Run analysis'
uses: ossf/scorecard-action@dc50aa9510b46c811795eb24b2f1ba02a914e534 # v2.3.3
uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0
with:
results_file: results.sarif
results_format: sarif
@@ -39,7 +39,7 @@ jobs:
# Upload the results as artifacts.
- name: 'Upload artifact'
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
with:
name: SARIF file
path: results.sarif
@@ -47,6 +47,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: 'Upload to code-scanning'
uses: github/codeql-action/upload-sarif@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3.25.10
uses: github/codeql-action/upload-sarif@eb055d739abdc2e8de2e5f4ba1a8b246daa779aa # v3.26.0
with:
sarif_file: results.sarif
+2 -1
View File
@@ -597,7 +597,8 @@ groups:
conditions:
- >
contains_any_globs(files, [
'adev/**/{.*,*}'
'adev/**/{.*,*}',
'aio/**/{.*,*}'
])
reviewers:
users:
+154 -108
View File
@@ -1,3 +1,157 @@
<a name="18.1.5"></a>
# 18.1.5 (2024-08-14)
### compiler-cli
| Commit | Type | Description |
| -- | -- | -- |
| [5401332b0e](https://github.com/angular/angular/commit/5401332b0ef1ec398a5e9767ca73cec544635c93) | fix | generate valid TS 5.6 type checking code ([#57303](https://github.com/angular/angular/pull/57303)) |
### core
| Commit | Type | Description |
| -- | -- | -- |
| [e39b22a932](https://github.com/angular/angular/commit/e39b22a932384f228e97414c44d10c7e158dfd2f) | fix | Account for addEventListener to be passed a Window or Document. ([#57282](https://github.com/angular/angular/pull/57282)) |
| [db65bc25ca](https://github.com/angular/angular/commit/db65bc25cab413221fef1c2cdaf7c53f569219c8) | fix | Account for addEventListener to be passed a Window or Document. ([#57354](https://github.com/angular/angular/pull/57354)) |
| [0e024ecc27](https://github.com/angular/angular/commit/0e024ecc27815c308feef0dbdf36d4d751af4436) | fix | complete post-hydration cleanup in components that use ViewContainerRef ([#57300](https://github.com/angular/angular/pull/57300)) |
| [822db64b93](https://github.com/angular/angular/commit/822db64b937db8a581ec9612cf9e3e6e149c820f) | fix | skip hydration for i18n nodes that were not projected ([#57356](https://github.com/angular/angular/pull/57356)) |
| [810f76f574](https://github.com/angular/angular/commit/810f76f57416853d5bc006e57bea070416117e79) | fix | take skip hydration flag into account while hydrating i18n blocks ([#57299](https://github.com/angular/angular/pull/57299)) |
<!-- CHANGELOG SPLIT MARKER -->
<a name="18.1.4"></a>
# 18.1.4 (2024-08-07)
### compiler
| Commit | Type | Description |
| -- | -- | -- |
| [6a99f83659](https://github.com/angular/angular/commit/6a99f836593de35989cddc3db849da133814f8fb) | fix | reduce chance of conflicts between generated factory and local variables ([#57181](https://github.com/angular/angular/pull/57181)) |
### compiler-cli
| Commit | Type | Description |
| -- | -- | -- |
| [afb05ff1cb](https://github.com/angular/angular/commit/afb05ff1cb008478c98987edfa54ec848ae555d6) | fix | support JIT transforms before other transforms modifying classes ([#57262](https://github.com/angular/angular/pull/57262)) |
| [bae54a1621](https://github.com/angular/angular/commit/bae54a1621e1c0427519abfa81fc4796c43f1551) | perf | improve performance of `interpolatedSignalNotInvoked` extended diagnostic ([#57291](https://github.com/angular/angular/pull/57291)) |
### language-service
| Commit | Type | Description |
| -- | -- | -- |
| [6ac209c24f](https://github.com/angular/angular/commit/6ac209c24f46a1240b1a186eb403c79ef596d68a) | fix | avoid generating TS suggestion diagnostics for templates ([#56241](https://github.com/angular/angular/pull/56241)) |
<!-- CHANGELOG SPLIT MARKER -->
<a name="18.1.3"></a>
# 18.1.3 (2024-07-31)
### compiler
| Commit | Type | Description |
| -- | -- | -- |
| [31dea066d6](https://github.com/angular/angular/commit/31dea066d636bb49fa18b1172815b1ef7af4dbe5) | fix | reduce chance of conflicts between generated factory and local variables ([#57181](https://github.com/angular/angular/pull/57181)) |
### compiler-cli
| Commit | Type | Description |
| -- | -- | -- |
| [1f9e090910](https://github.com/angular/angular/commit/1f9e09091076924a2f2c2d4bd6e5f65303bf8fea) | fix | emitting references to ngtypecheck files ([#57138](https://github.com/angular/angular/pull/57138)) ([#57202](https://github.com/angular/angular/pull/57202)) |
### core
| Commit | Type | Description |
| -- | -- | -- |
| [f7ab04018e](https://github.com/angular/angular/commit/f7ab04018ea0fe12781de987fff40fd1dc30f3f0) | fix | errors during ApplicationRef.tick should be rethrown for zoneless tests ([#56993](https://github.com/angular/angular/pull/56993)) |
| [eaa83f9d27](https://github.com/angular/angular/commit/eaa83f9d279855b104597d396e39fe3496470daf) | fix | hydration error in some let declaration setups ([#57173](https://github.com/angular/angular/pull/57173)) |
<!-- CHANGELOG SPLIT MARKER -->
<a name="18.1.2"></a>
# 18.1.2 (2024-07-24)
### compiler
| Commit | Type | Description |
| -- | -- | -- |
| [463945003d](https://github.com/angular/angular/commit/463945003dcf253c64809ffdcddabedb87e78e06) | fix | limit the number of chained instructions ([#57069](https://github.com/angular/angular/pull/57069)) |
### compiler-cli
| Commit | Type | Description |
| -- | -- | -- |
| [e904f34020](https://github.com/angular/angular/commit/e904f3402053ce9098854a31f2652020dd79e8f9) | fix | add warning for unused let declarations ([#57033](https://github.com/angular/angular/pull/57033)) |
### core
| Commit | Type | Description |
| -- | -- | -- |
| [9e52c1c840](https://github.com/angular/angular/commit/9e52c1c8402dd5a54cf73caafff4a5b81d73fabd) | fix | `afterNextRender` hooks return that callback value. ([#57031](https://github.com/angular/angular/pull/57031)) |
| [b9fb98c67c](https://github.com/angular/angular/commit/b9fb98c67c29d8e5697b72788f09f1263e8130f4) | fix | tree shake dev mode error message ([#57035](https://github.com/angular/angular/pull/57035)) |
<!-- CHANGELOG SPLIT MARKER -->
<a name="18.1.1"></a>
# 18.1.1 (2024-07-17)
### common
| Commit | Type | Description |
| -- | -- | -- |
| [a1cb9dfc0d](https://github.com/angular/angular/commit/a1cb9dfc0d3539d16020a53dd4c32311240a6265) | fix | Don't run preconnect assertion on the server. ([#56213](https://github.com/angular/angular/pull/56213)) |
### compiler
| Commit | Type | Description |
| -- | -- | -- |
| [daf0317bdc](https://github.com/angular/angular/commit/daf0317bdcef79445eead4a3e0e1be75671f43fb) | fix | JIT mode incorrectly interpreting host directive configuration in partial compilation ([#57002](https://github.com/angular/angular/pull/57002)) |
| [d7dca6dbb6](https://github.com/angular/angular/commit/d7dca6dbb6d8afc77a988de0b7471ac4e078762e) | fix | use strict equality for 'code' comparison ([#56944](https://github.com/angular/angular/pull/56944)) |
### compiler-cli
| Commit | Type | Description |
| -- | -- | -- |
| [c94a897248](https://github.com/angular/angular/commit/c94a8972488f62656f5d0368b8763776e2cd39c6) | fix | avoid emitting references to typecheck files in TS 5.4 ([#56961](https://github.com/angular/angular/pull/56961)) |
### core
| Commit | Type | Description |
| -- | -- | -- |
| [5682527d94](https://github.com/angular/angular/commit/5682527d949b078e92ff7588c7e25dad37fffc52) | fix | not all callbacks running when registered at the same time ([#56981](https://github.com/angular/angular/pull/56981)) |
### migrations
| Commit | Type | Description |
| -- | -- | -- |
| [b666d2c20f](https://github.com/angular/angular/commit/b666d2c20f932f435fa6c51e1d74d7bca53381f2) | fix | fix common module removal ([#56968](https://github.com/angular/angular/pull/56968)) |
<!-- CHANGELOG SPLIT MARKER -->
<a name="18.1.0"></a>
# 18.1.0 (2024-07-10)
### common
| Commit | Type | Description |
| -- | -- | -- |
| [f25653e231](https://github.com/angular/angular/commit/f25653e2311152d30b14d25acb0dccb4e2b5ea56) | fix | typo in NgOptimizedImage warning ([#56756](https://github.com/angular/angular/pull/56756)) |
| [9b35726e42](https://github.com/angular/angular/commit/9b35726e42ebdeed138a25581e0a7eefff466206) | fix | typo in warning for NgOptimizedDirective ([#56817](https://github.com/angular/angular/pull/56817)) |
### compiler
| Commit | Type | Description |
| -- | -- | -- |
| [fd6cd0422d](https://github.com/angular/angular/commit/fd6cd0422d2d761d2c6cc0cd41838fbba8a3f010) | feat | Add extended diagnostic to warn when there are uncalled functions in event bindings ([#56295](https://github.com/angular/angular/pull/56295)) |
| [341a116d61](https://github.com/angular/angular/commit/341a116d611c095ed414c82612adb529e7be310c) | fix | allow more characters in let declaration name ([#56764](https://github.com/angular/angular/pull/56764)) |
| [2a1291e942](https://github.com/angular/angular/commit/2a1291e942a3cd645ee635e72e7d83722383d39b) | fix | give precedence to local let declarations over parent ones ([#56752](https://github.com/angular/angular/pull/56752)) |
### compiler-cli
| Commit | Type | Description |
| -- | -- | -- |
| [66e582551e](https://github.com/angular/angular/commit/66e582551eb081e422e0df41badce1821c46dc62) | fix | avoid duplicate diagnostics for let declarations read before definition ([#56843](https://github.com/angular/angular/pull/56843)) |
| [4d18c5bfd5](https://github.com/angular/angular/commit/4d18c5bfd54c53655955c8cd90472081ade40b34) | fix | flag all conflicts between let declarations and local symbols ([#56752](https://github.com/angular/angular/pull/56752)) |
| [9e21582456](https://github.com/angular/angular/commit/9e215824565f0d30da7edb20087c4460069a6660) | fix | Show template syntax errors in local compilation modified ([#55855](https://github.com/angular/angular/pull/55855)) |
| [5996502921](https://github.com/angular/angular/commit/599650292107f8856c7cd41791bd0856f9d14eb1) | fix | type check let declarations nested inside nodes ([#56752](https://github.com/angular/angular/pull/56752)) |
| [cdebf751e4](https://github.com/angular/angular/commit/cdebf751e4949048b01acc92de2517f46fcd5d37) | fix | used before declared diagnostic not firing for control flow blocks ([#56843](https://github.com/angular/angular/pull/56843)) |
### core
| Commit | Type | Description |
| -- | -- | -- |
| [ea3c802056](https://github.com/angular/angular/commit/ea3c80205653af109c688a4d4808143b34591d54) | feat | Add a schematic to migrate afterRender phase flag ([#55648](https://github.com/angular/angular/pull/55648)) |
| [5df3e78c99](https://github.com/angular/angular/commit/5df3e78c9907f522f2f96c087b10ca12d57f7028) | feat | add equality function to rxjs-interop `toSignal` ([#56447](https://github.com/angular/angular/pull/56447)) |
| [0a48d584f2](https://github.com/angular/angular/commit/0a48d584f2ffeebb9402032182d4fc13a260c5cf) | feat | add support for let syntax ([#56715](https://github.com/angular/angular/pull/56715)) |
| [352e0782ec](https://github.com/angular/angular/commit/352e0782ec37d2adcc662cfc69c83d38058a34bf) | feat | expose signal input metadata in `ComponentMirror` ([#56402](https://github.com/angular/angular/pull/56402)) |
| [a655e46447](https://github.com/angular/angular/commit/a655e46447962bf56bf0184e3104328b9f7c1531) | feat | Redesign the `afterRender` & `afterNextRender` phases API ([#55648](https://github.com/angular/angular/pull/55648)) |
| [e5a6f91722](https://github.com/angular/angular/commit/e5a6f917225aafa7c5c860f280d2aafe3615727e) | feat | support TypeScript 5.5 ([#56096](https://github.com/angular/angular/pull/56096)) |
| [38effcc63e](https://github.com/angular/angular/commit/38effcc63eea360e948dc22860add72d3aa02288) | fix | Add back phase flag option as a deprecated API ([#55648](https://github.com/angular/angular/pull/55648)) |
| [86bcfd3e49](https://github.com/angular/angular/commit/86bcfd3e498b8ec1de1a2a1ad0847fe567f7e9d4) | fix | improve docs on afterRender hooks ([#56522](https://github.com/angular/angular/pull/56522)) |
| [b2445a0953](https://github.com/angular/angular/commit/b2445a095314aa66da038d3093e6a1b18fe5768b) | fix | link errors to ADEV ([#55554](https://github.com/angular/angular/pull/55554)) ([#56038](https://github.com/angular/angular/pull/56038)) |
| [03a2acd2a3](https://github.com/angular/angular/commit/03a2acd2a3bdc87aaeb6b835a7c1016f800b31cb) | fix | properly remove imports in the afterRender phase migration ([#56524](https://github.com/angular/angular/pull/56524)) |
| [4d87b9e899](https://github.com/angular/angular/commit/4d87b9e899381894a1de90f251da58613a96eed0) | fix | rename the equality function option in toSignal ([#56769](https://github.com/angular/angular/pull/56769)) ([#56922](https://github.com/angular/angular/pull/56922)) |
| [8bd4c074af](https://github.com/angular/angular/commit/8bd4c074afe85b739dff4d3c4dcc19384c42b85e) | fix | toSignal equal option should be passed to inner computed ([#56903](https://github.com/angular/angular/pull/56903)) |
### forms
| Commit | Type | Description |
| -- | -- | -- |
| [00bde8b1c2](https://github.com/angular/angular/commit/00bde8b1c2d1511da40526a374d4e94d31e0d575) | fix | Make `NgControlStatus` host bindings `OnPush` compatible ([#55720](https://github.com/angular/angular/pull/55720)) |
### http
| Commit | Type | Description |
| -- | -- | -- |
| [cc21989132](https://github.com/angular/angular/commit/cc21989132bc64b981df83cb6ff6e1506b42a1d0) | fix | Make `Content-Type` header case insensitive ([#56541](https://github.com/angular/angular/pull/56541)) |
### language-service
| Commit | Type | Description |
| -- | -- | -- |
| [b400e2e4d4](https://github.com/angular/angular/commit/b400e2e4d4c27a9c8d8e91b52852ef7b64f7591a) | feat | autocompletion for the component not imported ([#55595](https://github.com/angular/angular/pull/55595)) |
| [67b2c336bc](https://github.com/angular/angular/commit/67b2c336bc0bdce3f7ae054c094990a9831f5b20) | fix | import the default exported component correctly ([#56432](https://github.com/angular/angular/pull/56432)) |
### router
| Commit | Type | Description |
| -- | -- | -- |
| [a13f5da773](https://github.com/angular/angular/commit/a13f5da77303f4ab2f1543df1de1f416216b5a9c) | feat | Allow `UrlTree` as an input to `routerLink` ([#56265](https://github.com/angular/angular/pull/56265)) |
| [1d3a7529b4](https://github.com/angular/angular/commit/1d3a7529b4fa3617a5d6a97e742cb13818253a14) | feat | Set a different browser URL from the one for route matching ([#53318](https://github.com/angular/angular/pull/53318)) |
<!-- CHANGELOG SPLIT MARKER -->
<a name="18.0.5"></a>
# 18.0.5 (2024-06-26)
### core
@@ -9,51 +163,6 @@
<!-- CHANGELOG SPLIT MARKER -->
<a name="18.1.0-next.4"></a>
# 18.1.0-next.4 (2024-06-26)
### core
| Commit | Type | Description |
| -- | -- | -- |
| [0a48d584f2](https://github.com/angular/angular/commit/0a48d584f2ffeebb9402032182d4fc13a260c5cf) | feat | add support for let syntax ([#56715](https://github.com/angular/angular/pull/56715)) |
| [29ca6d10cc](https://github.com/angular/angular/commit/29ca6d10cc3cd75ebdf64658dafcb3ce579af343) | fix | improve support for i18n hydration of projected content ([#56192](https://github.com/angular/angular/pull/56192)) |
| [5be16d06bd](https://github.com/angular/angular/commit/5be16d06bd504b1a72b7fb6f8d6e0c1b93a8188b) | fix | prevent calling devMode only function on `@defer` error. ([#56559](https://github.com/angular/angular/pull/56559)) |
### http
| Commit | Type | Description |
| -- | -- | -- |
| [cc21989132](https://github.com/angular/angular/commit/cc21989132bc64b981df83cb6ff6e1506b42a1d0) | fix | Make `Content-Type` header case insensitive ([#56541](https://github.com/angular/angular/pull/56541)) |
<!-- CHANGELOG SPLIT MARKER -->
<a name="18.1.0-next.3"></a>
# 18.1.0-next.3 (2024-06-20)
### compiler-cli
| Commit | Type | Description |
| -- | -- | -- |
| [0b867e83b6](https://github.com/angular/angular/commit/0b867e83b624cbfe449ed3b7daf87642337824f0) | fix | insert constant statements after the first group of imports ([#56431](https://github.com/angular/angular/pull/56431)) |
### core
| Commit | Type | Description |
| -- | -- | -- |
| [5df3e78c99](https://github.com/angular/angular/commit/5df3e78c9907f522f2f96c087b10ca12d57f7028) | feat | add equality function to rxjs-interop `toSignal` ([#56447](https://github.com/angular/angular/pull/56447)) |
| [352e0782ec](https://github.com/angular/angular/commit/352e0782ec37d2adcc662cfc69c83d38058a34bf) | feat | expose signal input metadata in `ComponentMirror` ([#56402](https://github.com/angular/angular/pull/56402)) |
| [bf6df6f186](https://github.com/angular/angular/commit/bf6df6f18658dd0d477271f7eb969317ce1df024) | fix | do not activate event replay when no events are registered ([#56509](https://github.com/angular/angular/pull/56509)) |
| [86bcfd3e49](https://github.com/angular/angular/commit/86bcfd3e498b8ec1de1a2a1ad0847fe567f7e9d4) | fix | improve docs on afterRender hooks ([#56522](https://github.com/angular/angular/pull/56522)) |
| [03a2acd2a3](https://github.com/angular/angular/commit/03a2acd2a3bdc87aaeb6b835a7c1016f800b31cb) | fix | properly remove imports in the afterRender phase migration ([#56524](https://github.com/angular/angular/pull/56524)) |
### forms
| Commit | Type | Description |
| -- | -- | -- |
| [00bde8b1c2](https://github.com/angular/angular/commit/00bde8b1c2d1511da40526a374d4e94d31e0d575) | fix | Make `NgControlStatus` host bindings `OnPush` compatible ([#55720](https://github.com/angular/angular/pull/55720)) |
### language-service
| Commit | Type | Description |
| -- | -- | -- |
| [67b2c336bc](https://github.com/angular/angular/commit/67b2c336bc0bdce3f7ae054c094990a9831f5b20) | fix | import the default exported component correctly ([#56432](https://github.com/angular/angular/pull/56432)) |
### router
| Commit | Type | Description |
| -- | -- | -- |
| [1d3a7529b4](https://github.com/angular/angular/commit/1d3a7529b4fa3617a5d6a97e742cb13818253a14) | feat | Set a different browser URL from the one for route matching ([#53318](https://github.com/angular/angular/pull/53318)) |
| [0d52c6b182](https://github.com/angular/angular/commit/0d52c6b18275433923a5fe94f3fc24323b73d0be) | fix | Delay the view transition to ensure renders in microtasks complete ([#56494](https://github.com/angular/angular/pull/56494)) |
<!-- CHANGELOG SPLIT MARKER -->
<a name="18.0.4"></a>
# 18.0.4 (2024-06-20)
### compiler-cli
@@ -71,39 +180,6 @@
<!-- CHANGELOG SPLIT MARKER -->
<a name="18.1.0-next.2"></a>
# 18.1.0-next.2 (2024-06-12)
### benchpress
| Commit | Type | Description |
| -- | -- | -- |
| [31d9fa40bdb](https://github.com/angular/angular/commit/31d9fa40bdbd1b815a819527312994b6ab1b532f) | fix | adjust supported browser names for headless chrome ([#56360](https://github.com/angular/angular/pull/56360)) |
### core
| Commit | Type | Description |
| -- | -- | -- |
| [ea3c8020565](https://github.com/angular/angular/commit/ea3c80205653af109c688a4d4808143b34591d54) | feat | Add a schematic to migrate afterRender phase flag ([#55648](https://github.com/angular/angular/pull/55648)) |
| [a655e464479](https://github.com/angular/angular/commit/a655e46447962bf56bf0184e3104328b9f7c1531) | feat | Redesign the `afterRender` & `afterNextRender` phases API ([#55648](https://github.com/angular/angular/pull/55648)) |
| [38effcc63ee](https://github.com/angular/angular/commit/38effcc63eea360e948dc22860add72d3aa02288) | fix | Add back phase flag option as a deprecated API ([#55648](https://github.com/angular/angular/pull/55648)) |
| [d5c6ee432fc](https://github.com/angular/angular/commit/d5c6ee432fcd467c09b4d5d5366e731f5c91e8d4) | fix | async EventEmitter should contribute to app stability ([#56308](https://github.com/angular/angular/pull/56308)) |
| [4c7d5d8acd8](https://github.com/angular/angular/commit/4c7d5d8acd8a714fe89366f76dc69f91356f0a06) | fix | signals should be tracked when embeddedViewRef.detectChanges is called ([#55719](https://github.com/angular/angular/pull/55719)) |
### language-service
| Commit | Type | Description |
| -- | -- | -- |
| [b400e2e4d4c](https://github.com/angular/angular/commit/b400e2e4d4c27a9c8d8e91b52852ef7b64f7591a) | feat | autocompletion for the component not imported ([#55595](https://github.com/angular/angular/pull/55595)) |
### localize
| Commit | Type | Description |
| -- | -- | -- |
| [81486c2f477](https://github.com/angular/angular/commit/81486c2f4770361b4aecdc891107f70446e43bc3) | fix | add `@angular/localize/init` as polyfill in `angular.json` ([#56300](https://github.com/angular/angular/pull/56300)) |
### migrations
| Commit | Type | Description |
| -- | -- | -- |
| [96aa5c8a2c9](https://github.com/angular/angular/commit/96aa5c8a2c9dd607dbcffa5d7923df4c21636510) | fix | resolve error in standalone migration ([#56302](https://github.com/angular/angular/pull/56302)) |
### router
| Commit | Type | Description |
| -- | -- | -- |
| [a13f5da7730](https://github.com/angular/angular/commit/a13f5da77303f4ab2f1543df1de1f416216b5a9c) | feat | Allow `UrlTree` as an input to `routerLink` ([#56265](https://github.com/angular/angular/pull/56265)) |
<!-- CHANGELOG SPLIT MARKER -->
<a name="18.0.3"></a>
# 18.0.3 (2024-06-12)
### benchpress
@@ -126,26 +202,6 @@
<!-- CHANGELOG SPLIT MARKER -->
<a name="18.1.0-next.1"></a>
# 18.1.0-next.1 (2024-06-05)
### compiler-cli
| Commit | Type | Description |
| -- | -- | -- |
| [9e21582456](https://github.com/angular/angular/commit/9e215824565f0d30da7edb20087c4460069a6660) | fix | Show template syntax errors in local compilation modified ([#55855](https://github.com/angular/angular/pull/55855)) |
### core
| Commit | Type | Description |
| -- | -- | -- |
| [8d75627ddd](https://github.com/angular/angular/commit/8d75627dddb82380dc8b3455a13f128e88309aa6) | fix | Do not migrate `HttpClientModule` imports on components. ([#56067](https://github.com/angular/angular/pull/56067)) |
| [1081c8d623](https://github.com/angular/angular/commit/1081c8d6233ba1ff09187b95a09b0644e130cdf8) | fix | don't coerce all producers to consumers on liveness change ([#56140](https://github.com/angular/angular/pull/56140)) |
| [b6fb53cdc7](https://github.com/angular/angular/commit/b6fb53cdc7c9f9dad5a113a68e72a2b723626bef) | fix | Fix shouldPreventDefaultBeforeDispatching bug ([#56188](https://github.com/angular/angular/pull/56188)) |
| [31f3975e4b](https://github.com/angular/angular/commit/31f3975e4b1f418c1a6b2516618f44093ba20175) | fix | handle missing `withI18nSupport()` call for components that use i18n blocks ([#56175](https://github.com/angular/angular/pull/56175)) |
### migrations
| Commit | Type | Description |
| -- | -- | -- |
| [75e811c18b](https://github.com/angular/angular/commit/75e811c18b16f42c6f72ac32e44c4bc4d37fc3b1) | fix | do not generate aliased variables with the same name ([#56154](https://github.com/angular/angular/pull/56154)) |
<!-- CHANGELOG SPLIT MARKER -->
<a name="18.0.2"></a>
# 18.0.2 (2024-06-05)
### core
@@ -162,16 +218,6 @@
<!-- CHANGELOG SPLIT MARKER -->
<a name="18.1.0-next.0"></a>
# 18.1.0-next.0 (2024-05-29)
### core
| Commit | Type | Description |
| -- | -- | -- |
| [e5a6f91722](https://github.com/angular/angular/commit/e5a6f917225aafa7c5c860f280d2aafe3615727e) | feat | support TypeScript 5.5 ([#56096](https://github.com/angular/angular/pull/56096)) |
| [b2445a0953](https://github.com/angular/angular/commit/b2445a095314aa66da038d3093e6a1b18fe5768b) | fix | link errors to ADEV ([#55554](https://github.com/angular/angular/pull/55554)) ([#56038](https://github.com/angular/angular/pull/56038)) |
<!-- CHANGELOG SPLIT MARKER -->
<a name="18.0.1"></a>
# 18.0.1 (2024-05-29)
### compiler
+2 -2
View File
@@ -353,9 +353,9 @@ DEPRECATED: <what is deprecated>
Closes #<pr number>
```
Breaking Change section should start with the phrase "BREAKING CHANGE: " followed by a summary of the breaking change, a blank line, and a detailed description of the breaking change that also includes migration instructions.
Breaking Change section should start with the phrase `BREAKING CHANGE: ` followed by a summary of the breaking change, a blank line, and a detailed description of the breaking change that also includes migration instructions.
Similarly, a Deprecation section should start with "DEPRECATED: " followed by a short description of what is deprecated, a blank line, and a detailed description of the deprecation that also mentions the recommended update path.
Similarly, a Deprecation section should start with `DEPRECATED: ` followed by a short description of what is deprecated, a blank line, and a detailed description of the deprecation that also mentions the recommended update path.
### Revert commits
+1 -1
View File
@@ -18,7 +18,7 @@
·
<a href="https://github.com/angular/angular/issues">Submit an Issue</a>
·
<a href="https://blog.angular.io/">Blog</a>
<a href="https://blog.angular.dev/">Blog</a>
<br>
<br>
</p>
+4 -4
View File
@@ -75,7 +75,7 @@ yarn_install(
"//:.yarnrc",
"//:tools/npm-patches/@bazel+jasmine+5.8.1.patch",
"//tools:postinstall-patches.js",
"//tools/esm-interop:patches/npm/@angular+build-tooling+0.0.0-239d56b71911f9fa1eeefb6e4505dbe7b0cd81a7.patch",
"//tools/esm-interop:patches/npm/@angular+build-tooling+0.0.0-d30a56c19bafaac67cf44e605ed8c2c0e45b0a51.patch",
"//tools/esm-interop:patches/npm/@bazel+concatjs+5.8.1.patch",
"//tools/esm-interop:patches/npm/@bazel+esbuild+5.7.1.patch",
"//tools/esm-interop:patches/npm/@bazel+protractor+5.7.1.patch",
@@ -143,10 +143,10 @@ cldr_xml_data_repository(
# sass rules
http_archive(
name = "io_bazel_rules_sass",
sha256 = "47c50aa960ddf875a2a2dd7efd1c839e5f8598725c00014680122b0e48d0ec7f",
strip_prefix = "rules_sass-b222c61b3d3879ec45b66062b2c706a72f3d80bb",
sha256 = "cd83736ea65d0df064283aea5922dbaf132dd2b3aa54e7151aae7edaa9572c3e",
strip_prefix = "rules_sass-83022b98114c07e9588089c7fe8f76bc0262c7e7",
urls = [
"https://github.com/bazelbuild/rules_sass/archive/b222c61b3d3879ec45b66062b2c706a72f3d80bb.zip",
"https://github.com/bazelbuild/rules_sass/archive/83022b98114c07e9588089c7fe8f76bc0262c7e7.zip",
],
)
+10 -6
View File
@@ -7,6 +7,10 @@ load("//adev/tools/local_deps:index.bzl", "ensure_local_package_deps", "link_loc
package(default_visibility = ["//visibility:public"])
exports_files([
"tsconfig.json",
])
# All source and configuration files required to build the docs app
APPLICATION_FILES = [
"angular.json",
@@ -17,18 +21,15 @@ APPLICATION_FILES = [
["src/**/*"],
exclude = [
"src/**/*.spec.ts",
# Temporarily exclude generated sources produced by the non-bazel
# build until the whole project is built by bazel and this directory
# isn't needed.
"src/generated/**/*",
],
)
TEST_FILES = APPLICATION_FILES + [
"karma.conf.js",
"test-main.ts",
"tsconfig.spec.json",
] + glob(
["**/*.spec.ts"],
["src/**/*.spec.ts"],
)
APPLICATION_ASSETS = [
@@ -42,6 +43,7 @@ APPLICATION_ASSETS = [
]
APPLICATION_DEPS = [
"@npm//@angular/docs",
"@npm//@angular/build",
"@npm//@angular-devkit/build-angular",
"@npm//@angular/animations",
@@ -50,7 +52,6 @@ APPLICATION_DEPS = [
"@npm//@angular/compiler",
"@npm//@angular/compiler-cli",
"@npm//@angular/core",
"@npm//@angular/docs",
"@npm//@angular/forms",
"@npm//@angular/material",
"@npm//@angular/platform-browser",
@@ -78,6 +79,9 @@ APPLICATION_DEPS = [
"@npm//@lezer/highlight",
"@npm//@lezer/javascript",
"@npm//@lezer/common",
"@npm//@stackblitz/sdk",
"@npm//open-in-idx",
"@npm//@webcontainer/api",
"@npm//@xterm/xterm",
"@npm//@xterm/addon-fit",
"@npm//algoliasearch",
+17 -1
View File
@@ -2,7 +2,7 @@
This site is built with Angular.
The content is written primarly in Markdown format located in `src/content`. For simple edits, you can directly edit the file on GitHub and generate a Pull Request.
The content is written primarily in Markdown format located in `src/content`. For simple edits, you can directly edit the file on GitHub and generate a Pull Request.
## Local Development
@@ -24,6 +24,8 @@ yarn
yarn docs
```
If you are having issues with the docs building, see the [FAQs](#faqs) section.
## Contributing
Want to report a bug, contribute some code, or improve the documentation? Excellent!
@@ -35,3 +37,17 @@ And if you're new, check out one of our issues labeled as <kbd>[help wanted](htt
### Code of Conduct
Help us keep Angular open and inclusive. Please read and follow our [Code of Conduct](CODE_OF_CONDUCT.md).
## FAQs
### The build is failing and I'm seeing `bazel:bazel failed: missing input file` messages.
This is most likely due to a bazel dependency / caching issue. To resolve this, run the following command:
```
# Try this first
yarn bazel clean
# If that doesn't work, try it with the expunge flag
yarn bazel clean --expunge
```
+1 -1
View File
@@ -84,10 +84,10 @@
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": ["zone.js", "zone.js/testing"],
"tsConfig": "tsconfig.spec.json",
"include": ["src/app"],
"karmaConfig": "karma.conf.js",
"main": "test-main.ts",
"inlineStyleLanguage": "scss",
"assets": ["src/favicon.ico", "src/assets"],
"styles": ["@angular/docs/styles/global-styles.scss"],
+1 -1
View File
@@ -5,7 +5,7 @@
"ignore": ["**/.*"],
"headers": [
{
"source": "assets/tutorials/common/*.jpg",
"source": "assets/images/tutorials/common/*.jpg",
"headers": [
{
"key": "Cross-Origin-Resource-Policy",
+49
View File
@@ -0,0 +1,49 @@
load("//tools:defaults.bzl", "ng_module", "ng_package")
package(default_visibility = ["//visibility:private"])
ng_module(
name = "docs",
srcs = [
"index.ts",
],
module_name = "@angular/docs",
deps = [
"//adev/shared-docs/components",
"//adev/shared-docs/constants",
"//adev/shared-docs/directives",
"//adev/shared-docs/interfaces",
"//adev/shared-docs/pipes",
"//adev/shared-docs/providers",
"//adev/shared-docs/services",
"//adev/shared-docs/testing",
"//adev/shared-docs/utils",
],
)
ng_package(
name = "npm_package",
srcs = [
"package.json",
"//adev/shared-docs/icons",
"//adev/shared-docs/pipeline:BUILD.bazel",
"//adev/shared-docs/pipeline:_guides.bzl",
"//adev/shared-docs/pipeline:_playground.bzl",
"//adev/shared-docs/pipeline:_stackblitz.bzl",
"//adev/shared-docs/pipeline:_tutorial.bzl",
"//adev/shared-docs/pipeline:guides.mjs",
"//adev/shared-docs/pipeline:guides-no-mermaid.mjs",
"//adev/shared-docs/pipeline:playground.mjs",
"//adev/shared-docs/pipeline:stackblitz.mjs",
"//adev/shared-docs/pipeline:tutorial.mjs",
"//adev/shared-docs/pipeline/examples/template:files",
"//adev/shared-docs/pipeline/tutorials/common:files",
"//adev/shared-docs/styles",
],
visibility = [
"//adev:__pkg__",
],
deps = [
":docs",
],
)
+27
View File
@@ -0,0 +1,27 @@
load("//tools:defaults.bzl", "ts_library")
package(default_visibility = ["//visibility:private"])
ts_library(
name = "components",
srcs = [
"index.ts",
],
visibility = [
"//adev/shared-docs:__pkg__",
],
deps = [
"//adev/shared-docs/components/algolia-icon",
"//adev/shared-docs/components/breadcrumb",
"//adev/shared-docs/components/cookie-popup",
"//adev/shared-docs/components/copy-source-code-button",
"//adev/shared-docs/components/icon",
"//adev/shared-docs/components/navigation-list",
"//adev/shared-docs/components/search-dialog",
"//adev/shared-docs/components/select",
"//adev/shared-docs/components/slide-toggle",
"//adev/shared-docs/components/table-of-contents",
"//adev/shared-docs/components/text-field",
"//adev/shared-docs/components/viewers",
],
)
@@ -0,0 +1,20 @@
load("//tools:defaults.bzl", "ng_module")
package(default_visibility = ["//visibility:private"])
ng_module(
name = "algolia-icon",
srcs = [
"algolia-icon.component.ts",
],
assets = [
"algolia-icon.component.html",
],
visibility = [
"//adev/shared-docs/components:__pkg__",
"//adev/shared-docs/components/search-dialog:__pkg__",
],
deps = [
"//packages/core",
],
)
@@ -0,0 +1,52 @@
<!-- Algolia logo -->
<svg
id="Layer_1"
class="docs-algolia-logo"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 2196.2 500"
>
<defs>
<style>
.cls-1,
.cls-2 {
fill: #003dff;
}
.cls-2 {
fill-rule: evenodd;
}
</style>
</defs>
<path
class="cls-2"
d="M1070.38,275.3V5.91c0-3.63-3.24-6.39-6.82-5.83l-50.46,7.94c-2.87,.45-4.99,2.93-4.99,5.84l.17,273.22c0,12.92,0,92.7,95.97,95.49,3.33,.1,6.09-2.58,6.09-5.91v-40.78c0-2.96-2.19-5.51-5.12-5.84-34.85-4.01-34.85-47.57-34.85-54.72Z"
/>
<rect class="cls-1" x="1845.88" y="104.73" width="62.58" height="277.9" rx="5.9" ry="5.9" />
<path
class="cls-2"
d="M1851.78,71.38h50.77c3.26,0,5.9-2.64,5.9-5.9V5.9c0-3.62-3.24-6.39-6.82-5.83l-50.77,7.95c-2.87,.45-4.99,2.92-4.99,5.83v51.62c0,3.26,2.64,5.9,5.9,5.9Z"
/>
<path
class="cls-2"
d="M1764.03,275.3V5.91c0-3.63-3.24-6.39-6.82-5.83l-50.46,7.94c-2.87,.45-4.99,2.93-4.99,5.84l.17,273.22c0,12.92,0,92.7,95.97,95.49,3.33,.1,6.09-2.58,6.09-5.91v-40.78c0-2.96-2.19-5.51-5.12-5.84-34.85-4.01-34.85-47.57-34.85-54.72Z"
/>
<path
class="cls-2"
d="M1631.95,142.72c-11.14-12.25-24.83-21.65-40.78-28.31-15.92-6.53-33.26-9.85-52.07-9.85-18.78,0-36.15,3.17-51.92,9.85-15.59,6.66-29.29,16.05-40.76,28.31-11.47,12.23-20.38,26.87-26.76,44.03-6.38,17.17-9.24,37.37-9.24,58.36,0,20.99,3.19,36.87,9.55,54.21,6.38,17.32,15.14,32.11,26.45,44.36,11.29,12.23,24.83,21.62,40.6,28.46,15.77,6.83,40.12,10.33,52.4,10.48,12.25,0,36.78-3.82,52.7-10.48,15.92-6.68,29.46-16.23,40.78-28.46,11.29-12.25,20.05-27.04,26.25-44.36,6.22-17.34,9.24-33.22,9.24-54.21,0-20.99-3.34-41.19-10.03-58.36-6.38-17.17-15.14-31.8-26.43-44.03Zm-44.43,163.75c-11.47,15.75-27.56,23.7-48.09,23.7-20.55,0-36.63-7.8-48.1-23.7-11.47-15.75-17.21-34.01-17.21-61.2,0-26.89,5.59-49.14,17.06-64.87,11.45-15.75,27.54-23.52,48.07-23.52,20.55,0,36.63,7.78,48.09,23.52,11.47,15.57,17.36,37.98,17.36,64.87,0,27.19-5.72,45.3-17.19,61.2Z"
/>
<path
class="cls-2"
d="M894.42,104.73h-49.33c-48.36,0-90.91,25.48-115.75,64.1-14.52,22.58-22.99,49.63-22.99,78.73,0,44.89,20.13,84.92,51.59,111.1,2.93,2.6,6.05,4.98,9.31,7.14,12.86,8.49,28.11,13.47,44.52,13.47,1.23,0,2.46-.03,3.68-.09,.36-.02,.71-.05,1.07-.07,.87-.05,1.75-.11,2.62-.2,.34-.03,.68-.08,1.02-.12,.91-.1,1.82-.21,2.73-.34,.21-.03,.42-.07,.63-.1,32.89-5.07,61.56-30.82,70.9-62.81v57.83c0,3.26,2.64,5.9,5.9,5.9h50.42c3.26,0,5.9-2.64,5.9-5.9V110.63c0-3.26-2.64-5.9-5.9-5.9h-56.32Zm0,206.92c-12.2,10.16-27.97,13.98-44.84,15.12-.16,.01-.33,.03-.49,.04-1.12,.07-2.24,.1-3.36,.1-42.24,0-77.12-35.89-77.12-79.37,0-10.25,1.96-20.01,5.42-28.98,11.22-29.12,38.77-49.74,71.06-49.74h49.33v142.83Z"
/>
<path
class="cls-2"
d="M2133.97,104.73h-49.33c-48.36,0-90.91,25.48-115.75,64.1-14.52,22.58-22.99,49.63-22.99,78.73,0,44.89,20.13,84.92,51.59,111.1,2.93,2.6,6.05,4.98,9.31,7.14,12.86,8.49,28.11,13.47,44.52,13.47,1.23,0,2.46-.03,3.68-.09,.36-.02,.71-.05,1.07-.07,.87-.05,1.75-.11,2.62-.2,.34-.03,.68-.08,1.02-.12,.91-.1,1.82-.21,2.73-.34,.21-.03,.42-.07,.63-.1,32.89-5.07,61.56-30.82,70.9-62.81v57.83c0,3.26,2.64,5.9,5.9,5.9h50.42c3.26,0,5.9-2.64,5.9-5.9V110.63c0-3.26-2.64-5.9-5.9-5.9h-56.32Zm0,206.92c-12.2,10.16-27.97,13.98-44.84,15.12-.16,.01-.33,.03-.49,.04-1.12,.07-2.24,.1-3.36,.1-42.24,0-77.12-35.89-77.12-79.37,0-10.25,1.96-20.01,5.42-28.98,11.22-29.12,38.77-49.74,71.06-49.74h49.33v142.83Z"
/>
<path
class="cls-2"
d="M1314.05,104.73h-49.33c-48.36,0-90.91,25.48-115.75,64.1-11.79,18.34-19.6,39.64-22.11,62.59-.58,5.3-.88,10.68-.88,16.14s.31,11.15,.93,16.59c4.28,38.09,23.14,71.61,50.66,94.52,2.93,2.6,6.05,4.98,9.31,7.14,12.86,8.49,28.11,13.47,44.52,13.47h0c17.99,0,34.61-5.93,48.16-15.97,16.29-11.58,28.88-28.54,34.48-47.75v50.26h-.11v11.08c0,21.84-5.71,38.27-17.34,49.36-11.61,11.08-31.04,16.63-58.25,16.63-11.12,0-28.79-.59-46.6-2.41-2.83-.29-5.46,1.5-6.27,4.22l-12.78,43.11c-1.02,3.46,1.27,7.02,4.83,7.53,21.52,3.08,42.52,4.68,54.65,4.68,48.91,0,85.16-10.75,108.89-32.21,21.48-19.41,33.15-48.89,35.2-88.52V110.63c0-3.26-2.64-5.9-5.9-5.9h-56.32Zm0,64.1s.65,139.13,0,143.36c-12.08,9.77-27.11,13.59-43.49,14.7-.16,.01-.33,.03-.49,.04-1.12,.07-2.24,.1-3.36,.1-1.32,0-2.63-.03-3.94-.1-40.41-2.11-74.52-37.26-74.52-79.38,0-10.25,1.96-20.01,5.42-28.98,11.22-29.12,38.77-49.74,71.06-49.74h49.33Z"
/>
<path
class="cls-1"
d="M249.83,0C113.3,0,2,110.09,.03,246.16c-2,138.19,110.12,252.7,248.33,253.5,42.68,.25,83.79-10.19,120.3-30.03,3.56-1.93,4.11-6.83,1.08-9.51l-23.38-20.72c-4.75-4.21-11.51-5.4-17.36-2.92-25.48,10.84-53.17,16.38-81.71,16.03-111.68-1.37-201.91-94.29-200.13-205.96,1.76-110.26,92-199.41,202.67-199.41h202.69V407.41l-115-102.18c-3.72-3.31-9.42-2.66-12.42,1.31-18.46,24.44-48.53,39.64-81.93,37.34-46.33-3.2-83.87-40.5-87.34-86.81-4.15-55.24,39.63-101.52,94-101.52,49.18,0,89.68,37.85,93.91,85.95,.38,4.28,2.31,8.27,5.52,11.12l29.95,26.55c3.4,3.01,8.79,1.17,9.63-3.3,2.16-11.55,2.92-23.58,2.07-35.92-4.82-70.34-61.8-126.93-132.17-131.26-80.68-4.97-148.13,58.14-150.27,137.25-2.09,77.1,61.08,143.56,138.19,145.26,32.19,.71,62.03-9.41,86.14-26.95l150.26,133.2c6.44,5.71,16.61,1.14,16.61-7.47V9.48C499.66,4.25,495.42,0,490.18,0H249.83Z"
/>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

@@ -0,0 +1,18 @@
/*!
* @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 {ChangeDetectionStrategy, Component} from '@angular/core';
@Component({
selector: 'docs-algolia-icon',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [],
templateUrl: './algolia-icon.component.html',
})
export class AlgoliaIcon {}
@@ -0,0 +1,54 @@
load("//tools:defaults.bzl", "karma_web_test_suite", "ng_module", "ts_library")
load("@io_bazel_rules_sass//:defs.bzl", "sass_binary")
package(default_visibility = ["//visibility:private"])
ng_module(
name = "breadcrumb",
srcs = [
"breadcrumb.component.ts",
],
assets = [
":breadcrumb.component.css",
"breadcrumb.component.html",
],
visibility = [
"//adev/shared-docs/components:__pkg__",
"//adev/shared-docs/components/viewers:__pkg__",
],
deps = [
"//adev/shared-docs/interfaces",
"//adev/shared-docs/services",
"//packages/common",
"//packages/core",
"//packages/router",
],
)
sass_binary(
name = "style",
src = "breadcrumb.component.scss",
)
ts_library(
name = "test_lib",
testonly = True,
srcs = glob(
["*.spec.ts"],
),
deps = [
":breadcrumb",
"//adev/shared-docs/interfaces",
"//adev/shared-docs/services",
"//packages/core",
"//packages/core/testing",
"//packages/platform-browser",
"//packages/router",
"//packages/router/testing",
],
)
karma_web_test_suite(
name = "test",
deps = [":test_lib"],
)
@@ -0,0 +1,13 @@
@for (breadcrumb of breadcrumbItems(); track breadcrumb) {
<div class="docs-breadcrumb">
@if (breadcrumb.path) {
@if (breadcrumb.isExternal) {
<a [href]="breadcrumb.path">{{ breadcrumb.label }}</a>
} @else {
<a [routerLink]="'/' + breadcrumb.path">{{ breadcrumb.label }}</a>
}
} @else {
<span>{{ breadcrumb.label }}</span>
}
</div>
}
@@ -0,0 +1,25 @@
:host {
display: flex;
align-items: center;
padding-block-end: 1.5rem;
}
.docs-breadcrumb {
span {
color: var(--quaternary-contrast);
font-size: 0.875rem;
display: flex;
align-items: center;
}
&:not(:last-child) {
span {
&::after {
content: 'chevron_right';
font-family: var(--icons);
margin-inline: 0.5rem;
color: var(--quinary-contrast);
}
}
}
}
@@ -0,0 +1,83 @@
/*!
* @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 {ComponentFixture, TestBed} from '@angular/core/testing';
import {Breadcrumb} from './breadcrumb.component';
import {NavigationState} from '../../services';
import {NavigationItem} from '../../interfaces';
import {By} from '@angular/platform-browser';
import {RouterTestingModule} from '@angular/router/testing';
import {provideExperimentalZonelessChangeDetection} from '@angular/core';
describe('Breadcrumb', () => {
let fixture: ComponentFixture<Breadcrumb>;
let navigationStateSpy: jasmine.SpyObj<NavigationState>;
beforeEach(() => {
navigationStateSpy = jasmine.createSpyObj('NavigationState', ['activeNavigationItem']);
TestBed.configureTestingModule({
imports: [Breadcrumb, RouterTestingModule],
providers: [
provideExperimentalZonelessChangeDetection(),
{
provide: NavigationState,
useValue: navigationStateSpy,
},
],
});
fixture = TestBed.createComponent(Breadcrumb);
});
it('should display proper breadcrumb structure based on navigation state', () => {
navigationStateSpy.activeNavigationItem.and.returnValue(item);
fixture.detectChanges();
const breadcrumbs = fixture.debugElement.queryAll(By.css('.docs-breadcrumb span'));
expect(breadcrumbs.length).toBe(2);
expect(breadcrumbs[0].nativeElement.innerText).toEqual('Grandparent');
expect(breadcrumbs[1].nativeElement.innerText).toEqual('Parent');
});
it('should display breadcrumb links when navigation item has got path', () => {
navigationStateSpy.activeNavigationItem.and.returnValue(exampleItemWithPath);
fixture.detectChanges();
const breadcrumbs = fixture.debugElement.queryAll(By.css('.docs-breadcrumb a'));
expect(breadcrumbs.length).toBe(1);
expect(breadcrumbs[0].nativeElement.innerText).toEqual('Parent');
expect(breadcrumbs[0].nativeElement.href).toEqual(`${window.origin}/example`);
});
});
const grandparent: NavigationItem = {
label: 'Grandparent',
};
const parent: NavigationItem = {
label: 'Parent',
parent: grandparent,
};
const item: NavigationItem = {
label: 'Active Item',
parent: parent,
};
const parentWithPath: NavigationItem = {
label: 'Parent',
path: '/example',
};
const exampleItemWithPath: NavigationItem = {
label: 'Active Item',
parent: parentWithPath,
};
@@ -0,0 +1,50 @@
/*!
* @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 {ChangeDetectionStrategy, Component, OnInit, inject, signal} from '@angular/core';
import {NavigationState} from '../../services/index';
import {NavigationItem} from '../../interfaces/index';
import {NgFor, NgIf} from '@angular/common';
import {RouterLink} from '@angular/router';
@Component({
selector: 'docs-breadcrumb',
standalone: true,
imports: [NgIf, NgFor, RouterLink],
templateUrl: './breadcrumb.component.html',
styleUrls: ['./breadcrumb.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class Breadcrumb implements OnInit {
private readonly navigationState = inject(NavigationState);
breadcrumbItems = signal<NavigationItem[]>([]);
ngOnInit(): void {
this.setBreadcrumbItemsBasedOnNavigationStructure();
}
private setBreadcrumbItemsBasedOnNavigationStructure(): void {
let breadcrumbs: NavigationItem[] = [];
const traverse = (node: NavigationItem | null) => {
if (!node) {
return;
}
if (node.parent) {
breadcrumbs = [node.parent, ...breadcrumbs];
traverse(node.parent);
}
};
traverse(this.navigationState.activeNavigationItem());
this.breadcrumbItems.set(breadcrumbs);
}
}
@@ -0,0 +1,49 @@
load("//tools:defaults.bzl", "karma_web_test_suite", "ng_module", "ts_library")
load("@io_bazel_rules_sass//:defs.bzl", "sass_binary")
package(default_visibility = ["//visibility:private"])
ng_module(
name = "cookie-popup",
srcs = [
"cookie-popup.component.ts",
],
assets = [
":cookie-popup.component.css",
"cookie-popup.component.html",
],
visibility = [
"//adev/shared-docs/components:__pkg__",
],
deps = [
"//adev/shared-docs/providers",
"//adev/shared-docs/utils",
"//packages/common",
"//packages/core",
],
)
sass_binary(
name = "style",
src = "cookie-popup.component.scss",
)
ts_library(
name = "test_lib",
testonly = True,
srcs = glob(
["*.spec.ts"],
),
deps = [
":cookie-popup",
"//adev/shared-docs/providers",
"//adev/shared-docs/testing",
"//packages/core",
"//packages/core/testing",
],
)
karma_web_test_suite(
name = "test",
deps = [":test_lib"],
)
@@ -0,0 +1,22 @@
@if (!hasAccepted()) {
<div class="docs-cookies-popup docs-invert-mode">
<p>This site uses cookies from Google to deliver its services and to analyze traffic.</p>
<div>
<a href="https://policies.google.com/technologies/cookies" target="_blank" rel="noopener">
<button class="docs-primary-btn" [attr.text]="'Learn more'" aria-label="Learn More">
Learn more
</button>
</a>
<button
type="button"
(click)="accept()"
class="docs-primary-btn"
[attr.text]="'Ok, Got it'"
aria-label="Ok, Got it"
>
Ok, Got it
</button>
</div>
</div>
}
@@ -0,0 +1,40 @@
:host {
position: fixed;
bottom: 0.5rem;
right: 0.5rem;
z-index: var(--z-index-cookie-consent);
opacity: 0;
visibility: hidden;
animation: 1s linear forwards 0.5s fadeIn;
}
.docs-cookies-popup {
padding: 1rem;
background-color: var(--page-background);
border: 1px solid var(--senary-contrast);
border-radius: 0.25rem;
font-size: 0.875rem;
max-width: 265px;
transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease;
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
> div {
display: flex;
gap: 0.5rem;
align-items: center;
width: 100%;
margin-block-start: 1rem;
}
p {
margin-block: 0;
color: var(--primary-contrast);
}
}
@keyframes fadeIn {
100% {
opacity: 100%;
visibility: visible;
}
}
@@ -0,0 +1,82 @@
/*!
* @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 {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 {provideExperimentalZonelessChangeDetection} from '@angular/core';
describe('CookiePopup', () => {
let fixture: ComponentFixture<CookiePopup>;
let mockLocalStorage = new MockLocalStorage();
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CookiePopup],
providers: [
provideExperimentalZonelessChangeDetection(),
{
provide: LOCAL_STORAGE,
useValue: mockLocalStorage,
},
],
});
});
it('should make the popup visible by default', () => {
initComponent(false);
expect(getCookiesPopup()).not.toBeNull();
});
it('should hide the cookies popup if the user has already accepted cookies', () => {
initComponent(true);
expect(getCookiesPopup()).toBeNull();
});
it('should hide the cookies popup', () => {
initComponent(false);
accept();
fixture.detectChanges();
expect(getCookiesPopup()).toBeNull();
});
it('should store the user confirmation', () => {
initComponent(false);
expect(mockLocalStorage.getItem(STORAGE_KEY)).toBeNull();
accept();
expect(mockLocalStorage.getItem(STORAGE_KEY)).toBe('true');
});
// Helpers
function getCookiesPopup() {
return (fixture.nativeElement as HTMLElement).querySelector('.docs-cookies-popup');
}
function accept() {
(fixture.nativeElement as HTMLElement)
.querySelector<HTMLButtonElement>('button[text="Ok, Got it"]')
?.click();
}
function initComponent(cookiesAccepted: boolean) {
mockLocalStorage.setItem(STORAGE_KEY, cookiesAccepted ? 'true' : null);
fixture = TestBed.createComponent(CookiePopup);
fixture.detectChanges();
}
});
@@ -0,0 +1,56 @@
/*!
* @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 {ChangeDetectionStrategy, Component, inject, signal} from '@angular/core';
import {NgIf} from '@angular/common';
import {LOCAL_STORAGE} from '../../providers/index';
import {setCookieConsent} from '../../utils';
/**
* Decelare gtag as part of the window in this file as gtag is expected to already be loaded.
*/
declare const window: Window & typeof globalThis & {gtag?: Function};
export const STORAGE_KEY = 'docs-accepts-cookies';
@Component({
selector: 'docs-cookie-popup',
standalone: true,
imports: [NgIf],
templateUrl: './cookie-popup.component.html',
styleUrls: ['./cookie-popup.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CookiePopup {
private readonly localStorage = inject(LOCAL_STORAGE);
/** Whether the user has accepted the cookie disclaimer. */
hasAccepted = signal<boolean>(false);
constructor() {
// Needs to be in a try/catch, because some browsers will
// throw when using `localStorage` in private mode.
try {
this.hasAccepted.set(this.localStorage?.getItem(STORAGE_KEY) === 'true');
} catch {
this.hasAccepted.set(false);
}
}
/** Accepts the cookie disclaimer. */
protected accept(): void {
try {
this.localStorage?.setItem(STORAGE_KEY, 'true');
} catch {}
this.hasAccepted.set(true);
// Enable Google Analytics consent properties
setCookieConsent('granted');
}
}
@@ -0,0 +1,43 @@
load("//tools:defaults.bzl", "karma_web_test_suite", "ng_module", "ts_library")
package(default_visibility = ["//visibility:private"])
ng_module(
name = "copy-source-code-button",
srcs = [
"copy-source-code-button.component.ts",
],
assets = [
"copy-source-code-button.component.html",
],
visibility = [
"//adev/shared-docs/components:__pkg__",
"//adev/shared-docs/components/viewers:__pkg__",
],
deps = [
"//adev/shared-docs/components/icon",
"//packages/common",
"//packages/core",
"@npm//@angular/cdk",
],
)
ts_library(
name = "test_lib",
testonly = True,
srcs = glob(
["*.spec.ts"],
),
deps = [
":copy-source-code-button",
"//packages/core",
"//packages/core/testing",
"//packages/platform-browser",
"@npm//@angular/cdk",
],
)
karma_web_test_suite(
name = "test",
deps = [":test_lib"],
)
@@ -0,0 +1,18 @@
<i>
<svg
aria-hidden="true"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="docs-copy"
>
<path
d="M5 22C4.45 22 3.97917 21.8042 3.5875 21.4125C3.19583 21.0208 3 20.55 3 20V6H5V20H16V22H5ZM9 18C8.45 18 7.97917 17.8042 7.5875 17.4125C7.19583 17.0208 7 16.55 7 16V4C7 3.45 7.19583 2.97917 7.5875 2.5875C7.97917 2.19583 8.45 2 9 2H18C18.55 2 19.0208 2.19583 19.4125 2.5875C19.8042 2.97917 20 3.45 20 4V16C20 16.55 19.8042 17.0208 19.4125 17.4125C19.0208 17.8042 18.55 18 18 18H9ZM9 16H18V4H9V16Z"
fill="#A39FA9"
/>
</svg>
</i>
<docs-icon class="docs-check">check</docs-icon>
@@ -0,0 +1,127 @@
/*!
* @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 {ComponentFixture, TestBed, fakeAsync, tick} from '@angular/core/testing';
import {
CONFIRMATION_DISPLAY_TIME_MS,
CopySourceCodeButton,
} from './copy-source-code-button.component';
import {
ChangeDetectionStrategy,
Component,
provideExperimentalZonelessChangeDetection,
signal,
} from '@angular/core';
import {By} from '@angular/platform-browser';
import {Clipboard} from '@angular/cdk/clipboard';
const SUCCESSFULLY_COPY_CLASS_NAME = 'docs-copy-source-code-button-success';
const FAILED_COPY_CLASS_NAME = 'docs-copy-source-code-button-failed';
describe('CopySourceCodeButton', () => {
let component: CodeSnippetWrapper;
let fixture: ComponentFixture<CodeSnippetWrapper>;
let copySpy: jasmine.Spy<(text: string) => boolean>;
beforeEach(async () => {
TestBed.configureTestingModule({
imports: [CodeSnippetWrapper],
providers: [provideExperimentalZonelessChangeDetection()],
});
fixture = TestBed.createComponent(CodeSnippetWrapper);
component = fixture.componentInstance;
await fixture.whenStable();
});
beforeEach(() => {
const clipboardService = TestBed.inject(Clipboard);
copySpy = spyOn(clipboardService, 'copy');
});
it('should call clipboard service when clicked on copy source code', async () => {
const expectedCodeToBeCopied = 'npm install -g @angular/cli';
component.code.set(expectedCodeToBeCopied);
await fixture.whenStable();
const button = fixture.debugElement.query(By.directive(CopySourceCodeButton)).nativeElement;
button.click();
expect(copySpy.calls.argsFor(0)[0].trim()).toBe(expectedCodeToBeCopied);
});
it('should not copy lines marked as deleted when code snippet contains diff', async () => {
const codeInHtmlFormat = `
<code>
<div class="line remove"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> *<span class="hljs-attr">ngFor</span>=<span class="hljs-string">"let product of products"</span>&gt;</span></div>
<div class="line add"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> *<span class="hljs-attr">ngFor</span>=<span class="hljs-string">"let product of products()"</span>&gt;</span></div>
</code>
`;
const expectedCodeToBeCopied = `<div *ngFor="let product of products()">`;
component.code.set(codeInHtmlFormat);
await fixture.whenStable();
const button = fixture.debugElement.query(By.directive(CopySourceCodeButton)).nativeElement;
button.click();
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`, fakeAsync(() => {
component.code.set('example');
fixture.detectChanges();
const button = fixture.debugElement.query(By.directive(CopySourceCodeButton)).nativeElement;
button.click();
fixture.detectChanges();
expect(button).toHaveClass(SUCCESSFULLY_COPY_CLASS_NAME);
tick(CONFIRMATION_DISPLAY_TIME_MS);
fixture.detectChanges();
expect(button).not.toHaveClass(SUCCESSFULLY_COPY_CLASS_NAME);
}));
it(`should set ${FAILED_COPY_CLASS_NAME} for ${CONFIRMATION_DISPLAY_TIME_MS} ms when copy failed`, fakeAsync(() => {
component.code.set('example');
copySpy.and.throwError('Fake copy error');
fixture.detectChanges();
const button = fixture.debugElement.query(By.directive(CopySourceCodeButton)).nativeElement;
button.click();
fixture.detectChanges();
expect(button).toHaveClass(FAILED_COPY_CLASS_NAME);
tick(CONFIRMATION_DISPLAY_TIME_MS);
fixture.detectChanges();
expect(button).not.toHaveClass(FAILED_COPY_CLASS_NAME);
}));
});
@Component({
template: `
<pre>
<code [innerHtml]="code()"></code>
</pre>
<button docs-copy-source-code></button>
`,
imports: [CopySourceCodeButton],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
})
class CodeSnippetWrapper {
code = signal('');
}
@@ -0,0 +1,89 @@
/*!
* @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 {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
WritableSignal,
inject,
signal,
} from '@angular/core';
import {CommonModule} from '@angular/common';
import {Clipboard} from '@angular/cdk/clipboard';
import {IconComponent} from '../icon/icon.component';
export const REMOVED_LINE_CLASS_NAME = '.line.remove';
export const CONFIRMATION_DISPLAY_TIME_MS = 2000;
@Component({
selector: 'button[docs-copy-source-code]',
standalone: true,
imports: [CommonModule, IconComponent],
templateUrl: './copy-source-code-button.component.html',
host: {
'type': 'button',
'aria-label': 'Copy example source to clipboard',
'title': 'Copy example source',
'(click)': 'copySourceCode()',
'[class.docs-copy-source-code-button-success]': 'showCopySuccess()',
'[class.docs-copy-source-code-button-failed]': 'showCopyFailure()',
},
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CopySourceCodeButton {
private readonly changeDetector = inject(ChangeDetectorRef);
private readonly clipboard = inject(Clipboard);
private readonly elementRef = inject(ElementRef);
protected readonly showCopySuccess = signal(false);
protected readonly showCopyFailure = signal(false);
copySourceCode(): void {
try {
const codeElement = this.elementRef.nativeElement.parentElement.querySelector(
'code',
) as HTMLElement;
const sourceCode = this.getSourceCode(codeElement);
this.clipboard.copy(sourceCode);
this.showResult(this.showCopySuccess);
} catch {
this.showResult(this.showCopyFailure);
}
}
private getSourceCode(codeElement: HTMLElement): string {
this.showCopySuccess.set(false);
this.showCopyFailure.set(false);
const removedLines: NodeList = codeElement.querySelectorAll(REMOVED_LINE_CLASS_NAME);
if (removedLines.length) {
// Get only those lines which are not marked as removed
const formattedText = Array.from(codeElement.querySelectorAll('.line:not(.remove)'))
.map((line) => (line as HTMLDivElement).innerText)
.join('\n');
return formattedText.trim();
} else {
const text: string = codeElement.innerText || '';
return text.replace(/\n\n\n/g, ``).trim();
}
}
private showResult(messageState: WritableSignal<boolean>) {
messageState.set(true);
setTimeout(() => {
messageState.set(false);
// It's required for code snippets embedded in the ExampleViewer.
this.changeDetector.markForCheck();
}, CONFIRMATION_DISPLAY_TIME_MS);
}
}
@@ -0,0 +1,32 @@
load("//tools:defaults.bzl", "ng_module")
load("@io_bazel_rules_sass//:defs.bzl", "sass_binary")
package(default_visibility = ["//visibility:private"])
ng_module(
name = "icon",
srcs = [
"icon.component.ts",
],
assets = [
":icon.component.css",
"icon.component.html",
],
visibility = [
"//adev/shared-docs/components:__pkg__",
"//adev/shared-docs/components/copy-source-code-button:__pkg__",
"//adev/shared-docs/components/navigation-list:__pkg__",
"//adev/shared-docs/components/table-of-contents:__pkg__",
"//adev/shared-docs/components/text-field:__pkg__",
"//adev/shared-docs/components/viewers:__pkg__",
],
deps = [
"//packages/common",
"//packages/core",
],
)
sass_binary(
name = "style",
src = "icon.component.scss",
)
@@ -0,0 +1 @@
<ng-content></ng-content>
@@ -0,0 +1,3 @@
.docs-icon_high-contrast {
color: var(--primary-contrast);
}
@@ -0,0 +1,55 @@
/*!
* @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 {DOCUMENT} from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
afterNextRender,
computed,
inject,
signal,
} from '@angular/core';
@Component({
selector: 'docs-icon',
standalone: true,
templateUrl: './icon.component.html',
styleUrl: './icon.component.scss',
host: {
'[class]': 'MATERIAL_SYMBOLS_OUTLINED',
'[style.font-size.px]': 'fontSize()',
'aria-hidden': 'true',
'translate': 'no',
},
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class IconComponent {
fontSize = computed(() => {
return IconComponent.isFontLoaded() ? null : 0;
});
protected readonly MATERIAL_SYMBOLS_OUTLINED = 'material-symbols-outlined';
private static isFontLoaded = signal(false);
/** Share the same promise across different instances of the component */
private static whenFontLoad?: Promise<FontFace[]> | undefined;
constructor() {
if (IconComponent.isFontLoaded()) {
return;
}
const document = inject(DOCUMENT);
afterNextRender(async () => {
IconComponent.whenFontLoad ??= document.fonts.load('normal 1px "Material Symbols Outlined"');
await IconComponent.whenFontLoad;
IconComponent.isFontLoaded.set(true);
});
}
}
+17
View File
@@ -0,0 +1,17 @@
/*!
* @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
*/
export * from './viewers/docs-viewer/docs-viewer.component';
export * from './cookie-popup/cookie-popup.component';
export * from './navigation-list/navigation-list.component';
export * from './select/select.component';
export * from './slide-toggle/slide-toggle.component';
export * from './table-of-contents/table-of-contents.component';
export * from './text-field/text-field.component';
export * from './icon/icon.component';
export * from './search-dialog/search-dialog.component';
@@ -0,0 +1,58 @@
load("//tools:defaults.bzl", "karma_web_test_suite", "ng_module", "ts_library")
load("@io_bazel_rules_sass//:defs.bzl", "sass_binary")
package(default_visibility = ["//visibility:private"])
ng_module(
name = "navigation-list",
srcs = [
"navigation-list.component.ts",
],
assets = [
":navigation-list.component.css",
"navigation-list.component.html",
],
visibility = [
"//adev/shared-docs/components:__pkg__",
],
deps = [
"//adev/shared-docs/components/icon",
"//adev/shared-docs/interfaces",
"//adev/shared-docs/pipes",
"//adev/shared-docs/services",
"//packages/common",
"//packages/core",
"//packages/router",
],
)
sass_binary(
name = "style",
src = "navigation-list.component.scss",
deps = [
"//adev/shared-docs/styles",
],
)
ts_library(
name = "test_lib",
testonly = True,
srcs = glob(
["*.spec.ts"],
),
deps = [
":navigation-list",
"//adev/shared-docs/interfaces",
"//adev/shared-docs/services",
"//packages/core",
"//packages/core/testing",
"//packages/platform-browser",
"//packages/router",
"//packages/router/testing",
],
)
karma_web_test_suite(
name = "test",
deps = [":test_lib"],
)
@@ -0,0 +1,76 @@
<ng-template #navigationList let-navigationItems>
<ul
class="docs-navigation-list docs-faceted-list"
[class.docs-navigation-list-dropdown]="isDropdownView"
>
@for (item of navigationItems; track $index) {
<li
class="docs-faceted-list-item"
[class.docs-navigation-link-hidden]="displayItemsToLevel && item.level > displayItemsToLevel"
>
@if (item.path) { @if (item.isExternal) {
<a [href]="item.path" target="_blank">
<span [class.docs-external-link]="item.isExternal">{{ item.label }}</span>
@if (item.children && item.level! > 1 && !item.isExpanded) {
<docs-icon>chevron_right</docs-icon>
}
</a>
} @else {
<a
[routerLink]="'/' + item.path"
[routerLinkActiveOptions]="{
queryParams: 'ignored',
fragment: 'ignored',
matrixParams: 'exact',
paths: 'exact',
exact: false
}"
routerLinkActive="docs-faceted-list-item-active"
(click)="emitClickOnLink()"
>
<span>{{ item.label }}</span>
@if (item.children && !item.isExpanded) {
<docs-icon>chevron_right</docs-icon>
}
</a>
} } @else {
<!-- Nav Section Header -->
@if (item.level !== collapsableLevel && item.level !== expandableLevel) {
<div class="docs-secondary-nav-header">
<span>{{ item.label }}</span>
</div>
}
<!-- Nav Button Expand/Collapse -->
@if ((item.children && item.level === expandableLevel) || item.level === collapsableLevel) {
<button
type="button"
(click)="toggle(item)"
attr.aria-label="{{ item.isExpanded ? 'Collapse' : 'Expand' }} {{ item.label }}"
[attr.aria-expanded]="item.isExpanded"
class="docs-secondary-nav-button"
[class.docs-faceted-list-item-active]="item | isActiveNavigationItem: activeItem()"
[class.docs-expanded-button]="item.children && item.level == collapsableLevel"
[class.docs-not-expanded-button]="item.children && item.level === expandableLevel"
[class.docs-nav-item-has-icon]="
item.children && item.level === expandableLevel && !item.isExpanded
"
>
@if (item.children && item.level === collapsableLevel) {
<docs-icon>arrow_back</docs-icon>
}
<span>{{ item.label }}</span>
</button>
} } @if (item.children?.length > 0) {
<ng-container
*ngTemplateOutlet="navigationList; context: {$implicit: item.children}"
></ng-container>
}
</li>
}
</ul>
</ng-template>
<ng-container
*ngTemplateOutlet="navigationList; context: {$implicit: navigationItems}"
></ng-container>
@@ -0,0 +1,148 @@
@use '../../styles/media-queries' as mq;
:host {
display: flex;
min-width: var(--secondary-nav-width);
list-style: none;
overflow-y: auto;
overflow-x: hidden;
height: 100vh;
padding: 0;
margin: 0;
padding-block: 1.5rem;
font-size: 0.875rem;
box-sizing: border-box;
&::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0);
cursor: pointer;
}
&::-webkit-scrollbar {
width: 6px;
height: 6px;
}
&::-webkit-scrollbar-thumb {
background-color: var(--septenary-contrast);
@include mq.for-tablet-landscape-down {
background-color: var(--quinary-contrast);
}
border-radius: 10px;
transition: background-color 0.3s ease;
}
&::-webkit-scrollbar-thumb:hover {
background-color: var(--quinary-contrast);
}
.docs-nav-secondary & {
padding-block: 2rem;
}
> .docs-faceted-list {
border: 0;
}
.docs-navigation-link-hidden {
display: none;
}
.docs-nav-item-has-icon {
&::after {
// FIXME: for some reason this disappears when transformed
content: 'chevron_right';
font-size: 1.25rem;
font-family: var(--icons);
}
}
}
.docs-secondary-nav-header {
padding-block: 1.25rem;
font-weight: 500;
}
.docs-secondary-nav-button {
width: 15rem;
display: flex;
justify-content: space-between;
align-items: center;
border: none;
padding-block: 1.25rem;
padding-inline-start: 0;
color: var(--primary-contrast);
font-size: 0.875rem;
font-family: var(--inter-font);
line-height: 160%;
letter-spacing: -0.00875rem;
transition: color 0.3s ease, background 0.3s ease;
text-align: left; // forces left alignment of text in button
&.docs-secondary-nav-button-active {
// font gradient
background-image: var(--pink-to-purple-vertical-gradient);
&::before {
opacity: 1;
transform: scaleY(1);
background: var(--pink-to-purple-vertical-gradient);
}
&:hover {
&::before {
opacity: 1;
transform: scaleY(1.1);
}
}
}
}
.docs-expanded-button {
justify-content: start;
gap: 0.5rem;
}
a,
.docs-not-expanded-button {
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 500;
line-height: 1.4rem;
letter-spacing: -0.00875rem;
padding: 0.5rem;
padding-inline-start: 1rem;
text-align: left;
}
// Add padding-bottom to last item in the list
.docs-navigation-list {
width: 100%;
li:last-of-type {
ul:last-of-type {
li:last-of-type {
padding-block-end: 1rem;
}
}
}
&:first-child {
margin-inline-start: 1rem;
}
}
.docs-external-link {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
gap: 0.5rem;
&::after {
content: 'open_in_new';
font-family: var(--icons);
font-size: 1.1rem;
color: var(--quinary-contrast);
transition: color 0.3s ease;
margin-inline-end: 0.4rem;
}
}
@@ -0,0 +1,147 @@
/*!
* @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 {ComponentFixture, TestBed} from '@angular/core/testing';
import {NavigationList} from './navigation-list.component';
import {By} from '@angular/platform-browser';
import {NavigationItem} from '../../interfaces';
import {RouterTestingModule} from '@angular/router/testing';
import {provideExperimentalZonelessChangeDetection, signal} from '@angular/core';
import {NavigationState} from '../../services';
const navigationItems: NavigationItem[] = [
{
label: 'Introduction',
path: 'guide',
level: 1,
},
{
label: 'Getting Started',
level: 1,
children: [
{label: 'What is Angular?', path: 'guide/what-is-angular', level: 2},
{label: 'Setup', path: 'guide/setup', level: 2},
],
},
];
describe('NavigationList', () => {
let component: NavigationList;
let fixture: ComponentFixture<NavigationList>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [NavigationList, RouterTestingModule],
providers: [
{provide: NavigationState, useClass: FakeNavigationListState},
provideExperimentalZonelessChangeDetection(),
],
}).compileComponents();
fixture = TestBed.createComponent(NavigationList);
component = fixture.componentInstance;
});
it('should display provided navigation structure', () => {
component.navigationItems = [...navigationItems];
fixture.detectChanges();
const links = fixture.debugElement.queryAll(By.css('a'));
const nonClickableItem = fixture.debugElement.queryAll(By.css('.docs-secondary-nav-header'));
expect(links.length).toBe(3);
expect(nonClickableItem.length).toBe(1);
});
it('should append `docs-navigation-list-dropdown` when isDropdownView is true', () => {
component.isDropdownView = true;
fixture.detectChanges();
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', () => {
component.isDropdownView = false;
fixture.detectChanges();
const ulElement = fixture.debugElement.query(By.css('ul.docs-navigation-list-dropdown'));
expect(ulElement).toBeFalsy();
});
it('should emit linkClicked when user clicked on link', () => {
const emitClickOnLinkSpy = spyOn(component, 'emitClickOnLink');
component.navigationItems = [...navigationItems];
fixture.detectChanges(true);
const guideLink = fixture.debugElement.query(By.css('a[href="/guide"]'));
guideLink.nativeElement.click();
expect(emitClickOnLinkSpy).toHaveBeenCalledTimes(1);
});
it(`should not call navigationState.toggleItem() when item's level is equal to 1 and is not neither expandable or collapsable level`, () => {
const navigationState = TestBed.inject(NavigationState);
const toggleItemSpy = spyOn(navigationState, 'toggleItem');
const itemToToggle = navigationItems[1];
component.toggle(itemToToggle);
expect(toggleItemSpy).not.toHaveBeenCalled();
});
it(`should call navigationState.toggleItem() when item's level is expandable`, () => {
const navigationState = TestBed.inject(NavigationState);
const toggleItemSpy = spyOn(navigationState, 'toggleItem');
const itemToToggle = navigationItems[1];
component.expandableLevel = 1;
component.toggle(itemToToggle);
expect(toggleItemSpy).toHaveBeenCalledOnceWith(itemToToggle);
});
it(`should call navigationState.toggleItem() when item's level is collapsable`, () => {
const navigationState = TestBed.inject(NavigationState);
const toggleItemSpy = spyOn(navigationState, 'toggleItem');
const itemToToggle = navigationItems[1].children![1];
component.collapsableLevel = 2;
component.toggle(itemToToggle);
expect(toggleItemSpy).toHaveBeenCalledOnceWith(itemToToggle);
});
it('should display items to provided level', () => {
component.navigationItems = [...navigationItems];
component.displayItemsToLevel = 1;
fixture.detectChanges(true);
const visibleItems = fixture.debugElement.queryAll(
By.css('li.docs-faceted-list-item:not(.docs-navigation-link-hidden)'),
);
const hiddenItems = fixture.debugElement.queryAll(
By.css('li.docs-faceted-list-item.docs-navigation-link-hidden'),
);
expect(visibleItems.length).toBe(2);
expect(visibleItems[0].nativeElement.innerText).toBe(navigationItems[0].label);
expect(visibleItems[1].nativeElement.innerText).toBe(navigationItems[1].label);
expect(hiddenItems.length).toBe(2);
expect(hiddenItems[0].nativeElement.innerText).toBe(navigationItems[1].children![0].label);
expect(hiddenItems[1].nativeElement.innerText).toBe(navigationItems[1].children![1].label);
});
});
class FakeNavigationListState {
isOpened = signal(true);
activeNavigationItem = signal(navigationItems.at(1));
toggleItem(item: NavigationItem) {}
}
@@ -0,0 +1,60 @@
/*!
* @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 {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
Output,
inject,
} from '@angular/core';
import {NavigationItem} from '../../interfaces/index';
import {NavigationState} from '../../services/index';
import {RouterLink, RouterLinkActive} from '@angular/router';
import {CommonModule} from '@angular/common';
import {IconComponent} from '../icon/icon.component';
import {IsActiveNavigationItem} from '../../pipes/is-active-navigation-item.pipe';
@Component({
selector: 'docs-navigation-list',
standalone: true,
imports: [CommonModule, RouterLink, RouterLinkActive, IconComponent, IsActiveNavigationItem],
templateUrl: './navigation-list.component.html',
styleUrls: ['./navigation-list.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class NavigationList {
@Input({required: true}) navigationItems: NavigationItem[] = [];
@Input() displayItemsToLevel: number = 2;
@Input() collapsableLevel: number | undefined = undefined;
@Input() expandableLevel: number = 2;
@Input() isDropdownView = false;
@Output() linkClicked = new EventEmitter<void>();
private readonly navigationState = inject(NavigationState);
expandedItems = this.navigationState.expandedItems;
activeItem = this.navigationState.activeNavigationItem;
toggle(item: NavigationItem): void {
if (
item.level === 1 &&
item.level !== this.expandableLevel &&
item.level !== this.collapsableLevel
) {
return;
}
this.navigationState.toggleItem(item);
}
emitClickOnLink(): void {
this.linkClicked.emit();
}
}
@@ -0,0 +1,60 @@
load("//tools:defaults.bzl", "karma_web_test_suite", "ng_module", "ts_library")
load("@io_bazel_rules_sass//:defs.bzl", "sass_binary")
package(default_visibility = ["//visibility:private"])
ng_module(
name = "search-dialog",
srcs = [
"search-dialog.component.ts",
],
assets = [
":search-dialog.component.css",
"search-dialog.component.html",
],
visibility = [
"//adev/shared-docs/components:__pkg__",
],
deps = [
"//adev/shared-docs/components/algolia-icon",
"//adev/shared-docs/components/text-field",
"//adev/shared-docs/directives",
"//adev/shared-docs/interfaces",
"//adev/shared-docs/pipes",
"//adev/shared-docs/services",
"//packages/common",
"//packages/core",
"//packages/forms",
"//packages/router",
],
)
sass_binary(
name = "style",
src = "search-dialog.component.scss",
)
ts_library(
name = "test_lib",
testonly = True,
srcs = glob(
["*.spec.ts"],
),
deps = [
":search-dialog",
"//adev/shared-docs/components/algolia-icon",
"//adev/shared-docs/providers",
"//adev/shared-docs/services",
"//adev/shared-docs/testing",
"//packages/core",
"//packages/core/testing",
"//packages/platform-browser",
"//packages/router",
"//packages/router/testing",
],
)
karma_web_test_suite(
name = "test",
deps = [":test_lib"],
)
@@ -0,0 +1,89 @@
<dialog #searchDialog>
<div class="docs-search-container" (docsClickOutside)="closeSearchDialog()">
<docs-text-field
[autofocus]="true"
[hideIcon]="true"
[ngModel]="searchQuery()"
(ngModelChange)="updateSearchQuery($event)"
class="docs-search-input"
placeholder="Search docs"
></docs-text-field>
@if (searchResults() && searchResults()!.length > 0) {
<ul class="docs-search-results docs-mini-scroll-track">
@for (result of searchResults(); track result.objectID) {
<li docsSearchItem [item]="result">
<a
[routerLink]="'/' + result.url | relativeLink: 'pathname'"
[fragment]="result.url | relativeLink: 'hash'"
>
<div>
<div class="docs-result-icon-and-type">
<!-- Icon -->
<span class="docs-search-result-icon" aria-hidden="true">
<i role="presentation" class="material-symbols-outlined docs-icon-small">
{{ result.hierarchy.lvl0 === 'Tutorials' ? 'code' : 'description'}}
</i>
</span>
<!-- Results type -->
<span class="docs-search-results__type">
@let snippet = result._snippetResult.hierarchy?.lvl1?.value ?? '';
<ng-container
[ngTemplateOutlet]="highlightSnippet"
[ngTemplateOutletContext]="{snippet}"
></ng-container>
</span>
</div>
@let content = result._snippetResult.content;
@let hierarchy = result._snippetResult.hierarchy;
@if (content || hierarchy?.lvl2 || hierarchy?.lvl3 || hierarchy?.lvl4) {
<span class="docs-search-results__type docs-search-results__lvl2">
@let snippet = getBestSnippetForMatch(result);
<ng-container
[ngTemplateOutlet]="highlightSnippet"
[ngTemplateOutletContext]="{snippet}"
></ng-container>
</span>
}
</div>
<!-- Page title -->
<span class="docs-result-page-title">{{ result.hierarchy?.lvl0 }}</span>
</a>
</li>
}
</ul>
} @else {
<div class="docs-search-results docs-mini-scroll-track">
@if (searchResults() === undefined) {
<div class="docs-search-results__start-typing">
<span>Start typing to see results</span>
</div>
} @else if (searchResults()?.length === 0) {
<div class="docs-search-results__no-results">
<span>No results found</span>
</div>
}
</div>
}
<div class="docs-algolia">
<span>Search by</span>
<a href="https://www.algolia.com/developers/" target="_blank" rel="noopener">
<docs-algolia-icon />
</a>
</div>
</div>
</dialog>
<ng-template #highlightSnippet let-snippet="snippet">
@let parts = splitMarkedText(snippet);
@for (part of parts; track $index) {
@if (part.highlight) {
<mark>{{part.text}}</mark>
} @else {
<span>{{part.text}}</span>
}
}
</ng-template>
@@ -0,0 +1,155 @@
dialog {
background-color: transparent;
border: none;
padding-block-end: 3rem;
&::backdrop {
backdrop-filter: blur(5px);
}
}
.docs-search-container {
width: 750px;
max-width: 90vw;
background-color: var(--page-background);
border: 1px solid var(--senary-contrast);
border-radius: 0.25rem;
box-sizing: border-box;
.docs-search-input {
border-radius: 0.25rem 0.25rem 0 0;
border: none;
border-block-end: 1px solid var(--senary-contrast);
height: 2.6875rem; // 43px;
padding-inline-start: 1rem;
position: relative;
&::after {
content: 'Esc';
position: absolute;
right: 1rem;
color: var(--gray-400);
font-size: 0.875rem;
}
}
ul {
max-height: 50vh;
overflow-y: auto;
list-style-type: none;
padding-inline: 0;
padding-block-start: 1rem;
margin: 0;
border-block-end: 1px solid var(--senary-contrast);
li {
border-inline-start: 2px solid var(--senary-contrast);
margin-inline-start: 1rem;
padding-inline-end: 1rem;
padding-block: 0.25rem;
mark {
background: #e62600;
background: var(--red-to-orange-horizontal-gradient);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
}
a {
color: var(--secondary-contrast);
display: flex;
justify-content: space-between;
gap: 0.5rem;
.docs-search-result-icon {
i {
display: flex;
align-items: center;
font-size: 1.2rem;
}
}
}
&.active {
background-color: var(--septenary-contrast); // stylelint-disable-line
}
&:hover,
&.active {
background-color: var(--octonary-contrast); // stylelint-disable-line
border-inline-start: 2px solid var(--primary-contrast);
a {
span:not(.docs-result-page-title),
.docs-search-results__type {
color: var(--primary-contrast);
i {
color: var(--primary-contrast);
}
}
}
}
}
.docs-search-result-icon,
.docs-search-results__type,
.docs-result-page-title {
color: var(--quaternary-contrast);
display: inline-block;
font-size: 0.875rem;
transition: color 0.3s ease;
padding: 0.75rem;
padding-inline-end: 0;
}
.docs-search-results__lvl2 {
display: inline-block;
margin-inline-start: 2rem;
padding-block-start: 0;
}
.docs-search-results__lvl3 {
margin-inline-start: 2rem;
padding-block-start: 0;
}
}
.docs-result-page-title {
font-size: 0.875rem;
font-weight: 400;
}
}
.docs-search-results__start-typing,
.docs-search-results__no-results {
padding: 0.75rem;
color: var(--gray-400);
}
.docs-result-icon-and-type {
display: flex;
.docs-search-results__type {
padding-inline-start: 0;
}
}
.docs-algolia {
display: flex;
align-items: center;
justify-content: end;
color: var(--gray-400);
padding: 1rem;
font-size: 0.75rem;
font-weight: 500;
gap: 0.25rem;
background-color: var(--page-background);
border-radius: 0 0 0.25rem 0.25rem;
docs-algolia-icon {
display: inline-flex;
margin-block-start: 0.12rem;
margin-inline-start: 0.15rem;
width: 4rem;
}
}
@@ -0,0 +1,152 @@
/*!
* @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 {ComponentFixture, TestBed} from '@angular/core/testing';
import {SearchDialog} from './search-dialog.component';
import {WINDOW} from '../../providers';
import {Search} from '../../services';
import {FakeEventTarget} from '../../testing/index';
import {By} from '@angular/platform-browser';
import {AlgoliaIcon} from '../algolia-icon/algolia-icon.component';
import {RouterTestingModule} from '@angular/router/testing';
import {Router} from '@angular/router';
import {provideExperimentalZonelessChangeDetection} from '@angular/core';
describe('SearchDialog', () => {
let fixture: ComponentFixture<SearchDialog>;
const fakeSearch = {
searchQuery: jasmine.createSpy(),
searchResults: jasmine.createSpy(),
};
const fakeWindow = new FakeEventTarget();
beforeEach(async () => {
fakeSearch.searchResults.and.returnValue([]);
fakeSearch.searchQuery.and.returnValue('');
await TestBed.configureTestingModule({
imports: [SearchDialog, RouterTestingModule],
providers: [
provideExperimentalZonelessChangeDetection(),
{
provide: Search,
useValue: fakeSearch,
},
{
provide: WINDOW,
useValue: fakeWindow,
},
],
}).compileComponents();
fixture = TestBed.createComponent(SearchDialog);
fixture.detectChanges();
});
it('should navigate to active item when user pressed Enter', () => {
const router = TestBed.inject(Router);
const navigateByUrlSpy = spyOn(router, 'navigateByUrl');
fakeSearch.searchResults.and.returnValue(fakeSearchResults);
fixture.detectChanges();
fakeWindow.dispatchEvent(
new KeyboardEvent('keydown', {
code: 'Enter',
key: 'Enter',
charCode: 13,
keyCode: 13,
view: window,
bubbles: true,
}),
);
expect(navigateByUrlSpy).toHaveBeenCalledOnceWith('fakeUrl1#h1');
});
it('should always display algolia logo', () => {
const algoliaIcon = fixture.debugElement.query(By.directive(AlgoliaIcon));
expect(algoliaIcon).toBeTruthy();
});
it('should display `No results found` message when there are no results for provided query', () => {
fakeSearch.searchResults.and.returnValue([]);
fixture.detectChanges();
const noResultsContainer = fixture.debugElement.query(
By.css('.docs-search-results__no-results'),
);
expect(noResultsContainer).toBeTruthy();
});
it('should display `Start typing to see results` message when there are no provided query', () => {
fakeSearch.searchResults.and.returnValue(undefined);
fixture.detectChanges();
const startTypingContainer = fixture.debugElement.query(
By.css('.docs-search-results__start-typing'),
);
expect(startTypingContainer).toBeTruthy();
});
it('should display list of the search results when results exist', () => {
fakeSearch.searchResults.and.returnValue(fakeSearchResults);
fixture.detectChanges();
const resultListContainer = fixture.debugElement.query(By.css('ul.docs-search-results'));
const resultItems = fixture.debugElement.queryAll(By.css('ul.docs-search-results li a'));
expect(resultListContainer).toBeTruthy();
expect(resultItems.length).toBe(2);
expect(resultItems[0].nativeElement.href).toBe(`${window.origin}/fakeUrl1#h1`);
expect(resultItems[1].nativeElement.href).toBe(`${window.origin}/fakeUrl2#h1`);
});
it('should close search dialog when user clicked outside `.docs-search-container`', () => {
const dialogContainer = fixture.debugElement.query(By.css('dialog'));
const closeSearchDialogSpy = spyOn(fixture.componentInstance, 'closeSearchDialog');
dialogContainer.nativeElement.click();
expect(closeSearchDialogSpy).toHaveBeenCalled();
});
});
const fakeSearchResults = [
{
'url': 'https://angular.dev/fakeUrl1#h1',
'hierarchy': {
'lvl0': 'FakeLvl0',
'lvl1': 'FakeLvl1',
'lvl2': 'FakeLvl2',
'lvl3': null,
'lvl4': null,
'lvl5': null,
'lvl6': null,
},
'objectID': 'fakeObjectId1',
},
{
'url': 'https://angular.dev/fakeUrl2#h1',
'hierarchy': {
'lvl0': 'FakeLvl0',
'lvl1': 'FakeLvl1',
'lvl2': 'FakeLvl2',
'lvl3': null,
'lvl4': null,
'lvl5': null,
'lvl6': null,
},
'objectID': 'fakeObjectId2',
},
];
@@ -0,0 +1,173 @@
/*!
* @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 {
ChangeDetectionStrategy,
Component,
ElementRef,
Injector,
OnDestroy,
Signal,
afterNextRender,
effect,
inject,
output,
viewChild,
viewChildren,
} from '@angular/core';
import {NgTemplateOutlet} from '@angular/common';
import {WINDOW} from '../../providers/index';
import {ClickOutside} from '../../directives/index';
import {Search} from '../../services/index';
import {TextField} from '../text-field/text-field.component';
import {FormsModule} from '@angular/forms';
import {ActiveDescendantKeyManager} from '@angular/cdk/a11y';
import {SearchItem} from '../../directives/search-item/search-item.directive';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {Router, RouterLink} from '@angular/router';
import {filter, fromEvent} from 'rxjs';
import {AlgoliaIcon} from '../algolia-icon/algolia-icon.component';
import {RelativeLink} from '../../pipes/relative-link.pipe';
import {SearchResult, SnippetResult} from '../../interfaces';
@Component({
selector: 'docs-search-dialog',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
ClickOutside,
TextField,
FormsModule,
SearchItem,
AlgoliaIcon,
RelativeLink,
RouterLink,
NgTemplateOutlet,
],
templateUrl: './search-dialog.component.html',
styleUrls: ['./search-dialog.component.scss'],
})
export class SearchDialog implements OnDestroy {
onClose = output();
dialog = viewChild.required<ElementRef<HTMLDialogElement>>('searchDialog');
items = viewChildren(SearchItem);
private readonly search = inject(Search);
private readonly relativeLink = new RelativeLink();
private readonly router = inject(Router);
private readonly window = inject(WINDOW);
private readonly injector = inject(Injector);
private readonly keyManager = new ActiveDescendantKeyManager(
this.items,
this.injector,
).withWrap();
searchQuery = this.search.searchQuery;
searchResults = this.search.searchResults;
constructor() {
effect(() => {
this.items();
afterNextRender(
{
write: () => this.keyManager.setFirstItemActive(),
},
{injector: this.injector},
);
});
this.keyManager.change.pipe(takeUntilDestroyed()).subscribe(() => {
this.keyManager.activeItem?.scrollIntoView();
});
afterNextRender({
write: () => {
if (!this.dialog().nativeElement.open) {
this.dialog().nativeElement.showModal?.();
}
},
});
fromEvent<KeyboardEvent>(this.window, 'keydown')
.pipe(takeUntilDestroyed())
.subscribe((event) => {
// When user presses Enter we can navigate to currently selected item in the search result list.
if (event.key === 'Enter') {
this.navigateToTheActiveItem();
} else {
this.keyManager.onKeydown(event);
}
});
}
splitMarkedText(snippet: string): Array<{highlight: boolean; text: string}> {
const parts: Array<{highlight: boolean; text: string}> = [];
while (snippet.indexOf('<ɵ>') !== -1) {
const beforeMatch = snippet.substring(0, snippet.indexOf('<ɵ>'));
const match = snippet.substring(snippet.indexOf('<ɵ>') + 3, snippet.indexOf('</ɵ>'));
parts.push({highlight: false, text: beforeMatch});
parts.push({highlight: true, text: match});
snippet = snippet.substring(snippet.indexOf('</ɵ>') + 4);
}
parts.push({highlight: false, text: snippet});
return parts;
}
getBestSnippetForMatch(result: SearchResult): string {
// if there is content, return it
if (result._snippetResult.content !== undefined) {
return result._snippetResult.content.value;
}
const hierarchy = result._snippetResult.hierarchy;
if (hierarchy === undefined) {
return '';
}
function matched(snippet: SnippetResult | undefined) {
return snippet?.matchLevel !== undefined && snippet.matchLevel !== 'none';
}
// return the most specific subheader match
if (matched(hierarchy.lvl4)) {
return hierarchy.lvl4!.value;
}
if (matched(hierarchy.lvl3)) {
return hierarchy.lvl3!.value;
}
if (matched(hierarchy.lvl2)) {
return hierarchy.lvl2!.value;
}
// if no subheader matched the query, fall back to just returning the most specific one
return hierarchy.lvl3?.value ?? hierarchy.lvl2?.value ?? '';
}
ngOnDestroy(): void {
this.keyManager.destroy();
}
closeSearchDialog() {
this.dialog().nativeElement.close();
this.onClose.emit();
}
updateSearchQuery(query: string) {
this.search.updateSearchQuery(query);
}
private navigateToTheActiveItem(): void {
const activeItemLink: string | undefined = this.keyManager.activeItem?.item?.url;
if (!activeItemLink) {
return;
}
this.router.navigateByUrl(this.relativeLink.transform(activeItemLink));
this.onClose.emit();
}
}
@@ -0,0 +1,46 @@
load("//tools:defaults.bzl", "karma_web_test_suite", "ng_module", "ts_library")
load("@io_bazel_rules_sass//:defs.bzl", "sass_binary")
package(default_visibility = ["//visibility:private"])
ng_module(
name = "select",
srcs = [
"select.component.ts",
],
assets = [
":select.component.css",
"select.component.html",
],
visibility = [
"//adev/shared-docs/components:__pkg__",
],
deps = [
"//packages/common",
"//packages/core",
"//packages/forms",
],
)
sass_binary(
name = "style",
src = "select.component.scss",
)
ts_library(
name = "test_lib",
testonly = True,
srcs = glob(
["*.spec.ts"],
),
deps = [
":select",
"//packages/core",
"//packages/core/testing",
],
)
karma_web_test_suite(
name = "test",
deps = [":test_lib"],
)
@@ -0,0 +1,5 @@
<select [attr.id]="id" [attr.name]="name" [ngModel]="selectedOption()" (ngModelChange)="setOption($event)">
@for (item of options; track item) {
<option [value]="item.value">{{ item.label }}</option>
}
</select>
@@ -0,0 +1,31 @@
/*!
* @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 {ComponentFixture, TestBed} from '@angular/core/testing';
import {Select} from './select.component';
import {provideExperimentalZonelessChangeDetection} from '@angular/core';
describe('Select', () => {
let component: Select;
let fixture: ComponentFixture<Select>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [Select],
providers: [provideExperimentalZonelessChangeDetection()],
});
fixture = TestBed.createComponent(Select);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,79 @@
/*!
* @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 {ControlValueAccessor, NG_VALUE_ACCESSOR, FormsModule} from '@angular/forms';
import {ChangeDetectionStrategy, Component, Input, forwardRef, signal} from '@angular/core';
import {CommonModule} from '@angular/common';
type SelectOptionValue = string | number | boolean;
export interface SelectOption {
label: string;
value: SelectOptionValue;
}
@Component({
selector: 'docs-select',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, 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 {
@Input({required: true, alias: 'selectId'}) id!: string;
@Input({required: true}) name!: string;
@Input({required: true}) options!: SelectOption[];
@Input() disabled = 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 = isDisabled;
}
setOption($event: SelectOptionValue): void {
if (this.disabled) {
return;
}
this.selectedOption.set($event);
this.onChange($event);
this.onTouched();
}
}
@@ -0,0 +1,46 @@
load("//tools:defaults.bzl", "karma_web_test_suite", "ng_module", "ts_library")
load("@io_bazel_rules_sass//:defs.bzl", "sass_binary")
package(default_visibility = ["//visibility:private"])
ng_module(
name = "slide-toggle",
srcs = [
"slide-toggle.component.ts",
],
assets = [
":slide-toggle.component.css",
"slide-toggle.component.html",
],
visibility = [
"//adev/shared-docs/components:__pkg__",
],
deps = [
"//packages/common",
"//packages/core",
"//packages/forms",
],
)
sass_binary(
name = "style",
src = "slide-toggle.component.scss",
)
ts_library(
name = "test_lib",
testonly = True,
srcs = glob(
["*.spec.ts"],
),
deps = [
":slide-toggle",
"//packages/core",
"//packages/core/testing",
],
)
karma_web_test_suite(
name = "test",
deps = [":test_lib"],
)
@@ -0,0 +1,14 @@
<label [attr.for]="buttonId">
<span class="docs-label">{{ label }}</span>
<div class="docs-toggle">
<input
type="checkbox"
[id]="buttonId"
role="switch"
(click)="toggle()"
[class.docs-toggle-active]="checked()"
[checked]="checked()"
/>
<span class="docs-slider"></span>
</div>
</label>
@@ -0,0 +1,78 @@
:host,
label {
display: inline-flex;
gap: 0.5em;
align-items: center;
}
.docs-label {
font-size: 0.875rem;
font-style: normal;
font-weight: 500;
line-height: 160%; // 1.4rem
letter-spacing: -0.00875rem;
color: var(--quaternary-contrast);
}
.docs-toggle {
position: relative;
display: inline-block;
width: 3rem;
height: 1.5rem;
border: 1px solid var(--senary-contrast);
border-radius: 34px;
input {
opacity: 0;
width: 0;
height: 0;
}
}
.docs-slider {
position: absolute;
cursor: pointer;
border-radius: 34px;
inset: 0;
background-color: var(--septenary-contrast);
transition: background-color 0.3s ease, border-color 0.3s ease;
// background
&::before {
content: '';
position: absolute;
inset: 0;
border-radius: 34px;
background: var(--pink-to-purple-horizontal-gradient);
opacity: 0;
transition: opacity 0.3s ease;
}
// toggle knob
&::after {
position: absolute;
content: '';
height: 1.25rem;
width: 1.25rem;
left: 0.125rem;
bottom: 0.125rem;
background-color: var(--page-background);
transition: transform 0.3s ease, background-color 0.3s ease;
border-radius: 50%;
}
}
input {
&:checked + .docs-slider {
// background
&::before {
opacity: 1;
}
// toggle knob
&::after {
transform: translateX(1.5rem);
}
}
}
@@ -0,0 +1,60 @@
/*!
* @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 {ComponentFixture, TestBed} from '@angular/core/testing';
import {SlideToggle} from './slide-toggle.component';
import {provideExperimentalZonelessChangeDetection} from '@angular/core';
describe('SlideToggle', () => {
let component: SlideToggle;
let fixture: ComponentFixture<SlideToggle>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [SlideToggle],
providers: [provideExperimentalZonelessChangeDetection()],
});
fixture = TestBed.createComponent(SlideToggle);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should toggle the value when clicked', () => {
expect(component['checked']()).toBeFalse();
const buttonElement = fixture.nativeElement.querySelector('input');
buttonElement.click();
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);
fixture.detectChanges();
const buttonElement: HTMLButtonElement = fixture.nativeElement.querySelector('input');
expect(buttonElement.classList.contains('docs-toggle-active')).toBeTrue();
component.writeValue(false);
fixture.detectChanges();
expect(buttonElement.classList.contains('docs-toggle-active')).toBeFalse();
});
});
@@ -0,0 +1,69 @@
/*!
* @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 {ChangeDetectionStrategy, Component, Input, forwardRef, signal} from '@angular/core';
import {CommonModule} from '@angular/common';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
@Component({
selector: 'docs-slide-toggle',
standalone: true,
imports: [CommonModule],
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 {
@Input({required: true}) buttonId!: string;
@Input({required: true}) label!: string;
@Input() disabled = 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 = isDisabled;
}
// Toggles the checked state of the slide-toggle.
toggle(): void {
if (this.disabled) {
return;
}
this.checked.update((checked) => !checked);
this.onChange(this.checked());
this.onTouched();
}
}
@@ -0,0 +1,55 @@
load("//tools:defaults.bzl", "karma_web_test_suite", "ng_module", "ts_library")
load("@io_bazel_rules_sass//:defs.bzl", "sass_binary")
package(default_visibility = ["//visibility:private"])
ng_module(
name = "table-of-contents",
srcs = [
"table-of-contents.component.ts",
],
assets = [
":table-of-contents.component.css",
"table-of-contents.component.html",
],
visibility = [
"//adev/shared-docs/components:__pkg__",
"//adev/shared-docs/components/viewers:__pkg__",
],
deps = [
"//adev/shared-docs/components/icon",
"//adev/shared-docs/interfaces",
"//adev/shared-docs/services",
"//packages/common",
"//packages/core",
"//packages/router",
],
)
sass_binary(
name = "style",
src = "table-of-contents.component.scss",
)
ts_library(
name = "test_lib",
testonly = True,
srcs = glob(
["*.spec.ts"],
),
deps = [
":table-of-contents",
"//adev/shared-docs/interfaces",
"//adev/shared-docs/providers",
"//adev/shared-docs/services",
"//packages/core",
"//packages/core/testing",
"//packages/router",
"//packages/router/testing",
],
)
karma_web_test_suite(
name = "test",
deps = [":test_lib"],
)
@@ -0,0 +1,31 @@
<aside>
<nav>
<header>
<h2 class="docs-title">On this page</h2>
</header>
<ul class="docs-faceted-list">
<!-- TODO: Hide li elements with class docs-toc-item-h3 for laptop, table and phone screen resolutions -->
@for (item of tableOfContentItems(); track item.id) {
<li
class="docs-faceted-list-item"
[class.docs-toc-item-h2]="item.level === TableOfContentsLevel.H2"
[class.docs-toc-item-h3]="item.level === TableOfContentsLevel.H3"
>
<a
routerLink="."
[fragment]="item.id"
[class.docs-faceted-list-item-active]="item.id === activeItemId()"
>
{{ item.title }}
</a>
</li>
}
</ul>
</nav>
@if (shouldDisplayScrollToTop()) {
<button type="button" (click)="scrollToTop()">
<docs-icon role="presentation">arrow_upward_alt</docs-icon>
Back to the top
</button>
}
</aside>
@@ -0,0 +1,96 @@
:host {
display: flex;
flex-direction: column;
position: fixed;
right: 16px;
top: 0;
height: fit-content;
width: 14rem;
padding-inline: 1rem;
max-height: 100vh;
overflow-y: scroll;
aside {
margin-bottom: 2rem;
}
& :has(ul li:only-child) {
// Hide the entire TOC is there's only one item
display: none;
}
@media only screen and (max-width: 1430px) {
position: relative;
right: 0;
max-height: min-content;
width: 100%;
}
.docs-title {
font-size: 1.25rem;
margin-block-start: var(--layout-padding);
}
&::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0);
cursor: pointer;
}
&::-webkit-scrollbar {
width: 6px;
height: 6px;
}
&::-webkit-scrollbar-thumb {
background-color: var(--septenary-contrast);
border-radius: 10px;
transition: background-color 0.3s ease;
}
&::-webkit-scrollbar-thumb:hover {
background-color: var(--quinary-contrast);
}
.docs-faceted-list-item {
font-size: 0.875rem;
a {
display: block; // to prevent overflow from the li parent
padding: 0.5rem 0.5rem 0.5rem 1rem;
font-weight: 500;
}
&.docs-toc-item-h3 a {
padding-inline-start: 2rem;
}
}
}
button {
background: transparent;
border: none;
font-size: 0.875rem;
font-family: var(--inter-font);
display: flex;
align-items: center;
margin: 0.5rem 0;
color: var(--tertiary-contrast);
transition: color 0.3s ease;
cursor: pointer;
docs-icon {
margin-inline-end: 0.35rem;
opacity: 0.6;
transition: opacity 0.3s ease;
}
&:hover {
docs-icon {
opacity: 1;
}
}
@media only screen and (max-width: 1430px) {
display: none;
}
}
@@ -0,0 +1,119 @@
/*!
* @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 {ComponentFixture, TestBed} from '@angular/core/testing';
import {TableOfContents} from './table-of-contents.component';
import {RouterTestingModule} from '@angular/router/testing';
import {TableOfContentsItem, TableOfContentsLevel} from '../../interfaces/index';
import {TableOfContentsScrollSpy, TableOfContentsLoader} from '../../services/index';
import {WINDOW} from '../../providers/index';
import {provideExperimentalZonelessChangeDetection, signal} from '@angular/core';
describe('TableOfContents', () => {
let component: TableOfContents;
let fixture: ComponentFixture<TableOfContents>;
let scrollSpy: jasmine.SpyObj<TableOfContentsScrollSpy>;
const items: TableOfContentsItem[] = [
{
title: 'Heading 2',
top: 0,
id: 'item-heading-2',
level: TableOfContentsLevel.H2,
},
{
title: 'First Heading 3',
top: 100,
id: 'first-item-heading-3',
level: TableOfContentsLevel.H3,
},
{
title: 'Second Heading 3',
top: 200,
id: 'second-item-heading-3',
level: TableOfContentsLevel.H3,
},
];
const fakeWindow = {
addEventListener: () => {},
removeEventListener: () => {},
};
beforeEach(async () => {
scrollSpy = jasmine.createSpyObj<TableOfContentsScrollSpy>('TableOfContentsScrollSpy', [
'startListeningToScroll',
'activeItemId',
'scrollbarThumbOnTop',
]);
scrollSpy.startListeningToScroll.and.returnValue();
scrollSpy.activeItemId.and.returnValue(items[0].id);
scrollSpy.scrollbarThumbOnTop.and.returnValue(false);
await TestBed.configureTestingModule({
imports: [TableOfContents, RouterTestingModule],
providers: [
provideExperimentalZonelessChangeDetection(),
{
provide: WINDOW,
useValue: fakeWindow,
},
],
}).compileComponents();
TestBed.overrideProvider(TableOfContentsScrollSpy, {
useValue: scrollSpy,
});
const tableOfContentsLoaderSpy = TestBed.inject(TableOfContentsLoader);
spyOn(tableOfContentsLoaderSpy, 'buildTableOfContent').and.returnValue();
tableOfContentsLoaderSpy.tableOfContentItems.set(items);
fixture = TestBed.createComponent(TableOfContents);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
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();
expect(spy).toHaveBeenCalledOnceWith();
});
it('should render items when tableOfContentItems has value', () => {
fixture.detectChanges();
const renderedItems = fixture.nativeElement.querySelectorAll('li');
expect(renderedItems.length).toBe(3);
expect(component.tableOfContentItems().length).toBe(3);
});
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');
expect(h2Items.length).toBe(1);
expect(h3Items.length).toBe(2);
});
it('should append active class when item is active', () => {
fixture.detectChanges();
const activeItem = fixture.nativeElement.querySelector('.docs-faceted-list-item-active');
expect(activeItem).toBeTruthy();
});
});
@@ -0,0 +1,45 @@
/*!
* @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 {NgFor, NgIf} from '@angular/common';
import {ChangeDetectionStrategy, Component, Input, computed, inject} from '@angular/core';
import {RouterLink} from '@angular/router';
import {TableOfContentsLevel} from '../../interfaces/index';
import {TableOfContentsLoader} from '../../services/table-of-contents-loader.service';
import {TableOfContentsScrollSpy} from '../../services/table-of-contents-scroll-spy.service';
import {IconComponent} from '../icon/icon.component';
@Component({
selector: 'docs-table-of-contents',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './table-of-contents.component.html',
styleUrls: ['./table-of-contents.component.scss'],
imports: [NgIf, NgFor, RouterLink, IconComponent],
})
export class TableOfContents {
// Element that contains the content from which the Table of Contents is built
@Input({required: true}) contentSourceElement!: HTMLElement;
private readonly scrollSpy = inject(TableOfContentsScrollSpy);
private readonly tableOfContentsLoader = inject(TableOfContentsLoader);
tableOfContentItems = this.tableOfContentsLoader.tableOfContentItems;
activeItemId = this.scrollSpy.activeItemId;
shouldDisplayScrollToTop = computed(() => !this.scrollSpy.scrollbarThumbOnTop());
TableOfContentsLevel = TableOfContentsLevel;
ngAfterViewInit() {
this.tableOfContentsLoader.buildTableOfContent(this.contentSourceElement);
this.scrollSpy.startListeningToScroll(this.contentSourceElement);
}
scrollToTop(): void {
this.scrollSpy.scrollToTop();
}
}
@@ -0,0 +1,48 @@
load("//tools:defaults.bzl", "karma_web_test_suite", "ng_module", "ts_library")
load("@io_bazel_rules_sass//:defs.bzl", "sass_binary")
package(default_visibility = ["//visibility:private"])
ng_module(
name = "text-field",
srcs = [
"text-field.component.ts",
],
assets = [
":text-field.component.css",
"text-field.component.html",
],
visibility = [
"//adev/shared-docs/components:__pkg__",
"//adev/shared-docs/components/search-dialog:__pkg__",
],
deps = [
"//adev/shared-docs/components/icon",
"//packages/common",
"//packages/core",
"//packages/forms",
],
)
sass_binary(
name = "style",
src = "text-field.component.scss",
)
ts_library(
name = "test_lib",
testonly = True,
srcs = glob(
["*.spec.ts"],
),
deps = [
":text-field",
"//packages/core",
"//packages/core/testing",
],
)
karma_web_test_suite(
name = "test",
deps = [":test_lib"],
)
@@ -0,0 +1,12 @@
@if (!hideIcon) {
<docs-icon class="docs-icon_high-contrast">search</docs-icon>
}
<input
#inputRef
type="text"
[attr.placeholder]="placeholder"
[attr.name]="name"
[ngModel]="value()"
(ngModelChange)="setValue($event)"
class="docs-text-field"
/>
@@ -0,0 +1,9 @@
// search field
.docs-text-field {
font-size: 1.125rem;
}
// filter field on api reference list
docs-icon + .docs-text-field {
font-size: 1rem;
}
@@ -0,0 +1,31 @@
/*!
* @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 {ComponentFixture, TestBed} from '@angular/core/testing';
import {TextField} from './text-field.component';
import {provideExperimentalZonelessChangeDetection} from '@angular/core';
describe('TextField', () => {
let component: TextField;
let fixture: ComponentFixture<TextField>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [TextField],
providers: [provideExperimentalZonelessChangeDetection()],
});
fixture = TestBed.createComponent(TextField);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,93 @@
/*!
* @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 {
ChangeDetectionStrategy,
Component,
ElementRef,
Input,
ViewChild,
afterNextRender,
forwardRef,
signal,
} from '@angular/core';
import {CommonModule} from '@angular/common';
import {ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR} from '@angular/forms';
import {IconComponent} from '../icon/icon.component';
@Component({
selector: 'docs-text-field',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, FormsModule, 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 {
@ViewChild('inputRef') private input?: ElementRef<HTMLInputElement>;
@Input() name: string | null = null;
@Input() placeholder: string | null = null;
@Input() disabled = false;
@Input() hideIcon = false;
@Input() autofocus = false;
// 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();
}
});
}
// Implemented as part of ControlValueAccessor.
writeValue(value: string): void {
this.value.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 = isDisabled;
}
setValue(value: string): void {
if (this.disabled) {
return;
}
this.value.set(value);
this.onChange(value);
this.onTouched();
}
}
@@ -0,0 +1,77 @@
load("//tools:defaults.bzl", "karma_web_test_suite", "ng_module", "ts_library")
load("@io_bazel_rules_sass//:defs.bzl", "sass_binary")
package(default_visibility = ["//visibility:private"])
ng_module(
name = "viewers",
srcs = [
"docs-viewer/docs-viewer.component.ts",
"example-viewer/example-viewer.component.ts",
],
assets = [
":docs-viewer/docs-viewer.component.css",
":example-viewer/example-viewer.component.css",
"example-viewer/example-viewer.component.html",
],
visibility = [
"//adev/shared-docs/components:__pkg__",
],
deps = [
"//adev/shared-docs/components/breadcrumb",
"//adev/shared-docs/components/copy-source-code-button",
"//adev/shared-docs/components/table-of-contents",
"//adev/shared-docs/interfaces",
"//adev/shared-docs/providers",
"//packages/common",
"//packages/core",
"//packages/router",
"@npm//@angular/cdk",
"@npm//@angular/material",
"@npm//@types/dom-view-transitions",
],
)
sass_binary(
name = "example-viewer-style",
src = "example-viewer/example-viewer.component.scss",
)
sass_binary(
name = "docs-viewer-style",
src = "docs-viewer/docs-viewer.component.scss",
deps = [
"//adev/shared-docs/styles",
],
)
ts_library(
name = "test_lib",
testonly = True,
srcs = glob(
["**/*.spec.ts"],
),
deps = [
":viewers",
"//adev/shared-docs/components/breadcrumb",
"//adev/shared-docs/components/copy-source-code-button",
"//adev/shared-docs/components/icon",
"//adev/shared-docs/components/table-of-contents",
"//adev/shared-docs/interfaces",
"//adev/shared-docs/providers",
"//adev/shared-docs/services",
"//packages/core",
"//packages/core/testing",
"//packages/platform-browser",
"//packages/platform-browser/animations",
"//packages/router",
"//packages/router/testing",
"@npm//@angular/cdk",
"@npm//@angular/material",
],
)
karma_web_test_suite(
name = "test",
deps = [":test_lib"],
)
@@ -0,0 +1,129 @@
@use '../../../styles/links' as links;
@use '../../../styles/anchor' as anchor;
:host {
--translate-y: clamp(5px, 0.25em, 7px);
}
.docs-viewer {
display: flex;
flex-direction: column;
padding: var(--layout-padding);
max-width: var(--page-width);
width: 100%;
box-sizing: border-box;
@media only screen and (max-width: 1430px) {
container: docs-content / inline-size;
}
// If rendered on the docs page, accommodate width for TOC
docs-docs & {
@media only screen and (min-width: 1430px) and (max-width: 1550px) {
width: calc(100% - 195px - var(--layout-padding));
max-width: var(--page-width);
}
}
pre {
margin-block: 0;
}
h1,
h2,
h3,
h4,
h5,
h6 {
.docs-anchor {
margin-block-start: 2.5rem;
display: inline-block;
color: inherit;
@include anchor.docs-anchor();
}
}
h1 {
font-size: 2.5rem;
margin-block-end: 0;
}
h2 {
font-size: 2rem;
margin-block-end: 0.5rem;
}
h3 {
font-size: 1.5rem;
margin-block-end: 0.5rem;
}
h4 {
font-size: 1.25rem;
margin-block-end: 0.5rem;
}
h5 {
font-size: 1rem;
margin-block-end: 0;
}
h6 {
font-size: 0.875rem;
margin-block-end: 0;
}
> :last-child {
margin-block-end: 0;
}
a:not(.docs-github-links):not(.docs-card):not(.docs-pill):not(.docs-example-github-link) {
&[href^='http:'],
&[href^='https:'] {
@include links.external-link-with-icon();
}
}
&-scroll-margin-large {
h2,
h3 {
scroll-margin: 5em;
}
}
}
.docs-header {
margin-block-end: 1rem;
& > p:first-child {
color: var(--quaternary-contrast);
font-weight: 500;
margin: 0;
}
}
.docs-page-title {
display: flex;
justify-content: space-between;
h1 {
margin-block: 0;
font-size: 2.25rem;
}
a {
color: var(--primary-contrast);
height: fit-content;
docs-icon {
color: var(--gray-400);
transition: color 0.3s ease;
}
&:hover {
docs-icon {
color: var(--primary-contrast);
}
}
}
}
@@ -0,0 +1,209 @@
/*!
* @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 {ComponentFixture, TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {RouterTestingModule} from '@angular/router/testing';
import {ExampleViewerContentLoader} from '../../../interfaces';
import {EXAMPLE_VIEWER_CONTENT_LOADER} from '../../../providers';
import {CodeExampleViewMode, ExampleViewer} from '../example-viewer/example-viewer.component';
import {DocViewer} from './docs-viewer.component';
import {IconComponent} from '../../icon/icon.component';
import {Breadcrumb} from '../../breadcrumb/breadcrumb.component';
import {NavigationState} from '../../../services';
import {CopySourceCodeButton} from '../../copy-source-code-button/copy-source-code-button.component';
import {TableOfContents} from '../../table-of-contents/table-of-contents.component';
import {provideExperimentalZonelessChangeDetection} from '@angular/core';
describe('DocViewer', () => {
let fixture: ComponentFixture<DocViewer>;
let exampleContentSpy: jasmine.SpyObj<ExampleViewerContentLoader>;
let navigationStateSpy: jasmine.SpyObj<NavigationState>;
const exampleDocContentWithExampleViewerPlaceholders = `<div class="docs-code linenums" visibleLines="[12, 31]" expanded="true" path="hello-world/hello-world-new.ts">
<div class="docs-code-header">A styled code example</div>
<pre>
<code><div class="hljs-ln-line"><span class="hljs-comment">/*!</div><div class="hljs-ln-line"> * @license</div><div class="hljs-ln-line"> * Copyright Google LLC All Rights Reserved.</div><div class="hljs-ln-line"> *</div><div class="hljs-ln-line"> * Use of this source code is governed by an MIT-style license that can be</div><div class="hljs-ln-line"> * found in the LICENSE file at https://angular.dev/license</div><div class="hljs-ln-line"> */</span></div><div class="hljs-ln-line"></div><div class="hljs-ln-line remove"><span class="hljs-keyword">import</span> {ChangeDetectorRef, Component, <span class="hljs-keyword">inject</span>, signal} <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;@angular/core&#x27;</span>;</div><div class="hljs-ln-line add"><span class="hljs-keyword">import</span> {Component, signal} <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;@angular/core&#x27;</span>;</div><div class="hljs-ln-line"><span class="hljs-keyword">import</span> {CommonModule} <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;@angular/common&#x27;</span>;</div><div class="hljs-ln-line"></div><div class="hljs-ln-line highlighted">@Component({</div><div class="hljs-ln-line highlighted"> selector: <span class="hljs-string">&#x27;hello-world&#x27;</span>,</div><div class="hljs-ln-line highlighted"> standalone: <span class="hljs-keyword">true</span>,</div><div class="hljs-ln-line highlighted"> imports: [CommonModule],</div><div class="hljs-ln-line highlighted"> templateUrl: <span class="hljs-string">&#x27;./hello-world.html&#x27;</span>,</div><div class="hljs-ln-line highlighted"> styleUrls: [<span class="hljs-string">&#x27;./hello-world.css&#x27;</span>],</div><div class="hljs-ln-line highlighted">})</div><div class="hljs-ln-line">export <span class="hljs-keyword">default</span> <span class="hljs-keyword">class</span> HelloWorldComponent {</div><div class="hljs-ln-line remove"> world = <span class="hljs-string">&#x27;World&#x27;</span>;</div><div class="hljs-ln-line add"> world = <span class="hljs-string">&#x27;World!!!&#x27;</span>;</div><div class="hljs-ln-line"> <span class="hljs-keyword">count</span> = signal(<span class="hljs-number">0</span>);</div><div class="hljs-ln-line remove"> changeDetector = <span class="hljs-keyword">inject</span>(ChangeDetectorRef);</div><div class="hljs-ln-line"></div><div class="hljs-ln-line"> increase(): <span class="hljs-keyword">void</span> {</div><div class="hljs-ln-line"> <span class="hljs-keyword">this</span>.<span class="hljs-keyword">count</span>.update((<span class="hljs-keyword">previous</span>) =&gt; {</div><div class="hljs-ln-line highlighted"> <span class="hljs-keyword">return</span> <span class="hljs-keyword">previous</span> + <span class="hljs-number">1</span>;</div><div class="hljs-ln-line"> });</div><div class="hljs-ln-line remove"> <span class="hljs-keyword">this</span>.changeDetector.detectChanges();</div><div class="hljs-ln-line"> }</div><div class="hljs-ln-line">}</div><div class="hljs-ln-line"></div></code>
</pre>
</div>`;
const exampleDocContentWithExpandedExampleViewerPlaceholders = `<div class="docs-code-multifile" expanded="true" path="hello-world/hello-world-new.ts">
<div class="docs-code" visibleLines="[12, 31]" path="hello-world/hello-world-new.ts">
<pre>
<code><div class="hljs-ln-line"><span class="hljs-comment">/*!</div><div class="hljs-ln-line"> * @license</div><div class="hljs-ln-line"> * Copyright Google LLC All Rights Reserved.</div><div class="hljs-ln-line"> *</div><div class="hljs-ln-line"> * Use of this source code is governed by an MIT-style license that can be</div><div class="hljs-ln-line"> * found in the LICENSE file at https://angular.dev/license</div><div class="hljs-ln-line"> */</span></div><div class="hljs-ln-line"></div><div class="hljs-ln-line remove"><span class="hljs-keyword">import</span> {ChangeDetectorRef, Component, <span class="hljs-keyword">inject</span>, signal} <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;@angular/core&#x27;</span>;</div><div class="hljs-ln-line add"><span class="hljs-keyword">import</span> {Component, signal} <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;@angular/core&#x27;</span>;</div><div class="hljs-ln-line"><span class="hljs-keyword">import</span> {CommonModule} <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;@angular/common&#x27;</span>;</div><div class="hljs-ln-line"></div><div class="hljs-ln-line">@Component({</div><div class="hljs-ln-line"> selector: <span class="hljs-string">&#x27;hello-world&#x27;</span>,</div><div class="hljs-ln-line"> standalone: <span class="hljs-keyword">true</span>,</div><div class="hljs-ln-line"> imports: [CommonModule],</div><div class="hljs-ln-line"> templateUrl: <span class="hljs-string">&#x27;./hello-world.html&#x27;</span>,</div><div class="hljs-ln-line"> styleUrls: [<span class="hljs-string">&#x27;./hello-world.css&#x27;</span>],</div><div class="hljs-ln-line">})</div><div class="hljs-ln-line">export <span class="hljs-keyword">default</span> <span class="hljs-keyword">class</span> HelloWorldComponent {</div><div class="hljs-ln-line remove"> world = <span class="hljs-string">&#x27;World&#x27;</span>;</div><div class="hljs-ln-line add"> world = <span class="hljs-string">&#x27;World!!!&#x27;</span>;</div><div class="hljs-ln-line"> <span class="hljs-keyword">count</span> = signal(<span class="hljs-number">0</span>);</div><div class="hljs-ln-line remove"> changeDetector = <span class="hljs-keyword">inject</span>(ChangeDetectorRef);</div><div class="hljs-ln-line"></div><div class="hljs-ln-line"> increase(): <span class="hljs-keyword">void</span> {</div><div class="hljs-ln-line"> <span class="hljs-keyword">this</span>.<span class="hljs-keyword">count</span>.update((<span class="hljs-keyword">previous</span>) =&gt; {</div><div class="hljs-ln-line"> <span class="hljs-keyword">return</span> <span class="hljs-keyword">previous</span> + <span class="hljs-number">1</span>;</div><div class="hljs-ln-line"> });</div><div class="hljs-ln-line remove"> <span class="hljs-keyword">this</span>.changeDetector.detectChanges();</div><div class="hljs-ln-line"> }</div><div class="hljs-ln-line">}</div><div class="hljs-ln-line"></div></code>
</pre>
</div>
<div class="docs-code linenums" path="hello-world/hello-world.html">
<pre>
<code><div class="hljs-ln-line"><span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">h2</span>&gt;</span>Hello </span><span class="hljs-template-variable">{{ <span class="hljs-name">world</span> }}</span><span class="language-xml"><span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span></div><div class="hljs-ln-line"><span class="hljs-tag">&lt;<span class="hljs-name">button</span> (<span class="hljs-attr">click</span>)=<span class="hljs-string">&quot;increase()&quot;</span>&gt;</span>Increase<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span></div><div class="hljs-ln-line"><span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Counter: </span><span class="hljs-template-variable">{{ <span class="hljs-name">count</span>() }}</span><span class="language-xml"><span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span></div><div class="hljs-ln-line"></span></div></code>
</pre>
</div>
</div>`;
const exampleContentWithIcons = `
<p>Content</p>
<docs-icon>light_mode</docs-icon>
<p>More content</p>
<docs-icon>dark_mode</docs-icon>
`;
const exampleContentWithBreadcrumbPlaceholder = `
<docs-breadcrumb></docs-breadcrumb>
<p>Content</p>
`;
const exampleContentWithCodeSnippet = `
<div class="docs-code" path="forms/src/app/actor.ts" header="src/app/actor.ts">
<pre class="docs-mini-scroll-track">
<code>
<div class="hljs-ln-line"></div>
</code>
</pre>
</div>
`;
const exampleContentWithHeadings = `
<h2>Heading h2</h2>
<h3>Heading h3</h3>
`;
beforeEach(() => {
exampleContentSpy = jasmine.createSpyObj('ExampleViewerContentLoader', ['getCodeExampleData']);
navigationStateSpy = jasmine.createSpyObj(NavigationState, ['activeNavigationItem']);
});
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DocViewer, NoopAnimationsModule, RouterTestingModule],
providers: [
provideExperimentalZonelessChangeDetection(),
{provide: EXAMPLE_VIEWER_CONTENT_LOADER, useValue: exampleContentSpy},
{provide: NavigationState, useValue: navigationStateSpy},
],
}).compileComponents();
fixture = TestBed.createComponent(DocViewer);
fixture.detectChanges();
});
it('should load doc into innerHTML', () => {
const fixture = TestBed.createComponent(DocViewer);
fixture.componentRef.setInput('docContent', 'hello world');
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toBe('hello world');
});
it('should instantiate example viewer in snippet view mode', async () => {
const fixture = TestBed.createComponent(DocViewer);
fixture.componentRef.setInput('docContent', exampleDocContentWithExampleViewerPlaceholders);
fixture.detectChanges();
await fixture.whenStable();
const exampleViewer = fixture.debugElement.query(By.directive(ExampleViewer));
expect(exampleViewer).not.toBeNull();
expect(exampleViewer.componentInstance.view()).toBe(CodeExampleViewMode.SNIPPET);
});
it('should display example viewer in multi file mode when user clicks expand', async () => {
const fixture = TestBed.createComponent(DocViewer);
fixture.componentRef.setInput(
'docContent',
exampleDocContentWithExpandedExampleViewerPlaceholders,
);
fixture.detectChanges();
await fixture.whenStable();
const exampleViewer = fixture.debugElement.query(By.directive(ExampleViewer));
const expandButton = fixture.debugElement.query(
By.css('button[aria-label="Expand code example"]'),
);
expandButton.nativeElement.click();
expect(exampleViewer).not.toBeNull();
expect(exampleViewer.componentInstance.view()).toBe(CodeExampleViewMode.MULTI_FILE);
expect(exampleViewer.componentInstance.tabs().length).toBe(2);
});
it('should render Icon component when content has <docs-icon> element', async () => {
const fixture = TestBed.createComponent(DocViewer);
const renderComponentSpy = spyOn(fixture.componentInstance, 'renderComponent' as any);
fixture.componentRef.setInput('docContent', exampleContentWithIcons);
fixture.detectChanges();
await fixture.whenStable();
expect(renderComponentSpy).toHaveBeenCalledTimes(2);
expect(renderComponentSpy.calls.allArgs()[0][0]).toBe(IconComponent);
expect((renderComponentSpy.calls.allArgs()[0][1] as HTMLElement).innerText).toEqual(
`light_mode`,
);
expect(renderComponentSpy.calls.allArgs()[1][0]).toBe(IconComponent);
expect((renderComponentSpy.calls.allArgs()[1][1] as HTMLElement).innerText).toEqual(
`dark_mode`,
);
});
it('should render Breadcrumb component when content has <docs-breadcrumb> element', async () => {
navigationStateSpy.activeNavigationItem.and.returnValue({
label: 'Active Item',
parent: {
label: 'Parent Item',
},
});
const fixture = TestBed.createComponent(DocViewer);
const renderComponentSpy = spyOn(fixture.componentInstance, 'renderComponent' as any);
fixture.componentRef.setInput('docContent', exampleContentWithBreadcrumbPlaceholder);
fixture.detectChanges();
await fixture.whenStable();
expect(renderComponentSpy).toHaveBeenCalledTimes(1);
expect(renderComponentSpy.calls.allArgs()[0][0]).toBe(Breadcrumb);
});
it('should render copy source code buttons', async () => {
const fixture = TestBed.createComponent(DocViewer);
fixture.componentRef.setInput('docContent', exampleContentWithCodeSnippet);
fixture.detectChanges();
await fixture.whenStable();
const copySourceCodeButton = fixture.debugElement.query(By.directive(CopySourceCodeButton));
expect(copySourceCodeButton).toBeTruthy();
});
it('should render ToC', async () => {
const fixture = TestBed.createComponent(DocViewer);
const renderComponentSpy = spyOn(fixture.componentInstance, 'renderComponent' as any);
fixture.componentRef.setInput('docContent', exampleContentWithHeadings);
fixture.componentRef.setInput('hasToc', true);
fixture.detectChanges();
await fixture.whenStable();
expect(renderComponentSpy).toHaveBeenCalled();
expect(renderComponentSpy.calls.allArgs()[0][0]).toBe(TableOfContents);
});
it('should not render ToC when hasToc is false', async () => {
const fixture = TestBed.createComponent(DocViewer);
const renderComponentSpy = spyOn(fixture.componentInstance, 'renderComponent' as any);
fixture.componentRef.setInput('docContent', exampleContentWithHeadings);
fixture.componentRef.setInput('hasToc', false);
fixture.detectChanges();
await fixture.whenStable();
expect(renderComponentSpy).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,349 @@
/*!
* @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 {CommonModule, DOCUMENT, isPlatformBrowser, Location} from '@angular/common';
import {
ApplicationRef,
ChangeDetectionStrategy,
Component,
ComponentRef,
createComponent,
DestroyRef,
ElementRef,
EnvironmentInjector,
inject,
Injector,
Input,
OnChanges,
PLATFORM_ID,
SimpleChanges,
Type,
ViewContainerRef,
ViewEncapsulation,
ɵPendingTasks as PendingTasks,
EventEmitter,
Output,
} from '@angular/core';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {TOC_SKIP_CONTENT_MARKER, NavigationState} from '../../../services/index';
import {TableOfContents} from '../../table-of-contents/table-of-contents.component';
import {IconComponent} from '../../icon/icon.component';
import {handleHrefClickEventWithRouter} from '../../../utils/index';
import {Snippet} from '../../../interfaces/index';
import {Router} from '@angular/router';
import {fromEvent} from 'rxjs';
import {Breadcrumb} from '../../breadcrumb/breadcrumb.component';
import {CopySourceCodeButton} from '../../copy-source-code-button/copy-source-code-button.component';
import {ExampleViewer} from '../example-viewer/example-viewer.component';
/// <reference types="@types/dom-view-transitions" />
const TOC_HOST_ELEMENT_NAME = 'docs-table-of-contents';
export const ASSETS_EXAMPLES_PATH = 'assets/content/examples';
export const DOCS_VIEWER_SELECTOR = 'docs-viewer';
export const DOCS_CODE_SELECTOR = '.docs-code';
export const DOCS_CODE_MUTLIFILE_SELECTOR = '.docs-code-multifile';
// TODO: Update the branch/sha
export const GITHUB_CONTENT_URL =
'https://github.com/angular/angular/blob/main/adev/src/content/examples/';
@Component({
selector: DOCS_VIEWER_SELECTOR,
standalone: true,
imports: [CommonModule],
template: '',
styleUrls: ['docs-viewer.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
host: {
'[class.docs-animate-content]': 'animateContent',
},
})
export class DocViewer implements OnChanges {
@Input() docContent?: string;
@Input() hasToc = false;
@Output() contentLoaded = new EventEmitter<void>();
private readonly destroyRef = inject(DestroyRef);
private readonly document = inject(DOCUMENT);
private readonly elementRef = inject(ElementRef);
private readonly location = inject(Location);
private readonly navigationState = inject(NavigationState);
private readonly platformId = inject(PLATFORM_ID);
private readonly router = inject(Router);
private readonly viewContainer = inject(ViewContainerRef);
private readonly environmentInjector = inject(EnvironmentInjector);
private readonly injector = inject(Injector);
private readonly appRef = inject(ApplicationRef);
// tslint:disable-next-line:no-unused-variable
private animateContent = false;
private readonly pendingRenderTasks = inject(PendingTasks);
private countOfExamples = 0;
async ngOnChanges(changes: SimpleChanges): Promise<void> {
const taskId = this.pendingRenderTasks.add();
if ('docContent' in changes) {
await this.renderContentsAndRunClientSetup(this.docContent!);
}
this.pendingRenderTasks.remove(taskId);
}
async renderContentsAndRunClientSetup(content?: string): Promise<void> {
const isBrowser = isPlatformBrowser(this.platformId);
const contentContainer = this.elementRef.nativeElement;
if (content) {
if (isBrowser && !(this.document as any).startViewTransition) {
// Apply a special class to the host node to trigger animation.
// Note: when a page is hydrated, the `content` would be empty,
// so we don't trigger an animation to avoid a content flickering
// visual effect. In addition, if the browser supports view transitions (startViewTransition is present), the animation is handled by the native View Transition API so it does not need to be done here.
this.animateContent = true;
}
contentContainer.innerHTML = content;
}
if (isBrowser) {
// First we setup event listeners on the HTML we just loaded.
// We want to do this before things like the example viewers are loaded.
this.setupAnchorListeners(contentContainer);
// Rewrite relative anchors (hrefs starting with `#`) because relative hrefs are relative to the base URL, which is '/'
this.rewriteRelativeAnchors(contentContainer);
// In case when content contains placeholders for executable examples, create ExampleViewer components.
await this.loadExamples();
// In case when content contains static code snippets, then create buttons
// responsible for copy source code.
this.loadCopySourceCodeButtons();
}
// Display Breadcrumb component if the `<docs-breadcrumb>` element exists
this.loadBreadcrumb(contentContainer);
// Display Icon component if the `<docs-icon>` element exists
this.loadIcons(contentContainer);
// Render ToC
this.renderTableOfContents(contentContainer);
this.contentLoaded.next();
}
/**
* Load ExampleViewer component when:
* - exists docs-code-multifile element with multiple files OR
* - exists docs-code element with single file AND
* - 'preview' attribute was provided OR
* - 'visibleLines' attribute was provided
*/
private async loadExamples(): Promise<void> {
const multifileCodeExamples = <HTMLElement[]>(
Array.from(this.elementRef.nativeElement.querySelectorAll(DOCS_CODE_MUTLIFILE_SELECTOR))
);
for (let placeholder of multifileCodeExamples) {
const path = placeholder.getAttribute('path')!;
const snippets = this.getCodeSnippetsFromMultifileWrapper(placeholder);
await this.renderExampleViewerComponents(placeholder, snippets, path);
}
const docsCodeElements = this.elementRef.nativeElement.querySelectorAll(DOCS_CODE_SELECTOR);
for (const placeholder of docsCodeElements) {
const snippet = this.getStandaloneCodeSnippet(placeholder);
if (snippet) {
await this.renderExampleViewerComponents(placeholder, [snippet], snippet.name);
}
}
}
private renderTableOfContents(element: HTMLElement): void {
if (!this.hasToc) {
return;
}
const firstHeading = element.querySelector<HTMLHeadingElement>('h2,h3[id]');
if (!firstHeading) {
return;
}
// Since the content of the main area is dynamically created and there is
// no host element for a ToC component, we create it manually.
let tocHostElement: HTMLElement | null = element.querySelector(TOC_HOST_ELEMENT_NAME);
if (!tocHostElement) {
tocHostElement = this.document.createElement(TOC_HOST_ELEMENT_NAME);
tocHostElement.setAttribute(TOC_SKIP_CONTENT_MARKER, 'true');
firstHeading?.parentNode?.insertBefore(tocHostElement, firstHeading);
}
this.renderComponent(TableOfContents, tocHostElement, {contentSourceElement: element});
}
private async renderExampleViewerComponents(
placeholder: HTMLElement,
snippets: Snippet[],
path: string,
): Promise<void> {
const preview = Boolean(placeholder.getAttribute('preview'));
const title = placeholder.getAttribute('header') ?? undefined;
const firstCodeSnippetTitle =
snippets.length > 0 ? snippets[0].title ?? snippets[0].name : undefined;
const exampleRef = this.viewContainer.createComponent(ExampleViewer);
this.countOfExamples++;
exampleRef.instance.metadata = {
title: title ?? firstCodeSnippetTitle,
path,
files: snippets,
preview,
id: this.countOfExamples,
};
exampleRef.instance.githubUrl = `${GITHUB_CONTENT_URL}/${snippets[0].name}`;
exampleRef.instance.stackblitzUrl = `${ASSETS_EXAMPLES_PATH}/${snippets[0].name}.html`;
placeholder.parentElement!.replaceChild(exampleRef.location.nativeElement, placeholder);
await exampleRef.instance.renderExample();
}
private getCodeSnippetsFromMultifileWrapper(element: HTMLElement): Snippet[] {
const tabs = <Element[]>Array.from(element.querySelectorAll(DOCS_CODE_SELECTOR));
return tabs.map((tab) => ({
name: tab.getAttribute('path') ?? tab.getAttribute('header') ?? '',
content: tab.innerHTML,
visibleLinesRange: tab.getAttribute('visibleLines') ?? undefined,
}));
}
private getStandaloneCodeSnippet(element: HTMLElement): Snippet | null {
const visibleLines = element.getAttribute('visibleLines') ?? undefined;
const preview = element.getAttribute('preview');
if (!visibleLines && !preview) {
return null;
}
const content = element.querySelector('pre')!;
const path = element.getAttribute('path')!;
const title = element.getAttribute('header') ?? undefined;
return {
title,
name: path,
content: content?.outerHTML,
visibleLinesRange: visibleLines,
};
}
// If the content contains static code snippets, we should add buttons to copy
// the code
private loadCopySourceCodeButtons(): void {
const staticCodeSnippets = <Element[]>(
Array.from(this.elementRef.nativeElement.querySelectorAll('.docs-code:not([mermaid])'))
);
for (let codeSnippet of staticCodeSnippets) {
const copySourceCodeButton = this.viewContainer.createComponent(CopySourceCodeButton);
codeSnippet.appendChild(copySourceCodeButton.location.nativeElement);
}
}
private loadBreadcrumb(element: HTMLElement): void {
const breadcrumbPlaceholder = element.querySelector('docs-breadcrumb') as HTMLElement;
const activeNavigationItem = this.navigationState.activeNavigationItem();
if (breadcrumbPlaceholder && !!activeNavigationItem?.parent) {
this.renderComponent(Breadcrumb, breadcrumbPlaceholder);
}
}
private loadIcons(element: HTMLElement): void {
element.querySelectorAll('docs-icon').forEach((iconsPlaceholder) => {
this.renderComponent(IconComponent, iconsPlaceholder as HTMLElement);
});
}
/**
* Helper method to render a component dynamically in a context of this class.
*/
private renderComponent<T>(
type: Type<T>,
hostElement: HTMLElement,
inputs?: {[key: string]: unknown},
): ComponentRef<T> {
const componentRef = createComponent(type, {
hostElement,
elementInjector: this.injector,
environmentInjector: this.environmentInjector,
});
if (inputs) {
for (const [name, value] of Object.entries(inputs)) {
componentRef.setInput(name, value);
}
}
// Trigger change detection after setting inputs.
componentRef.changeDetectorRef.detectChanges();
// Attach a view to the ApplicationRef for change detection
// purposes and for hydration serialization to pick it up
// during SSG.
this.appRef.attachView(componentRef.hostView);
return componentRef;
}
private setupAnchorListeners(element: HTMLElement): void {
element.querySelectorAll(`a[href]`).forEach((anchor) => {
// Get the target element's ID from the href attribute
const url = new URL((anchor as HTMLAnchorElement).href);
const isExternalLink = url.origin !== this.document.location.origin;
if (isExternalLink) {
return;
}
fromEvent(anchor, 'click')
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((e) => {
const closestAnchor = (e.target as Element).closest('a');
if (closestAnchor?.target && closestAnchor.target !== 'self') {
return;
}
const hrefAttr = closestAnchor?.getAttribute?.('href');
if (!hrefAttr) {
return;
}
let relativeUrl: string;
if (hrefAttr.startsWith('http')) {
// Url is absolute but we're targeting the same domain
const url = new URL(hrefAttr);
relativeUrl = `${url.pathname}${url.hash}${url.search}`;
} else {
relativeUrl = hrefAttr;
}
handleHrefClickEventWithRouter(e, this.router, relativeUrl);
});
});
}
private rewriteRelativeAnchors(element: HTMLElement) {
for (const anchor of Array.from(element.querySelectorAll(`a[href^="#"]:not(a[download])`))) {
const url = new URL((anchor as HTMLAnchorElement).href);
(anchor as HTMLAnchorElement).href = this.location.path() + url.hash;
}
}
}
@@ -0,0 +1,155 @@
<div class="docs-example-viewer" role="group">
<header class="docs-example-viewer-actions">
@if (view() === CodeExampleViewMode.SNIPPET) {
<span>{{ exampleMetadata()?.title }}</span>
}
@if (view() === CodeExampleViewMode.MULTI_FILE) {
<mat-tab-group
#codeTabs
animationDuration="0ms"
mat-stretch-tabs="false"
>
@for (tab of tabs(); track tab) {
<mat-tab [label]="tab.name"></mat-tab>
}
</mat-tab-group>
}
<div class="docs-example-viewer-icons">
<button
type="button"
class="docs-example-copy-link"
[attr.aria-label]="'Copy link to ' + exampleMetadata()?.title + ' example to the clipboard'"
(click)="copyLink()"
>
<i aria-hidden="true">
<svg
aria-hidden="true"
width="24"
height="24"
viewBox="0 0 24 24"
fill="inherit"
xmlns="http://www.w3.org/2000/svg"
>
<!-- link icon -->
<path
d="M11 17H7C5.61667 17 4.4375 16.5125 3.4625 15.5375C2.4875 14.5625 2 13.3833 2 12C2 10.6167 2.4875 9.4375 3.4625 8.4625C4.4375 7.4875 5.61667 7 7 7H11V9H7C6.16667 9 5.45833 9.29167 4.875 9.875C4.29167 10.4583 4 11.1667 4 12C4 12.8333 4.29167 13.5417 4.875 14.125C5.45833 14.7083 6.16667 15 7 15H11V17ZM8 13V11H16V13H8ZM13 17V15H17C17.8333 15 18.5417 14.7083 19.125 14.125C19.7083 13.5417 20 12.8333 20 12C20 11.1667 19.7083 10.4583 19.125 9.875C18.5417 9.29167 17.8333 9 17 9H13V7H17C18.3833 7 19.5625 7.4875 20.5375 8.4625C21.5125 9.4375 22 10.6167 22 12C22 13.3833 21.5125 14.5625 20.5375 15.5375C19.5625 16.5125 18.3833 17 17 17H13Z"
fill="inherit"
/>
</svg>
</i>
</button>
<ng-container *ngTemplateOutlet="openCodeInExternalProvider" />
@if (expandable()) {
<button
type="button"
(click)="toggleExampleVisibility()"
[attr.title]="(expanded() ? 'Collapse' : 'Expand') + ' example'"
[attr.aria-label]="(expanded() ? 'Collapse' : 'Expand') + ' code example'"
>
<i aria-hidden="true">
@if (!expanded()) {
<svg
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
>
<!-- Expand arrow -->
<path d="M3 21v-8h2v4.6L17.6 5H13V3h8v8h-2V6.4L6.4 19H11v2H3Z" />
</svg>
} @else {
<svg
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
>
<path
fill="var(--gray-400)"
d="M3.4 22 2 20.6 8.6 14H4v-2h8v8h-2v-4.6L3.4 22ZM12 12V4h2v4.6L20.6 2 22 3.4 15.4 10H20v2h-8Z"
/>
</svg>
}
</i>
</button>
}
</div>
</header>
<div
class="docs-example-viewer-code-wrapper"
[class.docs-example-viewer-snippet]="view() === CodeExampleViewMode.SNIPPET"
[class.docs-example-viewer-multi-file]="view() === CodeExampleViewMode.MULTI_FILE"
>
<button docs-copy-source-code></button>
<docs-viewer [docContent]="snippetCode()?.content" />
</div>
@if (exampleComponent) {
<div class="docs-example-viewer-preview">
<ng-container *ngComponentOutlet="exampleComponent" />
</div>
}
<ng-template #openCodeInExternalProvider>
@if (exampleComponent) {
@if (githubUrl) {
<a
[href]="githubUrl"
target="_blank"
title="Open example on GitHub"
class="docs-example-github-link"
aria-label="Open example on GitHub"
>
<i aria-hidden="true">
<svg
aria-hidden="true"
width="24"
height="24"
viewBox="0 0 24 24"
fill="inherit"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M9.16141 22.8681C9.16141 22.5894 9.15159 21.8509 9.14614 20.8707C5.96014 21.5798 5.28759 19.296 5.28759 19.296C4.76668 17.9389 4.01559 17.5778 4.01559 17.5778C2.97541 16.8485 4.09414 16.8638 4.09414 16.8638C5.24396 16.9467 5.84886 18.0747 5.84886 18.0747C6.8705 19.8692 8.52923 19.3516 9.18268 19.0505C9.28686 18.2912 9.5825 17.7736 9.90977 17.4801C7.36632 17.184 4.69196 16.176 4.69196 11.6754C4.69196 10.3936 5.13868 9.34523 5.87123 8.52377C5.75396 8.22705 5.36014 7.03305 5.98359 5.41577C5.98359 5.41577 6.94577 5.09996 9.13359 6.61959C10.0467 6.35941 11.0269 6.2285 12.0016 6.22414C12.9741 6.2285 13.9538 6.35941 14.869 6.61959C17.0558 5.09996 18.0163 5.41577 18.0163 5.41577C18.6414 7.0325 18.2481 8.2265 18.1298 8.52377C18.864 9.34523 19.3069 10.3936 19.3069 11.6754C19.3069 16.1874 16.6287 17.1801 14.077 17.4709C14.4889 17.8336 14.8543 18.5503 14.8543 19.6461C14.8543 21.2165 14.8396 22.4836 14.8396 22.8681C14.8396 23.1829 15.0463 23.5478 15.6278 23.4327C20.1758 21.877 23.4545 17.4774 23.4545 12.2907C23.4545 5.80359 18.3256 0.54541 11.9994 0.54541C5.67432 0.54541 0.54541 5.80359 0.54541 12.2907C0.545956 17.479 3.82796 21.8814 8.37977 23.4343C8.95196 23.5418 9.16141 23.179 9.16141 22.8681Z"
fill="inherit"
/>
</svg>
</i>
</a>
}
@if (stackblitzUrl) {
<a
[href]="stackblitzUrl"
target="_blank"
class="docs-example-stackblitz-link"
title="Edit this example in StackBlitz"
aria-label="Edit this example in StackBlitz"
>
<i aria-hidden="true">
<svg
width="24"
height="24"
viewBox="0 0 356 511"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M138.719 150.22C62.6928 232.614 0.340573 300.4 0.158928 300.856C-0.0227172 301.311 33.9559 301.799 75.6665 301.939L151.505 302.195L117.656 396.511C74.7852 515.966 76.7972 510.288 77.3522 510.288C78.2145 510.288 355.296 209.735 355.296 208.799C355.296 208.245 325.263 207.879 279.943 207.879C233.709 207.879 204.591 207.518 204.591 206.943C204.591 206.428 220.136 162.751 239.137 109.883C279.06 -1.20153 278.545 0.264614 277.638 0.347453C277.26 0.382384 214.746 67.8247 138.719 150.22Z"
/>
</svg>
</i>
</a>
}
}
</ng-template>
</div>
@@ -0,0 +1,129 @@
:host {
.docs-example-viewer-preview {
.docs-dark-mode & {
background: var(--gray-100);
}
@media screen and (prefers-color-scheme: dark) {
background: var(--gray-100);
}
.docs-light-mode & {
background: var(--page-background);
}
}
}
.docs-example-viewer {
border: 1px solid var(--senary-contrast);
border-radius: 0.25rem;
overflow: hidden;
}
// Example viewer header
.docs-example-viewer-actions {
background: var(--subtle-purple);
display: flex;
justify-content: space-between;
align-items: center;
gap: 0.5rem;
border-bottom: 1px solid var(--senary-contrast);
transition: background 0.3s ease, border-color 0.3s ease;
padding-inline-end: 0.65rem;
font-family: var(--inter-tight-font);
mat-tab-group {
max-width: calc(100% - 140px);
}
span:first-of-type {
background-image: var(--purple-to-blue-horizontal-gradient);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
padding: 0.7rem 1.1rem;
font-size: 0.875rem;
font-style: normal;
font-weight: 400;
line-height: 1.4rem;
letter-spacing: -0.00875rem;
margin: 0;
word-wrap: break-word;
width: fit-content;
}
.docs-example-viewer-icons {
display: flex;
gap: 0.75rem;
svg {
fill: var(--gray-400);
}
}
a,
button {
padding: 0;
margin: 0;
cursor: pointer;
height: 24px;
width: 24px;
path {
transition: fill 0.3s ease;
}
&:hover {
svg {
fill: var(--tertiary-contrast);
}
}
}
}
// Example viewer code
.docs-example-viewer-code-wrapper {
position: relative;
font-size: 0.875rem;
// TODO: only show this if there is a preview
// border-block-end: 1px solid var(--senary-contrast);
transition: border-color 0.3s ease;
container: viewerblock / inline-size;
background-color: var(--octonary-contrast);
button[docs-copy-source-code] {
top: 0.31rem;
}
}
// stylelint-disable-next-line
::ng-deep {
.docs-example-viewer-preview {
// stylelint-disable-next-line
all: initial;
display: block;
padding: 1rem;
border-block-start: 1px solid var(--senary-contrast);
*,
code::before,
code,
pre,
a,
i,
p,
h1,
h2,
h3,
h4,
h5,
h6,
ol,
ul,
li,
hr,
input,
select,
table {
all: revert;
}
}
}
@@ -0,0 +1,230 @@
/*!
* @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 {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
import {ExampleViewer} from './example-viewer.component';
import {DocsContentLoader, ExampleMetadata, ExampleViewerContentLoader} from '../../../interfaces';
import {DOCS_CONTENT_LOADER, EXAMPLE_VIEWER_CONTENT_LOADER} from '../../../providers';
import {Component, provideExperimentalZonelessChangeDetection} from '@angular/core';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {HarnessLoader} from '@angular/cdk/testing';
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
import {Clipboard} from '@angular/cdk/clipboard';
import {By} from '@angular/platform-browser';
import {MatTabGroupHarness} from '@angular/material/tabs/testing';
import {CopySourceCodeButton} from '../../copy-source-code-button/copy-source-code-button.component';
import {ActivatedRoute} from '@angular/router';
describe('ExampleViewer', () => {
let component: ExampleViewer;
let fixture: ComponentFixture<ExampleViewer>;
let loader: HarnessLoader;
let exampleContentSpy: jasmine.SpyObj<ExampleViewerContentLoader>;
let contentServiceSpy: jasmine.SpyObj<DocsContentLoader>;
beforeEach(() => {
exampleContentSpy = jasmine.createSpyObj('ExampleContentLoader', ['loadPreview']);
contentServiceSpy = jasmine.createSpyObj('ContentLoader', ['getContent']);
contentServiceSpy.getContent.and.returnValue(Promise.resolve(undefined));
});
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ExampleViewer, NoopAnimationsModule],
providers: [
provideExperimentalZonelessChangeDetection(),
{provide: EXAMPLE_VIEWER_CONTENT_LOADER, useValue: exampleContentSpy},
{provide: DOCS_CONTENT_LOADER, useValue: contentServiceSpy},
{provide: ActivatedRoute, useValue: {snapshot: {fragment: 'fragment'}}},
],
}).compileComponents();
fixture = TestBed.createComponent(ExampleViewer);
component = fixture.componentInstance;
loader = TestbedHarnessEnvironment.loader(fixture);
fixture.detectChanges();
});
it('should set file extensions as tab names when all files have different extension', waitForAsync(async () => {
component.metadata = getMetadata({
files: [
{name: 'file.ts', content: ''},
{name: 'file.html', content: ''},
{name: 'file.css', content: ''},
],
});
await component.renderExample();
expect(component.tabs()!.length).toBe(3);
expect(component.tabs()![0].name).toBe('TS');
expect(component.tabs()![1].name).toBe('HTML');
expect(component.tabs()![2].name).toBe('CSS');
}));
it('should generate correct code content for multi file mode when it is expanded', waitForAsync(async () => {
component.metadata = getMetadata({
files: [
{name: 'file.ts', content: 'typescript file'},
{name: 'file.html', content: 'html file'},
{name: 'file.css', content: 'css file'},
],
});
await component.renderExample();
expect(component.tabs()!.length).toBe(3);
expect(component.tabs()![0].code).toBe('typescript file');
expect(component.tabs()![1].code).toBe('html file');
expect(component.tabs()![2].code).toBe('css file');
}));
it('should set file names as tab names when there is at least one duplication', async () => {
component.metadata = getMetadata({
files: [
{name: 'example.ts', content: 'typescript file'},
{name: 'example.html', content: 'html file'},
{name: 'another-example.ts', content: 'css file'},
],
});
await component.renderExample();
expect(component.tabs()!.length).toBe(3);
expect(component.tabs()![0].name).toBe('example.ts');
expect(component.tabs()![1].name).toBe('example.html');
expect(component.tabs()![2].name).toBe('another-example.ts');
});
it('should expandable be false when none of the example files have defined visibleLinesRange ', waitForAsync(async () => {
component.metadata = getMetadata();
await component.renderExample();
expect(component.expandable()).toBeFalse();
}));
it('should expandable be true when at least one example file has defined visibleLinesRange ', waitForAsync(async () => {
component.metadata = getMetadata({
files: [
{name: 'example.ts', content: 'typescript file'},
{
name: 'example.html',
content: 'html file',
visibleLinesRange: '[1, 2]',
},
{name: 'another-example.ts', content: 'css file'},
],
});
await component.renderExample();
expect(component.expandable()).toBeTrue();
}));
it('should set exampleComponent when metadata contains path and preview is true', waitForAsync(async () => {
exampleContentSpy.loadPreview.and.resolveTo(ExampleComponent);
component.metadata = getMetadata({
path: 'example.ts',
preview: true,
});
await component.renderExample();
expect(component.exampleComponent).toBe(ExampleComponent);
}));
it('should display GitHub button when githubUrl is provided and there is preview', waitForAsync(async () => {
exampleContentSpy.loadPreview.and.resolveTo(ExampleComponent);
component.metadata = getMetadata({
path: 'example.ts',
preview: true,
});
component.githubUrl = 'https://github.com/';
await component.renderExample();
const githubButton = fixture.debugElement.query(
By.css('a[aria-label="Open example on GitHub"]'),
);
expect(githubButton).toBeTruthy();
expect(githubButton.nativeElement.href).toBe(component.githubUrl);
}));
it('should display StackBlitz button when stackblitzUrl is provided and there is preview', waitForAsync(async () => {
exampleContentSpy.loadPreview.and.resolveTo(ExampleComponent);
component.metadata = getMetadata({
path: 'example.ts',
preview: true,
});
component.stackblitzUrl = 'https://stackblitz.com/';
await component.renderExample();
const stackblitzButton = fixture.debugElement.query(
By.css('a[aria-label="Edit this example in StackBlitz"]'),
);
expect(stackblitzButton).toBeTruthy();
expect(stackblitzButton.nativeElement.href).toBe(component.stackblitzUrl);
}));
it('should set expanded flag in metadata after toggleExampleVisibility', waitForAsync(async () => {
component.metadata = getMetadata();
await component.renderExample();
component.toggleExampleVisibility();
expect(component.expanded()).toBeTrue();
const tabGroup = await loader.getHarness(MatTabGroupHarness);
const tab = await tabGroup.getSelectedTab();
expect(await tab.getLabel()).toBe('TS');
component.toggleExampleVisibility();
expect(component.expanded()).toBeFalse();
}));
// TODO(josephperrott): enable once the docs-viewer/example-viewer circle is sorted out.
xit('should call clipboard service when clicked on copy source code', waitForAsync(async () => {
const expectedCodeSnippetContent = 'typescript code';
component.metadata = getMetadata({
files: [
{
name: 'example.ts',
content: `<pre><code>${expectedCodeSnippetContent}</code></pre>`,
},
{name: 'example.css', content: ''},
],
});
const clipboardService = TestBed.inject(Clipboard);
const spy = spyOn(clipboardService, 'copy');
await component.renderExample();
const button = fixture.debugElement.query(By.directive(CopySourceCodeButton)).nativeElement;
button.click();
expect(spy.calls.argsFor(0)[0]?.trim()).toBe(expectedCodeSnippetContent);
}));
it('should call clipboard service when clicked on copy example link', waitForAsync(async () => {
component.metadata = getMetadata();
component.expanded.set(true);
fixture.detectChanges();
const clipboardService = TestBed.inject(Clipboard);
const spy = spyOn(clipboardService, 'copy');
await component.renderExample();
const button = fixture.debugElement.query(
By.css('button.docs-example-copy-link'),
).nativeElement;
button.click();
expect(spy.calls.argsFor(0)[0].trim()).toBe(`${window.origin}/context.html#example-1`);
}));
});
const getMetadata = (value: Partial<ExampleMetadata> = {}): ExampleMetadata => {
return {
id: 1,
files: [
{name: 'example.ts', content: ''},
{name: 'example.css', content: ''},
],
preview: false,
...value,
};
};
@Component({
template: '',
standalone: true,
})
class ExampleComponent {}
@@ -0,0 +1,243 @@
/*!
* @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 {
ChangeDetectionStrategy,
Component,
DestroyRef,
Input,
Type,
computed,
inject,
ChangeDetectorRef,
ViewChild,
signal,
ElementRef,
forwardRef,
} from '@angular/core';
import {CommonModule, DOCUMENT} from '@angular/common';
import {MatTabGroup, MatTabsModule} from '@angular/material/tabs';
import {Clipboard} from '@angular/cdk/clipboard';
import {CopySourceCodeButton} from '../../copy-source-code-button/copy-source-code-button.component';
import {ExampleMetadata, Snippet} from '../../../interfaces/index';
import {EXAMPLE_VIEWER_CONTENT_LOADER} from '../../../providers/index';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {DocViewer} from '../docs-viewer/docs-viewer.component';
export enum CodeExampleViewMode {
SNIPPET = 'snippet',
MULTI_FILE = 'multi',
}
export const CODE_LINE_NUMBER_CLASS_NAME = 'shiki-ln-number';
export const CODE_LINE_CLASS_NAME = 'line';
export const GAP_CODE_LINE_CLASS_NAME = 'gap';
export const HIDDEN_CLASS_NAME = 'hidden';
@Component({
selector: 'docs-example-viewer',
standalone: true,
imports: [CommonModule, forwardRef(() => DocViewer), CopySourceCodeButton, MatTabsModule],
templateUrl: './example-viewer.component.html',
styleUrls: ['./example-viewer.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ExampleViewer {
// TODO: replace by signal-based input when it'll be available
@Input({required: true}) set metadata(value: ExampleMetadata) {
this.exampleMetadata.set(value);
}
@Input() githubUrl: string | null = null;
@Input() stackblitzUrl: string | null = null;
@ViewChild('codeTabs') matTabGroup?: MatTabGroup;
private readonly changeDetector = inject(ChangeDetectorRef);
private readonly clipboard = inject(Clipboard);
private readonly destroyRef = inject(DestroyRef);
private readonly document = inject(DOCUMENT);
private readonly elementRef = inject(ElementRef<HTMLElement>);
private readonly exampleViewerContentLoader = inject(EXAMPLE_VIEWER_CONTENT_LOADER);
private readonly shouldDisplayFullName = computed(() => {
const fileExtensions =
this.exampleMetadata()?.files.map((file) => this.getFileExtension(file.name)) ?? [];
// Display full file names only when exist files with the same extension
return new Set(fileExtensions).size !== fileExtensions.length;
});
CodeExampleViewMode = CodeExampleViewMode;
exampleComponent?: Type<unknown>;
expanded = signal<boolean>(false);
exampleMetadata = signal<ExampleMetadata | null>(null);
snippetCode = signal<Snippet | undefined>(undefined);
tabs = computed(() =>
this.exampleMetadata()?.files.map((file) => ({
name:
file.title ?? (this.shouldDisplayFullName() ? file.name : this.getFileExtension(file.name)),
code: file.content,
})),
);
view = computed(() =>
this.exampleMetadata()?.files.length === 1
? CodeExampleViewMode.SNIPPET
: CodeExampleViewMode.MULTI_FILE,
);
expandable = computed(() =>
this.exampleMetadata()?.files.some((file) => !!file.visibleLinesRange),
);
async renderExample(): Promise<void> {
// Lazy load live example component
if (this.exampleMetadata()?.path && this.exampleMetadata()?.preview) {
this.exampleComponent = await this.exampleViewerContentLoader.loadPreview(
this.exampleMetadata()?.path!,
);
}
this.snippetCode.set(this.exampleMetadata()?.files[0]);
this.changeDetector.detectChanges();
this.setCodeLinesVisibility();
this.elementRef.nativeElement.setAttribute(
'id',
`example-${this.exampleMetadata()?.id.toString()!}`,
);
this.matTabGroup?.realignInkBar();
this.listenToMatTabIndexChange();
}
toggleExampleVisibility(): void {
this.expanded.update((expanded) => !expanded);
this.setCodeLinesVisibility();
}
copyLink(): void {
// Reconstruct the URL using `origin + pathname` so we drop any pre-existing hash.
const fullUrl =
location.origin +
location.pathname +
location.search +
'#example-' +
this.exampleMetadata()?.id;
this.clipboard.copy(fullUrl);
}
private listenToMatTabIndexChange(): void {
this.matTabGroup?.realignInkBar();
this.matTabGroup?.selectedIndexChange
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((index) => {
this.snippetCode.set(this.exampleMetadata()?.files[index]);
this.changeDetector.detectChanges();
this.setCodeLinesVisibility();
});
}
private getFileExtension(name: string): string {
const segments = name.split('.');
return segments.length ? segments[segments.length - 1].toLocaleUpperCase() : '';
}
private setCodeLinesVisibility(): void {
this.expanded()
? this.handleExpandedStateForCodeBlock()
: this.handleCollapsedStateForCodeBlock();
}
private handleExpandedStateForCodeBlock(): void {
const lines = <HTMLDivElement[]>(
Array.from(
this.elementRef.nativeElement.querySelectorAll(
`.${CODE_LINE_CLASS_NAME}.${HIDDEN_CLASS_NAME}`,
),
)
);
const lineNumbers = <HTMLSpanElement[]>(
Array.from(
this.elementRef.nativeElement.querySelectorAll(
`.${CODE_LINE_NUMBER_CLASS_NAME}.${HIDDEN_CLASS_NAME}`,
),
)
);
const gapLines = <HTMLDivElement[]>(
Array.from(
this.elementRef.nativeElement.querySelectorAll(
`.${CODE_LINE_CLASS_NAME}.${GAP_CODE_LINE_CLASS_NAME}`,
),
)
);
for (const line of lines) {
line.classList.remove(HIDDEN_CLASS_NAME);
}
for (const lineNumber of lineNumbers) {
lineNumber.classList.remove(HIDDEN_CLASS_NAME);
}
for (const expandLine of gapLines) {
expandLine.remove();
}
}
private handleCollapsedStateForCodeBlock(): void {
const visibleLinesRange = this.snippetCode()?.visibleLinesRange;
if (!visibleLinesRange) {
return;
}
const linesToDisplay = (visibleLinesRange?.split(',') ?? []).map((line) => Number(line));
const lines = <HTMLDivElement[]>(
Array.from(this.elementRef.nativeElement.querySelectorAll(`.${CODE_LINE_CLASS_NAME}`))
);
const lineNumbers = <HTMLSpanElement[]>(
Array.from(this.elementRef.nativeElement.querySelectorAll(`.${CODE_LINE_NUMBER_CLASS_NAME}`))
);
const appendGapBefore = [];
for (const [index, line] of lines.entries()) {
if (!linesToDisplay.includes(index)) {
line.classList.add(HIDDEN_CLASS_NAME);
} else if (!linesToDisplay.includes(index - 1)) {
appendGapBefore.push(line);
}
}
for (const [index, lineNumber] of lineNumbers.entries()) {
if (!linesToDisplay.includes(index)) {
lineNumber.classList.add(HIDDEN_CLASS_NAME);
}
}
// Create gap line between visible ranges. For example we would like to display 10-16 and 20-29 lines.
// We should display separator, gap between those two scopes.
// TODO: we could replace div it with the component, and allow to expand code block after click.
for (const [index, element] of appendGapBefore.entries()) {
if (index === 0) {
continue;
}
const separator = this.document.createElement('div');
separator.textContent = `...`;
separator.classList.add(CODE_LINE_CLASS_NAME);
separator.classList.add(GAP_CODE_LINE_CLASS_NAME);
element.parentNode?.insertBefore(separator, element);
}
}
}
+28
View File
@@ -0,0 +1,28 @@
load("//tools:defaults.bzl", "ts_library")
package(default_visibility = ["//visibility:private"])
ts_library(
name = "constants",
srcs = [
"index.ts",
],
visibility = [
"//adev/shared-docs:__subpackages__",
],
deps = [
":lib",
],
)
ts_library(
name = "lib",
srcs = glob(
[
"*.ts",
],
exclude = [
"index.ts",
],
),
)
+13
View File
@@ -0,0 +1,13 @@
/*!
* @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
*/
// Used for both the table of contents and the home animation
export const RESIZE_EVENT_DELAY = 500;
// Used for the home animation
export const WEBGL_LOADED_DELAY = 250;
+9
View File
@@ -0,0 +1,9 @@
/*!
* @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
*/
export * from './delay';
+18
View File
@@ -0,0 +1,18 @@
load("//tools:defaults.bzl", "ts_library")
package(default_visibility = ["//visibility:private"])
ts_library(
name = "directives",
srcs = [
"index.ts",
],
visibility = [
"//adev/shared-docs:__subpackages__",
],
deps = [
"//adev/shared-docs/directives/click-outside",
"//adev/shared-docs/directives/external-link",
"//adev/shared-docs/directives/search-item",
],
)
@@ -0,0 +1,38 @@
load("//tools:defaults.bzl", "karma_web_test_suite", "ng_module", "ts_library")
package(default_visibility = ["//visibility:private"])
ng_module(
name = "click-outside",
srcs = [
"click-outside.directive.ts",
],
visibility = [
"//adev/shared-docs/directives:__pkg__",
],
deps = [
"//packages/common",
"//packages/core",
],
)
ts_library(
name = "test_lib",
testonly = True,
srcs = glob(
["*.spec.ts"],
),
deps = [
":click-outside",
"//packages/core",
"//packages/core/testing",
"//packages/platform-browser",
"//packages/router",
"//packages/router/testing",
],
)
karma_web_test_suite(
name = "test",
deps = [":test_lib"],
)
@@ -0,0 +1,77 @@
/*!
* @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 {Component} from '@angular/core';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {RouterTestingModule} from '@angular/router/testing';
import {ClickOutside} from './click-outside.directive';
import {By} from '@angular/platform-browser';
describe('ClickOutside', () => {
let component: ExampleComponent;
let fixture: ComponentFixture<ExampleComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ExampleComponent, RouterTestingModule],
});
fixture = TestBed.createComponent(ExampleComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should docsClickOutside be emitted when user click outside `content` element', () => {
const clickedOutsideSpy = spyOn(component, 'clickedOutside');
const button = fixture.debugElement.query(By.css('button[id="exampleButton"]'));
button.nativeElement.click();
expect(clickedOutsideSpy).toHaveBeenCalledTimes(1);
});
it('should not docsClickOutside be emitted when user click inside `content` element', () => {
const clickedOutsideSpy = spyOn(component, 'clickedOutside');
const content = fixture.debugElement.query(By.css('div[id="content"]'));
content.nativeElement.click();
expect(clickedOutsideSpy).not.toHaveBeenCalled();
});
it('should not docsClickOutside be emitted when user click inside `content` element', () => {
const clickedOutsideSpy = spyOn(component, 'clickedOutside');
const button = fixture.debugElement.query(By.css('button[id="ignoreThisButton"]'));
button.nativeElement.click();
expect(clickedOutsideSpy).not.toHaveBeenCalled();
});
});
@Component({
template: `
<div class="container">
<button type="button" id="exampleButton">Click me</button>
<button type="button" id="ignoreThisButton">Click me</button>
<div
id="content"
(docsClickOutside)="clickedOutside()"
[docsClickOutsideIgnore]="docsClickOutsideIgnore"
>
Content
</div>
</div>
`,
imports: [ClickOutside],
standalone: true,
})
class ExampleComponent {
docsClickOutsideIgnore = ['ignoreThisButton'];
clickedOutside(): void {}
}
@@ -0,0 +1,47 @@
/*!
* @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 {DOCUMENT} from '@angular/common';
import {Directive, ElementRef, EventEmitter, Input, Output, inject} from '@angular/core';
@Directive({
selector: '[docsClickOutside]',
standalone: true,
host: {
'(document:click)': 'onClick($event)',
},
})
export class ClickOutside {
@Input('docsClickOutsideIgnore') public ignoredElementsIds: string[] = [];
@Output('docsClickOutside') public clickOutside = new EventEmitter<void>();
private readonly document = inject(DOCUMENT);
private readonly elementRef = inject(ElementRef<HTMLElement>);
onClick($event: PointerEvent): void {
if (
!this.elementRef.nativeElement.contains($event.target) &&
!this.wasClickedOnIgnoredElement($event)
) {
this.clickOutside.emit();
}
}
private wasClickedOnIgnoredElement($event: PointerEvent): boolean {
if (this.ignoredElementsIds.length === 0) {
return false;
}
return this.ignoredElementsIds.some((elementId) => {
const element = this.document.getElementById(elementId);
const target = $event.target as Node;
const contains = element?.contains(target);
return contains;
});
}
}
@@ -0,0 +1,42 @@
load("//tools:defaults.bzl", "karma_web_test_suite", "ng_module", "ts_library")
package(default_visibility = ["//visibility:private"])
ng_module(
name = "external-link",
srcs = [
"external-link.directive.ts",
],
visibility = [
"//adev/shared-docs/directives:__pkg__",
],
deps = [
"//adev/shared-docs/providers",
"//adev/shared-docs/utils",
"//packages/common",
"//packages/core",
"//packages/router",
],
)
ts_library(
name = "test_lib",
testonly = True,
srcs = glob(
["*.spec.ts"],
),
deps = [
":external-link",
"//adev/shared-docs/providers",
"//packages/core",
"//packages/core/testing",
"//packages/platform-browser",
"//packages/router",
"//packages/router/testing",
],
)
karma_web_test_suite(
name = "test",
deps = [":test_lib"],
)
@@ -0,0 +1,77 @@
/*!
* @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 {Component} from '@angular/core';
import {ExternalLink} from './external-link.directive';
import {RouterLink} from '@angular/router';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {WINDOW} from '../../providers';
import {RouterTestingModule} from '@angular/router/testing';
import {By} from '@angular/platform-browser';
describe('ExternalLink', () => {
let fixture: ComponentFixture<ExampleComponentWithLinks>;
const fakeWindow = {
location: {
origin: window.origin,
},
};
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ExampleComponentWithLinks, RouterTestingModule],
providers: [
{
provide: WINDOW,
useValue: fakeWindow,
},
],
});
fixture = TestBed.createComponent(ExampleComponentWithLinks);
fixture.detectChanges();
});
it('should external link have target=_blank attribute', () => {
const externalLink = fixture.debugElement.query(
By.css('a[href="https://stackoverflow.com/questions/tagged/angular"]'),
);
expect(externalLink.attributes['target']).toEqual('_blank');
});
it('should not internal link have target=_blank attribute', () => {
const internalLink = fixture.debugElement.query(By.css('a[href="/roadmap"]'));
expect(internalLink.attributes['target']).toBeFalsy();
});
it('should not set target=_blank attribute external link when anchor has got noBlankForExternalLink attribute', () => {
const externalLink = fixture.debugElement.query(
By.css('a[href="https://github.com/angular/angular/issues"]'),
);
expect(externalLink.attributes['target']).toBeFalsy();
});
});
@Component({
template: `
<a
href="https://stackoverflow.com/questions/tagged/angular"
title="Stack Overflow: where the community answers your technical Angular questions."
>
Stack Overflow
</a>
<a routerLink="/roadmap" title="Roadmap">Roadmap</a>
<a
href="https://github.com/angular/angular/issues"
title="Post issues and suggestions on github"
noBlankForExternalLink
></a>
`,
imports: [ExternalLink, RouterLink],
standalone: true,
})
class ExampleComponentWithLinks {}
@@ -0,0 +1,45 @@
/*!
* @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 {isPlatformBrowser} from '@angular/common';
import {Directive, ElementRef, OnInit, PLATFORM_ID, inject} from '@angular/core';
import {isExternalLink} from '../../utils/index';
import {WINDOW} from '../../providers/index';
/**
* The directive will set target of anchor elements to '_blank' for the external links.
* We can opt-out this behavior by adding `noBlankForExternalLink` attribute to anchor element.
*/
@Directive({
selector: 'a[href]:not([noBlankForExternalLink])',
host: {
'[attr.target]': 'target',
},
standalone: true,
})
export class ExternalLink implements OnInit {
private readonly anchor: ElementRef<HTMLAnchorElement> = inject(ElementRef);
private readonly platformId = inject(PLATFORM_ID);
private readonly window = inject(WINDOW);
target?: '_blank' | '_self' | '_parent' | '_top' | '';
ngOnInit(): void {
this.setAnchorTarget();
}
private setAnchorTarget(): void {
if (!isPlatformBrowser(this.platformId)) {
return;
}
if (isExternalLink(this.anchor.nativeElement.href, this.window.location.origin)) {
this.target = '_blank';
}
}
}
+10
View File
@@ -0,0 +1,10 @@
/*!
* @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
*/
export * from './click-outside/click-outside.directive';
export * from './external-link/external-link.directive';
@@ -0,0 +1,18 @@
load("//tools:defaults.bzl", "ng_module")
package(default_visibility = ["//visibility:private"])
ng_module(
name = "search-item",
srcs = [
"search-item.directive.ts",
],
visibility = [
"//adev/shared-docs/directives:__pkg__",
],
deps = [
"//adev/shared-docs/interfaces",
"//packages/core",
"@npm//@angular/cdk",
],
)
@@ -0,0 +1,51 @@
/*!
* @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 {Directive, ElementRef, Input, inject, signal} from '@angular/core';
import {Highlightable} from '@angular/cdk/a11y';
import {SearchResult} from '../../interfaces/search-results';
@Directive({
selector: '[docsSearchItem]',
standalone: true,
host: {
'[class.active]': 'isActive',
},
})
export class SearchItem implements Highlightable {
@Input() item?: SearchResult;
@Input() disabled = false;
private readonly elementRef = inject(ElementRef<HTMLLIElement>);
private _isActive = signal(false);
protected get isActive() {
return this._isActive();
}
setActiveStyles(): void {
this._isActive.set(true);
}
setInactiveStyles(): void {
this._isActive.set(false);
}
getLabel(): string {
if (!this.item?.hierarchy) {
return '';
}
const {hierarchy} = this.item;
return `${hierarchy.lvl0}${hierarchy.lvl1}${hierarchy.lvl2}`;
}
scrollIntoView(): void {
this.elementRef?.nativeElement.scrollIntoView({block: 'nearest'});
}
}
+11
View File
@@ -0,0 +1,11 @@
package(default_visibility = ["//visibility:private"])
filegroup(
name = "icons",
srcs = glob([
"*.svg",
]),
visibility = [
"//adev/shared-docs:__pkg__",
],
)
+3
View File
@@ -0,0 +1,3 @@
<svg width="10" height="7" viewBox="0 0 10 7" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M-5.09966e-08 1.83341L1.16667 0.666748L5 4.50008L8.83333 0.666748L10 1.83341L5 6.83341L-5.09966e-08 1.83341Z" fill="#746E7C"/>
</svg>

After

Width:  |  Height:  |  Size: 239 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.59948 19.0428C7.59948 18.8069 7.59118 18.182 7.58656 17.3526C4.89071 17.9526 4.32164 16.0201 4.32164 16.0201C3.88087 14.8718 3.24533 14.5663 3.24533 14.5663C2.36518 13.9492 3.31179 13.9621 3.31179 13.9621C4.28471 14.0323 4.79656 14.9868 4.79656 14.9868C5.66102 16.5052 7.06456 16.0672 7.61748 15.8125C7.70564 15.17 7.95579 14.732 8.23271 14.4837C6.08056 14.2331 3.81764 13.3801 3.81764 9.57199C3.81764 8.48737 4.19564 7.6003 4.81548 6.90522C4.71625 6.65414 4.38302 5.64384 4.91056 4.27537C4.91056 4.27537 5.72471 4.00814 7.57594 5.29399C8.34856 5.07384 9.17795 4.96307 10.0027 4.95937C10.8256 4.96307 11.6546 5.07384 12.429 5.29399C14.2793 4.00814 15.0921 4.27537 15.0921 4.27537C15.621 5.64337 15.2883 6.65368 15.1881 6.90522C15.8093 7.6003 16.1841 8.48737 16.1841 9.57199C16.1841 13.3898 13.9179 14.2298 11.7589 14.4758C12.1073 14.7828 12.4166 15.3892 12.4166 16.3165C12.4166 17.6452 12.4041 18.7174 12.4041 19.0428C12.4041 19.3091 12.579 19.6178 13.071 19.5205C16.9193 18.2041 19.6936 14.4814 19.6936 10.0926C19.6936 4.60353 15.3538 0.154297 10.0009 0.154297C4.64887 0.154297 0.309021 4.60353 0.309021 10.0926C0.309483 14.4828 3.08656 18.2078 6.9381 19.5218C7.42225 19.6128 7.59948 19.3058 7.59948 19.0428Z" fill="#A39FA9"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.04145 0.04432l6.56351 8.77603L0 15.95564h1.48651l5.78263-6.24705 4.6722 6.24705h5.05865l-6.9328-9.26967L16.21504.04432h-1.48651l-5.32552 5.75341L5.1001.04432H.04145Zm2.18602 1.09497h2.32396l10.26221 13.72122h-2.32396L2.22747 1.13928Z" fill="#A39FA9"/>
</svg>

After

Width:  |  Height:  |  Size: 367 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="20" height="15" viewBox="0 0 20 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.7556 2.94783C18.5803 1.98018 17.745 1.27549 16.7756 1.05549C15.325 0.747832 12.6403 0.527832 9.73563 0.527832C6.83266 0.527832 4.105 0.747832 2.65266 1.05549C1.685 1.27549 0.847969 1.93549 0.672656 2.94783C0.495625 4.04783 0.320312 5.58783 0.320312 7.56783C0.320312 9.54783 0.495625 11.0878 0.715625 12.1878C0.892656 13.1555 1.72797 13.8602 2.69563 14.0802C4.23563 14.3878 6.87563 14.6078 9.78031 14.6078C12.685 14.6078 15.325 14.3878 16.865 14.0802C17.8327 13.8602 18.668 13.2002 18.845 12.1878C19.0203 11.0878 19.2403 9.50314 19.285 7.56783C19.1956 5.58783 18.9756 4.04783 18.7556 2.94783ZM7.36031 10.6478V4.48783L12.728 7.56783L7.36031 10.6478Z" fill="#A39FA9"/>
</svg>

After

Width:  |  Height:  |  Size: 782 B

+9
View File
@@ -0,0 +1,9 @@
load("//adev/shared-docs/pipeline:_guides.bzl", _generate_guides = "generate_guides")
load("//adev/shared-docs/pipeline:_stackblitz.bzl", _generate_stackblitz = "generate_stackblitz")
load("//adev/shared-docs/pipeline:_playground.bzl", _generate_playground = "generate_playground")
load("//adev/shared-docs/pipeline:_tutorial.bzl", _generate_tutorial = "generate_tutorial")
generate_guides = _generate_guides
generate_stackblitz = _generate_stackblitz
generate_playground = _generate_playground
generate_tutorial = _generate_tutorial
+20
View File
@@ -0,0 +1,20 @@
/*!
* @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
*/
/*
* Public API Surface of shared
*/
export * from './directives/index';
export * from './components/index';
export * from './constants/index';
export * from './interfaces/index';
export * from './providers/index';
export * from './services/index';
export * from './utils/index';
export * from './testing/index';
+32
View File
@@ -0,0 +1,32 @@
load("//tools:defaults.bzl", "ts_library")
package(default_visibility = ["//visibility:private"])
ts_library(
name = "interfaces",
srcs = [
"index.ts",
],
visibility = ["//adev/shared-docs:__subpackages__"],
deps = [
":lib",
],
)
ts_library(
name = "lib",
srcs = glob(
[
"**/*.ts",
],
exclude = [
"index.ts",
"**/*.spec.ts",
],
),
deps = [
"//packages/core",
"@npm//@types/node",
"@npm//@webcontainer/api",
],
)

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