Compare commits

...

108 Commits

Author SHA1 Message Date
Alex Rickabaugh 1853bbb061 release: cut the v18.2.2 release 2024-08-28 14:32:26 -07:00
Alex Rickabaugh 3ecf620cc5 Revert "fix(http): Dynamicaly call the global fetch implementation (#57531)" (#57571)
This reverts commit 21445a2932.

Reason: failing test

PR Close #57571
2024-08-28 12:42:27 -07:00
Paul Gschwendtner 36aa3af77b refactor(migrations): forward-fix conflicts with generic of ProgramInfo. (#57565)
One PR removed the generic of `ProgramInfo`, while another PR,
separately merged, introduced new instanes of `ProgramInfo`.

PR Close #57565
2024-08-28 09:17:34 -07:00
AleksanderBodurri adf32746c2 fix(devtools): correctly set environment injector path in the case where there are no element injectors (#57442)
Previously, when "Hide injectors with no providers" was toggled, it is possible for the injector tree visualizer to have no Element injectors to visualize. This caused a bug in the slicing logic that splits apart the environment and element injectors from DI resolution paths within the injector tree component in Angular DevTools.

Now, this logic is correctly handled when there are no element injectors to visualize.

PR Close #57442
2024-08-28 08:49:34 -07:00
Matthieu Riegler 5501e9c8ed docs: remove @searchKeywords (#57560)
This tag is not supported.

fixes #57540

PR Close #57560
2024-08-28 08:49:05 -07:00
Paul Gschwendtner 389933f079 refactor(migrations): allow tsurge migrations to run with just NgCompiler (#57562)
This is important so that migrations can easily be wired up in the
language service where only `NgCompiler` is available.

PR Close #57562
2024-08-28 08:47:46 -07:00
Jeremy Elbourn 674aed5984 docs: cover time-based statements in docs authoring guide (#57536)
Adds some additional guidance on writing content that is tied to a
specific version or point in time.

PR Close #57536
2024-08-28 08:45:10 -07:00
Paul Gschwendtner b14c864170 refactor(migrations): properly handle multi query migration (#57556)
Properly handles queries with multiple results, by extracting the
type from the `QueryList`.

Also adds more tests and handles imports.

PR Close #57556
2024-08-28 08:43:39 -07:00
Paul Gschwendtner 10fce207e0 refactor(migrations): initial migration logic for converting to signal queries (#57556)
Adds initial migration logic to convert decorator query declarations to
signal queries.

We will re-use more part of the signal input migration in follow-ups, to
properly migrate, and e.g. even handle control flow

PR Close #57556
2024-08-28 08:43:38 -07:00
Alex Rickabaugh eba3a0ade0 Revert "refactor(migrations): initial migration logic for converting to signal queries (#57525)" (#57555)
This reverts commit 6f5b435a69.

Reason: breaks g3

PR Close #57555
2024-08-27 14:03:06 -07:00
Alex Rickabaugh 3d48a72193 Revert "refactor(migrations): properly handle multi query migration (#57525)" (#57555)
This reverts commit f454ad3bcf.

Reason: breaks g3

PR Close #57555
2024-08-27 14:03:06 -07:00
Matthieu Riegler 5d2e243c76 fix(http): Dynamicaly call the global fetch implementation (#57531)
Instead of using the reference that existing when `FetchBackend` is setup.

fixes #57527

PR Close #57531
2024-08-27 13:34:19 -07:00
Andrew Scott 5e9661d69c fix(docs-infra): Fix scrolling in application (#57554)
Since the change to the after render hooks, the "this" context must be
correct for the destroy to work.

fixes #57552

PR Close #57554
2024-08-27 13:32:14 -07:00
Kristiyan Kostadinov df42d2be16 test: avoid leaking some LViews in tests (#57546)
We had some tests that were leaking LViews, because we were testing things like `createComponent`, but not destroying them afterwards. These changes clean up most of them, although there are a handful still left that I didn't have time to fully track down.

PR Close #57546
2024-08-27 13:29:09 -07:00
Kristiyan Kostadinov 106917af87 fix(core): avoid leaking memory if component throws during creation (#57546)
When we create the LView for a component, we track it in the `TRACKED_LVIEWS` map. It gets untracked when it is destroy, but if it throws during creation, the user won't have access to a `ComponentRef` in order to clean it up.

These changes automatically untrack the related LViews if the component couldn't be created.

PR Close #57546
2024-08-27 13:29:09 -07:00
Alan Agius b74b5f0c06 build: remove Sourcemaps from Schematics bundles (#57545)
This commit reduces the bundle size of `@angular/core` by 19.5 MB by excluding sourcemaps from the schematics.

Since the `esbuild` rule doesn't allow disabling sourcemaps directly, we work around this by setting the sourcemaps to `external`. Afterward, we filter the output to include only the `.js` files.

PR Close #57545
2024-08-27 13:27:33 -07:00
Angular Robot 4231404065 build: update github/codeql-action action to v3.26.5 (#57543)
See associated pull request for more information.

PR Close #57543
2024-08-27 13:25:58 -07:00
Angular Robot adcdfeca16 build: update dependency @babel/generator to v7.25.5 (#57542)
See associated pull request for more information.

PR Close #57542
2024-08-27 13:24:59 -07:00
Andrew Kushnir 4f8c406664 refactor(compiler): extend directive mock to avoid failing at matching logic (#57537)
This commit updates a directive mock instance to include an extra field that a compiler code was expecting, which caused issues while processing elements with local refs and exported directives.

PR Close #57537
2024-08-27 13:23:30 -07:00
Paul Gschwendtner 206b7be7f7 ci: fix fw-testing group triggering for schematics testing changes (#57526)
The `fw-testing` group should not trigger for schematic changes.

PR Close #57526
2024-08-27 13:21:48 -07:00
Andrew Scott 804925b114 fix(router): Do not unnecessarily run matcher twice on route matching (#57530)
This commit makes a small update to the route matching algorithm to
avoid running the matcher function of a route twice.

fixes #57511

PR Close #57530
2024-08-27 13:20:58 -07:00
Paul Gschwendtner 6a953c6ed4 refactor(migrations): properly handle multi query migration (#57525)
Properly handles queries with multiple results, by extracting the
type from the `QueryList`.

Also adds more tests and handles imports.

PR Close #57525
2024-08-27 13:20:19 -07:00
Paul Gschwendtner 01cae95596 refactor(migrations): initial migration logic for converting to signal queries (#57525)
Adds initial migration logic to convert decorator query declarations to
signal queries.

We will re-use more part of the signal input migration in follow-ups, to
properly migrate, and e.g. even handle control flow

PR Close #57525
2024-08-27 13:20:19 -07:00
AleksanderBodurri 371ad098d5 fix(devtools): ignore DOM Nodes from other frames when performing render tree detection (#57518)
Previously, if an application had DOM Nodes injected into it from other frames, DevTools would fail to parse component trees with the render tree strategy properly because of an instanceof Node check that the framework performs.

Now we check for instanceof Node before even calling framework debug APIs on DOM nodes so that we can skip nodes that come from other frames entirely.

PR Close #57518
2024-08-27 13:19:35 -07:00
Angular Robot 73ad6ea647 build: update dependency jsdom to v25 (#57514)
See associated pull request for more information.

PR Close #57514
2024-08-27 13:17:39 -07:00
Matthieu Riegler d3366815ee refactor(animations): Add loading strategy for the Async Animations (#57493)
In some cases apps need to schedule a bit later the loading of the animation module. This private token will allow to investigate which other strategy could be useful.

PR Close #57493
2024-08-27 13:15:51 -07:00
Doug Parker 76ec60d73c refactor(compiler): ensure context is always provided for WhitespaceVisitor (#56507)
When disabling `i18nPreserveSignificantWhitespaceForLegacyExtraction` I was looking at a test case with ICU messages containing leading and trailing whitespace:

```angular
<div i18n>
  {apples, plural, =other {I have many apples.}}
</div>
```

This would historically generate two messages:

```javascript
const MSG_TMP = goog.getMsg('{apples, plural, =other {I have many apples.}}');
const MSG_FOO = goog.getMsg(' {$ICU} ', { 'ICU': MSG_TMP });
```

But I found that I was getting just one message:

```javascript
const MSG_TMP = goog.getMsg(' {apples, plural, =other {I have many apples.}} ');
```

This is arguably an improvement, but changed the messages and message IDs, which isn't desirable with this option. I eventually traced this back to the `isIcu` initialization in [`i18n_parser.ts`](/packages/compiler/src/i18n/i18n_parser.ts):

```typescript
const context: I18nMessageVisitorContext = {
  isIcu: nodes.length == 1 && nodes[0] instanceof html.Expansion,
  // ...
};
```

[`_I18nVisitor.prototype.visitExpansion`](/packages/compiler/src/i18n/i18n_parser.ts) uses this to decide whether or not to generate a sub-message for a given ICU expansion:

```typescript
if (context.isIcu || context.icuDepth > 0) {
  // Returns an ICU node when:
  // - the message (vs a part of the message) is an ICU message, or
  // - the ICU message is nested.
  const expPh = context.placeholderRegistry.getUniquePlaceholder(`VAR_${icu.type}`);
  i18nIcu.expressionPlaceholder = expPh;
  context.placeholderToContent[expPh] = {
    text: icu.switchValue,
    sourceSpan: icu.switchValueSourceSpan,
  };
  return context.visitNodeFn(icu, i18nIcu);
}

// Else returns a placeholder
// ICU placeholders should not be replaced with their original content but with the their
// translations.
// TODO(vicb): add a html.Node -> i18n.Message cache to avoid having to re-create the msg
const phName = context.placeholderRegistry.getPlaceholderName('ICU', icu.sourceSpan.toString());
context.placeholderToMessage[phName] = this.toI18nMessage([icu], '', '', '', undefined);
const node = new i18n.IcuPlaceholder(i18nIcu, phName, icu.sourceSpan);
return context.visitNodeFn(icu, node);
```

Note that `isIcu` is the key condition between these two cases and depends on whether or not the ICU expansion has any siblings. The introduction of `WhitespaceVisitor` to `I18nMetaVisitor` trims insignificant whitespace, including empty text nodes not adjacent to an ICU expansion (from [`WhitespaceVisitor.prototype.visitText`](/packages/compiler/src/ml_parser/html_whitespaces.ts)):

```typescript
const isNotBlank = text.value.match(NO_WS_REGEXP);
const hasExpansionSibling =
  context && (context.prev instanceof html.Expansion || context.next instanceof html.Expansion);

if (isNotBlank || hasExpansionSibling) {
  // Transform node by trimming it...
  return trimmedNode;
}

return null; // Drop node which is empty and has no ICU expansion sibling.
```

`hasExpansionSibling` was intended to retain empty text nodes leading or trailing an ICU expansion, however `context` was `undefined`, so this check failed and the leading / trailing text nodes were dropped. This resulted in trimming the ICU text by dropping the leading / trailing whitespace nodes. Having only a single ICU expansion with no leading / trailing text nodes caused `_I18nVisitor` to initialize `isIcu` incorrectly and caused it to generate one message instead of two.

`WhitespaceVisitor` is supposed to get this context from `visitAllWithSiblings`. So the fix here is to make sure `WhitespaceVisitor` is always visited via this function which provides the required context. I updated all usage sites to make sure this context is use consistently and implemented the `WhitespaceVisitor.prototype.visit` method to throw when the context is missing to make sure we don't encounter a similar mistake in the future.

Unfortunately this broke one compliance test. Specifically the [`icu_logic/icu_only.js`](/home/douglasparker/Source/ng/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/icu_only.js) test which changed from generating:

```javascript
function MyComponent_Template(rf, ctx) {
  if (rf & 1) {
    i0.ɵɵi18n(0, 0);
  }
  // ...
}
```

To now generating:

```javascript
function MyComponent_Template(rf, ctx) {
  if (rf & 1) {
    i0.ɵɵtext(0, " ");
    i0.ɵɵi18n(1, 0);
    i0.ɵɵtext(2, "\n");
  }
  // ...
}
```

This test uses the default value `preserveWhitespaces: false` (`i18nPreserveSignificantWhitespaceForLegacyExtraction` should not affect compiled JS output, we already retain significant whitespace there). So what this indicates to me is that ICU logic is already broken because it's not preserving significant whitespace in this case. My change is probably a bug fix, but one which would affect the compiled runtime, which is not in scope here. The root cause is because using `visitAllWithSiblings` everywhere means the context is retained correctly in this case and the whitespace is leading/trailing an ICU message, therefore it is retained per the logic of `WhitespaceVisitor.prototype.visitText` I mentioned eariler.

To address this, I left one usage of `WhitespaceVisitor` using `html.visitAll` instead of `visitAllWithSiblings` to retain this bug. I has to lossen the assertion I put in `WhitespaceVisitor.prototype.visit` to make this possible, but it should still throw by default when misused, which is the important part.

PR Close #56507
2024-08-27 13:13:57 -07:00
Doug Parker 7bfe81700b refactor(compiler): add i18nPreserveWhitespaceForLegacyExtraction (#56507)
This configures whether or not to preserve whitespace content when extracting messages from Angular templates in the legacy (View Engine) extraction pipeline.

This includes several bug fixes which unfortunately cannot be landed without changing message IDs in a breaking fashion and are necessary to properly trim whitespace. Instead these bug fixes are included only when the new flag is disabled.

PR Close #56507
2024-08-27 13:13:57 -07:00
Michael van der Luit 3067633bc7 docs(docs-infra): Add query as an url query param to the api reference (#57062)
add an url query param to perform a search on api references directly via the url

PR Close #57062
2024-08-27 12:58:00 -07:00
AleksanderBodurri be82f282a4 fix(devtools): catch invalidated extension error to prevent devtools from spamming console (#55697)
When a browser extension is updated it becomes invalidated on currently open pages. If that extension then tries to send a message to those pages through `chrome.runtime.sendMessage(..)` then an error is thrown in the console

For Angular DevTools, this results in spamming the console with "Uncaught Error: Extension context invalidated." errors.

This commit catches that error and removes the event listener that triggers the `chrome.runtime.sendMessage(...)` call.

PR Close #55697
2024-08-27 12:54:44 -07:00
realstealthninja 03b35270b0 docs: fix typo in recommendations.ts (#57521)
PR Close #57521
2024-08-26 09:17:44 -07:00
Matthieu Riegler fe9005ce8e docs: fix formating in class-binding (#57447)
fixes #57430

PR Close #57447
2024-08-26 09:12:55 -07:00
Bjarki 03ec620e31 fix(upgrade): Address Trusted Types violations in @angular/upgrade (#57454)
Angular applications that are AngularJS hybrids are currently unable to
adopt Trusted Types due to violations eminating from an innerHTML
assignment in the @angular/upgrade package. This commit allows
developers of such applications to optionally ignore this class of
violations by configuring the Trusted Types header to allow the new
angular#unsafe-upgrade policy.

Note that the policy is explicitly labeled as unsafe as it does not in
any way mitigate the security risk of using AngularJS in an Angular
application, but does unblock Trusted Types adoption enabling XSS
protection for other parts of the application.

The implementation follows the approach taken in @angular/core;
see packages/core/src/util/security.

PR Close #57454
2024-08-26 09:04:48 -07:00
Thomas Nguyen 6d3a2af146 fix(core): Do not bubble capture events. (#57476)
These should only fire if the target is the same as the targetElement. Also, delete an out of date test since capture/non-capture tests are separately covered.

PR Close #57476
2024-08-23 14:46:55 -07:00
Paul Gschwendtner 223b7857ef refactor(migrations): replacements in tsurge are always serializable (#57501)
This removes an unnecessary type wrapping that can be removed because
`Replacements` is expected to be serializable by default.

Similar to how it's done in `TsurgeComplexMigration`.

PR Close #57501
2024-08-23 13:49:31 -07:00
Angular Robot 5bdae0387f build: update dependency mermaid to v11 (#57499)
See associated pull request for more information.

PR Close #57499
2024-08-23 13:00:59 -07:00
Peter 034b32c133 docs: fix typo of ChangeDetectionStrategy in the zoneless guide (#57482)
PR Close #57482
2024-08-23 12:59:09 -07:00
Kristiyan Kostadinov 2a89e184e0 fix(zone.js): more robust check for promise-like objects (#57388)
Fixes that Zone.js wasn't checking properly if an object is promise-like.

Fixes #57385.

PR Close #57388
2024-08-23 12:58:29 -07:00
Santosh 9b6865ec03 docs: add test for i18n code block and fix rendering (#57210)
PR Close #57210
2024-08-23 12:57:15 -07:00
Alex Rickabaugh 6059ca8f1f refactor(core): restructure AfterRenderManager to connect related phases (#57453) (#57504)
The `afterRender` infrastructure was first implemented around the idea of
independent, singular hooks. It was later updated to support a spec of
multiple hooks that pass values from one to another as they execute, but the
implementation still worked in terms of singular hooks under the hood. This
creates a number of maintenance issues, and a few bugs. For example, when
one hook fails, further hooks in the pipeline should no longer execute, but
this was hard to ensure under the old design.

This refactoring restructures `afterRender` infrastructure significantly to
introduce the concept of a "sequence", a collection of hooks of different
phases that execute together. Overall, the implementation is simplified
while making it more resilient to issues and future use cases, such as the
upcoming `afterRenderEffect`.

As part of this refactoring, the `internalAfterNextRender` concept is
removed, as well as the unused `queueStateUpdate` concept which used it.

PR Close #57453

PR Close #57504
2024-08-23 12:48:01 -07:00
Alex Rickabaugh 800f6c8ca3 refactor(core): track dirtiness bits in ApplicationRef (#57453) (#57504)
Previously the zoneless scheduler had a concept of whether views needed to
be refreshed or not, based on the notification type that was received. It
tracked this information as a boolean.

This commit refactors things to track dirtiness in `ApplicationRef` itself,
as a `dirtyFlags` field with bits corresponding to either view tree
dirtiness or after-render hooks.

PR Close #57453

PR Close #57504
2024-08-23 12:48:01 -07:00
Paul Gschwendtner 87c594b90b refactor(migrations): add support for simpler variant of tsurge migration (#57484)
Introduces a simpler, smaller variant of the current `Tsurge` migration
class. The difference is simply that for the migration phase (the third
stage), some migrations do not need a full set of workers re-analyzing
every compilation unit again to compute the "final migration
replacements".

This can be the case, for example, if a migration eagerly computes all
replacements in the analyze stage, visiting every unit, and then after
deriving the global metadata, problematic replacements are simply
filtered (e.g. via some unique IDs again).

PR Close #57484
2024-08-23 12:01:59 +00:00
Paul Gschwendtner f133489a16 refactor(migrations): clean up unused google3 code from signal input migration (#57484)
The code was replaced by the automatic Tsurge batch runner. This commit
cleans up the now unused code.

PR Close #57484
2024-08-23 12:01:59 +00:00
Otmane 09deb24d74 docs: fix indentation of file tree in app-shell (#57436)
This commit adds indentations to the file tree section in `app-shell.md`, it makes the tree easier to read. The indentation is now two spaces instead of no spaces.

PR Close #57436
2024-08-22 09:11:50 -07:00
Paweł Kubiak f1bc6f895b docs(docs-infra): add top level banner component (#57458)
docs(docs-infra): add top level banner component

- create top level banner component
- write unit tests
- close banner and keep state in the local storage
- fix: support screens of tablets and phones

PR Close #57458
2024-08-22 09:06:47 -07:00
Matthieu Riegler 5f95afb331 docs: new date for v19 (#57461)
PR Close #57461
2024-08-22 09:04:41 -07:00
Andrew Kushnir b30bc4531a refactor(compiler): create an internal util method to detect matching directives and pipes (#57466)
This commit adds an internal util method that allows to detect:

* which selectors are matching nodes in a template
* which pipes are present in a template

Both directives and pipes are split into 2 buckets: eagerly used and the ones that might potentially be defer-loaded.

PR Close #57466
2024-08-22 09:04:04 -07:00
Dylan Hunn a6225a6cdd release: cut the v18.2.1 release 2024-08-22 09:00:21 -07:00
Matthieu Riegler 69b20d8391 docs(docs-infra): fix regressions around members & deprecations (#57465)
This commit fixes regressions introduced by #57255

PR Close #57465
2024-08-21 11:26:27 -07:00
steciuk 6089772fe8 docs: remove extra space in DI example code (#57472)
PR Close #57472
2024-08-21 11:25:54 -07:00
Angular Robot 1a5f5ee5ce build: update github/codeql-action action to v3.26.3 (#57455)
See associated pull request for more information.

PR Close #57455
2024-08-20 13:20:01 -07:00
Angular Robot 76808cc328 build: update dependency saucelabs to v8 (#57413)
See associated pull request for more information.

PR Close #57413
2024-08-20 13:19:11 -07:00
Adam Koch 980c64abaa docs: remove incorrect documentation around the use of timezone abbreviations (#57425)
The ability to use an abbreviated timezone was removed in v5 (issue https://github.com/angular/angular/issues/20225 discusses this) and no alternative has been merged. See issue https://github.com/angular/angular/issues/40865 and PR https://github.com/angular/angular/pull/48482 The text suggests there is a way to pass in a timezone other than by an offset but there isn't.

PR Close #57425
2024-08-20 10:51:37 -07:00
Paul Gschwendtner df3e9c1661 refactor(migrations): leverage tsurge for signal input migration (#57451)
This commit simplifies the batching support for the signal input
migration by using the new tsurge framework we've built.

This allows for consistent setup across all possible entry-points and
also simplifies the 1P setup given that we can simply use the Tsurge
macros, instead of having to maintain our own Go-based runner.

PR Close #57451
2024-08-19 22:44:43 -07:00
Paul Gschwendtner cb7d817edd refactor(migrations): improve generic assignability in tsurge and pass more info (#57451)
* Improves some of the generic assignability for tsurge. Anything is
  allowed to be returned from an overridden `prepareProgram` method.
  This is useful for the signal input migration.
* Passes the absolute root paths to migrations. This is helpful for the
  signal input migration and there is no other way to access it. It's
  better to pass specifically, compared to passing the whole unsafe
  `ParsedConfiguration` object.

PR Close #57451
2024-08-19 22:44:43 -07:00
Angular Robot 8645152f8c build: update dependency ngx-progressbar to v12 (#57452)
See associated pull request for more information.

Closes #57443 as a pr takeover

PR Close #57452
2024-08-19 22:43:49 -07:00
Matthieu Riegler 0979ca0354 docs(docs-infra): Fix heading parsing (#57386)
fixes #57384

PR Close #57386
2024-08-19 09:24:08 -07:00
Matthieu Riegler af20b2a161 docs(docs-infra): remove remaining ngZone (#57229)
PR Close #57229
2024-08-19 09:22:01 -07:00
Matthieu Riegler b5719ba5eb docs(docs-infra): Add tests for marked rendering (#57344)
On top of #57338, to make sure we prevent similar regressions on marked rendering.

PR Close #57344
2024-08-19 09:21:17 -07:00
Michael Derfler c4901d4dfe docs: fix links to docs (#57391)
PR Close #57391
2024-08-19 09:20:16 -07:00
Andrew Kushnir 1682c60938 docs: add eager to lazy route migration (#57421)
PR Close #57421
2024-08-19 09:19:43 -07:00
Andrew Kushnir c65ab136bf docs: cleanup existing migration docs for consistency (#57421)
PR Close #57421
2024-08-19 09:19:43 -07:00
Andrew Kushnir c89be37193 docs: drop references to removed typed-forms migration (#57421)
PR Close #57421
2024-08-19 09:19:43 -07:00
Andrew Kushnir f723288adf docs: drop references to removed module-with-providers migration (#57421)
PR Close #57421
2024-08-19 09:19:42 -07:00
Joey Perrott 58e00deb34 fix(docs-infra): leverage http_server rule from @angular/build-tooling for adev local serving (#57428)
Use the http_server rule to serve adev locally to allow for slightly faster rebuilds and livereload on changes

PR Close #57428
2024-08-19 09:18:21 -07:00
Paul Gschwendtner 57db366522 refactor(migrations): framework to build batchable migrations (#57396)
Introduces a migration framework to build batchable migrations that
can run in Large Scale mode against e.g. all of Google, using workers.

This is the original signal input migration infrastructure extracted
into a more generic framework that we can use for writing additional
ones for output, signal queries etc, while making sure those are not
scoped to a single `ts.Program` that limits them to per-directory
execution in very large projects (e.g. G3).

The migration will be updated to use this, and in 1P we will add
helpers to easily integrate such migrations into a Go-based pipeline
runner.

PR Close #57396
2024-08-17 08:54:16 -05:00
Andrew Kushnir 3e6ee7eaff docs: update CHANGELOG to drop a breaking change note from 18.2.0 version (#57420)
This commit updates the CHANGELOG to remove an entry about a breaking change in Zone.js, since the change was not actually released. It will be released separately in Zone.js package.

PR Close #57420
2024-08-16 11:20:46 -04:00
Andrew Kushnir 086b754cef docs: add withEventReplay and withI18nSupport links to provideClientHydration docs (#57382)
PR Close #57382
2024-08-15 16:10:28 -04:00
vladboisa 4bd9ba714c docs(docs-infra): move link tag for correct view (#57395)
Move the link tag to the down, for correctly parsing of '@link'

Fixes #57332

PR Close #57395
2024-08-15 15:51:53 -04:00
cexbrayat 3b63082384 fix(migrations): avoid migrating route component in tests (#57317)
The migration was migrating all files in a project (like most migrations).
As there is no gain in migrating components used in test files. Excluding the test files reduces the migration noise.

PR Close #57317
2024-08-15 15:51:22 -04:00
David LJ 25bc810f83 docs: update version table for 18.2 (#57414)
PR Close #57414
2024-08-15 15:50:54 -04:00
Andrew Scott 9de30a7b1c fix(core): Allow zoneless scheduler to run inside fakeAsync (#56932)
The zoneless scheduler callback was executed in the root zone rather
than simply in `runOutsideAngular` to allow us to land the hybrid mode
change detection (scheduler always enabled, even for zones) without
breaking a ton of existing `fakeAsync` tests that could/would fail with
the "timer(s) still in queue" error. However, this caused another
problem: when a test executes inside `fakeAsync`, it cannot flush the
scheduled time. A similar problem exists with event and run coalescing (#56767).
This change would allow `fakeAsync` to flush the zoneless-scheduled
change detections and minimize breaking existing tests
by flushing pending timers at the end of the test, which actually now
matches what's done internally.

PR Close #56932
2024-08-15 12:32:24 -04:00
Matthieu Riegler b1ed7e2b5f docs(docs-infra): Add support for function/method overloads (#57379)
PR Close #57379
2024-08-15 12:12:26 -04:00
Kristiyan Kostadinov 6b4357fae4 fix(migrations): preserve type when using inject decorator (#57389)
Updates the migration so that it passes the type as a generic in the case of `@Inject(SOME_TOKEN) foo: SomeType`. This is done for two reasons:
1. It's a fairly common pattern and it ensures that the code can still be compiled.
2. It avoids leaving behind unused imports.

PR Close #57389
2024-08-15 12:11:59 -04:00
Kristiyan Kostadinov 0bb649b8fa fix(migrations): account for members with doc strings and no modifiers (#57389)
Fixes that the migration was duplicating the doc strings of members that don't have modifiers.

PR Close #57389
2024-08-15 12:11:59 -04:00
Andrew Kushnir 286012fb89 fix(core): handle hydration of components that project content conditionally (#57383)
This commit fixes an issue when hydration serialization tries to calculate DOM path to a content projection node (`<ng-content>`), but such nodes do not have DOM representation.

Resolves #56750.

PR Close #57383
2024-08-15 11:22:05 -04:00
Erik Wegner 9155f75e36 docs(common): fix example code (#57406)
Fix example code for interceptor.

PR Close #57406
2024-08-15 10:46:03 -04:00
Jeremy Elbourn 93410eda0a docs: add small clarification to ng-content (#57408)
This expands the recommendation to avoid conditionally showing `<ng-content>` with additional context

PR Close #57408
2024-08-15 10:45:16 -04:00
Andrew Kushnir d49e083999 release: cut the v18.2.0 release 2024-08-14 09:40:36 -07:00
Matthieu Riegler e50d239e80 docs(docs-infra): fix markdown link rendering (#57377)
fixes #57376

PR Close #57377
2024-08-13 15:53:56 -07:00
Thomas Nguyen e30c60e89f 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 8104fc2126 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:57 -07:00
Thomas Nguyen 7accd9d885 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:57 -07:00
Andrew Scott 334c99f968 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
Kristiyan Kostadinov b1a9d0f4de fix(migrations): avoid duplicating comments when generating properties (#57367)
Updates the logic that generates new component properties to avoid duplicating their doc strings.

PR Close #57367
2024-08-13 09:54:25 -07:00
Kristiyan Kostadinov 5d76401ff5 fix(migrations): preserve optional parameters (#57367)
Makes it so the inject migration preserves the optional token when declaring a parameter. This came up in some testing as something that can be potentially breaking for classes that implement interfaces.

PR Close #57367
2024-08-13 09:54:25 -07:00
Andrew Kushnir de85979648 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
Paul Gschwendtner bbc970bb0b refactor(migrations): always add readonly to migrated signal inputs (#57368)
Signal inputs are no longer updated by assignment, unlike `@Input()`, so
a good practice is adding `readonly` for the `InputSignal`— which should
never be swapped out.

This is a safe operation because the migration skips all inputs that are
being written anyway.

PR Close #57368
2024-08-13 09:41:09 -07:00
Paul Gschwendtner 87d00d26ff refactor(migrations): use input() shorthand if possible in input migration (#57368)
In some cases, the migration can detect when `input()` as a shorthand
may be usable. This commit adds such detection and migrates inputs to
this form when possible.

PR Close #57368
2024-08-13 09:41:08 -07:00
Matthieu Riegler 84752069f2 docs(docs-infra): Update marked to 14 (#57363)
This is a backport of the marked update and the fix that went with it (#57338)

PR Close #57363
2024-08-13 09:33:04 -07:00
Angular Robot d3b46ade94 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 1828d11d84 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
Paul Gschwendtner b8c82fa2f7 refactor(migrations): handle jit: true component templates in signal input migration (#57347)
Components with `jit: true` are not processed by the Angular compiler,
so we cannot ask the template checker for the parsed template; simply
because the template wasn't attempted to be parsed.

We still can migrate simple cases of such components, commonly seen in
unit tests. We do this by manually parsing the template and making use
of the reference fallback resolution that is also used for host bindings
(where we don't have any type check block information).

PR Close #57347
2024-08-12 15:17:41 -07:00
Paul Gschwendtner fa77c9e5b4 refactor(migrations): expose angular compiler options to signal input migration (#57347)
Instead of exposing just the `ts.CompilerOptions`, we should expose the
actual Angular compiler options throuhgout the signal input migration.

This will be useful for parsing templates, in cases of JIT-opted
components.

PR Close #57347
2024-08-12 15:17:40 -07:00
Paul Gschwendtner 5d16c286bf refactor(migrations): handle safe property reads in signal input migration (#57318)
As of this commit, the migration will also inspect safe property reads
and migrate them, if they reference an input that is being migrated.

PR Close #57318
2024-08-12 12:12:23 -07:00
Paul Gschwendtner 604270619a perf(migrations): speed up signal input migration by combining two analyze phases (#57318)
Instead of revisiting each source file, and each of its child nodes
twice, we now visit them together using a grouped AST visitor that only
traverses each source file once.

This seemed to speed up migration by 6-8% locally, but is likely
noticable better with large compilation scopes.

PR Close #57318
2024-08-12 12:12:23 -07:00
Paul Gschwendtner 1b546975f0 refactor(migrations): use import manager in signal input migration (#57318)
Instead of fiddling manually with the imports, which worked well, but
comes at a cost of complexity— we are now using the canonical import
manager. This simplifies deletion, insertion and updating of imports.

Notably, our import manager is not super great at preserving whitespaces
right now, but we assume a formatter runs over migrated code anyway.

PR Close #57318
2024-08-12 12:12:23 -07:00
Paul Gschwendtner 16ae748257 refactor(migrations): add best effort mode to signal input migration (#57318)
Introduces a best effort mode for the signal input migration. This mode
can be used to aggresively migrate as much as possible, ignoring most
of the incompatibility reasons, like "writes to the input".

PR Close #57318
2024-08-12 12:12:22 -07:00
Paul Gschwendtner ecb0f8f161 refactor(migrations): add initial docs for signal input migration incompatibility reasons (#57318)
Adds a markdown document capturing some of the incompatibilty reasons
on why the input wasn't migrated.

PR Close #57318
2024-08-12 12:12:22 -07:00
Gilad Mautner cea3e4b594 docs: reorder afterView and afterContent related lifecycle hooks (#57307)
PR Close #57307
2024-08-09 13:55:39 -07:00
Paul Gschwendtner 448279f57e refactor(migrations): detect if an input is not narrowed and can be migrated (#57308) (#57323)
By default, we don't migrate inputs if they are part of e.g. `@if` for
now. That is because we don't have the template narrowing feature
available yet.

To improve impact of the migration until we have the narrowing, we add
some additional checks that allow us to migrate instances of inputs that
are part of e.g. `@if` but are actually not used inside (and hence are
guaranteed to be _not_ narrowed).

PR Close #57308

PR Close #57323
2024-08-09 12:17:34 -07:00
Andrew Kushnir 5558e275ee 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:49 -07:00
Andrew Kushnir 86216792fd 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:13 -07:00
Kristiyan Kostadinov cab6c23602 refactor(migrations): add internal cleanup logic (#57315)
Expands the `inject` migration to add some cleanups that are only relevant internally. Externally this isn't exposed to users.

PR Close #57315
2024-08-09 08:02:34 -07:00
Andrew Scott 296216cbe1 fix(core): Allow hybrid CD scheduling to support multiple "Angular zones" (#57267)
This commit updates the inside/outside NgZone detection of the hybrid CD
scheduling to track the actual instance of the NgZone being used rather
than the name "Angular" (how `isInsideAngularZone` works). This allows
the scheduling to work correctly when there are multiple versions of
Angular running on the page.

fixes #57261

PR Close #57267
2024-08-08 10:46:26 -07:00
Thomas Nguyen 9af760eb51 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 b16dd6d67f 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:13 -07:00
Jessica Janiuk 02d613ef9c release: cut the v18.2.0-rc.0 release 2024-08-07 12:27:07 -07:00
321 changed files with 15168 additions and 5690 deletions
+2 -3
View File
@@ -158,9 +158,8 @@ test:saucelabs --flaky_test_attempts=1
# --ng_perf will ask the Ivy compiler to produce performance results for each build.
build --flag_alias=ng_perf=//packages/compiler-cli:ng_perf
# --adev_fast will run adev build/serve in a faster mode, skipping things like prerendering
# for local development.
build --flag_alias=fast_adev=//adev:fast_build_mode
# --prerender_adev will run adev build/serve in a full mode, performing prerendering
build --flag_alias=prerender_adev=//adev:prerender_adev
####################################################
# User bazel configuration
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
- name: Install node modules
run: yarn install --frozen-lockfile
- name: Build adev to ensure it continues to work
run: yarn bazel build //adev:build --config=release
run: yarn bazel build //adev:build --prerender_adev --config=release
- uses: angular/dev-infra/github-actions/previews/pack-and-upload-artifact@03b8a7dffd1205e061f0bee949024ebefc2a6592
with:
workflow-artifact-name: 'adev-preview'
+2 -2
View File
@@ -84,7 +84,7 @@ jobs:
- name: Install node modules
run: yarn install --frozen-lockfile
- name: Build adev in fast mode to ensure it continues to work
run: yarn bazel build //adev:build --fast_adev --config=release
run: yarn bazel build //adev:build --config=release
- name: Run tests
run: yarn bazel test //adev:test
@@ -192,7 +192,7 @@ jobs:
- name: Install node modules
run: yarn install --frozen-lockfile
- name: Build adev to ensure it continues to work
run: yarn bazel build //adev:build --config=release
run: yarn bazel build //adev:build --prerender_adev --config=release
- name: Deploy to firebase
uses: ./.github/actions/deploy-docs-site
with:
+1 -1
View File
@@ -113,7 +113,7 @@ jobs:
- name: Install node modules
run: yarn install --frozen-lockfile
- name: Build adev in fast mode to ensure it continues to work
run: yarn bazel build //adev:build --fast_adev --config=release
run: yarn bazel build //adev:build --config=release
- name: Run tests
run: yarn bazel test //adev:test
+2 -2
View File
@@ -39,7 +39,7 @@ jobs:
# Upload the results as artifacts.
- name: 'Upload artifact'
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
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@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15
uses: github/codeql-action/upload-sarif@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5
with:
sarif_file: results.sarif
+1 -1
View File
@@ -372,7 +372,7 @@ groups:
<<: *defaults
conditions:
- >
contains_any_globs(files.exclude('packages/compiler-cli/*').exclude('packages/language-service/*').exclude('packages/service-worker/*'), [
contains_any_globs(files.exclude('packages/compiler-cli/*').exclude('packages/language-service/*').exclude('packages/service-worker/*').exclude('packages/core/schematics/*'), [
'packages/**/testing/**/{*,.*}',
])
reviewers:
+3271 -2139
View File
File diff suppressed because it is too large Load Diff
+13 -20
View File
@@ -1,4 +1,5 @@
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@npm//@angular/build-tooling/bazel/http-server:index.bzl", "http_server")
load("@bazel_skylib//rules:common_settings.bzl", "bool_flag")
load("@build_bazel_rules_nodejs//:index.bzl", "copy_to_bin")
load("@npm//@angular-devkit/architect-cli:index.bzl", "architect", "architect_test")
@@ -113,27 +114,27 @@ copy_to_bin(
)
bool_flag(
name = "fast_build_mode",
name = "prerender_adev",
build_setting_default = False,
)
config_setting(
name = "fast",
name = "no_prerender",
flag_values = {
":fast_build_mode": "true",
":prerender_adev": "false",
},
)
config_setting(
name = "full",
name = "prerender",
flag_values = {
":fast_build_mode": "false",
":prerender_adev": "true",
},
)
config_based_architect_flags = select({
":fast": ["--no-prerender"],
":full": ["--prerender"],
":no_prerender": ["--no-prerender"],
":prerender": ["--prerender"],
})
architect(
@@ -154,21 +155,13 @@ architect(
],
)
architect(
http_server(
name = "serve",
args = [
"angular-dev:serve",
"--poll=1000",
"--live-reload",
"--watch",
],
chdir = package_name(),
data = ensure_local_package_deps(APPLICATION_DEPS) + APPLICATION_ASSETS + [
":application_files_bin",
],
tags = [
"no-remote-exec",
additional_root_paths = [
"angular/adev/build/browser",
],
enable_dev_ui = True,
deps = [":build"],
)
architect_test(
+1
View File
@@ -22,6 +22,7 @@ ts_library(
"//adev/shared-docs/components/slide-toggle",
"//adev/shared-docs/components/table-of-contents",
"//adev/shared-docs/components/text-field",
"//adev/shared-docs/components/top-level-banner",
"//adev/shared-docs/components/viewers",
],
)
@@ -18,6 +18,7 @@ ng_module(
"//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/top-level-banner:__pkg__",
"//adev/shared-docs/components/viewers:__pkg__",
],
deps = [
+1
View File
@@ -15,3 +15,4 @@ 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';
export * from './top-level-banner/top-level-banner.component';
@@ -22,6 +22,7 @@ ng_module(
"//adev/shared-docs/interfaces",
"//adev/shared-docs/pipes",
"//adev/shared-docs/services",
"//packages/common",
"//packages/core",
"//packages/forms",
"//packages/router",
@@ -10,65 +10,62 @@
></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">
@if (result.url) {
<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">
@if (result.hierarchy?.lvl0 === 'Docs') {
<i role="presentation" class="material-symbols-outlined docs-icon-small">
description
</i>
} @else if (result.hierarchy?.lvl0 === 'Tutorials') {
<i role="presentation" class="material-symbols-outlined docs-icon-small">code</i>
} @else if (result.hierarchy?.lvl0 === 'Reference') {
<i role="presentation" class="material-symbols-outlined docs-icon-small">
description
</i>
}
</span>
<!-- Results type -->
<span class="docs-search-results__type">{{ result.hierarchy?.lvl1 }}</span>
<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>
<!-- Hide level 2 if level 3 exists -->
<!-- Level 2 -->
@if (result.hierarchy?.lvl2 && !result.hierarchy?.lvl3) {
<span class="docs-search-results__type docs-search-results__lvl2">
{{ result.hierarchy?.lvl2 }}
</span>
}
<!-- Level 3 -->
@if (result.hierarchy?.lvl3) {
<span class="docs-search-results__type docs-search-results__lvl3">
{{ result.hierarchy?.lvl3 }}
</span>
}
</div>
<!-- Page title -->
<span class="docs-result-page-title">{{ result.hierarchy?.lvl0 }}</span>
</a>
<!-- Page title -->
<span class="docs-result-page-title">{{ result.hierarchy?.lvl0 }}</span>
</a>
</li>
}
</li>
}
</ul>
</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 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>
} @else if (searchResults()?.length === 0) {
<div class="docs-search-results__no-results">
<span>No results found</span>
</div>
}
</div>
}
<div class="docs-algolia">
@@ -79,3 +76,14 @@
</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>
@@ -48,6 +48,14 @@ dialog {
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;
@@ -20,6 +20,7 @@ import {
viewChild,
viewChildren,
} from '@angular/core';
import {NgTemplateOutlet} from '@angular/common';
import {WINDOW} from '../../providers/index';
import {ClickOutside} from '../../directives/index';
@@ -34,6 +35,7 @@ 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',
@@ -47,6 +49,7 @@ import {RelativeLink} from '../../pipes/relative-link.pipe';
AlgoliaIcon,
RelativeLink,
RouterLink,
NgTemplateOutlet,
],
templateUrl: './search-dialog.component.html',
styleUrls: ['./search-dialog.component.scss'],
@@ -104,6 +107,46 @@ export class SearchDialog implements OnDestroy {
});
}
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();
}
@@ -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 = "top-level-banner",
srcs = [
"top-level-banner.component.ts",
],
assets = [
":top-level-banner.component.css",
"top-level-banner.component.html",
],
visibility = [
"//adev/shared-docs/components:__pkg__",
],
deps = [
"//adev/shared-docs/components/icon",
"//adev/shared-docs/directives",
"//adev/shared-docs/providers",
"//packages/common",
"//packages/core",
],
)
sass_binary(
name = "style",
src = "top-level-banner.component.scss",
)
ts_library(
name = "test_lib",
testonly = True,
srcs = glob(
["*.spec.ts"],
),
deps = [
":top-level-banner",
"//adev/shared-docs/providers",
"//packages/core",
"//packages/core/testing",
],
)
karma_web_test_suite(
name = "test",
deps = [":test_lib"],
)
@@ -0,0 +1,15 @@
@if (!hasClosed()) {
@if (link()) {
<a [href]="link()" class="docs-top-level-banner">
<h1 tabindex="-1" class="docs-top-level-banner-cta">{{ text() }}</h1>
</a>
} @else {
<div class="docs-top-level-banner">
<h1 tabindex="-1" class="docs-top-level-banner-cta">{{ text() }}</h1>
</div>
}
<button class="docs-top-level-banner-close" type="button" (click)="close()">
<docs-icon class="docs-icon_high-contrast">close</docs-icon>
</button>
}
@@ -0,0 +1,76 @@
:host {
&:not(:empty) {
z-index: 50;
position: fixed;
height: 2rem;
width: 100vw;
border-bottom: 1px solid var(--septenary-contrast);
text-align: center;
align-content: center;
backdrop-filter: blur(16px);
background-color: color-mix(in srgb, var(--page-background) 70%, transparent);
}
a.docs-top-level-banner {
width: 100%;
display: inherit;
}
h1.docs-top-level-banner-cta {
display: inline;
position: relative;
font-size: 0.875rem;
margin: 0;
background-image: var(--red-to-pink-to-purple-horizontal-gradient);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
width: fit-content;
font-weight: 500;
&::after {
content: '';
position: absolute;
width: 100%;
transform: scaleX(0);
height: 1px;
bottom: -2px;
left: 0;
background: var(--tertiary-contrast);
animation-name: shimmer;
-webkit-animation-duration: 5s;
-moz-animation-duration: 5s;
animation-duration: 5s;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
}
&:hover {
h1.docs-top-level-banner-cta {
&::after {
transform: scaleX(1);
transform-origin: bottom left;
}
}
}
.docs-top-level-banner-close {
position: absolute;
top: 0.25rem;
right: 0.5rem;
color: var(--primary-contrast);
}
}
@keyframes shimmer {
0% {
transform: scaleX(0);
transform-origin: bottom right;
}
100% {
transform: scaleX(1);
transform-origin: bottom left;
}
}
@@ -0,0 +1,99 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {STORAGE_KEY_PREFIX, TopLevelBannerComponent} from './top-level-banner.component';
import {LOCAL_STORAGE} from '../../providers';
describe('TopLevelBannerComponent', () => {
let component: TopLevelBannerComponent;
let fixture: ComponentFixture<TopLevelBannerComponent>;
let mockLocalStorage: jasmine.SpyObj<Storage>;
const EXAMPLE_TEXT = 'Click Here';
const EXAMPLE_LINK = 'https://example.com';
const EXAMPLE_ID = 'banner-id';
beforeEach(async () => {
mockLocalStorage = jasmine.createSpyObj('Storage', ['getItem', 'setItem']);
await TestBed.configureTestingModule({
imports: [TopLevelBannerComponent],
providers: [{provide: LOCAL_STORAGE, useValue: mockLocalStorage}],
}).compileComponents();
fixture = TestBed.createComponent(TopLevelBannerComponent);
fixture.componentRef.setInput('text', EXAMPLE_TEXT);
fixture.componentRef.setInput('id', EXAMPLE_ID);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should render an anchor element when link is provided', () => {
fixture.componentRef.setInput('text', EXAMPLE_TEXT);
fixture.componentRef.setInput('link', EXAMPLE_LINK);
fixture.detectChanges();
const bannerElement = fixture.nativeElement.querySelector('a.adev-top-level-banner');
expect(bannerElement).toBeTruthy();
expect(bannerElement.getAttribute('href')).toBe(EXAMPLE_LINK);
expect(bannerElement.textContent).toContain(EXAMPLE_TEXT);
});
it('should render a div element when link is not provided', () => {
const EXAMPLE_TEXT = 'No Link Available';
fixture.componentRef.setInput('text', EXAMPLE_TEXT);
fixture.detectChanges();
const bannerElement = fixture.nativeElement.querySelector('div.adev-top-level-banner');
expect(bannerElement).toBeTruthy();
expect(bannerElement.textContent).toContain(EXAMPLE_TEXT);
});
it('should correctly render the text input', () => {
const EXAMPLE_TEXT = 'Lorem ipsum dolor...';
fixture.componentRef.setInput('text', EXAMPLE_TEXT);
fixture.detectChanges();
const bannerElement = fixture.nativeElement.querySelector('.adev-top-level-banner-cta');
expect(bannerElement).toBeTruthy();
expect(bannerElement.textContent).toBe(EXAMPLE_TEXT);
});
it('should set hasClosed to true if the banner was closed before', () => {
mockLocalStorage.getItem.and.returnValue('true');
component.ngOnInit();
expect(component.hasClosed()).toBeTrue();
expect(mockLocalStorage.getItem).toHaveBeenCalledWith(`${STORAGE_KEY_PREFIX}${EXAMPLE_ID}`);
});
it('should set hasClosed to false if the banner was not closed before', () => {
mockLocalStorage.getItem.and.returnValue('false');
component.ngOnInit();
expect(component.hasClosed()).toBeFalse();
expect(mockLocalStorage.getItem).toHaveBeenCalledWith(`${STORAGE_KEY_PREFIX}${EXAMPLE_ID}`);
});
it('should set hasClosed to false if accessing localStorage throws an error', () => {
mockLocalStorage.getItem.and.throwError('Local storage error');
component.ngOnInit();
expect(component.hasClosed()).toBeFalse();
});
it('should set the banner as closed in localStorage and update hasClosed', () => {
component.close();
expect(mockLocalStorage.setItem).toHaveBeenCalledWith(
`${STORAGE_KEY_PREFIX}${EXAMPLE_ID}`,
'true',
);
expect(component.hasClosed()).toBeTrue();
});
});
@@ -0,0 +1,52 @@
import {ChangeDetectionStrategy, Component, inject, input, OnInit, signal} from '@angular/core';
import {ExternalLink} from '../../directives';
import {LOCAL_STORAGE} from '../../providers';
import {IconComponent} from '../icon/icon.component';
export const STORAGE_KEY_PREFIX = 'docs-was-closed-top-banner-';
@Component({
selector: 'docs-top-level-banner',
standalone: true,
imports: [ExternalLink, IconComponent],
templateUrl: './top-level-banner.component.html',
styleUrl: './top-level-banner.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TopLevelBannerComponent implements OnInit {
private readonly localStorage = inject(LOCAL_STORAGE);
/**
* Unique identifier for the banner. This ID is required to ensure that
* the state of the banner (e.g., whether it has been closed) is tracked
* separately for different events or instances. Without a unique ID,
* closing one banner could inadvertently hide other banners for different events.
*/
id = input.required<string>();
// Optional URL link that the banner should navigate to when clicked.
link = input<string>();
// Text content to be displayed in the banner.
text = input.required<string>();
// Whether the user has closed the banner.
hasClosed = signal<boolean>(false);
ngOnInit(): void {
// Needs to be in a try/catch, because some browsers will
// throw when using `localStorage` in private mode.
try {
this.hasClosed.set(this.localStorage?.getItem(this.getBannerStorageKey()) === 'true');
} catch {
this.hasClosed.set(false);
}
}
close(): void {
this.localStorage?.setItem(this.getBannerStorageKey(), 'true');
this.hasClosed.set(true);
}
private getBannerStorageKey(): string {
return `${STORAGE_KEY_PREFIX}${this.id()}`;
}
}
+27 -2
View File
@@ -6,14 +6,39 @@
* found in the LICENSE file at https://angular.dev/license
*/
export interface SnippetResult {
value: string;
matchLevel: 'none' | 'full' | string;
}
/* The interface represents Algolia search result item. */
export interface SearchResult {
/* The url link to the search result page */
url?: string;
url: string;
/* The hierarchy of the item */
hierarchy?: Hierarchy;
hierarchy: Hierarchy;
/* The unique id of the search result item */
objectID: string;
/**
* The type of the result. A content result will have
* matched the content. A result of type 'lvl#' may have i
* matched a lvl above it. For example, a type 'lvl3' may be
* included in results because its 'lvl2' header matched the query.
*/
type: string;
/** Documentation content (not headers) */
content: string | null;
/** Snippets of the matched text */
_snippetResult: {
hierarchy?: {
lvl0?: SnippetResult;
lvl1?: SnippetResult;
lvl2?: SnippetResult;
lvl3?: SnippetResult;
lvl4?: SnippetResult;
};
content?: SnippetResult;
};
}
/* The hierarchy of the item */
+3 -3
View File
@@ -19,9 +19,9 @@
"fast-glob": "~3.3.2",
"fflate": "^0.8.2",
"html-entities": "~2.5.2",
"jsdom": "~24.1.0",
"marked": "~12.0.2",
"mermaid": "^10.8.0",
"jsdom": "~25.0.0",
"marked": "~14.0.0",
"mermaid": "^11.0.0",
"shiki": "^1.10.3"
},
"exports": {
@@ -132,15 +132,25 @@ export interface PipeEntry extends ClassEntry {
// TODO: add `isPure`.
}
export interface FunctionEntry extends DocEntry {
export interface FunctionSignatureMetadata extends DocEntry {
params: ParameterEntry[];
returnType: string;
returnDescription?: string;
generics: GenericEntry[];
isNewType: boolean;
}
export interface FunctionWithOverloadsEntry extends FunctionEntry {
overloads: FunctionEntry[] | null;
export type FunctionEntry = FunctionDefinitionEntry &
DocEntry & {
implementation: FunctionSignatureMetadata;
};
/** Interface describing a function with overload signatures. */
export interface FunctionDefinitionEntry {
name: string;
signatures: FunctionSignatureMetadata[];
implementation: FunctionSignatureMetadata | null;
}
/** Sub-entry for a single class or enum member. */
@@ -178,15 +188,9 @@ export interface ParameterEntry {
isRestParam: boolean;
}
export interface FunctionWithOverloads {
name: string;
signatures: FunctionEntry[];
implementation: FunctionEntry | null;
}
export interface InitializerApiFunctionEntry extends DocEntry {
callFunction: FunctionWithOverloads;
subFunctions: FunctionWithOverloads[];
callFunction: FunctionDefinitionEntry;
subFunctions: FunctionDefinitionEntry[];
__docsMetadata__?: {
/**
@@ -204,7 +208,3 @@ export interface InitializerApiFunctionEntry extends DocEntry {
export function isDocEntryWithSourceInfo(entry: DocEntry): entry is DocEntryWithSourceInfo {
return 'source' in entry;
}
export function isFunctionEntryWithOverloads(entry: DocEntry): entry is FunctionWithOverloadsEntry {
return 'overloads' in entry;
}
@@ -12,6 +12,7 @@ import {
DocEntry,
EnumEntry,
FunctionEntry,
FunctionSignatureMetadata,
InitializerApiFunctionEntry,
JsDocTagEntry,
MemberEntry,
@@ -55,7 +56,7 @@ export type TypeAliasEntryRenderable = TypeAliasEntry & DocEntryRenderable & Has
export type ClassEntryRenderable = ClassEntry &
DocEntryRenderable &
HasRenderableToc & {
membersGroups: Map<string, MemberEntryRenderable[]>;
members: MemberEntryRenderable[];
};
/** Documentation entity for a TypeScript enum augmented transformed content for rendering. */
@@ -71,9 +72,12 @@ export type InterfaceEntryRenderable = ClassEntryRenderable;
export type FunctionEntryRenderable = FunctionEntry &
DocEntryRenderable &
HasRenderableToc & {
params: ParameterEntryRenderable[];
deprecationMessage: string | null;
overloads: FunctionEntryRenderable[] | null;
};
export type FunctionSignatureMetadataRenderable = FunctionSignatureMetadata &
DocEntryRenderable & {
params: ParameterEntryRenderable[];
};
/** Sub-entry for a single class or enum member augmented with transformed content for rendering. */
@@ -1,8 +1,7 @@
import {FunctionEntry, JsDocTagEntry, MemberEntry, ParameterEntry} from '../entities';
import {JsDocTagEntry, MemberEntry, ParameterEntry} from '../entities';
import {
CodeLineRenderable,
FunctionEntryRenderable,
JsDocTagRenderable,
LinkEntryRenderable,
MemberEntryRenderable,
@@ -81,14 +80,6 @@ export interface HasRenderableParams {
params: ParameterEntryRenderable[];
}
export interface HasOverloads {
overloads: FunctionEntry[] | null;
}
export interface HasRenderableOverloads {
overloads: FunctionEntryRenderable[] | null;
}
export interface HasDeprecatedFlag {
isDeprecated: boolean;
deprecationMessage: string | null;
@@ -6,16 +6,18 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {Renderer as MarkedRenderer} from 'marked';
import {Renderer, Tokens} from 'marked';
import {codeToHtml} from '../shiki/shiki';
/**
* Custom renderer for marked that will be used to transform markdown files to HTML
* files that can be used in the Angular docs.
*/
export const renderer: Partial<MarkedRenderer> = {
code(code: string, language: string, isEscaped: boolean): string {
const highlightResult = codeToHtml(code, language).replace(/>\s+</g, '><');
export const renderer: Partial<Renderer> = {
code({lang, text}): string {
const highlightResult = codeToHtml(text, lang)
// remove spaces/line-breaks between elements to not mess-up `pre` style
.replace(/>\s+</g, '><');
return `
<div class="docs-code" role="group">
@@ -25,37 +27,46 @@ export const renderer: Partial<MarkedRenderer> = {
</div>
`;
},
image(href: string | null, title: string | null, text: string): string {
image({href, title, text}): string {
return `
<img src="${href}" alt="${text}" title="${title}" class="docs-image">
`;
},
link(href: string, title: string, text: string): string {
return `<a href="${href}">${text}</a>`;
link(this: Renderer, {href, tokens}): string {
return `<a href="${href}">${this.parser.parseInline(tokens)}</a>`;
},
list(body: string, ordered: boolean, start: number) {
list(this: Renderer, {items, ordered, start}) {
if (ordered) {
return `
<ol class="docs-ordered-list">
${body}
${items.map((item) => this.listitem(item)).join('')}
</ol>
`;
}
return `
<ul class="docs-list">
${body}
${items.map((item) => this.listitem(item)).join('')}
</ul>
`;
},
table(header: string, body: string): string {
table(this: Renderer, {header, rows}: Tokens.Table) {
return `
<div class="docs-table docs-scroll-track-transparent">
<table>
<thead>
${header}
${this.tablerow({
text: header.map((cell) => this.tablecell(cell)).join(''),
})}
</thead>
<tbody>
${body}
${rows
.map((row) =>
this.tablerow({
text: row.map((cell) => this.tablecell(cell)).join(''),
}),
)
.join('')}
</tbody>
</table>
</div>
@@ -21,8 +21,12 @@ export async function initHighlighter() {
});
}
export function codeToHtml(code: string, language: string | undefined): string {
return highlighter.codeToHtml(code, {
export function codeToHtml(
code: string,
language: string | undefined,
options?: {removeFunctionKeyword?: boolean},
): string {
const html = highlighter.codeToHtml(code, {
lang: language ?? 'text',
themes: {
light: 'github-light',
@@ -31,4 +35,20 @@ export function codeToHtml(code: string, language: string | undefined): string {
cssVariablePrefix: '--shiki-',
defaultColor: false,
});
if (options?.removeFunctionKeyword) {
return removeFunctionKeywordFromShikiHtml(html);
}
return html;
}
export function removeFunctionKeywordFromShikiHtml(shikiHtml: string): string {
return (
shikiHtml
// remove the leading space of the element after the "function" element
.replace(/(<[^>]*>function<\/\w+><[^>]*>)(\s)(\w+<\/\w+>)/g, '$1$3')
// Shiki requires the keyword function for highlighting functions signatures
// We don't want to display it so we remove elements with the keyword
.replace(/<[^>]*>function<\/\w+>/g, '')
);
}
@@ -16,6 +16,7 @@ export const REFERENCE_MEMBERS = 'docs-reference-members';
export const REFERENCE_DEPRECATED = 'docs-reference-deprecated';
export const REFERENCE_MEMBERS_CONTAINER = 'docs-reference-members-container';
export const REFERENCE_MEMBER_CARD = 'docs-reference-member-card';
export const REFERENCE_MEMBER_CARD_HEADER = 'docs-reference-card-header';
export const REFERENCE_MEMBER_CARD_BODY = 'docs-reference-card-body';
export const REFERENCE_MEMBER_CARD_ITEM = 'docs-reference-card-item';
@@ -10,11 +10,11 @@ import {h} from 'preact';
import {MemberEntryRenderable} from '../entities/renderables';
import {ClassMember} from './class-member';
export function ClassMemberList(props: {membersGroups: Map<string, MemberEntryRenderable[]>}) {
export function ClassMemberList(props: {members: MemberEntryRenderable[]}) {
return (
<div class="docs-reference-members">
{Array.from(props.membersGroups).map(([_, group]) => (
<ClassMember members={group} />
{props.members.map((member) => (
<ClassMember member={member} />
))}
</div>
);
@@ -13,53 +13,53 @@ import {
isPropertyEntry,
isSetterEntry,
} from '../entities/categorization';
import {MemberEntryRenderable} from '../entities/renderables';
import {
REFERENCE_HEADER,
FunctionSignatureMetadataRenderable,
MemberEntryRenderable,
MethodEntryRenderable,
} from '../entities/renderables';
import {
REFERENCE_MEMBER_CARD,
REFERENCE_MEMBER_CARD_BODY,
REFERENCE_MEMBER_CARD_HEADER,
REFERENCE_MEMBER_CARD_ITEM,
} from '../styling/css-classes';
import {ClassMethodInfo} from './class-method-info';
import {DeprecatedLabel} from './deprecated-label';
import {RawHtml} from './raw-html';
import {getFunctionMetadataRenderable} from '../transforms/function-transforms';
export function ClassMember(props: {members: MemberEntryRenderable[]}) {
const memberName = props.members[0].name;
const returnType = getMemberType(props.members[0]);
// Do not create body element when there is no description
const body = props.members.every(
(member) => !member.htmlDescription && !isClassMethodEntry(member),
) ? (
<></>
) : (
export function ClassMember(props: {member: MemberEntryRenderable}) {
const body = (
<div className={REFERENCE_MEMBER_CARD_BODY}>
{props.members.map((member) => {
return isClassMethodEntry(member) ? (
<ClassMethodInfo entry={member} isOverloaded={props.members.length > 1} />
) : (
<div className={REFERENCE_MEMBER_CARD_ITEM}>
{props.members.every((member) => member.deprecationMessage !== null) ? (
<DeprecatedLabel entry={props.members[0]} />
) : (
<></>
)}
<RawHtml value={member.htmlDescription} />
</div>
);
})}
{isClassMethodEntry(props.member) ? (
props.member.signatures.map((sig, i, signatures) => {
const renderableMember = getFunctionMetadataRenderable(sig);
return <ClassMethodInfo entry={renderableMember} isOverloaded={signatures.length > 1} />;
})
) : (
<div className={REFERENCE_MEMBER_CARD_ITEM}>
{props.member.deprecationMessage !== null ? (
<DeprecatedLabel entry={props.member} />
) : (
<></>
)}
<RawHtml value={props.member.htmlDescription} />
</div>
)}
</div>
);
const memberName = props.member.name;
const returnType = getMemberType(props.member);
return (
<div id={memberName} className={REFERENCE_MEMBER_CARD} tabIndex={-1}>
<header>
<div className={REFERENCE_HEADER}>
<div className={REFERENCE_MEMBER_CARD_HEADER}>
<h3>{memberName}</h3>
<div>
{props.members.length > 1 ? (
<span>{props.members.length} overloads</span>
{isClassMethodEntry(props.member) && props.member.signatures.length > 1 ? (
<span>{props.member.signatures.length} overloads</span>
) : returnType ? (
<code>{returnType}</code>
) : (
@@ -75,7 +75,7 @@ export function ClassMember(props: {members: MemberEntryRenderable[]}) {
function getMemberType(entry: MemberEntryRenderable): string | null {
if (isClassMethodEntry(entry)) {
return entry.returnType;
return entry.implementation.returnType;
} else if (isPropertyEntry(entry) || isGetterEntry(entry) || isSetterEntry(entry)) {
return entry.type;
}
@@ -9,6 +9,7 @@
import {Fragment, h} from 'preact';
import {
FunctionEntryRenderable,
FunctionSignatureMetadataRenderable,
MethodEntryRenderable,
ParameterEntryRenderable,
} from '../entities/renderables';
@@ -21,7 +22,7 @@ import {RawHtml} from './raw-html';
* Component to render the method-specific parts of a class's API reference.
*/
export function ClassMethodInfo(props: {
entry: MethodEntryRenderable | FunctionEntryRenderable;
entry: FunctionSignatureMetadataRenderable;
isOverloaded?: boolean;
}) {
const entry = props.entry;
@@ -32,12 +33,12 @@ export function ClassMethodInfo(props: {
>
<RawHtml value={entry.htmlDescription} className={'docs-function-definition'} />
{/* In case when method is overloaded we need to indicate which overload is deprecated */}
{!props.isOverloaded ? (
<></>
) : (
{entry.isDeprecated ? (
<div>
<DeprecatedLabel entry={entry} />
</div>
) : (
<></>
)}
{entry.params.map((param: ParameterEntryRenderable) => (
<Parameter param={param} />
@@ -24,9 +24,9 @@ export function ClassReference(entry: ClassEntryRenderable) {
<TabDescription entry={entry} />
<TabUsageNotes entry={entry} />
{
entry.membersGroups.size > 0
entry.members.length > 0
? (<div class={REFERENCE_MEMBERS_CONTAINER}>
<ClassMemberList membersGroups={entry.membersGroups} />
<ClassMemberList members={entry.members} />
</div>)
: (<></>)
}
@@ -9,12 +9,13 @@
import {Fragment, h} from 'preact';
import {CliCardRenderable} from '../entities/renderables';
import {DeprecatedLabel} from './deprecated-label';
import { REFERENCE_MEMBER_CARD, REFERENCE_MEMBER_CARD_HEADER } from '../styling/css-classes';
export function CliCard(props: {card: CliCardRenderable}) {
return (
<div id={props.card.type} class="docs-reference-member-card" tabIndex={-1}>
<div id={props.card.type} class={REFERENCE_MEMBER_CARD} tabIndex={-1}>
<header>
<div class="docs-card-ref-header">
<div class={REFERENCE_MEMBER_CARD_HEADER}>
<h3>{props.card.type}</h3>
</div>
</header>
@@ -23,7 +23,7 @@ export function CliCommandReference(entry: CliCommandRenderable) {
<div class="docs-code docs-reference-cli-toc">
<pre class="docs-mini-scroll-track">
<code>
<div className={'shiki-ln-line'}>
<div className={'shiki line cli'}>
ng {commandName(entry, command)}
{entry.argumentsLabel ? <button member-id={'Arguments'} className="shiki-ln-line-argument">{entry.argumentsLabel}</button> : <></>}
{entry.hasOptions ? <button member-id={'Options'} className="shiki-ln-line-option">[options]</button> : <></>}
@@ -7,7 +7,7 @@
*/
import {h, Fragment} from 'preact';
import {EnumEntryRenderable} from '../entities/renderables';
import {EnumEntryRenderable, MemberEntryRenderable} from '../entities/renderables';
import {HeaderApi} from './header-api';
import {TabDescription} from './tab-description';
import {TabApi} from './tab-api';
@@ -26,7 +26,7 @@ export function EnumReference(entry: EnumEntryRenderable) {
? (
<div class={REFERENCE_MEMBERS_CONTAINER}>
<div class={REFERENCE_MEMBERS}>
{entry.members.map((member: any) => (<ClassMember members={[member]}/>))}
{entry.members.map((member: MemberEntryRenderable) => (<ClassMember member={member}/>))}
</div>
</div>
)
@@ -7,23 +7,65 @@
*/
import {h} from 'preact';
import {FunctionEntryRenderable} from '../entities/renderables';
import {FunctionEntryRenderable, FunctionSignatureMetadataRenderable} from '../entities/renderables';
import {
PARAM_KEYWORD_CLASS_NAME,
REFERENCE_HEADER,
REFERENCE_MEMBERS,
REFERENCE_MEMBERS_CONTAINER,
REFERENCE_MEMBER_CARD,
REFERENCE_MEMBER_CARD_BODY,
REFERENCE_MEMBER_CARD_HEADER,
} from '../styling/css-classes';
import {ClassMethodInfo} from './class-method-info';
import {HeaderApi} from './header-api';
import {TabApi} from './tab-api';
import {TabDescription} from './tab-description';
import {TabUsageNotes} from './tab-usage-notes';
import {HighlightTypeScript} from './highlight-ts';
import {printInitializerFunctionSignatureLine} from '../transforms/code-transforms';
import {getFunctionMetadataRenderable} from '../transforms/function-transforms';
export const signatureCard = (
name: string,
signature: FunctionSignatureMetadataRenderable,
opts: {id: string},
printSignaturesAsHeader: boolean,
) => {
return (
<div class={REFERENCE_MEMBER_CARD} id={opts.id} tabIndex={-1}>
<header>
{printSignaturesAsHeader ? (
<code>
<HighlightTypeScript
code={printInitializerFunctionSignatureLine(
name,
signature,
// Always omit types in signature headers, to keep them short.
true,
)}
removeFunctionKeyword={true}
/>
</code>
) : (
<div className={REFERENCE_MEMBER_CARD_HEADER}>
<h3>{name}</h3>
<div>
<code>{signature.returnType}</code>
</div>
</div>
)}
</header>
<div class={REFERENCE_MEMBER_CARD_BODY}>
<ClassMethodInfo entry={signature} />
</div>
</div>
);
};
/** Component to render a function API reference document. */
export function FunctionReference(entry: FunctionEntryRenderable) {
// Use signatures as header if there are multiple signatures.
const printSignaturesAsHeader = entry.signatures.length > 1;
return (
<div class="api">
<HeaderApi entry={entry} />
@@ -32,26 +74,16 @@ export function FunctionReference(entry: FunctionEntryRenderable) {
<TabUsageNotes entry={entry} />
<div className={REFERENCE_MEMBERS_CONTAINER}>
<div className={REFERENCE_MEMBERS}>
<div className={REFERENCE_MEMBER_CARD}>
<header>
<div className={REFERENCE_HEADER}>
<h3>{entry.name}</h3>
<div>
<code>{entry.returnType}</code>
</div>
</div>
{entry.isDeprecated && (
<span className={`${PARAM_KEYWORD_CLASS_NAME} docs-deprecated`}>@deprecated</span>
)}
</header>
<div className={REFERENCE_MEMBER_CARD_BODY}>
{entry.overloads ? (
entry.overloads.map((overload) => <ClassMethodInfo entry={overload} />)
) : (
<ClassMethodInfo entry={entry} isOverloaded={true} />
)}
</div>
</div>
{entry.signatures.map((s, i) =>
signatureCard(
s.name,
getFunctionMetadataRenderable(s, entry.moduleName),
{
id: `${s.name}_${i}`,
},
printSignaturesAsHeader,
),
)}
</div>
</div>
</div>
@@ -6,15 +6,15 @@
* found in the LICENSE file at https://angular.io/license
*/
import { h } from 'preact';
import { RawHtml } from './raw-html';
import { codeToHtml } from '../shiki/shiki';
import {h} from 'preact';
import {RawHtml} from './raw-html';
import {codeToHtml} from '../shiki/shiki';
/** Component to render a header of the CLI page. */
export function HighlightTypeScript(props: {code: string}) {
const result = codeToHtml(props.code, 'typescript');
export function HighlightTypeScript(props: {code: string; removeFunctionKeyword?: boolean}) {
const result = codeToHtml(props.code, 'typescript', {
removeFunctionKeyword: props.removeFunctionKeyword,
});
return (
<RawHtml value={result} />
);
return <RawHtml value={result} />;
}
@@ -7,20 +7,13 @@
*/
import {h, JSX} from 'preact';
import {FunctionEntryRenderable, InitializerApiFunctionRenderable} from '../entities/renderables';
import {InitializerApiFunctionRenderable} from '../entities/renderables';
import {HeaderApi} from './header-api';
import {TabApi} from './tab-api';
import {TabUsageNotes} from './tab-usage-notes';
import {
REFERENCE_MEMBERS,
REFERENCE_MEMBERS_CONTAINER,
REFERENCE_MEMBER_CARD,
REFERENCE_MEMBER_CARD_BODY,
} from '../styling/css-classes';
import {printInitializerFunctionSignatureLine} from '../transforms/code-transforms';
import {HighlightTypeScript} from './highlight-ts';
import {ClassMethodInfo} from './class-method-info';
import {getFunctionRenderable} from '../transforms/function-transforms';
import {REFERENCE_MEMBERS, REFERENCE_MEMBERS_CONTAINER} from '../styling/css-classes';
import {getFunctionMetadataRenderable} from '../transforms/function-transforms';
import {signatureCard} from './function-reference';
/** Component to render a constant API reference document. */
export function InitializerApiFunction(entry: InitializerApiFunctionRenderable) {
@@ -40,32 +33,6 @@ export function InitializerApiFunction(entry: InitializerApiFunctionRenderable)
entry.callFunction.signatures[0].description = '';
}
const signatureCard = (name: string, signature: FunctionEntryRenderable, opts: {id: string}) => {
return (
<div class={REFERENCE_MEMBER_CARD} id={opts.id} tabIndex={-1}>
<header>
{printSignaturesAsHeader ? (
<code>
<HighlightTypeScript
code={printInitializerFunctionSignatureLine(
name,
signature,
// Always omit types in signature headers, to keep them short.
true,
)}
/>
</code>
) : (
<h3>{`${name}()`}</h3>
)}
</header>
<div class={REFERENCE_MEMBER_CARD_BODY}>
<ClassMethodInfo entry={signature} />
</div>
</div>
);
};
return (
<div class="api">
<HeaderApi entry={entry} showFullDescription={true} />
@@ -75,9 +42,14 @@ export function InitializerApiFunction(entry: InitializerApiFunctionRenderable)
<div class={REFERENCE_MEMBERS_CONTAINER}>
<div class={REFERENCE_MEMBERS}>
{entry.callFunction.signatures.map((s, i) =>
signatureCard(s.name, getFunctionRenderable(s, entry.moduleName), {
id: `${s.name}_${i}`,
}),
signatureCard(
s.name,
getFunctionMetadataRenderable(s, entry.moduleName),
{
id: `${s.name}_${i}`,
},
printSignaturesAsHeader,
),
)}
{entry.subFunctions.reduce(
@@ -86,10 +58,11 @@ export function InitializerApiFunction(entry: InitializerApiFunctionRenderable)
...subFunction.signatures.map((s, i) =>
signatureCard(
`${entry.name}.${s.name}`,
getFunctionRenderable(s, entry.moduleName),
getFunctionMetadataRenderable(s, entry.moduleName),
{
id: `${entry.name}_${s.name}_${i}`,
},
printSignaturesAsHeader,
),
),
],
@@ -19,10 +19,18 @@ ts_library(
),
deps = [
"//adev/shared-docs/pipeline/api-gen/rendering:render_api_to_html_lib",
"@npm//@bazel/runfiles",
"@npm//@types/jsdom",
"@npm//jsdom",
],
)
jasmine_node_test(
name = "unit_tests",
data = [
"@npm//jsdom",
] + glob([
"**/*.json",
]),
deps = [":unit_test_lib"],
)
@@ -3,81 +3,624 @@
"entries": [
{
"name": "NgTemplateOutlet",
"description": "*one* directive",
"isAbstract": false,
"entryType": "directive",
"members": [],
"jsdocTags": [],
"rawComment": "",
"source": {
"fileName": "/packages/core/src/ng_template_outlet.ts",
"line": 7,
"character": 7
}
},
{
"name": "UserProfile",
"entryType": "undecorated_class",
"members": [
{
"name": "userId",
"type": "number",
"name": "ngTemplateOutletContext",
"type": "C",
"memberType": "property",
"memberTags": [],
"description": "A user identifier.",
"jsdocTags": []
"memberTags": ["input"],
"description": "A context object to attach to the {@link EmbeddedViewRef}. This should be an\nobject, the object's keys will be available for binding by the local template `let`\ndeclarations.\nUsing the key `$implicit` in the context object will set its value as default.",
"jsdocTags": [],
"inputAlias": "ngTemplateOutletContext",
"isRequiredInput": false
},
{
"name": "name",
"type": "string",
"memberType": "getter",
"memberTags": [],
"description": "Name of the user",
"jsdocTags": []
"name": "ngTemplateOutlet",
"type": "TemplateRef<C>",
"memberType": "property",
"memberTags": ["input"],
"description": "A string defining the template reference and optionally the context object for the template.",
"jsdocTags": [],
"inputAlias": "ngTemplateOutlet",
"isRequiredInput": false
},
{
"name": "name",
"type": "string",
"memberType": "setter",
"memberTags": [],
"description": "Name of the user",
"jsdocTags": []
"name": "ngTemplateOutletInjector",
"type": "Injector",
"memberType": "property",
"memberTags": ["input"],
"description": "Injector to be used within the embedded view.",
"jsdocTags": [],
"inputAlias": "ngTemplateOutletInjector",
"isRequiredInput": false
},
{
"params": [
"name": "ngOnChanges",
"signatures": [
{
"name": "config",
"description": "Setting for saving.",
"type": "object",
"isOptional": false,
"isRestParam": false
"name": "ngOnChanges",
"entryType": "function",
"description": "",
"generics": [],
"isNewType": false,
"jsdocTags": [],
"params": [
{
"name": "changes",
"description": "",
"type": "SimpleChanges",
"isOptional": false,
"isRestParam": false
}
],
"rawComment": "",
"returnType": "void"
}
],
"name": "save",
"returnType": "boolean",
"implementation": {
"params": [
{
"name": "changes",
"description": "",
"type": "SimpleChanges",
"isOptional": false,
"isRestParam": false
}
],
"isNewType": false,
"returnType": "void",
"generics": [],
"name": "ngOnChanges",
"description": "",
"entryType": "function",
"jsdocTags": [],
"rawComment": ""
},
"entryType": "function",
"description": "Save the user.",
"jsdocTags": [
{
"name": "param",
"comment": "Setting for saving."
},
{
"name": "returns",
"comment": "Whether it succeeded"
}
],
"rawComment": "/**\n * Save the user.\n * @param config Setting for saving.\n * @returns Whether it succeeded\n */",
"description": "",
"jsdocTags": [],
"rawComment": "",
"memberType": "method",
"memberTags": []
}
],
"generics": [{"name": "C", "default": "unknown"}],
"description": "",
"jsdocTags": [
{"name": "ngModule", "comment": "CommonModule"},
{
"name": "description",
"comment": "Inserts an embedded view from a prepared `TemplateRef`.\n\nYou can attach a context object to the `EmbeddedViewRef` by setting `[ngTemplateOutletContext]`.\n`[ngTemplateOutletContext]` should be an object, the object's keys will be available for binding\nby the local template `let` declarations."
},
{
"name": "usageNotes",
"comment": "```\n<ng-container *ngTemplateOutlet=\"templateRefExp; context: contextExp\"></ng-container>\n```\n\nUsing the key `$implicit` in the context object will set its value as default.\n\n### Example\n\n{@example common/ngTemplateOutlet/ts/module.ts region='NgTemplateOutlet'}"
},
{"name": "publicApi", "comment": ""}
],
"rawComment": "/**\n * @ngModule CommonModule\n *\n * @description\n *\n * Inserts an embedded view from a prepared `TemplateRef`.\n *\n * You can attach a context object to the `EmbeddedViewRef` by setting `[ngTemplateOutletContext]`.\n * `[ngTemplateOutletContext]` should be an object, the object's keys will be available for binding\n * by the local template `let` declarations.\n *\n * @usageNotes\n * ```\n * <ng-container *ngTemplateOutlet=\"templateRefExp; context: contextExp\"></ng-container>\n * ```\n *\n * Using the key `$implicit` in the context object will set its value as default.\n *\n * ### Example\n *\n * {@example common/ngTemplateOutlet/ts/module.ts region='NgTemplateOutlet'}\n *\n * @publicApi\n */",
"isStandalone": true,
"selector": "[ngTemplateOutlet]",
"exportAs": [],
"source": {
"filePath": "/packages/common/src/directives/ng_template_outlet.ts",
"startLine": 45,
"endLine": 126
}
},
{
"name": "ViewRef",
"isAbstract": true,
"entryType": "undecorated_class",
"members": [
{
"name": "destroy",
"signatures": [
{
"name": "destroy",
"entryType": "function",
"description": "Destroys this view and all of the data structures associated with it.",
"generics": [],
"isNewType": false,
"jsdocTags": [],
"params": [],
"rawComment": "/**\n * Destroys this view and all of the data structures associated with it.\n */",
"returnType": "void"
}
],
"implementation": {
"params": [],
"isNewType": false,
"returnType": "void",
"generics": [],
"name": "destroy",
"description": "Destroys this view and all of the data structures associated with it.",
"entryType": "function",
"jsdocTags": [],
"rawComment": "/**\n * Destroys this view and all of the data structures associated with it.\n */"
},
"entryType": "function",
"description": "Destroys this view and all of the data structures associated with it.",
"jsdocTags": [],
"rawComment": "/**\n * Destroys this view and all of the data structures associated with it.\n */",
"memberType": "method",
"memberTags": ["abstract"]
},
{
"name": "destroyed",
"type": "boolean",
"memberType": "getter",
"memberTags": ["abstract"],
"description": "Reports whether this view has been destroyed.",
"jsdocTags": [
{
"name": "returns",
"comment": "True after the `destroy()` method has been called, false otherwise."
}
]
},
{
"name": "onDestroy",
"signatures": [
{
"name": "onDestroy",
"entryType": "function",
"description": "A lifecycle hook that provides additional developer-defined cleanup\nfunctionality for views.",
"generics": [],
"isNewType": false,
"jsdocTags": [
{
"name": "param",
"comment": "A handler function that cleans up developer-defined data\nassociated with a view. Called when the `destroy()` method is invoked."
}
],
"params": [
{
"name": "callback",
"description": "A handler function that cleans up developer-defined data\nassociated with a view. Called when the `destroy()` method is invoked.",
"type": "Function",
"isOptional": false,
"isRestParam": false
}
],
"rawComment": "/**\n * A lifecycle hook that provides additional developer-defined cleanup\n * functionality for views.\n * @param callback A handler function that cleans up developer-defined data\n * associated with a view. Called when the `destroy()` method is invoked.\n */",
"returnType": "void"
}
],
"implementation": {
"params": [
{
"name": "callback",
"description": "A handler function that cleans up developer-defined data\nassociated with a view. Called when the `destroy()` method is invoked.",
"type": "Function",
"isOptional": false,
"isRestParam": false
}
],
"isNewType": false,
"returnType": "void",
"generics": [],
"name": "onDestroy",
"description": "A lifecycle hook that provides additional developer-defined cleanup\nfunctionality for views.",
"entryType": "function",
"jsdocTags": [
{
"name": "param",
"comment": "A handler function that cleans up developer-defined data\nassociated with a view. Called when the `destroy()` method is invoked."
}
],
"rawComment": "/**\n * A lifecycle hook that provides additional developer-defined cleanup\n * functionality for views.\n * @param callback A handler function that cleans up developer-defined data\n * associated with a view. Called when the `destroy()` method is invoked.\n */"
},
"entryType": "function",
"description": "A lifecycle hook that provides additional developer-defined cleanup\nfunctionality for views.",
"jsdocTags": [
{
"name": "param",
"comment": "A handler function that cleans up developer-defined data\nassociated with a view. Called when the `destroy()` method is invoked."
}
],
"rawComment": "/**\n * A lifecycle hook that provides additional developer-defined cleanup\n * functionality for views.\n * @param callback A handler function that cleans up developer-defined data\n * associated with a view. Called when the `destroy()` method is invoked.\n */",
"memberType": "method",
"memberTags": ["abstract"]
},
{
"name": "markForCheck",
"signatures": [
{
"name": "markForCheck",
"entryType": "function",
"description": "When a view uses the {@link ChangeDetectionStrategy#OnPush} (checkOnce)\nchange detection strategy, explicitly marks the view as changed so that\nit can be checked again.\n\nComponents are normally marked as dirty (in need of rerendering) when inputs\nhave changed or events have fired in the view. Call this method to ensure that\na component is checked even if these triggers have not occurred.\n\n<!-- TODO: Add a link to a chapter on OnPush components -->",
"generics": [],
"isNewType": false,
"jsdocTags": [],
"params": [],
"rawComment": "/**\n * When a view uses the {@link ChangeDetectionStrategy#OnPush} (checkOnce)\n * change detection strategy, explicitly marks the view as changed so that\n * it can be checked again.\n *\n * Components are normally marked as dirty (in need of rerendering) when inputs\n * have changed or events have fired in the view. Call this method to ensure that\n * a component is checked even if these triggers have not occurred.\n *\n * <!-- TODO: Add a link to a chapter on OnPush components -->\n *\n */",
"returnType": "void"
}
],
"implementation": {
"params": [],
"isNewType": false,
"returnType": "void",
"generics": [],
"name": "markForCheck",
"description": "When a view uses the {@link ChangeDetectionStrategy#OnPush} (checkOnce)\nchange detection strategy, explicitly marks the view as changed so that\nit can be checked again.\n\nComponents are normally marked as dirty (in need of rerendering) when inputs\nhave changed or events have fired in the view. Call this method to ensure that\na component is checked even if these triggers have not occurred.\n\n<!-- TODO: Add a link to a chapter on OnPush components -->",
"entryType": "function",
"jsdocTags": [],
"rawComment": "/**\n * When a view uses the {@link ChangeDetectionStrategy#OnPush} (checkOnce)\n * change detection strategy, explicitly marks the view as changed so that\n * it can be checked again.\n *\n * Components are normally marked as dirty (in need of rerendering) when inputs\n * have changed or events have fired in the view. Call this method to ensure that\n * a component is checked even if these triggers have not occurred.\n *\n * <!-- TODO: Add a link to a chapter on OnPush components -->\n *\n */"
},
"entryType": "function",
"description": "When a view uses the {@link ChangeDetectionStrategy#OnPush} (checkOnce)\nchange detection strategy, explicitly marks the view as changed so that\nit can be checked again.\n\nComponents are normally marked as dirty (in need of rerendering) when inputs\nhave changed or events have fired in the view. Call this method to ensure that\na component is checked even if these triggers have not occurred.\n\n<!-- TODO: Add a link to a chapter on OnPush components -->",
"jsdocTags": [],
"rawComment": "/**\n * When a view uses the {@link ChangeDetectionStrategy#OnPush} (checkOnce)\n * change detection strategy, explicitly marks the view as changed so that\n * it can be checked again.\n *\n * Components are normally marked as dirty (in need of rerendering) when inputs\n * have changed or events have fired in the view. Call this method to ensure that\n * a component is checked even if these triggers have not occurred.\n *\n * <!-- TODO: Add a link to a chapter on OnPush components -->\n *\n */",
"memberType": "method",
"memberTags": ["abstract", "override"]
},
{
"name": "detach",
"signatures": [
{
"name": "detach",
"entryType": "function",
"description": "Detaches this view from the change-detection tree.\nA detached view is not checked until it is reattached.\nUse in combination with `detectChanges()` to implement local change detection checks.\n\nDetached views are not checked during change detection runs until they are\nre-attached, even if they are marked as dirty.\n\n<!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n<!-- TODO: Add a live demo once ref.detectChanges is merged into master -->",
"generics": [],
"isNewType": false,
"jsdocTags": [],
"params": [],
"rawComment": "/**\n * Detaches this view from the change-detection tree.\n * A detached view is not checked until it is reattached.\n * Use in combination with `detectChanges()` to implement local change detection checks.\n *\n * Detached views are not checked during change detection runs until they are\n * re-attached, even if they are marked as dirty.\n *\n * <!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n * <!-- TODO: Add a live demo once ref.detectChanges is merged into master -->\n *\n */",
"returnType": "void"
}
],
"implementation": {
"params": [],
"isNewType": false,
"returnType": "void",
"generics": [],
"name": "detach",
"description": "Detaches this view from the change-detection tree.\nA detached view is not checked until it is reattached.\nUse in combination with `detectChanges()` to implement local change detection checks.\n\nDetached views are not checked during change detection runs until they are\nre-attached, even if they are marked as dirty.\n\n<!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n<!-- TODO: Add a live demo once ref.detectChanges is merged into master -->",
"entryType": "function",
"jsdocTags": [],
"rawComment": "/**\n * Detaches this view from the change-detection tree.\n * A detached view is not checked until it is reattached.\n * Use in combination with `detectChanges()` to implement local change detection checks.\n *\n * Detached views are not checked during change detection runs until they are\n * re-attached, even if they are marked as dirty.\n *\n * <!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n * <!-- TODO: Add a live demo once ref.detectChanges is merged into master -->\n *\n */"
},
"entryType": "function",
"description": "Detaches this view from the change-detection tree.\nA detached view is not checked until it is reattached.\nUse in combination with `detectChanges()` to implement local change detection checks.\n\nDetached views are not checked during change detection runs until they are\nre-attached, even if they are marked as dirty.\n\n<!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n<!-- TODO: Add a live demo once ref.detectChanges is merged into master -->",
"jsdocTags": [],
"rawComment": "/**\n * Detaches this view from the change-detection tree.\n * A detached view is not checked until it is reattached.\n * Use in combination with `detectChanges()` to implement local change detection checks.\n *\n * Detached views are not checked during change detection runs until they are\n * re-attached, even if they are marked as dirty.\n *\n * <!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n * <!-- TODO: Add a live demo once ref.detectChanges is merged into master -->\n *\n */",
"memberType": "method",
"memberTags": ["abstract", "override"]
},
{
"name": "detectChanges",
"signatures": [
{
"name": "detectChanges",
"entryType": "function",
"description": "Checks this view and its children. Use in combination with {@link ChangeDetectorRef#detach}\nto implement local change detection checks.\n\n<!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n<!-- TODO: Add a live demo once ref.detectChanges is merged into master -->",
"generics": [],
"isNewType": false,
"jsdocTags": [],
"params": [],
"rawComment": "/**\n * Checks this view and its children. Use in combination with {@link ChangeDetectorRef#detach}\n * to implement local change detection checks.\n *\n * <!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n * <!-- TODO: Add a live demo once ref.detectChanges is merged into master -->\n *\n */",
"returnType": "void"
}
],
"implementation": {
"params": [],
"isNewType": false,
"returnType": "void",
"generics": [],
"name": "detectChanges",
"description": "Checks this view and its children. Use in combination with {@link ChangeDetectorRef#detach}\nto implement local change detection checks.\n\n<!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n<!-- TODO: Add a live demo once ref.detectChanges is merged into master -->",
"entryType": "function",
"jsdocTags": [],
"rawComment": "/**\n * Checks this view and its children. Use in combination with {@link ChangeDetectorRef#detach}\n * to implement local change detection checks.\n *\n * <!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n * <!-- TODO: Add a live demo once ref.detectChanges is merged into master -->\n *\n */"
},
"entryType": "function",
"description": "Checks this view and its children. Use in combination with {@link ChangeDetectorRef#detach}\nto implement local change detection checks.\n\n<!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n<!-- TODO: Add a live demo once ref.detectChanges is merged into master -->",
"jsdocTags": [],
"rawComment": "/**\n * Checks this view and its children. Use in combination with {@link ChangeDetectorRef#detach}\n * to implement local change detection checks.\n *\n * <!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n * <!-- TODO: Add a live demo once ref.detectChanges is merged into master -->\n *\n */",
"memberType": "method",
"memberTags": ["abstract", "override"]
},
{
"name": "checkNoChanges",
"signatures": [
{
"name": "checkNoChanges",
"entryType": "function",
"description": "Checks the change detector and its children, and throws if any changes are detected.\n\nUse in development mode to verify that running change detection doesn't introduce\nother changes. Calling it in production mode is a noop.",
"generics": [],
"isNewType": false,
"jsdocTags": [
{
"name": "deprecated",
"comment": "This is a test-only API that does not have a place in production interface.\n`checkNoChanges` is already part of an `ApplicationRef` tick when the app is running in dev\nmode. For more granular `checkNoChanges` validation, use `ComponentFixture`."
}
],
"params": [],
"rawComment": "/**\n * Checks the change detector and its children, and throws if any changes are detected.\n *\n * Use in development mode to verify that running change detection doesn't introduce\n * other changes. Calling it in production mode is a noop.\n *\n * @deprecated This is a test-only API that does not have a place in production interface.\n * `checkNoChanges` is already part of an `ApplicationRef` tick when the app is running in dev\n * mode. For more granular `checkNoChanges` validation, use `ComponentFixture`.\n */",
"returnType": "void"
}
],
"implementation": {
"params": [],
"isNewType": false,
"returnType": "void",
"generics": [],
"name": "checkNoChanges",
"description": "Checks the change detector and its children, and throws if any changes are detected.\n\nUse in development mode to verify that running change detection doesn't introduce\nother changes. Calling it in production mode is a noop.",
"entryType": "function",
"jsdocTags": [
{
"name": "deprecated",
"comment": "This is a test-only API that does not have a place in production interface.\n`checkNoChanges` is already part of an `ApplicationRef` tick when the app is running in dev\nmode. For more granular `checkNoChanges` validation, use `ComponentFixture`."
}
],
"rawComment": "/**\n * Checks the change detector and its children, and throws if any changes are detected.\n *\n * Use in development mode to verify that running change detection doesn't introduce\n * other changes. Calling it in production mode is a noop.\n *\n * @deprecated This is a test-only API that does not have a place in production interface.\n * `checkNoChanges` is already part of an `ApplicationRef` tick when the app is running in dev\n * mode. For more granular `checkNoChanges` validation, use `ComponentFixture`.\n */"
},
"entryType": "function",
"description": "Checks the change detector and its children, and throws if any changes are detected.\n\nUse in development mode to verify that running change detection doesn't introduce\nother changes. Calling it in production mode is a noop.",
"jsdocTags": [
{
"name": "deprecated",
"comment": "This is a test-only API that does not have a place in production interface.\n`checkNoChanges` is already part of an `ApplicationRef` tick when the app is running in dev\nmode. For more granular `checkNoChanges` validation, use `ComponentFixture`."
}
],
"rawComment": "/**\n * Checks the change detector and its children, and throws if any changes are detected.\n *\n * Use in development mode to verify that running change detection doesn't introduce\n * other changes. Calling it in production mode is a noop.\n *\n * @deprecated This is a test-only API that does not have a place in production interface.\n * `checkNoChanges` is already part of an `ApplicationRef` tick when the app is running in dev\n * mode. For more granular `checkNoChanges` validation, use `ComponentFixture`.\n */",
"memberType": "method",
"memberTags": ["abstract", "override"]
},
{
"name": "reattach",
"signatures": [
{
"name": "reattach",
"entryType": "function",
"description": "Re-attaches the previously detached view to the change detection tree.\nViews are attached to the tree by default.\n\n<!-- TODO: Add a link to a chapter on detach/reattach/local digest -->",
"generics": [],
"isNewType": false,
"jsdocTags": [],
"params": [],
"rawComment": "/**\n * Re-attaches the previously detached view to the change detection tree.\n * Views are attached to the tree by default.\n *\n * <!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n *\n */",
"returnType": "void"
}
],
"implementation": {
"params": [],
"isNewType": false,
"returnType": "void",
"generics": [],
"name": "reattach",
"description": "Re-attaches the previously detached view to the change detection tree.\nViews are attached to the tree by default.\n\n<!-- TODO: Add a link to a chapter on detach/reattach/local digest -->",
"entryType": "function",
"jsdocTags": [],
"rawComment": "/**\n * Re-attaches the previously detached view to the change detection tree.\n * Views are attached to the tree by default.\n *\n * <!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n *\n */"
},
"entryType": "function",
"description": "Re-attaches the previously detached view to the change detection tree.\nViews are attached to the tree by default.\n\n<!-- TODO: Add a link to a chapter on detach/reattach/local digest -->",
"jsdocTags": [],
"rawComment": "/**\n * Re-attaches the previously detached view to the change detection tree.\n * Views are attached to the tree by default.\n *\n * <!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n *\n */",
"memberType": "method",
"memberTags": ["abstract", "override"]
}
],
"generics": [],
"description": "Represents an Angular view.",
"jsdocTags": [
{
"name": "see",
"comment": "[Change detection usage](/api/core/ChangeDetectorRef?tab=usage-notes)"
},
{"name": "publicApi", "comment": ""}
],
"rawComment": "/**\n * Represents an Angular view.\n *\n * @see [Change detection usage](/api/core/ChangeDetectorRef?tab=usage-notes)\n *\n * @publicApi\n */",
"source": {
"filePath": "/packages/core/src/linker/view_ref.ts",
"startLine": 18,
"endLine": 37
}
},
{
"name": "afterNextRender",
"signatures": [
{
"name": "afterNextRender",
"entryType": "function",
"description": "Register callbacks to be invoked the next time the application finishes rendering, during the\nspecified phases. The available phases are:\n- `earlyRead`\n Use this phase to **read** from the DOM before a subsequent `write` callback, for example to\n perform custom layout that the browser doesn't natively support. Prefer the `read` phase if\n reading can wait until after the write phase. **Never** write to the DOM in this phase.\n- `write`\n Use this phase to **write** to the DOM. **Never** read from the DOM in this phase.\n- `mixedReadWrite`\n Use this phase to read from and write to the DOM simultaneously. **Never** use this phase if\n it is possible to divide the work among the other phases instead.\n- `read`\n Use this phase to **read** from the DOM. **Never** write to the DOM in this phase.\n\n<div class=\"alert is-critical\">\n\nYou should prefer using the `read` and `write` phases over the `earlyRead` and `mixedReadWrite`\nphases when possible, to avoid performance degradation.\n\n</div>\n\nNote that:\n- Callbacks run in the following phase order *once, after the next render*:\n 1. `earlyRead`\n 2. `write`\n 3. `mixedReadWrite`\n 4. `read`\n- Callbacks in the same phase run in the order they are registered.\n- Callbacks run on browser platforms only, they will not run on the server.\n\nThe first phase callback to run as part of this spec will receive no parameters. Each\nsubsequent phase callback in this spec will receive the return value of the previously run\nphase callback as a parameter. This can be used to coordinate work across multiple phases.\n\nAngular is unable to verify or enforce that phases are used correctly, and instead\nrelies on each developer to follow the guidelines documented for each value and\ncarefully choose the appropriate one, refactoring their code if necessary. By doing\nso, Angular is better able to minimize the performance degradation associated with\nmanual DOM access, ensuring the best experience for the end users of your application\nor library.\n\n<div class=\"alert is-important\">\n\nComponents are not guaranteed to be [hydrated](guide/hydration) before the callback runs.\nYou must use caution when directly reading or writing the DOM and layout.\n\n</div>",
"generics": [
{"name": "E", "default": "never"},
{"name": "W", "default": "never"},
{"name": "M", "default": "never"}
],
"isNewType": false,
"jsdocTags": [
{"name": "param", "comment": "The callback functions to register"},
{"name": "param", "comment": "Options to control the behavior of the callback"},
{
"name": "usageNotes",
"comment": "Use `afterNextRender` to read or write the DOM once,\nfor example to initialize a non-Angular library.\n\n### Example\n```ts\n@Component({\n selector: 'my-chart-cmp',\n template: `<div #chart>{{ ... }}</div>`,\n})\nexport class MyChartCmp {\n @ViewChild('chart') chartRef: ElementRef;\n chart: MyChart|null;\n\n constructor() {\n afterNextRender({\n write: () => {\n this.chart = new MyChart(this.chartRef.nativeElement);\n }\n });\n }\n}\n```"
},
{"name": "developerPreview", "comment": ""}
],
"params": [
{
"name": "spec",
"description": "The callback functions to register",
"type": "{ earlyRead?: () => E; write?: (...args: [E] extends [never] ? [] : [E]) => W; mixedReadWrite?: (...args: [W] extends [never] ? [E] extends [never] ? [] : [E] : [W]) => M; read?: (...args: [...] extends [...] ? [...] extends [...] ? [...] extends [...] ? [] : [...] : [...] : [...]) => void; }",
"isOptional": false,
"isRestParam": false
},
{
"name": "options",
"description": "Options to control the behavior of the callback",
"type": "Omit<AfterRenderOptions, \"phase\">",
"isOptional": true,
"isRestParam": false
}
],
"rawComment": "/**\n * Register callbacks to be invoked the next time the application finishes rendering, during the\n * specified phases. The available phases are:\n * - `earlyRead`\n * Use this phase to **read** from the DOM before a subsequent `write` callback, for example to\n * perform custom layout that the browser doesn't natively support. Prefer the `read` phase if\n * reading can wait until after the write phase. **Never** write to the DOM in this phase.\n * - `write`\n * Use this phase to **write** to the DOM. **Never** read from the DOM in this phase.\n * - `mixedReadWrite`\n * Use this phase to read from and write to the DOM simultaneously. **Never** use this phase if\n * it is possible to divide the work among the other phases instead.\n * - `read`\n * Use this phase to **read** from the DOM. **Never** write to the DOM in this phase.\n *\n * <div class=\"alert is-critical\">\n *\n * You should prefer using the `read` and `write` phases over the `earlyRead` and `mixedReadWrite`\n * phases when possible, to avoid performance degradation.\n *\n * </div>\n *\n * Note that:\n * - Callbacks run in the following phase order *once, after the next render*:\n * 1. `earlyRead`\n * 2. `write`\n * 3. `mixedReadWrite`\n * 4. `read`\n * - Callbacks in the same phase run in the order they are registered.\n * - Callbacks run on browser platforms only, they will not run on the server.\n *\n * The first phase callback to run as part of this spec will receive no parameters. Each\n * subsequent phase callback in this spec will receive the return value of the previously run\n * phase callback as a parameter. This can be used to coordinate work across multiple phases.\n *\n * Angular is unable to verify or enforce that phases are used correctly, and instead\n * relies on each developer to follow the guidelines documented for each value and\n * carefully choose the appropriate one, refactoring their code if necessary. By doing\n * so, Angular is better able to minimize the performance degradation associated with\n * manual DOM access, ensuring the best experience for the end users of your application\n * or library.\n *\n * <div class=\"alert is-important\">\n *\n * Components are not guaranteed to be [hydrated](guide/hydration) before the callback runs.\n * You must use caution when directly reading or writing the DOM and layout.\n *\n * </div>\n *\n * @param spec The callback functions to register\n * @param options Options to control the behavior of the callback\n *\n * @usageNotes\n *\n * Use `afterNextRender` to read or write the DOM once,\n * for example to initialize a non-Angular library.\n *\n * ### Example\n * ```ts\n * @Component({\n * selector: 'my-chart-cmp',\n * template: `<div #chart>{{ ... }}</div>`,\n * })\n * export class MyChartCmp {\n * @ViewChild('chart') chartRef: ElementRef;\n * chart: MyChart|null;\n *\n * constructor() {\n * afterNextRender({\n * write: () => {\n * this.chart = new MyChart(this.chartRef.nativeElement);\n * }\n * });\n * }\n * }\n * ```\n *\n * @developerPreview\n */",
"returnType": "AfterRenderRef"
},
{
"name": "afterNextRender",
"entryType": "function",
"description": "Register a callback to be invoked the next time the application finishes rendering, during the\n`mixedReadWrite` phase.\n\n<div class=\"alert is-critical\">\n\nYou should prefer specifying an explicit phase for the callback instead, or you risk significant\nperformance degradation.\n\n</div>\n\nNote that the callback will run\n- in the order it was registered\n- on browser platforms only\n- during the `mixedReadWrite` phase\n\n<div class=\"alert is-important\">\n\nComponents are not guaranteed to be [hydrated](guide/hydration) before the callback runs.\nYou must use caution when directly reading or writing the DOM and layout.\n\n</div>",
"generics": [],
"isNewType": false,
"jsdocTags": [
{"name": "param", "comment": "A callback function to register"},
{"name": "param", "comment": "Options to control the behavior of the callback"},
{
"name": "usageNotes",
"comment": "Use `afterNextRender` to read or write the DOM once,\nfor example to initialize a non-Angular library.\n\n### Example\n```ts\n@Component({\n selector: 'my-chart-cmp',\n template: `<div #chart>{{ ... }}</div>`,\n})\nexport class MyChartCmp {\n @ViewChild('chart') chartRef: ElementRef;\n chart: MyChart|null;\n\n constructor() {\n afterNextRender({\n write: () => {\n this.chart = new MyChart(this.chartRef.nativeElement);\n }\n });\n }\n}\n```"
},
{"name": "developerPreview", "comment": ""}
],
"params": [
{
"name": "callback",
"description": "A callback function to register",
"type": "VoidFunction",
"isOptional": false,
"isRestParam": false
},
{
"name": "options",
"description": "Options to control the behavior of the callback",
"type": "AfterRenderOptions",
"isOptional": true,
"isRestParam": false
}
],
"rawComment": "/**\n * Register a callback to be invoked the next time the application finishes rendering, during the\n * `mixedReadWrite` phase.\n *\n * <div class=\"alert is-critical\">\n *\n * You should prefer specifying an explicit phase for the callback instead, or you risk significant\n * performance degradation.\n *\n * </div>\n *\n * Note that the callback will run\n * - in the order it was registered\n * - on browser platforms only\n * - during the `mixedReadWrite` phase\n *\n * <div class=\"alert is-important\">\n *\n * Components are not guaranteed to be [hydrated](guide/hydration) before the callback runs.\n * You must use caution when directly reading or writing the DOM and layout.\n *\n * </div>\n *\n * @param callback A callback function to register\n * @param options Options to control the behavior of the callback\n *\n * @usageNotes\n *\n * Use `afterNextRender` to read or write the DOM once,\n * for example to initialize a non-Angular library.\n *\n * ### Example\n * ```ts\n * @Component({\n * selector: 'my-chart-cmp',\n * template: `<div #chart>{{ ... }}</div>`,\n * })\n * export class MyChartCmp {\n * @ViewChild('chart') chartRef: ElementRef;\n * chart: MyChart|null;\n *\n * constructor() {\n * afterNextRender({\n * write: () => {\n * this.chart = new MyChart(this.chartRef.nativeElement);\n * }\n * });\n * }\n * }\n * ```\n *\n * @developerPreview\n */",
"returnType": "AfterRenderRef"
}
],
"implementation": {
"params": [
{
"name": "callbackOrSpec",
"description": "",
"type": "VoidFunction | { earlyRead?: () => unknown; write?: (r?: unknown) => unknown; mixedReadWrite?: (r?: unknown) => unknown; read?: (r?: unknown) => void; }",
"isOptional": false,
"isRestParam": false
},
{
"name": "options",
"description": "",
"type": "AfterRenderOptions",
"isOptional": true,
"isRestParam": false
}
],
"isNewType": false,
"returnType": "AfterRenderRef",
"generics": [],
"name": "afterNextRender",
"description": "",
"entryType": "function",
"jsdocTags": [],
"rawComment": ""
},
"entryType": "function",
"description": "",
"jsdocTags": [],
"rawComment": "",
"source": {
"fileName": "/packages/core/src/user-profile.ts",
"line": 3,
"character": 7
"filePath": "/packages/core/src/render3/after_render_hooks.ts",
"startLine": 446,
"endLine": 454
}
},
{
"name": "provideClientHydration",
"signatures": [
{
"name": "provideClientHydration",
"entryType": "function",
"description": "Sets up providers necessary to enable hydration functionality for the application.\n\nBy default, the function enables the recommended set of features for the optimal\nperformance for most of the applications. It includes the following features:\n\n* Reconciling DOM hydration. Learn more about it [here](guide/hydration).\n* [`HttpClient`](api/common/http/HttpClient) response caching while running on the server and\ntransferring this cache to the client to avoid extra HTTP requests. Learn more about data caching\n[here](guide/ssr#caching-data-when-using-httpclient).\n\nThese functions allow you to disable some of the default features or configure features\n* {@link withNoHttpTransferCache} to disable HTTP transfer cache\n* {@link withHttpTransferCacheOptions} to configure some HTTP transfer cache options",
"generics": [],
"isNewType": false,
"jsdocTags": [
{
"name": "usageNotes",
"comment": "Basic example of how you can enable hydration in your application when\n`bootstrapApplication` function is used:\n```\nbootstrapApplication(AppComponent, {\n providers: [provideClientHydration()]\n});\n```\n\nAlternatively if you are using NgModules, you would add `provideClientHydration`\nto your root app module's provider list.\n```\n@NgModule({\n declarations: [RootCmp],\n bootstrap: [RootCmp],\n providers: [provideClientHydration()],\n})\nexport class AppModule {}\n```"
},
{"name": "see", "comment": "{@link withNoHttpTransferCache}"},
{"name": "see", "comment": "{@link withHttpTransferCacheOptions}"},
{
"name": "param",
"comment": "Optional features to configure additional router behaviors."
},
{"name": "returns", "comment": "A set of providers to enable hydration."},
{"name": "publicApi", "comment": ""}
],
"params": [
{
"name": "features",
"description": "Optional features to configure additional router behaviors.",
"type": "HydrationFeature<HydrationFeatureKind>[]",
"isOptional": false,
"isRestParam": true
}
],
"rawComment": "/**\n * Sets up providers necessary to enable hydration functionality for the application.\n *\n * By default, the function enables the recommended set of features for the optimal\n * performance for most of the applications. It includes the following features:\n *\n * * Reconciling DOM hydration. Learn more about it [here](guide/hydration).\n * * [`HttpClient`](api/common/http/HttpClient) response caching while running on the server and\n * transferring this cache to the client to avoid extra HTTP requests. Learn more about data caching\n * [here](guide/ssr#caching-data-when-using-httpclient).\n *\n * These functions allow you to disable some of the default features or configure features\n * * {@link withNoHttpTransferCache} to disable HTTP transfer cache\n * * {@link withHttpTransferCacheOptions} to configure some HTTP transfer cache options\n *\n * @usageNotes\n *\n * Basic example of how you can enable hydration in your application when\n * `bootstrapApplication` function is used:\n * ```\n * bootstrapApplication(AppComponent, {\n * providers: [provideClientHydration()]\n * });\n * ```\n *\n * Alternatively if you are using NgModules, you would add `provideClientHydration`\n * to your root app module's provider list.\n * ```\n * @NgModule({\n * declarations: [RootCmp],\n * bootstrap: [RootCmp],\n * providers: [provideClientHydration()],\n * })\n * export class AppModule {}\n * ```\n *\n * @see {@link withNoHttpTransferCache}\n * @see {@link withHttpTransferCacheOptions}\n *\n * @param features Optional features to configure additional router behaviors.\n * @returns A set of providers to enable hydration.\n *\n * @publicApi\n */",
"returnType": "EnvironmentProviders"
}
],
"implementation": {
"params": [
{
"name": "features",
"description": "Optional features to configure additional router behaviors.",
"type": "HydrationFeature<HydrationFeatureKind>[]",
"isOptional": false,
"isRestParam": true
}
],
"isNewType": false,
"returnType": "EnvironmentProviders",
"returnDescription": "A set of providers to enable hydration.",
"generics": [],
"name": "provideClientHydration",
"description": "Sets up providers necessary to enable hydration functionality for the application.\n\nBy default, the function enables the recommended set of features for the optimal\nperformance for most of the applications. It includes the following features:\n\n* Reconciling DOM hydration. Learn more about it [here](guide/hydration).\n* [`HttpClient`](api/common/http/HttpClient) response caching while running on the server and\ntransferring this cache to the client to avoid extra HTTP requests. Learn more about data caching\n[here](guide/ssr#caching-data-when-using-httpclient).\n\nThese functions allow you to disable some of the default features or configure features\n* {@link withNoHttpTransferCache} to disable HTTP transfer cache\n* {@link withHttpTransferCacheOptions} to configure some HTTP transfer cache options",
"entryType": "function",
"jsdocTags": [
{
"name": "usageNotes",
"comment": "Basic example of how you can enable hydration in your application when\n`bootstrapApplication` function is used:\n```\nbootstrapApplication(AppComponent, {\n providers: [provideClientHydration()]\n});\n```\n\nAlternatively if you are using NgModules, you would add `provideClientHydration`\nto your root app module's provider list.\n```\n@NgModule({\n declarations: [RootCmp],\n bootstrap: [RootCmp],\n providers: [provideClientHydration()],\n})\nexport class AppModule {}\n```"
},
{"name": "see", "comment": "{@link withNoHttpTransferCache}"},
{"name": "see", "comment": "{@link withHttpTransferCacheOptions}"},
{
"name": "param",
"comment": "Optional features to configure additional router behaviors."
},
{"name": "returns", "comment": "A set of providers to enable hydration."},
{"name": "publicApi", "comment": ""}
],
"rawComment": "/**\n * Sets up providers necessary to enable hydration functionality for the application.\n *\n * By default, the function enables the recommended set of features for the optimal\n * performance for most of the applications. It includes the following features:\n *\n * * Reconciling DOM hydration. Learn more about it [here](guide/hydration).\n * * [`HttpClient`](api/common/http/HttpClient) response caching while running on the server and\n * transferring this cache to the client to avoid extra HTTP requests. Learn more about data caching\n * [here](guide/ssr#caching-data-when-using-httpclient).\n *\n * These functions allow you to disable some of the default features or configure features\n * * {@link withNoHttpTransferCache} to disable HTTP transfer cache\n * * {@link withHttpTransferCacheOptions} to configure some HTTP transfer cache options\n *\n * @usageNotes\n *\n * Basic example of how you can enable hydration in your application when\n * `bootstrapApplication` function is used:\n * ```\n * bootstrapApplication(AppComponent, {\n * providers: [provideClientHydration()]\n * });\n * ```\n *\n * Alternatively if you are using NgModules, you would add `provideClientHydration`\n * to your root app module's provider list.\n * ```\n * @NgModule({\n * declarations: [RootCmp],\n * bootstrap: [RootCmp],\n * providers: [provideClientHydration()],\n * })\n * export class AppModule {}\n * ```\n *\n * @see {@link withNoHttpTransferCache}\n * @see {@link withHttpTransferCacheOptions}\n *\n * @param features Optional features to configure additional router behaviors.\n * @returns A set of providers to enable hydration.\n *\n * @publicApi\n */"
},
"entryType": "function",
"description": "Sets up providers necessary to enable hydration functionality for the application.\n\nBy default, the function enables the recommended set of features for the optimal\nperformance for most of the applications. It includes the following features:\n\n* Reconciling DOM hydration. Learn more about it [here](guide/hydration).\n* [`HttpClient`](api/common/http/HttpClient) response caching while running on the server and\ntransferring this cache to the client to avoid extra HTTP requests. Learn more about data caching\n[here](guide/ssr#caching-data-when-using-httpclient).\n\nThese functions allow you to disable some of the default features or configure features\n* {@link withNoHttpTransferCache} to disable HTTP transfer cache\n* {@link withHttpTransferCacheOptions} to configure some HTTP transfer cache options",
"jsdocTags": [
{
"name": "usageNotes",
"comment": "Basic example of how you can enable hydration in your application when\n`bootstrapApplication` function is used:\n```\nbootstrapApplication(AppComponent, {\n providers: [provideClientHydration()]\n});\n```\n\nAlternatively if you are using NgModules, you would add `provideClientHydration`\nto your root app module's provider list.\n```\n@NgModule({\n declarations: [RootCmp],\n bootstrap: [RootCmp],\n providers: [provideClientHydration()],\n})\nexport class AppModule {}\n```"
},
{"name": "see", "comment": "{@link withNoHttpTransferCache}"},
{"name": "see", "comment": "{@link withHttpTransferCacheOptions}"},
{"name": "param", "comment": "Optional features to configure additional router behaviors."},
{"name": "returns", "comment": "A set of providers to enable hydration."},
{"name": "publicApi", "comment": ""}
],
"rawComment": "/**\n * Sets up providers necessary to enable hydration functionality for the application.\n *\n * By default, the function enables the recommended set of features for the optimal\n * performance for most of the applications. It includes the following features:\n *\n * * Reconciling DOM hydration. Learn more about it [here](guide/hydration).\n * * [`HttpClient`](api/common/http/HttpClient) response caching while running on the server and\n * transferring this cache to the client to avoid extra HTTP requests. Learn more about data caching\n * [here](guide/ssr#caching-data-when-using-httpclient).\n *\n * These functions allow you to disable some of the default features or configure features\n * * {@link withNoHttpTransferCache} to disable HTTP transfer cache\n * * {@link withHttpTransferCacheOptions} to configure some HTTP transfer cache options\n *\n * @usageNotes\n *\n * Basic example of how you can enable hydration in your application when\n * `bootstrapApplication` function is used:\n * ```\n * bootstrapApplication(AppComponent, {\n * providers: [provideClientHydration()]\n * });\n * ```\n *\n * Alternatively if you are using NgModules, you would add `provideClientHydration`\n * to your root app module's provider list.\n * ```\n * @NgModule({\n * declarations: [RootCmp],\n * bootstrap: [RootCmp],\n * providers: [provideClientHydration()],\n * })\n * export class AppModule {}\n * ```\n *\n * @see {@link withNoHttpTransferCache}\n * @see {@link withHttpTransferCacheOptions}\n *\n * @param features Optional features to configure additional router behaviors.\n * @returns A set of providers to enable hydration.\n *\n * @publicApi\n */",
"source": {
"filePath": "/packages/platform-browser/src/hydration.ts",
"startLine": 196,
"endLine": 233
}
}
]
@@ -0,0 +1,52 @@
import {runfiles} from '@bazel/runfiles';
import {readFile} from 'fs/promises';
import {JSDOM} from 'jsdom';
import {renderEntry} from '../rendering';
import {getRenderable} from '../processing';
import {initHighlighter} from '../shiki/shiki';
import {configureMarkedGlobally} from '../marked/configuration';
describe('markdown to html', () => {
const entries = new Map<string, DocumentFragment>();
const entries2 = new Map<string, string>();
beforeAll(async () => {
await initHighlighter();
await configureMarkedGlobally();
const entryContent = await readFile(runfiles.resolvePackageRelative('fake-entries.json'), {
encoding: 'utf-8',
});
const entryJson = JSON.parse(entryContent) as any;
for (const entry of entryJson.entries) {
const renderableJson = getRenderable(entry, '@angular/fakeentry');
const fragment = JSDOM.fragment(await renderEntry(renderableJson));
entries.set(entry['name'], fragment);
entries2.set(entry['name'], await renderEntry(renderableJson));
}
});
it('should render description correctly', () => {
const afterNextRenderEntry = entries.get('afterNextRender')!;
const header = afterNextRenderEntry.querySelector('.docs-reference-header')!;
expect(header).toBeDefined();
expect(header.outerHTML).not.toContain('```');
console.log(entries2.get('afterNextRender'));
const list = afterNextRenderEntry.querySelector('ul')!;
expect(list).toBeDefined();
// List are rendered
expect(list.outerHTML).toContain('<li>');
// Code blocks are rendered
expect(list.outerHTML).toContain('<code>mixedReadWrite</code>');
});
it('should render multiple {@link} blocks', () => {
const provideClientHydrationEntry = entries.get('provideClientHydration')!;
expect(provideClientHydrationEntry).toBeDefined();
const cardItem = provideClientHydrationEntry.querySelector('.docs-reference-card-item ')!;
expect(cardItem.innerHTML).not.toContain('@link');
});
});
@@ -16,7 +16,7 @@ import {
addHtmlUsageNotes,
setEntryFlags,
} from './jsdoc-transforms';
import {addRenderableGroupMembers} from './member-transforms';
import {addRenderableMembers} from './member-transforms';
import {addModuleName} from './module-name';
/** Given an unprocessed class entry, get the fully renderable class entry. */
@@ -26,7 +26,7 @@ export function getClassRenderable(
): ClassEntryRenderable {
return setEntryFlags(
addRenderableCodeToc(
addRenderableGroupMembers(
addRenderableMembers(
addHtmlAdditionalLinks(
addHtmlUsageNotes(
addHtmlJsDocTagComments(addHtmlDescription(addModuleName(classEntry, moduleName))),
@@ -8,12 +8,11 @@
import {
DocEntry,
FunctionEntry,
FunctionSignatureMetadata,
MemberEntry,
MemberTags,
ParameterEntry,
PropertyEntry,
isFunctionEntryWithOverloads,
} from '../entities';
import {
@@ -64,7 +63,9 @@ export function addRenderableCodeToc<T extends DocEntry & HasModuleName>(
const metadata = mapDocEntryToCode(entry);
appendPrefixAndSuffix(entry, metadata);
const codeWithSyntaxHighlighting = codeToHtml(metadata.contents, 'typescript');
const codeWithSyntaxHighlighting = codeToHtml(metadata.contents, 'typescript', {
removeFunctionKeyword: true,
});
// shiki returns the lines wrapped by 2 node : 1 pre node, 1 code node.
// As leveraging jsdom isn't trivial here, we rely on a regex to extract the line nodes
@@ -78,7 +79,7 @@ export function addRenderableCodeToc<T extends DocEntry & HasModuleName>(
const afterCode = match[3];
const lines = splitLines(insideCode);
const groups = groupCodeLines(lines, metadata);
const groups = groupCodeLines(lines, metadata, entry);
return {
...entry,
@@ -89,11 +90,12 @@ export function addRenderableCodeToc<T extends DocEntry & HasModuleName>(
}
/** Group overloaded methods */
function groupCodeLines(lines: string[], metadata: CodeTableOfContentsData) {
function groupCodeLines(lines: string[], metadata: CodeTableOfContentsData, entry: DocEntry) {
const hasSingleSignature = isFunctionEntry(entry) && entry.signatures.length === 1;
return lines.reduce((groups, line, index) => {
const tocItem = {
const tocItem: CodeLineRenderable = {
contents: line,
id: metadata.codeLineNumbersWithIdentifiers.get(index),
id: hasSingleSignature ? undefined : metadata.codeLineNumbersWithIdentifiers.get(index),
isDeprecated: metadata.deprecatedLineNumbers.some((lineNumber) => lineNumber === index),
};
@@ -134,21 +136,26 @@ export function mapDocEntryToCode(entry: DocEntry): CodeTableOfContentsData {
if (isFunctionEntry(entry)) {
const isDeprecated = isDeprecatedEntry(entry);
const codeLineNumbersWithIdentifiers = new Map<number, string>();
const hasSingleSignature = entry.signatures.length === 1;
if (isFunctionEntryWithOverloads(entry) && entry.overloads) {
if (entry.signatures.length > 0) {
const initialMetadata: CodeTableOfContentsData = {
contents: '',
codeLineNumbersWithIdentifiers: new Map<number, string>(),
deprecatedLineNumbers: [],
};
return entry.overloads.reduce(
(acc: CodeTableOfContentsData, curr: FunctionEntry, index: number) => {
return entry.signatures.reduce(
(acc: CodeTableOfContentsData, curr: FunctionSignatureMetadata, index: number) => {
const lineNumber = index;
acc.codeLineNumbersWithIdentifiers.set(lineNumber, curr.name);
acc.contents += `${curr.name}(${curr.params
.map((param) => mapParamEntry(param))
.join(`, `)}): ${curr.returnType}\n`;
acc.codeLineNumbersWithIdentifiers.set(lineNumber, `${curr.name}_${index}`);
acc.contents += getMethodCodeLine(curr, [], hasSingleSignature, true);
// We don't want to add line break after the last item
if (!hasSingleSignature && index < entry.signatures.length - 1) {
acc.contents += '\n';
}
if (isDeprecatedEntry(curr)) {
acc.deprecatedLineNumbers.push(lineNumber);
}
@@ -157,9 +164,10 @@ export function mapDocEntryToCode(entry: DocEntry): CodeTableOfContentsData {
initialMetadata,
);
}
return {
// It is important to add the function keyword as shiki will only highlight valid ts
contents: `function ${getMethodCodeLine(entry, [], true)}`,
contents: `function ${getMethodCodeLine(entry.implementation, [], true)}`,
codeLineNumbersWithIdentifiers,
deprecatedLineNumbers: isDeprecated ? [0] : [],
};
@@ -248,22 +256,35 @@ function getCodeTocData(members: MemberEntry[], hasPrefixLine: boolean): CodeTab
// In case when hasPrefixLine is true we should take it into account when we're generating
// `codeLineNumbersWithIdentifiers` below.
const skip = !!hasPrefixLine ? 1 : 0;
let lineNumber = skip;
return members.reduce((acc: CodeTableOfContentsData, curr: MemberEntry, index: number) => {
const lineNumber = index + skip;
acc.codeLineNumbersWithIdentifiers.set(lineNumber, curr.name);
acc.contents += ` ${getCodeLine(curr).trim()}\n`;
if (isDeprecatedEntry(curr)) {
acc.deprecatedLineNumbers.push(lineNumber);
const setTocData = (entry: DocEntry | MemberEntry, content: string) => {
acc.contents += ` ${content.trim()}\n`;
acc.codeLineNumbersWithIdentifiers.set(lineNumber, entry.name);
if (isDeprecatedEntry(entry)) {
acc.deprecatedLineNumbers.push(lineNumber);
}
lineNumber++;
};
if (isClassMethodEntry(curr)) {
if (curr.signatures.length > 0) {
curr.signatures.forEach((signature) => {
setTocData(signature, getMethodCodeLine(signature, curr.memberTags));
});
} else {
setTocData(curr, getMethodCodeLine(curr.implementation, curr.memberTags));
}
} else {
setTocData(curr, getCodeLine(curr));
}
return acc;
}, initialMetadata);
}
function getCodeLine(member: MemberEntry) {
if (isClassMethodEntry(member)) {
return getMethodCodeLine(member, member.memberTags);
} else if (isGetterEntry(member)) {
function getCodeLine(member: MemberEntry): string {
if (isGetterEntry(member)) {
return getGetterCodeLine(member);
} else if (isSetterEntry(member)) {
return getSetterCodeLine(member);
@@ -281,11 +302,12 @@ function getPropertyCodeLine(member: PropertyEntry): string {
/** Map method entry to text */
function getMethodCodeLine(
member: FunctionEntry,
member: FunctionSignatureMetadata,
memberTags: MemberTags[] = [],
displayParamsInNewLines: boolean = false,
isFunction: boolean = false,
): string {
return `${memberTags.join(' ')} ${member.name}(${displayParamsInNewLines ? '\n ' : ''}${member.params
return `${isFunction ? 'function' : ''}${memberTags.join(' ')} ${member.name}(${displayParamsInNewLines ? '\n ' : ''}${member.params
.map((param) => mapParamEntry(param))
.join(`,${displayParamsInNewLines ? '\n ' : ' '}`)}${
displayParamsInNewLines ? '\n' : ''
@@ -344,7 +366,7 @@ function getNumberOfLinesOfCode(contents: string): number {
/** Prints an initializer function signature into a single line. */
export function printInitializerFunctionSignatureLine(
name: string,
signature: FunctionEntry,
signature: FunctionSignatureMetadata,
showTypesInSignaturePreview: boolean,
): string {
let res = name;
@@ -381,7 +403,7 @@ export function printInitializerFunctionSignatureLine(
res += `: ${signature.returnType}`;
}
res += ';';
return res;
return `function ${res}`;
}
function appendPrefixAndSuffix(entry: DocEntry, codeTocData: CodeTableOfContentsData): void {
@@ -6,9 +6,11 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {FunctionEntry, isFunctionEntryWithOverloads} from '../entities';
import {FunctionEntryRenderable} from '../entities/renderables';
import {HasRenderableOverloads} from '../entities/traits';
import {FunctionEntry, FunctionSignatureMetadata} from '../entities';
import {
FunctionEntryRenderable,
FunctionSignatureMetadataRenderable,
} from '../entities/renderables';
import {addRenderableCodeToc} from './code-transforms';
import {
addHtmlAdditionalLinks,
@@ -27,15 +29,10 @@ export function getFunctionRenderable(
): FunctionEntryRenderable {
return setEntryFlags(
addRenderableCodeToc(
addRenderableFunctionParams(
addOverloads(
moduleName,
addHtmlAdditionalLinks(
addHtmlUsageNotes(
setEntryFlags(
addHtmlJsDocTagComments(addHtmlDescription(addModuleName(entry, moduleName))),
),
),
addHtmlAdditionalLinks(
addHtmlUsageNotes(
setEntryFlags(
addHtmlJsDocTagComments(addHtmlDescription(addModuleName(entry, moduleName))),
),
),
),
@@ -43,15 +40,17 @@ export function getFunctionRenderable(
);
}
function addOverloads<T extends FunctionEntry>(
moduleName: string,
entry: T,
): T & HasRenderableOverloads {
return {
...entry,
overloads:
isFunctionEntryWithOverloads(entry) && entry.overloads
? entry.overloads.map((overload) => getFunctionRenderable(overload, moduleName))
: null,
};
export function getFunctionMetadataRenderable(
entry: FunctionSignatureMetadata,
moduleName: string = '',
): FunctionSignatureMetadataRenderable {
return addHtmlAdditionalLinks(
addRenderableFunctionParams(
addHtmlUsageNotes(
setEntryFlags(
addHtmlJsDocTagComments(addHtmlDescription(addModuleName(entry, moduleName))),
),
),
),
);
}
@@ -16,7 +16,7 @@ import {
addHtmlUsageNotes,
setEntryFlags,
} from './jsdoc-transforms';
import {addRenderableGroupMembers} from './member-transforms';
import {addRenderableMembers} from './member-transforms';
import {addModuleName} from './module-name';
/** Given an unprocessed interface entry, get the fully renderable interface entry. */
@@ -26,7 +26,7 @@ export function getInterfaceRenderable(
): InterfaceEntryRenderable {
return setEntryFlags(
addRenderableCodeToc(
addRenderableGroupMembers(
addRenderableMembers(
addHtmlAdditionalLinks(
addHtmlUsageNotes(
addHtmlJsDocTagComments(addHtmlDescription(addModuleName(entry, moduleName))),
@@ -37,6 +37,7 @@ export const JS_DOC_DESCRIPTION_TAG = 'description';
// Some links are written in the following format: {@link Route}
const jsDoclinkRegex = /\{\s*@link\s+([^}]+)\s*\}/;
const jsDoclinkRegexGlobal = /\{\s*@link\s+([^}]+)\s*\}/g;
/** Given an entity with a description, gets the entity augmented with an `htmlDescription`. */
export function addHtmlDescription<T extends HasDescription & HasModuleName>(
@@ -196,7 +197,7 @@ function convertJsDocExampleToHtmlExample(text: string): string {
}
function convertLinks(text: string, entry: HasModuleName) {
return text.replace(jsDoclinkRegex, (_, link) => {
return text.replace(jsDoclinkRegexGlobal, (_, link) => {
const [symbol, description] = link.split(/\s(.+)/);
if (symbol && description) {
// {@link Route Some route with description}
@@ -8,14 +8,7 @@
import {MemberEntry, MemberTags, MemberType} from '../entities';
import {isClassMethodEntry} from '../entities/categorization';
import {MemberEntryRenderable} from '../entities/renderables';
import {
HasMembers,
HasModuleName,
HasRenderableMembers,
HasRenderableMembersGroups,
} from '../entities/traits';
import {HasMembers, HasModuleName, HasRenderableMembers} from '../entities/traits';
import {
addHtmlDescription,
@@ -69,44 +62,13 @@ export function mergeGettersAndSetters(members: MemberEntry[]): MemberEntry[] {
);
}
/** Given an entity with members, gets the entity augmented with renderable members. */
export function addRenderableGroupMembers<T extends HasMembers & HasModuleName>(
entry: T,
): T & HasRenderableMembersGroups {
const members = filterLifecycleMethods(entry.members);
const membersGroups = members.reduce((groups, item) => {
const member = setEntryFlags(
addMethodParamsDescription(
addHtmlDescription(
addHtmlUsageNotes(addHtmlJsDocTagComments(addModuleName(item, entry.moduleName))),
),
),
);
if (groups.has(member.name)) {
const group = groups.get(member.name);
group?.push(member);
} else {
groups.set(member.name, [member]);
}
return groups;
}, new Map<string, MemberEntryRenderable[]>());
return {
...entry,
membersGroups,
};
}
export function addRenderableMembers<T extends HasMembers & HasModuleName>(
entry: T,
): T & HasRenderableMembers {
const members = entry.members.map((member) =>
setEntryFlags(
addMethodParamsDescription(
addHtmlDescription(
addHtmlUsageNotes(addHtmlJsDocTagComments(addModuleName(member, entry.moduleName))),
),
addHtmlDescription(
addHtmlUsageNotes(addHtmlJsDocTagComments(addModuleName(member, entry.moduleName))),
),
),
);
@@ -116,15 +78,3 @@ export function addRenderableMembers<T extends HasMembers & HasModuleName>(
members,
};
}
function addMethodParamsDescription<T extends MemberEntry & HasModuleName>(entry: T): T {
if (isClassMethodEntry(entry)) {
return {
...entry,
params: entry.params.map((param) =>
addHtmlDescription(addModuleName(param, entry.moduleName)),
),
};
}
return entry;
}
@@ -18,8 +18,8 @@ export interface DocsCodeToken extends CodeToken {
// Capture group 1: all attributes on the opening tag
// Capture group 2: all content between the open and close tags
const singleFileSelfClosingCodeRule = /^\s*<docs-code\s([^>]*)((?:.(?!\/>))*)\/>/s;
const singleFileCodeRule = /^\s*<docs-code\s([^>]*)>((?:.(?!\/docs-code))*)<\/docs-code>/s;
const singleFileCodeRule =
/^\s*<docs-code((?:\s+[\w-]+(?:="[^"]*"|='[^']*'|=[^\s>]*)?)*)\s*(?:\/>|>(.*?)<\/docs-code>)/s;
const pathRule = /path="([^"]*)"/;
const headerRule = /header="([^"]*)"/;
@@ -38,10 +38,7 @@ export const docsCodeExtension = {
return src.match(/^<docs-code\s/)?.index;
},
tokenizer(this: TokenizerThis, src: string): DocsCodeToken | undefined {
const code = singleFileCodeRule.exec(src);
const selfClosingCode = singleFileSelfClosingCodeRule.exec(src);
const match = selfClosingCode ?? code;
const match = singleFileCodeRule.exec(src);
if (match) {
const attr = match[1].trim();
@@ -55,7 +52,7 @@ export const docsCodeExtension = {
const visibleRegion = visibleRegionRule.exec(attr);
const preview = previewRule.exec(attr) ? true : false;
let code = match[2].trim();
let code = match[2]?.trim() ?? '';
if (path && path[1]) {
code = loadWorkspaceRelativeFile(path[1]);
// Remove ESLint Comments
@@ -7,7 +7,7 @@
*/
import {Token, Tokens, RendererThis, TokenizerThis} from 'marked';
import {headingRender} from '../../tranformations/heading';
import {formatHeading, headingRender} from '../../tranformations/heading';
interface DocsStepToken extends Tokens.Generic {
type: 'docs-step';
@@ -51,7 +51,7 @@ export const docsStepExtension = {
return `
<li>
<span class="docs-step-number" aria-hidden="true"></span>
${headingRender(token.title, 3, token.title)}
${formatHeading({text: token.title, depth: 3})}
${this.parser.parse(token.tokens)}
</li>
`;
+2 -3
View File
@@ -8,7 +8,7 @@
import {marked} from 'marked';
import {hooks} from './hooks';
import {renderer} from './renderer';
import {Renderer} from './renderer';
import {docsAlertExtension} from './extensions/docs-alert';
import {docsCalloutExtension} from './extensions/docs-callout';
import {docsPillExtension} from './extensions/docs-pill/docs-pill';
@@ -33,7 +33,6 @@ export async function parseMarkdown(
marked.use({
hooks,
renderer,
extensions: [
docsAlertExtension,
docsCalloutExtension,
@@ -55,5 +54,5 @@ export async function parseMarkdown(
async: true,
});
return marked.parse(markdownContent);
return marked.parse(markdownContent, {renderer: new Renderer()});
}
+9 -10
View File
@@ -1,20 +1,19 @@
import {RendererObject} from 'marked';
import {Renderer as _Renderer} from 'marked';
import {linkRender} from './tranformations/link';
import {tableRender} from './tranformations/table';
import {listRender} from './tranformations/list';
import {imageRender} from './tranformations/image';
import {textRender} from './tranformations/text';
import {headingRender} from './tranformations/heading';
/**
* Custom renderer for marked that will be used to transform markdown files to HTML
* files that can be used in the Angular docs.
*/
export const renderer: RendererObject = {
link: linkRender,
table: tableRender,
list: listRender,
image: imageRender,
text: textRender,
heading: headingRender,
};
export class Renderer extends _Renderer {
override link = linkRender;
override table = tableRender;
override list = listRender;
override image = imageRender;
override text = textRender;
override heading = headingRender;
}
@@ -8,3 +8,5 @@ this is code
<docs-code path="adev/shared-docs/pipeline/guides/testing/docs-code/new-code.ts"
diff="adev/shared-docs/pipeline/guides/testing/docs-code/old-code.ts" />
<docs-code header="src/locale/messages.fr.xlf (<trans-unit>)" path="docs/pipeline/guides/testing/docs-code/messages.fr.xlf.html" />
@@ -52,4 +52,9 @@ describe('markdown to html', () => {
expect(codeLines[2].classList.contains('add')).toBeFalse();
expect(codeLines[2].classList.contains('remove')).toBeFalse();
});
it('should load header and html code', () => {
const codeBlock = markdownDocument.querySelectorAll('code')[4];
expect(codeBlock).toBeTruthy();
});
});
@@ -0,0 +1,78 @@
<!-- The `messages.fr.xlf` after translation for documentation purposes -->
<!-- #docregion -->
<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="ng2.template">
<body>
<!-- #docregion translated-hello-before -->
<trans-unit id="introductionHeader" datatype="html">
<source>Hello i18n!</source>
<note priority="1" from="description">An introduction header for this sample</note>
<note priority="1" from="meaning">User welcome</note>
</trans-unit>
<!-- #enddocregion translated-hello-before -->
<!-- #docregion translated-hello -->
<!-- #docregion custom-id -->
<trans-unit id="introductionHeader" datatype="html">
<!-- #enddocregion custom-id -->
<source>Hello i18n!</source>
<target>Bonjour i18n !</target>
<note priority="1" from="description">An introduction header for this sample</note>
<note priority="1" from="meaning">User welcome</note>
</trans-unit>
<!-- #enddocregion translated-hello -->
<!-- #docregion translated-other-nodes -->
<!-- #docregion generated-id -->
<trans-unit id="ba0cc104d3d69bf669f97b8d96a4c5d8d9559aa3" datatype="html">
<!-- #enddocregion generated-id -->
<source>I don&apos;t output any element</source>
<target>Je n'affiche aucun élément</target>
</trans-unit>
<trans-unit id="701174153757adf13e7c24a248c8a873ac9f5193" datatype="html">
<source>Angular logo</source>
<target>Logo d'Angular</target>
</trans-unit>
<!-- #enddocregion translated-other-nodes -->
<!-- #docregion translated-plural -->
<trans-unit id="5a134dee893586d02bffc9611056b9cadf9abfad" datatype="html">
<source>{VAR_PLURAL, plural, =0 {just now} =1 {one minute ago} other {<x id="INTERPOLATION" equiv-text="{{minutes}}"/> minutes ago} }</source>
<target>{VAR_PLURAL, plural, =0 {à l'instant} =1 {il y a une minute} other {il y a <x id="INTERPOLATION" equiv-text="{{minutes}}"/> minutes} }</target>
</trans-unit>
<!-- #enddocregion translated-plural -->
<!-- #docregion translated-select -->
<!-- #docregion translate-select-1 -->
<trans-unit id="f99f34ac9bd4606345071bd813858dec29f3b7d1" datatype="html">
<source>The author is <x id="ICU" equiv-text="{gender, select, male {...} female {...} other {...}}"/></source>
<target>L'auteur est <x id="ICU" equiv-text="{gender, select, male {...} female {...} other {...}}"/></target>
</trans-unit>
<!-- #enddocregion translate-select-1 -->
<!-- #docregion translate-select-2 -->
<trans-unit id="eff74b75ab7364b6fa888f1cbfae901aaaf02295" datatype="html">
<source>{VAR_SELECT, select, male {male} female {female} other {other} }</source>
<target>{VAR_SELECT, select, male {un homme} female {une femme} other {autre} }</target>
</trans-unit>
<!-- #enddocregion translate-select-2 -->
<!-- #enddocregion translated-select -->
<!-- #docregion translate-nested -->
<!-- #docregion translate-nested-1 -->
<trans-unit id="972cb0cf3e442f7b1c00d7dab168ac08d6bdf20c" datatype="html">
<source>Updated: <x id="ICU" equiv-text="{minutes, plural, =0 {...} =1 {...} other {...}}"/></source>
<target>Mis à jour: <x id="ICU" equiv-text="{minutes, plural, =0 {...} =1 {...} other {...}}"/></target>
</trans-unit>
<!-- #enddocregion translate-nested-1 -->
<!-- #docregion translate-nested-2 -->
<trans-unit id="7151c2e67748b726f0864fc443861d45df21d706" datatype="html">
<source>{VAR_PLURAL, plural, =0 {just now} =1 {one minute ago} other {<x id="INTERPOLATION" equiv-text="{{minutes}}"/> minutes ago by {VAR_SELECT, select, male {male} female {female} other {other} }} }</source>
<target>{VAR_PLURAL, plural, =0 {à l'instant} =1 {il y a une minute} other {il y a <x id="INTERPOLATION" equiv-text="{{minutes}}"/> minutes par {VAR_SELECT, select, male {un homme} female {une femme} other {autre} }} }</target>
</trans-unit>
<!-- #enddocregion translate-nested-2 -->
<!-- #enddocregion translate-nested -->
<!-- #docregion i18n-duplicate-custom-id -->
<trans-unit id="myId" datatype="html">
<source>Hello</source>
<target state="new">Bonjour</target>
</trans-unit>
<!-- #enddocregion i18n-duplicate-custom-id -->
</body>
</file>
</xliff>
@@ -9,4 +9,5 @@
## Duplicate Anchor
## `myClass.myMethod` is the best
## ステップ 2 - アプリケーションのレイアウトに新しいコンポーネントを追加
## My heading {# my-custom-id }
## My heading {# my-custom-id }
## Query for the `<h1>`
@@ -83,4 +83,15 @@ describe('markdown to html', () => {
expect(h2HeaderId).toBe('my-custom-id');
expect(h2AnchorHref).toBe(`#${h2HeaderId}`);
});
it('should be able to parse heading with a valid tag in a code block', () => {
const h2List = markdownDocument.querySelectorAll('h2');
const h2 = h2List[6];
// The anchor element should be to only child
expect(h2.children.length).toBe(1);
expect(h2.firstElementChild?.tagName).toBe('A');
expect(h2.firstElementChild!.innerHTML).toBe('Query for the <code>&lt;h1&gt;</code>');
});
});
@@ -7,4 +7,6 @@
- Order
- here
- matter
- doesn't
- doesn't
- [some link](https://angular.dev)
- Code block `SomeClass`
@@ -21,7 +21,16 @@ describe('markdown to html', () => {
const unorderedList = markdownDocument.querySelector('ul');
expect(unorderedList?.className).toBe('docs-list');
expect(unorderedList?.childElementCount).toBe(4);
expect(unorderedList?.childElementCount).toBe(6);
expect(unorderedList?.textContent).toContain('matter');
});
it('should render list items', () => {
const unorderedList = markdownDocument.querySelector('ul');
const linkItem = unorderedList!.children[4];
expect(linkItem.outerHTML).toContain('href="https://angular.dev"');
const codeItem = unorderedList!.children[5];
expect(codeItem.outerHTML).toContain('<code>SomeClass</code>');
});
});
@@ -6,13 +6,18 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {RendererApi} from 'marked';
import {Renderer, Tokens} from 'marked';
import {getHeaderId} from '../state';
import {getPageTitle} from '../utils';
export const headingRender: RendererApi['heading'] = (text, level, raw) => {
if (level === 1) {
export function headingRender(this: Renderer, {depth, tokens}: Tokens.Heading): string {
const text = this?.parser.parseInline(tokens);
return formatHeading({text, depth});
}
export function formatHeading({text, depth}: {text: string; depth: number}): string {
if (depth === 1) {
return `
<header class="docs-header">
<docs-breadcrumb></docs-breadcrumb>
@@ -36,8 +41,8 @@ export const headingRender: RendererApi['heading'] = (text, level, raw) => {
const label = anchorLessText.replace(/`(.*?)`/g, '<code>$1</code>').replace(customIdRegex, '');
return `
<h${level} id="${link}">
<h${depth} id="${link}">
<a href="#${link}" class="docs-anchor" tabindex="-1" aria-label="Link to ${label}">${label}</a>
</h${level}>
</h${depth}>
`;
};
}
@@ -7,15 +7,16 @@
*/
import {normalize} from 'path';
import {RendererApi} from 'marked';
import {Renderer, Tokens} from 'marked';
// TODO(josephperrott): Determine how we can define/know the image content base path.
const imageContentBasePath = 'unknown';
export const imageRender: RendererApi['image'] = (href, title, text) => {
export function imageRender(this: Renderer, {href, title, text}: Tokens.Image) {
const isRelativeSrc = href?.startsWith('./');
const src = isRelativeSrc ? `${imageContentBasePath}/${normalize(href)}` : href;
return `
<img src="${src}" alt="${text}" title="${title}" class="docs-image">
`;
};
}
@@ -7,9 +7,9 @@
*/
import {anchorTarget} from '../helpers';
import {RendererApi} from 'marked';
import {Renderer, Tokens} from 'marked';
export const linkRender: RendererApi['link'] = (href, title, text) => {
export function linkRender(this: Renderer, {href, title, tokens}: Tokens.Link) {
const titleAttribute = title ? ` title=${title}` : '';
return `<a href="${href}"${titleAttribute}${anchorTarget(href)}>${text}</a>`;
};
return `<a href="${href}"${titleAttribute}${anchorTarget(href)}>${this.parser.parseInline(tokens)}</a>`;
}
@@ -6,19 +6,19 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {RendererApi} from 'marked';
import {Renderer, Tokens} from 'marked';
export const listRender: RendererApi['list'] = (body, ordered, start) => {
export function listRender(this: Renderer, {items, ordered}: Tokens.List) {
if (ordered) {
return `
<ol class="docs-ordered-list">
${body}
${items.map((item) => this.listitem(item)).join('')}
</ol>
`;
}
return `
<ul class="docs-list">
${body}
${items.map((item) => this.listitem(item)).join('')}
</ul>
`;
};
}
@@ -6,19 +6,27 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {RendererApi} from 'marked';
import {Renderer, Tokens} from 'marked';
export const tableRender: RendererApi['table'] = (header, body) => {
export function tableRender(this: Renderer, {header, rows}: Tokens.Table) {
return `
<div class="docs-table docs-scroll-track-transparent">
<table>
<thead>
${header}
${this.tablerow({
text: header.map((cell) => this.tablecell(cell)).join(''),
})}
</thead>
<tbody>
${body}
${rows
.map((row) =>
this.tablerow({
text: row.map((cell) => this.tablecell(cell)).join(''),
}),
)
.join('')}
</tbody>
</table>
</div>
`;
};
}
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {RendererApi} from 'marked';
import {Renderer, Tokens} from 'marked';
import emojiRegex from 'emoji-regex';
/** Regex to find unicode emojis. */
@@ -15,8 +15,9 @@ const UNICODE_EMOJI_REGEX = /&#x[\dA-Fa-f]+;/g;
/** Regex to find emojis. */
const regex = emojiRegex();
export const textRender: RendererApi['text'] = (text) => {
return regex.test(text) || UNICODE_EMOJI_REGEX.test(text)
export function textRender(this: Renderer, token: Tokens.Text) {
const text = token.tokens ? this.parser.parseInline(token.tokens) : token.text;
return regex.test(token.text) || UNICODE_EMOJI_REGEX.test(token.text)
? `<span class="docs-emoji">${text}</span>`
: text;
};
}
@@ -43,6 +43,32 @@ export class Search {
? from(
this.index.search(query, {
maxValuesPerFacet: MAX_VALUE_PER_FACET,
attributesToRetrieve: [
'hierarchy.lvl0',
'hierarchy.lvl1',
'hierarchy.lvl2',
'hierarchy.lvl3',
'hierarchy.lvl4',
'hierarchy.lvl5',
'hierarchy.lvl6',
'content',
'type',
'url',
],
hitsPerPage: 20,
snippetEllipsisText: '…',
highlightPreTag: '<ɵ>',
highlightPostTag: '</ɵ>',
attributesToHighlight: [],
attributesToSnippet: [
'hierarchy.lvl1:10',
'hierarchy.lvl2:10',
'hierarchy.lvl3:10',
'hierarchy.lvl4:10',
'hierarchy.lvl5:10',
'hierarchy.lvl6:10',
'content:10',
],
}),
)
: of(undefined);
@@ -72,6 +98,20 @@ export class Search {
const uniqueUrls = new Set<string>();
return items.filter((item) => {
if (item.type === 'content' && !item._snippetResult.content) {
return false;
}
// Ensure that this result actually matched on the type.
// If not, this is going to be a duplicate. There should be another result in
// the list that already matched on its type.
// A lvl2 match will also return all its lvl3 results as well, even if those
// values don't also match the query.
if (
item.type.indexOf('lvl') === 0 &&
item._snippetResult.hierarchy?.[item.type as 'lvl1']?.matchLevel === 'none'
) {
return false;
}
if (item.url && !uniqueUrls.has(item.url)) {
uniqueUrls.add(item.url);
return true;
@@ -14,7 +14,6 @@ import {
afterNextRender,
inject,
signal,
NgZone,
} from '@angular/core';
import {RESIZE_EVENT_DELAY} from '../constants/index';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
@@ -34,7 +33,6 @@ export class TableOfContentsScrollSpy {
private readonly tableOfContentsLoader = inject(TableOfContentsLoader);
private readonly document = inject(DOCUMENT);
private readonly window = inject(WINDOW);
private readonly ngZone = inject(NgZone);
private readonly viewportScroller = inject(ViewportScroller);
private readonly injector = inject(EnvironmentInjector);
private contentSourceElement: HTMLElement | null = null;
@@ -112,9 +110,7 @@ export class TableOfContentsScrollSpy {
takeUntilDestroyed(this.destroyRef),
);
this.ngZone.runOutsideAngular(() => {
scroll$.subscribe(() => this.setActiveItemId());
});
scroll$.subscribe(() => this.setActiveItemId());
}
private setActiveItemId(): void {
+22 -17
View File
@@ -90,21 +90,21 @@ $theme: mat.m2-define-light-theme(
}
}
.docs-dark-mode .shiki {
color: var(--shiki-dark) ;
background-color: var(--shiki-dark-bg) ;
color: var(--shiki-dark);
background-color: var(--shiki-dark-bg);
span {
color: var(--shiki-dark) ;
background-color: var(--shiki-dark-bg) ;
color: var(--shiki-dark);
background-color: var(--shiki-dark-bg);
/* Optional, if you also want font styles */
font-style: var(--shiki-dark-font-style) ;
font-weight: var(--shiki-dark-font-weight) ;
text-decoration: var(--shiki-dark-text-decoration) ;
font-style: var(--shiki-dark-font-style);
font-weight: var(--shiki-dark-font-weight);
text-decoration: var(--shiki-dark-text-decoration);
}
.shiki-ln-line-highlighted, button:hover {
.shiki-ln-line-highlighted,
button:hover {
span {
background-color: inherit;
}
@@ -113,24 +113,29 @@ $theme: mat.m2-define-light-theme(
.shiki {
padding-block: 1rem;
&.cli {
padding-inline-start: 1rem;
}
}
.docs-light-mode .shiki {
color: var(--shiki-light);
background-color: var(--shiki-light-bg) ;
background-color: var(--shiki-light-bg);
span {
color: var(--shiki-light) ;
background-color: var(--shiki-light-bg) ;
color: var(--shiki-light);
background-color: var(--shiki-light-bg);
/* Optional, if you also want font styles */
font-style: var(--shiki-light-font-style) ;
font-weight: var(--shiki-light-font-weight) ;
text-decoration: var(--shiki-light-text-decoration) ;
font-style: var(--shiki-light-font-style);
font-weight: var(--shiki-light-font-weight);
text-decoration: var(--shiki-light-text-decoration);
}
.shiki-ln-line-highlighted, button:hover {
.shiki-ln-line-highlighted,
button:hover {
span {
background-color: inherit;
}
}
}
}
+12 -2
View File
@@ -41,6 +41,13 @@ export class AppScroller {
this._lastScrollEvent = e;
}),
filter(() => !this.disableScrolling),
filter(() => {
const info = this.router.lastSuccessfulNavigation?.extras.info as Record<
'disableScrolling',
boolean
>;
return !info?.['disableScrolling'];
}),
switchMap((e) => {
return firstValueFrom(
this.appRef.isStable.pipe(
@@ -64,7 +71,7 @@ export class AppScroller {
const {anchor, position} = this._lastScrollEvent;
// Don't scroll during rendering
this.cancelScroll = afterNextRender(
const ref = afterNextRender(
{
write: () => {
if (position) {
@@ -77,6 +84,9 @@ export class AppScroller {
},
},
{injector: this.injector},
).destroy;
);
this.cancelScroll = () => {
ref.destroy();
};
}
}
+1
View File
@@ -1,5 +1,6 @@
@defer (when isBrowser) {
<adev-progress-bar />
<!-- <docs-top-level-banner id="ng-survey-2024" link="https://goo.gle/angular-v18" text="Take the Angular Developer Survey today!" /> -->
}
<button (click)="focusFirstHeading()" class="adev-skip">Skip to main content</button>
+32
View File
@@ -8,6 +8,38 @@
align-items: flex-start;
min-height: 100vh;
// Display top level banner below the navigation.
docs-top-level-banner {
@include mq.for-tablet {
top: 4.6875rem;
}
@include mq.for-phone-only {
top: 3.75rem;
transform: translateY(0);
transition: transform 0.3s ease-out 0.6s;
}
}
// Case: If secondary navigation exists, display banner below secondary navigation for tablets.
&:has(adev-secondary-navigation) {
docs-top-level-banner {
@include mq.for-tablet {
top: 8.125rem;
}
}
}
// Case: If primary navigation is opened, display banner at the top of the page.
&:has(.adev-nav-primary--open) {
docs-top-level-banner {
@include mq.for-phone-only {
transform: translateY(-3.75rem);
transition: transform 0.3s ease-in;
}
}
}
@include mq.for-tablet-landscape-down {
flex-direction: column;
}
+2
View File
@@ -25,6 +25,7 @@ import {
getActivatedRouteSnapshotFromRouter,
IS_SEARCH_DIALOG_OPEN,
SearchDialog,
TopLevelBannerComponent,
} from '@angular/docs';
import {Footer} from './core/layout/footer/footer.component';
import {Navigation} from './core/layout/navigation/navigation.component';
@@ -46,6 +47,7 @@ import {HeaderService} from './core/services/header.service';
RouterLink,
SearchDialog,
ProgressBarComponent,
TopLevelBannerComponent,
],
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
@@ -15,7 +15,7 @@ import {
ViewChild,
} from '@angular/core';
import {isPlatformBrowser} from '@angular/common';
import {NgProgressComponent} from 'ngx-progressbar';
import {NgProgressbar} from 'ngx-progressbar';
import {
NavigationCancel,
NavigationEnd,
@@ -32,7 +32,7 @@ export const PROGRESS_BAR_DELAY = 30;
@Component({
selector: 'adev-progress-bar',
standalone: true,
imports: [NgProgressComponent],
imports: [NgProgressbar],
template: `
<ng-progress aria-label="Page load progress" />
`,
@@ -41,7 +41,7 @@ export const PROGRESS_BAR_DELAY = 30;
export class ProgressBarComponent implements OnInit {
private readonly router = inject(Router);
@ViewChild(NgProgressComponent, {static: true}) progressBar!: NgProgressComponent;
@ViewChild(NgProgressbar, {static: true}) progressBar!: NgProgressbar;
isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
@@ -3,7 +3,7 @@
@if (group.isFeatured) {
<docs-icon aria-hidden>star</docs-icon>
}
<a routerLink="/api" [fragment]="group.id" class="adev-api-anchor" tabindex="-1">{{ group.title }}</a>
<a routerLink="/api" [fragment]="group.id" queryParamsHandling="preserve" class="adev-api-anchor" tabindex="-1">{{ group.title }}</a>
</h3>
</header>
@@ -232,6 +232,20 @@
letter-spacing: -0.00875rem;
}
.docs-reference-card-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
flex-wrap: wrap;
padding: 0.7rem 1rem;
code:not(pre *) {
padding: 0 0.3rem;
}
}
.docs-reference-member-card {
border: 1px solid var(--senary-contrast);
border-radius: 0.25rem;
@@ -265,7 +279,6 @@
header {
display: flex;
flex-direction: column;
padding: 0.7rem 1rem;
border-radius: 0.25rem;
background-color: var(--octonary-contrast);
position: relative;
@@ -275,20 +288,23 @@
background-color 0.3s ease,
border 0.3s ease;
// h3 + code || # of overloads
.docs-reference-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
flex-wrap: wrap;
div {
display: flex;
align-items: center;
gap: 1rem;
}
& > code {
max-width: 100%;
}
code:has(pre) {
padding: 0;
}
pre {
margin: 0;
/* Do we have a better alternative ? */
overflow: auto;
}
}
.docs-reference-card-header {
h3 {
display: inline-block;
font-family: var(--code-font);
@@ -14,6 +14,7 @@ import {signal} from '@angular/core';
import {ApiItemType} from '../interfaces/api-item-type';
import {RouterTestingHarness} from '@angular/router/testing';
import {provideRouter} from '@angular/router';
import {Location} from '@angular/common';
describe('ApiReferenceList', () => {
let component: ApiReferenceList;
@@ -117,4 +118,30 @@ describe('ApiReferenceList', () => {
harness.navigateByUrl(`/api`);
expect(component.type()).toBe(ALL_STATUSES_KEY);
});
it('should set the value of the queryParam equal to the query value', async () => {
const location = TestBed.inject(Location);
component.query.set('item1');
await fixture.whenStable();
expect(location.path()).toBe(`?query=item1&type=All`);
});
it('should keep the values of existing queryParams and set new queryParam equal to the type', async () => {
const location = TestBed.inject(Location);
component.query.set('item1');
await fixture.whenStable();
expect(location.path()).toBe(`?query=item1&type=All`);
component.filterByItemType(ApiItemType.BLOCK);
await fixture.whenStable();
expect(location.path()).toBe(`?query=item1&type=${ApiItemType.BLOCK}`);
});
it('should display all items when query and type are undefined', async () => {
component.query.set(undefined);
component.type.set(undefined);
await fixture.whenStable();
expect(component.filteredGroups()![0].items).toEqual([fakeItem1, fakeItem2]);
});
});
@@ -23,6 +23,7 @@ import ApiItemsSection from '../api-items-section/api-items-section.component';
import {FormsModule} from '@angular/forms';
import {SlideToggle, TextField} from '@angular/docs';
import {NgFor, NgIf} from '@angular/common';
import {Params, Router} from '@angular/router';
import {ApiItemType} from '../interfaces/api-item-type';
import {ApiReferenceManager} from './api-reference-manager.service';
import ApiItemLabel from '../api-item-label/api-item-label.component';
@@ -50,6 +51,7 @@ export const ALL_STATUSES_KEY = 'All';
})
export default class ApiReferenceList {
private readonly apiReferenceManager = inject(ApiReferenceManager);
private readonly router = inject(Router);
filterInput = viewChild.required(TextField, {read: ElementRef});
private readonly injector = inject(EnvironmentInjector);
@@ -71,9 +73,28 @@ export default class ApiReferenceList {
{injector: this.injector},
);
});
effect(
() => {
const params: Params = {
'query': this.query() ? this.query() : null,
'type': this.type() ? this.type() : null,
};
this.router.navigate([], {
queryParams: params,
replaceUrl: true,
preserveFragment: true,
info: {
disableScrolling: true,
},
});
},
{allowSignalWrites: true},
);
}
query = signal('');
query = model<string | undefined>('');
includeDeprecated = signal(false);
type = model<string | undefined>(ALL_STATUSES_KEY);
@@ -87,8 +108,10 @@ export default class ApiReferenceList {
id: group.id,
items: group.items.filter((apiItem) => {
return (
(this.query()
? apiItem.title.toLocaleLowerCase().includes(this.query().toLocaleLowerCase())
(this.query() !== undefined
? apiItem.title
.toLocaleLowerCase()
.includes((this.query() as string).toLocaleLowerCase())
: true) &&
(this.includeDeprecated() ? true : apiItem.isDeprecated === this.includeDeprecated()) &&
(this.type() === undefined ||
@@ -2094,7 +2094,7 @@ export const RECOMMENDATIONS: Step[] = [
level: ApplicationComplexity.Basic,
step: 'v17 zone.js support',
action:
'Make sure that you are using a supported version of Zone.js before you upgrade your application. Angular v16 supports Zone.js version 0.14.x or later.',
'Make sure that you are using a supported version of Zone.js before you upgrade your application. Angular v17 supports Zone.js version 0.14.x or later.',
},
{
possibleIn: 1700,
+5 -10
View File
@@ -1462,16 +1462,6 @@ const REFERENCE_SUB_NAVIGATION_DATA: NavigationItem[] = [
path: 'reference/migrations/standalone',
contentPath: 'reference/migrations/standalone',
},
{
label: 'ModuleWithProviders',
path: 'reference/migrations/module-with-providers',
contentPath: 'reference/migrations/module-with-providers',
},
{
label: 'Typed Forms',
path: 'reference/migrations/typed-forms',
contentPath: 'reference/migrations/typed-forms',
},
{
label: 'Control Flow Syntax',
path: 'reference/migrations/control-flow',
@@ -1482,6 +1472,11 @@ const REFERENCE_SUB_NAVIGATION_DATA: NavigationItem[] = [
path: 'reference/migrations/inject-function',
contentPath: 'reference/migrations/inject-function',
},
{
label: 'Lazy-loaded routes',
path: 'reference/migrations/route-lazy-loading',
contentPath: 'reference/migrations/route-lazy-loading',
},
],
},
{
@@ -35,10 +35,10 @@ src
├── app
│ ├── app.config.server.ts # server application configuration
│ └── app-shell # app-shell component
│ ├── app-shell.component.html
│ ├── app-shell.component.scss
│ ├── app-shell.component.spec.ts
│ └── app-shell.component.ts
│ ├── app-shell.component.html
│ ├── app-shell.component.scss
│ ├── app-shell.component.spec.ts
│ └── app-shell.component.ts
└── main.server.ts # main server application bootstrapping
</docs-code>
@@ -18,7 +18,7 @@ export class HeroService {
) {}
getHeroes() {
const auth = this.isAuthorized ? 'authorized ' : 'unauthorized';
const auth = this.isAuthorized ? 'authorized' : 'unauthorized';
this.logger.log(`Getting heroes for ${auth} user.`);
return HEROES.filter((hero) => this.isAuthorized || !hero.isSecret);
}
@@ -66,11 +66,12 @@ template.
**The `<ng-content>` element is neither a component nor DOM element**. Instead, it is a special
placeholder that tells Angular where to render content. Angular's compiler processes
all `<ng-content>` elements at build-time. You cannot insert, remove, or modify `<ng-content>` at
run time. You cannot add **<span style="text-decoration:underline;">directives</span>**, styles, or
arbitrary attributes to `<ng-content>`.
run time. You cannot add directives, styles, or arbitrary attributes to `<ng-content>`.
You should not conditionally include `<ng-content>` with `ngIf`, `ngFor`, or `ngSwitch`. For
conditional rendering of component content, see [Template fragments](api/core/ng-template).
You should not conditionally include `<ng-content>` with `@if`, `@for`, or `@switch`. Angular always
instantiates and creates DOM nodes for content rendered to a `<ng-content>` placeholder, even if
that `<ng-content>` placeholder is hidden. For conditional rendering of component content,
see [Template fragments](api/core/ng-template).
## Multiple content placeholders
+31 -31
View File
@@ -50,22 +50,22 @@ process.
<td><code>ngDoCheck</code></td>
<td>Runs every time this component is checked for changes.</td>
</tr>
<tr>
<td><code>ngAfterViewInit</code></td>
<td>Runs once after the component's <em>view</em> has been initialized.</td>
</tr>
<tr>
<td><code>ngAfterContentInit</code></td>
<td>Runs once after the component's <em>content</em> has been initialized.</td>
</tr>
<tr>
<td><code>ngAfterViewChecked</code></td>
<td>Runs every time the component's view has been checked for changes.</td>
</tr>
<tr>
<td><code>ngAfterContentChecked</code></td>
<td>Runs every time this component content has been checked for changes.</td>
</tr>
<tr>
<td><code>ngAfterViewInit</code></td>
<td>Runs once after the component's <em>view</em> has been initialized.</td>
</tr>
<tr>
<td><code>ngAfterViewChecked</code></td>
<td>Runs every time the component's view has been checked for changes.</td>
</tr>
<tr>
<td rowspan="2">Rendering</td>
<td><code>afterNextRender</code></td>
@@ -173,16 +173,6 @@ defining this hook whenever possible, only using it when you have no alternative
During initialization, the first `ngDoCheck` runs after `ngOnInit`.
### ngAfterViewInit
The `ngAfterViewInit` method runs once after all the children in the component's template (its
_view_) have been initialized.
You can use this lifecycle hook to read the results of
[view queries](guide/components/queries#view-queries). While you can access the initialized state of
these queries, attempting to change any state in this method results in an
[ExpressionChangedAfterItHasBeenCheckedError](errors/NG0100)
### ngAfterContentInit
The `ngAfterContentInit` method runs once after all the children nested inside the component (
@@ -193,6 +183,29 @@ You can use this lifecycle hook to read the results of
state of these queries, attempting to change any state in this method results in an
[ExpressionChangedAfterItHasBeenCheckedError](errors/NG0100)
### ngAfterContentChecked
The `ngAfterContentChecked` method runs every time the children nested inside the component (its
_content_) have been checked for changes.
This method runs very frequently and can significantly impact your page's performance. Avoid
defining this hook whenever possible, only using it when you have no alternative.
While you can access the updated state
of [content queries](guide/components/queries#content-queries) here, attempting to
change any state in this method results in
an [ExpressionChangedAfterItHasBeenCheckedError](errors/NG0100).
### ngAfterViewInit
The `ngAfterViewInit` method runs once after all the children in the component's template (its
_view_) have been initialized.
You can use this lifecycle hook to read the results of
[view queries](guide/components/queries#view-queries). While you can access the initialized state of
these queries, attempting to change any state in this method results in an
[ExpressionChangedAfterItHasBeenCheckedError](errors/NG0100)
### ngAfterViewChecked
The `ngAfterViewChecked` method runs every time the children in the component's template (its
@@ -206,19 +219,6 @@ here, attempting to
change any state in this method results in
an [ExpressionChangedAfterItHasBeenCheckedError](errors/NG0100).
### ngAfterContentChecked
The `ngAfterContentChecked` method runs every time the children nested inside the component (its
_content_) have been checked for changes.
This method runs very frequently and can significantly impact your page's performance. Avoid
defining this hook whenever possible, only using it when you have no alternative.
While you can access the updated state
of [content queries](guide/components/queries#content-queries) here, attempting to
change any state in this method results in
an [ExpressionChangedAfterItHasBeenCheckedError](errors/NG0100).
### afterRender and afterNextRender
The `afterRender` and `afterNextRender` functions let you register a **render callback** to be
@@ -110,7 +110,7 @@ class HeroService {
private isAuthorized: boolean) { }
getHeroes() {
const auth = this.isAuthorized ? 'authorized ' : 'unauthorized';
const auth = this.isAuthorized ? 'authorized' : 'unauthorized';
this.logger.log(`Getting heroes for ${auth} user.`);
return HEROES.filter(hero => this.isAuthorized || !hero.isSecret);
}
+3 -3
View File
@@ -95,9 +95,9 @@ export function authInterceptor(req: HttpRequest<unknown>, next: HttpHandlerFn)
const authToken = inject(AuthService).getAuthToken();
// Clone the request to add the authentication header.
const newReq = req.clone({headers: {
req.headers.append('X-Authentication-Token', authToken),
}});
const newReq = req.clone({
headers: req.headers.append('X-Authentication-Token', authToken),
});
return next(newReq);
}
</docs-code>
+7 -6
View File
@@ -201,12 +201,13 @@ See [caniuse.com/trusted-types](https://caniuse.com/trusted-types) for the curre
To enforce Trusted Types for your application, you must configure your application's web server to emit HTTP headers with one of the following Angular policies:
| Policies | Detail |
| :---------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `angular` | This policy is used in security-reviewed code that is internal to Angular, and is required for Angular to function when Trusted Types are enforced. Any inline template values or content sanitized by Angular is treated as safe by this policy. |
| `angular#unsafe-bypass` | This policy is used for applications that use any of the methods in Angular's [DomSanitizer](api/platform-browser/DomSanitizer) that bypass security, such as `bypassSecurityTrustHtml`. Any application that uses these methods must enable this policy. |
| `angular#unsafe-jit` | This policy is used by the [Just-In-Time (JIT) compiler](api/core/Compiler). You must enable this policy if your application interacts directly with the JIT compiler or is running in JIT mode using the [platform browser dynamic](api/platform-browser-dynamic/platformBrowserDynamic). |
| `angular#bundler` | This policy is used by the Angular CLI bundler when creating lazy chunk files. |
| Policies | Detail |
| :----------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `angular` | This policy is used in security-reviewed code that is internal to Angular, and is required for Angular to function when Trusted Types are enforced. Any inline template values or content sanitized by Angular is treated as safe by this policy. |
| `angular#bundler` | This policy is used by the Angular CLI bundler when creating lazy chunk files. |
| `angular#unsafe-bypass` | This policy is used for applications that use any of the methods in Angular's [DomSanitizer](api/platform-browser/DomSanitizer) that bypass security, such as `bypassSecurityTrustHtml`. Any application that uses these methods must enable this policy. |
| `angular#unsafe-jit` | This policy is used by the [Just-In-Time (JIT) compiler](api/core/Compiler). You must enable this policy if your application interacts directly with the JIT compiler or is running in JIT mode using the [platform browser dynamic](api/platform-browser-dynamic/platformBrowserDynamic). |
| `angular#unsafe-upgrade` | This policy is used by the [@angular/upgrade](api/upgrade/static/UpgradeModule) package. You must enable this policy if your application is an AngularJS hybrid. |
You should configure the HTTP headers for Trusted Types in the following locations:
@@ -31,12 +31,13 @@ If there are multiple bindings to the same class name, Angular uses styling prec
The following table summarizes class binding syntax.
| Binding Type | Syntax | Input Type | Example Input Values |
|:--- |:--- |:--- |:--- |
| Single class binding | `[class.sale]="onSale"` | <code>boolean | undefined | null</code> | `true`, `false` |
| Multi-class binding | `[class]="classExpression"` | `string` | `"my-class-1 my-class-2 my-class-3"` |
| Multi-class binding | `[class]="classExpression"` | <code>Record<string, boolean | undefined | null></code> | `{foo: true, bar: false}` |
| Multi-class binding | `[class]="classExpression"` | <code>Array<string></code> | `['foo', 'bar']` |
| Binding Type | Syntax | Input Type | Example Input Values |
| :------------------- | :-------------------------- | :---------------------- | :----------------------------------- |
| Single class binding | `[class.sale]="onSale"` | `boolean|undefined|null`| `true`, `false` |
| Multi-class binding | `[class]="classExpression"` | `string` | `"my-class-1 my-class-2 my-class-3"` |
| Multi-class binding | `[class]="classExpression"` | `Record<string, boolean|undefined|null>` | `{foo: true, bar: false}` |
| Multi-class binding | `[class]="classExpression"` | `Array<string>` | `['foo', 'bar']` |
## Binding to a single style
@@ -74,12 +75,12 @@ If there are multiple bindings to the same style attribute, Angular uses styling
The following table summarizes style binding syntax.
| Binding Type | Syntax | Input Type | Example Input Values |
|:--- |:--- |:--- |:--- |
| Single style binding | `[style.width]="width"` | <code>string | undefined | null</code> | `"100px"` |
| Single style binding with units | `[style.width.px]="width"` | <code>number | undefined | null</code> | `100` |
| Multi-style binding | `[style]="styleExpression"` | `string` | `"width: 100px; height: 100px"` |
| Multi-style binding | `[style]="styleExpression"` | <code>Record<string, string | undefined | null></code> | `{width: '100px', height: '100px'}` |
| Binding Type | Syntax | Input Type | Example Input Values |
| :------------------------------ | :-------------------------- | :---------------------- | :------------------------------ |
| Single style binding | `[style.width]="width"` | `string| undefined|null`| `"100px"` |
| Single style binding with units | `[style.width.px]="width"` | `number| undefined|null`| `100` |
| Multi-style binding | `[style]="styleExpression"` | `string` | `"width: 100px; height: 100px"` |
| Multi-style binding | `[style]="styleExpression"` | `Record<string, string|undefined|null>` | `{width: '100px', height: '100px'}` |
## Styling precedence
+1 -1
View File
@@ -45,7 +45,7 @@ One way to ensure that a component is using the correct notification mechanisms
use [ChangeDetectionStrategy.OnPush](/adev/src/content/best-practices/runtime-performance/skipping-subtrees#using-onpush).
The `OnPush` change detection strategy is not required, but it is a recommended step towards zoneless compatibility for application components. It is not always possible for library components to use `ChangeDetectionStrategy.OnPush`.
When a library component is a host for user-components which might use `ChangeDetectionStratey.Default`, it cannot use `OnPush` because that would prevent the child component from being refreshed if it is not `OnPush` compatible and relies on ZoneJS to trigger change detection. Components can use the `Default` strategy as long as they notify Angular when change detection needs to run (calling `markForCheck`, using signals, `AsyncPipe`, etc.).
When a library component is a host for user-components which might use `ChangeDetectionStrategy.Default`, it cannot use `OnPush` because that would prevent the child component from being refreshed if it is not `OnPush` compatible and relies on ZoneJS to trigger change detection. Components can use the `Default` strategy as long as they notify Angular when change detection needs to run (calling `markForCheck`, using signals, `AsyncPipe`, etc.).
### Remove `NgZone.onMicrotaskEmpty`, `NgZone.onUnstable`, `NgZone.isStable`, or `NgZone.onStable`
@@ -1,9 +1,10 @@
# Migration to Control Flow syntax
[Control flow syntax](guide/templates/control-flow) is available from Angular v17. The new syntax is baked into the template, so you don't need to import `CommonModule` anymore.
[Control flow syntax](guide/templates/control-flow) is available from Angular 17 and simplified way to use the control-flow directives like *ngFor, *ngIf and *ngSwitch.
This schematic migrates all existing code in your application to use new Control Flow Syntax.
The new syntax is baked into the template, so you don't need to import `CommonModule` anymore. But wait there is more to it, there is an migration available for migrating all your old code to use new Control Flow Syntax with Angular 17 release. Run the schematic with the following command:
Run the schematic using the following command:
<docs-code language="shell">
@@ -1,8 +1,10 @@
# Migration to the `inject` function
Angular's `inject` function offers more accurate types and better compatibility with standard
decorators, compared to constructor-based injection. You can convert your application to it by
running the following command:
Angular's `inject` function offers more accurate types and better compatibility with standard decorators, compared to constructor-based injection.
This schematic converts constructor-based injection in your classes to use the `inject` function instead.
Run the schematic using the following command:
<docs-code language="shell">
@@ -11,6 +13,7 @@ ng generate @angular/core:inject
</docs-code>
#### Before
<docs-code language="typescript">
import { Component, Inject, Optional } from '@angular/core';
import { MyService } from './service';
@@ -18,11 +21,14 @@ import { DI_TOKEN } from './token';
@Component()
export class MyComp {
constructor(private service: MyService, @Inject(TOKEN) @Optional() readonly token: string) {}
constructor(
private service: MyService,
@Inject(TOKEN) @Optional() readonly token: string) {}
}
</docs-code>
#### After
<docs-code language="typescript">
import { Component, inject } from '@angular/core';
import { MyService } from './service';
@@ -36,19 +42,23 @@ export class MyComp {
</docs-code>
## Migration options
The migration includes several options to customize its output.
### `path`
Determines which sub-path in your project should be migrated. Pass in `.` or leave it blank to
migrate the entire directory.
### `migrateAbstractClasses`
Angular doesn't validate that parameters of abstract classes are injectable. This means that the
migration can't reliably migrate them to `inject` without risking breakages which is why they're
disabled by default. Enable this option if you want abstract classes to be migrated, but note
that you may have to **fix some breakages manually**.
### `backwardsCompatibleConstructors`
By default the migration tries to clean up the code as much as it can, which includes deleting
parameters from the constructor, or even the entire constructor if it doesn't include any code.
In some cases this can lead to compilation errors when classes with Angular decorators inherit from
@@ -56,6 +66,7 @@ other classes with Angular decorators. If you enable this option, the migration
additional constructor signature to keep it backwards compatible, at the expense of more code.
#### Before
<docs-code language="typescript">
import { Component } from '@angular/core';
import { MyService } from './service';
@@ -67,6 +78,7 @@ export class MyComp {
</docs-code>
#### After
<docs-code language="typescript">
import { Component } from '@angular/core';
import { MyService } from './service';
@@ -83,6 +95,7 @@ export class MyComp {
</docs-code>
### `nonNullableOptional`
If injection fails for a parameter with the `@Optional` decorator, Angular returns `null` which
means that the real type of any `@Optional` parameter will be `| null`. However, because decorators
cannot influence their types, there is a lot of existing code whose type is incorrect. The type is
@@ -94,6 +107,7 @@ at the expense of potentially hiding type errors.
because the code that depends on them likely already accounts for their nullability.
#### Before
<docs-code language="typescript">
import { Component, Inject, Optional } from '@angular/core';
import { TOKEN_ONE, TOKEN_TWO } from './token';
@@ -107,6 +121,7 @@ export class MyComp {
</docs-code>
#### After
<docs-code language="typescript">
import { Component, inject } from '@angular/core';
import { TOKEN_ONE, TOKEN_TWO } from './token';
@@ -1,72 +0,0 @@
# `ModuleWithProviders` Migration
## What does this schematic do?
Some Angular libraries, such as `@angular/router` and `@ngrx/store`, implement APIs that return a type called `ModuleWithProviders` \(typically via a method named `forRoot()`\).
This type represents an `NgModule` along with additional providers.
Angular version 9 deprecates use of `ModuleWithProviders` without an explicitly generic type, where the generic type refers to the type of the `NgModule`.
This schematic will add a generic type to any `ModuleWithProviders` usages that are missing the generic.
In the example below, the type of the `NgModule` is `SomeModule`, so the schematic changes the type to be `ModuleWithProviders<SomeModule>`.
### Before
<docs-code language="typescript">
@NgModule({…})
export class MyModule {
static forRoot(config: SomeConfig): ModuleWithProviders {
return {
ngModule: SomeModule,
providers: [
{provide: SomeConfig, useValue: config}
]
};
}
}
</docs-code>
### After
<docs-code language="typescript">
@NgModule({…})
export class MyModule {
static forRoot(config: SomeConfig): ModuleWithProviders<SomeModule> {
return {
ngModule: SomeModule,
providers: [
{provide: SomeConfig, useValue: config }
]
};
}
}
</docs-code>
In the rare case that the schematic can't determine the type of `ModuleWithProviders`, you may see the schematic print a TODO comment to update the code manually.
## Why is this migration necessary?
`ModuleWithProviders` has had the generic type since Angular version 7, but it has been optional.
This has compiled because the `metadata.json` files contained all the metadata.
With Ivy, `metadata.json` files are no longer required, so the framework cannot assume that one with the necessary types has been provided.
Instead, Ivy relies on the generic type for `ModuleWithProviders` to get the correct type information.
For this reason, Angular version 9 deprecates `ModuleWithProviders` without a generic type.
A future version of Angular will remove the default generic type, making an explicit type required.
## Should I add the generic type when I add new `ModuleWithProviders` to my app?
Yes, any time your code references the `ModuleWithProviders` type, it should have a generic type that matches the actual `NgModule` that is returned \(for example, `ModuleWithProviders<MyModule>`\).
## What should I do if the schematic prints a TODO comment?
The schematic will print a TODO comment in the event that it cannot detect the correct generic for the `ModuleWithProviders` type.
In this case, you'll want to manually add the correct generic to `ModuleWithProviders`.
It should match the type of whichever `NgModule` is returned in the `ModuleWithProviders` object.
## What does this mean for libraries?
Libraries should add the generic type to any usages of the `ModuleWithProviders` type.
@@ -5,23 +5,14 @@ Learn about how you can migrate your existing angular project to the latest feat
<docs-card-container>
<docs-card title="Standalone" link="Migrate now" href="reference/migrations/standalone">
Standalone components provide a simplified way to build Angular applications. Standalone components specify their dependencies directly instead of getting them through NgModules.
Standalone components, directives, and pipes aim to streamline the authoring experience by reducing the need for NgModules.
</docs-card>
<docs-card title="`ModuleWithProviders`" link="Migrate now" href="reference/migrations/module-with-providers">
ModuleWithProviders has been optional since Angular version 7. But with Ivy the metadata present in metadata.json is no longer required, instead Ivy relies on the generic type for ModuleWithProviders to get the correct type information.
Angular version 9 deprecates ModuleWithProviders without a generic type. A future version of Angular will remove the default generic type, making an explicit type required.
</docs-card>
<docs-card title="Typed Forms" link="Migrate now" href="reference/migrations/typed-forms">
In previous Angular versions, most of the Form APIs included `any` somewhere in their types, and interacting with the structure of the controls, or the values themselves, was not type-safe.
Strictly typed reactive forms add type safety and the types enable a variety of other improvements, such as better autocomplete in IDEs, and an explicit way to specify form structure.
</docs-card>
<docs-card title="Control Flow Syntax" link="Migrate now" href="reference/migrations/control-flow">
Control Flow Syntax is available with Angular 17 release and allows you to use more ergonomic syntax which is close to javascript, better type checking and lazy load part of the component. It replaces the need to imports CommonModule to use functionalities like `*ngFor`, `*ngIf`.
Built-in Control Flow Syntax allows you to use more ergonomic syntax which is close to JavaScript and has better type checking. It replaces the need to import `CommonModule` to use functionality like `*ngFor`, `*ngIf` and `*ngSwitch`.
</docs-card>
<docs-card title="inject() Function" link="Migrate now" href="reference/migrations/inject-function">
Angular's `inject` function offers more accurate types and better compatibility with standard decorators, compared to constructor-based injection.
</docs-card>
<docs-card title="Lazy-loaded routes" link="Migrate now" href="reference/migrations/route-lazy-loading">
Convert eagerly loaded component routes to lazy loaded ones. This allows the build process to split production bundles into smaller chunks, to load less JavaScript at initial page load.
</docs-card>
</docs-card-container>
@@ -0,0 +1,75 @@
# Migration to lazy-loaded routes
This schematic helps developers to convert eagerly loaded component routes to lazy loaded routes. This allows the build process to split the production bundle into smaller chunks, to avoid big JS bundle that includes all routes, which negatively affects initial page load of an application.
Run the schematic using the following command:
<docs-code language="shell">
ng generate @angular/core:route-lazy-loading
</docs-code>
### `path` config option
By default, migration will go over the entire application. If you want to apply this migration to a subset of the files, you can pass the path argument as shown below:
<docs-code language="shell">
ng generate @angular/core:route-lazy-loading --path src/app/sub-component
</docs-code>
The value of the path parameter is a relative path within the project.
### How does it work?
The schematic will attempt to find all the places where the application routes as defined:
- `RouterModule.forRoot` and `RouterModule.forChild`
- `Router.resetConfig`
- `provideRouter`
- `provideRoutes`
- variables of type `Routes` or `Route[]` (e.g. `const routes: Routes = [{...}]`)
The migration will check all the components in the routes, check if they are standalone and eagerly loaded, and if so, it will convert them to lazy loaded routes.
#### Before
<docs-code language="typescript">
// app.module.ts
import { HomeComponent } from './home/home.component';
@NgModule({
imports: [
RouterModule.forRoot([
{
path: 'home',
// HomeComponent is standalone and eagerly loaded
component: HomeComponent,
},
]),
],
})
export class AppModule {}
</docs-code>
#### After
<docs-code language="typescript">
// app.module.ts
@NgModule({
imports: [
RouterModule.forRoot([
{
path: 'home',
// ↓ HomeComponent is now lazy loaded
loadComponent: () => import('./home/home.component').then(m => m.HomeComponent),
},
]),
],
})
export class AppModule {}
</docs-code>
This migration will also collect information about all the components declared in NgModules and output the list of routes that use them (including corresponding location of the file). Consider making those components standalone and run this migration again. You can use an existing migration (see https://angular.dev/reference/migrations/standalone) to convert those components to standalone.
@@ -4,7 +4,9 @@
<docs-video src="https://www.youtube.com/embed/x5PZwb4XurU" title="Getting started with standalone components"/>
As of version 15.2.0, Angular offers a [schematic](tools/cli/schematics) to help project authors convert existing projects to the standalone APIs. The schematic aims to transform as much code as possible automatically, but it may require some manual fixes by the project author. Run the schematic with the following command:
This schematic helps to transform components, directive and pipes in existing projects to become standalone. The schematic aims to transform as much code as possible automatically, but it may require some manual fixes by the project author.
Run the schematic using the following command:
<docs-code language="shell">
@@ -1,43 +0,0 @@
# Typed Forms Migration
As of Angular 14, reactive forms are strictly typed by default.
## Overview of Typed Forms
<docs-video src="https://www.youtube.com/embed/L-odCf4MfJc" title="Angular Typed Forms"/>
With Angular reactive forms, you explicitly specify a *form model*. As a simple example, consider this basic user login form:
```ts
const login = new FormGroup({
email: new FormControl(''),
password: new FormControl(''),
});
```
Angular provides many APIs for interacting with this `FormGroup`. For example, you may call `login.value`, `login.controls`, `login.patchValue`, etc. (For a full API reference, see the [API documentation](api/forms/FormGroup).)
In previous Angular versions, most of these APIs included `any` somewhere in their types, and interacting with the structure of the controls, or the values themselves, was not type-safe. For example: you could write the following invalid code:
```ts
const emailDomain = login.value.email.domain;
```
With strictly typed reactive forms, the above code does not compile, because there is no `domain` property on `email`.
In addition to the added safety, the types enable a variety of other improvements, such as better autocomplete in IDEs, and an explicit way to specify form structure.
These improvements currently apply only to *reactive* forms (not [*template-driven* forms](guide/forms/template-driven-forms)).
## Automated Untyped Forms Migration
When upgrading to Angular 14, an included migration will automatically replace all the forms classes in your code with corresponding untyped versions. For example, the snippet from above would become:
```ts
const login = new UntypedFormGroup({
email: new UntypedFormControl(''),
password: new UntypedFormControl(''),
});
```
Each `Untyped` symbol has exactly the same semantics as in previous Angular versions, so your application should continue to compile as before. By removing the `Untyped` prefixes, you can incrementally enable the types.
+1 -1
View File
@@ -76,7 +76,7 @@ HELPFUL: Approximate dates are offered as general guidance and are subject to ch
|:--------|:-------------------|
| v18.1 | Week of 2024-07-08 |
| v18.2 | Week of 2024-08-12 |
| v19.0 | Week of 2024-11-13 |
| v19.0 | Week of 2024-11-19 |
### Support window
+1 -1
View File
@@ -9,7 +9,7 @@ This table covers [Angular versions under active support](reference/releases#act
| Angular | Node.js | TypeScript | RxJS |
| ------------------ | ------------------------------------ | -------------- | ------------------ |
| 18.1.x | ^18.19.1 \|\| ^20.11.1 \|\| ^22.0.0 | >=5.4.0 <5.6.0 | ^6.5.3 \|\| ^7.4.0 |
| 18.1.x \|\| 18.2.x | ^18.19.1 \|\| ^20.11.1 \|\| ^22.0.0 | >=5.4.0 <5.6.0 | ^6.5.3 \|\| ^7.4.0 |
| 18.0.x | ^18.19.1 \|\| ^20.11.1 \|\| ^22.0.0 | >=5.4.0 <5.5.0 | ^6.5.3 \|\| ^7.4.0 |
| 17.3.x | ^18.13.0 \|\| ^20.9.0 | >=5.2.0 <5.5.0 | ^6.5.3 \|\| ^7.4.0 |
| 17.1.x \|\| 17.2.x | ^18.13.0 \|\| ^20.9.0 | >=5.2.0 <5.4.0 | ^6.5.3 \|\| ^7.4.0 |
+11 -10
View File
@@ -53,16 +53,17 @@ in [Tech Writing One](https://developers.google.com/tech-writing/one) and
[Tech Writing Two](https://developers.google.com/tech-writing/two), but we repeat them here
because they are _very_ common.
| Pitfall | Explanation | ❌ Bad example | ✅ Good example |
|-----------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------|
| **First person language** | Prefer second person language ("you"), including imperative language. | "We can use the `afterRender` function to register a callback." | "Use the `afterRender` function to register a callback." |
| **Past or future tense** | Always use present tense. This is especially common for future tense with "will". | "The query will return an instance of the component." | "The query returns an instance of the component" |
| [**Passive voice**][active-voice] | Prefer active voice. This often involves changing the order of words in a sentence or making a sentence imperative. | "Your template is compiled to JavaScript by Angular." | "Angular compiles your template to JavaScript." |
| **Mixed content types** | Guides should explain a topic, while tutorials walk readers through a set of step-by-step instructions. Avoid mixing these two content types. | "To define styles for a component, first..." | "Components include CSS styles via the `styles` and `styleUrl` properties." |
| **Topic scope creep** | Each topic should stay focused on one specific subject matter. Don't explain concepts that are better explained elsewhere, especially for web platform behaviors that are best explained by more general sites like [MDN](https://developer.mozilla.org). | | |
| **Nested treatments** | angular.dev supports several custom treatments like callouts, alerts, pills, etc. Do not nest these treatments. Do not use these treatments inside table cells. | | |
| **Lists that should be tables** | Any collection of items that includes more than one piece of information should be a table instead of a bulleted list. | | |
| **Non-descriptive links** | The text of a link should describe the linked content. This is an important accessibility consideration. | "Report all security vulnerabilities [here](https://bughunters.google.com). | "Report all security vulnerabilities to [Google's Vulnerability Bounty Portal](https://bughunters.google.com)." |
| Pitfall | Explanation | ❌ Bad example | ✅ Good example |
|------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------|
| **First person language** | Prefer second person language ("you"), including imperative language. | "We can use the `afterRender` function to register a callback." | "Use the `afterRender` function to register a callback." |
| **Past or future tense** | Always use present tense. This is especially common for future tense with "will". | "The query will return an instance of the component." | "The query returns an instance of the component" |
| [**Passive voice**][active-voice] | Prefer active voice. This often involves changing the order of words in a sentence or making a sentence imperative. | "Your template is compiled to JavaScript by Angular." | "Angular compiles your template to JavaScript." |
| **Mixed content types** | Guides should explain a topic, while tutorials walk readers through a set of step-by-step instructions. Avoid mixing these two content types. | "To define styles for a component, first..." | "Components include CSS styles via the `styles` and `styleUrl` properties." |
| **Topic scope creep** | Each topic should stay focused on one specific subject matter. Don't explain concepts that are better explained elsewhere, especially for web platform behaviors that are best explained by more general sites like [MDN](https://developer.mozilla.org). | | |
| **Nested treatments** | angular.dev supports several custom treatments like callouts, alerts, pills, etc. Do not nest these treatments. Do not use these treatments inside table cells. | | |
| **Lists that should be tables** | Any collection of items that includes more than one piece of information should be a table instead of a bulleted list. | | |
| **Non-descriptive links** | The text of a link should describe the linked content. This is an important accessibility consideration. | "Report all security vulnerabilities [here](https://bughunters.google.com). | "Report all security vulnerabilities to [Google's Vulnerability Bounty Portal](https://bughunters.google.com)." |
| **Making statements relative to a specific point in time** | Docs should describe the framework as it exists _now_, never relative to a specific point in time. Docs should avoid referring to anything as "new" or "recent". Docs should avoid mentioning "upcoming" or "future" changes. | "Angular v18 recently introduced fallback content for ng-content." | "The ng-content element allows you to..." |
[active-voice]: https://developers.google.com/tech-writing/one/active-voice
@@ -19,6 +19,11 @@ const extractViewTree = (
getComponent: (element: Element) => {} | null,
getDirectives: (node: Node) => {}[],
): ComponentTreeNode[] => {
// Ignore DOM Node if it came from a different frame. Use instanceof Node to check this.
if (!(domNode instanceof Node)) {
return result;
}
const directives = getDirectives(domNode);
if (!directives.length && !(domNode instanceof Element)) {
return result;
@@ -119,12 +119,18 @@ export function splitInjectorPathsIntoElementAndEnvironmentPaths(injectorPaths:
const startingElementToEnvironmentPath = new Map<string, SerializedInjector[]>();
injectorPaths.forEach(({node, path}) => {
const firstElementIndex = path.findIndex((injector) => injector.type === 'element');
// split the path into two paths,
// one for the element injector and one for the environment injector
const environmentPath = path.slice(0, firstElementIndex);
const elementPath = path.slice(firstElementIndex);
let environmentPath: SerializedInjector[] = [];
let elementPath: SerializedInjector[] = [];
const firstElementIndex = path.findIndex((injector) => injector.type === 'element');
if (firstElementIndex === -1) {
environmentPath = path;
elementPath = [];
} else {
environmentPath = path.slice(0, firstElementIndex);
elementPath = path.slice(firstElementIndex);
}
elementPaths.push({
node,
@@ -90,3 +90,22 @@ if (!backendInitialized) {
};
retry();
}
const proxyEventFromWindowToDevToolsExtension = (event: MessageEvent) => {
if (event.source === window && event.data) {
try {
chrome.runtime.sendMessage(event.data);
} catch (e) {
const {message} = e as Error;
if (message.includes('Extension context invalidated.')) {
console.error(
'Angular DevTools: Disconnecting content script due to invalid extension context. Please reload the page.',
);
window.removeEventListener('message', proxyEventFromWindowToDevToolsExtension);
}
throw e;
}
}
};
window.addEventListener('message', proxyEventFromWindowToDevToolsExtension);

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