Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c22c5c4543 | |||
| 5d41ab9cd1 | |||
| 243ccce624 | |||
| e8badecf4f | |||
| cdfcb779df | |||
| ac395d0f68 | |||
| e75a9a4d83 | |||
| 41f28f88b1 | |||
| 7df0a8a278 | |||
| 9889346620 | |||
| 39a50f9a8d | |||
| 1f77083a2d | |||
| bb35414a38 | |||
| fcfb42a8ba | |||
| 57e54e29e7 | |||
| 2909e9817d | |||
| 619f3c8792 | |||
| 0ee1a1419d | |||
| f386a04c9d | |||
| 492e03f699 | |||
| ba9ddd7ac2 | |||
| 5d23e601d1 | |||
| a7872ca77a | |||
| 10314787d6 | |||
| 40a94b3ab6 | |||
| 1f129f114e | |||
| 962934bc4f | |||
| 6ce8fc8637 | |||
| e82173e8c4 | |||
| b6a7d509fe | |||
| 01722ee637 | |||
| 7225485311 | |||
| a45e69f627 | |||
| a7d6c64a47 | |||
| 2f7c7b3833 | |||
| 0daa1ea19d | |||
| 658ae699d8 | |||
| 9cf6b9369e | |||
| 9ea0d69275 | |||
| f1991d49b0 | |||
| 198f2ff5f2 | |||
| 8d37ed035c | |||
| 365955322f | |||
| d870856840 | |||
| 7bfa3610c5 |
@@ -1,3 +1,44 @@
|
||||
<a name="17.3.0"></a>
|
||||
# 17.3.0 (2024-03-13)
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [1a6beae8a2](https://github.com/angular/angular/commit/1a6beae8a2bdcff27d4c1e402f98246a52247906) | feat | Enable template pipeline by default. ([#54571](https://github.com/angular/angular/pull/54571)) |
|
||||
| [f386a04c9d](https://github.com/angular/angular/commit/f386a04c9ddc2951c8105d5cafcce7b4bedea569) | fix | handle two-way bindings to signal-based template variables in instruction generation ([#54714](https://github.com/angular/angular/pull/54714)) |
|
||||
| [1f129f114e](https://github.com/angular/angular/commit/1f129f114edb21137d74471dd6b652848d2ffb68) | fix | not catching for loop empty tracking expressions ([#54772](https://github.com/angular/angular/pull/54772)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [12dc4d074e](https://github.com/angular/angular/commit/12dc4d074e63edaff626003ad6136a8d122b2ba6) | fix | account for as expression in docs extraction ([#54414](https://github.com/angular/angular/pull/54414)) |
|
||||
| [da7fbb40f0](https://github.com/angular/angular/commit/da7fbb40f06e6e37504f69e7b335f8219f424de2) | fix | detect when the linker is working in unpublished angular and widen supported versions ([#54439](https://github.com/angular/angular/pull/54439)) |
|
||||
| [492e03f699](https://github.com/angular/angular/commit/492e03f699ca95df4cf854885253f63a5a1fb5c1) | fix | flag two-way bindings to non-signal values in templates ([#54714](https://github.com/angular/angular/pull/54714)) |
|
||||
| [5afa4f0ec1](https://github.com/angular/angular/commit/5afa4f0ec1b64b88ef875d48bd143e0f36e0a955) | fix | support `ModuleWithProviders` literal detection with `typeof` ([#54650](https://github.com/angular/angular/pull/54650)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [331b16efd2](https://github.com/angular/angular/commit/331b16efd2f5af876e6dc0ad2474ee7a87b00de5) | feat | add API to inject attributes on the host node ([#54604](https://github.com/angular/angular/pull/54604)) |
|
||||
| [fb540e169a](https://github.com/angular/angular/commit/fb540e169a78a61f38d611f538eea8fdb0971f1d) | feat | add migration for invalid two-way bindings ([#54630](https://github.com/angular/angular/pull/54630)) |
|
||||
| [c687b8f453](https://github.com/angular/angular/commit/c687b8f4531252cd1c3dfbb9a7bd42bdbe666a36) | feat | expose new `output()` API ([#54650](https://github.com/angular/angular/pull/54650)) |
|
||||
| [c809069f21](https://github.com/angular/angular/commit/c809069f213244afd0e2d803a6a43510b218e6f5) | feat | introduce `outputFromObservable()` interop function ([#54650](https://github.com/angular/angular/pull/54650)) |
|
||||
| [aff65fd1f4](https://github.com/angular/angular/commit/aff65fd1f4a61ed76a6f9b623852f197eb3500e4) | feat | introduce `outputToObservable` interop helper ([#54650](https://github.com/angular/angular/pull/54650)) |
|
||||
| [974958913c](https://github.com/angular/angular/commit/974958913ca632971f878a045537472f2c99c665) | feat | support TypeScript 5.4 ([#54414](https://github.com/angular/angular/pull/54414)) |
|
||||
| [39a50f9a8d](https://github.com/angular/angular/commit/39a50f9a8df5afc4968d18924f3d9d7d6b649d3a) | fix | ensure all initializer functions run in an injection context ([#54761](https://github.com/angular/angular/pull/54761)) |
|
||||
| [243ccce624](https://github.com/angular/angular/commit/243ccce62475ae03a2e727d2b3cb2d51a595c4a7) | fix | exclude class attribute intended for projection matching from directive matching ([#54800](https://github.com/angular/angular/pull/54800)) |
|
||||
| [2909e9817d](https://github.com/angular/angular/commit/2909e9817daf69b6478d7d09229491a9a48cff16) | fix | prevent infinite loops in clobbered elements check ([#54425](https://github.com/angular/angular/pull/54425)) |
|
||||
| [7243c704cf](https://github.com/angular/angular/commit/7243c704cf8a4986fae419793027458e142658f0) | fix | return a readonly signal on `asReadonly`. ([#54706](https://github.com/angular/angular/pull/54706)) |
|
||||
| [bb35414a38](https://github.com/angular/angular/commit/bb35414a38f8a8447d03c66dc5bf60b84409f9e3) | perf | speed up retrieval of `DestroyRef` in `EventEmitter` ([#54748](https://github.com/angular/angular/pull/54748)) |
|
||||
### http
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [8d37ed035c](https://github.com/angular/angular/commit/8d37ed035c9e9796ba9a7b1f055404ea220dbb3b) | fix | exclude caching for authenticated HTTP requests ([#54746](https://github.com/angular/angular/pull/54746)) |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [c1c7384e02](https://github.com/angular/angular/commit/c1c7384e02becc623c6a42985f7178ca98137264) | feat | Add reusable types for router guards ([#54580](https://github.com/angular/angular/pull/54580)) |
|
||||
| [7225485311](https://github.com/angular/angular/commit/722548531108e247660ebe6966b99b57d510b615) | fix | Navigations triggered by cancellation events should cancel previous navigation ([#54710](https://github.com/angular/angular/pull/54710)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.2.4"></a>
|
||||
# 17.2.4 (2024-03-06)
|
||||
### compiler-cli
|
||||
@@ -38,46 +79,6 @@
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.3.0-next.1"></a>
|
||||
# 17.3.0-next.1 (2024-02-27)
|
||||
### common
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [707bfc9b32](https://github.com/angular/angular/commit/707bfc9b326f321f302dccfdfebef7380c914eb5) | perf | `AsyncPipe` should not call `markForCheck` on subscription ([#54554](https://github.com/angular/angular/pull/54554)) |
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [1a6beae8a2](https://github.com/angular/angular/commit/1a6beae8a2bdcff27d4c1e402f98246a52247906) | feat | Enable template pipeline by default. ([#54571](https://github.com/angular/angular/pull/54571)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [12dc4d074e](https://github.com/angular/angular/commit/12dc4d074e63edaff626003ad6136a8d122b2ba6) | fix | account for as expression in docs extraction ([#54414](https://github.com/angular/angular/pull/54414)) |
|
||||
| [f578889ca2](https://github.com/angular/angular/commit/f578889ca2dbb22f2e050d4c4af6aa2d597bf70d) | fix | catch function instance properties in interpolated signal diagnostic ([#54325](https://github.com/angular/angular/pull/54325)) |
|
||||
| [f04ecc0cda](https://github.com/angular/angular/commit/f04ecc0cdaeb3a292a748b1ccc94ce70a573fc79) | fix | identify aliased initializer functions ([#54480](https://github.com/angular/angular/pull/54480)) |
|
||||
| [f5c566c079](https://github.com/angular/angular/commit/f5c566c0793eacf9ca146c8a6b8da15b0e8f4c4d) | fix | identify aliased initializer functions ([#54609](https://github.com/angular/angular/pull/54609)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [331b16efd2](https://github.com/angular/angular/commit/331b16efd2f5af876e6dc0ad2474ee7a87b00de5) | feat | add API to inject attributes on the host node ([#54604](https://github.com/angular/angular/pull/54604)) |
|
||||
| [974958913c](https://github.com/angular/angular/commit/974958913ca632971f878a045537472f2c99c665) | feat | support TypeScript 5.4 ([#54414](https://github.com/angular/angular/pull/54414)) |
|
||||
| [dcb9deb363](https://github.com/angular/angular/commit/dcb9deb3631ff4e839f738f2fb97ca6b894256ef) | fix | collect providers from NgModules while rendering `@defer` block ([#52881](https://github.com/angular/angular/pull/52881)) |
|
||||
| [ff40c9f762](https://github.com/angular/angular/commit/ff40c9f762b2c14870ac2859201ece9660087e79) | fix | fix typo in injectors.svg file ([#54596](https://github.com/angular/angular/pull/54596)) |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [71e0c7df69](https://github.com/angular/angular/commit/71e0c7df695f274a54d5ffb9dfc0587d06d3a953) | fix | resolve infinite loop for a single line element with a long tag name and angle bracket on a new line ([#54588](https://github.com/angular/angular/pull/54588)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.3.0-next.0"></a>
|
||||
# 17.3.0-next.0 (2024-02-21)
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [da7fbb40f0](https://github.com/angular/angular/commit/da7fbb40f06e6e37504f69e7b335f8219f424de2) | fix | detect when the linker is working in unpublished angular and widen supported versions ([#54439](https://github.com/angular/angular/pull/54439)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.2.2"></a>
|
||||
# 17.2.2 (2024-02-21)
|
||||
### common
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2010-2023 Google LLC. https://angular.io/license
|
||||
Copyright (c) 2010-2024 Google LLC. https://angular.io/license
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {input, confirm} from '@inquirer/prompts';
|
||||
import {existsSync} from 'fs';
|
||||
import {copyFile, mkdir, writeFile} from 'fs/promises';
|
||||
import {dirname, join} from 'path';
|
||||
import {fileURLToPath} from 'url';
|
||||
import {copyFolder} from './utils/fs.mjs';
|
||||
|
||||
const EXAMPLES_BASE = join(
|
||||
dirname(fileURLToPath(import.meta.url)),
|
||||
'../../src/content/examples',
|
||||
);
|
||||
const STACKBLITZ_TEMPLATE = join(
|
||||
dirname(fileURLToPath(import.meta.url)),
|
||||
'../../src/content/stackblitz-template',
|
||||
);
|
||||
|
||||
const MAIN_TS_FILENAME = 'main.ts';
|
||||
const STACKBLITZ_CONFIG_FILENAME = 'stackblitz.json';
|
||||
|
||||
createExample();
|
||||
|
||||
async function createExample(): Promise<void> {
|
||||
console.info('Create example');
|
||||
const name = await input({message: 'What is the example name?'});
|
||||
const description = await input({message: 'Could you provide the description of the example?'});
|
||||
const tags: string[] = [];
|
||||
let addMoreTags = true;
|
||||
|
||||
while (addMoreTags) {
|
||||
const tag = await input({message: 'Could you provide the tag of the example?'});
|
||||
|
||||
if (!!tag) {
|
||||
tags.push(tag);
|
||||
}
|
||||
|
||||
console.info();
|
||||
|
||||
addMoreTags = await confirm({message: 'Would you like to add another tag?', default: true});
|
||||
}
|
||||
|
||||
const shouldCopySrcAppFolderFromTemplate = await confirm({
|
||||
message: 'Would you like to copy /src/app/**.* from template?',
|
||||
default: true,
|
||||
});
|
||||
const shouldCopyMainTsFromTemplate = await confirm({
|
||||
message: 'Would you like to copy /src/main.ts from template?',
|
||||
default: false,
|
||||
});
|
||||
|
||||
const examplePath = join(EXAMPLES_BASE, name);
|
||||
|
||||
await createEmptyExample(examplePath, name, description, tags);
|
||||
await copyFilesFromTemplate(
|
||||
examplePath,
|
||||
shouldCopySrcAppFolderFromTemplate,
|
||||
shouldCopyMainTsFromTemplate,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the directory and marker files for the new example.
|
||||
*/
|
||||
async function createEmptyExample(
|
||||
examplePath: string,
|
||||
exampleName: string,
|
||||
description: string,
|
||||
tags: string[],
|
||||
): Promise<void> {
|
||||
validateExampleName(exampleName);
|
||||
|
||||
await ensureExamplePath(examplePath);
|
||||
await writeStackBlitzFile(examplePath, description, tags);
|
||||
}
|
||||
|
||||
function validateExampleName(exampleName: string): void {
|
||||
if (/\s/.test(exampleName)) {
|
||||
throw new Error(`Unable to create example. The example name contains spaces: '${exampleName}'`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the new example directory exists.
|
||||
*/
|
||||
async function ensureExamplePath(examplePath: string): Promise<void> {
|
||||
if (existsSync(examplePath)) {
|
||||
throw new Error(
|
||||
`Unable to create example. The path to the new example already exists: ${examplePath}`,
|
||||
);
|
||||
}
|
||||
await mkdir(examplePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the `stackblitz.json` file into the new example.
|
||||
*/
|
||||
async function writeStackBlitzFile(
|
||||
examplePath: string,
|
||||
description: string,
|
||||
tags: string[],
|
||||
): Promise<void> {
|
||||
const config = {
|
||||
description,
|
||||
ignore: ['**/*.d.ts', '**/*.js', '**/*.[1,2].*'],
|
||||
tags,
|
||||
};
|
||||
await writeFile(
|
||||
join(examplePath, STACKBLITZ_CONFIG_FILENAME),
|
||||
JSON.stringify(config, null, 2) + '\n',
|
||||
);
|
||||
}
|
||||
|
||||
async function copyFilesFromTemplate(
|
||||
examplePath: string,
|
||||
shouldCopySrcAppFolderFromTemplate: boolean,
|
||||
shouldCopyMainTsFromTemplate: boolean,
|
||||
): Promise<void> {
|
||||
if (shouldCopySrcAppFolderFromTemplate) {
|
||||
const appTemplateFolderPath = join(STACKBLITZ_TEMPLATE, 'src/app');
|
||||
const exampleAppFolderPath = join(examplePath, 'src/app');
|
||||
await copyFolder(appTemplateFolderPath, exampleAppFolderPath);
|
||||
}
|
||||
if (shouldCopyMainTsFromTemplate) {
|
||||
const mainTsTemplatePath = join(STACKBLITZ_TEMPLATE, 'src', MAIN_TS_FILENAME);
|
||||
const exampleMainTsPath = join(examplePath, 'src', MAIN_TS_FILENAME);
|
||||
await copyFile(mainTsTemplatePath, exampleMainTsPath);
|
||||
}
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import ts from 'typescript';
|
||||
import * as fs from 'fs';
|
||||
import {dirname, join, relative} from 'path';
|
||||
import {fileURLToPath} from 'url';
|
||||
import {readFile, writeFile} from 'fs/promises';
|
||||
|
||||
const TYPESCRIPT_EXTENSION = '.ts';
|
||||
const SKIP_FILES_WITH_EXTENSIONS = ['.e2e-spec.ts', '.spec.ts', '.po.ts'];
|
||||
const EXAMPLES_PATH = `../../content/examples`;
|
||||
|
||||
interface File {
|
||||
path: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface AnalyzedFiles {
|
||||
path: string;
|
||||
componentNames: string[];
|
||||
}
|
||||
|
||||
/** Absolute disk path to the project directory. */
|
||||
const examplesDir = join(
|
||||
dirname(fileURLToPath(import.meta.url)),
|
||||
'../../src/content/examples',
|
||||
);
|
||||
const projectDir = join(dirname(fileURLToPath(import.meta.url)), '../../src');
|
||||
const previewsComponentMapFileName = 'previews.ts';
|
||||
|
||||
main();
|
||||
|
||||
async function main() {
|
||||
const files = await retrieveAllTypescriptFiles(
|
||||
examplesDir,
|
||||
(path) => !SKIP_FILES_WITH_EXTENSIONS.some((extensionToSkip) => path.endsWith(extensionToSkip)),
|
||||
);
|
||||
|
||||
const filesWithComponent = files
|
||||
.map((file) => ({
|
||||
componentNames: analyzeFile(file),
|
||||
path: file.path,
|
||||
}))
|
||||
.filter((result) => result.componentNames.length > 0);
|
||||
|
||||
const previewsComponentMap = generatePreviewsComponentMap(filesWithComponent);
|
||||
|
||||
fs.mkdirSync(join(projectDir, 'assets/previews'), {recursive: true});
|
||||
|
||||
await writeFile(
|
||||
join(projectDir, 'assets/previews', previewsComponentMapFileName),
|
||||
previewsComponentMap,
|
||||
);
|
||||
}
|
||||
|
||||
/** Recursively search the provided directory for all typescript files and asynchronously load them. */
|
||||
function retrieveAllTypescriptFiles(
|
||||
baseDir: string,
|
||||
predicateFn: (path: string) => boolean,
|
||||
): Promise<File[]> {
|
||||
const typescriptFiles: Promise<File>[] = [];
|
||||
|
||||
const checkFilesInDirectory = (dir: string) => {
|
||||
const files = fs.readdirSync(dir, {withFileTypes: true});
|
||||
for (const file of files) {
|
||||
const fullPathToFile = join(dir, file.name);
|
||||
const relativeFilePath = relative(baseDir, fullPathToFile);
|
||||
|
||||
if (
|
||||
file.isFile() &&
|
||||
file.name.endsWith(TYPESCRIPT_EXTENSION) &&
|
||||
predicateFn(relativeFilePath)
|
||||
) {
|
||||
typescriptFiles.push(
|
||||
readFile(fullPathToFile, {encoding: 'utf-8'}).then((fileContent) => {
|
||||
return {
|
||||
path: relativeFilePath,
|
||||
content: fileContent,
|
||||
};
|
||||
}),
|
||||
);
|
||||
} else if (file.isDirectory()) {
|
||||
checkFilesInDirectory(fullPathToFile);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
checkFilesInDirectory(baseDir);
|
||||
|
||||
return Promise.all(typescriptFiles);
|
||||
}
|
||||
|
||||
/** Returns list of the `Standalone` @Component class names for given file */
|
||||
function analyzeFile(file: File): string[] {
|
||||
const componentClassNames: string[] = [];
|
||||
const sourceFile = ts.createSourceFile(file.path, file.content, ts.ScriptTarget.Latest, false);
|
||||
|
||||
const visitNode = (node: ts.Node): void => {
|
||||
if (ts.isClassDeclaration(node)) {
|
||||
const decorators = ts.getDecorators(node);
|
||||
const componentName = node.name ? node.name.text : null;
|
||||
|
||||
if (decorators && decorators.length) {
|
||||
for (const decorator of decorators) {
|
||||
const call = decorator.expression;
|
||||
|
||||
if (
|
||||
ts.isCallExpression(call) &&
|
||||
ts.isIdentifier(call.expression) &&
|
||||
call.expression.text === 'Component' &&
|
||||
call.arguments.length > 0 &&
|
||||
ts.isObjectLiteralExpression(call.arguments[0])
|
||||
) {
|
||||
const hasStandaloneProperty = call.arguments[0].properties.some(
|
||||
(property) =>
|
||||
property.name &&
|
||||
ts.isIdentifier(property.name) &&
|
||||
property.name.text === 'standalone',
|
||||
);
|
||||
|
||||
if (hasStandaloneProperty && componentName) {
|
||||
componentClassNames.push(componentName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ts.forEachChild(node, visitNode);
|
||||
};
|
||||
|
||||
visitNode(sourceFile);
|
||||
|
||||
return componentClassNames;
|
||||
}
|
||||
|
||||
function generatePreviewsComponentMap(data: AnalyzedFiles[]): string {
|
||||
let result = '';
|
||||
for (const fileData of data) {
|
||||
for (const componentName of fileData.componentNames) {
|
||||
const key = `${fileData.path}${
|
||||
fileData.componentNames.length > 1 ? '_' + componentName : ''
|
||||
}`.replace(/\\/g, '/');
|
||||
result += `['${key}']: () => import('${EXAMPLES_PATH}/${fileData.path
|
||||
.replace(/\\/g, '/')
|
||||
.replace('.ts', '')}').then(c => c.${componentName}),\n`;
|
||||
}
|
||||
}
|
||||
return fs
|
||||
.readFileSync('scripts/examples/previews-components.template', 'utf8')
|
||||
.replace(/\${previewsComponents}/g, result);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
/**
|
||||
******************************************************************************
|
||||
* DO NOT MANUALLY EDIT THIS FILE. THIS FILE IS AUTOMATICALLY GENERATED.
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
import {Type} from '@angular/core';
|
||||
|
||||
/**
|
||||
* Map of the previews components, values are functions which returns the promise of the component type, which will be displayed as preview in the ExampleViewer component.
|
||||
* Keys has to be equal to paths written down in the docs markdown files.
|
||||
*/
|
||||
export const PREVIEWS_COMPONENTS_MAP: Record<string, () => Promise<Type<unknown>>> = {
|
||||
${previewsComponents}
|
||||
};
|
||||
@@ -1,221 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {dirname, join} from 'path';
|
||||
import {readFile, writeFile} from 'fs/promises';
|
||||
import {
|
||||
ASSETS_EXAMPLE_PATH,
|
||||
CSS_TS_COPYRIGHT,
|
||||
EXAMPLES_PATH,
|
||||
HTML_COPYRIGHT,
|
||||
STACKBLITZ_TEMPLATE_PATH,
|
||||
STACKBLITZ_CONFIG_FILENAME,
|
||||
TEMPORARY_EXAMPLES_PATH,
|
||||
EXCLUDE_FILES_FOR_STACKBLITZ,
|
||||
} from './utils/examples-constants.mjs';
|
||||
import {copyFolder, createFolder, removeFolder} from './utils/fs.mjs';
|
||||
import jsdom from 'jsdom';
|
||||
import {glob} from 'glob';
|
||||
import {regionParser} from './../../prerender/markdown-pipeline/regions/region-parser.mjs';
|
||||
|
||||
interface StackblitzConfig {
|
||||
ignore: string[];
|
||||
file: string;
|
||||
tags: string[];
|
||||
description: string;
|
||||
}
|
||||
|
||||
export async function generateStackblitzExample(
|
||||
exampleFolderName: string,
|
||||
primaryFilePath: string,
|
||||
title: string,
|
||||
) {
|
||||
const exampleDir = join(EXAMPLES_PATH, exampleFolderName);
|
||||
const temporaryExampleDir = join(TEMPORARY_EXAMPLES_PATH, exampleFolderName);
|
||||
const config = await readFile(join(exampleDir, STACKBLITZ_CONFIG_FILENAME), 'utf-8');
|
||||
const stackblitzConfig: StackblitzConfig = JSON.parse(config);
|
||||
primaryFilePath = join(...primaryFilePath.split('/'));
|
||||
|
||||
await createFolder(temporaryExampleDir);
|
||||
await combineTemplateWithExample(exampleDir, temporaryExampleDir);
|
||||
await generateStackblitzHtml(
|
||||
temporaryExampleDir,
|
||||
stackblitzConfig,
|
||||
exampleFolderName,
|
||||
primaryFilePath,
|
||||
title,
|
||||
);
|
||||
await removeFolder(temporaryExampleDir);
|
||||
}
|
||||
|
||||
async function combineTemplateWithExample(
|
||||
exampleDir: string,
|
||||
temporaryExampleDir: string,
|
||||
): Promise<void> {
|
||||
// Copy template files to TEMP folder
|
||||
await copyFolder(STACKBLITZ_TEMPLATE_PATH, temporaryExampleDir);
|
||||
|
||||
// Copy example files to TEMP folder
|
||||
await copyFolder(exampleDir, temporaryExampleDir);
|
||||
}
|
||||
|
||||
async function generateStackblitzHtml(
|
||||
temporaryExampleDir: string,
|
||||
stackBlitzConfig: StackblitzConfig,
|
||||
exampleFolderName: string,
|
||||
primaryFilePath: string,
|
||||
title: string,
|
||||
): Promise<void> {
|
||||
const defaultIncludes = [
|
||||
'**/*.ts',
|
||||
'**/*.js',
|
||||
'**/*.css',
|
||||
'**/*.html',
|
||||
'**/*.md',
|
||||
'**/*.json',
|
||||
'**/*.svg',
|
||||
];
|
||||
const exampleFilePaths = await glob(defaultIncludes, {
|
||||
cwd: temporaryExampleDir,
|
||||
nodir: true,
|
||||
dot: true,
|
||||
ignore: stackBlitzConfig.ignore,
|
||||
});
|
||||
|
||||
const postData = await createPostData(
|
||||
temporaryExampleDir,
|
||||
stackBlitzConfig,
|
||||
exampleFilePaths,
|
||||
title,
|
||||
);
|
||||
const primaryFile = getPrimaryFile(primaryFilePath ?? stackBlitzConfig.file, exampleFilePaths);
|
||||
const html = createStackblitzHtml(postData, primaryFile);
|
||||
|
||||
const stackblitzHtmlPath = join(
|
||||
join(ASSETS_EXAMPLE_PATH, exampleFolderName),
|
||||
`${primaryFile}.html`,
|
||||
);
|
||||
await createFolder(dirname(stackblitzHtmlPath));
|
||||
await writeFile(stackblitzHtmlPath, html, 'utf-8');
|
||||
}
|
||||
|
||||
function getPrimaryFile(primaryFilePath: string, exampleFilePaths: string[]): string {
|
||||
if (primaryFilePath) {
|
||||
if (!exampleFilePaths.some((filePath) => filePath === primaryFilePath)) {
|
||||
throw new Error(`The specified primary file (${primaryFilePath}) does not exist!`);
|
||||
}
|
||||
return primaryFilePath;
|
||||
} else {
|
||||
const defaultPrimaryFilePaths = [
|
||||
'src/app/app.component.html',
|
||||
'src/app/app.component.ts',
|
||||
'src/app/main.ts',
|
||||
];
|
||||
const primaryFile = defaultPrimaryFilePaths.find((path) =>
|
||||
exampleFilePaths.some((filePath) => filePath === path),
|
||||
);
|
||||
|
||||
if (!primaryFile) {
|
||||
throw new Error(
|
||||
`None of the default primary files (${defaultPrimaryFilePaths.join(', ')}) exists.`,
|
||||
);
|
||||
}
|
||||
|
||||
return primaryFile;
|
||||
}
|
||||
}
|
||||
|
||||
async function createPostData(
|
||||
exampleDir: string,
|
||||
config: StackblitzConfig,
|
||||
exampleFilePaths: string[],
|
||||
title: string,
|
||||
): Promise<Record<string, string>> {
|
||||
const postData: Record<string, string> = {};
|
||||
|
||||
for (const filePath of exampleFilePaths) {
|
||||
if (EXCLUDE_FILES_FOR_STACKBLITZ.some((excludedFile) => filePath.endsWith(excludedFile))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let content = await readFile(join(exampleDir, filePath), 'utf-8');
|
||||
content = appendCopyright(filePath, content);
|
||||
content = extractRegions(filePath, content);
|
||||
|
||||
postData[`project[files][${filePath}]`] = content;
|
||||
}
|
||||
|
||||
const tags = ['angular', 'example', ...(config.tags || [])];
|
||||
tags.forEach((tag, index) => (postData[`project[tags][${index}]`] = tag));
|
||||
|
||||
postData['project[description]'] = `Angular Example - ${config.description}`;
|
||||
postData['project[template]'] = 'node';
|
||||
postData['project[title]'] = title ?? 'Angular Example';
|
||||
|
||||
return postData;
|
||||
}
|
||||
|
||||
function createStackblitzHtml(postData: Record<string, string>, primaryFile: string): string {
|
||||
const baseHtml = createBaseStackblitzHtml(primaryFile);
|
||||
const doc = new jsdom.JSDOM(baseHtml).window.document;
|
||||
const form = doc.querySelector('form');
|
||||
|
||||
for (const [key, value] of Object.entries(postData)) {
|
||||
const element = htmlToElement(doc, `<input type="hidden" name="${key}">`);
|
||||
if (element && form) {
|
||||
element.setAttribute('value', value as string);
|
||||
form.appendChild(element);
|
||||
}
|
||||
}
|
||||
|
||||
return doc.documentElement.outerHTML;
|
||||
}
|
||||
|
||||
function createBaseStackblitzHtml(primaryFile: string) {
|
||||
const file = `?file=${primaryFile}`;
|
||||
const action = `https://stackblitz.com/run${file}`;
|
||||
|
||||
return `
|
||||
<!DOCTYPE html><html lang="en"><body>
|
||||
<form id="mainForm" method="post" action="${action}" target="_self"></form>
|
||||
<script>
|
||||
var embedded = 'ctl=1';
|
||||
var isEmbedded = window.location.search.indexOf(embedded) > -1;
|
||||
|
||||
if (isEmbedded) {
|
||||
var form = document.getElementById('mainForm');
|
||||
var action = form.action;
|
||||
var actionHasParams = action.indexOf('?') > -1;
|
||||
var symbol = actionHasParams ? '&' : '?'
|
||||
form.action = form.action + symbol + embedded;
|
||||
}
|
||||
document.getElementById("mainForm").submit();
|
||||
</script>
|
||||
</body></html>
|
||||
`.trim();
|
||||
}
|
||||
|
||||
function appendCopyright(filename: string, content: string): string {
|
||||
if (filename.endsWith('.html')) {
|
||||
return `${HTML_COPYRIGHT}${content}`;
|
||||
} else if (filename.endsWith('.ts') || filename.endsWith('.css')) {
|
||||
return `${CSS_TS_COPYRIGHT}${content}`;
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
function htmlToElement(document: Document, html: string) {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = html;
|
||||
return div.firstElementChild;
|
||||
}
|
||||
|
||||
function extractRegions(path: string, contents: string): string {
|
||||
const regionParserResult = regionParser(contents, path);
|
||||
return regionParserResult.contents;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"ts-node": {
|
||||
"compilerOptions": {
|
||||
"module": "es2020"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {dirname, join} from 'path';
|
||||
import {fileURLToPath} from 'url';
|
||||
|
||||
const BASE = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// Directory paths
|
||||
export const ASSETS_EXAMPLE_PATH = join(
|
||||
BASE,
|
||||
'../../../src/assets/content/examples',
|
||||
);
|
||||
export const EXAMPLES_PATH = join(BASE, '../../../src/content/examples');
|
||||
export const STACKBLITZ_TEMPLATE_PATH = join(
|
||||
BASE,
|
||||
'../../../src/content/stackblitz-template',
|
||||
);
|
||||
|
||||
export const TEMPORARY_EXAMPLES_PATH = join(fileURLToPath(import.meta.url), '../../../../TEMP');
|
||||
|
||||
// Extensions
|
||||
export const TEST_FILES_EXTENSION_SUFFIX = '.spec.ts';
|
||||
export const TEST_FILES_E2E_EXTENSION_SUFFIX = '.e2e-spec.ts';
|
||||
|
||||
// Filename
|
||||
// TODO: BUILD.bazel and example-config will be deprecated when migration to standalone examples will be done.
|
||||
export const BUILD_BAZEL_FILENAME = 'BUILD.bazel';
|
||||
export const EXAMPLE_CONFIG_FILENAME = 'example-config.json';
|
||||
export const STACKBLITZ_CONFIG_FILENAME = 'stackblitz.json';
|
||||
|
||||
// Copyright
|
||||
const PAD = '\n\n';
|
||||
const COPYRIGHT =
|
||||
'@license\n' +
|
||||
'Copyright Google LLC All Rights Reserved.\n' +
|
||||
'\n' +
|
||||
'Use of this source code is governed by an MIT-style license that can be\n' +
|
||||
'found in the LICENSE file at https://angular.dev/license\n';
|
||||
export const CSS_TS_COPYRIGHT = `/*\n${COPYRIGHT}\n*/${PAD}`;
|
||||
export const HTML_COPYRIGHT = `<!-- \n${COPYRIGHT}\n-->${PAD}`;
|
||||
|
||||
// Exclude
|
||||
export const EXCLUDE_FILES_FOR_STACKBLITZ = [
|
||||
STACKBLITZ_CONFIG_FILENAME,
|
||||
BUILD_BAZEL_FILENAME,
|
||||
EXAMPLE_CONFIG_FILENAME,
|
||||
TEST_FILES_EXTENSION_SUFFIX,
|
||||
TEST_FILES_E2E_EXTENSION_SUFFIX,
|
||||
];
|
||||
@@ -1,77 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {Dirent, existsSync, readdirSync} from 'fs';
|
||||
import {copyFile, mkdir, readFile, readdir, rm, stat} from 'fs/promises';
|
||||
import {join, relative} from 'path';
|
||||
|
||||
export async function copyFolder(source: string, destination: string) {
|
||||
if (!existsSync(destination)) {
|
||||
await mkdir(destination, {recursive: true});
|
||||
}
|
||||
|
||||
const files = await readdir(source);
|
||||
|
||||
for (const file of files) {
|
||||
const sourcePath = join(source, file);
|
||||
const destPath = join(destination, file);
|
||||
|
||||
const stats = await stat(sourcePath);
|
||||
const isDirectory = await stats.isDirectory();
|
||||
|
||||
if (isDirectory) {
|
||||
await copyFolder(sourcePath, destPath);
|
||||
} else {
|
||||
await copyFile(sourcePath, destPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Recursively search the provided directory for all files which satisfy predicate and asynchronously load them. */
|
||||
export function retrieveFiles(
|
||||
baseDir: string,
|
||||
predicate?: (file: Dirent) => boolean,
|
||||
): Promise<{path: string; content: string}[]> {
|
||||
const filesWithContent: Promise<{path: string; content: string}>[] = [];
|
||||
|
||||
const checkFilesInDirectory = (dir: string) => {
|
||||
const files = readdirSync(dir, {withFileTypes: true});
|
||||
for (const file of files) {
|
||||
const fullPathToFile = join(dir, file.name);
|
||||
|
||||
if (file.isFile() && (predicate ? predicate(file) : true)) {
|
||||
filesWithContent.push(
|
||||
readFile(fullPathToFile, {encoding: 'utf-8'}).then((fileContent) => {
|
||||
return {
|
||||
path: relative(baseDir, fullPathToFile),
|
||||
content: fileContent,
|
||||
};
|
||||
}),
|
||||
);
|
||||
} else if (file.isDirectory()) {
|
||||
checkFilesInDirectory(fullPathToFile);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
checkFilesInDirectory(baseDir);
|
||||
|
||||
return Promise.all(filesWithContent);
|
||||
}
|
||||
|
||||
export async function createFolder(path: string): Promise<void> {
|
||||
if (!existsSync(path)) {
|
||||
await mkdir(path, {recursive: true});
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeFolder(path: string): Promise<void> {
|
||||
if (existsSync(path)) {
|
||||
await rm(path, {recursive: true});
|
||||
}
|
||||
}
|
||||
@@ -116,7 +116,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<p class="docs-license">
|
||||
Super-powered by Google ©2010-2023. Code licensed under an
|
||||
Super-powered by Google ©2010-2024. Code licensed under an
|
||||
<a routerLink="/license" title="License text">MIT-style License</a>
|
||||
. Documentation licensed under
|
||||
<a href="https://creativecommons.org/licenses/by/4.0/">CC BY 4.0</a>
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
# EmbeddedEditor components, services and functionality
|
||||
|
||||
- [Scenarios](#scenarios)
|
||||
- [Loading a project](#loading-a-project)
|
||||
- [Updating the code](#updating-the-code)
|
||||
- [Creating a new file](#creating-a-new-file)
|
||||
- [Deleting a file](#deleting-a-file)
|
||||
- [Switching a project](#switching-a-project)
|
||||
- [Components and services](#components-and-services)
|
||||
|
||||
- [EmbeddedEditor](#EmbeddedEditor)
|
||||
- [CodeEditor](#CodeEditor)
|
||||
- [CodeMirrorEditor](#CodeMirrorEditor)
|
||||
- [TypeScript Web Worker](#typescript-web-worker)
|
||||
- [Preview](#Preview)
|
||||
- [Terminal](#Terminal)
|
||||
- [InteractiveTerminal](#InteractiveTerminal)
|
||||
- [Console](#Console)
|
||||
- [NodeRuntimeSandbox](#NodeRuntimeSandbox)
|
||||
- [NodeRuntimeState](#NodeRuntimeState)
|
||||
- [EmbeddedTutorialManager](#EmbeddedTutorialManager)
|
||||
- [EditorUiState](#EditorUiState)
|
||||
- [DownloadManager](#DownloadManager)
|
||||
- [AlertManager](#AlertManager)
|
||||
- [TypingsLoader](#TypingsLoader)
|
||||
|
||||
## External libraries
|
||||
|
||||
- [WebContainers API](https://webcontainers.io/)
|
||||
- [CodeMirror](https://codemirror.net/)
|
||||
- [@typescript/vfs](https://www.npmjs.com/package/@typescript/vfs)
|
||||
- [Xterm.js](https://xtermjs.org/)
|
||||
|
||||
## Notes
|
||||
|
||||
- See [scripts/tutorials/README.md](/scripts/tutorials/README.md) for more information about the tutorials script.
|
||||
- See [adev/src/content/tutorials/README.md](/adev/src/content/tutorials/README.md) for more information about the tutorials content.
|
||||
|
||||
---
|
||||
|
||||
## Scenarios
|
||||
|
||||
### Loading a project
|
||||
|
||||
1. The page responsible for the embedded editor lazy loads the [`EmbeddedEditor`](./embedded-editor.component.ts) component and the [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts), then triggers the initialization of all components and services. The embedded editor is available in the following pages:
|
||||
|
||||
- homepage: https://angular.dev
|
||||
- playground: https://angular.dev/playground
|
||||
- tutorial pages: https://angular.dev/tutorials
|
||||
|
||||
2. The project assets are fetched by the [`EmbeddedTutorialManager`](./embedded-tutorial-manager.service.ts). Meanwhile:
|
||||
|
||||
- The code editor is initialized
|
||||
- The code editor initializes the TypeScript Web Worker, which initializes the "default file system map" using TypeScript's CDN.
|
||||
- The WebContainer is initialized
|
||||
- The terminal is initialized
|
||||
|
||||
3. The tutorial source code is mounted in the `WebContainer`'s filesystem by the [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts)
|
||||
4. The tutorial project dependencies are installed by the [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts).
|
||||
5. The development server is started by the [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts) and the types are loaded by the [`TypingsLoader`](./typings-loader.service.ts) service.
|
||||
6. The preview is loaded with the URL provided by the WebContainer API after the development server is started.
|
||||
7. The project is ready.
|
||||
|
||||
### Updating the code
|
||||
|
||||
1. The user update the code in the code editor.
|
||||
2. The code editor state is updated on real time, without debouncing so that the user can see the changes in the code editor and CodeMirror can handle the changes accordingly.
|
||||
3. At the same time, the changes are sent to the TypeScript web worker to provide diagnostics, autocomplete and type features as soon as possible.
|
||||
4. The code changes are debounced to be written in the WebContainer filesystem by the [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts).
|
||||
5. After the debounce time is reached, the code changes are written in the WebContainer filesystem by the [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts), then the user can see the changes in the preview.
|
||||
|
||||
### Creating a new file
|
||||
|
||||
1. The user clicks on the new file button.
|
||||
2. The new file tab is opened.
|
||||
3. The user types the new file name.
|
||||
4. If the file name is valid, the file is created in the WebContainer filesystem by the [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts).
|
||||
|
||||
- `..` is disallowed in the file name to prevent users to create files outside the `src` directory.
|
||||
|
||||
5. The file is added to the TypeScript virtual file system, allowing the TypeScript web worker to provide diagnostics, autocomplete and type features for the new file. Also, exports from the new file are available in other files.
|
||||
6. The new file is added as the last tab in the code editor and the new file can be edited.
|
||||
|
||||
Note: If the new file name matches a file that already exists but is hidden in the code editor, the content for that file will show up in the created file. An example for a file that always exists is `index.html`.
|
||||
|
||||
### Deleting a file
|
||||
|
||||
1. The user clicks on the delete file button.
|
||||
2. The file is deleted from the WebContainer filesystem by the [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts).
|
||||
3. The file is removed from the TypeScript virtual file system.
|
||||
4. The file is removed from the code editor tabs.
|
||||
|
||||
Note: Some files can't be deleted to prevent users to break the app, being `src/main.ts`and `src/index.html`
|
||||
|
||||
### Switching a project
|
||||
|
||||
The embedded editor considers a project change when the embedded editor was already initialized and the user changes the page in the following scenarios:
|
||||
|
||||
- Navigating through tutorial steps
|
||||
- Going from the homepage after the embedded editor is initialized to the playground
|
||||
- Going from a tutorial page to the playground
|
||||
- Going from a tutorial page to the homepage
|
||||
- Going from the playground to the homepage
|
||||
|
||||
When a project change is detected, the [`EmbeddedTutorialManager`](./embedded-tutorial-manager.service.ts) emits the `tutorialChanged` observable, which is listened in multiple sub-components and services, then each component/service performs the necessary operations to switch the project.
|
||||
|
||||
The following steps are executed on project change:
|
||||
|
||||
1. The new project files are fetched by the [`EmbeddedTutorialManager`](./embedded-tutorial-manager.service.ts).
|
||||
2. The new project files are mounted in the WebContainer filesystem.
|
||||
3. The TypeScript virtual filesystem is updated with the new files and contents.
|
||||
4. The previous project and new project files are compared.
|
||||
1. Files that are not available in the new project are deleted from the WebContainer filesystem.
|
||||
2. Files that have the same path and name have their content replaced on the previous step when the files are mounted.
|
||||
5. The previous project dependencies are compared with the new project dependencies.
|
||||
1. If there are differences, a `npm install` is triggered, hiding the preview and going to the install loading step.
|
||||
2. If there are no differences, the project is ready.
|
||||
6. Some states are resetted, for example the "reveal answer" state if the previous project was in the "reveal answer" state.
|
||||
|
||||
## Components and services
|
||||
|
||||
### [`EmbeddedEditor`](./embedded-editor.component.ts)
|
||||
|
||||
The embedded editor is the parent component that holds all the components and services that compose the embedded editor.
|
||||
|
||||
#### [`CodeEditor`](./code-editor/code-editor.component.ts)
|
||||
|
||||
The component that holds the code editor view and the code editor state.
|
||||
|
||||
##### [`CodeMirrorEditor`](./code-editor/code-mirror-editor.service.ts)
|
||||
|
||||
[CodeMirror](https://codemirror.net/) is the library used to handle the code editor.
|
||||
|
||||
The `CodeMirrorEditor` service manages the CodeMirror instance and all the interactions with the library used to handle the code editor.
|
||||
|
||||
- handle the file edits and the CodeMirror view and state
|
||||
- handle the current project files in the code editor
|
||||
- handle the file creations and deletions
|
||||
- handle the file changes
|
||||
- handle all the CodeMirror specific events and extensions
|
||||
|
||||
###### [TypeScript Web Worker](./code-editor/workers/typescript-vfs.worker.ts)
|
||||
|
||||
The TypeScript features are provided by the TypeScript web worker, that is initialized by the `CodeMirrorEditor` service.
|
||||
|
||||
The TypeScript web worker uses `@typescript/vfs` and the TypeScript language service to provide diagnostics, autocomplete and type features.
|
||||
|
||||
#### [`Preview`](./preview/preview.component.ts)
|
||||
|
||||
The preview component manages the `iframe` responsible for displaying the tutorial project preview, with the URL provided by the WebContainer API after the development server is started.
|
||||
|
||||
While the project is being initialized, the preview displays the loading state.
|
||||
|
||||
#### [`Terminal`](./terminal/terminal.component.ts)
|
||||
|
||||
[Xterm.js](https://xtermjs.org/) is the library used to handle the terminals.
|
||||
|
||||
The terminal component handles the Xterm.js instance for the console and for the interactive terminal.
|
||||
|
||||
##### [`InteractiveTerminal`](./terminal/interactive-terminal.ts)
|
||||
|
||||
The interactive terminal is the terminal where the user can interact with the terminal and run commands, supporting only commands for the Angular CLI.
|
||||
|
||||
##### Console
|
||||
|
||||
The console displays the output for `npm install` and `ng serve`.
|
||||
|
||||
#### [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts)
|
||||
|
||||
Responsible for managing the WebContainer instance and all communication with its API. This service handles:
|
||||
|
||||
- the WebContainer instance
|
||||
- all Node.js scripts
|
||||
- the WebContainer filesystem, mounting the tutorial project files, writing new content, deleting and creating files.
|
||||
- the terminal session, reading and processing user inputs.
|
||||
- the tutorial project dependencies, installing the dependencies.
|
||||
- the processes running inside the WebContainer, being the npm scripts to install the dependencies, run the development server and the user inputs for the `ng` CLI.
|
||||
|
||||
##### [`NodeRuntimeState`](./node-runtime-state.service.ts)
|
||||
|
||||
Manages the [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts) loading and error state.
|
||||
|
||||
#### [`EmbeddedTutorialManager`](./embedded-tutorial-manager.service.ts)
|
||||
|
||||
Manages the tutorial assets, being responsible for fetching the tutorial source code and metadata.
|
||||
|
||||
The source code is mounted in the WebContainer filesystem by the [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts).
|
||||
|
||||
The metadata is used to manage the project, handle the project changes and the user interactivity with the app.
|
||||
|
||||
This service also handles the reveal answer and reset reveal answer feature.
|
||||
|
||||
#### [`EditorUiState`](./editor-ui-state.service.ts)
|
||||
|
||||
Manages the editor UI state, being responsible for handling the user interactions with the editor tabs, switching between the preview, the terminal and the console.
|
||||
|
||||
#### [`DownloadManager`](./download-manager.service.ts)
|
||||
|
||||
Responsible for handling the download button in the embedded editor, fetching the tutorial project files and generating a zip file with the project content.
|
||||
|
||||
#### [`AlertManager`](./alert-manager.service.ts)
|
||||
|
||||
Manage the alerts displayed in the embedded editor, being the out of memory alert when multiple tabs are opened, and unsupported enviroments alerts.
|
||||
|
||||
#### [`TypingsLoader`](./typings-loader.service.ts)
|
||||
|
||||
Manages the types definitions for the code editor.
|
||||
@@ -157,6 +157,11 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
|
||||
path: 'guide/components/advanced-configuration',
|
||||
contentPath: 'guide/components/advanced-configuration',
|
||||
},
|
||||
{
|
||||
label: 'Custom Elements',
|
||||
path: 'guide/elements',
|
||||
contentPath: 'guide/elements',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -218,9 +223,39 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
|
||||
contentPath: 'guide/templates/control-flow',
|
||||
},
|
||||
{
|
||||
label: 'Deferrable views',
|
||||
path: 'guide/defer',
|
||||
contentPath: 'guide/defer',
|
||||
label: 'Pipes',
|
||||
children: [
|
||||
{
|
||||
label: 'Overview',
|
||||
path: 'guide/pipes',
|
||||
contentPath: 'guide/pipes/overview',
|
||||
},
|
||||
{
|
||||
label: 'Using a pipe in a template',
|
||||
path: 'guide/pipes/template',
|
||||
contentPath: 'guide/pipes/template',
|
||||
},
|
||||
{
|
||||
label: 'Custom pipes',
|
||||
path: 'guide/pipes/transform-data',
|
||||
contentPath: 'guide/pipes/transform-data',
|
||||
},
|
||||
{
|
||||
label: 'Pipe precedence in expressions',
|
||||
path: 'guide/pipes/precedence',
|
||||
contentPath: 'guide/pipes/precedence',
|
||||
},
|
||||
{
|
||||
label: 'Change detection with pipes',
|
||||
path: 'guide/pipes/change-detection',
|
||||
contentPath: 'guide/pipes/change-detection',
|
||||
},
|
||||
{
|
||||
label: 'Unwrapping data from an observable',
|
||||
path: 'guide/pipes/unwrapping-data-observables',
|
||||
contentPath: 'guide/pipes/unwrapping-data-observables',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Template reference variables',
|
||||
@@ -317,6 +352,16 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
|
||||
path: 'guide/signals/rxjs-interop',
|
||||
contentPath: 'guide/signals/rxjs-interop',
|
||||
},
|
||||
{
|
||||
label: 'Inputs as signals',
|
||||
path: 'guide/signals/inputs',
|
||||
contentPath: 'guide/signals/inputs',
|
||||
},
|
||||
{
|
||||
label: 'Model inputs',
|
||||
path: 'guide/signals/model',
|
||||
contentPath: 'guide/signals/model',
|
||||
},
|
||||
{
|
||||
label: 'Queries as signals',
|
||||
path: 'guide/signals/queries',
|
||||
@@ -324,71 +369,6 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'NgModule',
|
||||
children: [
|
||||
{
|
||||
label: 'Overview',
|
||||
path: 'guide/ngmodules',
|
||||
contentPath: 'guide/ngmodules/overview',
|
||||
},
|
||||
{
|
||||
label: 'JS Modules vs NgModules',
|
||||
path: 'guide/ngmodules/vs-jsmodule',
|
||||
contentPath: 'guide/ngmodules/vs-jsmodule',
|
||||
},
|
||||
{
|
||||
label: 'Launching your app with a root module',
|
||||
path: 'guide/ngmodules/bootstrapping',
|
||||
contentPath: 'guide/ngmodules/bootstrapping',
|
||||
},
|
||||
{
|
||||
label: 'Sharing NgModules',
|
||||
path: 'guide/ngmodules/sharing',
|
||||
contentPath: 'guide/ngmodules/sharing',
|
||||
},
|
||||
{
|
||||
label: 'Frequently used NgModules',
|
||||
path: 'guide/ngmodules/frequent',
|
||||
contentPath: 'guide/ngmodules/frequent',
|
||||
},
|
||||
{
|
||||
label: 'Feature modules',
|
||||
path: 'guide/ngmodules/feature-modules',
|
||||
contentPath: 'guide/ngmodules/feature-modules',
|
||||
},
|
||||
{
|
||||
label: 'Types of feature modules',
|
||||
path: 'guide/ngmodules/module-types',
|
||||
contentPath: 'guide/ngmodules/module-types',
|
||||
},
|
||||
{
|
||||
label: 'Providing dependencies',
|
||||
path: 'guide/ngmodules/providers',
|
||||
contentPath: 'guide/ngmodules/providers',
|
||||
},
|
||||
{
|
||||
label: 'Singleton services',
|
||||
path: 'guide/ngmodules/singleton-services',
|
||||
contentPath: 'guide/ngmodules/singleton-services',
|
||||
},
|
||||
{
|
||||
label: 'Lazy-loading feature modules',
|
||||
path: 'guide/ngmodules/lazy-loading',
|
||||
contentPath: 'guide/ngmodules/lazy-loading',
|
||||
},
|
||||
{
|
||||
label: 'NgModule API',
|
||||
path: 'guide/ngmodules/api',
|
||||
contentPath: 'guide/ngmodules/api',
|
||||
},
|
||||
{
|
||||
label: 'NgModule FAQs',
|
||||
path: 'guide/ngmodules/faq',
|
||||
contentPath: 'guide/ngmodules/faq',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Routing',
|
||||
children: [
|
||||
@@ -455,29 +435,64 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Server-side Rendering',
|
||||
path: 'guide/ssr',
|
||||
contentPath: 'guide/ssr',
|
||||
label: 'HTTP Client',
|
||||
children: [
|
||||
{
|
||||
label: 'Overview',
|
||||
path: 'guide/http',
|
||||
contentPath: 'guide/http/overview',
|
||||
},
|
||||
{
|
||||
label: 'Setting up HttpClient',
|
||||
path: 'guide/http/setup',
|
||||
contentPath: 'guide/http/setup',
|
||||
},
|
||||
{
|
||||
label: 'Making requests',
|
||||
path: 'guide/http/making-requests',
|
||||
contentPath: 'guide/http/making-requests',
|
||||
},
|
||||
{
|
||||
label: 'Intercepting requests and responses',
|
||||
path: 'guide/http/interceptors',
|
||||
contentPath: 'guide/http/interceptors',
|
||||
},
|
||||
{
|
||||
label: 'Testing',
|
||||
path: 'guide/http/testing',
|
||||
contentPath: 'guide/http/testing',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Build-time prerendering',
|
||||
path: 'guide/prerendering',
|
||||
contentPath: 'guide/prerendering',
|
||||
},
|
||||
{
|
||||
label: 'Hydration',
|
||||
path: 'guide/hydration',
|
||||
contentPath: 'guide/hydration',
|
||||
},
|
||||
{
|
||||
label: 'Deferrable views',
|
||||
path: 'guide/defer',
|
||||
contentPath: 'guide/defer',
|
||||
},
|
||||
{
|
||||
label: 'Image Optimization',
|
||||
path: 'guide/image-optimization',
|
||||
contentPath: 'guide/image-optimization',
|
||||
label: 'Performance',
|
||||
children: [
|
||||
{
|
||||
label: 'Deferrable views',
|
||||
path: 'guide/defer',
|
||||
contentPath: 'guide/defer',
|
||||
},
|
||||
{
|
||||
label: 'Image Optimization',
|
||||
path: 'guide/image-optimization',
|
||||
contentPath: 'guide/image-optimization',
|
||||
},
|
||||
{
|
||||
label: 'Server-side Rendering',
|
||||
path: 'guide/ssr',
|
||||
contentPath: 'guide/ssr',
|
||||
},
|
||||
{
|
||||
label: 'Build-time prerendering',
|
||||
path: 'guide/prerendering',
|
||||
contentPath: 'guide/prerendering',
|
||||
},
|
||||
{
|
||||
label: 'Hydration',
|
||||
path: 'guide/hydration',
|
||||
contentPath: 'guide/hydration',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Testing',
|
||||
@@ -589,81 +604,6 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Security',
|
||||
path: 'guide/security',
|
||||
contentPath: 'guide/security',
|
||||
},
|
||||
{
|
||||
label: 'Pipes',
|
||||
children: [
|
||||
{
|
||||
label: 'Overview',
|
||||
path: 'guide/pipes',
|
||||
contentPath: 'guide/pipes/overview',
|
||||
},
|
||||
{
|
||||
label: 'Using a pipe in a template',
|
||||
path: 'guide/pipes/template',
|
||||
contentPath: 'guide/pipes/template',
|
||||
},
|
||||
{
|
||||
label: 'Custom pipes',
|
||||
path: 'guide/pipes/transform-data',
|
||||
contentPath: 'guide/pipes/transform-data',
|
||||
},
|
||||
{
|
||||
label: 'Pipe precedence in expressions',
|
||||
path: 'guide/pipes/precedence',
|
||||
contentPath: 'guide/pipes/precedence',
|
||||
},
|
||||
{
|
||||
label: 'Change detection with pipes',
|
||||
path: 'guide/pipes/change-detection',
|
||||
contentPath: 'guide/pipes/change-detection',
|
||||
},
|
||||
{
|
||||
label: 'Unwrapping data from an observable',
|
||||
path: 'guide/pipes/unwrapping-data-observables',
|
||||
contentPath: 'guide/pipes/unwrapping-data-observables',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Http Client',
|
||||
children: [
|
||||
{
|
||||
label: 'Overview',
|
||||
path: 'guide/http',
|
||||
contentPath: 'guide/http/overview',
|
||||
},
|
||||
{
|
||||
label: 'Setting up HttpClient',
|
||||
path: 'guide/http/setup',
|
||||
contentPath: 'guide/http/setup',
|
||||
},
|
||||
{
|
||||
label: 'Making requests',
|
||||
path: 'guide/http/making-requests',
|
||||
contentPath: 'guide/http/making-requests',
|
||||
},
|
||||
{
|
||||
label: 'Intercepting requests and responses',
|
||||
path: 'guide/http/interceptors',
|
||||
contentPath: 'guide/http/interceptors',
|
||||
},
|
||||
{
|
||||
label: 'Security',
|
||||
path: 'guide/http/security',
|
||||
contentPath: 'guide/http/security',
|
||||
},
|
||||
{
|
||||
label: 'Testing',
|
||||
path: 'guide/http/testing',
|
||||
contentPath: 'guide/http/testing',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Animations',
|
||||
children: [
|
||||
@@ -694,11 +634,6 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Custom Elements',
|
||||
path: 'guide/elements',
|
||||
contentPath: 'guide/elements',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -830,9 +765,9 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
|
||||
contentPath: 'best-practices/style-guide',
|
||||
},
|
||||
{
|
||||
label: 'Keeping up-to-date',
|
||||
path: 'update',
|
||||
contentPath: 'best-practices/update',
|
||||
label: 'Security',
|
||||
path: 'best-practices/security',
|
||||
contentPath: 'guide/security', // Have not refactored due to build issues
|
||||
},
|
||||
{
|
||||
label: 'Accessibility',
|
||||
@@ -864,6 +799,11 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Keeping up-to-date',
|
||||
path: 'update',
|
||||
contentPath: 'best-practices/update',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -1349,6 +1289,81 @@ const REFERENCE_SUB_NAVIGATION_DATA: NavigationItem[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Concepts',
|
||||
children: [
|
||||
{
|
||||
label: 'Overview',
|
||||
path: 'reference/concepts',
|
||||
contentPath: 'reference/concepts/overview',
|
||||
},
|
||||
{
|
||||
label: 'NgModule',
|
||||
children: [
|
||||
{
|
||||
label: 'Overview',
|
||||
path: 'guide/ngmodules',
|
||||
contentPath: 'guide/ngmodules/overview',
|
||||
},
|
||||
{
|
||||
label: 'JS Modules vs NgModules',
|
||||
path: 'guide/ngmodules/vs-jsmodule',
|
||||
contentPath: 'guide/ngmodules/vs-jsmodule',
|
||||
},
|
||||
{
|
||||
label: 'Launching your app with a root module',
|
||||
path: 'guide/ngmodules/bootstrapping',
|
||||
contentPath: 'guide/ngmodules/bootstrapping',
|
||||
},
|
||||
{
|
||||
label: 'Sharing NgModules',
|
||||
path: 'guide/ngmodules/sharing',
|
||||
contentPath: 'guide/ngmodules/sharing',
|
||||
},
|
||||
{
|
||||
label: 'Frequently used NgModules',
|
||||
path: 'guide/ngmodules/frequent',
|
||||
contentPath: 'guide/ngmodules/frequent',
|
||||
},
|
||||
{
|
||||
label: 'Feature modules',
|
||||
path: 'guide/ngmodules/feature-modules',
|
||||
contentPath: 'guide/ngmodules/feature-modules',
|
||||
},
|
||||
{
|
||||
label: 'Types of feature modules',
|
||||
path: 'guide/ngmodules/module-types',
|
||||
contentPath: 'guide/ngmodules/module-types',
|
||||
},
|
||||
{
|
||||
label: 'Providing dependencies',
|
||||
path: 'guide/ngmodules/providers',
|
||||
contentPath: 'guide/ngmodules/providers',
|
||||
},
|
||||
{
|
||||
label: 'Singleton services',
|
||||
path: 'guide/ngmodules/singleton-services',
|
||||
contentPath: 'guide/ngmodules/singleton-services',
|
||||
},
|
||||
{
|
||||
label: 'Lazy-loading feature modules',
|
||||
path: 'guide/ngmodules/lazy-loading',
|
||||
contentPath: 'guide/ngmodules/lazy-loading',
|
||||
},
|
||||
{
|
||||
label: 'NgModule API',
|
||||
path: 'guide/ngmodules/api',
|
||||
contentPath: 'guide/ngmodules/api',
|
||||
},
|
||||
{
|
||||
label: 'NgModule FAQs',
|
||||
path: 'guide/ngmodules/faq',
|
||||
contentPath: 'guide/ngmodules/faq',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const FOOTER_NAVIGATION_DATA: NavigationItem[] = [
|
||||
|
||||
@@ -27,6 +27,7 @@ copy_to_directory(
|
||||
"//adev/src/content/introduction",
|
||||
"//adev/src/content/introduction/essentials",
|
||||
"//adev/src/content/reference",
|
||||
"//adev/src/content/reference/concepts",
|
||||
"//adev/src/content/reference/configs",
|
||||
"//adev/src/content/reference/errors",
|
||||
"//adev/src/content/reference/extended-diagnostics",
|
||||
|
||||
|
Before Width: | Height: | Size: 2.2 MiB After Width: | Height: | Size: 2.2 MiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 254 KiB After Width: | Height: | Size: 254 KiB |
|
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 724 KiB After Width: | Height: | Size: 724 KiB |
|
Before Width: | Height: | Size: 745 KiB After Width: | Height: | Size: 745 KiB |
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 1.4 MiB |
@@ -0,0 +1,148 @@
|
||||
# Signal inputs
|
||||
|
||||
Signal inputs allow values to be bound from parent components.
|
||||
Those values are exposed using a `Signal` and can change during the lifecycle of your component.
|
||||
|
||||
HELPFUL: Signal inputs are currently in [developer preview](/guide/releases#developer-preview).
|
||||
|
||||
Angular supports two variants of inputs:
|
||||
|
||||
**Optional inputs**
|
||||
Inputs are optional by default, unless you use `input.required`.
|
||||
You can specify an explicit initial value, or Angular will use `undefined` implicitly.
|
||||
|
||||
**Required inputs**
|
||||
Required inputs always have a value of the given input type.
|
||||
They are declared using the `input.required` function.
|
||||
|
||||
```typescript
|
||||
import {Component, input} from '@angular/core';
|
||||
|
||||
@Component({...})
|
||||
export class MyComp {
|
||||
// optional
|
||||
firstName = input<string>(); // InputSignal<string|undefined>
|
||||
age = input(0); // InputSignal<number>
|
||||
|
||||
// required
|
||||
lastName = input.required<string>(); // InputSignal<string>
|
||||
}
|
||||
```
|
||||
|
||||
An input is automatically recognized by Angular whenever you use the `input` or `input.required` functions as initializer of class members.
|
||||
|
||||
## Aliasing an input
|
||||
|
||||
Angular uses the class member name as the name of the input.
|
||||
You can alias inputs to change their public name to be different.
|
||||
|
||||
```typescript
|
||||
class StudentDirective {
|
||||
age = input(0, {alias: 'studentAge'});
|
||||
}
|
||||
```
|
||||
|
||||
This allows users to bind to your input using `[studentAge]`, while inside your component you can access the input values using `this.age`.
|
||||
|
||||
## Using in templates
|
||||
|
||||
Signal inputs are read-only signals.
|
||||
As with signals declared via `signal()`, you access the current value of the input by calling the input signal.
|
||||
|
||||
```html
|
||||
<p>First name: {{firstName()}}</p>
|
||||
<p>Last name: {{lastName()}}</p>
|
||||
```
|
||||
|
||||
This access to the value is captured in reactive contexts and can notify active consumers, like Angular itself, whenever the input value changes.
|
||||
|
||||
An input signal in practice is a trivial extension of signals that you know from [the signals guide](/guide/signals).
|
||||
|
||||
```typescript
|
||||
export class InputSignal<T> extends Signal<T> { ... }`.
|
||||
```
|
||||
|
||||
## Deriving values
|
||||
|
||||
As with signals, you can derive values from inputs using `computed`.
|
||||
|
||||
```typescript
|
||||
import {Component, input, computed} from '@angular/core';
|
||||
|
||||
@Component({...})
|
||||
export class MyComp {
|
||||
age = input(0);
|
||||
|
||||
// age multiplied by two.
|
||||
ageMultiplied = computed(() => this.age() * 2);
|
||||
}
|
||||
```
|
||||
|
||||
Computed signals memoize values.
|
||||
See more details in the [dedicated section for computed](/guide/signals#computed-signals).
|
||||
|
||||
## Monitoring changes
|
||||
|
||||
With signal inputs, users can leverage the `effect` function.
|
||||
The function will execute whenever the input changes.
|
||||
|
||||
Consider the following example.
|
||||
The new value is printed to the console whenever the `firstName` input changes.
|
||||
|
||||
```typescript
|
||||
import {input, effect} from '@angular/core';
|
||||
|
||||
class MyComp {
|
||||
firstName = input.required<string>();
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
console.log(this.firstName());
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `console.log` function is invoked every time the `firstName` input changes.
|
||||
This will happen as soon as `firstName` is available, and for subsequent changes during the lifetime of `MyComp`.
|
||||
|
||||
## Value transforms
|
||||
|
||||
You may want to coerce or parse input values without changing the meaning of the input.
|
||||
Transforms convert the raw value from parent templates to the expected input type.
|
||||
Transforms should be [pure functions](https://en.wikipedia.org/wiki/Pure_function).
|
||||
|
||||
```typescript
|
||||
class MyComp {
|
||||
disabled = input(false, {
|
||||
transform: (value: boolean|string) => typeof value === 'string' ? value === '' : value,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
In the example above, you are declaring an input named `disabled` that is accepting values of type `boolean` and `string`.
|
||||
This is captured by the explicit parameter type of `value` in the `transform` option.
|
||||
These values are then parsed to a `boolean` with the transform, resulting in booleans.
|
||||
|
||||
That way, you are only dealing with `boolean` inside your component when calling `this.disabled()`, while users of your component can pass an empty string as a shorthand to mark your component as disabled.
|
||||
|
||||
```html
|
||||
<my-custom-comp disabled>
|
||||
```
|
||||
|
||||
|
||||
IMPORTANT:Do not use transforms if they change the meaning of the input, or if they are [impure](https://en.wikipedia.org/wiki/Pure_function#Impure_functions).
|
||||
Instead, use `computed` for transformations with different meaning, or an `effect` for impure code that should run whenever the input changes.
|
||||
|
||||
## Why should we use signal inputs and not `@Input()`?
|
||||
|
||||
Signal inputs are a reactive alternative to decorator-based `@Input()`.
|
||||
|
||||
In comparison to decorator-based `@Input`, signal inputs provide numerous benefits:
|
||||
|
||||
1. Signal inputs are more **type safe**:
|
||||
<br/>• Required inputs do not require initial values, or tricks to tell TypeScript that an input _always_ has a value.
|
||||
<br/>• Transforms are automatically checked to match the accepted input values.
|
||||
2. Signal inputs, when used in templates, will **automatically** mark `OnPush` components as dirty.
|
||||
3. Values can be easily **derived** whenever an input changes using `computed`.
|
||||
4. Easier and more local monitoring of inputs using `effect` instead of `ngOnChanges` or setters.
|
||||
@@ -0,0 +1,138 @@
|
||||
# Model inputs
|
||||
|
||||
**Model inputs** are a special type of input that enable a component to propagate new values
|
||||
back to another component.
|
||||
|
||||
HELPFUL: Model inputs are currently in [developer preview](/guide/releases#developer-preview).
|
||||
|
||||
When creating a component, you can define a model input similarly to how you create a standard
|
||||
input.
|
||||
|
||||
```typescript
|
||||
import {Component, model, input} from '@angular/core';
|
||||
|
||||
@Component({...})
|
||||
export class CustomCheckbox {
|
||||
// This is a model input.
|
||||
checked = model(false);
|
||||
|
||||
// This is a standard input.
|
||||
disabled = input(false);
|
||||
}
|
||||
```
|
||||
|
||||
Both types of input allow someone to bind a value into the property. However, **model inputs allow
|
||||
the component author to write values into the property**.
|
||||
|
||||
In other respects, you can use model inputs the same way you use standard inputs. You can read the
|
||||
value by calling the signal function, including in reactive contexts like `computed` and `effect`.
|
||||
|
||||
```typescript
|
||||
import {Component, model, input} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'custom-checkbox',
|
||||
template: '<div (click)="toggle()"> ... </div>',
|
||||
})
|
||||
export class CustomCheckbox {
|
||||
checked = model(false);
|
||||
disabled = input(false);
|
||||
|
||||
toggle() {
|
||||
// While standard inputs are read-only, you can write directly to model inputs.
|
||||
this.checked.set(!this.checked());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When a component writes a new value into a model input, Angular can propagate the new value back
|
||||
to the component that is binding a value into that input. This is called **two-way binding** because
|
||||
values can flow in both directions.
|
||||
|
||||
## Two-way binding with signals
|
||||
|
||||
You can bind a writable signal to a model input.
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
...,
|
||||
// `checked` is a model input.
|
||||
// The parenthesis-inside-square-brackets syntax (aka "banana-in-a-box") creates a two-way binding
|
||||
template: '<custom-checkbox [(checked)]="isAdmin" />',
|
||||
})
|
||||
export class UserProfile {
|
||||
protected isAdmin = signal(false);
|
||||
}
|
||||
```
|
||||
|
||||
In the above example, the `CustomCheckbox` can write values into its `checked` model input, which
|
||||
then propagates those values back to the `isAdmin` signal in `UserProfile`. This binding keeps that
|
||||
values of `checked` and `isAdmin` in sync. Notice that the binding passes the `isAdmin` signal
|
||||
itself, not the _value_ of the signal.
|
||||
|
||||
## Two-way binding with plain properties
|
||||
|
||||
You can bind a plain JavaScript property to a model input.
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
...,
|
||||
// `checked` is a model input.
|
||||
// The parenthesis-inside-square-brackets syntax (aka "banana-in-a-box") creates a two-way binding
|
||||
template: '<custom-checkbox [(checked)]="isAdmin" />',
|
||||
})
|
||||
export class UserProfile {
|
||||
protected isAdmin = false;
|
||||
}
|
||||
```
|
||||
|
||||
In the example above, the `CustomCheckbox` can write values into its `checked` model input, which
|
||||
then propagates those values back to the `isAdmin` property in `UserProfile`. This binding keeps
|
||||
that values of `checked` and `isAdmin` in sync.
|
||||
|
||||
## Implicit `change` events
|
||||
|
||||
When you declare a model input in a component or directive, Angular automatically creates a
|
||||
corresponding [output](/guide/inputs-outputs) for that model. The output's name is the model
|
||||
input's name suffixed with "Change".
|
||||
|
||||
```typescript
|
||||
@Directive({...})
|
||||
export class CustomCheckbox {
|
||||
// This automatically creates an output named "checkedChange".
|
||||
// Can be subscribed to using `(checkedChange)="handler()"` in the template.
|
||||
checked = model(false);
|
||||
}
|
||||
```
|
||||
|
||||
Angular emits this change event whenever you write a new value into the model input by calling
|
||||
its `set` or `update` methods.
|
||||
|
||||
## Customizing model inputs
|
||||
|
||||
You can mark a model input as required or provide an alias in the same way as a
|
||||
[standard input](/guide/signal-inputs).
|
||||
|
||||
Model inputs do not support input transforms.
|
||||
|
||||
## Differences between `model()` and `input()`
|
||||
|
||||
Both `input()` and `model()` functions are ways to define signal-based inputs in Angular, but they
|
||||
differ in a few ways:
|
||||
1. `model()` defines **both** an input and an output. The output's name is always the name of the
|
||||
input suffixed with `Change` to support two-way bindings. It will be up to the consumer of your
|
||||
directive to decide if they want to use just the input, just the output, or both.
|
||||
2. `ModelSignal` is a `WritableSignal` which means that its value can be changed from anywhere
|
||||
using the `set` and `update` methods. When a new value is assigned, the `ModelSignal` will emit
|
||||
to its output. This is different from `InputSignal` which is read-only and can only be changed
|
||||
through the template.
|
||||
3. Model inputs do not support input transforms while signal inputs do.
|
||||
|
||||
## When to use model inputs
|
||||
|
||||
Use model inputs in components that exist to modify a value based on user interaction.
|
||||
Custom form controls, such as a date picker or combobox, should use model inputs for their
|
||||
primary value.
|
||||
|
||||
Avoid using model inputs as a convenience to avoid introducing an additional class property for
|
||||
containing local state.
|
||||
@@ -0,0 +1,9 @@
|
||||
load("@npm//@angular/docs:index.bzl", "generate_guides")
|
||||
|
||||
generate_guides(
|
||||
name = "concepts",
|
||||
srcs = glob([
|
||||
"*.md",
|
||||
]),
|
||||
visibility = ["//adev:__subpackages__"],
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
# Concepts
|
||||
|
||||
<docs-card-container>
|
||||
<docs-card title="NgModules" link="Learn more" href="guide/ngmodules">
|
||||
NgModules is a concept that commonly used in architecture v16 and earlier to help configure the injector and the compiler and help organize related things together.
|
||||
</docs-card>
|
||||
</docs-card-container>
|
||||
@@ -1,6 +1,6 @@
|
||||
# The MIT License
|
||||
|
||||
Copyright (c) 2010-2023 Google LLC. https://angular.dev/license
|
||||
Copyright (c) 2010-2024 Google LLC. https://angular.dev/license
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ Note: Look for alerts like this one, which call out steps that may only be for y
|
||||
The lessons in this tutorial create an Angular app that lists houses for rent and shows the details of individual houses.
|
||||
This app uses features that are common to many Angular apps.
|
||||
|
||||
<img alt="Output of heroes dashboard" src="assets/images/tutorials/first-app/homes-app-landing-page.png">
|
||||
<img alt="Output of homes landing page" src="assets/images/tutorials/first-app/homes-app-landing-page.png">
|
||||
|
||||
## Local development environment
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ In this activity you will learn to use constructor-based dependency injection.
|
||||
|
||||
<hr>
|
||||
|
||||
To inject a service or some other injectable resource into your componenet use the following syntax:
|
||||
To inject a service or some other injectable resource into your component use the following syntax:
|
||||
|
||||
<docs-code language="ts" highlight="[3]">
|
||||
@Component({...})
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
@description
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2010-2023 Google LLC. https://angular.io/license
|
||||
Copyright (c) 2010-2024 Google LLC. https://angular.io/license
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files \(the "Software"\), to deal
|
||||
|
||||
@@ -1336,5 +1336,11 @@
|
||||
"start": "2024-06-13"
|
||||
},
|
||||
"name": "JSNation"
|
||||
},
|
||||
{
|
||||
"date": {
|
||||
"start": "2024-06-27"
|
||||
},
|
||||
"name": "dotJS"
|
||||
}
|
||||
]
|
||||
@@ -17,7 +17,7 @@
|
||||
"uncompressed": {
|
||||
"runtime": 2734,
|
||||
"main": 231349,
|
||||
"polyfills": 33810,
|
||||
"polyfills": 34169,
|
||||
"src_app_lazy_lazy_routes_ts": 487
|
||||
}
|
||||
},
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"@angular/upgrade": "file:../../dist/packages-dist/upgrade",
|
||||
"@types/jasmine": "file:../../node_modules/@types/jasmine",
|
||||
"rxjs": "file:../../node_modules/rxjs",
|
||||
"typescript": "5.4.1-rc",
|
||||
"typescript": "5.4.2",
|
||||
"zone.js": "file:../../dist/zone.js-dist/archive/zone.js.tgz"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -796,10 +796,10 @@ tslib@^2.1.0, tslib@^2.3.0:
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
|
||||
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
|
||||
|
||||
typescript@5.4.1-rc:
|
||||
version "5.4.1-rc"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.1-rc.tgz#1ecdd897df1d9ef5bd1f844bad64691ecc23314d"
|
||||
integrity sha512-gInURzaO0bbfzfQAc3mfcHxh8qev+No4QOFUZHajo9vBgOLaljELJ3wuzyoGo/zHIzMSezdhtrsRdqL6E9SvNA==
|
||||
typescript@5.4.2:
|
||||
version "5.4.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.2.tgz#0ae9cebcfae970718474fe0da2c090cad6577372"
|
||||
integrity sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==
|
||||
|
||||
update-browserslist-db@^1.0.13:
|
||||
version "1.0.13"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "angular-srcs",
|
||||
"version": "17.3.0-next.1",
|
||||
"version": "17.3.0",
|
||||
"private": true,
|
||||
"description": "Angular - a web framework for modern web apps",
|
||||
"homepage": "https://github.com/angular/angular",
|
||||
@@ -141,7 +141,7 @@
|
||||
"todomvc-common": "^1.0.5",
|
||||
"tslib": "^2.3.0",
|
||||
"tslint": "6.1.3",
|
||||
"typescript": "5.4.1-rc",
|
||||
"typescript": "5.4.2",
|
||||
"webtreemap": "^2.0.1",
|
||||
"ws": "^8.15.0",
|
||||
"xhr2": "0.2.1",
|
||||
|
||||
@@ -100,7 +100,10 @@ export function transferCacheInterceptorFn(
|
||||
// POST requests are allowed either globally or at request level
|
||||
(requestMethod === 'POST' && !globalOptions.includePostRequests && !requestOptions) ||
|
||||
(requestMethod !== 'POST' && !ALLOWED_METHODS.includes(requestMethod)) ||
|
||||
requestOptions === false || //
|
||||
// Do not cache request that require authorization
|
||||
req.headers.has('authorization') ||
|
||||
req.headers.has('proxy-authorization') ||
|
||||
requestOptions === false ||
|
||||
globalOptions.filter?.(req) === false
|
||||
) {
|
||||
return next(req);
|
||||
|
||||
@@ -248,6 +248,22 @@ describe('TransferCache', () => {
|
||||
makeRequestAndExpectNone('/test-2?foo=1', 'POST', {transferCache: true});
|
||||
});
|
||||
|
||||
it('should not cache request that requires authorization', async () => {
|
||||
makeRequestAndExpectOne('/test-auth', 'foo', {
|
||||
headers: {Authorization: 'Basic YWxhZGRpbjpvcGVuc2VzYW1l'},
|
||||
});
|
||||
|
||||
makeRequestAndExpectOne('/test-auth', 'foo');
|
||||
});
|
||||
|
||||
it('should not cache request that requires proxy authorization', async () => {
|
||||
makeRequestAndExpectOne('/test-auth', 'foo', {
|
||||
headers: {'Proxy-Authorization': 'Basic YWxhZGRpbjpvcGVuc2VzYW1l'},
|
||||
});
|
||||
|
||||
makeRequestAndExpectOne('/test-auth', 'foo');
|
||||
});
|
||||
|
||||
describe('caching with global setting', () => {
|
||||
beforeEach(
|
||||
withBody('<test-app-http></test-app-http>', () => {
|
||||
|
||||
@@ -24,11 +24,10 @@ ts_library(
|
||||
"//packages/compiler-cli/src/ngtsc/perf",
|
||||
"//packages/compiler-cli/src/ngtsc/reflection",
|
||||
"//packages/compiler-cli/src/ngtsc/scope",
|
||||
#"//packages/compiler-cli/src/ngtsc/shims:api",
|
||||
"//packages/compiler-cli/src/ngtsc/transform",
|
||||
"//packages/compiler-cli/src/ngtsc/typecheck/api",
|
||||
#"//packages/compiler-cli/src/ngtsc/typecheck/diagnostics",
|
||||
"//packages/compiler-cli/src/ngtsc/typecheck/extended/api",
|
||||
"//packages/compiler-cli/src/ngtsc/typecheck/template_semantics/api",
|
||||
"//packages/compiler-cli/src/ngtsc/util",
|
||||
"//packages/compiler-cli/src/ngtsc/xi18n",
|
||||
"@npm//@types/node",
|
||||
|
||||
@@ -25,6 +25,7 @@ import {getDiagnosticNode, makeUnknownComponentDeferredImportDiagnostic} from '.
|
||||
import {AnalysisOutput, CompilationMode, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence, ResolveResult} from '../../../transform';
|
||||
import {TypeCheckableDirectiveMeta, TypeCheckContext} from '../../../typecheck/api';
|
||||
import {ExtendedTemplateChecker} from '../../../typecheck/extended/api';
|
||||
import {TemplateSemanticsChecker} from '../../../typecheck/template_semantics/api/api';
|
||||
import {getSourceFile} from '../../../util/src/typescript';
|
||||
import {Xi18nContext} from '../../../xi18n';
|
||||
import {combineResolvers, compileDeclareFactory, compileInputTransformFields, compileNgFactoryDefField, compileResults, extractClassDebugInfo, extractClassMetadata, extractSchemas, findAngularDecorator, forwardRefResolver, getDirectiveDiagnostics, getProviderDiagnostics, InjectableClassRegistry, isExpressionForwardReference, readBaseClass, ReferencesRegistry, removeIdentifierReferences, resolveEncapsulationEnumValueLocally, resolveEnumValue, resolveImportedFile, resolveLiteral, resolveProvidersRequiringFactory, ResourceLoader, toFactoryMetadata, tryUnwrapForwardRef, validateHostDirectives, wrapFunctionExpressionsInParens} from '../../common';
|
||||
@@ -673,6 +674,12 @@ export class ComponentDecoratorHandler implements
|
||||
return extendedTemplateChecker.getDiagnosticsForComponent(component);
|
||||
}
|
||||
|
||||
templateSemanticsCheck(
|
||||
component: ts.ClassDeclaration,
|
||||
templateSemanticsChecker: TemplateSemanticsChecker): ts.Diagnostic[] {
|
||||
return templateSemanticsChecker.getDiagnosticsForComponent(component);
|
||||
}
|
||||
|
||||
resolve(
|
||||
node: ClassDeclaration, analysis: Readonly<ComponentAnalysisData>,
|
||||
symbol: ComponentSymbol): ResolveResult<ComponentResolutionData> {
|
||||
|
||||
@@ -25,8 +25,8 @@ import {ClassMember, ReflectionHost} from '../../../reflection';
|
||||
|
||||
export interface InitializerApiFunction {
|
||||
owningModule: '@angular/core'|'@angular/core/rxjs-interop';
|
||||
functionName: ('input'|'model'|'ɵoutput'|'output'|'outputFromObservable'|'viewChild'|
|
||||
'viewChildren'|'contentChild'|'contentChildren');
|
||||
functionName: ('input'|'model'|'output'|'outputFromObservable'|'viewChild'|'viewChildren'|
|
||||
'contentChild'|'contentChildren');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -39,6 +39,8 @@ ts_library(
|
||||
"//packages/compiler-cli/src/ngtsc/typecheck/diagnostics",
|
||||
"//packages/compiler-cli/src/ngtsc/typecheck/extended",
|
||||
"//packages/compiler-cli/src/ngtsc/typecheck/extended/api",
|
||||
"//packages/compiler-cli/src/ngtsc/typecheck/template_semantics",
|
||||
"//packages/compiler-cli/src/ngtsc/typecheck/template_semantics/api",
|
||||
"//packages/compiler-cli/src/ngtsc/util",
|
||||
"//packages/compiler-cli/src/ngtsc/xi18n",
|
||||
"@npm//@types/semver",
|
||||
|
||||
@@ -33,6 +33,8 @@ import {TemplateTypeCheckerImpl} from '../../typecheck';
|
||||
import {OptimizeFor, TemplateTypeChecker, TypeCheckingConfig} from '../../typecheck/api';
|
||||
import {ALL_DIAGNOSTIC_FACTORIES, ExtendedTemplateCheckerImpl, SUPPORTED_DIAGNOSTIC_NAMES} from '../../typecheck/extended';
|
||||
import {ExtendedTemplateChecker} from '../../typecheck/extended/api';
|
||||
import {TemplateSemanticsChecker} from '../../typecheck/template_semantics/api/api';
|
||||
import {TemplateSemanticsCheckerImpl} from '../../typecheck/template_semantics/src/template_semantics_checker';
|
||||
import {getSourceFileOrNull, isDtsPath, toUnredirectedSourceFile} from '../../util/src/typescript';
|
||||
import {Xi18nContext} from '../../xi18n';
|
||||
import {DiagnosticCategoryLabel, NgCompilerAdapter, NgCompilerOptions} from '../api';
|
||||
@@ -59,6 +61,7 @@ interface LazyCompilationState {
|
||||
templateTypeChecker: TemplateTypeChecker;
|
||||
resourceRegistry: ResourceRegistry;
|
||||
extendedTemplateChecker: ExtendedTemplateChecker|null;
|
||||
templateSemanticsChecker: TemplateSemanticsChecker|null;
|
||||
|
||||
/**
|
||||
* Only available in local compilation mode when option `generateExtraImportsInLocalMode` is set.
|
||||
@@ -442,11 +445,7 @@ export class NgCompiler {
|
||||
// by running the extended template checking code, which will attempt to
|
||||
// generate the same TCB.
|
||||
try {
|
||||
diagnostics.push(...this.getTemplateDiagnostics());
|
||||
|
||||
if (this.options.strictTemplates) {
|
||||
diagnostics.push(...this.getExtendedTemplateDiagnostics());
|
||||
}
|
||||
diagnostics.push(...this.getTemplateDiagnostics(), ...this.runAdditionalChecks());
|
||||
} catch (err: unknown) {
|
||||
if (!isFatalDiagnosticError(err)) {
|
||||
throw err;
|
||||
@@ -472,11 +471,9 @@ export class NgCompiler {
|
||||
// by running the extended template checking code, which will attempt to
|
||||
// generate the same TCB.
|
||||
try {
|
||||
diagnostics.push(...this.getTemplateDiagnosticsForFile(file, optimizeFor));
|
||||
|
||||
if (this.options.strictTemplates) {
|
||||
diagnostics.push(...this.getExtendedTemplateDiagnostics(file));
|
||||
}
|
||||
diagnostics.push(
|
||||
...this.getTemplateDiagnosticsForFile(file, optimizeFor),
|
||||
...this.runAdditionalChecks(file));
|
||||
} catch (err: unknown) {
|
||||
if (!isFatalDiagnosticError(err)) {
|
||||
throw err;
|
||||
@@ -503,8 +500,12 @@ export class NgCompiler {
|
||||
try {
|
||||
diagnostics.push(...ttc.getDiagnosticsForComponent(component));
|
||||
|
||||
const extendedTemplateChecker = compilation.extendedTemplateChecker;
|
||||
if (this.options.strictTemplates && extendedTemplateChecker) {
|
||||
const {extendedTemplateChecker, templateSemanticsChecker} = compilation;
|
||||
|
||||
if (templateSemanticsChecker !== null) {
|
||||
diagnostics.push(...templateSemanticsChecker.getDiagnosticsForComponent(component));
|
||||
}
|
||||
if (this.options.strictTemplates && extendedTemplateChecker !== null) {
|
||||
diagnostics.push(...extendedTemplateChecker.getDiagnosticsForComponent(component));
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
@@ -968,26 +969,23 @@ export class NgCompiler {
|
||||
return this.nonTemplateDiagnostics;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the `extendedTemplateCheck` phase of the trait compiler
|
||||
* @param sf optional parameter to get diagnostics for a certain file
|
||||
* or all files in the program if `sf` is undefined
|
||||
* @returns generated extended template diagnostics
|
||||
*/
|
||||
private getExtendedTemplateDiagnostics(sf?: ts.SourceFile): ts.Diagnostic[] {
|
||||
private runAdditionalChecks(sf?: ts.SourceFile): ts.Diagnostic[] {
|
||||
const diagnostics: ts.Diagnostic[] = [];
|
||||
const compilation = this.ensureAnalyzed();
|
||||
const extendedTemplateChecker = compilation.extendedTemplateChecker;
|
||||
if (!extendedTemplateChecker) {
|
||||
return [];
|
||||
}
|
||||
const {extendedTemplateChecker, templateSemanticsChecker} = compilation;
|
||||
const files = sf ? [sf] : this.inputProgram.getSourceFiles();
|
||||
|
||||
if (sf !== undefined) {
|
||||
return compilation.traitCompiler.extendedTemplateCheck(sf, extendedTemplateChecker);
|
||||
}
|
||||
for (const sf of this.inputProgram.getSourceFiles()) {
|
||||
diagnostics.push(
|
||||
...compilation.traitCompiler.extendedTemplateCheck(sf, extendedTemplateChecker));
|
||||
for (const sf of files) {
|
||||
if (templateSemanticsChecker !== null) {
|
||||
diagnostics.push(...compilation.traitCompiler.runAdditionalChecks(sf, (clazz, handler) => {
|
||||
return handler.templateSemanticsCheck?.(clazz, templateSemanticsChecker) || null;
|
||||
}));
|
||||
}
|
||||
if (this.options.strictTemplates && extendedTemplateChecker !== null) {
|
||||
diagnostics.push(...compilation.traitCompiler.runAdditionalChecks(sf, (clazz, handler) => {
|
||||
return handler.extendedTemplateCheck?.(clazz, extendedTemplateChecker) || null;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return diagnostics;
|
||||
@@ -1233,6 +1231,10 @@ export class NgCompiler {
|
||||
templateTypeChecker, checker, ALL_DIAGNOSTIC_FACTORIES, this.options) :
|
||||
null;
|
||||
|
||||
const templateSemanticsChecker = this.constructionDiagnostics.length === 0 ?
|
||||
new TemplateSemanticsCheckerImpl(templateTypeChecker) :
|
||||
null;
|
||||
|
||||
return {
|
||||
isCore,
|
||||
traitCompiler,
|
||||
@@ -1248,6 +1250,7 @@ export class NgCompiler {
|
||||
resourceRegistry,
|
||||
extendedTemplateChecker,
|
||||
localCompilationExtraImportsTracker,
|
||||
templateSemanticsChecker,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ ts_library(
|
||||
"//packages/compiler-cli/src/ngtsc/translator",
|
||||
"//packages/compiler-cli/src/ngtsc/typecheck/api",
|
||||
"//packages/compiler-cli/src/ngtsc/typecheck/extended/api",
|
||||
"//packages/compiler-cli/src/ngtsc/typecheck/template_semantics/api",
|
||||
"//packages/compiler-cli/src/ngtsc/util",
|
||||
"//packages/compiler-cli/src/ngtsc/xi18n",
|
||||
"@npm//typescript",
|
||||
|
||||
@@ -16,6 +16,7 @@ import {ClassDeclaration, Decorator, ReflectionHost} from '../../reflection';
|
||||
import {ImportManager} from '../../translator';
|
||||
import {TypeCheckContext} from '../../typecheck/api';
|
||||
import {ExtendedTemplateChecker} from '../../typecheck/extended/api';
|
||||
import {TemplateSemanticsChecker} from '../../typecheck/template_semantics/api/api';
|
||||
import {Xi18nContext} from '../../xi18n';
|
||||
|
||||
/**
|
||||
@@ -175,6 +176,10 @@ export interface DecoratorHandler<D, A, S extends SemanticSymbol|null, R> {
|
||||
(component: ts.ClassDeclaration, extendedTemplateChecker: ExtendedTemplateChecker):
|
||||
ts.Diagnostic[];
|
||||
|
||||
templateSemanticsCheck?
|
||||
(component: ts.ClassDeclaration, templateSemanticsChecker: TemplateSemanticsChecker):
|
||||
ts.Diagnostic[];
|
||||
|
||||
/**
|
||||
* Generate a description of the field which should be added to the class, including any
|
||||
* initialization code to be generated.
|
||||
|
||||
@@ -17,7 +17,6 @@ import {IndexingContext} from '../../indexer';
|
||||
import {PerfEvent, PerfRecorder} from '../../perf';
|
||||
import {ClassDeclaration, DeclarationNode, Decorator, isNamedClassDeclaration, ReflectionHost} from '../../reflection';
|
||||
import {ProgramTypeCheckAdapter, TypeCheckContext} from '../../typecheck/api';
|
||||
import {ExtendedTemplateChecker} from '../../typecheck/extended/api';
|
||||
import {getSourceFile} from '../../util/src/typescript';
|
||||
import {Xi18nContext} from '../../xi18n';
|
||||
|
||||
@@ -522,8 +521,12 @@ export class TraitCompiler implements ProgramTypeCheckAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
extendedTemplateCheck(sf: ts.SourceFile, extendedTemplateChecker: ExtendedTemplateChecker):
|
||||
ts.Diagnostic[] {
|
||||
runAdditionalChecks(
|
||||
sf: ts.SourceFile,
|
||||
check:
|
||||
(clazz: ts.ClassDeclaration,
|
||||
handler: DecoratorHandler<unknown, unknown, SemanticSymbol|null, unknown>) =>
|
||||
ts.Diagnostic[] | null): ts.Diagnostic[] {
|
||||
if (this.compilationMode === CompilationMode.LOCAL) {
|
||||
return [];
|
||||
}
|
||||
@@ -539,10 +542,10 @@ export class TraitCompiler implements ProgramTypeCheckAdapter {
|
||||
}
|
||||
const record = this.classes.get(clazz)!;
|
||||
for (const trait of record.traits) {
|
||||
if (trait.handler.extendedTemplateCheck === undefined) {
|
||||
continue;
|
||||
const result = check(clazz, trait.handler);
|
||||
if (result !== null) {
|
||||
diagnostics.push(...result);
|
||||
}
|
||||
diagnostics.push(...trait.handler.extendedTemplateCheck(clazz, extendedTemplateChecker));
|
||||
}
|
||||
}
|
||||
return diagnostics;
|
||||
|
||||
@@ -15,7 +15,6 @@ import {NOOP_PERF_RECORDER} from '../../perf';
|
||||
import {ClassDeclaration, Decorator, isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflection';
|
||||
import {getDeclaration, makeProgram} from '../../testing';
|
||||
import {CompilationMode, DetectResult, DtsTransformRegistry, TraitCompiler} from '../../transform';
|
||||
import {ExtendedTemplateChecker} from '../../typecheck/extended/api';
|
||||
import {AnalysisOutput, CompileResult, DecoratorHandler, HandlerPrecedence, ResolveResult} from '../src/api';
|
||||
|
||||
const fakeSfTypeIdentifier = {
|
||||
@@ -309,10 +308,6 @@ runInEachFileSystem(() => {
|
||||
|
||||
register(): void {}
|
||||
|
||||
extendedTemplateCheck() {
|
||||
return [];
|
||||
}
|
||||
|
||||
updateResources() {}
|
||||
|
||||
symbol(): null {
|
||||
@@ -368,21 +363,6 @@ runInEachFileSystem(() => {
|
||||
expect(handler.register).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not call extendedTemplateCheck', () => {
|
||||
const contents = `
|
||||
export class Test {}
|
||||
`;
|
||||
const handler = new TestDecoratorHandler();
|
||||
spyOn(handler, 'extendedTemplateCheck');
|
||||
const {compiler, sourceFile} = setup(contents, [handler], CompilationMode.LOCAL);
|
||||
|
||||
compiler.analyzeSync(sourceFile);
|
||||
compiler.resolve();
|
||||
compiler.extendedTemplateCheck(sourceFile, {} as ExtendedTemplateChecker);
|
||||
|
||||
expect(handler.extendedTemplateCheck).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not call updateResources', () => {
|
||||
const contents = `
|
||||
export class Test {}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AST, LiteralPrimitive, ParseSourceSpan, PropertyRead, SafePropertyRead, TmplAstElement, TmplAstNode, TmplAstTemplate, TmplAstTextAttribute} from '@angular/compiler';
|
||||
import {AST, LiteralPrimitive, ParseSourceSpan, PropertyRead, SafePropertyRead, TmplAstElement, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstTextAttribute, TmplAstVariable} from '@angular/compiler';
|
||||
import ts from 'typescript';
|
||||
|
||||
import {AbsoluteFsPath} from '../../../../src/ngtsc/file_system';
|
||||
@@ -213,6 +213,13 @@ export interface TemplateTypeChecker {
|
||||
*/
|
||||
invalidateClass(clazz: ts.ClassDeclaration): void;
|
||||
|
||||
/**
|
||||
* Gets the target of a template expression, if possible.
|
||||
* See `BoundTarget.getExpressionTarget` for more information.
|
||||
*/
|
||||
getExpressionTarget(expression: AST, clazz: ts.ClassDeclaration): TmplAstReference|TmplAstVariable
|
||||
|null;
|
||||
|
||||
/**
|
||||
* Constructs a `ts.Diagnostic` for a given `ParseSourceSpan` within a template.
|
||||
*/
|
||||
|
||||
@@ -10,6 +10,7 @@ ts_library(
|
||||
deps = [
|
||||
"//packages/compiler",
|
||||
"//packages/compiler-cli/src/ngtsc/diagnostics",
|
||||
"//packages/compiler-cli/src/ngtsc/typecheck",
|
||||
"//packages/compiler-cli/src/ngtsc/typecheck/api",
|
||||
"//packages/compiler-cli/src/ngtsc/typecheck/extended/api",
|
||||
"@npm//typescript",
|
||||
|
||||
@@ -11,16 +11,9 @@ import ts from 'typescript';
|
||||
|
||||
import {ErrorCode, ExtendedTemplateDiagnosticName} from '../../../../diagnostics';
|
||||
import {NgTemplateDiagnostic, SymbolKind} from '../../../api';
|
||||
import {isSignalReference} from '../../../src/symbol_util';
|
||||
import {TemplateCheckFactory, TemplateCheckWithVisitor, TemplateContext} from '../../api';
|
||||
|
||||
/** Names of known signal functions. */
|
||||
const SIGNAL_FNS = new Set([
|
||||
'WritableSignal',
|
||||
'Signal',
|
||||
'InputSignal',
|
||||
'InputSignalWithTransform',
|
||||
'ModelSignal',
|
||||
]);
|
||||
|
||||
/** Names of known signal instance properties. */
|
||||
const SIGNAL_INSTANCE_PROPERTIES = new Set(['set', 'update', 'asReadonly']);
|
||||
@@ -50,18 +43,6 @@ class InterpolatedSignalCheck extends
|
||||
}
|
||||
}
|
||||
|
||||
function isSignal(symbol: ts.Symbol|undefined): boolean {
|
||||
const declarations = symbol?.getDeclarations();
|
||||
|
||||
return declarations !== undefined && declarations.some(decl => {
|
||||
const fileName = decl.getSourceFile().fileName;
|
||||
|
||||
return (ts.isInterfaceDeclaration(decl) || ts.isTypeAliasDeclaration(decl)) &&
|
||||
SIGNAL_FNS.has(decl.name.text) &&
|
||||
(fileName.includes('@angular/core') || fileName.includes('angular2/rc/packages/core'));
|
||||
});
|
||||
}
|
||||
|
||||
function isFunctionInstanceProperty(name: string): boolean {
|
||||
return FUNCTION_INSTANCE_PROPERTIES.has(name);
|
||||
}
|
||||
@@ -76,8 +57,7 @@ function buildDiagnosticForSignal(
|
||||
Array<NgTemplateDiagnostic<ErrorCode.INTERPOLATED_SIGNAL_NOT_INVOKED>> {
|
||||
// check for `{{ mySignal }}`
|
||||
const symbol = ctx.templateTypeChecker.getSymbolOfNode(node, component);
|
||||
if (symbol?.kind === SymbolKind.Expression &&
|
||||
(isSignal(symbol.tsType.symbol) || isSignal(symbol.tsType.aliasSymbol))) {
|
||||
if (symbol !== null && symbol.kind === SymbolKind.Expression && isSignalReference(symbol)) {
|
||||
const templateMapping =
|
||||
ctx.templateTypeChecker.getTemplateMappingAtTcbLocation(symbol.tcbLocation)!;
|
||||
const errorString = `${node.name} is a function and should be invoked: ${node.name}()`;
|
||||
@@ -92,8 +72,8 @@ function buildDiagnosticForSignal(
|
||||
// `{{ mySignal.asReadonly }}` as these are the names of instance properties of Signal
|
||||
const symbolOfReceiver = ctx.templateTypeChecker.getSymbolOfNode(node.receiver, component);
|
||||
if ((isFunctionInstanceProperty(node.name) || isSignalInstanceProperty(node.name)) &&
|
||||
symbolOfReceiver?.kind === SymbolKind.Expression &&
|
||||
(isSignal(symbolOfReceiver.tsType.symbol) || isSignal(symbolOfReceiver.tsType.aliasSymbol))) {
|
||||
symbolOfReceiver !== null && symbolOfReceiver.kind === SymbolKind.Expression &&
|
||||
isSignalReference(symbolOfReceiver)) {
|
||||
const templateMapping =
|
||||
ctx.templateTypeChecker.getTemplateMappingAtTcbLocation(symbolOfReceiver.tcbLocation)!;
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AST, CssSelector, DomElementSchemaRegistry, ExternalExpr, LiteralPrimitive, ParseSourceSpan, PropertyRead, SafePropertyRead, TmplAstElement, TmplAstNode, TmplAstTemplate, TmplAstTextAttribute, WrappedNodeExpr} from '@angular/compiler';
|
||||
import {AST, CssSelector, DomElementSchemaRegistry, ExternalExpr, LiteralPrimitive, ParseSourceSpan, PropertyRead, SafePropertyRead, TmplAstElement, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstTextAttribute, TmplAstVariable, WrappedNodeExpr} from '@angular/compiler';
|
||||
import ts from 'typescript';
|
||||
|
||||
import {ErrorCode, ngErrorCode} from '../../diagnostics';
|
||||
@@ -345,6 +345,12 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
|
||||
this.isComplete = false;
|
||||
}
|
||||
|
||||
getExpressionTarget(expression: AST, clazz: ts.ClassDeclaration): TmplAstReference|TmplAstVariable
|
||||
|null {
|
||||
return this.getLatestComponentState(clazz).data?.boundTarget.getExpressionTarget(expression) ||
|
||||
null;
|
||||
}
|
||||
|
||||
makeTemplateDiagnostic<T extends ErrorCode>(
|
||||
clazz: ts.ClassDeclaration, sourceSpan: ParseSourceSpan, category: ts.DiagnosticCategory,
|
||||
errorCode: T, message: string, relatedInformation?: {
|
||||
|
||||
@@ -70,9 +70,6 @@ export interface OutOfBandDiagnosticRecorder {
|
||||
*/
|
||||
deferredComponentUsedEagerly(templateId: TemplateId, element: TmplAstElement): void;
|
||||
|
||||
illegalAssignmentToTemplateVar(
|
||||
templateId: TemplateId, assignment: PropertyWrite, target: TmplAstVariable): void;
|
||||
|
||||
/**
|
||||
* Reports a duplicate declaration of a template variable.
|
||||
*
|
||||
@@ -217,27 +214,6 @@ export class OutOfBandDiagnosticRecorderImpl implements OutOfBandDiagnosticRecor
|
||||
ngErrorCode(ErrorCode.DEFERRED_DIRECTIVE_USED_EAGERLY), errorMsg));
|
||||
}
|
||||
|
||||
illegalAssignmentToTemplateVar(
|
||||
templateId: TemplateId, assignment: PropertyWrite, target: TmplAstVariable): void {
|
||||
const mapping = this.resolver.getSourceMapping(templateId);
|
||||
const errorMsg = `Cannot use variable '${
|
||||
assignment
|
||||
.name}' as the left-hand side of an assignment expression. Template variables are read-only.`;
|
||||
|
||||
const sourceSpan = this.resolver.toParseSourceSpan(templateId, assignment.sourceSpan);
|
||||
if (sourceSpan === null) {
|
||||
throw new Error(`Assertion failure: no SourceLocation found for property binding.`);
|
||||
}
|
||||
this._diagnostics.push(makeTemplateDiagnostic(
|
||||
templateId, mapping, sourceSpan, ts.DiagnosticCategory.Error,
|
||||
ngErrorCode(ErrorCode.WRITE_TO_READ_ONLY_VARIABLE), errorMsg, [{
|
||||
text: `The variable ${assignment.name} is declared here.`,
|
||||
start: target.valueSpan?.start.offset || target.sourceSpan.start.offset,
|
||||
end: target.valueSpan?.end.offset || target.sourceSpan.end.offset,
|
||||
sourceFile: mapping.node.getSourceFile(),
|
||||
}]));
|
||||
}
|
||||
|
||||
duplicateTemplateVar(
|
||||
templateId: TemplateId, variable: TmplAstVariable, firstDecl: TmplAstVariable): void {
|
||||
const mapping = this.resolver.getSourceMapping(templateId);
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import ts from 'typescript';
|
||||
|
||||
import {Symbol, SymbolKind} from '../api';
|
||||
|
||||
/** Names of known signal functions. */
|
||||
const SIGNAL_FNS = new Set([
|
||||
'WritableSignal',
|
||||
'Signal',
|
||||
'InputSignal',
|
||||
'InputSignalWithTransform',
|
||||
'ModelSignal',
|
||||
]);
|
||||
|
||||
/** Returns whether a symbol is a reference to a signal. */
|
||||
export function isSignalReference(symbol: Symbol): boolean {
|
||||
return (symbol.kind === SymbolKind.Expression || symbol.kind === SymbolKind.Variable) &&
|
||||
// Note that `tsType.symbol` isn't optional in the typings,
|
||||
// but it appears that it can be undefined at runtime.
|
||||
(symbol.tsType.symbol !== undefined && isSignalSymbol(symbol.tsType.symbol) ||
|
||||
(symbol.tsType.aliasSymbol !== undefined && isSignalSymbol(symbol.tsType.aliasSymbol)));
|
||||
}
|
||||
|
||||
/** Checks whether a symbol points to a signal. */
|
||||
function isSignalSymbol(symbol: ts.Symbol): boolean {
|
||||
const declarations = symbol.getDeclarations();
|
||||
|
||||
return declarations !== undefined && declarations.some(decl => {
|
||||
const fileName = decl.getSourceFile().fileName;
|
||||
|
||||
return (ts.isInterfaceDeclaration(decl) || ts.isTypeAliasDeclaration(decl)) &&
|
||||
SIGNAL_FNS.has(decl.name.text) &&
|
||||
(fileName.includes('@angular/core') || fileName.includes('angular2/rc/packages/core'));
|
||||
});
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AST, BoundTarget, ImplicitReceiver, PropertyWrite, RecursiveAstVisitor, TmplAstVariable} from '@angular/compiler';
|
||||
|
||||
import {TemplateId} from '../api';
|
||||
|
||||
import {OutOfBandDiagnosticRecorder} from './oob';
|
||||
|
||||
/**
|
||||
* Visits a template and records any semantic errors within its expressions.
|
||||
*/
|
||||
export class ExpressionSemanticVisitor extends RecursiveAstVisitor {
|
||||
constructor(
|
||||
private templateId: TemplateId, private boundTarget: BoundTarget<any>,
|
||||
private oob: OutOfBandDiagnosticRecorder) {
|
||||
super();
|
||||
}
|
||||
|
||||
override visitPropertyWrite(ast: PropertyWrite, context: any): void {
|
||||
super.visitPropertyWrite(ast, context);
|
||||
|
||||
if (!(ast.receiver instanceof ImplicitReceiver)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = this.boundTarget.getExpressionTarget(ast);
|
||||
if (target instanceof TmplAstVariable) {
|
||||
// Template variables are read-only.
|
||||
this.oob.illegalAssignmentToTemplateVar(this.templateId, ast, target);
|
||||
}
|
||||
}
|
||||
|
||||
static visit(
|
||||
ast: AST, id: TemplateId, boundTarget: BoundTarget<any>,
|
||||
oob: OutOfBandDiagnosticRecorder): void {
|
||||
ast.visit(new ExpressionSemanticVisitor(id, boundTarget, oob));
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,6 @@ import {DomSchemaChecker} from './dom';
|
||||
import {Environment} from './environment';
|
||||
import {astToTypescript, NULL_AS_ANY} from './expression';
|
||||
import {OutOfBandDiagnosticRecorder} from './oob';
|
||||
import {ExpressionSemanticVisitor} from './template_semantics';
|
||||
import {tsCallMethod, tsCastToAny, tsCreateElement, tsCreateTypeQueryForCoercedInput, tsCreateVariable, tsDeclareVariable} from './ts_util';
|
||||
import {requiresInlineTypeCtor} from './type_constructor';
|
||||
import {TypeParameterEmitter} from './type_parameter_emitter';
|
||||
@@ -1214,9 +1213,6 @@ export class TcbDirectiveOutputsOp extends TcbOp {
|
||||
const handler = tcbCreateEventHandler(output, this.tcb, this.scope, EventParamType.Any);
|
||||
this.scope.addStatement(ts.factory.createExpressionStatement(handler));
|
||||
}
|
||||
|
||||
ExpressionSemanticVisitor.visit(
|
||||
output.handler, this.tcb.id, this.tcb.boundTarget, this.tcb.oobRecorder);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -1292,9 +1288,6 @@ class TcbUnclaimedOutputsOp extends TcbOp {
|
||||
const handler = tcbCreateEventHandler(output, this.tcb, this.scope, EventParamType.Any);
|
||||
this.scope.addStatement(ts.factory.createExpressionStatement(handler));
|
||||
}
|
||||
|
||||
ExpressionSemanticVisitor.visit(
|
||||
output.handler, this.tcb.id, this.tcb.boundTarget, this.tcb.oobRecorder);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
load("//tools:defaults.bzl", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "template_semantics",
|
||||
srcs = glob(
|
||||
["**/*.ts"],
|
||||
),
|
||||
visibility = ["//packages/compiler-cli/src/ngtsc:__subpackages__"],
|
||||
deps = [
|
||||
"//packages/compiler",
|
||||
"//packages/compiler-cli/src/ngtsc/core:api",
|
||||
"//packages/compiler-cli/src/ngtsc/diagnostics",
|
||||
"//packages/compiler-cli/src/ngtsc/typecheck",
|
||||
"//packages/compiler-cli/src/ngtsc/typecheck/api",
|
||||
"//packages/compiler-cli/src/ngtsc/typecheck/template_semantics/api",
|
||||
"@npm//typescript",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,16 @@
|
||||
load("//tools:defaults.bzl", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "api",
|
||||
srcs = glob(
|
||||
["**/*.ts"],
|
||||
),
|
||||
visibility = ["//packages/compiler-cli/src/ngtsc:__subpackages__"],
|
||||
deps = [
|
||||
"//packages/compiler",
|
||||
"//packages/compiler-cli/src/ngtsc/core:api",
|
||||
"//packages/compiler-cli/src/ngtsc/diagnostics",
|
||||
"//packages/compiler-cli/src/ngtsc/typecheck/api",
|
||||
"@npm//typescript",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import ts from 'typescript';
|
||||
|
||||
import {TemplateDiagnostic} from '../../api';
|
||||
|
||||
/**
|
||||
* Interface to generate diagnostics related to the semantics of a component's template.
|
||||
*/
|
||||
export interface TemplateSemanticsChecker {
|
||||
/**
|
||||
* Run `TemplateSemanticsChecker`s for a component and return the generated `ts.Diagnostic`s.
|
||||
*/
|
||||
getDiagnosticsForComponent(component: ts.ClassDeclaration): TemplateDiagnostic[];
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AST, ASTWithSource, ImplicitReceiver, ParsedEventType, PropertyRead, PropertyWrite, RecursiveAstVisitor, TmplAstBoundEvent, TmplAstNode, TmplAstRecursiveVisitor, TmplAstVariable} from '@angular/compiler';
|
||||
import ts from 'typescript';
|
||||
|
||||
import {ErrorCode, ngErrorCode} from '../../../diagnostics';
|
||||
import {TemplateDiagnostic, TemplateTypeChecker} from '../../api';
|
||||
import {isSignalReference} from '../../src/symbol_util';
|
||||
import {TemplateSemanticsChecker} from '../api/api';
|
||||
|
||||
export class TemplateSemanticsCheckerImpl implements TemplateSemanticsChecker {
|
||||
constructor(private templateTypeChecker: TemplateTypeChecker) {}
|
||||
|
||||
getDiagnosticsForComponent(component: ts.ClassDeclaration): TemplateDiagnostic[] {
|
||||
const template = this.templateTypeChecker.getTemplate(component);
|
||||
return template !== null ?
|
||||
TemplateSemanticsVisitor.visit(template, component, this.templateTypeChecker) :
|
||||
[];
|
||||
}
|
||||
}
|
||||
|
||||
/** Visitor that verifies the semantics of a template. */
|
||||
class TemplateSemanticsVisitor extends TmplAstRecursiveVisitor {
|
||||
private constructor(private expressionVisitor: ExpressionsSemanticsVisitor) {
|
||||
super();
|
||||
}
|
||||
|
||||
static visit(
|
||||
nodes: TmplAstNode[], component: ts.ClassDeclaration,
|
||||
templateTypeChecker: TemplateTypeChecker) {
|
||||
const diagnostics: TemplateDiagnostic[] = [];
|
||||
const expressionVisitor =
|
||||
new ExpressionsSemanticsVisitor(templateTypeChecker, component, diagnostics);
|
||||
const templateVisitor = new TemplateSemanticsVisitor(expressionVisitor);
|
||||
nodes.forEach(node => node.visit(templateVisitor));
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
override visitBoundEvent(event: TmplAstBoundEvent): void {
|
||||
super.visitBoundEvent(event);
|
||||
event.handler.visit(this.expressionVisitor, event);
|
||||
}
|
||||
}
|
||||
|
||||
/** Visitor that verifies the semantics of the expressions within a template. */
|
||||
class ExpressionsSemanticsVisitor extends RecursiveAstVisitor {
|
||||
constructor(
|
||||
private templateTypeChecker: TemplateTypeChecker, private component: ts.ClassDeclaration,
|
||||
private diagnostics: TemplateDiagnostic[]) {
|
||||
super();
|
||||
}
|
||||
|
||||
override visitPropertyWrite(ast: PropertyWrite, context: TmplAstNode): void {
|
||||
super.visitPropertyWrite(ast, context);
|
||||
this.checkForIllegalWriteInEventBinding(ast, context);
|
||||
}
|
||||
|
||||
override visitPropertyRead(ast: PropertyRead, context: TmplAstNode) {
|
||||
super.visitPropertyRead(ast, context);
|
||||
this.checkForIllegalWriteInTwoWayBinding(ast, context);
|
||||
}
|
||||
|
||||
private checkForIllegalWriteInEventBinding(ast: PropertyWrite, context: TmplAstNode) {
|
||||
if (!(context instanceof TmplAstBoundEvent) || !(ast.receiver instanceof ImplicitReceiver)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = this.templateTypeChecker.getExpressionTarget(ast, this.component);
|
||||
if (target instanceof TmplAstVariable) {
|
||||
const errorMessage = `Cannot use variable '${
|
||||
target
|
||||
.name}' as the left-hand side of an assignment expression. Template variables are read-only.`;
|
||||
this.diagnostics.push(this.makeIllegalTemplateVarDiagnostic(target, context, errorMessage));
|
||||
}
|
||||
}
|
||||
|
||||
private checkForIllegalWriteInTwoWayBinding(ast: PropertyRead, context: TmplAstNode) {
|
||||
// Only check top-level property reads inside two-way bindings for illegal assignments.
|
||||
if (!(context instanceof TmplAstBoundEvent) || context.type !== ParsedEventType.TwoWay ||
|
||||
!(ast.receiver instanceof ImplicitReceiver) ||
|
||||
ast !== unwrapAstWithSource(context.handler)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = this.templateTypeChecker.getExpressionTarget(ast, this.component);
|
||||
if (!(target instanceof TmplAstVariable)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Two-way bindings to template variables are only allowed if the variables are signals.
|
||||
const symbol = this.templateTypeChecker.getSymbolOfNode(target, this.component);
|
||||
if (symbol !== null && !isSignalReference(symbol)) {
|
||||
const errorMessage = `Cannot use a non-signal variable '${
|
||||
target.name}' in a two-way binding expression. Template variables are read-only.`;
|
||||
this.diagnostics.push(this.makeIllegalTemplateVarDiagnostic(target, context, errorMessage));
|
||||
}
|
||||
}
|
||||
|
||||
private makeIllegalTemplateVarDiagnostic(
|
||||
target: TmplAstVariable, expressionNode: TmplAstBoundEvent,
|
||||
errorMessage: string): TemplateDiagnostic {
|
||||
return this.templateTypeChecker.makeTemplateDiagnostic(
|
||||
this.component, expressionNode.handlerSpan, ts.DiagnosticCategory.Error,
|
||||
ngErrorCode(ErrorCode.WRITE_TO_READ_ONLY_VARIABLE), errorMessage, [{
|
||||
text: `The variable ${target.name} is declared here.`,
|
||||
start: target.valueSpan?.start.offset || target.sourceSpan.start.offset,
|
||||
end: target.valueSpan?.end.offset || target.sourceSpan.end.offset,
|
||||
sourceFile: this.component.getSourceFile(),
|
||||
}]);
|
||||
}
|
||||
}
|
||||
|
||||
function unwrapAstWithSource(ast: AST): AST {
|
||||
return ast instanceof ASTWithSource ? ast.ast : ast;
|
||||
}
|
||||
@@ -859,7 +859,6 @@ export class NoopOobRecorder implements OutOfBandDiagnosticRecorder {
|
||||
missingPipe(): void {}
|
||||
deferredPipeUsedEagerly(templateId: TemplateId, ast: BindingPipe): void {}
|
||||
deferredComponentUsedEagerly(templateId: TemplateId, element: TmplAstElement): void {}
|
||||
illegalAssignmentToTemplateVar(): void {}
|
||||
duplicateTemplateVar(): void {}
|
||||
requiresInlineTcb(): void {}
|
||||
requiresInlineTypeConstructors(): void {}
|
||||
|
||||
@@ -917,3 +917,61 @@ export declare class App {
|
||||
static ɵcmp: i0.ɵɵComponentDeclaration<App, "ng-component", never, {}, {}, never, never, true, never>;
|
||||
}
|
||||
|
||||
/****************************************************************************************************
|
||||
* PARTIAL FILE: two_way_binding_to_signal_loop_variable.js
|
||||
****************************************************************************************************/
|
||||
import { Component, Directive, model, signal } from '@angular/core';
|
||||
import * as i0 from "@angular/core";
|
||||
export class NgModelDirective {
|
||||
constructor() {
|
||||
this.ngModel = model.required();
|
||||
}
|
||||
}
|
||||
NgModelDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: NgModelDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
||||
NgModelDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "0.0.0-PLACEHOLDER", type: NgModelDirective, isStandalone: true, selector: "[ngModel]", inputs: { ngModel: { classPropertyName: "ngModel", publicName: "ngModel", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { ngModel: "ngModelChange" }, ngImport: i0 });
|
||||
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: NgModelDirective, decorators: [{
|
||||
type: Directive,
|
||||
args: [{
|
||||
selector: '[ngModel]',
|
||||
standalone: true,
|
||||
}]
|
||||
}] });
|
||||
export class TestCmp {
|
||||
constructor() {
|
||||
this.names = [signal('Angular')];
|
||||
}
|
||||
}
|
||||
TestCmp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestCmp, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
||||
TestCmp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "0.0.0-PLACEHOLDER", type: TestCmp, isStandalone: true, selector: "ng-component", ngImport: i0, template: `
|
||||
@for (name of names; track $index) {
|
||||
<input [(ngModel)]="name" />
|
||||
}
|
||||
`, isInline: true, dependencies: [{ kind: "directive", type: NgModelDirective, selector: "[ngModel]", inputs: ["ngModel"], outputs: ["ngModelChange"] }] });
|
||||
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestCmp, decorators: [{
|
||||
type: Component,
|
||||
args: [{
|
||||
template: `
|
||||
@for (name of names; track $index) {
|
||||
<input [(ngModel)]="name" />
|
||||
}
|
||||
`,
|
||||
standalone: true,
|
||||
imports: [NgModelDirective],
|
||||
}]
|
||||
}] });
|
||||
|
||||
/****************************************************************************************************
|
||||
* PARTIAL FILE: two_way_binding_to_signal_loop_variable.d.ts
|
||||
****************************************************************************************************/
|
||||
import * as i0 from "@angular/core";
|
||||
export declare class NgModelDirective {
|
||||
ngModel: import("@angular/core").ModelSignal<string>;
|
||||
static ɵfac: i0.ɵɵFactoryDeclaration<NgModelDirective, never>;
|
||||
static ɵdir: i0.ɵɵDirectiveDeclaration<NgModelDirective, "[ngModel]", never, { "ngModel": { "alias": "ngModel"; "required": true; "isSignal": true; }; }, { "ngModel": "ngModelChange"; }, never, never, true, never>;
|
||||
}
|
||||
export declare class TestCmp {
|
||||
names: import("@angular/core").WritableSignal<string>[];
|
||||
static ɵfac: i0.ɵɵFactoryDeclaration<TestCmp, never>;
|
||||
static ɵcmp: i0.ɵɵComponentDeclaration<TestCmp, "ng-component", never, {}, {}, never, never, true, never>;
|
||||
}
|
||||
|
||||
|
||||
@@ -355,6 +355,23 @@
|
||||
"failureMessage": "Incorrect template"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "should generate a two-way binding to a @for loop variable that is a signal",
|
||||
"inputFiles": [
|
||||
"two_way_binding_to_signal_loop_variable.ts"
|
||||
],
|
||||
"expectations": [
|
||||
{
|
||||
"files": [
|
||||
{
|
||||
"expected": "two_way_binding_to_signal_loop_variable_template.js",
|
||||
"generated": "two_way_binding_to_signal_loop_variable.js"
|
||||
}
|
||||
],
|
||||
"failureMessage": "Incorrect template"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import {Component, Directive, model, signal} from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[ngModel]',
|
||||
standalone: true,
|
||||
})
|
||||
export class NgModelDirective {
|
||||
ngModel = model.required<string>();
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
@for (name of names; track $index) {
|
||||
<input [(ngModel)]="name" />
|
||||
}
|
||||
`,
|
||||
standalone: true,
|
||||
imports: [NgModelDirective],
|
||||
})
|
||||
export class TestCmp {
|
||||
names = [signal('Angular')];
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
function TestCmp_For_1_Template(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
const $_r1$ = $r3$.ɵɵgetCurrentView();
|
||||
$r3$.ɵɵelementStart(0, "input", 0);
|
||||
$r3$.ɵɵtwoWayListener("ngModelChange", function TestCmp_For_1_Template_input_ngModelChange_0_listener($event) {
|
||||
const $name_r2$ = $r3$.ɵɵrestoreView($_r1$).$implicit;
|
||||
$r3$.ɵɵtwoWayBindingSet($name_r2$, $event);
|
||||
return $r3$.ɵɵresetView($event);
|
||||
});
|
||||
$r3$.ɵɵelementEnd();
|
||||
}
|
||||
if (rf & 2) {
|
||||
const $name_r2$ = ctx.$implicit;
|
||||
$r3$.ɵɵtwoWayProperty("ngModel", $name_r2$);
|
||||
}
|
||||
}
|
||||
|
||||
…
|
||||
|
||||
function TestCmp_Template(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
$r3$.ɵɵrepeaterCreate(0, TestCmp_For_1_Template, 1, 1, "input", null, $r3$.ɵɵrepeaterTrackByIndex);
|
||||
}
|
||||
if (rf & 2) {
|
||||
$r3$.ɵɵrepeater(ctx.names);
|
||||
}
|
||||
}
|
||||
@@ -199,7 +199,6 @@ runInEachFileSystem(() => {
|
||||
.toContain(`inputs: { data: [i0.ɵɵInputFlags.SignalBased, "data"] }`);
|
||||
});
|
||||
|
||||
// TODO(crisbeto): we may not want to support this combination. Will discuss with the team.
|
||||
it('should report mixed two-way binding with a signal input', () => {
|
||||
env.write('test.ts', `
|
||||
import {Component, Directive, input, Output, EventEmitter} from '@angular/core';
|
||||
|
||||
@@ -4868,6 +4868,70 @@ suppress
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not be able to write to loop template variables in a two-way binding', () => {
|
||||
env.write('test.ts', `
|
||||
import {Component, Directive, Input, Output, EventEmitter} from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[twoWayDir]',
|
||||
standalone: true
|
||||
})
|
||||
export class TwoWayDir {
|
||||
@Input() value: number = 0;
|
||||
@Output() valueChanges: EventEmitter<number> = new EventEmitter();
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: \`
|
||||
@for (item of items; track item) {
|
||||
<button twoWayDir [(value)]="$index"></button>
|
||||
}
|
||||
\`,
|
||||
standalone: true,
|
||||
imports: [TwoWayDir]
|
||||
})
|
||||
export class Main {
|
||||
items = [];
|
||||
}
|
||||
`);
|
||||
|
||||
const diags = env.driveDiagnostics();
|
||||
expect(diags.map(d => ts.flattenDiagnosticMessageText(d.messageText, ''))).toEqual([
|
||||
`Cannot use a non-signal variable '$index' in a two-way binding expression. Template variables are read-only.`,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should allow writes to signal-based template variables in two-way bindings', () => {
|
||||
env.write('test.ts', `
|
||||
import {Component, Directive, Input, Output, EventEmitter, signal} from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[twoWayDir]',
|
||||
standalone: true
|
||||
})
|
||||
export class TwoWayDir {
|
||||
@Input() value: number = 0;
|
||||
@Output() valueChanges: EventEmitter<number> = new EventEmitter();
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: \`
|
||||
@for (current of signals; track current) {
|
||||
<button twoWayDir [(value)]="current"></button>
|
||||
}
|
||||
\`,
|
||||
standalone: true,
|
||||
imports: [TwoWayDir]
|
||||
})
|
||||
export class Main {
|
||||
signals = [signal(1)];
|
||||
}
|
||||
`);
|
||||
|
||||
const diags = env.driveDiagnostics();
|
||||
expect(diags).toEqual([]);
|
||||
});
|
||||
|
||||
it('should check the track expression of a for loop block', () => {
|
||||
env.write('test.ts', `
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ASTWithSource} from '../expression_parser/ast';
|
||||
import {ASTWithSource, EmptyExpr} from '../expression_parser/ast';
|
||||
import * as html from '../ml_parser/ast';
|
||||
import {ParseError, ParseSourceSpan} from '../parse_util';
|
||||
import {BindingParser} from '../template_parser/binding_parser';
|
||||
@@ -256,6 +256,9 @@ function parseForLoopParameters(
|
||||
new ParseError(param.sourceSpan, '@for loop can only have one "track" expression'));
|
||||
} else {
|
||||
const expression = parseBlockParameterToBinding(param, bindingParser, trackMatch[1]);
|
||||
if (expression.ast instanceof EmptyExpr) {
|
||||
errors.push(new ParseError(param.sourceSpan, '@for loop must have a "track" expression'));
|
||||
}
|
||||
const keywordSpan = new ParseSourceSpan(
|
||||
param.sourceSpan.start, param.sourceSpan.start.moveBy('track'.length));
|
||||
result.trackBy = {expression, keywordSpan};
|
||||
|
||||
@@ -32,12 +32,24 @@ export function transformTwoWayBindingSet(job: CompilationJob): void {
|
||||
}
|
||||
}
|
||||
|
||||
function wrapSetOperation(target: o.ReadPropExpr|o.ReadKeyExpr, value: o.Expression): o.Expression {
|
||||
function wrapSetOperation(
|
||||
target: o.ReadPropExpr|o.ReadKeyExpr|ir.ReadVariableExpr, value: o.Expression): o.Expression {
|
||||
// ASSUMPTION: here we're assuming that `ReadVariableExpr` will be a reference
|
||||
// to a local template variable. This appears to be the case at the time of writing.
|
||||
// If the expression is targeting a variable read, we only emit the `twoWayBindingSet` since
|
||||
// the fallback would be attempting to write into a constant. Invalid usages will be flagged
|
||||
// during template type checking.
|
||||
if (target instanceof ir.ReadVariableExpr) {
|
||||
return ng.twoWayBindingSet(target, value);
|
||||
}
|
||||
|
||||
return ng.twoWayBindingSet(target, value).or(target.set(value));
|
||||
}
|
||||
|
||||
function isReadExpression(value: unknown): value is o.ReadPropExpr|o.ReadKeyExpr {
|
||||
return value instanceof o.ReadPropExpr || value instanceof o.ReadKeyExpr;
|
||||
function isReadExpression(value: unknown): value is o.ReadPropExpr|o.ReadKeyExpr|
|
||||
ir.ReadVariableExpr {
|
||||
return value instanceof o.ReadPropExpr || value instanceof o.ReadKeyExpr ||
|
||||
value instanceof ir.ReadVariableExpr;
|
||||
}
|
||||
|
||||
function wrapAction(target: o.Expression, value: o.Expression): o.Expression {
|
||||
|
||||
@@ -1520,6 +1520,11 @@ describe('R3 template transform', () => {
|
||||
@case {case}
|
||||
}
|
||||
`)).toThrowError(/@case block must have exactly one parameter/);
|
||||
expect(() => parse(`
|
||||
@switch (cond) {
|
||||
@case ( ) {case}
|
||||
}
|
||||
`)).toThrowError(/@case block must have exactly one parameter/);
|
||||
});
|
||||
|
||||
it('should report if a case has more than one parameter', () => {
|
||||
@@ -1711,6 +1716,8 @@ describe('R3 template transform', () => {
|
||||
it('should report if for loop does not have a tracking expression', () => {
|
||||
expect(() => parse(`@for (a of b) {hello}`))
|
||||
.toThrowError(/@for loop must have a "track" expression/);
|
||||
expect(() => parse(`@for (a of b; track ) {hello}`))
|
||||
.toThrowError(/@for loop must have a "track" expression/);
|
||||
});
|
||||
|
||||
it('should report mismatching optional parentheses around for loop expression', () => {
|
||||
@@ -1932,6 +1939,9 @@ describe('R3 template transform', () => {
|
||||
expect(() => parse(`
|
||||
@if {hello}
|
||||
`)).toThrowError(/Conditional block does not have an expression/);
|
||||
expect(() => parse(`
|
||||
@if ( ) {hello}
|
||||
`)).toThrowError(/Conditional block does not have an expression/);
|
||||
});
|
||||
|
||||
it('should report an unknown parameter in an if block', () => {
|
||||
|
||||
@@ -21,7 +21,6 @@ import {EnvironmentInjector} from '../di/r3_injector';
|
||||
import {ErrorHandler, INTERNAL_APPLICATION_ERROR_HANDLER} from '../error_handler';
|
||||
import {formatRuntimeError, RuntimeError, RuntimeErrorCode} from '../errors';
|
||||
import {Type} from '../interface/type';
|
||||
import {isG3} from '../is_internal';
|
||||
import {ComponentFactory, ComponentRef} from '../linker/component_factory';
|
||||
import {ComponentFactoryResolver} from '../linker/component_factory_resolver';
|
||||
import {NgModuleRef} from '../linker/ng_module_factory';
|
||||
@@ -492,6 +491,11 @@ export class ApplicationRef {
|
||||
* detection pass during which all change detection must complete.
|
||||
*/
|
||||
tick(): void {
|
||||
this._tick(true);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_tick(refreshViews: boolean): void {
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) && this.warnIfDestroyed();
|
||||
if (this._runningTick) {
|
||||
throw new RuntimeError(
|
||||
@@ -503,7 +507,7 @@ export class ApplicationRef {
|
||||
try {
|
||||
this._runningTick = true;
|
||||
|
||||
this.detectChangesInAttachedViews();
|
||||
this.detectChangesInAttachedViews(refreshViews);
|
||||
|
||||
if ((typeof ngDevMode === 'undefined' || ngDevMode)) {
|
||||
for (let view of this._views) {
|
||||
@@ -520,7 +524,7 @@ export class ApplicationRef {
|
||||
}
|
||||
}
|
||||
|
||||
private detectChangesInAttachedViews() {
|
||||
private detectChangesInAttachedViews(refreshViews: boolean) {
|
||||
let runs = 0;
|
||||
const afterRenderEffectManager = this.afterRenderEffectManager;
|
||||
while (true) {
|
||||
@@ -532,10 +536,12 @@ export class ApplicationRef {
|
||||
'Ensure afterRender or queueStateUpdate hooks are not continuously causing updates.');
|
||||
}
|
||||
|
||||
const isFirstPass = runs === 0;
|
||||
this.beforeRender.next(isFirstPass);
|
||||
for (let {_lView, notifyErrorHandler} of this._views) {
|
||||
detectChangesInViewIfRequired(_lView, isFirstPass, notifyErrorHandler);
|
||||
if (refreshViews) {
|
||||
const isFirstPass = runs === 0;
|
||||
this.beforeRender.next(isFirstPass);
|
||||
for (let {_lView, notifyErrorHandler} of this._views) {
|
||||
detectChangesInViewIfRequired(_lView, isFirstPass, notifyErrorHandler);
|
||||
}
|
||||
}
|
||||
runs++;
|
||||
|
||||
@@ -712,9 +718,7 @@ export function detectChangesInViewIfRequired(
|
||||
}
|
||||
|
||||
function shouldRecheckView(view: LView): boolean {
|
||||
return requiresRefreshOrTraversal(view) ||
|
||||
// TODO(atscott): Remove isG3 check and make this a breaking change for v18
|
||||
(isG3 && !!(view[FLAGS] & LViewFlags.Dirty));
|
||||
return requiresRefreshOrTraversal(view);
|
||||
}
|
||||
|
||||
function detectChangesInView(lView: LView, notifyErrorHandler: boolean, isFirstPass: boolean) {
|
||||
|
||||
@@ -6,17 +6,21 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {assertInInjectionContext} from '../../di';
|
||||
|
||||
import {createInputSignal, InputOptions, InputOptionsWithoutTransform, InputOptionsWithTransform, InputSignal, InputSignalWithTransform} from './input_signal';
|
||||
import {REQUIRED_UNSET_VALUE} from './input_signal_node';
|
||||
|
||||
export function inputFunction<ReadT, WriteT>(
|
||||
initialValue?: ReadT,
|
||||
opts?: InputOptions<ReadT, WriteT>): InputSignalWithTransform<ReadT|undefined, WriteT> {
|
||||
ngDevMode && assertInInjectionContext(input);
|
||||
return createInputSignal(initialValue, opts);
|
||||
}
|
||||
|
||||
export function inputRequiredFunction<ReadT, WriteT>(opts?: InputOptions<ReadT, WriteT>):
|
||||
InputSignalWithTransform<ReadT, WriteT> {
|
||||
ngDevMode && assertInInjectionContext(input);
|
||||
return createInputSignal(REQUIRED_UNSET_VALUE as never, opts);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ export interface OutputOptions {
|
||||
}
|
||||
|
||||
/**
|
||||
* The `outputs` function allows declaration of outputs in directives and
|
||||
* The `output` function allows declaration of outputs in directives and
|
||||
* components.
|
||||
*
|
||||
* Initializes an output that can emit values to consumers of your
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import {setActiveConsumer} from '@angular/core/primitives/signals';
|
||||
|
||||
import {inject} from '../../di/injector_compatibility';
|
||||
import {ErrorHandler} from '../../error_handler';
|
||||
import {RuntimeError, RuntimeErrorCode} from '../../errors';
|
||||
import {DestroyRef} from '../../linker/destroy_ref';
|
||||
|
||||
@@ -30,6 +31,7 @@ import {OutputRef, OutputRefSubscription} from './output_ref';
|
||||
export class OutputEmitterRef<T> implements OutputRef<T> {
|
||||
private destroyed = false;
|
||||
private listeners: Array<(value: T) => void>|null = null;
|
||||
private errorHandler = inject(ErrorHandler, {optional: true});
|
||||
|
||||
/** @internal */
|
||||
destroyRef: DestroyRef = inject(DestroyRef);
|
||||
@@ -73,10 +75,19 @@ export class OutputEmitterRef<T> implements OutputRef<T> {
|
||||
'The owning directive/component is destroyed.');
|
||||
}
|
||||
|
||||
if (this.listeners === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const previousConsumer = setActiveConsumer(null);
|
||||
try {
|
||||
// TODO: Run every listener using `try/catch`.
|
||||
this.listeners?.forEach(fn => fn(value));
|
||||
for (const listenerFn of this.listeners) {
|
||||
try {
|
||||
listenerFn(value);
|
||||
} catch (err: unknown) {
|
||||
this.errorHandler?.handleError(err);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
setActiveConsumer(previousConsumer);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {assertInInjectionContext} from '../di';
|
||||
import {ProviderToken} from '../di/provider_token';
|
||||
import {createMultiResultQuerySignalFn, createSingleResultOptionalQuerySignalFn, createSingleResultRequiredQuerySignalFn} from '../render3/query_reactive';
|
||||
import {Signal} from '../render3/reactivity/api';
|
||||
@@ -13,11 +14,13 @@ import {Signal} from '../render3/reactivity/api';
|
||||
function viewChildFn<LocatorT, ReadT>(
|
||||
locator: ProviderToken<LocatorT>|string,
|
||||
opts?: {read?: ProviderToken<ReadT>}): Signal<ReadT|undefined> {
|
||||
ngDevMode && assertInInjectionContext(viewChild);
|
||||
return createSingleResultOptionalQuerySignalFn<ReadT>();
|
||||
}
|
||||
|
||||
function viewChildRequiredFn<LocatorT, ReadT>(
|
||||
locator: ProviderToken<LocatorT>|string, opts?: {read?: ProviderToken<ReadT>}): Signal<ReadT> {
|
||||
ngDevMode && assertInInjectionContext(viewChild);
|
||||
return createSingleResultRequiredQuerySignalFn<ReadT>();
|
||||
}
|
||||
|
||||
@@ -108,18 +111,21 @@ export function viewChildren<LocatorT, ReadT>(
|
||||
export function viewChildren<LocatorT, ReadT>(
|
||||
locator: ProviderToken<LocatorT>|string,
|
||||
opts?: {read?: ProviderToken<ReadT>}): Signal<ReadonlyArray<ReadT>> {
|
||||
ngDevMode && assertInInjectionContext(viewChildren);
|
||||
return createMultiResultQuerySignalFn<ReadT>();
|
||||
}
|
||||
|
||||
export function contentChildFn<LocatorT, ReadT>(
|
||||
locator: ProviderToken<LocatorT>|string,
|
||||
opts?: {descendants?: boolean, read?: ProviderToken<ReadT>}): Signal<ReadT|undefined> {
|
||||
ngDevMode && assertInInjectionContext(contentChild);
|
||||
return createSingleResultOptionalQuerySignalFn<ReadT>();
|
||||
}
|
||||
|
||||
function contentChildRequiredFn<LocatorT, ReadT>(
|
||||
locator: ProviderToken<LocatorT>|string,
|
||||
opts?: {descendants?: boolean, read?: ProviderToken<ReadT>}): Signal<ReadT> {
|
||||
ngDevMode && assertInInjectionContext(contentChildren);
|
||||
return createSingleResultRequiredQuerySignalFn<ReadT>();
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,14 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
export const enum NotificationType {
|
||||
RefreshViews,
|
||||
AfterRenderHooks,
|
||||
}
|
||||
|
||||
/**
|
||||
* Injectable that is notified when an `LView` is made aware of changes to application state.
|
||||
*/
|
||||
export abstract class ChangeDetectionScheduler {
|
||||
abstract notify(): void;
|
||||
abstract notify(source?: NotificationType): void;
|
||||
}
|
||||
|
||||
@@ -12,16 +12,22 @@ import {PendingTasks} from '../../pending_tasks';
|
||||
import {global} from '../../util/global';
|
||||
import {NgZone, NoopNgZone} from '../../zone/ng_zone';
|
||||
|
||||
import {ChangeDetectionScheduler} from './zoneless_scheduling';
|
||||
import {ChangeDetectionScheduler, NotificationType} from './zoneless_scheduling';
|
||||
|
||||
@Injectable({providedIn: 'root'})
|
||||
class ChangeDetectionSchedulerImpl implements ChangeDetectionScheduler {
|
||||
private appRef = inject(ApplicationRef);
|
||||
private taskService = inject(PendingTasks);
|
||||
private pendingRenderTaskId: number|null = null;
|
||||
private shouldRefreshViews = false;
|
||||
|
||||
notify(): void {
|
||||
if (this.pendingRenderTaskId !== null) return;
|
||||
notify(type = NotificationType.RefreshViews): void {
|
||||
// When the only source of notification is an afterRender hook will skip straight to the hooks
|
||||
// rather than refreshing views in ApplicationRef.tick
|
||||
this.shouldRefreshViews ||= type === NotificationType.RefreshViews;
|
||||
if (this.pendingRenderTaskId !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.pendingRenderTaskId = this.taskService.add();
|
||||
this.raceTimeoutAndRequestAnimationFrame();
|
||||
@@ -63,9 +69,10 @@ class ChangeDetectionSchedulerImpl implements ChangeDetectionScheduler {
|
||||
private tick() {
|
||||
try {
|
||||
if (!this.appRef.destroyed) {
|
||||
this.appRef.tick();
|
||||
this.appRef._tick(this.shouldRefreshViews);
|
||||
}
|
||||
} finally {
|
||||
this.shouldRefreshViews = false;
|
||||
// If this is the last task, the service will synchronously emit a stable notification. If
|
||||
// there is a subscriber that then acts in a way that tries to notify the scheduler again,
|
||||
// we need to be able to respond to schedule a new change detection. Therefore, we should
|
||||
|
||||
@@ -25,13 +25,12 @@ export {INJECTOR_SCOPE as ɵINJECTOR_SCOPE} from './di/scope';
|
||||
export {XSS_SECURITY_URL as ɵXSS_SECURITY_URL} from './error_details_base_url';
|
||||
export {formatRuntimeError as ɵformatRuntimeError, RuntimeError as ɵRuntimeError, RuntimeErrorCode as ɵRuntimeErrorCode} from './errors';
|
||||
export {annotateForHydration as ɵannotateForHydration} from './hydration/annotate';
|
||||
export {withDomHydration as ɵwithDomHydration} from './hydration/api';
|
||||
export {withDomHydration as ɵwithDomHydration, withI18nHydration as ɵwithI18nHydration} from './hydration/api';
|
||||
export {IS_HYDRATION_DOM_REUSE_ENABLED as ɵIS_HYDRATION_DOM_REUSE_ENABLED} from './hydration/tokens';
|
||||
export {HydratedNode as ɵHydratedNode, HydrationInfo as ɵHydrationInfo, readHydrationInfo as ɵreadHydrationInfo, SSR_CONTENT_INTEGRITY_MARKER as ɵSSR_CONTENT_INTEGRITY_MARKER} from './hydration/utils';
|
||||
export {CurrencyIndex as ɵCurrencyIndex, ExtraLocaleDataIndex as ɵExtraLocaleDataIndex, findLocaleData as ɵfindLocaleData, getLocaleCurrencyCode as ɵgetLocaleCurrencyCode, getLocalePluralCase as ɵgetLocalePluralCase, LocaleDataIndex as ɵLocaleDataIndex, registerLocaleData as ɵregisterLocaleData, unregisterAllLocaleData as ɵunregisterLocaleData} from './i18n/locale_data_api';
|
||||
export {DEFAULT_LOCALE_ID as ɵDEFAULT_LOCALE_ID} from './i18n/localization';
|
||||
export {Writable as ɵWritable} from './interface/type';
|
||||
export {isG3 as ɵisG3} from './is_internal';
|
||||
export {ComponentFactory as ɵComponentFactory} from './linker/component_factory';
|
||||
export {clearResolutionOfComponentResourcesQueue as ɵclearResolutionOfComponentResourcesQueue, isComponentDefPendingResolution as ɵisComponentDefPendingResolution, resolveComponentResources as ɵresolveComponentResources, restoreComponentResolutionQueue as ɵrestoreComponentResolutionQueue} from './metadata/resource_loading';
|
||||
export {PendingTasks as ɵPendingTasks} from './pending_tasks';
|
||||
|
||||
@@ -8,9 +8,10 @@
|
||||
|
||||
import {RuntimeError, RuntimeErrorCode} from '../errors';
|
||||
import {InjectorProfilerContext, setInjectorProfilerContext} from '../render3/debug/injector_profiler';
|
||||
|
||||
import {getInjectImplementation, setInjectImplementation} from './inject_switch';
|
||||
import type {Injector} from './injector';
|
||||
import {getCurrentInjector, setCurrentInjector} from './injector_compatibility';
|
||||
import {getInjectImplementation, setInjectImplementation} from './inject_switch';
|
||||
import {R3Injector} from './r3_injector';
|
||||
|
||||
/**
|
||||
@@ -47,6 +48,12 @@ export function runInInjectionContext<ReturnT>(injector: Injector, fn: () => Ret
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the current stack frame is inside an injection context.
|
||||
*/
|
||||
export function isInInjectionContext(): boolean {
|
||||
return getInjectImplementation() !== undefined || getCurrentInjector() != null;
|
||||
}
|
||||
/**
|
||||
* Asserts that the current stack frame is within an [injection
|
||||
* context](guide/dependency-injection-context) and has access to `inject`.
|
||||
@@ -58,7 +65,7 @@ export function runInInjectionContext<ReturnT>(injector: Injector, fn: () => Ret
|
||||
export function assertInInjectionContext(debugFn: Function): void {
|
||||
// Taking a `Function` instead of a string name here prevents the unminified name of the function
|
||||
// from being retained in the bundle regardless of minification.
|
||||
if (!getInjectImplementation() && !getCurrentInjector()) {
|
||||
if (!isInInjectionContext()) {
|
||||
throw new RuntimeError(
|
||||
RuntimeErrorCode.MISSING_INJECTION_CONTEXT,
|
||||
ngDevMode &&
|
||||
|
||||
@@ -10,6 +10,7 @@ import {setActiveConsumer} from '@angular/core/primitives/signals';
|
||||
import {PartialObserver, Subject, Subscription} from 'rxjs';
|
||||
|
||||
import {OutputRef} from './authoring/output/output_ref';
|
||||
import {isInInjectionContext} from './di/contextual';
|
||||
import {inject} from './di/injector_compatibility';
|
||||
import {DestroyRef} from './linker/destroy_ref';
|
||||
|
||||
@@ -107,18 +108,16 @@ export interface EventEmitter<T> extends Subject<T>, OutputRef<T> {
|
||||
|
||||
class EventEmitter_ extends Subject<any> implements OutputRef<any> {
|
||||
__isAsync: boolean; // tslint:disable-line
|
||||
destroyRef: DestroyRef|undefined;
|
||||
destroyRef: DestroyRef|undefined = undefined;
|
||||
|
||||
constructor(isAsync: boolean = false) {
|
||||
super();
|
||||
this.__isAsync = isAsync;
|
||||
|
||||
// Attempt to retrieve a `DestroyRef` optionally.
|
||||
// For backwards compatibility reasons, this cannot be required.
|
||||
try {
|
||||
this.destroyRef = inject(DestroyRef);
|
||||
} catch {
|
||||
this.destroyRef = undefined;
|
||||
// For backwards compatibility reasons, this cannot be required
|
||||
if (isInInjectionContext()) {
|
||||
this.destroyRef = inject(DestroyRef, {optional: true}) ?? undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import {CONTEXT, HEADER_OFFSET, HOST, LView, PARENT, RENDERER, TView, TVIEW, TVi
|
||||
import {unwrapLView, unwrapRNode} from '../render3/util/view_utils';
|
||||
import {TransferState} from '../transfer_state';
|
||||
|
||||
import {isI18nHydrationSupportEnabled} from './api';
|
||||
import {unsupportedProjectionOfDomNodes} from './error_handling';
|
||||
import {CONTAINERS, DISCONNECTED_NODES, ELEMENT_CONTAINERS, MULTIPLIER, NODES, NUM_ROOT_NODES, SerializedContainerView, SerializedView, TEMPLATE_ID, TEMPLATES} from './interfaces';
|
||||
import {calcPathForNode, isDisconnectedNode} from './node_lookup_utils';
|
||||
@@ -526,7 +527,8 @@ function componentUsesShadowDomEncapsulation(lView: LView): boolean {
|
||||
function annotateHostElementForHydration(
|
||||
element: RElement, lView: LView, context: HydrationContext): number|null {
|
||||
const renderer = lView[RENDERER];
|
||||
if (hasI18n(lView) || componentUsesShadowDomEncapsulation(lView)) {
|
||||
if ((hasI18n(lView) && !isI18nHydrationSupportEnabled()) ||
|
||||
componentUsesShadowDomEncapsulation(lView)) {
|
||||
// Attach the skip hydration attribute if this component:
|
||||
// - either has i18n blocks, since hydrating such blocks is not yet supported
|
||||
// - or uses ShadowDom view encapsulation, since Domino doesn't support
|
||||
|
||||
@@ -12,6 +12,7 @@ import {ENVIRONMENT_INITIALIZER, EnvironmentProviders, Injector, makeEnvironment
|
||||
import {inject} from '../di/injector_compatibility';
|
||||
import {formatRuntimeError, RuntimeError, RuntimeErrorCode} from '../errors';
|
||||
import {enableLocateOrCreateContainerRefImpl} from '../linker/view_container_ref';
|
||||
import {enableLocateOrCreateI18nNodeImpl} from '../render3/i18n/i18n_apply';
|
||||
import {enableLocateOrCreateElementNodeImpl} from '../render3/instructions/element';
|
||||
import {enableLocateOrCreateElementContainerNodeImpl} from '../render3/instructions/element_container';
|
||||
import {enableApplyRootElementTransformImpl} from '../render3/instructions/shared';
|
||||
@@ -24,7 +25,7 @@ import {performanceMarkFeature} from '../util/performance';
|
||||
import {NgZone} from '../zone';
|
||||
|
||||
import {cleanupDehydratedViews} from './cleanup';
|
||||
import {IS_HYDRATION_DOM_REUSE_ENABLED, PRESERVE_HOST_CONTENT} from './tokens';
|
||||
import {IS_HYDRATION_DOM_REUSE_ENABLED, IS_I18N_HYDRATION_ENABLED, PRESERVE_HOST_CONTENT} from './tokens';
|
||||
import {enableRetrieveHydrationInfoImpl, NGH_DATA_KEY, SSR_CONTENT_INTEGRITY_MARKER} from './utils';
|
||||
import {enableFindMatchingDehydratedViewImpl} from './views';
|
||||
|
||||
@@ -34,6 +35,11 @@ import {enableFindMatchingDehydratedViewImpl} from './views';
|
||||
*/
|
||||
let isHydrationSupportEnabled = false;
|
||||
|
||||
/**
|
||||
* Indicates whether support for hydrating i18n blocks is enabled.
|
||||
*/
|
||||
let _isI18nHydrationSupportEnabled = false;
|
||||
|
||||
/**
|
||||
* Defines a period of time that Angular waits for the `ApplicationRef.isStable` to emit `true`.
|
||||
* If there was no event with the `true` value during this time, Angular reports a warning.
|
||||
@@ -62,6 +68,7 @@ function enableHydrationRuntimeSupport() {
|
||||
enableLocateOrCreateContainerRefImpl();
|
||||
enableFindMatchingDehydratedViewImpl();
|
||||
enableApplyRootElementTransformImpl();
|
||||
enableLocateOrCreateI18nNodeImpl();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,6 +152,8 @@ export function withDomHydration(): EnvironmentProviders {
|
||||
{
|
||||
provide: ENVIRONMENT_INITIALIZER,
|
||||
useValue: () => {
|
||||
_isI18nHydrationSupportEnabled = !!inject(IS_I18N_HYDRATION_ENABLED, {optional: true});
|
||||
|
||||
// Since this function is used across both server and client,
|
||||
// make sure that the runtime code is only added when invoked
|
||||
// on the client. Moving forward, the `isPlatformBrowser` check should
|
||||
@@ -198,6 +207,26 @@ export function withDomHydration(): EnvironmentProviders {
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a set of providers required to setup support for i18n hydration.
|
||||
* Requires hydration to be enabled separately.
|
||||
*/
|
||||
export function withI18nHydration(): EnvironmentProviders {
|
||||
return makeEnvironmentProviders([
|
||||
{
|
||||
provide: IS_I18N_HYDRATION_ENABLED,
|
||||
useValue: true,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether i18n hydration support is enabled.
|
||||
*/
|
||||
export function isI18nHydrationSupportEnabled(): boolean {
|
||||
return _isI18nHydrationSupportEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param time The time in ms until the stable timedout warning message is logged
|
||||
|
||||
@@ -11,7 +11,7 @@ import {CONTAINER_HEADER_OFFSET, DEHYDRATED_VIEWS, LContainer} from '../render3/
|
||||
import {Renderer} from '../render3/interfaces/renderer';
|
||||
import {RNode} from '../render3/interfaces/renderer_dom';
|
||||
import {isLContainer, isLView} from '../render3/interfaces/type_checks';
|
||||
import {HEADER_OFFSET, HOST, LView, PARENT, RENDERER, TVIEW} from '../render3/interfaces/view';
|
||||
import {HEADER_OFFSET, HOST, HYDRATION, LView, PARENT, RENDERER, TVIEW} from '../render3/interfaces/view';
|
||||
import {nativeRemoveNode} from '../render3/node_manipulation';
|
||||
import {EMPTY_ARRAY} from '../util/empty';
|
||||
|
||||
@@ -68,11 +68,29 @@ function cleanupLContainer(lContainer: LContainer) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes any remaining dehydrated i18n nodes from a given LView,
|
||||
* both in internal data structure, as well as removing the
|
||||
* corresponding DOM nodes.
|
||||
*/
|
||||
function cleanupDehydratedI18nNodes(lView: LView) {
|
||||
const i18nNodes = lView[HYDRATION]?.i18nNodes;
|
||||
if (i18nNodes) {
|
||||
const renderer = lView[RENDERER];
|
||||
for (const node of i18nNodes.values()) {
|
||||
nativeRemoveNode(renderer, node, false);
|
||||
}
|
||||
lView[HYDRATION]!.i18nNodes = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Walks over `LContainer`s and components registered within
|
||||
* this LView and invokes dehydrated views cleanup function for each one.
|
||||
*/
|
||||
function cleanupLView(lView: LView) {
|
||||
cleanupDehydratedI18nNodes(lView);
|
||||
|
||||
const tView = lView[TVIEW];
|
||||
for (let i = HEADER_OFFSET; i < tView.bindingStartIndex; i++) {
|
||||
if (isLContainer(lView[i])) {
|
||||
|
||||
@@ -165,6 +165,16 @@ export interface DehydratedView {
|
||||
* nodes detected in this view at serialization time.
|
||||
*/
|
||||
disconnectedNodes?: Set<number>|null;
|
||||
|
||||
/**
|
||||
* A mapping from a view to the first child to begin claiming nodes.
|
||||
*
|
||||
* This mapping is generated by an i18n block, and is the source of
|
||||
* truth for the nodes inside of it. Once a view has been hydrated,
|
||||
* the node is removed from the map. The remaining entries indicate
|
||||
* dehydrated nodes that need to be cleaned up.
|
||||
*/
|
||||
i18nNodes?: Map<number, RNode>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -43,6 +43,26 @@ export function isDisconnectedNode(tNode: TNode, lView: LView) {
|
||||
!(unwrapRNode(lView[tNode.index]) as Node)?.isConnected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Locate a node in an i18n tree that corresponds to a given instruction index.
|
||||
*
|
||||
* @param hydrationInfo The hydration annotation data
|
||||
* @param noOffsetIndex the instruction index
|
||||
* @returns an RNode that corresponds to the instruction index
|
||||
*/
|
||||
export function locateI18nRNodeByIndex<T extends RNode>(
|
||||
hydrationInfo: DehydratedView, noOffsetIndex: number): T|null {
|
||||
const i18nNodes = hydrationInfo.i18nNodes;
|
||||
if (i18nNodes) {
|
||||
const native = i18nNodes.get(noOffsetIndex);
|
||||
if (native) {
|
||||
i18nNodes.delete(noOffsetIndex);
|
||||
}
|
||||
return native as T;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Locate a node in DOM tree that corresponds to a given TNode.
|
||||
*
|
||||
@@ -54,49 +74,52 @@ export function isDisconnectedNode(tNode: TNode, lView: LView) {
|
||||
*/
|
||||
export function locateNextRNode<T extends RNode>(
|
||||
hydrationInfo: DehydratedView, tView: TView, lView: LView<unknown>, tNode: TNode): T|null {
|
||||
let native: RNode|null = null;
|
||||
const noOffsetIndex = getNoOffsetIndex(tNode);
|
||||
const nodes = hydrationInfo.data[NODES];
|
||||
if (nodes?.[noOffsetIndex]) {
|
||||
// We know the exact location of the node.
|
||||
native = locateRNodeByPath(nodes[noOffsetIndex], lView);
|
||||
} else if (tView.firstChild === tNode) {
|
||||
// We create a first node in this view, so we use a reference
|
||||
// to the first child in this DOM segment.
|
||||
native = hydrationInfo.firstChild;
|
||||
} else {
|
||||
// Locate a node based on a previous sibling or a parent node.
|
||||
const previousTNodeParent = tNode.prev === null;
|
||||
const previousTNode = (tNode.prev ?? tNode.parent)!;
|
||||
ngDevMode &&
|
||||
assertDefined(
|
||||
previousTNode,
|
||||
'Unexpected state: current TNode does not have a connection ' +
|
||||
'to the previous node or a parent node.');
|
||||
if (isFirstElementInNgContainer(tNode)) {
|
||||
const noOffsetParentIndex = getNoOffsetIndex(tNode.parent!);
|
||||
native = getSegmentHead(hydrationInfo, noOffsetParentIndex);
|
||||
let native: RNode|null = locateI18nRNodeByIndex(hydrationInfo, noOffsetIndex);
|
||||
|
||||
if (!native) {
|
||||
const nodes = hydrationInfo.data[NODES];
|
||||
if (nodes?.[noOffsetIndex]) {
|
||||
// We know the exact location of the node.
|
||||
native = locateRNodeByPath(nodes[noOffsetIndex], lView);
|
||||
} else if (tView.firstChild === tNode) {
|
||||
// We create a first node in this view, so we use a reference
|
||||
// to the first child in this DOM segment.
|
||||
native = hydrationInfo.firstChild;
|
||||
} else {
|
||||
let previousRElement = getNativeByTNode(previousTNode, lView);
|
||||
if (previousTNodeParent) {
|
||||
native = (previousRElement as RElement).firstChild;
|
||||
// Locate a node based on a previous sibling or a parent node.
|
||||
const previousTNodeParent = tNode.prev === null;
|
||||
const previousTNode = (tNode.prev ?? tNode.parent)!;
|
||||
ngDevMode &&
|
||||
assertDefined(
|
||||
previousTNode,
|
||||
'Unexpected state: current TNode does not have a connection ' +
|
||||
'to the previous node or a parent node.');
|
||||
if (isFirstElementInNgContainer(tNode)) {
|
||||
const noOffsetParentIndex = getNoOffsetIndex(tNode.parent!);
|
||||
native = getSegmentHead(hydrationInfo, noOffsetParentIndex);
|
||||
} else {
|
||||
// If the previous node is an element, but it also has container info,
|
||||
// this means that we are processing a node like `<div #vcrTarget>`, which is
|
||||
// represented in the DOM as `<div></div>...<!--container-->`.
|
||||
// In this case, there are nodes *after* this element and we need to skip
|
||||
// all of them to reach an element that we are looking for.
|
||||
const noOffsetPrevSiblingIndex = getNoOffsetIndex(previousTNode);
|
||||
const segmentHead = getSegmentHead(hydrationInfo, noOffsetPrevSiblingIndex);
|
||||
if (previousTNode.type === TNodeType.Element && segmentHead) {
|
||||
const numRootNodesToSkip =
|
||||
calcSerializedContainerSize(hydrationInfo, noOffsetPrevSiblingIndex);
|
||||
// `+1` stands for an anchor comment node after all the views in this container.
|
||||
const nodesToSkip = numRootNodesToSkip + 1;
|
||||
// First node after this segment.
|
||||
native = siblingAfter(nodesToSkip, segmentHead);
|
||||
let previousRElement = getNativeByTNode(previousTNode, lView);
|
||||
if (previousTNodeParent) {
|
||||
native = (previousRElement as RElement).firstChild;
|
||||
} else {
|
||||
native = previousRElement.nextSibling;
|
||||
// If the previous node is an element, but it also has container info,
|
||||
// this means that we are processing a node like `<div #vcrTarget>`, which is
|
||||
// represented in the DOM as `<div></div>...<!--container-->`.
|
||||
// In this case, there are nodes *after* this element and we need to skip
|
||||
// all of them to reach an element that we are looking for.
|
||||
const noOffsetPrevSiblingIndex = getNoOffsetIndex(previousTNode);
|
||||
const segmentHead = getSegmentHead(hydrationInfo, noOffsetPrevSiblingIndex);
|
||||
if (previousTNode.type === TNodeType.Element && segmentHead) {
|
||||
const numRootNodesToSkip =
|
||||
calcSerializedContainerSize(hydrationInfo, noOffsetPrevSiblingIndex);
|
||||
// `+1` stands for an anchor comment node after all the views in this container.
|
||||
const nodesToSkip = numRootNodesToSkip + 1;
|
||||
// First node after this segment.
|
||||
native = siblingAfter(nodesToSkip, segmentHead);
|
||||
} else {
|
||||
native = previousRElement.nextSibling;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,3 +28,10 @@ export const PRESERVE_HOST_CONTENT = new InjectionToken<boolean>(
|
||||
providedIn: 'root',
|
||||
factory: () => PRESERVE_HOST_CONTENT_DEFAULT,
|
||||
});
|
||||
|
||||
/**
|
||||
* Internal token that indicates whether hydration support for i18n
|
||||
* is enabled.
|
||||
*/
|
||||
export const IS_I18N_HYDRATION_ENABLED = new InjectionToken<boolean>(
|
||||
(typeof ngDevMode === 'undefined' || !!ngDevMode ? 'IS_I18N_HYDRATION_ENABLED' : ''));
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {TNode, TNodeFlags} from '../render3/interfaces/node';
|
||||
|
||||
/**
|
||||
* Checks whether a TNode is considered detached, i.e. not present in the
|
||||
* translated i18n template. We should not attempt hydration for such nodes
|
||||
* and instead, use a regular "creation mode".
|
||||
*/
|
||||
export function isDetachedByI18n(tNode: TNode) {
|
||||
return (tNode.flags & TNodeFlags.isDetached) === TNodeFlags.isDetached;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
/**
|
||||
* Used to patch behavior that needs to _temporarily_ be different between g3 and external.
|
||||
*
|
||||
* For example, make breaking changes ahead of the main branch targeting a major version.
|
||||
* Permanent differences between g3 and external should be configured by individual patches.
|
||||
*/
|
||||
export const isG3 = false;
|
||||
@@ -6,7 +6,8 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {assertInInjectionContext, Injector, ɵɵdefineInjectable} from '../di';
|
||||
import {ChangeDetectionScheduler, NotificationType} from '../change_detection/scheduling/zoneless_scheduling';
|
||||
import {assertInInjectionContext, Injector, runInInjectionContext, ɵɵdefineInjectable} from '../di';
|
||||
import {inject} from '../di/injector_compatibility';
|
||||
import {ErrorHandler} from '../error_handler';
|
||||
import {DestroyRef} from '../linker/destroy_ref';
|
||||
@@ -241,7 +242,7 @@ export function afterRender(callback: VoidFunction, options?: AfterRenderOptions
|
||||
unregisterFn();
|
||||
};
|
||||
const unregisterFn = injector.get(DestroyRef).onDestroy(destroy);
|
||||
const instance = new AfterRenderCallback(injector, phase, callback);
|
||||
const instance = runInInjectionContext(injector, () => new AfterRenderCallback(phase, callback));
|
||||
|
||||
callbackHandler.register(instance);
|
||||
return {destroy};
|
||||
@@ -317,10 +318,10 @@ export function afterNextRender(
|
||||
unregisterFn();
|
||||
};
|
||||
const unregisterFn = injector.get(DestroyRef).onDestroy(destroy);
|
||||
const instance = new AfterRenderCallback(injector, phase, () => {
|
||||
destroy();
|
||||
callback();
|
||||
});
|
||||
const instance = runInInjectionContext(injector, () => new AfterRenderCallback(phase, () => {
|
||||
destroy();
|
||||
callback();
|
||||
}));
|
||||
|
||||
callbackHandler.register(instance);
|
||||
return {destroy};
|
||||
@@ -330,14 +331,12 @@ export function afterNextRender(
|
||||
* A wrapper around a function to be used as an after render callback.
|
||||
*/
|
||||
class AfterRenderCallback {
|
||||
private zone: NgZone;
|
||||
private errorHandler: ErrorHandler|null;
|
||||
private zone = inject(NgZone);
|
||||
private errorHandler = inject(ErrorHandler, {optional: true});
|
||||
|
||||
constructor(
|
||||
injector: Injector, public readonly phase: AfterRenderPhase,
|
||||
private callbackFn: VoidFunction) {
|
||||
this.zone = injector.get(NgZone);
|
||||
this.errorHandler = injector.get(ErrorHandler, null, {optional: true});
|
||||
constructor(readonly phase: AfterRenderPhase, private callbackFn: VoidFunction) {
|
||||
// Registering a callback will notify the scheduler.
|
||||
inject(ChangeDetectionScheduler, {optional: true})?.notify(NotificationType.AfterRenderHooks);
|
||||
}
|
||||
|
||||
invoke() {
|
||||
|
||||
@@ -18,7 +18,7 @@ import {RElement, RNode, RText} from '../interfaces/renderer_dom';
|
||||
import {SanitizerFn} from '../interfaces/sanitization';
|
||||
import {HEADER_OFFSET, LView, RENDERER, TView} from '../interfaces/view';
|
||||
import {createCommentNode, createElementNode, createTextNode, nativeInsertBefore, nativeParentNode, nativeRemoveNode, updateTextNode} from '../node_manipulation';
|
||||
import {getBindingIndex} from '../state';
|
||||
import {getBindingIndex, lastNodeWasCreated, wasLastNodeCreated} from '../state';
|
||||
import {renderStringify} from '../util/stringify_utils';
|
||||
import {getNativeByIndex, unwrapRNode} from '../util/view_utils';
|
||||
|
||||
@@ -78,6 +78,39 @@ export function applyI18n(tView: TView, lView: LView, index: number) {
|
||||
changeMaskCounter = 0;
|
||||
}
|
||||
|
||||
function createNodeWithoutHydration(
|
||||
lView: LView, textOrName: string,
|
||||
nodeType: typeof Node.COMMENT_NODE|typeof Node.TEXT_NODE|typeof Node.ELEMENT_NODE) {
|
||||
const renderer = lView[RENDERER];
|
||||
|
||||
switch (nodeType) {
|
||||
case Node.COMMENT_NODE:
|
||||
return createCommentNode(renderer, textOrName);
|
||||
|
||||
case Node.TEXT_NODE:
|
||||
return createTextNode(renderer, textOrName);
|
||||
|
||||
case Node.ELEMENT_NODE:
|
||||
return createElementNode(renderer, textOrName, null);
|
||||
}
|
||||
}
|
||||
|
||||
let _locateOrCreateNode: typeof locateOrCreateNodeImpl = (lView, index, textOrName, nodeType) => {
|
||||
lastNodeWasCreated(true);
|
||||
return createNodeWithoutHydration(lView, textOrName, nodeType);
|
||||
};
|
||||
|
||||
function locateOrCreateNodeImpl(
|
||||
lView: LView, index: number, textOrName: string,
|
||||
nodeType: typeof Node.COMMENT_NODE|typeof Node.TEXT_NODE|typeof Node.ELEMENT_NODE) {
|
||||
// TODO: Add support for hydration
|
||||
lastNodeWasCreated(true);
|
||||
return createNodeWithoutHydration(lView, textOrName, nodeType);
|
||||
}
|
||||
|
||||
export function enableLocateOrCreateI18nNodeImpl() {
|
||||
_locateOrCreateNode = locateOrCreateNodeImpl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply `I18nCreateOpCodes` op-codes as stored in `TI18n.create`.
|
||||
@@ -102,13 +135,15 @@ export function applyCreateOpCodes(
|
||||
(opCode & I18nCreateOpCode.APPEND_EAGERLY) === I18nCreateOpCode.APPEND_EAGERLY;
|
||||
const index = opCode >>> I18nCreateOpCode.SHIFT;
|
||||
let rNode = lView[index];
|
||||
let lastNodeWasCreated = false;
|
||||
if (rNode === null) {
|
||||
// We only create new DOM nodes if they don't already exist: If ICU switches case back to a
|
||||
// case which was already instantiated, no need to create new DOM nodes.
|
||||
rNode = lView[index] =
|
||||
isComment ? renderer.createComment(text) : createTextNode(renderer, text);
|
||||
_locateOrCreateNode(lView, index, text, isComment ? Node.COMMENT_NODE : Node.TEXT_NODE);
|
||||
lastNodeWasCreated = wasLastNodeCreated();
|
||||
}
|
||||
if (appendNow && parentRNode !== null) {
|
||||
if (appendNow && parentRNode !== null && lastNodeWasCreated) {
|
||||
nativeInsertBefore(renderer, parentRNode, rNode, insertInFrontOf, false);
|
||||
}
|
||||
}
|
||||
@@ -141,7 +176,7 @@ export function applyMutableOpCodes(
|
||||
if (lView[textNodeIndex] === null) {
|
||||
ngDevMode && ngDevMode.rendererCreateTextNode++;
|
||||
ngDevMode && assertIndexInRange(lView, textNodeIndex);
|
||||
lView[textNodeIndex] = createTextNode(renderer, opCode);
|
||||
lView[textNodeIndex] = _locateOrCreateNode(lView, textNodeIndex, opCode, Node.TEXT_NODE);
|
||||
}
|
||||
} else if (typeof opCode == 'number') {
|
||||
switch (opCode & IcuCreateOpCode.MASK_INSTRUCTION) {
|
||||
@@ -219,7 +254,7 @@ export function applyMutableOpCodes(
|
||||
ngDevMode && ngDevMode.rendererCreateComment++;
|
||||
ngDevMode && assertIndexInExpandoRange(lView, commentNodeIndex);
|
||||
const commentRNode = lView[commentNodeIndex] =
|
||||
createCommentNode(renderer, commentValue);
|
||||
_locateOrCreateNode(lView, commentNodeIndex, commentValue, Node.COMMENT_NODE);
|
||||
// FIXME(misko): Attaching patch data is only needed for the root (Also add tests)
|
||||
attachPatchData(commentRNode, lView);
|
||||
}
|
||||
@@ -236,7 +271,7 @@ export function applyMutableOpCodes(
|
||||
ngDevMode && ngDevMode.rendererCreateElement++;
|
||||
ngDevMode && assertIndexInExpandoRange(lView, elementNodeIndex);
|
||||
const elementRNode = lView[elementNodeIndex] =
|
||||
createElementNode(renderer, tagName, null);
|
||||
_locateOrCreateNode(lView, elementNodeIndex, tagName, Node.ELEMENT_NODE);
|
||||
// FIXME(misko): Attaching patch data is only needed for the root (Also add tests)
|
||||
attachPatchData(elementRNode, lView);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import {CharCode} from '../../util/char_code';
|
||||
import {loadIcuContainerVisitor} from '../instructions/i18n_icu_container_visitor';
|
||||
import {allocExpando, createTNodeAtIndex} from '../instructions/shared';
|
||||
import {getDocument} from '../interfaces/document';
|
||||
import {ELEMENT_MARKER, I18nCreateOpCode, I18nCreateOpCodes, I18nRemoveOpCodes, I18nUpdateOpCode, I18nUpdateOpCodes, ICU_MARKER, IcuCreateOpCode, IcuCreateOpCodes, IcuExpression, IcuType, TI18n, TIcu} from '../interfaces/i18n';
|
||||
import {ELEMENT_MARKER, I18nCreateOpCode, I18nCreateOpCodes, I18nElementNode, I18nNode, I18nNodeKind, I18nPlaceholderNode, I18nPlaceholderType, I18nRemoveOpCodes, I18nUpdateOpCode, I18nUpdateOpCodes, ICU_MARKER, IcuCreateOpCode, IcuCreateOpCodes, IcuExpression, IcuType, TI18n, TIcu} from '../interfaces/i18n';
|
||||
import {TNode, TNodeType} from '../interfaces/node';
|
||||
import {SanitizerFn} from '../interfaces/sanitization';
|
||||
import {HEADER_OFFSET, LView, TView} from '../interfaces/view';
|
||||
@@ -88,6 +88,7 @@ export function i18nStartFirstCreatePass(
|
||||
const createOpCodes: I18nCreateOpCodes = [] as any;
|
||||
const updateOpCodes: I18nUpdateOpCodes = [] as any;
|
||||
const existingTNodeStack: TNode[][] = [[]];
|
||||
const astStack: Array<Array<I18nNode>> = [[]];
|
||||
if (ngDevMode) {
|
||||
attachDebugGetter(createOpCodes, i18nCreateOpCodesToString);
|
||||
attachDebugGetter(updateOpCodes, i18nUpdateOpCodesToString);
|
||||
@@ -108,7 +109,8 @@ export function i18nStartFirstCreatePass(
|
||||
ngDevMode && assertString(text, 'Parsed ICU part should be string');
|
||||
if (text !== '') {
|
||||
i18nStartFirstCreatePassProcessTextNode(
|
||||
tView, rootTNode, existingTNodeStack[0], createOpCodes, updateOpCodes, lView, text);
|
||||
astStack[0], tView, rootTNode, existingTNodeStack[0], createOpCodes, updateOpCodes,
|
||||
lView, text);
|
||||
}
|
||||
} else {
|
||||
// `j` is Even therefor `part` is an `ICUExpression`
|
||||
@@ -129,7 +131,9 @@ export function i18nStartFirstCreatePass(
|
||||
ngDevMode &&
|
||||
assertGreaterThanOrEqual(
|
||||
icuNodeIndex, HEADER_OFFSET, 'Index must be in absolute LView offset');
|
||||
icuStart(tView, lView, updateOpCodes, parentTNodeIndex, icuExpression, icuNodeIndex);
|
||||
icuStart(
|
||||
astStack[0], tView, lView, updateOpCodes, parentTNodeIndex, icuExpression,
|
||||
icuNodeIndex);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -141,11 +145,22 @@ export function i18nStartFirstCreatePass(
|
||||
const index = HEADER_OFFSET + Number.parseInt(value.substring((isClosing ? 2 : 1)));
|
||||
if (isClosing) {
|
||||
existingTNodeStack.shift();
|
||||
astStack.shift();
|
||||
setCurrentTNode(getCurrentParentTNode()!, false);
|
||||
} else {
|
||||
const tNode = createTNodePlaceholder(tView, existingTNodeStack[0], index);
|
||||
existingTNodeStack.unshift([]);
|
||||
setCurrentTNode(tNode, true);
|
||||
|
||||
const placeholderNode: I18nPlaceholderNode = {
|
||||
kind: I18nNodeKind.PLACEHOLDER,
|
||||
index,
|
||||
children: [],
|
||||
type: type === CharCode.HASH ? I18nPlaceholderType.ELEMENT :
|
||||
I18nPlaceholderType.SUBTEMPLATE,
|
||||
};
|
||||
astStack[0].push(placeholderNode);
|
||||
astStack.unshift(placeholderNode.children);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -153,6 +168,7 @@ export function i18nStartFirstCreatePass(
|
||||
tView.data[index] = <TI18n>{
|
||||
create: createOpCodes,
|
||||
update: updateOpCodes,
|
||||
ast: astStack[0],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -229,14 +245,17 @@ function createTNodeAndAddOpCode(
|
||||
* @param text The translated text (which may contain binding)
|
||||
*/
|
||||
function i18nStartFirstCreatePassProcessTextNode(
|
||||
tView: TView, rootTNode: TNode|null, existingTNodes: TNode[], createOpCodes: I18nCreateOpCodes,
|
||||
updateOpCodes: I18nUpdateOpCodes, lView: LView, text: string): void {
|
||||
ast: I18nNode[], tView: TView, rootTNode: TNode|null, existingTNodes: TNode[],
|
||||
createOpCodes: I18nCreateOpCodes, updateOpCodes: I18nUpdateOpCodes, lView: LView,
|
||||
text: string): void {
|
||||
const hasBinding = text.match(BINDING_REGEXP);
|
||||
const tNode = createTNodeAndAddOpCode(
|
||||
tView, rootTNode, existingTNodes, lView, createOpCodes, hasBinding ? null : text, false);
|
||||
const index = tNode.index;
|
||||
if (hasBinding) {
|
||||
generateBindingUpdateOpCodes(updateOpCodes, text, tNode.index, null, 0, null);
|
||||
generateBindingUpdateOpCodes(updateOpCodes, text, index, null, 0, null);
|
||||
}
|
||||
ast.push({kind: I18nNodeKind.TEXT, index});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -366,7 +385,7 @@ function toMaskBit(bindingIndex: number): number {
|
||||
return 1 << Math.min(bindingIndex, 31);
|
||||
}
|
||||
|
||||
export function isRootTemplateMessage(subTemplateIndex: number): subTemplateIndex is - 1 {
|
||||
function isRootTemplateMessage(subTemplateIndex: number): subTemplateIndex is - 1 {
|
||||
return subTemplateIndex === -1;
|
||||
}
|
||||
|
||||
@@ -441,9 +460,9 @@ export function getTranslationForTemplate(message: string, subTemplateIndex: num
|
||||
* - `lView[anchorIdx]` points to a `Comment` node representing the anchor for the ICU.
|
||||
* - `tView.data[anchorIdx]` points to the `TIcuContainerNode` if ICU is root (`null` otherwise)
|
||||
*/
|
||||
export function icuStart(
|
||||
tView: TView, lView: LView, updateOpCodes: I18nUpdateOpCodes, parentIdx: number,
|
||||
icuExpression: IcuExpression, anchorIdx: number) {
|
||||
function icuStart(
|
||||
ast: I18nNode[], tView: TView, lView: LView, updateOpCodes: I18nUpdateOpCodes,
|
||||
parentIdx: number, icuExpression: IcuExpression, anchorIdx: number) {
|
||||
ngDevMode && assertDefined(icuExpression, 'ICU expression must be defined');
|
||||
let bindingMask = 0;
|
||||
const tIcu: TIcu = {
|
||||
@@ -458,6 +477,7 @@ export function icuStart(
|
||||
addUpdateIcuSwitch(updateOpCodes, icuExpression, anchorIdx);
|
||||
setTIcu(tView, anchorIdx, tIcu);
|
||||
const values = icuExpression.values;
|
||||
const cases: I18nNode[][] = [];
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
// Each value is an array of strings & other ICU expressions
|
||||
const valueArr = values[i];
|
||||
@@ -471,14 +491,22 @@ export function icuStart(
|
||||
valueArr[j] = `<!--<2D>${icuIndex}<EFBFBD>-->`;
|
||||
}
|
||||
}
|
||||
const caseAst: I18nNode[] = [];
|
||||
cases.push(caseAst);
|
||||
bindingMask = parseIcuCase(
|
||||
tView, tIcu, lView, updateOpCodes, parentIdx, icuExpression.cases[i],
|
||||
caseAst, tView, tIcu, lView, updateOpCodes, parentIdx, icuExpression.cases[i],
|
||||
valueArr.join(''), nestedIcus) |
|
||||
bindingMask;
|
||||
}
|
||||
if (bindingMask) {
|
||||
addUpdateIcuUpdate(updateOpCodes, bindingMask, anchorIdx);
|
||||
}
|
||||
ast.push({
|
||||
kind: I18nNodeKind.ICU,
|
||||
index: anchorIdx,
|
||||
cases,
|
||||
currentCaseLViewIndex: tIcu.currentCaseLViewIndex
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -488,7 +516,7 @@ export function icuStart(
|
||||
* @param pattern Text containing an ICU expression that needs to be parsed.
|
||||
*
|
||||
*/
|
||||
export function parseICUBlock(pattern: string): IcuExpression {
|
||||
function parseICUBlock(pattern: string): IcuExpression {
|
||||
const cases = [];
|
||||
const values: (string|IcuExpression)[][] = [];
|
||||
let icuType = IcuType.plural;
|
||||
@@ -536,7 +564,7 @@ export function parseICUBlock(pattern: string): IcuExpression {
|
||||
* - odd positions: `string` => text between ICU expressions
|
||||
* - even positions: `ICUExpression` => ICU expression parsed into `ICUExpression` record.
|
||||
*/
|
||||
export function i18nParseTextIntoPartsAndICU(pattern: string): (string|IcuExpression)[] {
|
||||
function i18nParseTextIntoPartsAndICU(pattern: string): (string|IcuExpression)[] {
|
||||
if (!pattern) {
|
||||
return [];
|
||||
}
|
||||
@@ -585,9 +613,10 @@ export function i18nParseTextIntoPartsAndICU(pattern: string): (string|IcuExpres
|
||||
* Parses a node, its children and its siblings, and generates the mutate & update OpCodes.
|
||||
*
|
||||
*/
|
||||
export function parseIcuCase(
|
||||
tView: TView, tIcu: TIcu, lView: LView, updateOpCodes: I18nUpdateOpCodes, parentIdx: number,
|
||||
caseName: string, unsafeCaseHtml: string, nestedIcus: IcuExpression[]): number {
|
||||
function parseIcuCase(
|
||||
ast: I18nNode[], tView: TView, tIcu: TIcu, lView: LView, updateOpCodes: I18nUpdateOpCodes,
|
||||
parentIdx: number, caseName: string, unsafeCaseHtml: string,
|
||||
nestedIcus: IcuExpression[]): number {
|
||||
const create: IcuCreateOpCodes = [] as any;
|
||||
const remove: I18nRemoveOpCodes = [] as any;
|
||||
const update: I18nUpdateOpCodes = [] as any;
|
||||
@@ -607,7 +636,7 @@ export function parseIcuCase(
|
||||
const inertRootNode = getTemplateContent(inertBodyElement!) as Element || inertBodyElement;
|
||||
if (inertRootNode) {
|
||||
return walkIcuTree(
|
||||
tView, tIcu, lView, updateOpCodes, create, remove, update, inertRootNode, parentIdx,
|
||||
ast, tView, tIcu, lView, updateOpCodes, create, remove, update, inertRootNode, parentIdx,
|
||||
nestedIcus, 0);
|
||||
} else {
|
||||
return 0;
|
||||
@@ -615,7 +644,7 @@ export function parseIcuCase(
|
||||
}
|
||||
|
||||
function walkIcuTree(
|
||||
tView: TView, tIcu: TIcu, lView: LView, sharedUpdateOpCodes: I18nUpdateOpCodes,
|
||||
ast: I18nNode[], tView: TView, tIcu: TIcu, lView: LView, sharedUpdateOpCodes: I18nUpdateOpCodes,
|
||||
create: IcuCreateOpCodes, remove: I18nRemoveOpCodes, update: I18nUpdateOpCodes,
|
||||
parentNode: Element, parentIdx: number, nestedIcus: IcuExpression[], depth: number): number {
|
||||
let bindingMask = 0;
|
||||
@@ -654,10 +683,17 @@ function walkIcuTree(
|
||||
addCreateAttribute(create, newIndex, attr);
|
||||
}
|
||||
}
|
||||
const elementNode: I18nElementNode = {
|
||||
kind: I18nNodeKind.ELEMENT,
|
||||
index: newIndex,
|
||||
children: [],
|
||||
};
|
||||
ast.push(elementNode);
|
||||
// Parse the children of this node (if any)
|
||||
bindingMask = walkIcuTree(
|
||||
tView, tIcu, lView, sharedUpdateOpCodes, create, remove, update,
|
||||
currentNode as Element, newIndex, nestedIcus, depth + 1) |
|
||||
bindingMask =
|
||||
walkIcuTree(
|
||||
elementNode.children, tView, tIcu, lView, sharedUpdateOpCodes, create, remove,
|
||||
update, currentNode as Element, newIndex, nestedIcus, depth + 1) |
|
||||
bindingMask;
|
||||
addRemoveNode(remove, newIndex, depth);
|
||||
}
|
||||
@@ -671,6 +707,10 @@ function walkIcuTree(
|
||||
bindingMask =
|
||||
generateBindingUpdateOpCodes(update, value, newIndex, null, 0, null) | bindingMask;
|
||||
}
|
||||
ast.push({
|
||||
kind: I18nNodeKind.TEXT,
|
||||
index: newIndex,
|
||||
});
|
||||
break;
|
||||
case Node.COMMENT_NODE:
|
||||
// Check if the comment node is a placeholder for a nested ICU
|
||||
@@ -682,7 +722,7 @@ function walkIcuTree(
|
||||
addCreateNodeAndAppend(
|
||||
create, ICU_MARKER, ngDevMode ? `nested ICU ${nestedIcuIndex}` : '', parentIdx,
|
||||
newIndex);
|
||||
icuStart(tView, lView, sharedUpdateOpCodes, parentIdx, icuExpression, newIndex);
|
||||
icuStart(ast, tView, lView, sharedUpdateOpCodes, parentIdx, icuExpression, newIndex);
|
||||
addRemoveNestedIcu(remove, newIndex, depth);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -11,7 +11,7 @@ import {setActiveConsumer} from '@angular/core/primitives/signals';
|
||||
import {TrackByFunction} from '../../change_detection';
|
||||
import {DehydratedContainerView} from '../../hydration/interfaces';
|
||||
import {findMatchingDehydratedView} from '../../hydration/views';
|
||||
import {assertDefined} from '../../util/assert';
|
||||
import {assertDefined, assertFunction} from '../../util/assert';
|
||||
import {performanceMarkFeature} from '../../util/performance';
|
||||
import {assertLContainer, assertLView, assertTNode} from '../assert';
|
||||
import {bindingUpdated} from '../bindings';
|
||||
@@ -150,6 +150,11 @@ export function ɵɵrepeaterCreate(
|
||||
emptyDecls?: number, emptyVars?: number, emptyTagName?: string|null,
|
||||
emptyAttrsIndex?: number|null): void {
|
||||
performanceMarkFeature('NgControlFlow');
|
||||
|
||||
ngDevMode &&
|
||||
assertFunction(
|
||||
trackByFn, `A track expression must be a function, was ${typeof trackByFn} instead.`);
|
||||
|
||||
const hasEmptyBlock = emptyTemplateFn !== undefined;
|
||||
const hostLView = getLView();
|
||||
const boundTrackBy = trackByUsesComponentInstance ?
|
||||
|
||||
@@ -10,6 +10,7 @@ import {invalidSkipHydrationHost, validateMatchingNode, validateNodeExists} from
|
||||
import {locateNextRNode} from '../../hydration/node_lookup_utils';
|
||||
import {hasSkipHydrationAttrOnRElement, hasSkipHydrationAttrOnTNode} from '../../hydration/skip_hydration';
|
||||
import {getSerializedContainerViews, isDisconnectedNode, markRNodeAsClaimedByHydration, markRNodeAsSkippedByHydration, setSegmentHead} from '../../hydration/utils';
|
||||
import {isDetachedByI18n} from '../../i18n/utils';
|
||||
import {assertDefined, assertEqual, assertIndexInRange} from '../../util/assert';
|
||||
import {assertFirstCreatePass, assertHasParent} from '../assert';
|
||||
import {attachPatchData} from '../context_discovery';
|
||||
@@ -102,7 +103,7 @@ export function ɵɵelementStart(
|
||||
setCurrentTNode(tNode, true);
|
||||
setupStaticAttributes(renderer, native, tNode);
|
||||
|
||||
if ((tNode.flags & TNodeFlags.isDetached) !== TNodeFlags.isDetached && wasLastNodeCreated()) {
|
||||
if (!isDetachedByI18n(tNode) && wasLastNodeCreated()) {
|
||||
// In the i18n case, the translation may have removed this element, so only add it if it is not
|
||||
// detached. See `TNodeType.Placeholder` and `LFrame.inI18n` for more context.
|
||||
appendChild(tView, lView, native, tNode);
|
||||
@@ -203,8 +204,8 @@ function locateOrCreateElementNodeImpl(
|
||||
tView: TView, lView: LView, tNode: TNode, renderer: Renderer, name: string,
|
||||
index: number): RElement {
|
||||
const hydrationInfo = lView[HYDRATION];
|
||||
const isNodeCreationMode =
|
||||
!hydrationInfo || isInSkipHydrationBlock() || isDisconnectedNode(hydrationInfo, index);
|
||||
const isNodeCreationMode = !hydrationInfo || isInSkipHydrationBlock() ||
|
||||
isDetachedByI18n(tNode) || isDisconnectedNode(hydrationInfo, index);
|
||||
lastNodeWasCreated(isNodeCreationMode);
|
||||
|
||||
// Regular creation mode.
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import {validateMatchingNode, validateNodeExists} from '../../hydration/error_handling';
|
||||
import {locateNextRNode, siblingAfter} from '../../hydration/node_lookup_utils';
|
||||
import {getNgContainerSize, markRNodeAsClaimedByHydration, setSegmentHead} from '../../hydration/utils';
|
||||
import {isDetachedByI18n} from '../../i18n/utils';
|
||||
import {assertEqual, assertIndexInRange, assertNumber} from '../../util/assert';
|
||||
import {assertHasParent} from '../assert';
|
||||
import {attachPatchData} from '../context_discovery';
|
||||
@@ -164,7 +165,7 @@ function locateOrCreateElementContainerNode(
|
||||
tView: TView, lView: LView, tNode: TNode, index: number): RComment {
|
||||
let comment: RComment;
|
||||
const hydrationInfo = lView[HYDRATION];
|
||||
const isNodeCreationMode = !hydrationInfo || isInSkipHydrationBlock();
|
||||
const isNodeCreationMode = !hydrationInfo || isInSkipHydrationBlock() || isDetachedByI18n(tNode);
|
||||
|
||||
lastNodeWasCreated(isNodeCreationMode);
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {validateMatchingNode, validateNodeExists} from '../../hydration/error_ha
|
||||
import {TEMPLATES} from '../../hydration/interfaces';
|
||||
import {locateNextRNode, siblingAfter} from '../../hydration/node_lookup_utils';
|
||||
import {calcSerializedContainerSize, isDisconnectedNode, markRNodeAsClaimedByHydration, setSegmentHead} from '../../hydration/utils';
|
||||
import {isDetachedByI18n} from '../../i18n/utils';
|
||||
import {populateDehydratedViewsInLContainer} from '../../linker/view_container_ref';
|
||||
import {assertEqual} from '../../util/assert';
|
||||
import {assertFirstCreatePass} from '../assert';
|
||||
@@ -132,8 +133,8 @@ function createContainerAnchorImpl(
|
||||
function locateOrCreateContainerAnchorImpl(
|
||||
tView: TView, lView: LView, tNode: TNode, index: number): RComment {
|
||||
const hydrationInfo = lView[HYDRATION];
|
||||
const isNodeCreationMode =
|
||||
!hydrationInfo || isInSkipHydrationBlock() || isDisconnectedNode(hydrationInfo, index);
|
||||
const isNodeCreationMode = !hydrationInfo || isInSkipHydrationBlock() ||
|
||||
isDetachedByI18n(tNode) || isDisconnectedNode(hydrationInfo, index);
|
||||
lastNodeWasCreated(isNodeCreationMode);
|
||||
|
||||
// Regular creation mode.
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import {validateMatchingNode} from '../../hydration/error_handling';
|
||||
import {locateNextRNode} from '../../hydration/node_lookup_utils';
|
||||
import {isDisconnectedNode, markRNodeAsClaimedByHydration} from '../../hydration/utils';
|
||||
import {isDetachedByI18n} from '../../i18n/utils';
|
||||
import {assertEqual, assertIndexInRange} from '../../util/assert';
|
||||
import {TElementNode, TNode, TNodeType} from '../interfaces/node';
|
||||
import {RText} from '../interfaces/renderer_dom';
|
||||
@@ -66,8 +67,8 @@ let _locateOrCreateTextNode: typeof locateOrCreateTextNodeImpl =
|
||||
function locateOrCreateTextNodeImpl(
|
||||
tView: TView, lView: LView, tNode: TNode, value: string, index: number): RText {
|
||||
const hydrationInfo = lView[HYDRATION];
|
||||
const isNodeCreationMode =
|
||||
!hydrationInfo || isInSkipHydrationBlock() || isDisconnectedNode(hydrationInfo, index);
|
||||
const isNodeCreationMode = !hydrationInfo || isInSkipHydrationBlock() ||
|
||||
isDetachedByI18n(tNode) || isDisconnectedNode(hydrationInfo, index);
|
||||
lastNodeWasCreated(isNodeCreationMode);
|
||||
|
||||
// Regular creation mode.
|
||||
|
||||
@@ -341,6 +341,12 @@ export interface TI18n {
|
||||
* DOM are required.
|
||||
*/
|
||||
update: I18nUpdateOpCodes;
|
||||
|
||||
/**
|
||||
* An AST representing the translated message. This is used for hydration (and serialization),
|
||||
* while the Update and Create OpCodes are used at runtime.
|
||||
*/
|
||||
ast: Array<I18nNode>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -408,3 +414,79 @@ export interface IcuExpression {
|
||||
cases: string[];
|
||||
values: (string|IcuExpression)[][];
|
||||
}
|
||||
|
||||
// A parsed I18n AST Node
|
||||
export type I18nNode = I18nTextNode|I18nElementNode|I18nICUNode|I18nPlaceholderNode;
|
||||
|
||||
/**
|
||||
* Represents a block of text in a translation, such as `Hello, {{ name }}!`.
|
||||
*/
|
||||
export interface I18nTextNode {
|
||||
/** The AST node kind */
|
||||
kind: I18nNodeKind.TEXT;
|
||||
|
||||
/** The LView index */
|
||||
index: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a simple DOM element in a translation, such as `<div>...</div>`
|
||||
*/
|
||||
export interface I18nElementNode {
|
||||
/** The AST node kind */
|
||||
kind: I18nNodeKind.ELEMENT;
|
||||
|
||||
/** The LView index */
|
||||
index: number;
|
||||
|
||||
/** The child nodes */
|
||||
children: Array<I18nNode>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an ICU in a translation.
|
||||
*/
|
||||
export interface I18nICUNode {
|
||||
/** The AST node kind */
|
||||
kind: I18nNodeKind.ICU;
|
||||
|
||||
/** The LView index */
|
||||
index: number;
|
||||
|
||||
/** The branching cases */
|
||||
cases: Array<Array<I18nNode>>;
|
||||
|
||||
/** The LView index that stores the active case */
|
||||
currentCaseLViewIndex: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents special content that is embedded into the translation. This can
|
||||
* either be a special built-in element, such as <ng-container> and <ng-content>,
|
||||
* or it can be a sub-template, for example, from a structural directive.
|
||||
*/
|
||||
export interface I18nPlaceholderNode {
|
||||
/** The AST node kind */
|
||||
kind: I18nNodeKind.PLACEHOLDER;
|
||||
|
||||
/** The LView index */
|
||||
index: number;
|
||||
|
||||
/** The child nodes */
|
||||
children: Array<I18nNode>;
|
||||
|
||||
/** The placeholder type */
|
||||
type: I18nPlaceholderType;
|
||||
}
|
||||
|
||||
export const enum I18nPlaceholderType {
|
||||
ELEMENT,
|
||||
SUBTEMPLATE,
|
||||
}
|
||||
|
||||
export const enum I18nNodeKind {
|
||||
TEXT,
|
||||
ELEMENT,
|
||||
PLACEHOLDER,
|
||||
ICU,
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import {consumerDestroy, getActiveConsumer, setActiveConsumer} from '@angular/core/primitives/signals';
|
||||
|
||||
import {NotificationType} from '../change_detection/scheduling/zoneless_scheduling';
|
||||
import {hasInSkipHydrationBlockFlag} from '../hydration/skip_hydration';
|
||||
import {ViewEncapsulation} from '../metadata/view';
|
||||
import {RendererStyleFlags2} from '../render/api_flags';
|
||||
@@ -173,6 +174,10 @@ export function addViewToDOM(
|
||||
* @param lView the `LView` to be detached.
|
||||
*/
|
||||
export function detachViewFromDOM(tView: TView, lView: LView) {
|
||||
// When we remove a view from the DOM, we need to rerun afterRender hooks
|
||||
// We don't necessarily needs to run change detection. DOM removal only requires
|
||||
// change detection if animations are enabled (this notification is handled by animations).
|
||||
lView[ENVIRONMENT].changeDetectionScheduler?.notify(NotificationType.AfterRenderHooks);
|
||||
applyView(tView, lView, lView[RENDERER], WalkTNodeTreeAction.Detach, null, null);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,57 +10,53 @@ import '../util/ng_dev_mode';
|
||||
|
||||
import {assertDefined, assertEqual, assertNotEqual} from '../util/assert';
|
||||
|
||||
import {AttributeMarker} from './interfaces/attribute_marker';
|
||||
import {TAttributes, TNode, TNodeType} from './interfaces/node';
|
||||
import {CssSelector, CssSelectorList, SelectorFlags} from './interfaces/projection';
|
||||
import {classIndexOf} from './styling/class_differ';
|
||||
import {isNameOnlyAttributeMarker} from './util/attrs_utils';
|
||||
import { AttributeMarker } from './interfaces/attribute_marker';
|
||||
|
||||
const NG_TEMPLATE_SELECTOR = 'ng-template';
|
||||
|
||||
/**
|
||||
* Search the `TAttributes` to see if it contains `cssClassToMatch` (case insensitive)
|
||||
*
|
||||
* @param tNode static data of the node to match
|
||||
* @param attrs `TAttributes` to search through.
|
||||
* @param cssClassToMatch class to match (lowercase)
|
||||
* @param isProjectionMode Whether or not class matching should look into the attribute `class` in
|
||||
* addition to the `AttributeMarker.Classes`.
|
||||
*/
|
||||
function isCssClassMatching(
|
||||
attrs: TAttributes, cssClassToMatch: string, isProjectionMode: boolean): boolean {
|
||||
// TODO(misko): The fact that this function needs to know about `isProjectionMode` seems suspect.
|
||||
// It is strange to me that sometimes the class information comes in form of `class` attribute
|
||||
// and sometimes in form of `AttributeMarker.Classes`. Some investigation is needed to determine
|
||||
// if that is the right behavior.
|
||||
tNode: TNode, attrs: TAttributes, cssClassToMatch: string, isProjectionMode: boolean): boolean {
|
||||
ngDevMode &&
|
||||
assertEqual(
|
||||
cssClassToMatch, cssClassToMatch.toLowerCase(), 'Class name expected to be lowercase.');
|
||||
let i = 0;
|
||||
// Indicates whether we are processing value from the implicit
|
||||
// attribute section (i.e. before the first marker in the array).
|
||||
let isImplicitAttrsSection = true;
|
||||
while (i < attrs.length) {
|
||||
let item = attrs[i++];
|
||||
if (typeof item === 'string' && isImplicitAttrsSection) {
|
||||
const value = attrs[i++] as string;
|
||||
if (isProjectionMode && item === 'class') {
|
||||
// We found a `class` attribute in the implicit attribute section,
|
||||
// check if it matches the value of the `cssClassToMatch` argument.
|
||||
if (classIndexOf(value.toLowerCase(), cssClassToMatch, 0) !== -1) {
|
||||
return true;
|
||||
}
|
||||
if (isProjectionMode) {
|
||||
for (; i < attrs.length && typeof attrs[i] === 'string'; i += 2) {
|
||||
// Search for an implicit `class` attribute and check if its value matches `cssClassToMatch`.
|
||||
if (attrs[i] === 'class' &&
|
||||
classIndexOf((attrs[i + 1] as string).toLowerCase(), cssClassToMatch, 0) !== -1) {
|
||||
return true;
|
||||
}
|
||||
} else if (item === AttributeMarker.Classes) {
|
||||
// We found the classes section. Start searching for the class.
|
||||
while (i < attrs.length && typeof (item = attrs[i++]) == 'string') {
|
||||
// while we have strings
|
||||
if (item.toLowerCase() === cssClassToMatch) return true;
|
||||
}
|
||||
} else if (isInlineTemplate(tNode)) {
|
||||
// Matching directives (i.e. when not matching for projection mode) should not consider the
|
||||
// class bindings that are present on inline templates, as those class bindings only target
|
||||
// the root node of the template, not the template itself.
|
||||
return false;
|
||||
}
|
||||
|
||||
// Resume the search for classes after the `Classes` marker.
|
||||
i = attrs.indexOf(AttributeMarker.Classes, i);
|
||||
if (i > -1) {
|
||||
// We found the classes section. Start searching for the class.
|
||||
let item: TAttributes[number];
|
||||
while (++i < attrs.length && typeof (item = attrs[i]) === 'string') {
|
||||
if (item.toLowerCase() === cssClassToMatch) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} else if (typeof item === 'number') {
|
||||
// We've came across a first marker, which indicates
|
||||
// that the implicit attribute section is over.
|
||||
isImplicitAttrsSection = false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -96,7 +92,7 @@ function hasTagAndTypeMatch(
|
||||
/**
|
||||
* A utility function to match an Ivy node static data against a simple CSS selector
|
||||
*
|
||||
* @param node static data of the node to match
|
||||
* @param tNode static data of the node to match
|
||||
* @param selector The selector to try matching against the node.
|
||||
* @param isProjectionMode if `true` we are matching for content projection, otherwise we are doing
|
||||
* directive matching.
|
||||
@@ -106,10 +102,10 @@ export function isNodeMatchingSelector(
|
||||
tNode: TNode, selector: CssSelector, isProjectionMode: boolean): boolean {
|
||||
ngDevMode && assertDefined(selector[0], 'Selector should have a tag name');
|
||||
let mode: SelectorFlags = SelectorFlags.ELEMENT;
|
||||
const nodeAttrs = tNode.attrs || [];
|
||||
const nodeAttrs = tNode.attrs;
|
||||
|
||||
// Find the index of first attribute that has no value, only a name.
|
||||
const nameOnlyMarkerIdx = getNameOnlyMarkerIndex(nodeAttrs);
|
||||
const nameOnlyMarkerIdx = nodeAttrs !== null ? getNameOnlyMarkerIndex(nodeAttrs) : 0;
|
||||
|
||||
// When processing ":not" selectors, we skip to the next ":not" if the
|
||||
// current one doesn't match
|
||||
@@ -139,22 +135,15 @@ export function isNodeMatchingSelector(
|
||||
if (isPositive(mode)) return false;
|
||||
skipToNextSelector = true;
|
||||
}
|
||||
} else {
|
||||
const selectorAttrValue = mode & SelectorFlags.CLASS ? current : selector[++i];
|
||||
|
||||
// special case for matching against classes when a tNode has been instantiated with
|
||||
// class and style values as separate attribute values (e.g. ['title', CLASS, 'foo'])
|
||||
if ((mode & SelectorFlags.CLASS) && tNode.attrs !== null) {
|
||||
if (!isCssClassMatching(tNode.attrs, selectorAttrValue as string, isProjectionMode)) {
|
||||
if (isPositive(mode)) return false;
|
||||
skipToNextSelector = true;
|
||||
}
|
||||
continue;
|
||||
} else if (mode & SelectorFlags.CLASS) {
|
||||
if (nodeAttrs === null || !isCssClassMatching(tNode, nodeAttrs, current, isProjectionMode)) {
|
||||
if (isPositive(mode)) return false;
|
||||
skipToNextSelector = true;
|
||||
}
|
||||
|
||||
const attrName = (mode & SelectorFlags.CLASS) ? 'class' : current;
|
||||
} else {
|
||||
const selectorAttrValue = selector[++i];
|
||||
const attrIndexInNode =
|
||||
findAttrIndexInNode(attrName, nodeAttrs, isInlineTemplate(tNode), isProjectionMode);
|
||||
findAttrIndexInNode(current, nodeAttrs, isInlineTemplate(tNode), isProjectionMode);
|
||||
|
||||
if (attrIndexInNode === -1) {
|
||||
if (isPositive(mode)) return false;
|
||||
@@ -169,18 +158,15 @@ export function isNodeMatchingSelector(
|
||||
} else {
|
||||
ngDevMode &&
|
||||
assertNotEqual(
|
||||
nodeAttrs[attrIndexInNode], AttributeMarker.NamespaceURI,
|
||||
nodeAttrs![attrIndexInNode], AttributeMarker.NamespaceURI,
|
||||
'We do not match directives on namespaced attributes');
|
||||
// we lowercase the attribute value to be able to match
|
||||
// selectors without case-sensitivity
|
||||
// (selectors are already in lowercase when generated)
|
||||
nodeAttrValue = (nodeAttrs[attrIndexInNode + 1] as string).toLowerCase();
|
||||
nodeAttrValue = (nodeAttrs![attrIndexInNode + 1] as string).toLowerCase();
|
||||
}
|
||||
|
||||
const compareAgainstClassName = mode & SelectorFlags.CLASS ? nodeAttrValue : null;
|
||||
if (compareAgainstClassName &&
|
||||
classIndexOf(compareAgainstClassName, selectorAttrValue as string, 0) !== -1 ||
|
||||
mode & SelectorFlags.ATTRIBUTE && selectorAttrValue !== nodeAttrValue) {
|
||||
if (mode & SelectorFlags.ATTRIBUTE && selectorAttrValue !== nodeAttrValue) {
|
||||
if (isPositive(mode)) return false;
|
||||
skipToNextSelector = true;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,5 @@ export function queueStateUpdate(callback: VoidFunction, options?: {injector?: I
|
||||
};
|
||||
|
||||
internalAfterNextRender(runCallbackOnce, {injector, runOnServer: true});
|
||||
queueMicrotask(() => {
|
||||
runCallbackOnce();
|
||||
});
|
||||
queueMicrotask(runCallbackOnce);
|
||||
}
|
||||
|
||||