mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
Compare commits
108 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1853bbb061 | |||
| 3ecf620cc5 | |||
| 36aa3af77b | |||
| adf32746c2 | |||
| 5501e9c8ed | |||
| 389933f079 | |||
| 674aed5984 | |||
| b14c864170 | |||
| 10fce207e0 | |||
| eba3a0ade0 | |||
| 3d48a72193 | |||
| 5d2e243c76 | |||
| 5e9661d69c | |||
| df42d2be16 | |||
| 106917af87 | |||
| b74b5f0c06 | |||
| 4231404065 | |||
| adcdfeca16 | |||
| 4f8c406664 | |||
| 206b7be7f7 | |||
| 804925b114 | |||
| 6a953c6ed4 | |||
| 01cae95596 | |||
| 371ad098d5 | |||
| 73ad6ea647 | |||
| d3366815ee | |||
| 76ec60d73c | |||
| 7bfe81700b | |||
| 3067633bc7 | |||
| be82f282a4 | |||
| 03b35270b0 | |||
| fe9005ce8e | |||
| 03ec620e31 | |||
| 6d3a2af146 | |||
| 223b7857ef | |||
| 5bdae0387f | |||
| 034b32c133 | |||
| 2a89e184e0 | |||
| 9b6865ec03 | |||
| 6059ca8f1f | |||
| 800f6c8ca3 | |||
| 87c594b90b | |||
| f133489a16 | |||
| 09deb24d74 | |||
| f1bc6f895b | |||
| 5f95afb331 | |||
| b30bc4531a | |||
| a6225a6cdd | |||
| 69b20d8391 | |||
| 6089772fe8 | |||
| 1a5f5ee5ce | |||
| 76808cc328 | |||
| 980c64abaa | |||
| df3e9c1661 | |||
| cb7d817edd | |||
| 8645152f8c | |||
| 0979ca0354 | |||
| af20b2a161 | |||
| b5719ba5eb | |||
| c4901d4dfe | |||
| 1682c60938 | |||
| c65ab136bf | |||
| c89be37193 | |||
| f723288adf | |||
| 58e00deb34 | |||
| 57db366522 | |||
| 3e6ee7eaff | |||
| 086b754cef | |||
| 4bd9ba714c | |||
| 3b63082384 | |||
| 25bc810f83 | |||
| 9de30a7b1c | |||
| b1ed7e2b5f | |||
| 6b4357fae4 | |||
| 0bb649b8fa | |||
| 286012fb89 | |||
| 9155f75e36 | |||
| 93410eda0a | |||
| d49e083999 | |||
| e50d239e80 | |||
| e30c60e89f | |||
| 8104fc2126 | |||
| 7accd9d885 | |||
| 334c99f968 | |||
| b1a9d0f4de | |||
| 5d76401ff5 | |||
| de85979648 | |||
| bbc970bb0b | |||
| 87d00d26ff | |||
| 84752069f2 | |||
| d3b46ade94 | |||
| 1828d11d84 | |||
| b8c82fa2f7 | |||
| fa77c9e5b4 | |||
| 5d16c286bf | |||
| 604270619a | |||
| 1b546975f0 | |||
| 16ae748257 | |||
| ecb0f8f161 | |||
| cea3e4b594 | |||
| 448279f57e | |||
| 5558e275ee | |||
| 86216792fd | |||
| cab6c23602 | |||
| 296216cbe1 | |||
| 9af760eb51 | |||
| b16dd6d67f | |||
| 02d613ef9c |
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+13
-20
@@ -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(
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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()}`;
|
||||
}
|
||||
}
|
||||
@@ -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 */
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
+14
-41
@@ -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>
|
||||
`;
|
||||
|
||||
@@ -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()});
|
||||
}
|
||||
|
||||
@@ -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'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><h1></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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
+29
-13
@@ -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);
|
||||
|
||||
+27
@@ -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]);
|
||||
});
|
||||
});
|
||||
|
||||
+26
-3
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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;
|
||||
|
||||
+10
-4
@@ -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
Reference in New Issue
Block a user