Compare commits
155 Commits
10.1.0-nex
...
traducir-b
Author | SHA1 | Date | |
---|---|---|---|
a9920d6776 | |||
71b1f1ad88 | |||
a198e2984c | |||
e160d8466c | |||
575479a6a1 | |||
8b97de61f5 | |||
d870e5c309 | |||
2da56a70a2 | |||
0b2f134680 | |||
5d32a199a9 | |||
f677c22fba | |||
97ae2d3b9b | |||
a3fe2de883 | |||
7f6c3b3fc1 | |||
ab4ab682e5 | |||
a69cb738a8 | |||
e9eeee5608 | |||
d89ab70036 | |||
fa82a82a07 | |||
39f92d985b | |||
da0129b83e | |||
c980caecac | |||
b88abd81c8 | |||
201c38e407 | |||
31c53670e7 | |||
e169f00193 | |||
e1ee231993 | |||
7d4aebd603 | |||
2cde8da71c | |||
6f54c28c87 | |||
17841959f5 | |||
cfe5fadb3a | |||
3cd4a5ded5 | |||
36156244a1 | |||
a66a804082 | |||
d2524eef7f | |||
f6d2af17fc | |||
a86754b4c4 | |||
2968435f81 | |||
4096bbfeee | |||
0314fd4b6e | |||
3f2257f209 | |||
271e0f9896 | |||
79b5e1891d | |||
b4b453d6de | |||
06f085560c | |||
38fb735551 | |||
b0ecee8155 | |||
ba46a0c05b | |||
0112164c34 | |||
f60fafbb2e | |||
004079b07a | |||
523a1a71d7 | |||
d3999d904a | |||
63894062d4 | |||
5c60a65eaf | |||
c387933d3c | |||
882336e0e6 | |||
e91aa6db32 | |||
325b3fd0db | |||
84348299b3 | |||
e934864a90 | |||
920205bc61 | |||
0dcc34837e | |||
57d8d7fcd4 | |||
f26f014ffc | |||
134aa72467 | |||
a7a8938291 | |||
9338556872 | |||
28ef4bc441 | |||
686f9ae4b2 | |||
a57119761b | |||
6881149540 | |||
4f842e23df | |||
47873a339a | |||
03e02185d9 | |||
e565d97bc8 | |||
6bf8d2b356 | |||
2981656ebe | |||
5ec0ba72cd | |||
f020941629 | |||
f1a92872a4 | |||
9e7ce2c916 | |||
788532dc99 | |||
54679ea9cc | |||
cb0aeaf708 | |||
8de04115ec | |||
e65e5ea9d0 | |||
19b577bf66 | |||
ccb0b42ffb | |||
1d46ef38d2 | |||
06356d806a | |||
45847c62ce | |||
9cb6bb8535 | |||
e9b25a1e98 | |||
ad51b996a2 | |||
8a05f311b9 | |||
e72267bc00 | |||
2e0973a814 | |||
27092a822a | |||
0cd8e48c36 | |||
2b95289ee2 | |||
5df5ce6463 | |||
04ad0077f4 | |||
1bf2f118c7 | |||
2119abf954 | |||
fc728d0cfb | |||
dd01edd168 | |||
7c3edad3d4 | |||
445ac15a78 | |||
856db56cca | |||
354e66efad | |||
702958e968 | |||
df7f3b04b5 | |||
7525f3afc1 | |||
570d156ce4 | |||
26be5b4994 | |||
3821dc5f6c | |||
18cd1a9937 | |||
0551fbdf88 | |||
dca4443a8e | |||
2a745c8df8 | |||
a18f82b458 | |||
696a9b01ef | |||
d7c043ba35 | |||
0c2490368e | |||
bb88c9fa3d | |||
8227b56f9e | |||
1609815743 | |||
c4d76a65da | |||
763023472b | |||
ba175be41f | |||
f0766a4474 | |||
6f0f0d3ea2 | |||
576e329f33 | |||
585ef2bdd7 | |||
db394e2b81 | |||
92c7145139 | |||
d133525502 | |||
cd1817bf6d | |||
0d1491b1a6 | |||
f67774d84b | |||
37668d159e | |||
38554288f5 | |||
04dcc3bceb | |||
0910a2fc0d | |||
3325cb2f86 | |||
a1e8443bfb | |||
599d34b41e | |||
7fd1733882 | |||
2e42123870 | |||
4cbb90daf7 | |||
cf2663b034 | |||
95681b16bd | |||
e620827fa8 |
@ -32,8 +32,8 @@ var_4_win: &cache_key_win_fallback v7-angular-win-node-12-{{ checksum ".bazelver
|
||||
|
||||
# Cache key for the `components-repo-unit-tests` job. **Note** when updating the SHA in the
|
||||
# cache keys also update the SHA for the "COMPONENTS_REPO_COMMIT" environment variable.
|
||||
var_5: &components_repo_unit_tests_cache_key v7-angular-components-f428c00465dfcf8a020237f22532480eedbd2cb6
|
||||
var_6: &components_repo_unit_tests_cache_key_fallback v7-angular-components-
|
||||
var_5: &components_repo_unit_tests_cache_key v9-angular-components-09e68db8ed5b1253f2fe38ff954ef0df019fc25a
|
||||
var_6: &components_repo_unit_tests_cache_key_fallback v9-angular-components-
|
||||
|
||||
# Workspace initially persisted by the `setup` job, and then enhanced by `build-npm-packages` and
|
||||
# `build-ivy-npm-packages`.
|
||||
|
@ -74,7 +74,7 @@ setPublicVar COMPONENTS_REPO_TMP_DIR "/tmp/angular-components-repo"
|
||||
setPublicVar COMPONENTS_REPO_URL "https://github.com/angular/components.git"
|
||||
setPublicVar COMPONENTS_REPO_BRANCH "master"
|
||||
# **NOTE**: When updating the commit SHA, also update the cache key in the CircleCI `config.yml`.
|
||||
setPublicVar COMPONENTS_REPO_COMMIT "f428c00465dfcf8a020237f22532480eedbd2cb6"
|
||||
setPublicVar COMPONENTS_REPO_COMMIT "09e68db8ed5b1253f2fe38ff954ef0df019fc25a"
|
||||
|
||||
|
||||
####################################################################################################
|
||||
|
69
.github/ISSUE_TEMPLATE/1-bug-report.md
vendored
69
.github/ISSUE_TEMPLATE/1-bug-report.md
vendored
@ -1,69 +0,0 @@
|
||||
---
|
||||
name: "\U0001F41EBug report"
|
||||
about: Report a bug in the Angular Framework
|
||||
---
|
||||
<!--🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅
|
||||
|
||||
Oh hi there! 😄
|
||||
|
||||
To expedite issue processing please search open and closed issues before submitting a new one.
|
||||
Existing issues often contain information about workarounds, resolution, or progress updates.
|
||||
|
||||
🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅-->
|
||||
|
||||
|
||||
# 🐞 bug report
|
||||
|
||||
### Affected Package
|
||||
<!-- Can you pin-point one or more @angular/* packages as the source of the bug? -->
|
||||
<!-- ✍️edit: --> The issue is caused by package @angular/....
|
||||
|
||||
|
||||
### Is this a regression?
|
||||
|
||||
<!-- Did this behavior use to work in the previous version? -->
|
||||
<!-- ✍️--> Yes, the previous version in which this bug was not present was: ....
|
||||
|
||||
|
||||
### Description
|
||||
|
||||
<!-- ✍️--> A clear and concise description of the problem...
|
||||
|
||||
|
||||
## 🔬 Minimal Reproduction
|
||||
<!--
|
||||
Please create and share minimal reproduction of the issue starting with this template: https://stackblitz.com/fork/angular-ivy
|
||||
-->
|
||||
<!-- ✍️--> https://stackblitz.com/...
|
||||
|
||||
<!--
|
||||
If StackBlitz is not suitable for reproduction of your issue, please create a minimal GitHub repository with the reproduction of the issue.
|
||||
A good way to make a minimal reproduction is to create a new app via `ng new repro-app` and add the minimum possible code to show the problem.
|
||||
Share the link to the repo below along with step-by-step instructions to reproduce the problem, as well as expected and actual behavior.
|
||||
|
||||
Issues that don't have enough info and can't be reproduced will be closed.
|
||||
|
||||
You can read more about issue submission guidelines here: https://github.com/angular/angular/blob/master/CONTRIBUTING.md#-submitting-an-issue
|
||||
-->
|
||||
|
||||
## 🔥 Exception or Error
|
||||
<pre><code>
|
||||
<!-- If the issue is accompanied by an exception or an error, please share it below: -->
|
||||
<!-- ✍️-->
|
||||
|
||||
</code></pre>
|
||||
|
||||
|
||||
## 🌍 Your Environment
|
||||
|
||||
**Angular Version:**
|
||||
<pre><code>
|
||||
<!-- run `ng version` and paste output below -->
|
||||
<!-- ✍️-->
|
||||
|
||||
</code></pre>
|
||||
|
||||
**Anything else relevant?**
|
||||
<!-- ✍️Is this a browser specific issue? If so, please specify the browser and version. -->
|
||||
|
||||
<!-- ✍️Do any of these matter: operating system, IDE, package manager, HTTP server, ...? If so, please mention it below. -->
|
32
.github/ISSUE_TEMPLATE/2-feature-request.md
vendored
32
.github/ISSUE_TEMPLATE/2-feature-request.md
vendored
@ -1,32 +0,0 @@
|
||||
---
|
||||
name: "\U0001F680Feature request"
|
||||
about: Suggest a feature for Angular Framework
|
||||
|
||||
---
|
||||
<!--🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅
|
||||
|
||||
Oh hi there! 😄
|
||||
|
||||
To expedite issue processing please search open and closed issues before submitting a new one.
|
||||
Existing issues often contain information about workarounds, resolution, or progress updates.
|
||||
|
||||
🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅-->
|
||||
|
||||
|
||||
# 🚀 feature request
|
||||
|
||||
### Relevant Package
|
||||
<!-- Can you pin-point one or more @angular/* packages the are relevant for this feature request? -->
|
||||
<!-- ✍️edit: --> This feature request is for @angular/....
|
||||
|
||||
|
||||
### Description
|
||||
<!-- ✍️--> A clear and concise description of the problem or missing capability...
|
||||
|
||||
|
||||
### Describe the solution you'd like
|
||||
<!-- ✍️--> If you have a solution in mind, please describe it.
|
||||
|
||||
|
||||
### Describe alternatives you've considered
|
||||
<!-- ✍️--> Have you considered any alternative solutions or workarounds?
|
55
.github/ISSUE_TEMPLATE/3-docs-bug.md
vendored
55
.github/ISSUE_TEMPLATE/3-docs-bug.md
vendored
@ -1,55 +0,0 @@
|
||||
---
|
||||
name: "📚 Docs or angular.io issue report"
|
||||
about: Report an issue in Angular's documentation or angular.io application
|
||||
|
||||
---
|
||||
<!--🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅
|
||||
|
||||
Oh hi there! 😄
|
||||
|
||||
To expedite issue processing please search open and closed issues before submitting a new one.
|
||||
Existing issues often contain information about workarounds, resolution, or progress updates.
|
||||
|
||||
🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅-->
|
||||
|
||||
# 📚 Docs or angular.io bug report
|
||||
|
||||
### Description
|
||||
|
||||
<!-- ✍️edit:--> A clear and concise description of the problem...
|
||||
|
||||
|
||||
## 🔬 Minimal Reproduction
|
||||
|
||||
### What's the affected URL?**
|
||||
<!-- ✍️edit:--> https://angular.io/...
|
||||
|
||||
### Reproduction Steps**
|
||||
<!-- If applicable please list the steps to take to reproduce the issue -->
|
||||
<!-- ✍️edit:-->
|
||||
|
||||
### Expected vs Actual Behavior**
|
||||
<!-- If applicable please describe the difference between the expected and actual behavior after following the repro steps. -->
|
||||
<!-- ✍️edit:-->
|
||||
|
||||
|
||||
## 📷Screenshot
|
||||
<!-- Often a screenshot can help to capture the issue better than a long description. -->
|
||||
<!-- ✍️upload a screenshot:-->
|
||||
|
||||
|
||||
## 🔥 Exception or Error
|
||||
<pre><code>
|
||||
<!-- If the issue is accompanied by an exception or an error, please share it below: -->
|
||||
<!-- ✍️-->
|
||||
|
||||
</code></pre>
|
||||
|
||||
|
||||
## 🌍 Your Environment
|
||||
|
||||
### Browser info
|
||||
<!-- ✍️Is this a browser specific issue? If so, please specify the device, browser, and version. -->
|
||||
|
||||
### Anything else relevant?
|
||||
<!-- ✍️Please provide additional info if necessary. -->
|
@ -1,11 +0,0 @@
|
||||
---
|
||||
name: ⚠️ Security issue disclosure
|
||||
about: Report a security issue in Angular Framework, Material, or CLI
|
||||
|
||||
---
|
||||
|
||||
🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑
|
||||
|
||||
Please read https://angular.io/guide/security#report-issues on how to disclose security related issues.
|
||||
|
||||
🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑
|
16
.github/ISSUE_TEMPLATE/5-support-request.md
vendored
16
.github/ISSUE_TEMPLATE/5-support-request.md
vendored
@ -1,16 +0,0 @@
|
||||
---
|
||||
name: "❓Support request"
|
||||
about: Questions and requests for support
|
||||
|
||||
---
|
||||
|
||||
🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑
|
||||
|
||||
Please do not file questions or support requests on the GitHub issues tracker.
|
||||
|
||||
You can get your questions answered using other communication channels. Please see:
|
||||
https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question
|
||||
|
||||
Thank you!
|
||||
|
||||
🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑
|
13
.github/ISSUE_TEMPLATE/6-angular-cli.md
vendored
13
.github/ISSUE_TEMPLATE/6-angular-cli.md
vendored
@ -1,13 +0,0 @@
|
||||
---
|
||||
name: "\U0001F6E0️Angular CLI"
|
||||
about: Issues and feature requests for Angular CLI
|
||||
|
||||
---
|
||||
|
||||
🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑
|
||||
|
||||
Please file any Angular CLI issues at: https://github.com/angular/angular-cli/issues/new
|
||||
|
||||
For the time being, we keep Angular CLI issues in a separate repository.
|
||||
|
||||
🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑
|
13
.github/ISSUE_TEMPLATE/7-angular-components.md
vendored
13
.github/ISSUE_TEMPLATE/7-angular-components.md
vendored
@ -1,13 +0,0 @@
|
||||
---
|
||||
name: "\U0001F48EAngular Components"
|
||||
about: Issues and feature requests for Angular Components
|
||||
|
||||
---
|
||||
|
||||
🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑
|
||||
|
||||
Please file any Angular Components issues at: https://github.com/angular/components/issues/new
|
||||
|
||||
For the time being, we keep Angular Components issues in a separate repository.
|
||||
|
||||
🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑
|
22
.github/ISSUE_TEMPLATE/8-translate-docs.md
vendored
Normal file
22
.github/ISSUE_TEMPLATE/8-translate-docs.md
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
---
|
||||
name: "📚Traducir doc al español"
|
||||
about: Solicitud para traducir ciertos docs al español
|
||||
|
||||
---
|
||||
|
||||
📚Traducir: <!-- ✍️ editar: --> creating-libraries.md
|
||||
|
||||
<!--🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅
|
||||
|
||||
Traducción de la documentación oficial de Angular a español
|
||||
|
||||
🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅-->
|
||||
|
||||
|
||||
## Nombre del archivo:
|
||||
<!-- ✍️ editar: --> creating-libraries.md
|
||||
|
||||
|
||||
## Ruta donde se encuentra el archivo dentro del proyecto de Angular
|
||||
|
||||
<!-- ✍️ editar: --> https://github.com/angular-hispano/angular/blob/master/aio/content/guide/creating-libraries.md
|
62
.github/PULL_REQUEST_TEMPLATE.md
vendored
62
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,43 +1,35 @@
|
||||
## PR Checklist
|
||||
Please check if your PR fulfills the following requirements:
|
||||
## Lista de Verificación del PR
|
||||
Comprueba si tu PR cumple los siguientes requisitos:
|
||||
|
||||
- [ ] The commit message follows our guidelines: https://github.com/angular/angular/blob/master/CONTRIBUTING.md#commit
|
||||
- [ ] Tests for the changes have been added (for bug fixes / features)
|
||||
- [ ] Docs have been added / updated (for bug fixes / features)
|
||||
- [ ] El mensaje de commit conforme con [nuestras reglas](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#commit)
|
||||
- [ ] Prueba los cambios que agregaste (arreglo de bugs / funcionalidades)
|
||||
- [ ] Revisa tus traducciones o cambios de contenido
|
||||
- [ ] Consulté el [diccionario de términos](https://github.com/angular-hispano/angular/issues/9) en español
|
||||
- [ ] Se han creado dos archivos con la extensión correspondiente(.en.md para el archivo en inglés y .md para el Archivo en español)
|
||||
- [ ] PR enlazada con el issue correspondiente
|
||||
|
||||
|
||||
## PR Type
|
||||
What kind of change does this PR introduce?
|
||||
## Tipo de PR
|
||||
¿Qué tipo de cambio introduce este PR?
|
||||
|
||||
<!-- Please check the one that applies to this PR using "x". -->
|
||||
<!-- Marca con una "x" las opciones que aplican. -->
|
||||
|
||||
- [ ] Bugfix
|
||||
- [ ] Feature
|
||||
- [ ] Code style update (formatting, local variables)
|
||||
- [ ] Refactoring (no functional changes, no api changes)
|
||||
- [ ] Build related changes
|
||||
- [ ] CI related changes
|
||||
- [ ] Documentation content changes
|
||||
- [ ] angular.io application / infrastructure changes
|
||||
- [ ] Other... Please describe:
|
||||
- [ ] Funcionalidad
|
||||
- [ ] Actualizar el estilo del código (formato, variables locales)
|
||||
- [ ] Refactorización (no cambios en la funcionalidad, no cambios en el api)
|
||||
- [ ] Cambios relacionados al build
|
||||
- [ ] Cambios relacionados al CI (Integración continua)
|
||||
- [ ] Cambios en el contenido de la documentación
|
||||
- [ ] Cambios en la aplicación / infrastructura de angular.io
|
||||
- [ ] Otro... Por favor describela:
|
||||
|
||||
## ¿Cuál es el comportamiento actual?
|
||||
<!-- Describe el comportamiento actual que está modificando o vincule a un problema relevante.
|
||||
-->
|
||||
|
||||
|
||||
## What is the current behavior?
|
||||
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->
|
||||
|
||||
Issue Number: N/A
|
||||
|
||||
|
||||
## What is the new behavior?
|
||||
|
||||
|
||||
## Does this PR introduce a breaking change?
|
||||
|
||||
- [ ] Yes
|
||||
- [ ] No
|
||||
|
||||
|
||||
<!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below. -->
|
||||
|
||||
|
||||
## Other information
|
||||
## ¿Cuál es el nuevo comportamiento?
|
||||
<!--
|
||||
Ejemplo: Archivo en inglés traducido al español
|
||||
-->
|
||||
|
@ -6,7 +6,7 @@ import {CommitMessageConfig} from '../dev-infra/commit-message/config';
|
||||
export const commitMessage: CommitMessageConfig = {
|
||||
maxLineLength: 120,
|
||||
minBodyLength: 20,
|
||||
minBodyLengthTypeExcludes: ['docs'],
|
||||
minBodyLengthTypeExcludes: ['docs', 'upstream'],
|
||||
types: [
|
||||
'build',
|
||||
'ci',
|
||||
@ -18,6 +18,7 @@ export const commitMessage: CommitMessageConfig = {
|
||||
'release',
|
||||
'style',
|
||||
'test',
|
||||
'upstream',
|
||||
],
|
||||
scopes: [
|
||||
'animations',
|
||||
|
@ -882,6 +882,7 @@ groups:
|
||||
- *can-be-global-docs-approved
|
||||
- >
|
||||
contains_any_globs(files, [
|
||||
'aio/content/guide/roadmap.md',
|
||||
'aio/content/marketing/**',
|
||||
'aio/content/images/bios/**',
|
||||
'aio/content/images/marketing/**',
|
||||
|
359
CONTRIBUTING.en.md
Normal file
359
CONTRIBUTING.en.md
Normal file
@ -0,0 +1,359 @@
|
||||
# Contributing to Angular
|
||||
|
||||
We would love for you to contribute to Angular and help make it even better than it is today!
|
||||
As a contributor, here are the guidelines we would like you to follow:
|
||||
|
||||
- [Code of Conduct](#coc)
|
||||
- [Question or Problem?](#question)
|
||||
- [Issues and Bugs](#issue)
|
||||
- [Feature Requests](#feature)
|
||||
- [Submission Guidelines](#submit)
|
||||
- [Coding Rules](#rules)
|
||||
- [Commit Message Guidelines](#commit)
|
||||
- [Signing the CLA](#cla)
|
||||
|
||||
|
||||
## <a name="coc"></a> Code of Conduct
|
||||
|
||||
Help us keep Angular open and inclusive.
|
||||
Please read and follow our [Code of Conduct][coc].
|
||||
|
||||
|
||||
## <a name="question"></a> Got a Question or Problem?
|
||||
|
||||
Do not open issues for general support questions as we want to keep GitHub issues for bug reports and feature requests.
|
||||
Instead, we recommend using [Stack Overflow](https://stackoverflow.com/questions/tagged/angular) to ask support-related questions. When creating a new question on Stack Overflow, make sure to add the `angular` tag.
|
||||
|
||||
Stack Overflow is a much better place to ask questions since:
|
||||
|
||||
- there are thousands of people willing to help on Stack Overflow
|
||||
- questions and answers stay available for public viewing so your question/answer might help someone else
|
||||
- Stack Overflow's voting system assures that the best answers are prominently visible.
|
||||
|
||||
To save your and our time, we will systematically close all issues that are requests for general support and redirect people to Stack Overflow.
|
||||
|
||||
If you would like to chat about the question in real-time, you can reach out via [our gitter channel][gitter].
|
||||
|
||||
|
||||
## <a name="issue"></a> Found a Bug?
|
||||
|
||||
If you find a bug in the source code, you can help us by [submitting an issue](#submit-issue) to our [GitHub Repository][github].
|
||||
Even better, you can [submit a Pull Request](#submit-pr) with a fix.
|
||||
|
||||
|
||||
## <a name="feature"></a> Missing a Feature?
|
||||
You can *request* a new feature by [submitting an issue](#submit-issue) to our GitHub Repository.
|
||||
If you would like to *implement* a new feature, please consider the size of the change in order to determine the right steps to proceed:
|
||||
|
||||
* For a **Major Feature**, first open an issue and outline your proposal so that it can be discussed.
|
||||
This process allows us to better coordinate our efforts, prevent duplication of work, and help you to craft the change so that it is successfully accepted into the project.
|
||||
|
||||
**Note**: Adding a new topic to the documentation, or significantly re-writing a topic, counts as a major feature.
|
||||
|
||||
* **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr).
|
||||
|
||||
|
||||
## <a name="submit"></a> Submission Guidelines
|
||||
|
||||
|
||||
### <a name="submit-issue"></a> Submitting an Issue
|
||||
|
||||
Before you submit an issue, please search the issue tracker, maybe an issue for your problem already exists and the discussion might inform you of workarounds readily available.
|
||||
|
||||
We want to fix all the issues as soon as possible, but before fixing a bug we need to reproduce and confirm it.
|
||||
In order to reproduce bugs, we require that you provide a minimal reproduction.
|
||||
Having a minimal reproducible scenario gives us a wealth of important information without going back and forth to you with additional questions.
|
||||
|
||||
A minimal reproduction allows us to quickly confirm a bug (or point out a coding problem) as well as confirm that we are fixing the right problem.
|
||||
|
||||
We require a minimal reproduction to save maintainers' time and ultimately be able to fix more bugs.
|
||||
Often, developers find coding problems themselves while preparing a minimal reproduction.
|
||||
We understand that sometimes it might be hard to extract essential bits of code from a larger codebase but we really need to isolate the problem before we can fix it.
|
||||
|
||||
Unfortunately, we are not able to investigate / fix bugs without a minimal reproduction, so if we don't hear back from you, we are going to close an issue that doesn't have enough info to be reproduced.
|
||||
|
||||
You can file new issues by selecting from our [new issue templates](https://github.com/angular/angular/issues/new/choose) and filling out the issue template.
|
||||
|
||||
|
||||
### <a name="submit-pr"></a> Submitting a Pull Request (PR)
|
||||
|
||||
Before you submit your Pull Request (PR) consider the following guidelines:
|
||||
|
||||
1. Search [GitHub](https://github.com/angular/angular/pulls) for an open or closed PR that relates to your submission.
|
||||
You don't want to duplicate existing efforts.
|
||||
|
||||
2. Be sure that an issue describes the problem you're fixing, or documents the design for the feature you'd like to add.
|
||||
Discussing the design upfront helps to ensure that we're ready to accept your work.
|
||||
|
||||
3. Please sign our [Contributor License Agreement (CLA)](#cla) before sending PRs.
|
||||
We cannot accept code without a signed CLA.
|
||||
Make sure you author all contributed Git commits with email address associated with your CLA signature.
|
||||
|
||||
4. Fork the angular/angular repo.
|
||||
|
||||
5. Make your changes in a new git branch:
|
||||
|
||||
```shell
|
||||
git checkout -b my-fix-branch master
|
||||
```
|
||||
|
||||
6. Create your patch, **including appropriate test cases**.
|
||||
|
||||
7. Follow our [Coding Rules](#rules).
|
||||
|
||||
8. Run the full Angular test suite, as described in the [developer documentation][dev-doc], and ensure that all tests pass.
|
||||
|
||||
9. Commit your changes using a descriptive commit message that follows our [commit message conventions](#commit).
|
||||
Adherence to these conventions is necessary because release notes are automatically generated from these messages.
|
||||
|
||||
```shell
|
||||
git commit -a
|
||||
```
|
||||
Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files.
|
||||
|
||||
10. Push your branch to GitHub:
|
||||
|
||||
```shell
|
||||
git push origin my-fix-branch
|
||||
```
|
||||
|
||||
11. In GitHub, send a pull request to `angular:master`.
|
||||
|
||||
If we ask for changes via code reviews then:
|
||||
|
||||
* Make the required updates.
|
||||
* Re-run the Angular test suites to ensure tests are still passing.
|
||||
* Rebase your branch and force push to your GitHub repository (this will update your Pull Request):
|
||||
|
||||
```shell
|
||||
git rebase master -i
|
||||
git push -f
|
||||
```
|
||||
|
||||
That's it! Thank you for your contribution!
|
||||
|
||||
|
||||
#### After your pull request is merged
|
||||
|
||||
After your pull request is merged, you can safely delete your branch and pull the changes from the main (upstream) repository:
|
||||
|
||||
* Delete the remote branch on GitHub either through the GitHub web UI or your local shell as follows:
|
||||
|
||||
```shell
|
||||
git push origin --delete my-fix-branch
|
||||
```
|
||||
|
||||
* Check out the master branch:
|
||||
|
||||
```shell
|
||||
git checkout master -f
|
||||
```
|
||||
|
||||
* Delete the local branch:
|
||||
|
||||
```shell
|
||||
git branch -D my-fix-branch
|
||||
```
|
||||
|
||||
* Update your master with the latest upstream version:
|
||||
|
||||
```shell
|
||||
git pull --ff upstream master
|
||||
```
|
||||
|
||||
|
||||
## <a name="rules"></a> Coding Rules
|
||||
To ensure consistency throughout the source code, keep these rules in mind as you are working:
|
||||
|
||||
* All features or bug fixes **must be tested** by one or more specs (unit-tests).
|
||||
* All public API methods **must be documented**.
|
||||
* We follow [Google's JavaScript Style Guide][js-style-guide], but wrap all code at **100 characters**.
|
||||
|
||||
An automated formatter is available, see [DEVELOPER.md](docs/DEVELOPER.md#clang-format).
|
||||
|
||||
|
||||
## <a name="commit"></a> Commit Message Format
|
||||
|
||||
*This specification is inspired and supersedes the [AngularJS commit message format][commit-message-format].*
|
||||
|
||||
We have very precise rules over how our Git commit messages must be formatted.
|
||||
This format leads to **easier to read commit history**.
|
||||
|
||||
Each commit message consists of a **header**, a **body**, and a **footer**.
|
||||
|
||||
|
||||
```
|
||||
<header>
|
||||
<BLANK LINE>
|
||||
<body>
|
||||
<BLANK LINE>
|
||||
<footer>
|
||||
```
|
||||
|
||||
The `header` is mandatory and must conform to the [Commit Message Header](#commit-header) format.
|
||||
|
||||
The `body` is mandatory for all commits except for those of scope "docs".
|
||||
When the body is required it must be at least 20 characters long.
|
||||
|
||||
The `footer` is optional.
|
||||
|
||||
Any line of the commit message cannot be longer than 100 characters.
|
||||
|
||||
|
||||
#### <a href="commit-header"></a>Commit Message Header
|
||||
|
||||
```
|
||||
<type>(<scope>): <short summary>
|
||||
│ │ │
|
||||
│ │ └─⫸ Summary in present tense. Not capitalized. No period at the end.
|
||||
│ │
|
||||
│ └─⫸ Commit Scope: animations|bazel|benchpress|common|compiler|compiler-cli|core|
|
||||
│ elements|forms|http|language-service|localize|platform-browser|
|
||||
│ platform-browser-dynamic|platform-server|platform-webworker|
|
||||
│ platform-webworker-dynamic|router|service-worker|upgrade|zone.js|
|
||||
│ packaging|changelog|dev-infra|docs-infra|migrations|ngcc|ve
|
||||
│
|
||||
└─⫸ Commit Type: build|ci|docs|feat|fix|perf|refactor|style|test
|
||||
```
|
||||
|
||||
The `<type>` and `<summary>` fields are mandatory, the `(<scope>)` field is optional.
|
||||
|
||||
|
||||
##### Type
|
||||
|
||||
Must be one of the following:
|
||||
|
||||
* **build**: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
|
||||
* **ci**: Changes to our CI configuration files and scripts (example scopes: Circle, BrowserStack, SauceLabs)
|
||||
* **docs**: Documentation only changes
|
||||
* **feat**: A new feature
|
||||
* **fix**: A bug fix
|
||||
* **perf**: A code change that improves performance
|
||||
* **refactor**: A code change that neither fixes a bug nor adds a feature
|
||||
* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
|
||||
* **test**: Adding missing tests or correcting existing tests
|
||||
|
||||
|
||||
##### Scope
|
||||
The scope should be the name of the npm package affected (as perceived by the person reading the changelog generated from commit messages).
|
||||
|
||||
The following is the list of supported scopes:
|
||||
|
||||
* `animations`
|
||||
* `bazel`
|
||||
* `benchpress`
|
||||
* `common`
|
||||
* `compiler`
|
||||
* `compiler-cli`
|
||||
* `core`
|
||||
* `elements`
|
||||
* `forms`
|
||||
* `http`
|
||||
* `language-service`
|
||||
* `localize`
|
||||
* `platform-browser`
|
||||
* `platform-browser-dynamic`
|
||||
* `platform-server`
|
||||
* `platform-webworker`
|
||||
* `platform-webworker-dynamic`
|
||||
* `router`
|
||||
* `service-worker`
|
||||
* `upgrade`
|
||||
* `zone.js`
|
||||
|
||||
There are currently a few exceptions to the "use package name" rule:
|
||||
|
||||
* `packaging`: used for changes that change the npm package layout in all of our packages, e.g. public path changes, package.json changes done to all packages, d.ts file/format changes, changes to bundles, etc.
|
||||
|
||||
* `changelog`: used for updating the release notes in CHANGELOG.md
|
||||
|
||||
* `dev-infra`: used for dev-infra related changes within the directories /scripts, /tools and /dev-infra
|
||||
|
||||
* `docs-infra`: used for docs-app (angular.io) related changes within the /aio directory of the repo
|
||||
|
||||
* `migrations`: used for changes to the `ng update` migrations.
|
||||
|
||||
* `ngcc`: used for changes to the [Angular Compatibility Compiler](./packages/compiler-cli/ngcc/README.md)
|
||||
|
||||
* `ve`: used for changes specific to ViewEngine (legacy compiler/renderer).
|
||||
|
||||
* none/empty string: useful for `style`, `test` and `refactor` changes that are done across all packages (e.g. `style: add missing semicolons`) and for docs changes that are not related to a specific package (e.g. `docs: fix typo in tutorial`).
|
||||
|
||||
|
||||
##### Summary
|
||||
|
||||
Use the summary field to provide a succinct description of the change:
|
||||
|
||||
* use the imperative, present tense: "change" not "changed" nor "changes"
|
||||
* don't capitalize the first letter
|
||||
* no dot (.) at the end
|
||||
|
||||
|
||||
#### Commit Message Body
|
||||
|
||||
Just as in the summary, use the imperative, present tense: "fix" not "fixed" nor "fixes".
|
||||
|
||||
Explain the motivation for the change in the commit message body. This commit message should explain _why_ you are making the change.
|
||||
You can include a comparison of the previous behavior with the new behavior in order to illustrate the impact of the change.
|
||||
|
||||
|
||||
#### Commit Message Footer
|
||||
|
||||
The footer can contain information about breaking changes and is also the place to reference GitHub issues, Jira tickets, and other PRs that this commit closes or is related to.
|
||||
|
||||
```
|
||||
BREAKING CHANGE: <breaking change summary>
|
||||
<BLANK LINE>
|
||||
<breaking change description + migration instructions>
|
||||
<BLANK LINE>
|
||||
<BLANK LINE>
|
||||
Fixes #<issue number>
|
||||
```
|
||||
|
||||
Breaking Change section should start with the phrase "BREAKING CHANGE: " followed by a summary of the breaking change, a blank line, and a detailed description of the breaking change that also includes migration instructions.
|
||||
|
||||
|
||||
### Revert commits
|
||||
|
||||
If the commit reverts a previous commit, it should begin with `revert: `, followed by the header of the reverted commit.
|
||||
|
||||
The content of the commit message body should contain:
|
||||
|
||||
- information about the SHA of the commit being reverted in the following format: `This reverts commit <SHA>`,
|
||||
- a clear description of the reason for reverting the commit message.
|
||||
|
||||
|
||||
## <a name="cla"></a> Signing the CLA
|
||||
|
||||
Please sign our Contributor License Agreement (CLA) before sending pull requests. For any code
|
||||
changes to be accepted, the CLA must be signed. It's a quick process, we promise!
|
||||
|
||||
* For individuals, we have a [simple click-through form][individual-cla].
|
||||
* For corporations, we'll need you to
|
||||
[print, sign and one of scan+email, fax or mail the form][corporate-cla].
|
||||
|
||||
If you have more than one GitHub accounts, or multiple email addresses associated with a single GitHub account, you must sign the CLA using the primary email address of the GitHub account used to author Git commits and send pull requests.
|
||||
|
||||
The following documents can help you sort out issues with GitHub accounts and multiple email addresses:
|
||||
|
||||
* https://help.github.com/articles/setting-your-commit-email-address-in-git/
|
||||
* https://stackoverflow.com/questions/37245303/what-does-usera-committed-with-userb-13-days-ago-on-github-mean
|
||||
* https://help.github.com/articles/about-commit-email-addresses/
|
||||
* https://help.github.com/articles/blocking-command-line-pushes-that-expose-your-personal-email-address/
|
||||
|
||||
|
||||
|
||||
|
||||
[angular-group]: https://groups.google.com/forum/#!forum/angular
|
||||
[coc]: https://github.com/angular/code-of-conduct/blob/master/CODE_OF_CONDUCT.md
|
||||
[commit-message-format]: https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit#
|
||||
[corporate-cla]: http://code.google.com/legal/corporate-cla-v1.0.html
|
||||
[dev-doc]: https://github.com/angular/angular/blob/master/docs/DEVELOPER.md
|
||||
[github]: https://github.com/angular/angular
|
||||
[gitter]: https://gitter.im/angular/angular
|
||||
[individual-cla]: http://code.google.com/legal/individual-cla-v1.0.html
|
||||
[js-style-guide]: https://google.github.io/styleguide/jsguide.html
|
||||
[jsfiddle]: http://jsfiddle.net
|
||||
[plunker]: http://plnkr.co/edit
|
||||
[runnable]: http://runnable.com
|
||||
[stackoverflow]: http://stackoverflow.com/questions/tagged/angular
|
305
CONTRIBUTING.md
305
CONTRIBUTING.md
@ -1,243 +1,243 @@
|
||||
# Contributing to Angular
|
||||
# Contribuye a Angular
|
||||
|
||||
We would love for you to contribute to Angular and help make it even better than it is today!
|
||||
As a contributor, here are the guidelines we would like you to follow:
|
||||
¡Nos encantaría que contribuyeras a Angular y que ayudaras a hacerlo aún mejor de lo que es hoy!
|
||||
Como colaborador, estos son los lineamientos que nos gustaría que siguieras:
|
||||
|
||||
- [Code of Conduct](#coc)
|
||||
- [Question or Problem?](#question)
|
||||
- [Issues and Bugs](#issue)
|
||||
- [Feature Requests](#feature)
|
||||
- [Submission Guidelines](#submit)
|
||||
- [Coding Rules](#rules)
|
||||
- [Commit Message Guidelines](#commit)
|
||||
- [Signing the CLA](#cla)
|
||||
- [Código de conducta](#coc)
|
||||
- [¿Preguntas o problemas?](#question)
|
||||
- [_Issues_ y _bugs_](#issue)
|
||||
- [Solicitud de funcionalidades](#feature)
|
||||
- [Lineamientos para la creación de _issues_ y _PR_](#submit)
|
||||
- [Reglas del código](#rules)
|
||||
- [Convención para el mensaje de los _commits_](#commit)
|
||||
- [Firma del Acuerdo de Licencia de Colaborador (CLA)](#cla)
|
||||
|
||||
|
||||
## <a name="coc"></a> Code of Conduct
|
||||
## <a name="coc"></a> Código de conducta
|
||||
|
||||
Help us keep Angular open and inclusive.
|
||||
Please read and follow our [Code of Conduct][coc].
|
||||
Ayúdanos a mantener Angular abierto e inclusivo.
|
||||
Por favor lee y sigue nuestro [Código de conducta][coc].
|
||||
|
||||
|
||||
## <a name="question"></a> Got a Question or Problem?
|
||||
## <a name="question"></a> ¿Tienes alguna pregunta o problema?
|
||||
|
||||
Do not open issues for general support questions as we want to keep GitHub issues for bug reports and feature requests.
|
||||
Instead, we recommend using [Stack Overflow](https://stackoverflow.com/questions/tagged/angular) to ask support-related questions. When creating a new question on Stack Overflow, make sure to add the `angular` tag.
|
||||
No abras *issues* para preguntas de soporte general ya que queremos mantener los *issues* de GitHub para reporte de *bugs* y solicitud de funcionalidades.
|
||||
En su lugar, recomendamos utilizar [Stack Overflow](https://stackoverflow.com/questions/tagged/angular) para hacer preguntas relacionadas con soporte. Al crear una nueva pregunta en Stack Overflow, asegúrate de agregar el etiqueta (tag) de `angular`.
|
||||
|
||||
Stack Overflow is a much better place to ask questions since:
|
||||
Stack Overflow es mucho mejor para hacer preguntas ya que:
|
||||
|
||||
- there are thousands of people willing to help on Stack Overflow
|
||||
- questions and answers stay available for public viewing so your question/answer might help someone else
|
||||
- Stack Overflow's voting system assures that the best answers are prominently visible.
|
||||
- Hay miles de personas dispuestas a ayudar en preguntas y respuestas de Stack Overflow
|
||||
que permanecen disponibles para el público, por lo que tu pregunta o respuesta podría ayudar a otra persona.
|
||||
- El sistema de votación de Stack Overflow asegura que las mejores respuestas sobresalgan y sean visibles.
|
||||
|
||||
To save your and our time, we will systematically close all issues that are requests for general support and redirect people to Stack Overflow.
|
||||
Para ahorrar tu tiempo y el nuestro, cerraremos sistemáticamente todos los *issues* que sean solicitudes de soporte general y redirigiremos a las personas a Stack Overflow.
|
||||
|
||||
If you would like to chat about the question in real-time, you can reach out via [our gitter channel][gitter].
|
||||
Si deseas chatear sobre alguna pregunta en tiempo real, puedes hacerlo a través de nuestro [canal de Gitter][gitter].
|
||||
|
||||
|
||||
## <a name="issue"></a> Found a Bug?
|
||||
## <a name="issue"></a> ¿Encontraste un Bug?
|
||||
|
||||
If you find a bug in the source code, you can help us by [submitting an issue](#submit-issue) to our [GitHub Repository][github].
|
||||
Even better, you can [submit a Pull Request](#submit-pr) with a fix.
|
||||
Si encontraste un error en el código fuente, puedes ayudarnos [creando un *issue*](#submit-issue) en nuestro [repositorio de GitHub][github].
|
||||
O incluso mejor, puedes [crear un *Pull Request*](#submit-pr) con la solución.
|
||||
|
||||
|
||||
## <a name="feature"></a> Missing a Feature?
|
||||
You can *request* a new feature by [submitting an issue](#submit-issue) to our GitHub Repository.
|
||||
If you would like to *implement* a new feature, please consider the size of the change in order to determine the right steps to proceed:
|
||||
## <a name="feature"></a> ¿Falta alguna funcionalidad?
|
||||
Puedes solicitar una nueva funcionalidad [creando un *issue*](#submit-issue) en nuestro repositorio de GitHub.
|
||||
Si deseas implementar una nueva funcionalidad, por favor considera el tamaño del cambio para determinar los pasos correctos para continuar:
|
||||
|
||||
* For a **Major Feature**, first open an issue and outline your proposal so that it can be discussed.
|
||||
This process allows us to better coordinate our efforts, prevent duplication of work, and help you to craft the change so that it is successfully accepted into the project.
|
||||
* Para un **cambio significativo**, primero abre un *issue* y describe tu propuesta para que pueda ser discutida.
|
||||
Este proceso nos permite coordinar mejor nuestros esfuerzos, evitar trabajo duplicado y ayudarte a diseñar el cambio para que sea aceptado con éxito en el proyecto.
|
||||
|
||||
**Note**: Adding a new topic to the documentation, or significantly re-writing a topic, counts as a major feature.
|
||||
**Nota**: Agregar un nuevo tema a la documentación o reescribir significativamente un tema, también cuenta como *cambio significativo*.
|
||||
|
||||
* **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr).
|
||||
* **Cambios pequeños** pueden ser elaborados y directamente [creados como un _pull request_](#submit-pr).
|
||||
|
||||
|
||||
## <a name="submit"></a> Submission Guidelines
|
||||
## <a name="submit"></a> Lineamientos para la creación de _issues_ y _pull requests_
|
||||
|
||||
|
||||
### <a name="submit-issue"></a> Submitting an Issue
|
||||
### <a name="submit-issue"></a> Creación de _issues_
|
||||
|
||||
Before you submit an issue, please search the issue tracker, maybe an issue for your problem already exists and the discussion might inform you of workarounds readily available.
|
||||
Antes de crear un *issue*, por favor busca en el el *issue tracker*, quizá un *issue* para tu problema ya existe y la discusión puede informarte sobre soluciones alternativas disponibles.
|
||||
|
||||
We want to fix all the issues as soon as possible, but before fixing a bug we need to reproduce and confirm it.
|
||||
In order to reproduce bugs, we require that you provide a minimal reproduction.
|
||||
Having a minimal reproducible scenario gives us a wealth of important information without going back and forth to you with additional questions.
|
||||
Queremos solucionar todos los problemas lo antes posible, pero antes de corregir un bug necesitamos reproducirlo y confirmarlo.
|
||||
Para reproducir errores, requerimos que proporciones una reproducción mínima.
|
||||
Tener un escenario reproducible mínimo nos brinda una gran cantidad de información importante sin tener que ir y venir con preguntas adicionales.
|
||||
|
||||
A minimal reproduction allows us to quickly confirm a bug (or point out a coding problem) as well as confirm that we are fixing the right problem.
|
||||
Una reproducción mínima nos permite confirmar rápidamente un bug (o señalar un problema de código), así también confirmar que estamos solucionando el problema correcto.
|
||||
|
||||
We require a minimal reproduction to save maintainers' time and ultimately be able to fix more bugs.
|
||||
Often, developers find coding problems themselves while preparing a minimal reproduction.
|
||||
We understand that sometimes it might be hard to extract essential bits of code from a larger codebase but we really need to isolate the problem before we can fix it.
|
||||
Requerimos una reproducción mínima para ahorrar tiempo a los encargados del mantenimiento y en última instancia, poder corregir más bugs.
|
||||
A menudo los desarrolladores encuentran problemas de código mientras preparan una reproducción mínima.
|
||||
Entendemos que a veces puede ser difícil extraer porciones esenciales de código de un código más grande, pero realmente necesitamos aislar el problema antes de poder solucionarlo.
|
||||
|
||||
Unfortunately, we are not able to investigate / fix bugs without a minimal reproduction, so if we don't hear back from you, we are going to close an issue that doesn't have enough info to be reproduced.
|
||||
Desafortunadamente no podemos investigar/corregir errores sin una reproducción mínima, por lo que si no tenemos tu retroalimentación del bug, vamos a cerrar el *issue* ya que no tiene suficiente información para reproducirse.
|
||||
|
||||
You can file new issues by selecting from our [new issue templates](https://github.com/angular/angular/issues/new/choose) and filling out the issue template.
|
||||
Puedes presentar nuevos *issues* seleccionando nuestra [plantilla de _issues_](https://github.com/angular/angular/issues/new/choose) y complentando la plantilla.
|
||||
|
||||
|
||||
### <a name="submit-pr"></a> Submitting a Pull Request (PR)
|
||||
### <a name="submit-pr"></a> Creación de un Pull Requests (PR)
|
||||
|
||||
Before you submit your Pull Request (PR) consider the following guidelines:
|
||||
Antes de crear tu Pull Request (PR) considera los siguientes lineamientos:
|
||||
|
||||
1. Search [GitHub](https://github.com/angular/angular/pulls) for an open or closed PR that relates to your submission.
|
||||
You don't want to duplicate existing efforts.
|
||||
1. Busca en [GitHub](https://github.com/angular/angular/pulls) PRs que estén abiertos o cerrados y que estén relacionados con el que vas a crear.
|
||||
No deseas duplicar los esfuerzos existentes.
|
||||
|
||||
2. Be sure that an issue describes the problem you're fixing, or documents the design for the feature you'd like to add.
|
||||
Discussing the design upfront helps to ensure that we're ready to accept your work.
|
||||
2. Asegúrate de que el PR describa el problema que estás solucionando o que documente el diseño de la funcionalidad que deseas agregar.
|
||||
Discutir el diseño por adelantado ayuda a garantizar que estemos listos para aceptar tu trabajo.
|
||||
|
||||
3. Please sign our [Contributor License Agreement (CLA)](#cla) before sending PRs.
|
||||
We cannot accept code without a signed CLA.
|
||||
Make sure you author all contributed Git commits with email address associated with your CLA signature.
|
||||
3. Por favor firma nuestro [Acuerdo de Licencia de Colaborador (CLA)](#cla) antes de crear PRs.
|
||||
No podemos aceptar el código sin el Acuerdo de Licencia de Colaborador (CLA) firmado.
|
||||
Asegúrate de crear todas las contribuciones de Git con la dirección de correo electrónico asociada con tu firma del Acuerdo de Licencia de Colaborador (CLA).
|
||||
|
||||
4. Fork the angular/angular repo.
|
||||
4. Haz *fork* del repositorio angular/angular.
|
||||
|
||||
5. Make your changes in a new git branch:
|
||||
5. Haz tus cambios en una nueva rama de Git:
|
||||
|
||||
```shell
|
||||
git checkout -b my-fix-branch master
|
||||
```
|
||||
|
||||
6. Create your patch, **including appropriate test cases**.
|
||||
6. Crea tu correción, **incluyendo casos de prueba apropiados**.
|
||||
|
||||
7. Follow our [Coding Rules](#rules).
|
||||
7. Sigue nuestras [Reglas de código](#rules).
|
||||
|
||||
8. Run the full Angular test suite, as described in the [developer documentation][dev-doc], and ensure that all tests pass.
|
||||
8. Ejecuta todo el conjunto de pruebas de Angular, tal como está descrito en la [documentación del desarrollador][dev-doc], y asegúrate de que todas las pruebas pasen.
|
||||
|
||||
9. Commit your changes using a descriptive commit message that follows our [commit message conventions](#commit).
|
||||
Adherence to these conventions is necessary because release notes are automatically generated from these messages.
|
||||
9. Crea un commit de tus cambios utilizando un mensaje de commit descriptivo que siga nuestra [convención para el mensaje de los commits](#commit).
|
||||
Es necesario cumplir con estas convenciones porque las notas de las versiones se generan automáticamente a partir de estos mensajes.
|
||||
|
||||
```shell
|
||||
git commit -a
|
||||
```
|
||||
Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files.
|
||||
Nota: la opción de la línea de comandos de Git `-a` automaticamente hará "add" y "rm" a los archivos editados.
|
||||
|
||||
10. Push your branch to GitHub:
|
||||
10. Haz push de tu rama a GitHub:
|
||||
|
||||
```shell
|
||||
git push origin my-fix-branch
|
||||
```
|
||||
|
||||
11. In GitHub, send a pull request to `angular:master`.
|
||||
11. En GitHub, crea un pull request a `angular:master`.
|
||||
|
||||
If we ask for changes via code reviews then:
|
||||
Si solicitamos cambios a través de revisiones de código, sigue las siguientes indicaciones:
|
||||
|
||||
* Make the required updates.
|
||||
* Re-run the Angular test suites to ensure tests are still passing.
|
||||
* Rebase your branch and force push to your GitHub repository (this will update your Pull Request):
|
||||
* Haz los cambios requeridos.
|
||||
* Ejecuta nuevamente el conjunto de pruebas de Angular para asegurar que todas las pruebas aún están pasando.
|
||||
* Haz rebase de tu rama a la rama master y haz push con la opción `-f` a tu repositorio de Github (esto actualizará tu Pull Request):
|
||||
|
||||
```shell
|
||||
git rebase master -i
|
||||
git push -f
|
||||
```
|
||||
|
||||
That's it! Thank you for your contribution!
|
||||
¡Es todo! ¡Muchas gracias por tu contribución!
|
||||
|
||||
|
||||
#### After your pull request is merged
|
||||
#### Después del merge de tu pull request
|
||||
|
||||
After your pull request is merged, you can safely delete your branch and pull the changes from the main (upstream) repository:
|
||||
Después de que se hizo merge de tu pull request, puedes eliminar de forma segura tu rama y hacer pull de los cambios del repositorio principal (upstream):
|
||||
|
||||
* Delete the remote branch on GitHub either through the GitHub web UI or your local shell as follows:
|
||||
* Elimina la rama remota en GitHub a través de la interfaz de usuario web de GitHub o en tu línea de comandos de la siguiente manera:
|
||||
|
||||
```shell
|
||||
git push origin --delete my-fix-branch
|
||||
```
|
||||
|
||||
* Check out the master branch:
|
||||
* Muévete a la rama master:
|
||||
|
||||
```shell
|
||||
git checkout master -f
|
||||
```
|
||||
|
||||
* Delete the local branch:
|
||||
* Elimina tu rama local:
|
||||
|
||||
```shell
|
||||
git branch -D my-fix-branch
|
||||
```
|
||||
|
||||
* Update your master with the latest upstream version:
|
||||
* Actualiza tu rama master con la última versión del fork (upstream):
|
||||
|
||||
```shell
|
||||
git pull --ff upstream master
|
||||
```
|
||||
|
||||
|
||||
## <a name="rules"></a> Coding Rules
|
||||
To ensure consistency throughout the source code, keep these rules in mind as you are working:
|
||||
## <a name="rules"></a> Reglas del código
|
||||
Para garantizar la coherencia en todo el código fuente, ten en cuenta estas reglas mientras trabajas:
|
||||
|
||||
* All features or bug fixes **must be tested** by one or more specs (unit-tests).
|
||||
* All public API methods **must be documented**.
|
||||
* We follow [Google's JavaScript Style Guide][js-style-guide], but wrap all code at **100 characters**.
|
||||
* Todas las funcionalidades o solución de bugs **deben ser probadas** por una o más pruebas (pruebas unitarias).
|
||||
* Todos los métodos públicos del API **deben ser documentados**.
|
||||
* Seguimos la [guía de estilo JavaScript de Google][js-style-guide], pero cada línea no debe exceder **100 caracteres**.
|
||||
|
||||
An automated formatter is available, see [DEVELOPER.md](docs/DEVELOPER.md#clang-format).
|
||||
Un formateador automatizado está disponible, revisar [DEVELOPER.md](docs/DEVELOPER.md#clang-format).
|
||||
|
||||
|
||||
## <a name="commit"></a> Commit Message Format
|
||||
## <a name="commit"></a> Formato para el mensaje de los commits
|
||||
|
||||
*This specification is inspired and supersedes the [AngularJS commit message format][commit-message-format].*
|
||||
*Esta especificación está inspirada y reemplaza el [Formato de mensaje de commits de AngularJS][commit-message-format].*
|
||||
|
||||
We have very precise rules over how our Git commit messages must be formatted.
|
||||
This format leads to **easier to read commit history**.
|
||||
Tenemos reglas muy precisas sobre cómo deben formatearse nuestros mensajes de los commits de Git.
|
||||
Este formato permite tener **un historial de commits más facil de leer**.
|
||||
|
||||
Each commit message consists of a **header**, a **body**, and a **footer**.
|
||||
Cada mensaje de un commit consta del **header**, el **body**, y el **footer**.
|
||||
|
||||
|
||||
```
|
||||
<header>
|
||||
<BLANK LINE>
|
||||
<LINEA VACIA>
|
||||
<body>
|
||||
<BLANK LINE>
|
||||
<LINEA VACIA>
|
||||
<footer>
|
||||
```
|
||||
|
||||
The `header` is mandatory and must conform to the [Commit Message Header](#commit-header) format.
|
||||
El `header` es obligatorio y debe ajustarse al formato del [mensaje del header del commit](#commit-header).
|
||||
|
||||
The `body` is mandatory for all commits except for those of scope "docs".
|
||||
When the body is required it must be at least 20 characters long.
|
||||
El `body` es obligatorio para todos los commits excepto los que tenga scope "docs".
|
||||
Cuando el body es requerido debe tener al menos 20 caracteres.
|
||||
|
||||
The `footer` is optional.
|
||||
El `footer` es opcional.
|
||||
|
||||
Any line of the commit message cannot be longer than 100 characters.
|
||||
Cualquier línea del mensaje del commit no puede tener más de 100 caracteres.
|
||||
|
||||
|
||||
#### <a href="commit-header"></a>Commit Message Header
|
||||
#### <a href="commit-header"></a>Mensaje del header del commit
|
||||
|
||||
```
|
||||
<type>(<scope>): <short summary>
|
||||
<tipo>(<alcance>): <resumen>
|
||||
│ │ │
|
||||
│ │ └─⫸ Summary in present tense. Not capitalized. No period at the end.
|
||||
│ │ └─⫸ Resumen corto escrito en modo imperativo, tiempo presente. Sin mayúsculas. Sin punto final.
|
||||
│ │
|
||||
│ └─⫸ Commit Scope: animations|bazel|benchpress|common|compiler|compiler-cli|core|
|
||||
│ └─⫸ Alcance del commit: animations|bazel|benchpress|common|compiler|compiler-cli|core|
|
||||
│ elements|forms|http|language-service|localize|platform-browser|
|
||||
│ platform-browser-dynamic|platform-server|platform-webworker|
|
||||
│ platform-webworker-dynamic|router|service-worker|upgrade|zone.js|
|
||||
│ packaging|changelog|dev-infra|docs-infra|migrations|ngcc|ve
|
||||
│
|
||||
└─⫸ Commit Type: build|ci|docs|feat|fix|perf|refactor|style|test
|
||||
└─⫸ Tipo de commit: build|ci|docs|feat|fix|perf|refactor|style|test
|
||||
```
|
||||
|
||||
The `<type>` and `<summary>` fields are mandatory, the `(<scope>)` field is optional.
|
||||
El `<tipo>` y `<resumen>` son obligatorios, el `(<alcance>)` es opcional.
|
||||
|
||||
|
||||
##### Type
|
||||
##### Tipo
|
||||
|
||||
Must be one of the following:
|
||||
El tipo debe ser uno de los siguientes:
|
||||
|
||||
* **build**: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
|
||||
* **ci**: Changes to our CI configuration files and scripts (example scopes: Circle, BrowserStack, SauceLabs)
|
||||
* **docs**: Documentation only changes
|
||||
* **feat**: A new feature
|
||||
* **fix**: A bug fix
|
||||
* **perf**: A code change that improves performance
|
||||
* **refactor**: A code change that neither fixes a bug nor adds a feature
|
||||
* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
|
||||
* **test**: Adding missing tests or correcting existing tests
|
||||
* **build**: cambios que afectan el sistema de compilación o dependencias externas (ejemplos de scopes: gulp, broccoli, npm)
|
||||
* **ci**: cambios en nuestros archivos y scripts de configuración de CI (ejemplos de scopes: Circle, BrowserStack, SauceLabs)
|
||||
* **docs**: cambios en la documentación
|
||||
* **feat**: una nueva funcionalidad
|
||||
* **fix**: una solución de un bug
|
||||
* **perf**: un cambio de código que mejora el rendimiento.
|
||||
* **refactor**: un cambio de código que no corrige ningún error ni agrega ninguna funcionalidad
|
||||
* **style**: cambios que no afectan el significado del código (espacios en blanco, formato, falta de punto y coma, etc.)
|
||||
* **test**: se agregan pruebas faltantes o se corrigen pruebas existentes
|
||||
|
||||
|
||||
##### Scope
|
||||
The scope should be the name of the npm package affected (as perceived by the person reading the changelog generated from commit messages).
|
||||
##### Alcance
|
||||
El alcance debe ser el nombre del paquete npm afectado (tal como lo percibe la persona que lee el registro de cambios generado a partir de los mensajes de commit).
|
||||
|
||||
The following is the list of supported scopes:
|
||||
La siguiente es la lista de alcances permitidos:
|
||||
|
||||
* `animations`
|
||||
* `bazel`
|
||||
@ -261,80 +261,79 @@ The following is the list of supported scopes:
|
||||
* `upgrade`
|
||||
* `zone.js`
|
||||
|
||||
There are currently a few exceptions to the "use package name" rule:
|
||||
Actualmente hay algunas excepciones a la regla "usar el nombre de paquete":
|
||||
|
||||
* `packaging`: used for changes that change the npm package layout in all of our packages, e.g. public path changes, package.json changes done to all packages, d.ts file/format changes, changes to bundles, etc.
|
||||
* `packaging`: usado para cambios que cambian el diseño de los paquetes de npm en todos nuestros paquetes. Ejemplos: cambios de la ruta públic, package.json cambios hechos a todos los paquetes, cambios a archivos o formatos d.ts, cambios a bundles, etc.
|
||||
|
||||
* `changelog`: used for updating the release notes in CHANGELOG.md
|
||||
* `changelog`: utilizado para actualizar las notas de la versión en CHANGELOG.md
|
||||
|
||||
* `dev-infra`: used for dev-infra related changes within the directories /scripts, /tools and /dev-infra
|
||||
* `dev-infra`: utilizado para cambios relacionados con dev-infra dentro de los directorios /scripts, /tools y /dev-infra
|
||||
|
||||
* `docs-infra`: used for docs-app (angular.io) related changes within the /aio directory of the repo
|
||||
* `docs-infra`: utilizado para cambios relacionados con la documentación (angular.io) dentro del directorio /aio del repositorio
|
||||
|
||||
* `migrations`: used for changes to the `ng update` migrations.
|
||||
* `migrations`: utilizado para los cambios en las migraciones `ng update`.
|
||||
|
||||
* `ngcc`: used for changes to the [Angular Compatibility Compiler](./packages/compiler-cli/ngcc/README.md)
|
||||
* `ngcc`: usado para los cambios del [Compilador de compatibilidad de Angular](./packages/compiler-cli/ngcc/README.md)
|
||||
|
||||
* `ve`: used for changes specific to ViewEngine (legacy compiler/renderer).
|
||||
* `ve`: utilizado para cambios específicos de ViewEngine (legacy compiler/renderer).
|
||||
|
||||
* none/empty string: useful for `style`, `test` and `refactor` changes that are done across all packages (e.g. `style: add missing semicolons`) and for docs changes that are not related to a specific package (e.g. `docs: fix typo in tutorial`).
|
||||
* alcance vacío: útil para cambios de `style`, `test` y `refactor` que se realizan en todos los paquetes (ejemplo: `style: add missing semicolons`) y para cambios de la documentación que no están relacionados a un paquete en específico(ejemplo: `docs: corrige error gramatical en el tutorial`).
|
||||
|
||||
|
||||
##### Summary
|
||||
##### Resumen
|
||||
|
||||
Use the summary field to provide a succinct description of the change:
|
||||
Usa el campo resumen para proporcionar una descripción breve del cambio:
|
||||
|
||||
* use the imperative, present tense: "change" not "changed" nor "changes"
|
||||
* don't capitalize the first letter
|
||||
* no dot (.) at the end
|
||||
* usa el modo imperativo, tiempo presente: "cambia" no "cambió" o "cambios"
|
||||
* no debe de contener ninguna letra mayúscula
|
||||
* no debe de conter punto (.) al final
|
||||
|
||||
|
||||
#### Commit Message Body
|
||||
#### Mensaje del cuerpo del commit
|
||||
|
||||
Just as in the summary, use the imperative, present tense: "fix" not "fixed" nor "fixes".
|
||||
Tal como en el resumen, usa el modo imperativo, tiempo presente: "cambia" no "cambió" o "cambios".
|
||||
|
||||
Explain the motivation for the change in the commit message body. This commit message should explain _why_ you are making the change.
|
||||
You can include a comparison of the previous behavior with the new behavior in order to illustrate the impact of the change.
|
||||
Explica la razón del cambio en el el mensaje del cuerpo del commit. Este mensaje de confirmación debe explicar _por qué_ está realizando el cambio.
|
||||
Puedes incluir una comparación del comportamiento anterior con el nuevo comportamiento para ilustrar el impacto del cambio.
|
||||
|
||||
|
||||
#### Commit Message Footer
|
||||
#### Mensaje del footer del commit
|
||||
|
||||
The footer can contain information about breaking changes and is also the place to reference GitHub issues, Jira tickets, and other PRs that this commit closes or is related to.
|
||||
El footer puede contener información sobre cambios significativos y también es el lugar para hacer referencia a issues de GitHub, tickets de Jira y otros PRs que están relacionados con el commit.
|
||||
|
||||
```
|
||||
BREAKING CHANGE: <breaking change summary>
|
||||
<BLANK LINE>
|
||||
<breaking change description + migration instructions>
|
||||
<BLANK LINE>
|
||||
<BLANK LINE>
|
||||
Fixes #<issue number>
|
||||
CAMBIO SIGNIFICATIVO: <resumen del cambio significativo>
|
||||
<LINEA VACIA>
|
||||
<descripción del cambio significativo + instrucciones para la migración>
|
||||
<LINEA VACIA>
|
||||
<LINEA VACIA>
|
||||
Soluciona #<issue número>
|
||||
```
|
||||
|
||||
Breaking Change section should start with the phrase "BREAKING CHANGE: " followed by a summary of the breaking change, a blank line, and a detailed description of the breaking change that also includes migration instructions.
|
||||
La sección de cambios significativos debería comenzar con la frase "CAMBIO SIGNIFICATIVO: " seguido de un resumen del cambio significativo, una línea en blanco y una descripción detallada del cambio significativo que también incluya instrucciones de migración.
|
||||
|
||||
|
||||
### Revert commits
|
||||
### Revirtiendo commits
|
||||
|
||||
If the commit reverts a previous commit, it should begin with `revert: `, followed by the header of the reverted commit.
|
||||
Si el commit revierte un commit previo, el commit debería comenzar con `revert: `, seguido por el header del commit revertido.
|
||||
|
||||
The content of the commit message body should contain:
|
||||
El contenido del mensaje del commit debería contener:
|
||||
|
||||
- information about the SHA of the commit being reverted in the following format: `This reverts commit <SHA>`,
|
||||
- a clear description of the reason for reverting the commit message.
|
||||
- Información sobre el SHA del commit que se revierte en el siguiente formato: `Esto revierte el commit <SHA>`,
|
||||
- Una descripción clara de la razón para revertir el mensaje del _commit_.
|
||||
|
||||
|
||||
## <a name="cla"></a> Signing the CLA
|
||||
## <a name="cla"></a> Firma del Acuerdo de Licencia de Colaborador (CLA)
|
||||
|
||||
Please sign our Contributor License Agreement (CLA) before sending pull requests. For any code
|
||||
changes to be accepted, the CLA must be signed. It's a quick process, we promise!
|
||||
Por favor firma nuestro Acuerdo de Licencia de Colaborador (CLA) antes de crear _pull requests_. Para que cualquier cambio de código sea aceptado, el Acuerdo de Licencia de Colaborador (CLA) debe ser firmado. ¡Es un proceso rápido, lo prometemos!
|
||||
|
||||
* For individuals, we have a [simple click-through form][individual-cla].
|
||||
* For corporations, we'll need you to
|
||||
[print, sign and one of scan+email, fax or mail the form][corporate-cla].
|
||||
* Para las personas, tenemos un [formulario simple][individual-cla].
|
||||
* Para corporaciones, necesitaremos que
|
||||
[impriman, firmen, escaneen y envíen por email, fax o correo el formulario][corporate-cla].
|
||||
|
||||
If you have more than one GitHub accounts, or multiple email addresses associated with a single GitHub account, you must sign the CLA using the primary email address of the GitHub account used to author Git commits and send pull requests.
|
||||
Si tiene más de una cuenta de GitHub o varias direcciones de correo electrónico asociadas con una sola cuenta de GitHub, debes firmar el CLA utilizando la dirección de correo electrónico principal de la cuenta de GitHub utilizada para crear los commits de Git y la creación de PRs.
|
||||
|
||||
The following documents can help you sort out issues with GitHub accounts and multiple email addresses:
|
||||
Los siguientes documentos pueden ayudarte a resolver problemas con cuentas de GitHub y múltiples direcciones de correo electrónico:
|
||||
|
||||
* https://help.github.com/articles/setting-your-commit-email-address-in-git/
|
||||
* https://stackoverflow.com/questions/37245303/what-does-usera-committed-with-userb-13-days-ago-on-github-mean
|
||||
|
21
README.md
21
README.md
@ -5,22 +5,21 @@
|
||||
|
||||
# Angular
|
||||
|
||||
Angular is a development platform for building mobile and desktop web applications using TypeScript/JavaScript and other languages.
|
||||
Angular es una plataforma de desarrollo para construir aplicaciones web y móviles que usa TypeScript/JavaScript y otros lenguajes de programación.
|
||||
|
||||
## Quickstart
|
||||
## Guía rápida
|
||||
|
||||
[Get started in 5 minutes][quickstart].
|
||||
[Comienza a usarlo en 5 minutos][quickstart].
|
||||
|
||||
## Changelog
|
||||
## Registro de cambios (Changelog)
|
||||
|
||||
[Learn about the latest improvements][changelog].
|
||||
[Últimas mejoras realizadas][changelog].
|
||||
|
||||
## Want to help?
|
||||
## ¿Quieres ayudar?
|
||||
|
||||
Want to file a bug, contribute some code, or improve documentation? Excellent! Read up on our
|
||||
guidelines for [contributing][contributing] and then check out one of our issues in the [hotlist: community-help](https://github.com/angular/angular/labels/hotlist%3A%20community-help).
|
||||
¿Quieres encontrar fallos, colaborar con código, o mejorar la documentación? ¡Excelente! Lee nuestras
|
||||
pautas para [colaborar][contributing] y luego revisa algunos de nuestras incidencias (issues) en [ayuda comunitaria](https://github.com/angular-hispano/angular/labels/ayuda%20comunitaria).
|
||||
|
||||
[contributing]: https://github.com/angular/angular/blob/master/CONTRIBUTING.md
|
||||
[quickstart]: https://angular.io/start
|
||||
[contributing]: https://github.com/angular-hispano/angular/blob/master/CONTRIBUTING.md
|
||||
[quickstart]: https://docs.angular.lat/start
|
||||
[changelog]: https://github.com/angular/angular/blob/master/CHANGELOG.md
|
||||
[ng]: https://angular.io
|
||||
|
@ -1,111 +1,112 @@
|
||||
# Accessibility in Angular
|
||||
# Accesibilidad en Angular
|
||||
|
||||
The web is used by a wide variety of people, including those who have visual or motor impairments.
|
||||
A variety of assistive technologies are available that make it much easier for these groups to
|
||||
interact with web-based software applications.
|
||||
In addition, designing an application to be more accessible generally improves the user experience for all users.
|
||||
Hay una amplia variedad de personas que utilizan la web, algunas de ellas con discapacidad visual o motora.
|
||||
Existen diferentes tecnologías de apoyo que hacen que sea mucho más fácil para estos grupos
|
||||
interactuar con aplicaciones de software basadas en la web.
|
||||
Además, diseñar una aplicación para que sea más accesible, normalmente mejora la experiencia de usuario en general.
|
||||
|
||||
For an in-depth introduction to issues and techniques for designing accessible applications, see the [Accessibility](https://developers.google.com/web/fundamentals/accessibility/#what_is_accessibility) section of the Google's [Web Fundamentals](https://developers.google.com/web/fundamentals/).
|
||||
Para una introducción en profundidad a los problemas y técnicas sobre el diseño de aplicaciones accesibles, puede consultar la sección de [Accesibilidad](https://developers.google.com/web/fundamentals/accessibility/#what_is_accessibility) de Google [Fundamentos Web](https://developers.google.com/web/fundamentals/).
|
||||
|
||||
This page discusses best practices for designing Angular applications that
|
||||
work well for all users, including those who rely on assistive technologies.
|
||||
Esta página habla de las mejores prácticas para diseñar aplicaciones en Angular que funcionan
|
||||
bien para todos los usuarios, incluyendo aquéllos que necesitan tecnologías de apoyo.
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
For the sample app that this page describes, see the <live-example></live-example>.
|
||||
Para ver la aplicación de ejemplo que describe esta página, ir a <live-example></live-example>.
|
||||
|
||||
</div>
|
||||
|
||||
## Accessibility attributes
|
||||
## Atributos de accesibilidad
|
||||
|
||||
Building accessible web experience often involves setting [ARIA attributes](https://developers.google.com/web/fundamentals/accessibility/semantics-aria)
|
||||
to provide semantic meaning where it might otherwise be missing.
|
||||
Use [attribute binding](guide/attribute-binding) template syntax to control the values of accessibility-related attributes.
|
||||
Crear una web accesible a menudo implica establecer los [atributos ARIA](https://developers.google.com/web/fundamentals/accessibility/semantics-aria)
|
||||
para proporcionar la semántica que, de otro modo, podría no estar presente.
|
||||
Usa la plantilla de sintaxis del [enlace de atributos](attribute binding) (guide/attribute-binding) para controlar los valores de los atributos relacionados con la accesibilidad.
|
||||
|
||||
When binding to ARIA attributes in Angular, you must use the `attr.` prefix, as the ARIA
|
||||
specification depends specifically on HTML attributes rather than properties of DOM elements.
|
||||
Para enlazar los atributos ARIA en Angular, debes usar el prefijo `attr.`, ya que la especificación ARIA
|
||||
depende de los atributos HTML y no de las propiedades de los elementos del DOM.
|
||||
|
||||
```html
|
||||
<!-- Use attr. when binding to an ARIA attribute -->
|
||||
<button [attr.aria-label]="myActionLabel">...</button>
|
||||
```
|
||||
|
||||
Note that this syntax is only necessary for attribute _bindings_.
|
||||
Static ARIA attributes require no extra syntax.
|
||||
Observa que esta sintaxis solo es necesaria para los _enlaces_ de atributos.
|
||||
Los atributos ARIA estáticos no requieren de ninguna sintaxis adicional.
|
||||
|
||||
```html
|
||||
<!-- Static ARIA attributes require no extra syntax -->
|
||||
<button aria-label="Save document">...</button>
|
||||
```
|
||||
|
||||
NOTE:
|
||||
NOTA:
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
By convention, HTML attributes use lowercase names (`tabindex`), while properties use camelCase names (`tabIndex`).
|
||||
Por convenio, los atributos HTML se escriben en minúscula (`tabindex`), mientras que para las propiedades se usa *camelCase* (`tabIndex`).
|
||||
|
||||
See the [Binding syntax](guide/binding-syntax#html-attribute-vs-dom-property) guide for more background on the difference between attributes and properties.
|
||||
Consulta la guía [Sintaxis del enlace](guide/binding-syntax#html-attribute-vs-dom-property) para saber más sobre las diferencias entre atributos y propiedades.
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
## Angular UI components
|
||||
## Componentes del interfaz de usuario de Angular
|
||||
|
||||
The [Angular Material](https://material.angular.io/) library, which is maintained by the Angular team, is a suite of reusable UI components that aims to be fully accessible.
|
||||
The [Component Development Kit (CDK)](https://material.angular.io/cdk/categories) includes the `a11y` package that provides tools to support various areas of accessibility.
|
||||
For example:
|
||||
La librería [Angular Material](https://material.angular.io/), que es mantenida por el equipo Angular, es un conjunto de componentes reutilizables para la interfaz de usuario que pretende ser totalmente accesible.
|
||||
El [Kit de Desarrollo de Componentes (CDK)](https://material.angular.io/cdk/categories) (Component Development Kit) incluye el paquete `a11y` que proporciona herramientas para dar soporte a distintos aspectos de la accesibilidad.
|
||||
Por ejemplo:
|
||||
|
||||
* `LiveAnnouncer` is used to announce messages for screen-reader users using an `aria-live` region. See the W3C documentation for more information on [aria-live regions](https://www.w3.org/WAI/PF/aria-1.1/states_and_properties#aria-live).
|
||||
* `LiveAnnouncer` se utiliza para comunicar mensajes a los usuarios de lectores de pantalla que usan la region `aria-live`. Se puede consultar la documentación de la W3C para obtener más información sobre [regiones aria-live](https://www.w3.org/WAI/PF/aria-1.1/states_and_properties#aria-live).
|
||||
|
||||
* The `cdkTrapFocus` directive traps Tab-key focus within an element. Use it to create accessible experience for components like modal dialogs, where focus must be constrained.
|
||||
* La directiva `cdkTrapFocus` limita el foco de la tecla de tabulación para que se quede dentro de un elemento. Úsala para crear una experiencia accesible en componentes como las ventanas modales, donde el foco debe estar limitado.
|
||||
|
||||
For full details of these and other tools, see the [Angular CDK accessibility overview](https://material.angular.io/cdk/a11y/overview).
|
||||
Para obtener más detalles sobre esta y otras herramientas, consulta el [resumen de accesibilidad del Angular CDK](https://material.angular.io/cdk/a11y/overview).
|
||||
|
||||
|
||||
### Augmenting native elements
|
||||
### Aumento de elementos nativos
|
||||
|
||||
Native HTML elements capture a number of standard interaction patterns that are important to accessibility.
|
||||
When authoring Angular components, you should re-use these native elements directly when possible, rather than re-implementing well-supported behaviors.
|
||||
Los elementos HTML nativos capturan una serie de patrones de interacción estándar que son importantes para la accesibilidad.
|
||||
Al crear componentes de Angular, deberías reutilizar estos elementos nativos directamente cuando sea posible, en lugar de volver a implementar comportamientos bien soportados.
|
||||
|
||||
For example, instead of creating a custom element for a new variety of button, you can create a component that uses an attribute selector with a native `<button>` element.
|
||||
This most commonly applies to `<button>` and `<a>`, but can be used with many other types of element.
|
||||
Por ejemplo, en lugar de crear un elemento personalizado para un nuevo tipo de botón, puedes crear un componente que use un selector de atributos con un elemento nativo `<button>`.
|
||||
Esto se aplica sobre todo a `<button>` y `<a>`, pero se puede usar con muchos otros tipos de elementos.
|
||||
|
||||
You can see examples of this pattern in Angular Material: [`MatButton`](https://github.com/angular/components/blob/master/src/material/button/button.ts#L66-L68), [`MatTabNav`](https://github.com/angular/components/blob/master/src/material/tabs/tab-nav-bar/tab-nav-bar.ts#L67), [`MatTable`](https://github.com/angular/components/blob/master/src/material/table/table.ts#L17).
|
||||
|
||||
### Using containers for native elements
|
||||
### Uso de contenedores para elementos nativos
|
||||
|
||||
Sometimes using the appropriate native element requires a container element.
|
||||
For example, the native `<input>` element cannot have children, so any custom text entry components need
|
||||
to wrap an `<input>` with additional elements.
|
||||
While you might just include the `<input>` in your custom component's template,
|
||||
this makes it impossible for users of the component to set arbitrary properties and attributes to the input element.
|
||||
Instead, you can create a container component that uses content projection to include the native control in the
|
||||
component's API.
|
||||
A veces, para usar el elemento nativo apropiado hace falta un contenedor.
|
||||
Por ejemplo, el elemento nativo `<input>` no puede tener hijos, por lo que cualquier componente de entrada de texto personalizado necesita envolver un `<input>` con elementos adicionales.
|
||||
|
||||
You can see [`MatFormField`](https://material.angular.io/components/form-field/overview) as an example of this pattern.
|
||||
Si bien puedes incluir el `<input>` en la plantilla de tu componente personalizado,
|
||||
esto hace que sea imposible para los usuarios de dicho componente establecer propiedades y atributos arbitrarios para el elemento de entrada.
|
||||
En su lugar, puedes crear un componente contenedor que utilice la proyección de contenido para incluir el control nativo en el
|
||||
API del componente.
|
||||
|
||||
## Case study: Building a custom progress bar
|
||||
Puedes consultar [`MatFormField`](https://material.angular.io/components/form-field/overview) para ver un ejemplo de este patrón.
|
||||
|
||||
The following example shows how to make a simple progress bar accessible by using host binding to control accessibility-related attributes.
|
||||
## Caso práctico: creación de una barra de progreso personalizada
|
||||
|
||||
* The component defines an accessibility-enabled element with both the standard HTML attribute `role`, and ARIA attributes. The ARIA attribute `aria-valuenow` is bound to the user's input.
|
||||
El siguiente ejemplo muestra cómo hacer que una barra de progreso simple sea accesible utilizando el *host binding* para controlar los atributos relacionados con la accesibilidad.
|
||||
|
||||
* El componente define un elemento habilitado para accesibilidad con el atributo HTML estándar `role` y los atributos ARIA. El atributo ARIA `aria-valuenow` está vinculado a la entrada del usuario.
|
||||
|
||||
<code-example path="accessibility/src/app/progress-bar.component.ts" header="src/app/progress-bar.component.ts" region="progressbar-component"></code-example>
|
||||
|
||||
|
||||
* In the template, the `aria-label` attribute ensures that the control is accessible to screen readers.
|
||||
* En la plantilla, el atributo `aria-label` asegura que el control sea accesible para los lectores de pantalla.
|
||||
|
||||
<code-example path="accessibility/src/app/app.component.html" header="src/app/app.component.html" region="template"></code-example>
|
||||
|
||||
|
||||
## Routing and focus management
|
||||
## Enrutamiento y gestión del foco
|
||||
|
||||
Tracking and controlling [focus](https://developers.google.com/web/fundamentals/accessibility/focus/) in a UI is an important consideration in designing for accessibility.
|
||||
When using Angular routing, you should decide where page focus goes upon navigation.
|
||||
El seguimiento y el control del [foco](https://developers.google.com/web/fundamentals/accessibility/focus/) en una interfaz de usuario son aspectos muy importantes en el diseño si queremos tener en cuenta la accesibilidad.
|
||||
Al usar el enrutamiento de Angular, debes decidir dónde va el foco de la página al navegar.
|
||||
|
||||
To avoid relying solely on visual cues, you need to make sure your routing code updates focus after page navigation.
|
||||
Use the `NavigationEnd` event from the `Router` service to know when to update
|
||||
focus.
|
||||
Para evitar depender únicamente de señales visuales, te debes asegurar de que el código de enrutamiento actualiza el foco después de la navegación de la página.
|
||||
Usa el evento `NavigationEnd` del servicio` Router` para saber cuándo actualizar el foco.
|
||||
|
||||
El siguiente ejemplo muestra cómo encontrar y poner el foco en el contenido principal de la cabecera (el elemento `#main-content-header`) dentro del DOM después de la navegación.
|
||||
|
||||
The following example shows how to find and focus the main content header in the DOM after navigation.
|
||||
|
||||
@ -119,13 +120,12 @@ router.events.pipe(filter(e => e instanceof NavigationEnd)).subscribe(() => {
|
||||
});
|
||||
|
||||
```
|
||||
In a real application, the element that receives focus will depend on your specific
|
||||
application structure and layout.
|
||||
The focused element should put users in a position to immediately move into the main content that has just been routed into view.
|
||||
You should avoid situations where focus returns to the `body` element after a route change.
|
||||
En una aplicación real, el elemento que recibe el foco dependerá de la estructura específica y del diseño que tenga tu aplicación.
|
||||
El elemento enfocado debe colocar a los usuarios en una posición en la que pasen inmediatamente al contenido principal que acaba de ser visualizado.
|
||||
Debe evitar situaciones en las que el foco vuelva al elemento `body` después de un cambio de ruta.
|
||||
|
||||
|
||||
## Additional resources
|
||||
## Recursos adicionales
|
||||
|
||||
* [Accessibility - Google Web Fundamentals](https://developers.google.com/web/fundamentals/accessibility)
|
||||
|
||||
@ -145,13 +145,13 @@ You should avoid situations where focus returns to the `body` element after a ro
|
||||
|
||||
* [Codelyzer](http://codelyzer.com/rules/) provides linting rules that can help you make sure your code meets accessibility standards.
|
||||
|
||||
Books
|
||||
Libros
|
||||
|
||||
* "A Web for Everyone: Designing Accessible User Experiences", Sarah Horton and Whitney Quesenbery
|
||||
|
||||
* "Inclusive Design Patterns", Heydon Pickering
|
||||
|
||||
## More on accessibility
|
||||
## Más sobre accesibilidad
|
||||
|
||||
You may also be interested in the following:
|
||||
Podrías estar interesado en lo siguiente:
|
||||
* [Audit your Angular app's accessibility with codelyzer](https://web.dev/accessible-angular-with-codelyzer/).
|
||||
|
@ -1,32 +1,32 @@
|
||||
# App shell
|
||||
|
||||
App shell is a way to render a portion of your application via a route at build time.
|
||||
It can improve the user experience by quickly launching a static rendered page (a skeleton common to all pages) while the browser downloads the full client version and switches to it automatically after the code loads.
|
||||
App shell es una manera de renderizar una porción de tu aplicación a través de una ruta en tiempo de compilación (build time).
|
||||
Puede mejorar la experiencia de usuario lanzando rápidamente una página estática renderizada (un esqueleto común a todas las páginas) mientras el navegador descarga la versión completa del cliente y la muestra automáticamente al finalizar su carga.
|
||||
|
||||
This gives users a meaningful first paint of your application that appears quickly because the browser can simply render the HTML and CSS without the need to initialize any JavaScript.
|
||||
Esto da a los usuarios una primera visualización significativa de su aplicación que aparece rápidamente porque el navegador simplemente puede renderizar HTML y CSS sin la necesidad de inicializar JavaScript.
|
||||
|
||||
Learn more in [The App Shell Model](https://developers.google.com/web/fundamentals/architecture/app-shell).
|
||||
Obténga más información en [El modelo de aplicación Shell](https://developers.google.com/web/fundamentals/architecture/app-shell).
|
||||
|
||||
## Step 1: Prepare the application
|
||||
## Paso 1: Prepara la aplicación
|
||||
|
||||
You can do this with the following CLI command:
|
||||
Puedes hacer esto con el siguiente comando CLI:
|
||||
<code-example language="bash">
|
||||
ng new my-app --routing
|
||||
</code-example>
|
||||
|
||||
For an existing application, you have to manually add the `RouterModule` and defining a `<router-outlet>` within your application.
|
||||
Para una aplicación existente, debes agregar manualmente el `RouterModule` y definir un` <router-outlet> `dentro de tu aplicación.
|
||||
|
||||
## Step 2: Create the app shell
|
||||
## Paso 2: Crea el shell de la aplicación
|
||||
|
||||
Use the CLI to automatically create the app shell.
|
||||
Usa la CLI para crear automáticamente el shell de la aplicación.
|
||||
|
||||
<code-example language="bash">
|
||||
ng generate app-shell
|
||||
</code-example>
|
||||
|
||||
* `client-project` takes the name of your client application.
|
||||
* `client-project` toma el nombre de tu aplicación cliente.
|
||||
|
||||
After running this command you will notice that the `angular.json` configuration file has been updated to add two new targets, with a few other changes.
|
||||
Después de ejecutar este comando, notará que el archivo de configuración `angular.json` se ha actualizado para agregar dos nuevos targets, con algunos otros cambios.
|
||||
|
||||
<code-example language="json">
|
||||
"server": {
|
||||
@ -53,20 +53,18 @@ After running this command you will notice that the `angular.json` configuration
|
||||
}
|
||||
</code-example>
|
||||
|
||||
## Step 3: Verify the app is built with the shell content
|
||||
## Paso 3: Verifica que la aplicación está construida con el contenido del shell
|
||||
|
||||
Use the CLI to build the `app-shell` target.
|
||||
Usa la CLI para construir el `app-shell` target.
|
||||
|
||||
<code-example language="bash">
|
||||
ng run my-app:app-shell
|
||||
</code-example>
|
||||
|
||||
Or to use the production configuration.
|
||||
O usa la configuración de producción.
|
||||
|
||||
<code-example language="bash">
|
||||
ng run my-app:app-shell:production
|
||||
</code-example>
|
||||
|
||||
To verify the build output, open `dist/my-app/index.html`. Look for default text `app-shell works!` to show that the app shell route was rendered as part of the output.
|
||||
|
||||
|
||||
Para verificar el resultado de la compilación, abre `dist/my-app/index.html`. Busca el texto por defecto `app-shell works!` para mostrar que la ruta del shell de la aplicación se ha renderizado como parte de la carpeta de distribución.
|
||||
|
@ -124,9 +124,9 @@ Data binding plays an important role in communication between a template and its
|
||||
|
||||
Angular pipes let you declare display-value transformations in your template HTML. A class with the `@Pipe` decorator defines a function that transforms input values to output values for display in a view.
|
||||
|
||||
Angular defines various pipes, such as the [date](https://angular.io/api/common/DatePipe) pipe and [currency](https://angular.io/api/common/CurrencyPipe) pipe; for a complete list, see the [Pipes API list](https://angular.io/api?type=pipe). You can also define new pipes.
|
||||
Angular defines various pipes, such as the [date](api/common/DatePipe) pipe and [currency](api/common/CurrencyPipe) pipe; for a complete list, see the [Pipes API list](api?type=pipe). You can also define new pipes.
|
||||
|
||||
To specify a value transformation in an HTML template, use the [pipe operator (|)](https://angular.io/guide/template-expression-operators#pipe).
|
||||
To specify a value transformation in an HTML template, use the [pipe operator (|)](guide/template-expression-operators#pipe).
|
||||
|
||||
`{{interpolated_value | pipe_name}}`
|
||||
|
||||
|
@ -140,7 +140,7 @@ Angular provides *value accessors* for all of the basic HTML form elements and t
|
||||
|
||||
You can't apply `[(ngModel)]` to a non-form native element or a
|
||||
third-party custom component until you write a suitable value accessor. For more information, see
|
||||
the API documentation on [DefaultValueAccessor](https://angular.io/api/forms/DefaultValueAccessor).
|
||||
the API documentation on [DefaultValueAccessor](api/forms/DefaultValueAccessor).
|
||||
|
||||
You don't need a value accessor for an Angular component that
|
||||
you write because you can name the value and event properties
|
||||
|
@ -10,7 +10,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code><b>platformBrowserDynamic().bootstrapModule</b>(AppModule);</code></td>
|
||||
<td><p>Bootstraps the app, using the root component from the specified <code>NgModule</code>. </p>
|
||||
<td><p>Carga la app, usando el componente raíz del <code>NgModule</code> especificado.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
@ -24,370 +24,371 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>@<b>NgModule</b>({ declarations: ..., imports: ...,<br> exports: ..., providers: ..., bootstrap: ...})<br>class MyModule {}</code></td>
|
||||
<td><p>Defines a module that contains components, directives, pipes, and providers.</p>
|
||||
<td><p>Define un módulo que contiene componentes, directivas, pipes y proveedores.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><b>declarations:</b> [MyRedComponent, MyBlueComponent, MyDatePipe]</code></td>
|
||||
<td><p>List of components, directives, and pipes that belong to this module.</p>
|
||||
<td><p>Lista de componentes, directivas y pipes que pertenecen a este módulo.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><b>imports:</b> [BrowserModule, SomeOtherModule]</code></td>
|
||||
<td><p>List of modules to import into this module. Everything from the imported modules
|
||||
is available to <code>declarations</code> of this module.</p>
|
||||
<td><p>Lista de módulos para importar en este módulo. Todo, desde los módulos importados,
|
||||
está disponible para las declaraciones (<code>declarations</code>) de este módulo.
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><b>exports:</b> [MyRedComponent, MyDatePipe]</code></td>
|
||||
<td><p>List of components, directives, and pipes visible to modules that import this module.</p>
|
||||
<td><p>Lista de componentes, directivas y pipes visibles a los módulos que importan este módulo.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><b>providers:</b> [MyService, { provide: ... }]</code></td>
|
||||
<td><p>List of dependency injection providers visible both to the contents of this module and to importers of this module.</p>
|
||||
<td><p>Lista de proveedores de inyección de dependencias visibles tanto para los contenidos de este módulo como para los importadores de este módulo.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><b>entryComponents:</b> [SomeComponent, OtherComponent]</code></td>
|
||||
<td><p>List of components not referenced in any reachable template, for example dynamically created from code.</p></td>
|
||||
<td><p>Lista de componentes no referenciados en cualquier plantilla accesible, por ejemplo, creada dinámicamente a partir de código.</p></td>
|
||||
</tr><tr>
|
||||
<td><code><b>bootstrap:</b> [MyAppComponent]</code></td>
|
||||
<td><p>List of components to bootstrap when this module is bootstrapped.</p>
|
||||
<td><p>Lista de componentes a empaquetar cuando este módulo se inicia.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
|
||||
<table class="is-full-width is-fixed-layout">
|
||||
<tbody><tr>
|
||||
<th>Template syntax</th>
|
||||
<th>Sintaxis de plantilla</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code><input <b>[value]</b>="firstName"></code></td>
|
||||
<td><p>Binds property <code>value</code> to the result of expression <code>firstName</code>.</p>
|
||||
<td><p>Vincula la propiedad <code>value</code> al resultado de la expresión <code>firstName</code>.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><div <b>[attr.role]</b>="myAriaRole"></code></td>
|
||||
<td><p>Binds attribute <code>role</code> to the result of expression <code>myAriaRole</code>.</p>
|
||||
<td><p>Vincula el atributo <code>role</code> al resultado de la expresión <code>myAriaRole</code>.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><div <b>[class.extra-sparkle]</b>="isDelightful"></code></td>
|
||||
<td><p>Binds the presence of the CSS class <code>extra-sparkle</code> on the element to the truthiness of the expression <code>isDelightful</code>.</p>
|
||||
<td><p>Vincula la presencia de la clase CSS <code>extra-sparkle</code> sobre el elemento a la veracidad de la expresión <code>isDelightful</code>.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><div <b>[style.width.px]</b>="mySize"></code></td>
|
||||
<td><p>Binds style property <code>width</code> to the result of expression <code>mySize</code> in pixels. Units are optional.</p>
|
||||
<td><p>Vincula la propiedad de estilo <code>width</code> al resultado de la expresión <code>mySize</code> en píxeles. La unidad de medida es opcional.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><button <b>(click)</b>="readRainbow($event)"></code></td>
|
||||
<td><p>Calls method <code>readRainbow</code> when a click event is triggered on this button element (or its children) and passes in the event object.</p>
|
||||
<td><p>Llama al método <code>readRainbow</code> cuando se lanza un evento click en este elemento botón (o sus hijos) y pasa por argumento el objeto evento.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><div title="Hello <b>{{ponyName}}</b>"></code></td>
|
||||
<td><p>Binds a property to an interpolated string, for example, "Hello Seabiscuit". Equivalent to:
|
||||
<code><div [title]="'Hello ' + ponyName"></code></p>
|
||||
<td><code><div title="Hola <b>{{ponyName}}</b>"></code></td>
|
||||
<td><p>Vincula una propiedad a una cadena interpolada, por ejemplo, "Hola Seabiscuit". Equivalente a:
|
||||
<code><div [title]="'Hola ' + ponyName"></code></p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><p>Hello <b>{{ponyName}}</b></p></code></td>
|
||||
<td><p>Binds text content to an interpolated string, for example, "Hello Seabiscuit".</p>
|
||||
<td><code><p>Hola <b>{{ponyName}}</b></p></code></td>
|
||||
<td><p>Vincula el contenido de texto a una cadena interpolada, por ejemplo, "Hola Seabiscuit".</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><my-cmp <b>[(title)]</b>="name"></code></td>
|
||||
<td><p>Sets up two-way data binding. Equivalent to: <code><my-cmp [title]="name" (titleChange)="name=$event"></code></p>
|
||||
<td><p>Establece el two-way data binding. Equivalente a: <code><my-cmp [title]="name" (titleChange)="name=$event"></code></p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><video <b>#movieplayer</b> ...><br> <button <b>(click)</b>="movieplayer.play()"><br></video></code></td>
|
||||
<td><p>Creates a local variable <code>movieplayer</code> that provides access to the <code>video</code> element instance in data-binding and event-binding expressions in the current template.</p>
|
||||
<td><p>Crea una variable local <code>movieplayer</code> que provee acceso a la instancia del elemento <code>video</code> en las expresiones de data-binding y event-binding de la actual plantilla.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><p <b>*myUnless</b>="myExpression">...</p></code></td>
|
||||
<td><p>The <code>*</code> symbol turns the current element into an embedded template. Equivalent to:
|
||||
<td><p>El símbolo <code>*</code> convierte el elemento actual en una plantilla incrustada. Equivalente a:
|
||||
<code><ng-template [myUnless]="myExpression"><p>...</p></ng-template></code></p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><p>Card No.: <b>{{cardNumber | myCardNumberFormatter}}</b></p></code></td>
|
||||
<td><p>Transforms the current value of expression <code>cardNumber</code> via the pipe called <code>myCardNumberFormatter</code>.</p>
|
||||
<td><p>Transforma el valor actual de la expresión <code>cardNumber</code> a través de la pipe <code>myCardNumberFormatter</code>.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><p>Employer: <b>{{employer?.companyName}}</b></p></code></td>
|
||||
<td><p>The safe navigation operator (<code>?</code>) means that the <code>employer</code> field is optional and if <code>undefined</code>, the rest of the expression should be ignored.</p>
|
||||
<td><p>El operador de navegación seguro (<code>?</code>) significa que el campo <code>employer</code> es opcional y que si es <code>undefined</code>, el resto de la expresión debería ser ignorado.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><<b>svg:</b>rect x="0" y="0" width="100" height="100"/></code></td>
|
||||
<td><p>An SVG snippet template needs an <code>svg:</code> prefix on its root element to disambiguate the SVG element from an HTML component.</p>
|
||||
<td><p>Una plantilla de fragmento SVG necesita un prefijo <code>svg:</code> en su elemento raíz para distinguir el elemento SVG de un componente HTML.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><<b>svg</b>><br> <rect x="0" y="0" width="100" height="100"/><br></<b>svg</b>></code></td>
|
||||
<td><p>An <code><svg></code> root element is detected as an SVG element automatically, without the prefix.</p>
|
||||
<td><p>Un elemento raíz <code><svg></code> es detectado como un elemento SVG automáticamente, sin el prefijo.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
|
||||
<table class="is-full-width is-fixed-layout">
|
||||
<tbody><tr>
|
||||
<th>Built-in directives</th>
|
||||
<th>Directivas incorporadas</th>
|
||||
<th><p><code>import { CommonModule } from '@angular/common';</code>
|
||||
</p>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code><section <b>*ngIf</b>="showSection"></code></td>
|
||||
<td><p>Removes or recreates a portion of the DOM tree based on the <code>showSection</code> expression.</p>
|
||||
<td><p>Elimina o recrea una parte del árbol DOM basado en la expresión <code>showSection</code>.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><li <b>*ngFor</b>="let item of list"></code></td>
|
||||
<td><p>Turns the li element and its contents into a template, and uses that to instantiate a view for each item in list.</p>
|
||||
<td><p>Convierte el elemento li y su contenido en una plantilla, y lo utiliza para crear una vista por cada elemento de la lista.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><div <b>[ngSwitch]</b>="conditionExpression"><br> <ng-template <b>[<b>ngSwitchCase</b>]</b>="case1Exp">...</ng-template><br> <ng-template <b>ngSwitchCase</b>="case2LiteralString">...</ng-template><br> <ng-template <b>ngSwitchDefault</b>>...</ng-template><br></div></code></td>
|
||||
<td><p>Conditionally swaps the contents of the div by selecting one of the embedded templates based on the current value of <code>conditionExpression</code>.</p>
|
||||
<td><p>Intercambia condicionalmente el contenido del div seleccionando una de las plantillas incrustadas en función del valor actual de <code>conditionExpression</code>.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><div <b>[ngClass]</b>="{'active': isActive, 'disabled': isDisabled}"></code></td>
|
||||
<td><p>Binds the presence of CSS classes on the element to the truthiness of the associated map values. The right-hand expression should return {class-name: true/false} map.</p>
|
||||
<td><p>Vincula la presencia de clases CSS en el elemento a la veracidad de los valores de mapa asociados. La expresión de la derecha debería devolver el mapa {class-name: true/false}.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code><div <b>[ngStyle]</b>="{'property': 'value'}"></code><br><code><div <b>[ngStyle]</b>="dynamicStyles()"></code></td>
|
||||
<td><p>Allows you to assign styles to an HTML element using CSS. You can use CSS directly, as in the first example, or you can call a method from the component.</p>
|
||||
<p>Te permite asignar estilos a un elemento HTML usando CSS. Puedes usar CSS directamente, como en el primer ejemplo, o puedes llamar a un método desde el componente.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
|
||||
<table class="is-full-width is-fixed-layout">
|
||||
<tbody><tr>
|
||||
<th>Forms</th>
|
||||
<th>Formularios</th>
|
||||
<th><p><code>import { FormsModule } from '@angular/forms';</code>
|
||||
</p>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code><input <b>[(ngModel)]</b>="userName"></code></td>
|
||||
<td><p>Provides two-way data-binding, parsing, and validation for form controls.</p>
|
||||
<td><p>Provee two-way data-binding, conversión y validación para controles de formulario.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
|
||||
<table class="is-full-width is-fixed-layout">
|
||||
<tbody><tr>
|
||||
<th>Class decorators</th>
|
||||
<th>Decoradores de clases</th>
|
||||
<th><p><code>import { Directive, ... } from '@angular/core';</code>
|
||||
</p>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code><b>@Component({...})</b><br>class MyComponent() {}</code></td>
|
||||
<td><p>Declares that a class is a component and provides metadata about the component.</p>
|
||||
<td><p>Declara que una clase es un componente y proporciona metadatos sobre el componente.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><b>@Directive({...})</b><br>class MyDirective() {}</code></td>
|
||||
<td><p>Declares that a class is a directive and provides metadata about the directive.</p>
|
||||
<td><p>Declara que una clase es una directiva y proporciona metadatos sobre la directiva.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><b>@Pipe({...})</b><br>class MyPipe() {}</code></td>
|
||||
<td><p>Declares that a class is a pipe and provides metadata about the pipe.</p>
|
||||
<td><p>Declara que una clase es una pipe y proporciona metadatos sobre la pipe.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><b>@Injectable()</b><br>class MyService() {}</code></td>
|
||||
<td><p>Declares that a class can be provided and injected by other classes. Without this decorator, the compiler won't generate enough metadata to allow the class to be created properly when it's injected somewhere.</p>
|
||||
<td><p>Declara que una clase puede ser proporcionada e inyectada por otras clases. Sin este decorador, el compilador no generará suficientes metadatos para permitir que la clase se cree correctamente cuando se inyecta en alguna parte.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
|
||||
<table class="is-full-width is-fixed-layout">
|
||||
<tbody><tr>
|
||||
<th>Directive configuration</th>
|
||||
<th>Configuración de Directiva</th>
|
||||
<th><p><code>@Directive({ property1: value1, ... })</code>
|
||||
</p>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code><b>selector:</b> '.cool-button:not(a)'</code></td>
|
||||
<td><p>Specifies a CSS selector that identifies this directive within a template. Supported selectors include <code>element</code>,
|
||||
<td><p>Especifica un selector CSS que identifica esta directiva dentro de una plantilla. Los selectores compatibles incluyen <code>element</code>,
|
||||
<code>[attribute]</code>, <code>.class</code>, and <code>:not()</code>.</p>
|
||||
<p>Does not support parent-child relationship selectors.</p>
|
||||
<p>No soporta selectores de relación padre-hijo.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><b>providers:</b> [MyService, { provide: ... }]</code></td>
|
||||
<td><p>List of dependency injection providers for this directive and its children.</p>
|
||||
<td><p>Lista de proveedores de inyección de dependencia para esta directiva y sus hijos.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
|
||||
<table class="is-full-width is-fixed-layout">
|
||||
<tbody><tr>
|
||||
<th>Component configuration</th>
|
||||
<th>Configuración de Componente</th>
|
||||
<th><p>
|
||||
<code>@Component</code> extends <code>@Directive</code>,
|
||||
so the <code>@Directive</code> configuration applies to components as well</p>
|
||||
<code>@Component</code> extiende <code>@Directive</code>,
|
||||
entonces la configuración de <code>@Directive</code> se aplica también a los componentes</p>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code><b>moduleId:</b> module.id</code></td>
|
||||
<td><p>If set, the <code>templateUrl</code> and <code>styleUrl</code> are resolved relative to the component.</p>
|
||||
<td><p>Si está presente, el <code>templateUrl</code> y <code>styleUrl</code> se resuelven en relación con el componente.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><b>viewProviders:</b> [MyService, { provide: ... }]</code></td>
|
||||
<td><p>List of dependency injection providers scoped to this component's view.</p>
|
||||
<td><p>Lista de proveedores de inyección de dependencias en la vista de este componente.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><b>template:</b> 'Hello {{name}}'<br><b>templateUrl:</b> 'my-component.html'</code></td>
|
||||
<td><p>Inline template or external template URL of the component's view.</p>
|
||||
<td><code><b>template:</b> 'Hola {{name}}'<br><b>templateUrl:</b> 'my-component.html'</code></td>
|
||||
<td><p>Plantilla en línea o URL de plantilla externa de la vista del componente.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><b>styles:</b> ['.primary {color: red}']<br><b>styleUrls:</b> ['my-component.css']</code></td>
|
||||
<td><p>List of inline CSS styles or external stylesheet URLs for styling the component’s view.</p>
|
||||
<td><p>Lista de estilos CSS en línea o URL de hojas de estilo externas para estilar la vista del componente.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
|
||||
<table class="is-full-width is-fixed-layout">
|
||||
<tbody><tr>
|
||||
<th>Class field decorators for directives and components</th>
|
||||
<th>Decoradores para los campos de la clase para directivas y componentes.</th>
|
||||
<th><p><code>import { Input, ... } from '@angular/core';</code>
|
||||
</p>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code><b>@Input()</b> myProperty;</code></td>
|
||||
<td><p>Declares an input property that you can update via property binding (example:
|
||||
<td><p>Declara una propiedad de entrada (input) que puede actualizar mediante el enlace de propiedad (property binding) (ejemplo:
|
||||
<code><my-cmp [myProperty]="someExpression"></code>).</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><b>@Output()</b> myEvent = new EventEmitter();</code></td>
|
||||
<td><p>Declares an output property that fires events that you can subscribe to with an event binding (example: <code><my-cmp (myEvent)="doSomething()"></code>).</p>
|
||||
<td><p>Declara una propiedad de salida (output) que dispara eventos a los que puedes suscribirse con un enlace de evento (event binding) (ejemplo: <code><my-cmp (myEvent)="doSomething()"></code>).</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><b>@HostBinding('class.valid')</b> isValid;</code></td>
|
||||
<td><p>Binds a host element property (here, the CSS class <code>valid</code>) to a directive/component property (<code>isValid</code>).</p>
|
||||
<td><p>Vincula una propiedad del elemento anfitrión (aquí, la clase CSS <code>valid</code>) a una propiedad de directiva/componente (<code>isValid</code>).</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><b>@HostListener('click', ['$event'])</b> onClick(e) {...}</code></td>
|
||||
<td><p>Subscribes to a host element event (<code>click</code>) with a directive/component method (<code>onClick</code>), optionally passing an argument (<code>$event</code>).</p>
|
||||
<td><p>Se suscribe a un evento del elemento anfitrión (<code>click</code>) con un método de directiva/componente (<code>onClick</code>), opcionalmente, pasando un argumento (<code>$event</code>).</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><b>@ContentChild(myPredicate)</b> myChildComponent;</code></td>
|
||||
<td><p>Binds the first result of the component content query (<code>myPredicate</code>) to a property (<code>myChildComponent</code>) of the class.</p>
|
||||
<td><p>Vincula el primer resultado de la consulta de contenido del componente (<code>myPredicate</code>) a una propiedad (<code>myChildComponent</code>) de la clase.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><b>@ContentChildren(myPredicate)</b> myChildComponents;</code></td>
|
||||
<td><p>Binds the results of the component content query (<code>myPredicate</code>) to a property (<code>myChildComponents</code>) of the class.</p>
|
||||
<td><p>Vincula los resultados de la consulta de contenido del componente (<code>myPredicate</code>) a una propiedad (<code>myChildComponents</code>) de la clase.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><b>@ViewChild(myPredicate)</b> myChildComponent;</code></td>
|
||||
<td><p>Binds the first result of the component view query (<code>myPredicate</code>) to a property (<code>myChildComponent</code>) of the class. Not available for directives.</p>
|
||||
<td><p>Vincula el primer resultado de la consulta de vista del componente (<code>myPredicate</code>) a una propiedad (<code>myChildComponent</code>) de la clase. No disponible para directivas.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><b>@ViewChildren(myPredicate)</b> myChildComponents;</code></td>
|
||||
<td><p>Binds the results of the component view query (<code>myPredicate</code>) to a property (<code>myChildComponents</code>) of the class. Not available for directives.</p>
|
||||
<td><p>Vincula los resultados de la consulta de vista del componente (<code>myPredicate</code>) a una propiedad (<code>myChildComponents</code>) de la clase. No disponible para directivas.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
|
||||
<table class="is-full-width is-fixed-layout">
|
||||
<tbody><tr>
|
||||
<th>Directive and component change detection and lifecycle hooks</th>
|
||||
<th><p>(implemented as class methods)
|
||||
|
||||
<th>Detección de cambios (change detection) y ciclos de vida (lifecycle hooks) en directivas y componentes</th>
|
||||
<th><p>(implementado como métodos de clase)
|
||||
</p>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code><b>constructor(myService: MyService, ...)</b> { ... }</code></td>
|
||||
<td><p>Called before any other lifecycle hook. Use it to inject dependencies, but avoid any serious work here.</p>
|
||||
<td><p>Se llama antes que cualquier ciclo de vida. Úselo para inyectar dependencias, pero evite cualquier trabajo serio aquí.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><b>ngOnChanges(changeRecord)</b> { ... }</code></td>
|
||||
<td><p>Called after every change to input properties and before processing content or child views.</p>
|
||||
<td><p>Se llama después de cada cambio en las propiedades de entrada (input) y antes de procesar contenido o vistas de hijos.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><b>ngOnInit()</b> { ... }</code></td>
|
||||
<td><p>Called after the constructor, initializing input properties, and the first call to <code>ngOnChanges</code>.</p>
|
||||
<td><p>Se llama después del constructor, inicializando propiedades de entrada (input), y la primera llamada a <code>ngOnChanges</code>.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><b>ngDoCheck()</b> { ... }</code></td>
|
||||
<td><p>Called every time that the input properties of a component or a directive are checked. Use it to extend change detection by performing a custom check.</p>
|
||||
<td><p>Se llama cada vez que se verifican las propiedades de entrada (input) de un componente o una directiva. Úselo para extender la detección de cambios (change detection) realizando una verificación personalizada.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><b>ngAfterContentInit()</b> { ... }</code></td>
|
||||
<td><p>Called after <code>ngOnInit</code> when the component's or directive's content has been initialized.</p>
|
||||
<td><p>Se llama después de <code>ngOnInit</code> cuando el contenido del componente o directiva ha sido inicializado.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><b>ngAfterContentChecked()</b> { ... }</code></td>
|
||||
<td><p>Called after every check of the component's or directive's content.</p>
|
||||
<td><p>Se llama después de cada verificación del contenido del componente o directiva.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><b>ngAfterViewInit()</b> { ... }</code></td>
|
||||
<td><p>Called after <code>ngAfterContentInit</code> when the component's views and child views / the view that a directive is in has been initialized.</p>
|
||||
<td><p>Se llama después de <code>ngAfterContentInit</code> cuando las vistas del componente y las vistas hijas / la vista en la que se encuentra una directiva ha sido inicializado.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><b>ngAfterViewChecked()</b> { ... }</code></td>
|
||||
<td><p>Called after every check of the component's views and child views / the view that a directive is in.</p>
|
||||
<td><p>Se llama después de cada verificación de las vistas del componentes y las vistas hijas / la vista en la que se encuentra una directiva.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><b>ngOnDestroy()</b> { ... }</code></td>
|
||||
<td><p>Called once, before the instance is destroyed.</p>
|
||||
<td><p>Se llama una vez, antes de que la instancia se destruya.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
|
||||
<table class="is-full-width is-fixed-layout">
|
||||
<tbody><tr>
|
||||
<th>Dependency injection configuration</th>
|
||||
<th>Configuración de inyección de dependencia</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>{ <b>provide</b>: MyService, <b>useClass</b>: MyMockService }</code></td>
|
||||
<td><p>Sets or overrides the provider for <code>MyService</code> to the <code>MyMockService</code> class.</p>
|
||||
<td><p>Establece o sobre-escribe el proveedor para <code>MyService</code> en la clase <code>MyMockService</code>.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code>{ <b>provide</b>: MyService, <b>useFactory</b>: myFactory }</code></td>
|
||||
<td><p>Sets or overrides the provider for <code>MyService</code> to the <code>myFactory</code> factory function.</p>
|
||||
<td><p>Establece o sobre-escribe el proveedor para <code>MyService</code> en la factoría de función <code>myFactory</code>.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code>{ <b>provide</b>: MyValue, <b>useValue</b>: 41 }</code></td>
|
||||
<td><p>Sets or overrides the provider for <code>MyValue</code> to the value <code>41</code>.</p>
|
||||
<td><p>Establece o sobre-escribe el proveedor para <code>MyValue</code> al valor <code>41</code>.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
|
||||
<table class="is-full-width is-fixed-layout">
|
||||
<tbody><tr>
|
||||
<th>Routing and navigation</th>
|
||||
<th>Enrutamiento y navegación</th>
|
||||
<th><p><code>import { Routes, RouterModule, ... } from '@angular/router';</code>
|
||||
</p>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>const routes: <b>Routes</b> = [<br> { path: '', component: HomeComponent },<br> { path: 'path/:routeParam', component: MyComponent },<br> { path: 'staticPath', component: ... },<br> { path: '**', component: ... },<br> { path: 'oldPath', redirectTo: '/staticPath' },<br> { path: ..., component: ..., data: { message: 'Custom' } }<br>]);<br><br>const routing = RouterModule.forRoot(routes);</code></td>
|
||||
<td><p>Configures routes for the application. Supports static, parameterized, redirect, and wildcard routes. Also supports custom route data and resolve.</p>
|
||||
<td><p>Configura rutas para la aplicación. Soporta rutas estáticas, parametrizadas, de redireccionamiento y comodines. También soporta datos de ruta personalizados y los resuelve.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><br><<b>router-outlet</b>></<b>router-outlet</b>><br><<b>router-outlet</b> name="aux"></<b>router-outlet</b>><br></code></td>
|
||||
<td><p>Marks the location to load the component of the active route.</p>
|
||||
<td><p>Marca la ubicación para cargar el componente de la ruta activa.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><br><a routerLink="/path"><br><a <b>[routerLink]</b>="[ '/path', routeParam ]"><br><a <b>[routerLink]</b>="[ '/path', { matrixParam: 'value' } ]"><br><a <b>[routerLink]</b>="[ '/path' ]" [queryParams]="{ page: 1 }"><br><a <b>[routerLink]</b>="[ '/path' ]" fragment="anchor"><br></code></td>
|
||||
<td><p>Creates a link to a different view based on a route instruction consisting of a route path, required and optional parameters, query parameters, and a fragment. To navigate to a root route, use the <code>/</code> prefix; for a child route, use the <code>./</code>prefix; for a sibling or parent, use the <code>../</code> prefix.</p>
|
||||
<td><p>Crea un enlace a una vista diferente basada en una instrucción de ruta que consta de un camino de de ruta, parámetros obligatorios y opcionales, parámetros de consulta y un fragmento. Para navegar a un camino de ruta, usa el prefijo <code>/</code>; para una ruta hija, usa el prefijo <code>./</code>; para un padre o hermano, usa el prefijo <code>../</code>.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code><a [routerLink]="[ '/path' ]" routerLinkActive="active"></code></td>
|
||||
<td><p>The provided classes are added to the element when the <code>routerLink</code> becomes the current active route.</p>
|
||||
<td><p>Las clases proporcionadas se agregan al elemento cuando el <code>routerLink</code> se convierte en la ruta activa actual.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code>class <b>CanActivate</b>Guard implements <b>CanActivate</b> {<br> canActivate(<br> route: ActivatedRouteSnapshot,<br> state: RouterStateSnapshot<br> ): Observable<boolean|UrlTree>|Promise<boolean|UrlTree>|boolean|UrlTree { ... }<br>}<br><br>{ path: ..., canActivate: [<b>CanActivate</b>Guard] }</code></td>
|
||||
<td><p>An interface for defining a class that the router should call first to determine if it should activate this component. Should return a boolean|UrlTree or an Observable/Promise that resolves to a boolean|UrlTree.</p>
|
||||
<td><p>Una interfaz para definir una clase que el enrutador debe llamar primero para determinar si debe activar este componente. Debe devolver un boolean|UrlTree o un Observable/Promise que se resuelba en un boolean|UrlTree.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code>class <b>CanDeactivate</b>Guard implements <b>CanDeactivate</b><T> {<br> canDeactivate(<br> component: T,<br> route: ActivatedRouteSnapshot,<br> state: RouterStateSnapshot<br> ): Observable<boolean|UrlTree>|Promise<boolean|UrlTree>|boolean|UrlTree { ... }<br>}<br><br>{ path: ..., canDeactivate: [<b>CanDeactivate</b>Guard] }</code></td>
|
||||
<td><p>An interface for defining a class that the router should call first to determine if it should deactivate this component after a navigation. Should return a boolean|UrlTree or an Observable/Promise that resolves to a boolean|UrlTree.</p>
|
||||
<td><p>Una interfaz para definir una clase que el enrutador debería llamar primero para determinar si debería desactivar este componente después de una navegación. Debe devolver un boolean|UrlTree o un Observable/Promise que se resuelva a boolean|UrlTree.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code>class <b>CanActivateChild</b>Guard implements <b>CanActivateChild</b> {<br> canActivateChild(<br> route: ActivatedRouteSnapshot,<br> state: RouterStateSnapshot<br> ): Observable<boolean|UrlTree>|Promise<boolean|UrlTree>|boolean|UrlTree { ... }<br>}<br><br>{ path: ..., canActivateChild: [CanActivateGuard],<br> children: ... }</code></td>
|
||||
<td><p>An interface for defining a class that the router should call first to determine if it should activate the child route. Should return a boolean|UrlTree or an Observable/Promise that resolves to a boolean|UrlTree.</p>
|
||||
<td><p>Una interfaz para definir una clase que el enrutador debe llamar primero para determinar si debe activar la ruta hija. Debe devolver un boolean|UrlTree o un Observable/Promise que se resuelva en un boolean|UrlTree.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code>class <b>Resolve</b>Guard implements <b>Resolve</b><T> {<br> resolve(<br> route: ActivatedRouteSnapshot,<br> state: RouterStateSnapshot<br> ): Observable<any>|Promise<any>|any { ... }<br>}<br><br>{ path: ..., resolve: [<b>Resolve</b>Guard] }</code></td>
|
||||
<td><p>An interface for defining a class that the router should call first to resolve route data before rendering the route. Should return a value or an Observable/Promise that resolves to a value.</p>
|
||||
<td><p>Una interfaz para definir una clase que el enrutador debe llamar primero para resolver los datos de la ruta antes de representar la ruta. Debe devolver un valor o un Observable/Promise que se resuelva en un valor.</p>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td><code>class <b>CanLoad</b>Guard implements <b>CanLoad</b> {<br> canLoad(<br> route: Route<br> ): Observable<boolean|UrlTree>|Promise<boolean|UrlTree>|boolean|UrlTree { ... }<br>}<br><br>{ path: ..., canLoad: [<b>CanLoad</b>Guard], loadChildren: ... }</code></td>
|
||||
<td><p>An interface for defining a class that the router should call first to check if the lazy loaded module should be loaded. Should return a boolean|UrlTree or an Observable/Promise that resolves to a boolean|UrlTree.</p>
|
||||
<td><p>Una interfaz para definir una clase a la que el enrutador debe llamar primero para verificar si el módulo perezoso cargado (lazy loaded module) debe cargarse. Debe devolver un boolean|UrlTree o un Observable/Promise que se resuelva en un boolean|UrlTree.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
|
@ -42,11 +42,11 @@ For example, your `myBuilder` folder could contain the following files.
|
||||
| `src/my-builder.ts` | Main source file for the builder definition. |
|
||||
| `src/my-builder.spec.ts` | Source file for tests. |
|
||||
| `src/schema.json` | Definition of builder input options. |
|
||||
| `builders.json` | Testing configuration. |
|
||||
| `builders.json` | Builders definition. |
|
||||
| `package.json` | Dependencies. See https://docs.npmjs.com/files/package.json. |
|
||||
| `tsconfig.json` | [TypeScript configuration](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html). |
|
||||
|
||||
You can publish the builder to `npm` (see [Publishing your Library](https://angular.io/guide/creating-libraries#publishing-your-library)). If you publish it as `@example/my-builder`, you can install it using the following command.
|
||||
You can publish the builder to `npm` (see [Publishing your Library](guide/creating-libraries#publishing-your-library)). If you publish it as `@example/my-builder`, you can install it using the following command.
|
||||
|
||||
<code-example language="sh">
|
||||
|
||||
|
318
aio/content/guide/comparing-observables.en.md
Normal file
318
aio/content/guide/comparing-observables.en.md
Normal file
@ -0,0 +1,318 @@
|
||||
# Observables compared to other techniques
|
||||
|
||||
You can often use observables instead of promises to deliver values asynchronously. Similarly, observables can take the place of event handlers. Finally, because observables deliver multiple values, you can use them where you might otherwise build and operate on arrays.
|
||||
|
||||
Observables behave somewhat differently from the alternative techniques in each of these situations, but offer some significant advantages. Here are detailed comparisons of the differences.
|
||||
|
||||
## Observables compared to promises
|
||||
|
||||
Observables are often compared to promises. Here are some key differences:
|
||||
|
||||
* Observables are declarative; computation does not start until subscription. Promises execute immediately on creation. This makes observables useful for defining recipes that can be run whenever you need the result.
|
||||
|
||||
* Observables provide many values. Promises provide one. This makes observables useful for getting multiple values over time.
|
||||
|
||||
* Observables differentiate between chaining and subscription. Promises only have `.then()` clauses. This makes observables useful for creating complex transformation recipes to be used by other part of the system, without causing the work to be executed.
|
||||
|
||||
* Observables `subscribe()` is responsible for handling errors. Promises push errors to the child promises. This makes observables useful for centralized and predictable error handling.
|
||||
|
||||
|
||||
### Creation and subscription
|
||||
|
||||
* Observables are not executed until a consumer subscribes. The `subscribe()` executes the defined behavior once, and it can be called again. Each subscription has its own computation. Resubscription causes recomputation of values.
|
||||
|
||||
<code-example
|
||||
path="comparing-observables/src/observables.ts"
|
||||
header="src/observables.ts (observable)"
|
||||
region="observable">
|
||||
</code-example>
|
||||
|
||||
* Promises execute immediately, and just once. The computation of the result is initiated when the promise is created. There is no way to restart work. All `then` clauses (subscriptions) share the same computation.
|
||||
|
||||
<code-example
|
||||
path="comparing-observables/src/promises.ts"
|
||||
header="src/promises.ts (promise)"
|
||||
region="promise">
|
||||
</code-example>
|
||||
|
||||
### Chaining
|
||||
|
||||
* Observables differentiate between transformation function such as a map and subscription. Only subscription activates the subscriber function to start computing the values.
|
||||
|
||||
<code-example
|
||||
path="comparing-observables/src/observables.ts"
|
||||
header="src/observables.ts (chain)"
|
||||
region="chain">
|
||||
</code-example>
|
||||
|
||||
* Promises do not differentiate between the last `.then` clauses (equivalent to subscription) and intermediate `.then` clauses (equivalent to map).
|
||||
|
||||
<code-example
|
||||
path="comparing-observables/src/promises.ts"
|
||||
header="src/promises.ts (chain)"
|
||||
region="chain">
|
||||
</code-example>
|
||||
|
||||
### Cancellation
|
||||
|
||||
* Observable subscriptions are cancellable. Unsubscribing removes the listener from receiving further values, and notifies the subscriber function to cancel work.
|
||||
|
||||
<code-example
|
||||
path="comparing-observables/src/observables.ts"
|
||||
header="src/observables.ts (unsubcribe)"
|
||||
region="unsubscribe">
|
||||
</code-example>
|
||||
|
||||
* Promises are not cancellable.
|
||||
|
||||
### Error handling
|
||||
|
||||
* Observable execution errors are delivered to the subscriber's error handler, and the subscriber automatically unsubscribes from the observable.
|
||||
|
||||
<code-example
|
||||
path="comparing-observables/src/observables.ts"
|
||||
header="src/observables.ts (error)"
|
||||
region="error">
|
||||
</code-example>
|
||||
|
||||
* Promises push errors to the child promises.
|
||||
|
||||
<code-example
|
||||
path="comparing-observables/src/promises.ts"
|
||||
header="src/promises.ts (error)"
|
||||
region="error">
|
||||
</code-example>
|
||||
|
||||
### Cheat sheet
|
||||
|
||||
The following code snippets illustrate how the same kind of operation is defined using observables and promises.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Operation</th>
|
||||
<th>Observable</th>
|
||||
<th>Promise</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Creation</td>
|
||||
<td>
|
||||
<pre>
|
||||
new Observable((observer) => {
|
||||
observer.next(123);
|
||||
});</pre>
|
||||
</td>
|
||||
<td>
|
||||
<pre>
|
||||
new Promise((resolve, reject) => {
|
||||
resolve(123);
|
||||
});</pre>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Transform</td>
|
||||
<td><pre>obs.pipe(map((value) => value * 2));</pre></td>
|
||||
<td><pre>promise.then((value) => value * 2);</pre></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Subscribe</td>
|
||||
<td>
|
||||
<pre>
|
||||
sub = obs.subscribe((value) => {
|
||||
console.log(value)
|
||||
});</pre>
|
||||
</td>
|
||||
<td>
|
||||
<pre>
|
||||
promise.then((value) => {
|
||||
console.log(value);
|
||||
});</pre>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Unsubscribe</td>
|
||||
<td><pre>sub.unsubscribe();</pre></td>
|
||||
<td>Implied by promise resolution.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## Observables compared to events API
|
||||
|
||||
Observables are very similar to event handlers that use the events API. Both techniques define notification handlers, and use them to process multiple values delivered over time. Subscribing to an observable is equivalent to adding an event listener. One significant difference is that you can configure an observable to transform an event before passing the event to the handler.
|
||||
|
||||
Using observables to handle events and asynchronous operations can have the advantage of greater consistency in contexts such as HTTP requests.
|
||||
|
||||
Here are some code samples that illustrate how the same kind of operation is defined using observables and the events API.
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Observable</th>
|
||||
<th>Events API</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Creation & cancellation</td>
|
||||
<td>
|
||||
<pre>// Setup
|
||||
let clicks$ = fromEvent(buttonEl, ‘click’);
|
||||
// Begin listening
|
||||
let subscription = clicks$
|
||||
.subscribe(e => console.log(‘Clicked’, e))
|
||||
// Stop listening
|
||||
subscription.unsubscribe();</pre>
|
||||
</td>
|
||||
<td>
|
||||
<pre>function handler(e) {
|
||||
console.log(‘Clicked’, e);
|
||||
}
|
||||
// Setup & begin listening
|
||||
button.addEventListener(‘click’, handler);
|
||||
// Stop listening
|
||||
button.removeEventListener(‘click’, handler);
|
||||
</pre>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Subscription</td>
|
||||
<td>
|
||||
<pre>observable.subscribe(() => {
|
||||
// notification handlers here
|
||||
});</pre>
|
||||
</td>
|
||||
<td>
|
||||
<pre>element.addEventListener(eventName, (event) => {
|
||||
// notification handler here
|
||||
});</pre>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Configuration</td>
|
||||
<td>Listen for keystrokes, but provide a stream representing the value in the input.
|
||||
<pre>fromEvent(inputEl, 'keydown').pipe(
|
||||
map(e => e.target.value)
|
||||
);</pre>
|
||||
</td>
|
||||
<td>Does not support configuration.
|
||||
<pre>element.addEventListener(eventName, (event) => {
|
||||
// Cannot change the passed Event into another
|
||||
// value before it gets to the handler
|
||||
});</pre>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
## Observables compared to arrays
|
||||
|
||||
An observable produces values over time. An array is created as a static set of values. In a sense, observables are asynchronous where arrays are synchronous. In the following examples, ➞ implies asynchronous value delivery.
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Observable</th>
|
||||
<th>Array</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Given</td>
|
||||
<td>
|
||||
<pre>obs: ➞1➞2➞3➞5➞7</pre>
|
||||
<pre>obsB: ➞'a'➞'b'➞'c'</pre>
|
||||
</td>
|
||||
<td>
|
||||
<pre>arr: [1, 2, 3, 5, 7]</pre>
|
||||
<pre>arrB: ['a', 'b', 'c']</pre>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><pre>concat()</pre></td>
|
||||
<td>
|
||||
<pre>concat(obs, obsB)</pre>
|
||||
<pre>➞1➞2➞3➞5➞7➞'a'➞'b'➞'c'</pre>
|
||||
</td>
|
||||
<td>
|
||||
<pre>arr.concat(arrB)</pre>
|
||||
<pre>[1,2,3,5,7,'a','b','c']</pre>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><pre>filter()</pre></td>
|
||||
<td>
|
||||
<pre>obs.pipe(filter((v) => v>3))</pre>
|
||||
<pre>➞5➞7</pre>
|
||||
</td>
|
||||
<td>
|
||||
<pre>arr.filter((v) => v>3)</pre>
|
||||
<pre>[5, 7]</pre>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><pre>find()</pre></td>
|
||||
<td>
|
||||
<pre>obs.pipe(find((v) => v>3))</pre>
|
||||
<pre>➞5</pre>
|
||||
</td>
|
||||
<td>
|
||||
<pre>arr.find((v) => v>3)</pre>
|
||||
<pre>5</pre>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><pre>findIndex()</pre></td>
|
||||
<td>
|
||||
<pre>obs.pipe(findIndex((v) => v>3))</pre>
|
||||
<pre>➞3</pre>
|
||||
</td>
|
||||
<td>
|
||||
<pre>arr.findIndex((v) => v>3)</pre>
|
||||
<pre>3</pre>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><pre>forEach()</pre></td>
|
||||
<td>
|
||||
<pre>obs.pipe(tap((v) => {
|
||||
console.log(v);
|
||||
}))
|
||||
1
|
||||
2
|
||||
3
|
||||
5
|
||||
7</pre>
|
||||
</td>
|
||||
<td>
|
||||
<pre>arr.forEach((v) => {
|
||||
console.log(v);
|
||||
})
|
||||
1
|
||||
2
|
||||
3
|
||||
5
|
||||
7</pre>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><pre>map()</pre></td>
|
||||
<td>
|
||||
<pre>obs.pipe(map((v) => -v))</pre>
|
||||
<pre>➞-1➞-2➞-3➞-5➞-7</pre>
|
||||
</td>
|
||||
<td>
|
||||
<pre>arr.map((v) => -v)</pre>
|
||||
<pre>[-1, -2, -3, -5, -7]</pre>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><pre>reduce()</pre></td>
|
||||
<td>
|
||||
<pre>obs.pipe(reduce((s,v)=> s+v, 0))</pre>
|
||||
<pre>➞18</pre>
|
||||
</td>
|
||||
<td>
|
||||
<pre>arr.reduce((s,v) => s+v, 0)</pre>
|
||||
<pre>18</pre>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
@ -1,25 +1,24 @@
|
||||
# Observables compared to other techniques
|
||||
# Observables en comparación con otras técnicas
|
||||
|
||||
You can often use observables instead of promises to deliver values asynchronously. Similarly, observables can take the place of event handlers. Finally, because observables deliver multiple values, you can use them where you might otherwise build and operate on arrays.
|
||||
A menudo puedes usar observables en lugar de promesas para entregar valores de forma asíncrona. Del mismo modo, los observables pueden reemplazar a los controladores de eventos. Finalmente, porque los observables entregan múltiples valores, puedes usarlos donde de otro modo podrías construir y operar con arrays.
|
||||
|
||||
Observables behave somewhat differently from the alternative techniques in each of these situations, but offer some significant advantages. Here are detailed comparisons of the differences.
|
||||
Los observables se comportan de manera algo diferente a las técnicas alternativas en cada una de estas situaciones, pero ofrecen algunas ventajas significativas. Aquí hay comparaciones detalladas de las diferencias.
|
||||
|
||||
## Observables compared to promises
|
||||
## Observables en comparación con promesas
|
||||
|
||||
Observables are often compared to promises. Here are some key differences:
|
||||
Los observables a menudo se comparan con las promesas. Aquí hay algunas diferencias clave:
|
||||
|
||||
* Observables are declarative; computation does not start until subscription. Promises execute immediately on creation. This makes observables useful for defining recipes that can be run whenever you need the result.
|
||||
* Los observables son declarativos; La ejecución no comienza hasta la suscripción. Las promesas se ejecutan inmediatamente después de la creación. Esto hace que los observables sean útiles para definir recetas que se pueden ejecutar cuando necesites el resultado.
|
||||
|
||||
* Observables provide many values. Promises provide one. This makes observables useful for getting multiple values over time.
|
||||
* Los observables proporcionan muchos valores. Las promesas proporcionan un valor. Esto hace que los observables sean útiles para obtener múltiples valores a lo largo del tiempo.
|
||||
|
||||
* Observables differentiate between chaining and subscription. Promises only have `.then()` clauses. This makes observables useful for creating complex transformation recipes to be used by other part of the system, without causing the work to be executed.
|
||||
* Los observables diferencian entre encadenamiento y suscripción. Las promesas solo tienen cláusulas `.then ()`. Esto hace que los observables sean útiles para crear recetas de transformación complejas para ser utilizadas por otra parte del sistema, sin que el trabajo se ejecute.
|
||||
|
||||
* Observables `subscribe()` is responsible for handling errors. Promises push errors to the child promises. This makes observables useful for centralized and predictable error handling.
|
||||
* Observables `subscribe()` es responsable de manejar los errores. Las promesas empujan los errores a promesas hijas. Esto hace que los observables sean útiles para el manejo centralizado y predecible de errores.
|
||||
|
||||
### Creación y suscripción
|
||||
|
||||
### Creation and subscription
|
||||
|
||||
* Observables are not executed until a consumer subscribes. The `subscribe()` executes the defined behavior once, and it can be called again. Each subscription has its own computation. Resubscription causes recomputation of values.
|
||||
* Los observables no se ejecutan hasta que un consumidor se suscribe. El `subscribe()` ejecuta el comportamiento definido una vez, y se puede volver a llamar. Cada suscripción tiene su propia computación. La resuscripción provoca la recomputación de los valores.
|
||||
|
||||
<code-example
|
||||
path="comparing-observables/src/observables.ts"
|
||||
@ -27,7 +26,7 @@ Observables are often compared to promises. Here are some key differences:
|
||||
region="observable">
|
||||
</code-example>
|
||||
|
||||
* Promises execute immediately, and just once. The computation of the result is initiated when the promise is created. There is no way to restart work. All `then` clauses (subscriptions) share the same computation.
|
||||
* Las promesas se ejecutan de inmediato, y solo una vez. La computación del resultado se inicia cuando se crea la promesa. No hay forma de reiniciar el trabajo. Todas las cláusulas `then` (suscripciones) comparten la misma computación.
|
||||
|
||||
<code-example
|
||||
path="comparing-observables/src/promises.ts"
|
||||
@ -35,9 +34,9 @@ Observables are often compared to promises. Here are some key differences:
|
||||
region="promise">
|
||||
</code-example>
|
||||
|
||||
### Chaining
|
||||
### Encadenamiento
|
||||
|
||||
* Observables differentiate between transformation function such as a map and subscription. Only subscription activates the subscriber function to start computing the values.
|
||||
* Los observables diferencian entre la función de transformación, como `map` y `subscription`. Solo la suscripción activa la función de suscriptor para comenzar a calcular los valores.
|
||||
|
||||
<code-example
|
||||
path="comparing-observables/src/observables.ts"
|
||||
@ -45,7 +44,7 @@ Observables are often compared to promises. Here are some key differences:
|
||||
region="chain">
|
||||
</code-example>
|
||||
|
||||
* Promises do not differentiate between the last `.then` clauses (equivalent to subscription) and intermediate `.then` clauses (equivalent to map).
|
||||
* Las promesas no diferencian entre las últimas cláusulas `.then` (equivalentes al subscription) y las cláusulas intermedias `.then` (equivalentes al map).
|
||||
|
||||
<code-example
|
||||
path="comparing-observables/src/promises.ts"
|
||||
@ -53,9 +52,9 @@ Observables are often compared to promises. Here are some key differences:
|
||||
region="chain">
|
||||
</code-example>
|
||||
|
||||
### Cancellation
|
||||
### Cancelación
|
||||
|
||||
* Observable subscriptions are cancellable. Unsubscribing removes the listener from receiving further values, and notifies the subscriber function to cancel work.
|
||||
* Las suscripciones de los observables son cancelables. La cancelación de la suscripción evita que el oyente reciba más valores y notifica a la función del suscriptor que cancele el trabajo.
|
||||
|
||||
<code-example
|
||||
path="comparing-observables/src/observables.ts"
|
||||
@ -63,11 +62,11 @@ Observables are often compared to promises. Here are some key differences:
|
||||
region="unsubscribe">
|
||||
</code-example>
|
||||
|
||||
* Promises are not cancellable.
|
||||
* Las promesas no son cancelables.
|
||||
|
||||
### Error handling
|
||||
### Manejo de errores
|
||||
|
||||
* Observable execution errors are delivered to the subscriber's error handler, and the subscriber automatically unsubscribes from the observable.
|
||||
* Los errores de ejecución en observables se entregan al controlador de errores del suscriptor, y el suscriptor cancela automáticamente la suscripción del observable.
|
||||
|
||||
<code-example
|
||||
path="comparing-observables/src/observables.ts"
|
||||
@ -75,7 +74,7 @@ Observables are often compared to promises. Here are some key differences:
|
||||
region="error">
|
||||
</code-example>
|
||||
|
||||
* Promises push errors to the child promises.
|
||||
* Las promesas empujan los errores a las promesas hijas.
|
||||
|
||||
<code-example
|
||||
path="comparing-observables/src/promises.ts"
|
||||
@ -83,9 +82,9 @@ Observables are often compared to promises. Here are some key differences:
|
||||
region="error">
|
||||
</code-example>
|
||||
|
||||
### Cheat sheet
|
||||
### Hoja de trucos
|
||||
|
||||
The following code snippets illustrate how the same kind of operation is defined using observables and promises.
|
||||
Los siguientes fragmentos de código ilustran cómo se define el mismo tipo de operación utilizando observables y promesas.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
@ -139,13 +138,13 @@ promise.then((value) => {
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## Observables compared to events API
|
||||
## Observables en comparación con eventos API
|
||||
|
||||
Observables are very similar to event handlers that use the events API. Both techniques define notification handlers, and use them to process multiple values delivered over time. Subscribing to an observable is equivalent to adding an event listener. One significant difference is that you can configure an observable to transform an event before passing the event to the handler.
|
||||
Los observables son muy similares a los controladores de eventos que usan la API de eventos. Ambas técnicas definen manejadores de notificaciones y las utilizan para procesar múltiples valores entregados a lo largo del tiempo. Suscribirse a un observable es equivalente a agregar un detector de eventos. Una diferencia significativa es que puedes configurar un observable para transformar un evento antes de pasar el evento al controlador.
|
||||
|
||||
Using observables to handle events and asynchronous operations can have the advantage of greater consistency in contexts such as HTTP requests.
|
||||
El uso de observables para manejar eventos y operaciones asíncronas puede tener la ventaja de una mayor coherencia en contextos como las solicitudes HTTP.
|
||||
|
||||
Here are some code samples that illustrate how the same kind of operation is defined using observables and the events API.
|
||||
Aquí hay algunos ejemplos de código que ilustran cómo se define el mismo tipo de operación usando observables y la API de eventos.
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
@ -205,9 +204,9 @@ button.removeEventListener(‘click’, handler);
|
||||
</table>
|
||||
|
||||
|
||||
## Observables compared to arrays
|
||||
## Observables en comparación con arrays
|
||||
|
||||
An observable produces values over time. An array is created as a static set of values. In a sense, observables are asynchronous where arrays are synchronous. In the following examples, ➞ implies asynchronous value delivery.
|
||||
Un observable produce valores a lo largo del tiempo. Se crea un array como un conjunto estático de valores. En cierto sentido, los observables son asíncronos mientras que los arrays son síncronos. En los siguientes ejemplos, ➞ implica entrega de valor asíncrono.
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
|
@ -14,7 +14,7 @@ established through the components' [view objects](guide/glossary#view).
|
||||
Each component has a *host view*, and can have additional *embedded views*.
|
||||
An embedded view in component A is the
|
||||
host view of component B, which can in turn have embedded view.
|
||||
This means that there is a [view hierarchy](guide/glossary#view-hierarchy) for each component,
|
||||
This means that there is a [view hierarchy](guide/glossary#view-tree) for each component,
|
||||
of which that component's host view is the root.
|
||||
|
||||
There is an API for navigating *down* the view hierarchy.
|
||||
|
@ -91,7 +91,7 @@ Angular components, templates, and styles go here.
|
||||
|
||||
| `src/app/` FILES | PURPOSE |
|
||||
| :-------------------------- | :------------------------------------------|
|
||||
| `app/app.component.ts` | Defines the logic for the app's root component, named `AppComponent`. The view associated with this root component becomes the root of the [view hierarchy](guide/glossary#view-hierarchy) as you add components and services to your application. |
|
||||
| `app/app.component.ts` | Defines the logic for the app's root component, named `AppComponent`. The view associated with this root component becomes the root of the [view hierarchy](guide/glossary#view-tree) as you add components and services to your application. |
|
||||
| `app/app.component.html` | Defines the HTML template associated with the root `AppComponent`. |
|
||||
| `app/app.component.css` | Defines the base CSS stylesheet for the root `AppComponent`. |
|
||||
| `app/app.component.spec.ts` | Defines a unit test for the root `AppComponent`. |
|
||||
|
1090
aio/content/guide/glossary.en.md
Normal file
1090
aio/content/guide/glossary.en.md
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -39,7 +39,7 @@ To prepare your app for translations, you should have a basic understanding of t
|
||||
|
||||
* [Templates](guide/glossary#template "Definition of a template")
|
||||
* [Components](guide/glossary#component "Definition of a component")
|
||||
* [Angular CLI](guide/glossary#command-line-interface-cli "Definition of CLI") command-line tool for managing the Angular development cycle
|
||||
* [Angular CLI](guide/glossary#cli "Definition of CLI") command-line tool for managing the Angular development cycle
|
||||
* [Extensible Markup Language (XML)](https://www.w3.org/XML/ "W3C: Extensible Markup Language (XML)") used for translation files
|
||||
|
||||
## Steps to localize your app
|
||||
@ -58,7 +58,7 @@ While following these steps, you can [explore the translated example app](#app-p
|
||||
|
||||
The following are optional practices that may be required in special cases:
|
||||
|
||||
* [Set the source locale manually](#set-source-manually) if you need to set the [LOCALE_ID](https://angular.io/api/core/LOCALE_ID "API reference for LOCALE_ID") token.
|
||||
* [Set the source locale manually](#set-source-manually) if you need to set the [LOCALE_ID](api/core/LOCALE_ID "API reference for LOCALE_ID") token.
|
||||
* [Import global variants of the locale data](#import-locale) for extra locale data.
|
||||
* [Manage marked text with custom IDs](#custom-id) if you require more control over matching translations.
|
||||
|
||||
@ -77,7 +77,7 @@ This command updates your project's `package.json` and `polyfills.ts` files to i
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
For more information about `package.json` and polyfill packages, see [Workspace npm dependencies](https://angular.io/guide/npm-packages).
|
||||
For more information about `package.json` and polyfill packages, see [Workspace npm dependencies](guide/npm-packages).
|
||||
|
||||
</div>
|
||||
|
||||
@ -534,13 +534,13 @@ The following example shows both translation units after translating:
|
||||
|
||||
## Merge translations into the app
|
||||
|
||||
To merge the completed translations into the app, use the [Angular CLI](guide/glossary#command-line-interface-cli "Definition of CLI") to build a copy of the app's distributable files for each locale.
|
||||
To merge the completed translations into the app, use the [Angular CLI](guide/glossary#cli "Definition of CLI") to build a copy of the app's distributable files for each locale.
|
||||
The build process replaces the original text with translated text, and sets the `LOCALE_ID` token for each distributable copy of the app.
|
||||
It also loads and registers the locale data.
|
||||
|
||||
After merging, you can serve each distributable copy of the app using server-side language detection or different subdirectories, as described in the next section about [deploying multiple locales](#deploy-locales).
|
||||
|
||||
The build process uses [ahead-of-time (AOT) compilation](guide/glossary#ahead-of-time-aot-compilation) to produce a small, fast,
|
||||
The build process uses [ahead-of-time (AOT) compilation](guide/glossary#aot) to produce a small, fast,
|
||||
ready-to-run app. With Ivy in Angular version 9, AOT is used by default for both
|
||||
development and production builds, and AOT is required to localize component templates.
|
||||
|
||||
@ -610,7 +610,7 @@ To use your locale definition in the build configuration, use the `"localize"` o
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
Note: [Ahead-of-time (AOT) compilation](guide/glossary#ahead-of-time-aot-compilation) is required to localize component templates.
|
||||
Note: [Ahead-of-time (AOT) compilation](guide/glossary#aot) is required to localize component templates.
|
||||
If you changed this setting, set `"aot"` to `true` in order to use AOT.
|
||||
|
||||
</div>
|
||||
@ -804,7 +804,7 @@ The following tabs show the example app and its translation files:
|
||||
|
||||
The following are optional practices that may be required in special cases:
|
||||
|
||||
* [Set the source locale manually](#set-source-manually) by setting the [LOCALE_ID](https://angular.io/api/core/LOCALE_ID "API reference for LOCALE_ID") token.
|
||||
* [Set the source locale manually](#set-source-manually) by setting the [LOCALE_ID](api/core/LOCALE_ID "API reference for LOCALE_ID") token.
|
||||
* [Import global variants of the locale data](#import-locale) for extra locale data.
|
||||
* [Manage marked text with custom IDs](#custom-id) if you require more control over matching translations.
|
||||
|
||||
|
@ -457,7 +457,7 @@ The object reference did not change when the value of its own `name` property ch
|
||||
|
||||
### Responding to view changes
|
||||
|
||||
As Angular traverses the [view hierarchy](guide/glossary#view-hierarchy "Definition of view hierarchy definition") during change detection, it needs to be sure that a change in a child does not attempt to cause a change in its own parent. Such a change would not be rendered properly, because of how [unidirectional data flow](guide/glossary#unidirectional-data-flow "Definition") works.
|
||||
As Angular traverses the [view hierarchy](guide/glossary#view-tree "Definition of view hierarchy definition") during change detection, it needs to be sure that a change in a child does not attempt to cause a change in its own parent. Such a change would not be rendered properly, because of how [unidirectional data flow](guide/glossary#unidirectional-data-flow "Definition") works.
|
||||
|
||||
If you need to make a change that inverts the expected data flow, you must trigger a new change detection cycle to allow that change to be rendered.
|
||||
The examples illustrate how to make such changes safely.
|
||||
|
@ -146,7 +146,7 @@ You can then use your custom pipe in template expressions, the same way you use
|
||||
|
||||
### Marking a class as a pipe
|
||||
|
||||
To mark a class as a pipe and supply configuration metadata, apply the [`@Pipe`](/api/core/Pipe "API reference for Pipe") [decorator](/guide/glossary#decorator--decoration "Definition for decorator") to the class.
|
||||
To mark a class as a pipe and supply configuration metadata, apply the [`@Pipe`](/api/core/Pipe "API reference for Pipe") [decorator](/guide/glossary#decorator "Definition for decorator") to the class.
|
||||
Use [UpperCamelCase](guide/glossary#case-types "Definition of case types") (the general convention for class names) for the pipe class name, and [camelCase](guide/glossary#case-types "Definition of case types") for the corresponding `name` string.
|
||||
Do not use hyphens in the `name`.
|
||||
For details and more examples, see [Pipe names](guide/styleguide#pipe-names "Pipe names in the Angular coding style guide").
|
||||
|
93
aio/content/guide/roadmap.md
Normal file
93
aio/content/guide/roadmap.md
Normal file
@ -0,0 +1,93 @@
|
||||
# Angular Roadmap
|
||||
|
||||
Angular receives a large number of feature requests, both from inside Google and from the broader open-source community. At the same time, our list of projects contains plenty of maintenance tasks, code refactorings, potential performance improvements, and so on. We bring together representatives from developer relations, product management, and engineering to prioritize this list. As new projects come into the queue, we regularly position them based on relative priority to other projects. As work gets done, projects will move up in the queue.
|
||||
|
||||
The projects below are not associated with a particular Angular version. We'll release them on completion, and they will be part of a specific version based on our release schedule, following semantic versioning. For example, features are released in the next minor after they are complete, or the next major if they include breaking changes.
|
||||
|
||||
## In Progress
|
||||
|
||||
### Operation Bye Bye Backlog (aka Operation Byelog)
|
||||
|
||||
We are actively investing up to 50% of our engineering capacity on triaging issues and PRs until we have a clear understanding of broader community needs. After that, we'll commit up to 20% of our engineering capacity to keep up with new submissions promptly.
|
||||
|
||||
### Support TypeScript 4.0
|
||||
|
||||
We're working on adding support for TypeScript 4.0 ahead of its stable release. We always want Angular to stay up-to-date with the latest version of TypeScript so that developers get the best the language has to offer.
|
||||
|
||||
### Update our e2e testing strategy
|
||||
|
||||
To ensure we provide a future-proof e2e testing strategy, we want to evaluate the state of Protractor, community innovations, e2e best practices, and explore novel opportunities.
|
||||
|
||||
### Angular libraries use Ivy
|
||||
|
||||
We are investing in the design and development of Ivy library distribution plan, which will include an update of the library package format to use Ivy compilation, unblock the deprecation of the View Engine library format, and [ngcc](guide/glossary#ngcc).
|
||||
|
||||
### Evaluate future RxJS changes (v7 and beyond)
|
||||
|
||||
We want to ensure Angular developers are taking advantage of the latest capabilities of RxJS and have a smooth transition to the next major releases of the framework. For this purpose, we will explore and document the scope of the changes in v7 and beyond of RxJS and plan an update strategy.
|
||||
|
||||
### Angular language service uses Ivy
|
||||
|
||||
Today the language service still uses the View Engine compiler and type checking, even for Ivy applications. We want to use the Ivy template parser and improved type checking for the Angular Language service to match application behavior. This migration will also be a step towards unblocking the removal of View Engine, which will simplify Angular, reduce the npm package size, and improve the framework's maintainability.
|
||||
|
||||
### Expand component harnesses best practices
|
||||
|
||||
Angular CDK introduced the concept of [component test harnesses](https://material.angular.io/cdk/test-harnesses) to Angular in version 9. Test harnesses allow component authors to create supported APIs for testing component interactions. We're continuing to improve this harness infrastructure and clarifying the best practices around using harnesses. We're also working to drive more harness adoption inside of Google.
|
||||
|
||||
### Support native [Trusted Types](https://web.dev/trusted-types/) in Angular
|
||||
|
||||
In collaboration with Google's security team, we're adding support for the new Trusted Types API. This web platform API will help developers build more secure web applications.
|
||||
|
||||
### Integrate [MDC Web](https://material.io/develop/web/) into Angular Material
|
||||
|
||||
MDC Web is a library created by Google's Material Design team that provides reusable primitives for building Material Design components. The Angular team is incorporating these primitives into Angular Material. Using MDC Web will align Angular Material more closely with the Material Design specification, expand accessibility, overall improve component quality, and improve our team's velocity.
|
||||
|
||||
### Offer Google engineers better integration with Angular and Google's internal server stack
|
||||
|
||||
This is an internal project to add support for Angular front-ends to Google's internal integrated server stack.
|
||||
|
||||
### Angular versioning & branching
|
||||
|
||||
We want to consolidate release management tooling between Angular's multiple GitHub repositories ([angular/angular](https://github.com/angular/angular), [angular/angular-cli](https://github.com/angular/angular-cli), and [angular/components](https://github.com/angular/components)). This effort will allow us to reuse infrastructure, unify and simplify processes, and improve our release process's reliability.
|
||||
|
||||
## Future
|
||||
|
||||
### Refresh introductory documentation
|
||||
|
||||
We will redefine the user learning journeys and refresh the introductory documentation. We will clearly state the benefits of Angular, how to explore its capabilities, and provide guidance so developers can become proficient with the framework in as little time as possible.
|
||||
|
||||
### Strict typing for `@angular/forms`
|
||||
|
||||
We will work on implementing stricter type checking for reactive forms. This way, we will allow developers to catch more issues during development time, enable better text editor and IDE support, and improve the type checking for reactive forms.
|
||||
|
||||
### webpack 5 in the Angular CLI
|
||||
|
||||
Webpack 5 brings a lot of build speed and bundle size improvements. To make them available for Angular developers, we will invest in migrating Angular CLI from using deprecated and removed webpack APIs.
|
||||
|
||||
### Commit message standardization
|
||||
|
||||
We want to unify commit message requirements and conformance across Angular repositories ([angular/angular](https://github.com/angular/angular), [angular/components](https://github.com/angular/components), [angular/angular-cli](https://github.com/angular/angular-cli)) to bring consistency to our development process and reuse infrastructure tooling.
|
||||
|
||||
### Optional Zone.js
|
||||
|
||||
We are going to design and implement a plan to make Zone.js optional from Angular applications. This way, we will simplify the framework, improve debugging, and reduce application bundle size. Additionally, this will allow us to take advantage of native async/await syntax, which currently Zone.js does not support.
|
||||
|
||||
### Remove legacy [View Engine](guide/ivy)
|
||||
|
||||
After the transition of all our internal tooling to Ivy has completed, we want to remove the legacy View Engine for smaller Angular conceptual overhead, smaller package size, lower maintenance cost, and lower complexity of the codebase.
|
||||
|
||||
### Angular DevTools
|
||||
|
||||
We’ll be working on development tooling for Angular that will provide utilities for debugging and performance profiling. This project aims to help developers understand the component structure and the change detection in an Angular application.
|
||||
|
||||
### Optional NgModules
|
||||
|
||||
To simplify the Angular mental model and learning journey, we’ll be working on making NgModules optional. This work will allow developers to develop standalone components and implement an alternative API for declaring the component’s compilation scope.
|
||||
|
||||
### Ergonomic component level code-splitting APIs
|
||||
|
||||
A common problem of web applications is their slow initial load time. A way to improve it is to apply more granular code-splitting on a component level. To encourage this practice, we’ll be working on more ergonomic code-splitting APIs.
|
||||
|
||||
### Migration to ESLint
|
||||
|
||||
With the deprecation of TSLint we will be moving to ESLint. As part of the process, we will work on ensuring backward compatibility with our current recommended TSLint configuration, implement a migration strategy for existing Angular applications and introduce new tooling to the Angular CLI toolchain.
|
@ -53,14 +53,27 @@ This field contains an array of asset groups, each of which defines a set of ass
|
||||
|
||||
```json
|
||||
{
|
||||
"assetGroups": [{
|
||||
...
|
||||
}, {
|
||||
...
|
||||
}]
|
||||
"assetGroups": [
|
||||
{
|
||||
...
|
||||
},
|
||||
{
|
||||
...
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
When the ServiceWorker handles a request, it checks asset groups in the order in which they appear in `ngsw-config.json`.
|
||||
The first asset group that matches the requested resource handles the request.
|
||||
|
||||
It is recommended that you put the more specific asset groups higher in the list.
|
||||
For example, an asset group that matches `/foo.js` should appear before one that matches `*.js`.
|
||||
|
||||
</div>
|
||||
|
||||
Each asset group specifies both a group of resources and a policy that governs them. This policy determines when the resources are fetched and what happens when changes are detected.
|
||||
|
||||
Asset groups follow the Typescript interface shown here:
|
||||
@ -123,6 +136,31 @@ These options are used to modify the matching behavior of requests. They are pas
|
||||
|
||||
Unlike asset resources, data requests are not versioned along with the app. They're cached according to manually-configured policies that are more useful for situations such as API requests and other data dependencies.
|
||||
|
||||
This field contains an array of data groups, each of which defines a set of data resources and the policy by which they are cached.
|
||||
|
||||
```json
|
||||
{
|
||||
"dataGroups": [
|
||||
{
|
||||
...
|
||||
},
|
||||
{
|
||||
...
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
When the ServiceWorker handles a request, it checks data groups in the order in which they appear in `ngsw-config.json`.
|
||||
The first data group that matches the requested resource handles the request.
|
||||
|
||||
It is recommended that you put the more specific data groups higher in the list.
|
||||
For example, a data group that matches `/api/foo.json` should appear before one that matches `/api/*.json`.
|
||||
|
||||
</div>
|
||||
|
||||
Data groups follow this Typescript interface:
|
||||
|
||||
```typescript
|
||||
|
58
aio/content/marketing/docs.en.md
Normal file
58
aio/content/marketing/docs.en.md
Normal file
@ -0,0 +1,58 @@
|
||||
<h1 class="no-toc">Introduction to the Angular Docs</h1>
|
||||
|
||||
Angular is an application design framework and development platform for creating efficient and sophisticated single-page apps.
|
||||
|
||||
These Angular docs help you learn and use the Angular framework and development platform, from your first application to optimizing complex single-page apps for enterprises.
|
||||
Tutorials and guides include downloadable examples to accelerate your projects.
|
||||
|
||||
|
||||
<div class="card-container">
|
||||
<a href="guide/setup-local" class="docs-card"
|
||||
title="Angular Local Environment Setup">
|
||||
<section>Get Started</section>
|
||||
<p>Set up your local environment for development with the Angular CLI.</p>
|
||||
<p class="card-footer">Local setup</p>
|
||||
</a>
|
||||
<a href="guide/architecture" class="docs-card" title="Angular Concepts">
|
||||
<section>Learn and Explore</section>
|
||||
<p>Learn about the fundamental design concepts and architecture of Angular apps.</p>
|
||||
<p class="card-footer">Introduction to Angular concepts</p>
|
||||
</a>
|
||||
<a href="start" class="docs-card" title="Try out Angular">
|
||||
<section>Take a Look</section>
|
||||
<p>Examine and work with a small ready-made Angular app, without any setup.</p>
|
||||
<p class="card-footer">Try it now</p>
|
||||
</a>
|
||||
<a href="tutorial" class="docs-card" title="Create an app">
|
||||
<section>Hello World</section>
|
||||
<p>Work through a full tutorial to create your first app.</p>
|
||||
<p class="card-footer">Tour of Heroes tutorial</p>
|
||||
</a>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
## Assumptions
|
||||
|
||||
|
||||
These docs assume that you are already familiar with [HTML](https://developer.mozilla.org/docs/Learn/HTML/Introduction_to_HTML "Learn HTML"), [CSS](https://developer.mozilla.org/docs/Learn/CSS/First_steps "Learn CSS"), [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript/A_re-introduction_to_JavaScript "Learn JavaScript"),
|
||||
and some of the tools from the [latest standards](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Language_Resources "Latest JavaScript standards"), such as [classes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes "ES2015 Classes") and [modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import "ES2015 Modules").
|
||||
The code samples are written using [TypeScript](https://www.typescriptlang.org/ "TypeScript").
|
||||
Most Angular code can be written with just the latest JavaScript, using [types](https://www.typescriptlang.org/docs/handbook/classes.html "TypeScript Types") for dependency injection, and using [decorators](https://www.typescriptlang.org/docs/handbook/decorators.html "Decorators") for metadata.
|
||||
|
||||
|
||||
## Feedback
|
||||
|
||||
<h3>You can sit with us!</h3>
|
||||
|
||||
We want to hear from you. [Report problems or submit suggestions for future docs.](https://github.com/angular/angular/issues/new/choose "Angular GitHub repository new issue form")
|
||||
|
||||
Contribute to Angular docs by creating
|
||||
[pull requests](https://github.com/angular/angular/pulls "Angular Github pull requests")
|
||||
on the Angular Github repository.
|
||||
See [Contributing to Angular](https://github.com/angular/angular/blob/master/CONTRIBUTING.md "Contributing guide")
|
||||
for information about submission guidelines.
|
||||
|
||||
Our community values respectful, supportive communication.
|
||||
Please consult and adhere to the [Code of Conduct](https://github.com/angular/code-of-conduct/blob/master/CODE_OF_CONDUCT.md "Contributor code of conduct").
|
@ -1,58 +1,71 @@
|
||||
<h1 class="no-toc">Introduction to the Angular Docs</h1>
|
||||
<h1 class="no-toc">Introducción a la Documentación de Angular</h1>
|
||||
|
||||
Angular is an application design framework and development platform for creating efficient and sophisticated single-page apps.
|
||||
Angular es un framework de diseño de aplicaciones y plataforma de desarrollo para crear aplicaciones de una
|
||||
sola página eficientes y sofisticadas
|
||||
|
||||
These Angular docs help you learn and use the Angular framework and development platform, from your first application to optimizing complex single-page apps for enterprises.
|
||||
Tutorials and guides include downloadable examples to accelerate your projects.
|
||||
Esta documentación de Angular te ayuda a aprender y usar el framework y la plataforma de desarrollo, desde su
|
||||
primera aplicación hasta la optimización de aplicaciones complejas de una sola página, para empresas.
|
||||
Los tutoriales y guías incluyen ejemplos descargables para acelerar tus proyectos.
|
||||
|
||||
|
||||
<div class="card-container">
|
||||
<a href="guide/setup-local" class="docs-card"
|
||||
title="Angular Local Environment Setup">
|
||||
<section>Get Started</section>
|
||||
<p>Set up your local environment for development with the Angular CLI.</p>
|
||||
<p class="card-footer">Local setup</p>
|
||||
<section>Comienza</section>
|
||||
<p>Configura tu entorno local para el desarrollo, con Angular CLI.</p>
|
||||
<p class="card-footer">Configuración local</p>
|
||||
</a>
|
||||
<a href="guide/architecture" class="docs-card" title="Angular Concepts">
|
||||
<section>Learn and Explore</section>
|
||||
<p>Learn about the fundamental design concepts and architecture of Angular apps.</p>
|
||||
<p class="card-footer">Introduction to Angular concepts</p>
|
||||
<section>Aprende y Explora</section>
|
||||
<p>Aprende sobre arquitectura y conceptos de diseño fundamentales de aplicaciones Angular.</p>
|
||||
<p class="card-footer">Introducción a conceptos de Angular</p>
|
||||
</a>
|
||||
<a href="start" class="docs-card" title="Try out Angular">
|
||||
<section>Take a Look</section>
|
||||
<p>Examine and work with a small ready-made Angular app, without any setup.</p>
|
||||
<p class="card-footer">Try it now</p>
|
||||
<section>Echa un vistazo</section>
|
||||
<p>Examina y trabaja con una pequeña aplicación Angular lista para usar, sin configuraciónes.</p>
|
||||
<p class="card-footer">Pruébalo ahora</p>
|
||||
</a>
|
||||
<a href="tutorial" class="docs-card" title="Create an app">
|
||||
<section>Hello World</section>
|
||||
<p>Work through a full tutorial to create your first app.</p>
|
||||
<p class="card-footer">Tour of Heroes tutorial</p>
|
||||
<section>Hola Mundo</section>
|
||||
<p>Trabaja mediante un tutorial completo para crear tu primera aplicación.</p>
|
||||
<p class="card-footer">Tutorial Tour de Héroes</p>
|
||||
</a>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
## Assumptions
|
||||
## Supuestos
|
||||
|
||||
|
||||
These docs assume that you are already familiar with [HTML](https://developer.mozilla.org/docs/Learn/HTML/Introduction_to_HTML "Learn HTML"), [CSS](https://developer.mozilla.org/docs/Learn/CSS/First_steps "Learn CSS"), [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript/A_re-introduction_to_JavaScript "Learn JavaScript"),
|
||||
and some of the tools from the [latest standards](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Language_Resources "Latest JavaScript standards"), such as [classes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes "ES2015 Classes") and [modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import "ES2015 Modules").
|
||||
The code samples are written using [TypeScript](https://www.typescriptlang.org/ "TypeScript").
|
||||
Most Angular code can be written with just the latest JavaScript, using [types](https://www.typescriptlang.org/docs/handbook/classes.html "TypeScript Types") for dependency injection, and using [decorators](https://www.typescriptlang.org/docs/handbook/decorators.html "Decorators") for metadata.
|
||||
Esta documentación asume que estas familiarizado con
|
||||
[HTML](https://developer.mozilla.org/es/docs/Learn/HTML/Introduccion_a_HTML "Aprende HTML"),
|
||||
[CSS](https://developer.mozilla.org/es/docs/Learn/CSS/First_steps "Aprende CSS"),
|
||||
[JavaScript](https://developer.mozilla.org/es/docs/Web/JavaScript/Una_re-introducci%C3%B3n_a_JavaScript "Aprende JavaScript"),
|
||||
y algunas herramientas de los
|
||||
[últimos estándares javascript](https://developer.mozilla.org/es/docs/Web/JavaScript/Language_Resources "Últimos estándares JavaScript"),
|
||||
como son las [clases](https://developer.mozilla.org/es/docs/Web/JavaScript/Referencia/Classes "Clases ES2015")
|
||||
y [modulos](https://developer.mozilla.org/es/docs/Web/JavaScript/Referencia/Sentencias/import "Modulos ES2015").
|
||||
Los ejemplos de código son escritos usando [TypeScript](https://www.typescriptlang.org/ "TypeScript").
|
||||
La mayor parte del código Angular se puede escribir tan solo con la versión más reciente de JavaScript, usando
|
||||
[tipos](https://www.typescriptlang.org/docs/handbook/classes.html "Tipos TypeScript") para inyección de
|
||||
dependencia, y usando [decoradores](https://www.typescriptlang.org/docs/handbook/decorators.html "Decoradores")
|
||||
para metadatos.
|
||||
|
||||
|
||||
## Feedback
|
||||
## Retrolimentación
|
||||
|
||||
<h3>You can sit with us!</h3>
|
||||
<h3>Puedes sentarte con nosotros!</h3>
|
||||
|
||||
We want to hear from you. [Report problems or submit suggestions for future docs.](https://github.com/angular/angular/issues/new/choose "Angular GitHub repository new issue form")
|
||||
Queremos saber de ti.
|
||||
[Informa problemas o envía sugerencias para nueva documentación.](https://github.com/angular-hispano/angular/issues/new/choose "Repositorio GitHub de Angular, form para nuevo issue")
|
||||
|
||||
Contribute to Angular docs by creating
|
||||
[pull requests](https://github.com/angular/angular/pulls "Angular Github pull requests")
|
||||
on the Angular Github repository.
|
||||
See [Contributing to Angular](https://github.com/angular/angular/blob/master/CONTRIBUTING.md "Contributing guide")
|
||||
for information about submission guidelines.
|
||||
Colabora con la documentación de Angular creando
|
||||
[pull requests](https://github.com/angular-hispano/angular/pulls "Angular Github pull requests")
|
||||
en el repositorio Github de Angular en español.
|
||||
Mira [Colaborando a Angular](https://github.com/angular-hispano/angular/blob/master/CONTRIBUTING.md "Guía de Colaboración")
|
||||
para obtener información sobre las pautas de envío de nuevas colaboraciones.
|
||||
|
||||
Our community values respectful, supportive communication.
|
||||
Please consult and adhere to the [Code of Conduct](https://github.com/angular/code-of-conduct/blob/master/CODE_OF_CONDUCT.md "Contributor code of conduct").
|
||||
Nuestra comunidad valora la comunicación respetuosa y solidaria.
|
||||
Consulta y adhierete al
|
||||
[Código de Conducta](https://angular.lat/coc "Código de Conducta del comunidad").
|
||||
|
@ -14,8 +14,8 @@
|
||||
|
||||
<!-- CONTAINER -->
|
||||
<div class="homepage-container">
|
||||
<div class="hero-headline no-toc">One framework.<br>Mobile & desktop.</div>
|
||||
<a class="button hero-cta" href="docs">Get Started</a>
|
||||
<div class="hero-headline no-toc">Un framework.<br>Móvil y escritorio.</div>
|
||||
<a class="button hero-cta" href="docs">Empezar</a>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
@ -41,8 +41,8 @@
|
||||
|
||||
<div class="text-container">
|
||||
<div class="text-block promo-1-desc l-pad-top-2">
|
||||
<div class="text-headline">Develop Across All Platforms</div>
|
||||
<p class="text-body">Learn one way to build applications with Angular and reuse your code and abilities to build apps for any deployment target. For web, mobile web, native mobile and native desktop.
|
||||
<div class="text-headline">Desarrolla en todas las plataformas</div>
|
||||
<p class="text-body">Aprende una forma de crear aplicaciones con Angular y reutiliza tu código y habilidades para crear aplicaciones para cualquier objetivo de implementación. Para web, web móvil, móvil nativo y escritorio nativo.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -53,9 +53,9 @@
|
||||
<div layout="row" layout-xs="column" class="home-row">
|
||||
<div class="text-container">
|
||||
<div class="text-block">
|
||||
<div class="text-headline">Speed & Performance</div>
|
||||
<p class="text-body">Achieve the maximum speed possible on the Web Platform today, and take it further, via Web Workers and server-side rendering.</p>
|
||||
<p class="text-body">Angular puts you in control over scalability. Meet huge data requirements by building data models on RxJS, Immutable.js or another push-model.</p>
|
||||
<div class="text-headline">Velocidad y Rendimiento</div>
|
||||
<p class="text-body">Logra la máxima velocidad posible en la Plataforma Web hoy y avance, a través de Web Workers y renderizado del lado del servidor.</p>
|
||||
<p class="text-body">Angular te permite controlar la escalabilidad. Satisface requerimientos enormes de datos construyendo modelos en RxJS, Immutable.js o algún otro.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -75,8 +75,8 @@
|
||||
|
||||
<div class="text-container">
|
||||
<div class="text-block promo-3-desc">
|
||||
<div class="text-headline">Incredible Tooling</div>
|
||||
<p class="text-body">Build features quickly with simple, declarative templates. Extend the template language with your own components and use a wide array of existing components. Get immediate Angular-specific help and feedback with nearly every IDE and editor. All this comes together so you can focus on building amazing apps rather than trying to make the code work.
|
||||
<div class="text-headline">Herramientas Increíbles</div>
|
||||
<p class="text-body">Crea funciones rápidamente con plantillas simples y declarativas. Extiende el lenguaje de plantilla con tus propios componentes y usa una amplia gama de componentes existentes. Obtén ayuda inmediata y comentarios específicos de Angular con casi todos los IDE y editores. Todo esto se une para que puedas concentrarte en crear aplicaciones increíbles en lugar de intentar que el código funcione.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -88,8 +88,9 @@
|
||||
<div layout="row" layout-xs="column" class="home-row">
|
||||
<div class="text-container">
|
||||
<div class="text-block l-pad-top-2">
|
||||
<div class="text-headline">Loved by Millions</div>
|
||||
<p class="text-body">From prototype through global deployment, Angular delivers the productivity and scalable infrastructure that supports Google's largest applications.</p>
|
||||
<div class="text-headline">Amada por millones</div>
|
||||
<p class="text-body">Desde el prototipo hasta la implementación global, Angular ofrece la productividad y la infraestructura escalable que soportan a las aplicaciones más grandes de Google.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -106,8 +107,9 @@
|
||||
<div class="card">
|
||||
<img src="generated/images/marketing/home/code-icon.svg" height="70px" alt="Get Started with Angular">
|
||||
<div class="card-text-container">
|
||||
<div class="text-headline">Try it now</div>
|
||||
<p>Explore Angular's capabilities with a ready-made sample app. No setup required.</p>
|
||||
<div class="text-headline">Pruebalo ahora</div>
|
||||
<p>Explora las capacidades de Angular con una aplicación de muestra preparada. No se requiere configuración.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
@ -2,7 +2,7 @@
|
||||
"TopBar": [
|
||||
{
|
||||
"url": "features",
|
||||
"title": "Features"
|
||||
"title": "Caracteristicas"
|
||||
},
|
||||
{
|
||||
"url": "docs",
|
||||
@ -10,11 +10,11 @@
|
||||
},
|
||||
{
|
||||
"url": "resources",
|
||||
"title": "Resources"
|
||||
"title": "Recursos"
|
||||
},
|
||||
{
|
||||
"url": "events",
|
||||
"title": "Events"
|
||||
"title": "Eventos"
|
||||
},
|
||||
{
|
||||
"url": "https://blog.angular.io/",
|
||||
@ -23,19 +23,19 @@
|
||||
],
|
||||
"TopBarNarrow": [
|
||||
{
|
||||
"title": "About Angular",
|
||||
"title": "Acerca de Angular",
|
||||
"children": [
|
||||
{
|
||||
"url": "features",
|
||||
"title": "Features"
|
||||
"title": "Caracteristicas"
|
||||
},
|
||||
{
|
||||
"url": "resources",
|
||||
"title": "Resources"
|
||||
"title": "Recursos"
|
||||
},
|
||||
{
|
||||
"url": "events",
|
||||
"title": "Events"
|
||||
"title": "Eventos"
|
||||
},
|
||||
{
|
||||
"url": "https://blog.angular.io/",
|
||||
@ -820,6 +820,11 @@
|
||||
"title": "Release Practices",
|
||||
"tooltip": "Angular versioning, release, support, and deprecation policies and practices."
|
||||
},
|
||||
{
|
||||
"url": "guide/roadmap",
|
||||
"title": "Roadmap",
|
||||
"tooltip": "Roadmap of the Angular team."
|
||||
},
|
||||
{
|
||||
"title": "Updating to Version 10",
|
||||
"tooltip": "Support for updating your application from version 9 to 10.",
|
||||
@ -955,80 +960,100 @@
|
||||
],
|
||||
"Footer": [
|
||||
{
|
||||
"title": "Resources",
|
||||
"title": "Recursos",
|
||||
"children": [
|
||||
{
|
||||
"url": "about",
|
||||
"title": "About",
|
||||
"tooltip": "Angular contributors."
|
||||
"title": "Contribuyentes",
|
||||
"tooltip": "Contribuyentes de Angular."
|
||||
},
|
||||
{
|
||||
"url": "resources",
|
||||
"title": "Resource Listing",
|
||||
"tooltip": "Angular tools, training, and blogs from around the web."
|
||||
"title": "Listado de recursos",
|
||||
"tooltip": "Herramientas de Angular, capacitación y blogs de toda la web."
|
||||
},
|
||||
{
|
||||
"url": "presskit",
|
||||
"title": "Press Kit",
|
||||
"tooltip": "Press contacts, logos, and branding."
|
||||
"title": "Kit de prensa",
|
||||
"tooltip": "Contactos de prensa, logotipos y marcas."
|
||||
},
|
||||
{
|
||||
"url": "https://blog.angular.io/",
|
||||
"title": "Blog",
|
||||
"tooltip": "Angular Blog"
|
||||
"tooltip": "Blog de Angular"
|
||||
},
|
||||
{
|
||||
"url": "analytics",
|
||||
"title": "Usage Analytics",
|
||||
"tooltip": "Angular Usage Analytics"
|
||||
"title": "Analítica de uso",
|
||||
"tooltip": "Analítica de uso Angular"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Help",
|
||||
"title": "Consigue ayuda",
|
||||
"children": [
|
||||
{
|
||||
"url": "https://chat.angular.lat/",
|
||||
"title": "Angular Hispano Chat",
|
||||
"tooltip": "Chatea en castellano sobre Angular con otros miembros de la comunidad."
|
||||
},
|
||||
{
|
||||
"url": "https://angular.lat/coc",
|
||||
"title": "Código de Conducta",
|
||||
"tooltip": "Tratarnos con respeto y proporcionar un lugar seguro para contribuir."
|
||||
},
|
||||
{
|
||||
"url": "https://stackoverflow.com/questions/tagged/angular",
|
||||
"title": "Stack Overflow",
|
||||
"tooltip": "Stack Overflow: where the community answers your technical Angular questions."
|
||||
"tooltip": "Stack Overflow: donde la comunidad responde sus preguntas técnicas de Angular."
|
||||
},
|
||||
{
|
||||
"url": "https://gitter.im/angular/angular",
|
||||
"title": "Gitter",
|
||||
"tooltip": "Chat about Angular with other birds of a feather."
|
||||
"tooltip": "Chatea en inglés sobre Angular con otros miembros de la comunidad."
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/angular/angular/issues",
|
||||
"title": "Report Issues",
|
||||
"tooltip": "Post issues and suggestions on github."
|
||||
"title": "Informar Issues",
|
||||
"tooltip": "Publica problemas y sugerencias en GitHub."
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/angular/code-of-conduct/blob/master/CODE_OF_CONDUCT.md",
|
||||
"title": "Code of Conduct",
|
||||
"tooltip": "Treating each other with respect."
|
||||
"url": "https://github.com/angular-hispano/angular/issues",
|
||||
"title": "Issues de traducción",
|
||||
"tooltip": "Publica issues y sugerencias de traducción en GitHub."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Community",
|
||||
"title": "Comunidad",
|
||||
"children": [
|
||||
{
|
||||
"url": "events",
|
||||
"title": "Events",
|
||||
"tooltip": "Angular events around the world."
|
||||
"url": "https://angular.lat/conferencias",
|
||||
"title": "Eventos Hispanos",
|
||||
"tooltip": "Eventos en castellano de Angular alrededor del mundo."
|
||||
},
|
||||
{
|
||||
"url": "https://angular.lat/meetups",
|
||||
"title": "Meetups Hispanos",
|
||||
"tooltip": "Asista a una reunión y aprenda de otros desarrolladores."
|
||||
},
|
||||
{
|
||||
"url": "http://www.meetup.com/topics/angularjs/",
|
||||
"title": "Meetups",
|
||||
"tooltip": "Attend a meetup and learn from fellow developers."
|
||||
"title": "Mas Meetups",
|
||||
"tooltip": "Asista a una reunión y aprenda de otros desarrolladores."
|
||||
},
|
||||
{
|
||||
"url": "https://twitter.com/angular",
|
||||
"url": "events",
|
||||
"title": "Mas Eventos",
|
||||
"tooltip": "Eventos de Angular alrededor del mundo."
|
||||
},
|
||||
{
|
||||
"url": "https://twitter.com/AngularHispana",
|
||||
"title": "Twitter",
|
||||
"tooltip": "Twitter"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/angular/angular",
|
||||
"url": "https://github.com/angular-hispano",
|
||||
"title": "GitHub",
|
||||
"tooltip": "GitHub"
|
||||
},
|
||||
@ -1040,7 +1065,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Languages",
|
||||
"title": "Idiomas",
|
||||
"children": [
|
||||
{
|
||||
"title": "简体中文版",
|
||||
|
@ -12,7 +12,7 @@ This page guides you through creating the shopping cart in three phases:
|
||||
{@a services}
|
||||
## Services
|
||||
|
||||
Services are an integral part of Angular applications. In Angular, a service is an instance of a class that you can make available to any part of your application using Angular's [dependency injection system](guide/glossary#dependency-injection-di "Dependency injection definition").
|
||||
Services are an integral part of Angular applications. In Angular, a service is an instance of a class that you can make available to any part of your application using Angular's [dependency injection system](guide/glossary#dependency-injection "Dependency injection definition").
|
||||
|
||||
Services are the place where you share data between parts of your application. For the online store, the cart service is where you store your cart data and methods.
|
||||
|
||||
|
@ -25,10 +25,10 @@
|
||||
<aio-top-menu *ngIf="showTopMenu" [nodes]="topMenuNodes" [currentNode]="currentNodes?.TopBar"></aio-top-menu>
|
||||
<aio-search-box class="search-container" #searchBox (onSearch)="doSearch($event)" (onFocus)="doSearch($event)"></aio-search-box>
|
||||
<div class="toolbar-external-icons-container">
|
||||
<a href="https://twitter.com/angular" title="Twitter" aria-label="Angular on twitter">
|
||||
<a href="https://twitter.com/AngularHispana" title="Twitter" aria-label="Angular Hispano en Twitter">
|
||||
<mat-icon svgIcon="logos:twitter"></mat-icon>
|
||||
</a>
|
||||
<a href="https://github.com/angular/angular" title="GitHub" aria-label="Angular on github">
|
||||
<a href="https://github.com/angular-hispano/angular" title="GitHub" aria-label="Angular Hispano en GitHub">
|
||||
<mat-icon svgIcon="logos:github"></mat-icon>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -10,11 +10,11 @@
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Super-powered by Google ©2010-2020.
|
||||
Code licensed under an <a href="license" title="License text">MIT-style License</a>.
|
||||
Documentation licensed under
|
||||
Superalimentado por Google LLC ©2020.
|
||||
Código licenciado bajo un <a href="license" title="Texto completo de la licencia">Licencia de estilo MIT</a>.
|
||||
Documentación bajo licencia
|
||||
<a href="http://creativecommons.org/licenses/by/4.0/">CC BY 4.0</a>.
|
||||
</p>
|
||||
<p>
|
||||
Version {{versionInfo?.full}}.
|
||||
Versión {{versionInfo?.full}}.
|
||||
</p>
|
||||
|
@ -17,8 +17,8 @@ import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
|
||||
selector: 'aio-search-box',
|
||||
template: `<input #searchBox
|
||||
type="search"
|
||||
aria-label="search"
|
||||
placeholder="Search"
|
||||
aria-label="Buscar"
|
||||
placeholder="Buscar"
|
||||
(input)="doSearch()"
|
||||
(keyup)="doSearch()"
|
||||
(focus)="doFocus()"
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { ReflectiveInjector } from '@angular/core';
|
||||
import { Location, LocationStrategy, PlatformLocation, ViewportScroller } from '@angular/common';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { MockLocationStrategy, SpyLocation } from '@angular/common/testing';
|
||||
import { fakeAsync, tick } from '@angular/core/testing';
|
||||
import {Location, LocationStrategy, PlatformLocation, ViewportScroller} from '@angular/common';
|
||||
import {DOCUMENT} from '@angular/common';
|
||||
import {MockLocationStrategy, SpyLocation} from '@angular/common/testing';
|
||||
import {ReflectiveInjector} from '@angular/core';
|
||||
import {fakeAsync, tick} from '@angular/core/testing';
|
||||
|
||||
import { ScrollService, topMargin } from './scroll.service';
|
||||
import {ScrollService, topMargin} from './scroll.service';
|
||||
|
||||
describe('ScrollService', () => {
|
||||
const scrollServiceInstances: ScrollService[] = [];
|
||||
@ -32,27 +32,25 @@ describe('ScrollService', () => {
|
||||
}
|
||||
|
||||
class MockElement {
|
||||
getBoundingClientRect = jasmine.createSpy('Element getBoundingClientRect')
|
||||
.and.returnValue({top: 0});
|
||||
getBoundingClientRect =
|
||||
jasmine.createSpy('Element getBoundingClientRect').and.returnValue({top: 0});
|
||||
scrollIntoView = jasmine.createSpy('Element scrollIntoView');
|
||||
}
|
||||
|
||||
const viewportScrollerStub = jasmine.createSpyObj(
|
||||
'viewportScroller',
|
||||
['getScrollPosition', 'scrollToPosition']);
|
||||
const viewportScrollerStub =
|
||||
jasmine.createSpyObj('viewportScroller', ['getScrollPosition', 'scrollToPosition']);
|
||||
|
||||
beforeEach(() => {
|
||||
injector = ReflectiveInjector.resolveAndCreate([
|
||||
{
|
||||
provide: ScrollService,
|
||||
useFactory: createScrollService,
|
||||
deps: [DOCUMENT, PlatformLocation, ViewportScroller, Location],
|
||||
},
|
||||
{ provide: Location, useClass: SpyLocation },
|
||||
{ provide: DOCUMENT, useClass: MockDocument },
|
||||
{ provide: PlatformLocation, useClass: MockPlatformLocation },
|
||||
{ provide: ViewportScroller, useValue: viewportScrollerStub },
|
||||
{ provide: LocationStrategy, useClass: MockLocationStrategy }
|
||||
{
|
||||
provide: ScrollService,
|
||||
useFactory: createScrollService,
|
||||
deps: [DOCUMENT, PlatformLocation, ViewportScroller, Location],
|
||||
},
|
||||
{provide: Location, useClass: SpyLocation}, {provide: DOCUMENT, useClass: MockDocument},
|
||||
{provide: PlatformLocation, useClass: MockPlatformLocation},
|
||||
{provide: ViewportScroller, useValue: viewportScrollerStub},
|
||||
{provide: LocationStrategy, useClass: MockLocationStrategy}
|
||||
]);
|
||||
platformLocation = injector.get(PlatformLocation);
|
||||
document = injector.get(DOCUMENT);
|
||||
@ -68,18 +66,39 @@ describe('ScrollService', () => {
|
||||
});
|
||||
|
||||
it('should debounce `updateScrollPositonInHistory()`', fakeAsync(() => {
|
||||
const updateScrollPositionInHistorySpy = spyOn(scrollService, 'updateScrollPositionInHistory');
|
||||
const updateScrollPositionInHistorySpy =
|
||||
spyOn(scrollService, 'updateScrollPositionInHistory');
|
||||
|
||||
window.dispatchEvent(new Event('scroll'));
|
||||
tick(249);
|
||||
window.dispatchEvent(new Event('scroll'));
|
||||
tick(249);
|
||||
window.dispatchEvent(new Event('scroll'));
|
||||
tick(249);
|
||||
expect(updateScrollPositionInHistorySpy).not.toHaveBeenCalled();
|
||||
tick(1);
|
||||
expect(updateScrollPositionInHistorySpy).toHaveBeenCalledTimes(1);
|
||||
}));
|
||||
window.dispatchEvent(new Event('scroll'));
|
||||
tick(249);
|
||||
window.dispatchEvent(new Event('scroll'));
|
||||
tick(249);
|
||||
window.dispatchEvent(new Event('scroll'));
|
||||
tick(249);
|
||||
expect(updateScrollPositionInHistorySpy).not.toHaveBeenCalled();
|
||||
tick(1);
|
||||
expect(updateScrollPositionInHistorySpy).toHaveBeenCalledTimes(1);
|
||||
}));
|
||||
|
||||
it('should not support `manual` scrollRestoration when it is not writable', () => {
|
||||
const original = Object.getOwnPropertyDescriptor(window.history, 'scrollRestoration');
|
||||
try {
|
||||
Object.defineProperty(window.history, 'scrollRestoration', {
|
||||
value: 'auto',
|
||||
configurable: true,
|
||||
});
|
||||
scrollService = createScrollService(
|
||||
document, platformLocation as PlatformLocation, viewportScrollerStub, location);
|
||||
|
||||
expect(scrollService.supportManualScrollRestoration).toBe(false);
|
||||
} finally {
|
||||
if (original !== undefined) {
|
||||
Object.defineProperty(window.history, 'scrollRestoration', original);
|
||||
} else {
|
||||
delete window.history.scrollRestoration;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should set `scrollRestoration` to `manual` if supported', () => {
|
||||
if (scrollService.supportManualScrollRestoration) {
|
||||
@ -96,7 +115,9 @@ describe('ScrollService', () => {
|
||||
try {
|
||||
// Simulate `window.sessionStorage` being inaccessible, when cookies are disabled.
|
||||
Object.defineProperty(window, 'sessionStorage', {
|
||||
get() { throw new Error('The operation is insecure'); },
|
||||
get() {
|
||||
throw new Error('The operation is insecure');
|
||||
},
|
||||
});
|
||||
|
||||
const platformLoc = platformLocation as PlatformLocation;
|
||||
@ -198,8 +219,7 @@ describe('ScrollService', () => {
|
||||
platformLocation.hash = '';
|
||||
|
||||
const topOfPage = new MockElement();
|
||||
document.getElementById.and
|
||||
.callFake((id: string) => id === 'top-of-page' ? topOfPage : null);
|
||||
document.getElementById.and.callFake((id: string) => id === 'top-of-page' ? topOfPage : null);
|
||||
|
||||
scrollService.scroll();
|
||||
expect(topOfPage.scrollIntoView).toHaveBeenCalled();
|
||||
@ -227,7 +247,7 @@ describe('ScrollService', () => {
|
||||
|
||||
it('should scroll to the element whose id matches the hash with encoded characters', () => {
|
||||
const element = new MockElement();
|
||||
platformLocation.hash = '%F0%9F%91%8D'; // 👍
|
||||
platformLocation.hash = '%F0%9F%91%8D'; // 👍
|
||||
document.getElementById.and.returnValue(element);
|
||||
|
||||
scrollService.scroll();
|
||||
@ -289,8 +309,7 @@ describe('ScrollService', () => {
|
||||
it('should scroll to top', () => {
|
||||
const topOfPageElement = new MockElement() as any as Element;
|
||||
document.getElementById.and.callFake(
|
||||
(id: string) => id === 'top-of-page' ? topOfPageElement : null
|
||||
);
|
||||
(id: string) => id === 'top-of-page' ? topOfPageElement : null);
|
||||
|
||||
scrollService.scrollToTop();
|
||||
expect(topOfPageElement.scrollIntoView).toHaveBeenCalled();
|
||||
@ -312,58 +331,55 @@ describe('ScrollService', () => {
|
||||
|
||||
describe('#needToFixScrollPosition', async () => {
|
||||
it('should return true when popState event was fired after a back navigation if the browser supports ' +
|
||||
'scrollRestoration`. Otherwise, needToFixScrollPosition() returns false', () => {
|
||||
'scrollRestoration`. Otherwise, needToFixScrollPosition() returns false',
|
||||
() => {
|
||||
if (scrollService.supportManualScrollRestoration) {
|
||||
location.go('/initial-url1');
|
||||
// We simulate a scroll down
|
||||
location.replaceState('/initial-url1', 'hack', {scrollPosition: [2000, 0]});
|
||||
location.go('/initial-url2');
|
||||
location.back();
|
||||
|
||||
if (scrollService.supportManualScrollRestoration) {
|
||||
location.go('/initial-url1');
|
||||
// We simulate a scroll down
|
||||
location.replaceState('/initial-url1', 'hack', {scrollPosition: [2000, 0]});
|
||||
location.go('/initial-url2');
|
||||
location.back();
|
||||
expect(scrollService.poppedStateScrollPosition).toEqual([2000, 0]);
|
||||
expect(scrollService.needToFixScrollPosition()).toBe(true);
|
||||
} else {
|
||||
location.go('/initial-url1');
|
||||
location.go('/initial-url2');
|
||||
location.back();
|
||||
|
||||
expect(scrollService.poppedStateScrollPosition).toEqual([2000, 0]);
|
||||
expect(scrollService.needToFixScrollPosition()).toBe(true);
|
||||
} else {
|
||||
location.go('/initial-url1');
|
||||
location.go('/initial-url2');
|
||||
location.back();
|
||||
|
||||
expect(scrollService.poppedStateScrollPosition).toBe(null);
|
||||
expect(scrollService.needToFixScrollPosition()).toBe(false);
|
||||
}
|
||||
|
||||
});
|
||||
expect(scrollService.poppedStateScrollPosition).toBe(null);
|
||||
expect(scrollService.needToFixScrollPosition()).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('should return true when popState event was fired after a forward navigation if the browser supports ' +
|
||||
'scrollRestoration`. Otherwise, needToFixScrollPosition() returns false', () => {
|
||||
'scrollRestoration`. Otherwise, needToFixScrollPosition() returns false',
|
||||
() => {
|
||||
if (scrollService.supportManualScrollRestoration) {
|
||||
location.go('/initial-url1');
|
||||
location.go('/initial-url2');
|
||||
// We simulate a scroll down
|
||||
location.replaceState('/initial-url1', 'hack', {scrollPosition: [2000, 0]});
|
||||
|
||||
if (scrollService.supportManualScrollRestoration) {
|
||||
location.go('/initial-url1');
|
||||
location.go('/initial-url2');
|
||||
// We simulate a scroll down
|
||||
location.replaceState('/initial-url1', 'hack', {scrollPosition: [2000, 0]});
|
||||
location.back();
|
||||
scrollService.poppedStateScrollPosition = [0, 0];
|
||||
location.forward();
|
||||
|
||||
location.back();
|
||||
scrollService.poppedStateScrollPosition = [0, 0];
|
||||
location.forward();
|
||||
expect(scrollService.poppedStateScrollPosition).toEqual([2000, 0]);
|
||||
expect(scrollService.needToFixScrollPosition()).toBe(true);
|
||||
} else {
|
||||
location.go('/initial-url1');
|
||||
location.go('/initial-url2');
|
||||
location.back();
|
||||
location.forward();
|
||||
|
||||
expect(scrollService.poppedStateScrollPosition).toEqual([2000, 0]);
|
||||
expect(scrollService.needToFixScrollPosition()).toBe(true);
|
||||
} else {
|
||||
location.go('/initial-url1');
|
||||
location.go('/initial-url2');
|
||||
location.back();
|
||||
location.forward();
|
||||
|
||||
expect(scrollService.poppedStateScrollPosition).toBe(null);
|
||||
expect(scrollService.needToFixScrollPosition()).toBe(false);
|
||||
}
|
||||
|
||||
});
|
||||
expect(scrollService.poppedStateScrollPosition).toBe(null);
|
||||
expect(scrollService.needToFixScrollPosition()).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('#scrollAfterRender', async () => {
|
||||
|
||||
let scrollSpy: jasmine.Spy;
|
||||
let scrollToTopSpy: jasmine.Spy;
|
||||
let needToFixScrollPositionSpy: jasmine.Spy;
|
||||
@ -383,69 +399,69 @@ describe('ScrollService', () => {
|
||||
|
||||
|
||||
it('should call `scroll` when we navigate to a location with anchor', fakeAsync(() => {
|
||||
needToFixScrollPositionSpy.and.returnValue(false);
|
||||
getStoredScrollPositionSpy.and.returnValue(null);
|
||||
isLocationWithHashSpy.and.returnValue(true);
|
||||
needToFixScrollPositionSpy.and.returnValue(false);
|
||||
getStoredScrollPositionSpy.and.returnValue(null);
|
||||
isLocationWithHashSpy.and.returnValue(true);
|
||||
|
||||
scrollService.scrollAfterRender(scrollDelay);
|
||||
scrollService.scrollAfterRender(scrollDelay);
|
||||
|
||||
expect(scrollSpy).not.toHaveBeenCalled();
|
||||
tick(scrollDelay);
|
||||
expect(scrollSpy).toHaveBeenCalled();
|
||||
}));
|
||||
expect(scrollSpy).not.toHaveBeenCalled();
|
||||
tick(scrollDelay);
|
||||
expect(scrollSpy).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should call `scrollToTop` when we navigate to a location without anchor', fakeAsync(() => {
|
||||
needToFixScrollPositionSpy.and.returnValue(false);
|
||||
getStoredScrollPositionSpy.and.returnValue(null);
|
||||
isLocationWithHashSpy.and.returnValue(false);
|
||||
needToFixScrollPositionSpy.and.returnValue(false);
|
||||
getStoredScrollPositionSpy.and.returnValue(null);
|
||||
isLocationWithHashSpy.and.returnValue(false);
|
||||
|
||||
scrollService.scrollAfterRender(scrollDelay);
|
||||
scrollService.scrollAfterRender(scrollDelay);
|
||||
|
||||
expect(scrollToTopSpy).toHaveBeenCalled();
|
||||
tick(scrollDelay);
|
||||
expect(scrollSpy).not.toHaveBeenCalled();
|
||||
}));
|
||||
expect(scrollToTopSpy).toHaveBeenCalled();
|
||||
tick(scrollDelay);
|
||||
expect(scrollSpy).not.toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should call `viewportScroller.scrollToPosition` when we reload a page', fakeAsync(() => {
|
||||
getStoredScrollPositionSpy.and.returnValue([0, 1000]);
|
||||
getStoredScrollPositionSpy.and.returnValue([0, 1000]);
|
||||
|
||||
scrollService.scrollAfterRender(scrollDelay);
|
||||
scrollService.scrollAfterRender(scrollDelay);
|
||||
|
||||
expect(viewportScrollerStub.scrollToPosition).toHaveBeenCalled();
|
||||
expect(getStoredScrollPositionSpy).toHaveBeenCalled();
|
||||
}));
|
||||
expect(viewportScrollerStub.scrollToPosition).toHaveBeenCalled();
|
||||
expect(getStoredScrollPositionSpy).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should call `scrollToPosition` after a popState', fakeAsync(() => {
|
||||
needToFixScrollPositionSpy.and.returnValue(true);
|
||||
getStoredScrollPositionSpy.and.returnValue(null);
|
||||
scrollService.scrollAfterRender(scrollDelay);
|
||||
expect(scrollToPosition).toHaveBeenCalled();
|
||||
tick(scrollDelay);
|
||||
expect(scrollSpy).not.toHaveBeenCalled();
|
||||
expect(scrollToTopSpy).not.toHaveBeenCalled();
|
||||
}));
|
||||
needToFixScrollPositionSpy.and.returnValue(true);
|
||||
getStoredScrollPositionSpy.and.returnValue(null);
|
||||
scrollService.scrollAfterRender(scrollDelay);
|
||||
expect(scrollToPosition).toHaveBeenCalled();
|
||||
tick(scrollDelay);
|
||||
expect(scrollSpy).not.toHaveBeenCalled();
|
||||
expect(scrollToTopSpy).not.toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('once destroyed', () => {
|
||||
it('should stop updating scroll position', fakeAsync(() => {
|
||||
const updateScrollPositionInHistorySpy =
|
||||
spyOn(scrollService, 'updateScrollPositionInHistory');
|
||||
const updateScrollPositionInHistorySpy =
|
||||
spyOn(scrollService, 'updateScrollPositionInHistory');
|
||||
|
||||
window.dispatchEvent(new Event('scroll'));
|
||||
tick(250);
|
||||
expect(updateScrollPositionInHistorySpy).toHaveBeenCalledTimes(1);
|
||||
window.dispatchEvent(new Event('scroll'));
|
||||
tick(250);
|
||||
expect(updateScrollPositionInHistorySpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
window.dispatchEvent(new Event('scroll'));
|
||||
tick(250);
|
||||
expect(updateScrollPositionInHistorySpy).toHaveBeenCalledTimes(2);
|
||||
window.dispatchEvent(new Event('scroll'));
|
||||
tick(250);
|
||||
expect(updateScrollPositionInHistorySpy).toHaveBeenCalledTimes(2);
|
||||
|
||||
updateScrollPositionInHistorySpy.calls.reset();
|
||||
scrollService.ngOnDestroy();
|
||||
updateScrollPositionInHistorySpy.calls.reset();
|
||||
scrollService.ngOnDestroy();
|
||||
|
||||
window.dispatchEvent(new Event('scroll'));
|
||||
tick(250);
|
||||
expect(updateScrollPositionInHistorySpy).not.toHaveBeenCalled();
|
||||
}));
|
||||
window.dispatchEvent(new Event('scroll'));
|
||||
tick(250);
|
||||
expect(updateScrollPositionInHistorySpy).not.toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should stop updating the stored location href', () => {
|
||||
const updateScrollLocationHrefSpy = spyOn(scrollService, 'updateScrollLocationHref');
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { DOCUMENT, Location, PlatformLocation, PopStateEvent, ViewportScroller } from '@angular/common';
|
||||
import { Injectable, Inject, OnDestroy } from '@angular/core';
|
||||
import { fromEvent, Subject } from 'rxjs';
|
||||
import { debounceTime, takeUntil } from 'rxjs/operators';
|
||||
import {DOCUMENT, Location, PlatformLocation, PopStateEvent, ViewportScroller} from '@angular/common';
|
||||
import {Inject, Injectable, OnDestroy} from '@angular/core';
|
||||
import {fromEvent, Subject} from 'rxjs';
|
||||
import {debounceTime, takeUntil} from 'rxjs/operators';
|
||||
|
||||
type ScrollPosition = [number, number];
|
||||
interface ScrollPositionPopStateEvent extends PopStateEvent {
|
||||
@ -15,18 +15,16 @@ export const topMargin = 16;
|
||||
*/
|
||||
@Injectable()
|
||||
export class ScrollService implements OnDestroy {
|
||||
|
||||
private _topOffset: number | null;
|
||||
private _topOffset: number|null;
|
||||
private _topOfPageElement: Element;
|
||||
private onDestroy = new Subject<void>();
|
||||
private storage: Storage;
|
||||
|
||||
// The scroll position which has to be restored, after a `popstate` event.
|
||||
poppedStateScrollPosition: ScrollPosition | null = null;
|
||||
poppedStateScrollPosition: ScrollPosition|null = null;
|
||||
// Whether the browser supports the necessary features for manual scroll restoration.
|
||||
supportManualScrollRestoration: boolean =
|
||||
!!window && ('scrollTo' in window) && ('scrollX' in window) && ('scrollY' in window) &&
|
||||
!!history && ('scrollRestoration' in history);
|
||||
supportManualScrollRestoration: boolean = !!window && ('scrollTo' in window) &&
|
||||
('scrollX' in window) && ('scrollY' in window) && isScrollRestorationWritable();
|
||||
|
||||
// Offset from the top of the document to bottom of any static elements
|
||||
// at the top (e.g. toolbar) + some margin
|
||||
@ -46,10 +44,8 @@ export class ScrollService implements OnDestroy {
|
||||
}
|
||||
|
||||
constructor(
|
||||
@Inject(DOCUMENT) private document: any,
|
||||
private platformLocation: PlatformLocation,
|
||||
private viewportScroller: ViewportScroller,
|
||||
private location: Location) {
|
||||
@Inject(DOCUMENT) private document: any, private platformLocation: PlatformLocation,
|
||||
private viewportScroller: ViewportScroller, private location: Location) {
|
||||
try {
|
||||
this.storage = window.sessionStorage;
|
||||
} catch {
|
||||
@ -118,9 +114,7 @@ export class ScrollService implements OnDestroy {
|
||||
*/
|
||||
scroll() {
|
||||
const hash = this.getCurrentHash();
|
||||
const element: HTMLElement = hash
|
||||
? this.document.getElementById(hash)
|
||||
: this.topOfPageElement;
|
||||
const element: HTMLElement = hash ? this.document.getElementById(hash) : this.topOfPageElement;
|
||||
this.scrollToElement(element);
|
||||
}
|
||||
|
||||
@ -132,8 +126,8 @@ export class ScrollService implements OnDestroy {
|
||||
}
|
||||
|
||||
/**
|
||||
* When we load a document, we have to scroll to the correct position depending on whether this is a new location,
|
||||
* a back/forward in the history, or a refresh
|
||||
* When we load a document, we have to scroll to the correct position depending on whether this is
|
||||
* a new location, a back/forward in the history, or a refresh
|
||||
* @param delay before we scroll to the good position
|
||||
*/
|
||||
scrollAfterRender(delay: number) {
|
||||
@ -208,19 +202,22 @@ export class ScrollService implements OnDestroy {
|
||||
updateScrollPositionInHistory() {
|
||||
if (this.supportManualScrollRestoration) {
|
||||
const currentScrollPosition = this.viewportScroller.getScrollPosition();
|
||||
this.location.replaceState(this.location.path(true), undefined, {scrollPosition: currentScrollPosition});
|
||||
this.location.replaceState(
|
||||
this.location.path(true), undefined, {scrollPosition: currentScrollPosition});
|
||||
this.storage.setItem('scrollPosition', currentScrollPosition.join(','));
|
||||
}
|
||||
}
|
||||
|
||||
getStoredScrollLocationHref(): string | null {
|
||||
getStoredScrollLocationHref(): string|null {
|
||||
const href = this.storage.getItem('scrollLocationHref');
|
||||
return href || null;
|
||||
}
|
||||
|
||||
getStoredScrollPosition(): ScrollPosition | null {
|
||||
getStoredScrollPosition(): ScrollPosition|null {
|
||||
const position = this.storage.getItem('scrollPosition');
|
||||
if (!position) { return null; }
|
||||
if (!position) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [x, y] = position.split(',');
|
||||
return [+x, +y];
|
||||
@ -245,3 +242,20 @@ export class ScrollService implements OnDestroy {
|
||||
return decodeURIComponent(this.platformLocation.hash.replace(/^#/, ''));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We need to check whether we can write to `history.scrollRestoration`
|
||||
*
|
||||
* We do this by checking the property descriptor of the property, but
|
||||
* it might actually be defined on the `history` prototype not the instance.
|
||||
*
|
||||
* In this context "writable" means either than the property is a `writable`
|
||||
* data file or a property that has a setter.
|
||||
*/
|
||||
function isScrollRestorationWritable() {
|
||||
const scrollRestorationDescriptor =
|
||||
Object.getOwnPropertyDescriptor(history, 'scrollRestoration') ||
|
||||
Object.getOwnPropertyDescriptor(Object.getPrototypeOf(history), 'scrollRestoration');
|
||||
return scrollRestorationDescriptor !== undefined &&
|
||||
!!(scrollRestorationDescriptor.writable || scrollRestorationDescriptor.set);
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="es">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Angular</title>
|
||||
<meta name="Description" content="Angular is a platform for building mobile and desktop web applications.
|
||||
Join the community of millions of developers who build compelling user interfaces with Angular.">
|
||||
<meta name="Description" content="Angular es una plataforma para crear aplicaciones de escritorio web y móviles.
|
||||
Únete a la comunidad de millones de desarrolladores que crean interfaces de usuario atractivas con Angular.">
|
||||
<base href="/">
|
||||
|
||||
<link rel="preconnect" href="https://www.google-analytics.com">
|
||||
@ -101,11 +101,11 @@
|
||||
<img src="assets/images/logos/angular/angular.svg" width="250" height="250" alt="Angular">
|
||||
</div>
|
||||
<div class="homepage-container">
|
||||
<div class="hero-headline">One framework.<br>Mobile & desktop.</div>
|
||||
<div class="hero-headline">Un framework.<br>Móvil y escritorio.</div>
|
||||
</div>
|
||||
</section>
|
||||
<h2 style="color: red; margin-top: 40px; position: relative; text-align: center; text-shadow: 1px 1px #fafafa;">
|
||||
<b><i>This website requires JavaScript.</i></b>
|
||||
<b><i>Este sitio web requiere JavaScript.</i></b>
|
||||
</h2>
|
||||
</noscript>
|
||||
</body>
|
||||
|
@ -4,11 +4,9 @@ module.exports = function processCliCommands(createDocMessage) {
|
||||
$runBefore: ['rendering-docs'],
|
||||
$process(docs) {
|
||||
const navigationDoc = docs.find(doc => doc.docType === 'navigation-json');
|
||||
const navigationNode = navigationDoc &&
|
||||
navigationDoc.data['SideNav'].find(
|
||||
node => node.children && node.children.length && node.children[0].url === 'cli');
|
||||
const cliCommandsNode = navigationDoc && findCliCommandsNode(navigationDoc.data['SideNav']);
|
||||
|
||||
if (!navigationNode) {
|
||||
if (!cliCommandsNode) {
|
||||
throw new Error(createDocMessage(
|
||||
'Missing `cli` url - CLI Commands must include a first child node with url set at `cli`',
|
||||
navigationDoc));
|
||||
@ -24,13 +22,41 @@ module.exports = function processCliCommands(createDocMessage) {
|
||||
doc.optionKeywords = Array.from(optionKeywords).join(' ');
|
||||
|
||||
// Add to navigation doc
|
||||
navigationNode.children.push({url: doc.path, title: `ng ${doc.name}`});
|
||||
cliCommandsNode.children.push({url: doc.path, title: `ng ${doc.name}`});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Look for the `CLI Commands` navigation node. It is the node whose first child has `url: 'cli'`.
|
||||
// (NOTE: Using the URL instead of the title, because it is more robust.)
|
||||
function findCliCommandsNode(nodes) {
|
||||
// We will "recursively" check all navigation nodes and their children (in breadth-first order),
|
||||
// until we find the `CLI Commands` node. Keep a list of nodes lists to check.
|
||||
// (NOTE: Each item in the list is a LIST of nodes.)
|
||||
const nodesList = [nodes];
|
||||
|
||||
while (nodesList.length > 0) {
|
||||
// Get the first item from the list of nodes lists.
|
||||
const currentNodes = nodesList.shift();
|
||||
const cliCommandsNode = currentNodes.find(isCliCommandsNode);
|
||||
|
||||
// One of the nodes in `currentNodes` was the `CLI Commands` node. Return it.
|
||||
if (cliCommandsNode) return cliCommandsNode;
|
||||
|
||||
// The `CLI Commands` node is not in `currentNodes`. Check each node's children (if any).
|
||||
currentNodes.forEach(node => node.children && nodesList.push(node.children));
|
||||
}
|
||||
|
||||
// We checked all navigation nodes and their children and did not find the `CLI Commands` node.
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isCliCommandsNode(node) {
|
||||
return node.children && node.children.length && node.children[0].url === 'cli';
|
||||
}
|
||||
|
||||
function processOptions(container, options, optionKeywords) {
|
||||
container.positionalOptions = [];
|
||||
container.namedOptions = [];
|
||||
|
@ -258,14 +258,15 @@ describe('processCliCommands processor', () => {
|
||||
docType: 'navigation-json',
|
||||
data: {
|
||||
SideNav: [
|
||||
{url: 'some/page', title: 'Some Page'}, {
|
||||
{url: 'some/page', title: 'Some Page'},
|
||||
{
|
||||
title: 'CLI Commands',
|
||||
tooltip: 'Angular CLI command reference',
|
||||
children: [{'title': 'Overview', 'url': 'cli'}]
|
||||
children: [{'title': 'Overview', 'url': 'cli'}],
|
||||
},
|
||||
{url: 'other/page', title: 'Other Page'}
|
||||
]
|
||||
}
|
||||
{url: 'other/page', title: 'Other Page'},
|
||||
],
|
||||
},
|
||||
};
|
||||
processor.$process([command, navigation]);
|
||||
expect(navigation.data.SideNav[1].title).toEqual('CLI Commands');
|
||||
@ -275,6 +276,54 @@ describe('processCliCommands processor', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('should detect the CLI node if it is nested in another node (as long as there is a first child node with a `cli` url',
|
||||
() => {
|
||||
const command = {
|
||||
docType: 'cli-command',
|
||||
name: 'command1',
|
||||
commandAliases: ['alias1', 'alias2'],
|
||||
options: [],
|
||||
path: 'cli/command1',
|
||||
};
|
||||
const navigation = {
|
||||
docType: 'navigation-json',
|
||||
data: {
|
||||
SideNav: [
|
||||
{url: 'some/page', title: 'Some Page'},
|
||||
{
|
||||
title: 'CLI Commands Grandparent',
|
||||
children: [
|
||||
{url: 'some/nested/page', title: 'Some Nested Page'},
|
||||
{
|
||||
title: 'CLI Commands Parent',
|
||||
children: [
|
||||
{url: 'some/more/nested/page', title: 'Some More Nested Page'},
|
||||
{
|
||||
title: 'CLI Commands',
|
||||
tooltip: 'Angular CLI command reference',
|
||||
children: [{'title': 'Overview', 'url': 'cli'}],
|
||||
},
|
||||
{url: 'other/more/nested/page', title: 'Other More Nested Page'},
|
||||
],
|
||||
},
|
||||
{url: 'other/nested/page', title: 'Other Nested Page'},
|
||||
],
|
||||
},
|
||||
{url: 'other/page', title: 'Other Page'},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
processor.$process([command, navigation]);
|
||||
|
||||
const cliCommandsNode = navigation.data.SideNav[1].children[1].children[1];
|
||||
expect(cliCommandsNode.title).toEqual('CLI Commands');
|
||||
expect(cliCommandsNode.children).toEqual([
|
||||
{url: 'cli', title: 'Overview'},
|
||||
{url: 'cli/command1', title: 'ng command1'},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should complain if there is no child with `cli` url', () => {
|
||||
const command = {
|
||||
docType: 'cli-command',
|
||||
|
@ -1,8 +1,12 @@
|
||||
load("@npm_bazel_typescript//:index.bzl", "ts_library")
|
||||
load("//tools:defaults.bzl", "jasmine_node_test")
|
||||
|
||||
ts_library(
|
||||
name = "merge",
|
||||
srcs = glob(["**/*.ts"]),
|
||||
srcs = glob(
|
||||
["**/*.ts"],
|
||||
exclude = ["**/*.spec.ts"],
|
||||
),
|
||||
module_name = "@angular/dev-infra-private/pr/merge",
|
||||
visibility = ["//dev-infra:__subpackages__"],
|
||||
deps = [
|
||||
@ -11,8 +15,37 @@ ts_library(
|
||||
"@npm//@octokit/rest",
|
||||
"@npm//@types/inquirer",
|
||||
"@npm//@types/node",
|
||||
"@npm//@types/node-fetch",
|
||||
"@npm//@types/semver",
|
||||
"@npm//@types/yargs",
|
||||
"@npm//chalk",
|
||||
],
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "test_lib",
|
||||
testonly = True,
|
||||
srcs = glob(["**/*.spec.ts"]),
|
||||
deps = [
|
||||
":merge",
|
||||
"//dev-infra/utils",
|
||||
"@npm//@types/jasmine",
|
||||
"@npm//@types/node",
|
||||
"@npm//@types/node-fetch",
|
||||
"@npm//nock",
|
||||
],
|
||||
)
|
||||
|
||||
jasmine_node_test(
|
||||
name = "test",
|
||||
# Disable the Bazel patched module resolution. It always loads ".mjs" files first. This
|
||||
# breaks NodeJS execution for "node-fetch" as it uses experimental modules which are not
|
||||
# enabled in NodeJS. TODO: Remove this with rules_nodejs 3.x where patching is optional.
|
||||
# https://github.com/bazelbuild/rules_nodejs/commit/7d070ffadf9c3b41711382a4737b995f987c14fa.
|
||||
args = ["--nobazel_patch_module_resolver"],
|
||||
deps = [
|
||||
":test_lib",
|
||||
"@npm//node-fetch",
|
||||
"@npm//semver",
|
||||
],
|
||||
)
|
||||
|
@ -6,10 +6,14 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {getConfig, GitClientConfig, NgDevConfig} from '../../utils/config';
|
||||
import {GitClientConfig, NgDevConfig} from '../../utils/config';
|
||||
import {GithubClient} from '../../utils/git/github';
|
||||
|
||||
import {GithubApiMergeStrategyConfig} from './strategies/api-merge';
|
||||
|
||||
/** Describes possible values that can be returned for `branches` of a target label. */
|
||||
export type TargetLabelBranchResult = string[]|Promise<string[]>;
|
||||
|
||||
/**
|
||||
* Possible merge methods supported by the Github API.
|
||||
* https://developer.github.com/v3/pulls/#merge-a-pull-request-merge-button.
|
||||
@ -27,8 +31,11 @@ export interface TargetLabel {
|
||||
* List of branches a pull request with this target label should be merged into.
|
||||
* Can also be wrapped in a function that accepts the target branch specified in the
|
||||
* Github Web UI. This is useful for supporting labels like `target: development-branch`.
|
||||
*
|
||||
* @throws {InvalidTargetLabelError} Invalid label has been applied to pull request.
|
||||
* @throws {InvalidTargetBranchError} Invalid Github target branch has been selected.
|
||||
*/
|
||||
branches: string[]|((githubTargetBranch: string) => string[]);
|
||||
branches: TargetLabelBranchResult|((githubTargetBranch: string) => TargetLabelBranchResult);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -72,12 +79,13 @@ export interface MergeConfig {
|
||||
* on branch name computations. We don't want to run these immediately whenever
|
||||
* the dev-infra configuration is loaded as that could slow-down other commands.
|
||||
*/
|
||||
export type DevInfraMergeConfig = NgDevConfig<{'merge': () => MergeConfig}>;
|
||||
export type DevInfraMergeConfig =
|
||||
NgDevConfig<{'merge': (api: GithubClient) => MergeConfig | Promise<MergeConfig>}>;
|
||||
|
||||
/** Loads and validates the merge configuration. */
|
||||
export function loadAndValidateConfig(): {config?: MergeConfigWithRemote, errors?: string[]} {
|
||||
const config: Partial<DevInfraMergeConfig> = getConfig();
|
||||
|
||||
export async function loadAndValidateConfig(
|
||||
config: Partial<DevInfraMergeConfig>,
|
||||
api: GithubClient): Promise<{config?: MergeConfig, errors?: string[]}> {
|
||||
if (config.merge === undefined) {
|
||||
return {errors: ['No merge configuration found. Set the `merge` configuration.']};
|
||||
}
|
||||
@ -86,22 +94,14 @@ export function loadAndValidateConfig(): {config?: MergeConfigWithRemote, errors
|
||||
return {errors: ['Expected merge configuration to be defined lazily through a function.']};
|
||||
}
|
||||
|
||||
const mergeConfig = config.merge();
|
||||
const mergeConfig = await config.merge(api);
|
||||
const errors = validateMergeConfig(mergeConfig);
|
||||
|
||||
if (errors.length) {
|
||||
return {errors};
|
||||
}
|
||||
|
||||
if (mergeConfig.remote) {
|
||||
mergeConfig.remote = {...config.github, ...mergeConfig.remote};
|
||||
} else {
|
||||
mergeConfig.remote = config.github;
|
||||
}
|
||||
|
||||
// We always set the `remote` option, so we can safely cast the
|
||||
// config to `MergeConfigWithRemote`.
|
||||
return {config: mergeConfig as MergeConfigWithRemote};
|
||||
return {config: mergeConfig};
|
||||
}
|
||||
|
||||
/** Validates the specified configuration. Returns a list of failure messages. */
|
||||
|
212
dev-infra/pr/merge/defaults/branches.ts
Normal file
212
dev-infra/pr/merge/defaults/branches.ts
Normal file
@ -0,0 +1,212 @@
|
||||
/**
|
||||
* @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 * as semver from 'semver';
|
||||
import {GithubClient} from '../../../utils/git/github';
|
||||
|
||||
/** Type describing a Github repository with corresponding API client. */
|
||||
export interface GithubRepo {
|
||||
/** API client that can access the repository. */
|
||||
api: GithubClient;
|
||||
/** Owner login of the repository. */
|
||||
owner: string;
|
||||
/** Name of the repository. */
|
||||
repo: string;
|
||||
/**
|
||||
* NPM package representing this repository. Angular repositories usually contain
|
||||
* multiple packages in a monorepo scheme, but packages commonly are released with
|
||||
* the same versions. This means that a single package can be used for querying
|
||||
* NPM about previously published versions (e.g. to determine active LTS versions).
|
||||
* */
|
||||
npmPackageName: string;
|
||||
}
|
||||
|
||||
/** Type describing a version-branch. */
|
||||
export interface VersionBranch {
|
||||
/** Name of the branch in Git. e.g. `10.0.x`. */
|
||||
name: string;
|
||||
/**
|
||||
* Parsed SemVer version for the version-branch. Version branches technically do
|
||||
* not follow the SemVer format, but we can have representative SemVer versions
|
||||
* that can be used for comparisons, sorting and other checks.
|
||||
*/
|
||||
parsed: semver.SemVer;
|
||||
}
|
||||
|
||||
/** Branch name for the `next` branch. */
|
||||
export const nextBranchName = 'master';
|
||||
|
||||
/** Regular expression that matches version-branches for a release-train. */
|
||||
const releaseTrainBranchNameRegex = /(\d+)\.(\d+)\.x/;
|
||||
|
||||
/**
|
||||
* Fetches the active release train and its branches for the specified major version. i.e.
|
||||
* the latest active release-train branch name is resolved and an optional version-branch for
|
||||
* a currently active feature-freeze/release-candidate release-train.
|
||||
*/
|
||||
export async function fetchActiveReleaseTrainBranches(
|
||||
repo: GithubRepo, nextVersion: semver.SemVer): Promise<{
|
||||
/**
|
||||
* Name of the currently active release-candidate branch. Null if no
|
||||
* feature-freeze/release-candidate is currently active.
|
||||
*/
|
||||
releaseCandidateBranch: string | null,
|
||||
/** Name of the latest non-prerelease version branch (i.e. the patch branch). */
|
||||
latestVersionBranch: string
|
||||
}> {
|
||||
const majorVersionsToConsider: number[] = [];
|
||||
let expectedReleaseCandidateMajor: number;
|
||||
|
||||
// If the `next` branch (i.e. `master` branch) is for an upcoming major version, we know
|
||||
// that there is no patch branch or feature-freeze/release-candidate branch for this major
|
||||
// digit. If the current `next` version is the first minor of a major version, we know that
|
||||
// the feature-freeze/release-candidate branch can only be the actual major branch. The
|
||||
// patch branch is based on that, either the actual major branch or the last minor from the
|
||||
// preceding major version. In all other cases, the patch branch and feature-freeze or
|
||||
// release-candidate branch are part of the same major version. Consider the following:
|
||||
//
|
||||
// CASE 1. next: 11.0.0-next.0: patch and feature-freeze/release-candidate can only be
|
||||
// most recent `10.<>.x` branches. The FF/RC branch can only be the last-minor of v10.
|
||||
// CASE 2. next: 11.1.0-next.0: patch can be either `11.0.x` or last-minor in v10 based
|
||||
// on whether there is a feature-freeze/release-candidate branch (=> `11.0.x`).
|
||||
// CASE 3. next: 10.6.0-next.0: patch can be either `10.5.x` or `10.4.x` based on whether
|
||||
// there is a feature-freeze/release-candidate branch (=> `10.5.x`)
|
||||
if (nextVersion.minor === 0) {
|
||||
expectedReleaseCandidateMajor = nextVersion.major - 1;
|
||||
majorVersionsToConsider.push(nextVersion.major - 1);
|
||||
} else if (nextVersion.minor === 1) {
|
||||
expectedReleaseCandidateMajor = nextVersion.major;
|
||||
majorVersionsToConsider.push(nextVersion.major, nextVersion.major - 1);
|
||||
} else {
|
||||
expectedReleaseCandidateMajor = nextVersion.major;
|
||||
majorVersionsToConsider.push(nextVersion.major);
|
||||
}
|
||||
|
||||
// Collect all version-branches that should be considered for the latest version-branch,
|
||||
// or the feature-freeze/release-candidate.
|
||||
const branches = (await getBranchesForMajorVersions(repo, majorVersionsToConsider));
|
||||
const {latestVersionBranch, releaseCandidateBranch} =
|
||||
await findActiveVersionBranches(repo, nextVersion, branches, expectedReleaseCandidateMajor);
|
||||
|
||||
if (latestVersionBranch === null) {
|
||||
throw Error(
|
||||
`Unable to determine the latest release-train. The following branches ` +
|
||||
`have been considered: [${branches.join(', ')}]`);
|
||||
}
|
||||
|
||||
return {releaseCandidateBranch, latestVersionBranch};
|
||||
}
|
||||
|
||||
/** Gets the version of a given branch by reading the `package.json` upstream. */
|
||||
export async function getVersionOfBranch(
|
||||
repo: GithubRepo, branchName: string): Promise<semver.SemVer> {
|
||||
const {data} =
|
||||
await repo.api.repos.getContents({...repo, path: '/package.json', ref: branchName});
|
||||
const {version} = JSON.parse(Buffer.from(data.content, 'base64').toString());
|
||||
const parsedVersion = semver.parse(version);
|
||||
if (parsedVersion === null) {
|
||||
throw Error(`Invalid version detected in following branch: ${branchName}.`);
|
||||
}
|
||||
return parsedVersion;
|
||||
}
|
||||
|
||||
/** Whether the given branch corresponds to a release-train branch. */
|
||||
export function isReleaseTrainBranch(branchName: string): boolean {
|
||||
return releaseTrainBranchNameRegex.test(branchName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a given version-branch into a SemVer version that can be used with SemVer
|
||||
* utilities. e.g. to determine semantic order, extract major digit, compare.
|
||||
*
|
||||
* For example `10.0.x` will become `10.0.0` in SemVer. The patch digit is not
|
||||
* relevant but needed for parsing. SemVer does not allow `x` as patch digit.
|
||||
*/
|
||||
export function getVersionForReleaseTrainBranch(branchName: string): semver.SemVer|null {
|
||||
// Convert a given version-branch into a SemVer version that can be used
|
||||
// with the SemVer utilities. i.e. to determine semantic order.
|
||||
return semver.parse(branchName.replace(releaseTrainBranchNameRegex, '$1.$2.0'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the version branches for the specified major versions in descending
|
||||
* order. i.e. latest version branches first.
|
||||
*/
|
||||
export async function getBranchesForMajorVersions(
|
||||
repo: GithubRepo, majorVersions: number[]): Promise<VersionBranch[]> {
|
||||
const {data: branchData} = await repo.api.repos.listBranches({...repo, protected: true});
|
||||
const branches: VersionBranch[] = [];
|
||||
|
||||
for (const {name} of branchData) {
|
||||
if (!isReleaseTrainBranch(name)) {
|
||||
continue;
|
||||
}
|
||||
// Convert the version-branch into a SemVer version that can be used with the
|
||||
// SemVer utilities. e.g. to determine semantic order, compare versions.
|
||||
const parsed = getVersionForReleaseTrainBranch(name);
|
||||
// Collect all version-branches that match the specified major versions.
|
||||
if (parsed !== null && majorVersions.includes(parsed.major)) {
|
||||
branches.push({name, parsed});
|
||||
}
|
||||
}
|
||||
|
||||
// Sort captured version-branches in descending order.
|
||||
return branches.sort((a, b) => semver.rcompare(a.parsed, b.parsed));
|
||||
}
|
||||
|
||||
export async function findActiveVersionBranches(
|
||||
repo: GithubRepo, nextVersion: semver.SemVer, branches: VersionBranch[],
|
||||
expectedReleaseCandidateMajor: number): Promise<{
|
||||
latestVersionBranch: string | null,
|
||||
releaseCandidateBranch: string | null,
|
||||
}> {
|
||||
let latestVersionBranch: string|null = null;
|
||||
let releaseCandidateBranch: string|null = null;
|
||||
|
||||
// Iterate through the captured branches and find the latest non-prerelease branch and a
|
||||
// potential release candidate branch. From the collected branches we iterate descending
|
||||
// order (most recent semantic version-branch first). The first branch is either the latest
|
||||
// active version branch (i.e. patch) or a feature-freeze/release-candidate branch. A FF/RC
|
||||
// branch cannot be older than the latest active version-branch, so we stop iterating once
|
||||
// we found such a branch. Otherwise, if we found a FF/RC branch, we continue looking for the
|
||||
// next version-branch as that one is supposed to be the latest active version-branch. If it
|
||||
// is not, then an error will be thrown due to two FF/RC branches existing at the same time.
|
||||
for (const {name, parsed} of branches) {
|
||||
// It can happen that version branches that are more recent than the version in the next
|
||||
// branch (i.e. `master`) have been created. We could ignore such branches silently, but
|
||||
// it might actually be symptomatic for an outdated version in the `next` branch, or an
|
||||
// accidentally created branch by the caretaker. In either way we want to raise awareness.
|
||||
if (semver.gte(parsed, nextVersion)) {
|
||||
throw Error(
|
||||
`Discovered unexpected version-branch that is representing a minor ` +
|
||||
`version more recent than the one in the "${nextBranchName}" branch. Consider ` +
|
||||
`deleting the branch, or check if the version in "${nextBranchName}" is outdated.`);
|
||||
}
|
||||
|
||||
const version = await getVersionOfBranch(repo, name);
|
||||
const isPrerelease = version.prerelease[0] === 'rc' || version.prerelease[0] === 'next';
|
||||
if (isPrerelease) {
|
||||
if (releaseCandidateBranch !== null) {
|
||||
throw Error(
|
||||
`Unable to determine latest release-train. Found two consecutive ` +
|
||||
`branches in feature-freeze/release-candidate phase. Did not expect both "${name}" ` +
|
||||
`and "${releaseCandidateBranch}" to be in feature-freeze/release-candidate mode.`);
|
||||
} else if (version.major !== expectedReleaseCandidateMajor) {
|
||||
throw Error(
|
||||
`Discovered unexpected old feature-freeze/release-candidate branch. Expected no ` +
|
||||
`version-branch in feature-freeze/release-candidate mode for v${version.major}.`);
|
||||
}
|
||||
releaseCandidateBranch = name;
|
||||
} else {
|
||||
latestVersionBranch = name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {releaseCandidateBranch, latestVersionBranch};
|
||||
}
|
11
dev-infra/pr/merge/defaults/index.ts
Normal file
11
dev-infra/pr/merge/defaults/index.ts
Normal file
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
export * from './labels';
|
||||
export * from './branches';
|
||||
export * from './lts-branch';
|
455
dev-infra/pr/merge/defaults/integration.spec.ts
Normal file
455
dev-infra/pr/merge/defaults/integration.spec.ts
Normal file
@ -0,0 +1,455 @@
|
||||
/**
|
||||
* @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 * as nock from 'nock';
|
||||
import * as nodeFetch from 'node-fetch';
|
||||
|
||||
import {GithubConfig} from '../../../utils/config';
|
||||
import * as console from '../../../utils/console';
|
||||
import {GithubClient} from '../../../utils/git/github';
|
||||
import {TargetLabel} from '../config';
|
||||
import {getBranchesFromTargetLabel, getTargetLabelFromPullRequest} from '../target-label';
|
||||
|
||||
import {getDefaultTargetLabelConfiguration} from './index';
|
||||
|
||||
const API_ENDPOINT = `https://api.github.com`;
|
||||
|
||||
describe('default target labels', () => {
|
||||
let api: GithubClient;
|
||||
let config: GithubConfig;
|
||||
let npmPackageName: string;
|
||||
|
||||
beforeEach(() => {
|
||||
api = new GithubClient();
|
||||
config = {owner: 'angular', name: 'dev-infra-test'};
|
||||
npmPackageName = '@angular/dev-infra-test-pkg';
|
||||
|
||||
// The label determination will print warn messages. These should not be
|
||||
// printed to the console, so we turn `console.warn` into a spy.
|
||||
spyOn(console, 'warn');
|
||||
});
|
||||
|
||||
afterEach(() => nock.cleanAll());
|
||||
|
||||
async function computeTargetLabels(): Promise<TargetLabel[]> {
|
||||
return getDefaultTargetLabelConfiguration(api, config, npmPackageName);
|
||||
}
|
||||
|
||||
function getRepoApiRequestUrl(): string {
|
||||
return `${API_ENDPOINT}/repos/${config.owner}/${config.name}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mocks a branch `package.json` version API request.
|
||||
* https://docs.github.com/en/rest/reference/repos#get-repository-content.
|
||||
*/
|
||||
function interceptBranchVersionRequest(branchName: string, version: string) {
|
||||
nock(getRepoApiRequestUrl())
|
||||
.get('/contents//package.json')
|
||||
.query(params => params.ref === branchName)
|
||||
.reply(200, {content: Buffer.from(JSON.stringify({version})).toString('base64')});
|
||||
}
|
||||
|
||||
/** Fakes a prompt confirm question with the given value. */
|
||||
function fakePromptConfirmValue(returnValue: boolean) {
|
||||
spyOn(console, 'promptConfirm').and.resolveTo(returnValue);
|
||||
}
|
||||
|
||||
/** Fakes a NPM package query API request. */
|
||||
function fakeNpmPackageQueryRequest(data: unknown) {
|
||||
// Note: We only need to mock the `json` function for a `Response`. Types
|
||||
// would expect us to mock more functions, so we need to cast to `any`.
|
||||
spyOn(nodeFetch, 'default').and.resolveTo({json: async () => data} as any);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mocks a repository branch list API request.
|
||||
* https://docs.github.com/en/rest/reference/repos#list-branches.
|
||||
*/
|
||||
function interceptBranchesListRequest(branches: string[]) {
|
||||
nock(getRepoApiRequestUrl())
|
||||
.get('/branches')
|
||||
.query(true)
|
||||
.reply(200, branches.map(name => ({name})));
|
||||
}
|
||||
|
||||
async function getBranchesForLabel(
|
||||
name: string, githubTargetBranch = 'master', labels?: TargetLabel[]): Promise<string[]|null> {
|
||||
if (labels === undefined) {
|
||||
labels = await computeTargetLabels();
|
||||
}
|
||||
const label = getTargetLabelFromPullRequest({labels}, [name]);
|
||||
if (label === null) {
|
||||
return null;
|
||||
}
|
||||
return await getBranchesFromTargetLabel(label, githubTargetBranch);
|
||||
}
|
||||
|
||||
it('should detect "master" as branch for target: minor', async () => {
|
||||
interceptBranchVersionRequest('master', '11.0.0-next.0');
|
||||
interceptBranchVersionRequest('10.2.x', '10.2.4');
|
||||
interceptBranchesListRequest(['10.2.x']);
|
||||
|
||||
expect(await getBranchesForLabel('target: minor')).toEqual(['master']);
|
||||
});
|
||||
|
||||
it('should error if non version-branch is targeted with "target: lts"', async () => {
|
||||
interceptBranchVersionRequest('master', '11.0.0-next.0');
|
||||
interceptBranchVersionRequest('10.2.x', '10.2.4');
|
||||
interceptBranchesListRequest(['10.2.x']);
|
||||
|
||||
await expectAsync(getBranchesForLabel('target: lts', 'master'))
|
||||
.toBeRejectedWith(jasmine.objectContaining({
|
||||
failureMessage:
|
||||
'PR cannot be merged as it does not target a long-term support branch: "master"'
|
||||
}));
|
||||
});
|
||||
|
||||
it('should error if patch branch is targeted with "target: lts"', async () => {
|
||||
interceptBranchVersionRequest('master', '11.0.0-next.0');
|
||||
interceptBranchVersionRequest('10.2.x', '10.2.4');
|
||||
interceptBranchesListRequest(['10.2.x']);
|
||||
|
||||
await expectAsync(getBranchesForLabel('target: lts', '10.2.x'))
|
||||
.toBeRejectedWith(jasmine.objectContaining({
|
||||
failureMessage:
|
||||
'PR cannot be merged with "target: lts" into patch branch. Consider changing the ' +
|
||||
'label to "target: patch" if this is intentional.'
|
||||
}));
|
||||
});
|
||||
|
||||
it('should error if feature-freeze branch is targeted with "target: lts"', async () => {
|
||||
interceptBranchVersionRequest('master', '11.0.0-next.0');
|
||||
interceptBranchVersionRequest('10.2.x', '10.2.0-next.0');
|
||||
interceptBranchVersionRequest('10.1.x', '10.1.0');
|
||||
interceptBranchesListRequest(['10.1.x', '10.2.x']);
|
||||
|
||||
await expectAsync(getBranchesForLabel('target: lts', '10.2.x'))
|
||||
.toBeRejectedWith(jasmine.objectContaining({
|
||||
failureMessage:
|
||||
'PR cannot be merged with "target: lts" into feature-freeze/release-candidate branch. ' +
|
||||
'Consider changing the label to "target: rc" if this is intentional.'
|
||||
}));
|
||||
});
|
||||
|
||||
it('should error if release-candidate branch is targeted with "target: lts"', async () => {
|
||||
interceptBranchVersionRequest('master', '11.0.0-next.0');
|
||||
interceptBranchVersionRequest('10.2.x', '10.2.0-rc.0');
|
||||
interceptBranchVersionRequest('10.1.x', '10.1.0');
|
||||
interceptBranchesListRequest(['10.1.x', '10.2.x']);
|
||||
|
||||
await expectAsync(getBranchesForLabel('target: lts', '10.2.x'))
|
||||
.toBeRejectedWith(jasmine.objectContaining({
|
||||
failureMessage:
|
||||
'PR cannot be merged with "target: lts" into feature-freeze/release-candidate branch. ' +
|
||||
'Consider changing the label to "target: rc" if this is intentional.'
|
||||
}));
|
||||
});
|
||||
|
||||
it('should error if branch targeted with "target: lts" is no longer active', async () => {
|
||||
interceptBranchVersionRequest('master', '11.1.0-next.0');
|
||||
interceptBranchVersionRequest('11.0.x', '11.0.0');
|
||||
interceptBranchVersionRequest('10.5.x', '10.5.1');
|
||||
interceptBranchesListRequest(['10.5.x', '11.0.x']);
|
||||
|
||||
// We support forcibly proceeding with merging if a given branch previously was in LTS mode
|
||||
// but no longer is (after a period of time). In this test, we are not forcibly proceeding.
|
||||
fakePromptConfirmValue(false);
|
||||
fakeNpmPackageQueryRequest({
|
||||
'dist-tags': {
|
||||
'v10-lts': '10.5.1',
|
||||
},
|
||||
'time': {
|
||||
// v10 has been released at the given specified date. We pick a date that
|
||||
// guarantees that the version is no longer considered as active LTS version.
|
||||
'10.0.0': new Date(1912, 5, 23),
|
||||
}
|
||||
});
|
||||
|
||||
await expectAsync(getBranchesForLabel('target: lts', '10.5.x'))
|
||||
.toBeRejectedWith(jasmine.objectContaining({
|
||||
failureMessage:
|
||||
'Long-term supported ended for v10 on 12/23/1913. Pull request cannot be merged ' +
|
||||
'into the 10.5.x branch.'
|
||||
}));
|
||||
});
|
||||
|
||||
it('should error if branch targeted with "target: lts" is not latest LTS for given major',
|
||||
async () => {
|
||||
interceptBranchVersionRequest('master', '11.1.0-next.0');
|
||||
interceptBranchVersionRequest('11.0.x', '11.0.0');
|
||||
interceptBranchVersionRequest('10.5.x', '10.5.1');
|
||||
interceptBranchVersionRequest('10.4.x', '10.4.4');
|
||||
interceptBranchesListRequest(['10.4.x', '10.5.x', '11.0.x']);
|
||||
|
||||
fakeNpmPackageQueryRequest({
|
||||
'dist-tags': {
|
||||
'v10-lts': '10.5.1',
|
||||
}
|
||||
});
|
||||
|
||||
await expectAsync(getBranchesForLabel('target: lts', '10.4.x'))
|
||||
.toBeRejectedWith(jasmine.objectContaining({
|
||||
failureMessage:
|
||||
'Not using last-minor branch for v10 LTS version. PR should be updated to ' +
|
||||
'target: 10.5.x'
|
||||
}));
|
||||
});
|
||||
|
||||
it('should error if branch targeted with "target: lts" is not a major version with LTS',
|
||||
async () => {
|
||||
interceptBranchVersionRequest('master', '11.1.0-next.0');
|
||||
interceptBranchVersionRequest('11.0.x', '11.0.0');
|
||||
interceptBranchVersionRequest('10.5.x', '10.5.1');
|
||||
interceptBranchesListRequest(['10.5.x', '11.0.x']);
|
||||
|
||||
fakeNpmPackageQueryRequest({'dist-tags': {}});
|
||||
|
||||
await expectAsync(getBranchesForLabel('target: lts', '10.5.x'))
|
||||
.toBeRejectedWith(
|
||||
jasmine.objectContaining({failureMessage: 'No LTS version tagged for v10 in NPM.'}));
|
||||
});
|
||||
|
||||
it('should allow forcibly proceeding with merge if branch targeted with "target: lts" is no ' +
|
||||
'longer active',
|
||||
async () => {
|
||||
interceptBranchVersionRequest('master', '11.1.0-next.0');
|
||||
interceptBranchVersionRequest('11.0.x', '11.0.0');
|
||||
interceptBranchVersionRequest('10.5.x', '10.5.1');
|
||||
interceptBranchesListRequest(['10.5.x', '11.0.x']);
|
||||
|
||||
// We support forcibly proceeding with merging if a given branch previously was in LTS mode
|
||||
// but no longer is (after a period of time). In this test, we are forcibly proceeding and
|
||||
// expect the Github target branch to be picked up as branch for the `target: lts` label.
|
||||
fakePromptConfirmValue(true);
|
||||
fakeNpmPackageQueryRequest({
|
||||
'dist-tags': {
|
||||
'v10-lts': '10.5.1',
|
||||
},
|
||||
'time': {
|
||||
// v10 has been released at the given specified date. We pick a date that
|
||||
// guarantees that the version is no longer considered as active LTS version.
|
||||
'10.0.0': new Date(1912, 5, 23),
|
||||
}
|
||||
});
|
||||
|
||||
expect(await getBranchesForLabel('target: lts', '10.5.x')).toEqual(['10.5.x']);
|
||||
});
|
||||
|
||||
it('should use target branch for "target: lts" if it matches an active LTS branch', async () => {
|
||||
interceptBranchVersionRequest('master', '11.1.0-next.0');
|
||||
interceptBranchVersionRequest('11.0.x', '11.0.0');
|
||||
interceptBranchVersionRequest('10.5.x', '10.5.1');
|
||||
interceptBranchesListRequest(['10.5.x', '11.0.x']);
|
||||
|
||||
spyOn(require('node-fetch'), 'default').and.callFake(() => ({
|
||||
json: () => ({
|
||||
'dist-tags': {
|
||||
'v10-lts': '10.5.1',
|
||||
},
|
||||
'time': {
|
||||
'10.0.0': new Date().toISOString(),
|
||||
}
|
||||
}),
|
||||
}));
|
||||
|
||||
expect(await getBranchesForLabel('target: lts', '10.5.x')).toEqual(['10.5.x']);
|
||||
});
|
||||
|
||||
it('should error if no active branch for given major version could be found', async () => {
|
||||
interceptBranchVersionRequest('master', '12.0.0-next.0');
|
||||
interceptBranchesListRequest(['9.0.x', '9.1.x']);
|
||||
|
||||
await expectAsync(getBranchesForLabel('target: lts', '10.2.x'))
|
||||
.toBeRejectedWithError(
|
||||
'Unable to determine the latest release-train. The following branches have ' +
|
||||
'been considered: []');
|
||||
});
|
||||
|
||||
it('should error if invalid version is set for version-branch', async () => {
|
||||
interceptBranchVersionRequest('master', '11.2.0-next.0');
|
||||
interceptBranchVersionRequest('11.1.x', '11.1.x');
|
||||
interceptBranchesListRequest(['11.1.x']);
|
||||
|
||||
await expectAsync(getBranchesForLabel('target: lts', '10.2.x'))
|
||||
.toBeRejectedWithError('Invalid version detected in following branch: 11.1.x.');
|
||||
});
|
||||
|
||||
it('should error if branch more recent than version in "next" branch is found', async () => {
|
||||
interceptBranchVersionRequest('master', '11.2.0-next.0');
|
||||
interceptBranchVersionRequest('11.2.x', '11.2.0-next.0');
|
||||
interceptBranchVersionRequest('11.1.x', '11.1.5');
|
||||
interceptBranchesListRequest(['11.1.x', '11.2.x']);
|
||||
|
||||
await expectAsync(getBranchesForLabel('target: lts', '10.2.x'))
|
||||
.toBeRejectedWithError(
|
||||
'Discovered unexpected version-branch that is representing a minor version more ' +
|
||||
'recent than the one in the "master" branch. Consider deleting the branch, or check ' +
|
||||
'if the version in "master" is outdated.');
|
||||
});
|
||||
|
||||
it('should allow merging PR only into patch branch with "target: patch"', async () => {
|
||||
interceptBranchVersionRequest('master', '11.2.0-next.0');
|
||||
interceptBranchVersionRequest('11.1.x', '11.1.0');
|
||||
interceptBranchesListRequest(['11.1.x']);
|
||||
|
||||
expect(await getBranchesForLabel('target: patch', '11.1.x')).toEqual(['11.1.x']);
|
||||
});
|
||||
|
||||
describe('next: major release', () => {
|
||||
it('should detect "master" as branch for target: major', async () => {
|
||||
interceptBranchVersionRequest('master', '11.0.0-next.0');
|
||||
interceptBranchVersionRequest('10.2.x', '10.2.4');
|
||||
interceptBranchesListRequest(['10.2.x']);
|
||||
|
||||
expect(await getBranchesForLabel('target: major')).toEqual(['master']);
|
||||
});
|
||||
|
||||
describe('without active release-candidate', () => {
|
||||
it('should detect last-minor from previous major as branch for target: patch', async () => {
|
||||
interceptBranchVersionRequest('master', '11.0.0-next.0');
|
||||
interceptBranchVersionRequest('10.2.x', '10.2.4');
|
||||
interceptBranchesListRequest(['10.0.x', '10.1.x', '10.2.x']);
|
||||
|
||||
expect(await getBranchesForLabel('target: patch')).toEqual(['master', '10.2.x']);
|
||||
});
|
||||
|
||||
it('should error if "target: rc" is applied', async () => {
|
||||
interceptBranchVersionRequest('master', '11.0.0-next.0');
|
||||
interceptBranchVersionRequest('10.2.x', '10.2.4');
|
||||
interceptBranchesListRequest(['10.0.x', '10.1.x', '10.2.x']);
|
||||
|
||||
await expectAsync(getBranchesForLabel('target: rc'))
|
||||
.toBeRejectedWith(jasmine.objectContaining({
|
||||
failureMessage:
|
||||
'No active feature-freeze/release-candidate branch. Unable to merge ' +
|
||||
'pull request using "target: rc" label.'
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('with active release-candidate', () => {
|
||||
it('should detect most recent non-prerelease minor branch from previous major for ' +
|
||||
'target: patch',
|
||||
async () => {
|
||||
interceptBranchVersionRequest('master', '11.0.0-next.0');
|
||||
interceptBranchVersionRequest('10.2.x', '10.2.0-rc.0');
|
||||
interceptBranchVersionRequest('10.1.x', '10.2.3');
|
||||
interceptBranchesListRequest(['10.1.x', '10.2.x']);
|
||||
|
||||
// Pull requests should also be merged into the RC and `next` (i.e. `master`) branch.
|
||||
expect(await getBranchesForLabel('target: patch')).toEqual([
|
||||
'master', '10.1.x', '10.2.x'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should detect release-candidate branch for "target: rc"', async () => {
|
||||
interceptBranchVersionRequest('master', '11.0.0-next.0');
|
||||
interceptBranchVersionRequest('10.2.x', '10.2.0-rc.0');
|
||||
interceptBranchVersionRequest('10.1.x', '10.1.0');
|
||||
interceptBranchesListRequest(['10.0.x', '10.1.x', '10.2.x']);
|
||||
|
||||
expect(await getBranchesForLabel('target: rc')).toEqual(['master', '10.2.x']);
|
||||
});
|
||||
|
||||
it('should detect feature-freeze branch with "target: rc"', async () => {
|
||||
interceptBranchVersionRequest('master', '11.0.0-next.0');
|
||||
interceptBranchVersionRequest('10.2.x', '10.2.0-next.0');
|
||||
interceptBranchVersionRequest('10.1.x', '10.1.0');
|
||||
interceptBranchesListRequest(['10.0.x', '10.1.x', '10.2.x']);
|
||||
|
||||
expect(await getBranchesForLabel('target: rc')).toEqual(['master', '10.2.x']);
|
||||
});
|
||||
|
||||
it('should error if multiple consecutive release-candidate branches are found', async () => {
|
||||
interceptBranchVersionRequest('master', '11.0.0-next.0');
|
||||
interceptBranchVersionRequest('10.4.x', '10.4.0-next.0');
|
||||
interceptBranchVersionRequest('10.3.x', '10.4.0-rc.5');
|
||||
interceptBranchesListRequest(['10.3.x', '10.4.x']);
|
||||
|
||||
await expectAsync(getBranchesForLabel('target: patch'))
|
||||
.toBeRejectedWithError(
|
||||
'Unable to determine latest release-train. Found two consecutive ' +
|
||||
'branches in feature-freeze/release-candidate phase. Did not expect both ' +
|
||||
'"10.3.x" and "10.4.x" to be in feature-freeze/release-candidate mode.');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('next: minor release', () => {
|
||||
it('should error if "target: major" is applied', async () => {
|
||||
interceptBranchVersionRequest('master', '11.2.0-next.0');
|
||||
interceptBranchVersionRequest('11.1.x', '11.1.4');
|
||||
interceptBranchesListRequest(['11.1.x']);
|
||||
|
||||
await expectAsync(getBranchesForLabel('target: major'))
|
||||
.toBeRejectedWith(jasmine.objectContaining({
|
||||
failureMessage:
|
||||
'Unable to merge pull request. The "master" branch will be released as ' +
|
||||
'a minor version.',
|
||||
}));
|
||||
});
|
||||
|
||||
describe('without active release-candidate', () => {
|
||||
it('should detect last-minor from previous major as branch for target: patch', async () => {
|
||||
interceptBranchVersionRequest('master', '11.2.0-next.0');
|
||||
interceptBranchVersionRequest('11.1.x', '11.1.0');
|
||||
interceptBranchesListRequest(['11.1.x']);
|
||||
|
||||
expect(await getBranchesForLabel('target: patch')).toEqual(['master', '11.1.x']);
|
||||
});
|
||||
|
||||
it('should error if "target: rc" is applied', async () => {
|
||||
interceptBranchVersionRequest('master', '11.2.0-next.0');
|
||||
interceptBranchVersionRequest('11.1.x', '11.1.0');
|
||||
interceptBranchesListRequest(['11.1.x']);
|
||||
|
||||
await expectAsync(getBranchesForLabel('target: rc'))
|
||||
.toBeRejectedWith(jasmine.objectContaining({
|
||||
failureMessage:
|
||||
'No active feature-freeze/release-candidate branch. Unable to merge pull ' +
|
||||
'request using "target: rc" label.'
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('with active release-candidate', () => {
|
||||
it('should detect most recent non-prerelease minor branch from previous major for ' +
|
||||
'target: patch',
|
||||
async () => {
|
||||
interceptBranchVersionRequest('master', '11.2.0-next.0');
|
||||
interceptBranchVersionRequest('11.1.x', '11.1.0-rc.0');
|
||||
interceptBranchVersionRequest('11.0.x', '11.0.0');
|
||||
interceptBranchesListRequest(['11.0.x', '11.1.x']);
|
||||
|
||||
// Pull requests should also be merged into the RC and `next` (i.e. `master`) branch.
|
||||
expect(await getBranchesForLabel('target: patch')).toEqual([
|
||||
'master', '11.0.x', '11.1.x'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should detect release-candidate branch for "target: rc"', async () => {
|
||||
interceptBranchVersionRequest('master', '11.2.0-next.0');
|
||||
interceptBranchVersionRequest('11.1.x', '11.1.0-rc.0');
|
||||
interceptBranchVersionRequest('11.0.x', '10.0.0');
|
||||
interceptBranchesListRequest(['11.0.x', '11.1.x']);
|
||||
|
||||
expect(await getBranchesForLabel('target: rc')).toEqual(['master', '11.1.x']);
|
||||
});
|
||||
|
||||
it('should detect feature-freeze branch with "target: rc"', async () => {
|
||||
interceptBranchVersionRequest('master', '11.2.0-next.0');
|
||||
interceptBranchVersionRequest('11.1.x', '11.1.0-next.0');
|
||||
interceptBranchVersionRequest('11.0.x', '10.0.0');
|
||||
interceptBranchesListRequest(['11.0.x', '11.1.x']);
|
||||
|
||||
expect(await getBranchesForLabel('target: rc')).toEqual(['master', '11.1.x']);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
124
dev-infra/pr/merge/defaults/labels.ts
Normal file
124
dev-infra/pr/merge/defaults/labels.ts
Normal file
@ -0,0 +1,124 @@
|
||||
/**
|
||||
* @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 {GithubConfig} from '../../../utils/config';
|
||||
import {GithubClient} from '../../../utils/git/github';
|
||||
import {TargetLabel} from '../config';
|
||||
import {InvalidTargetBranchError, InvalidTargetLabelError} from '../target-label';
|
||||
|
||||
import {fetchActiveReleaseTrainBranches, getVersionOfBranch, GithubRepo, isReleaseTrainBranch, nextBranchName} from './branches';
|
||||
import {assertActiveLtsBranch} from './lts-branch';
|
||||
|
||||
/**
|
||||
* Gets a label configuration for the merge tooling that reflects the default Angular
|
||||
* organization-wide labeling and branching semantics as outlined in the specification.
|
||||
*
|
||||
* https://docs.google.com/document/d/197kVillDwx-RZtSVOBtPb4BBIAw0E9RT3q3v6DZkykU
|
||||
*/
|
||||
export async function getDefaultTargetLabelConfiguration(
|
||||
api: GithubClient, github: GithubConfig, npmPackageName: string): Promise<TargetLabel[]> {
|
||||
const repo: GithubRepo = {owner: github.owner, repo: github.name, api, npmPackageName};
|
||||
const nextVersion = await getVersionOfBranch(repo, nextBranchName);
|
||||
const hasNextMajorTrain = nextVersion.minor === 0;
|
||||
const {latestVersionBranch, releaseCandidateBranch} =
|
||||
await fetchActiveReleaseTrainBranches(repo, nextVersion);
|
||||
|
||||
return [
|
||||
{
|
||||
pattern: 'target: major',
|
||||
branches: () => {
|
||||
// If `next` is currently not designated to be a major version, we do not
|
||||
// allow merging of PRs with `target: major`.
|
||||
if (!hasNextMajorTrain) {
|
||||
throw new InvalidTargetLabelError(
|
||||
`Unable to merge pull request. The "${nextBranchName}" branch will be ` +
|
||||
`released as a minor version.`);
|
||||
}
|
||||
return [nextBranchName];
|
||||
},
|
||||
},
|
||||
{
|
||||
pattern: 'target: minor',
|
||||
// Changes labeled with `target: minor` are merged most commonly into the next branch
|
||||
// (i.e. `master`). In rare cases of an exceptional minor version while being already
|
||||
// on a major release train, this would need to be overridden manually.
|
||||
// TODO: Consider handling this automatically by checking if the NPM version matches
|
||||
// the last-minor. If not, then an exceptional minor might be in progress. See:
|
||||
// https://docs.google.com/document/d/197kVillDwx-RZtSVOBtPb4BBIAw0E9RT3q3v6DZkykU/edit#heading=h.h7o5pjq6yqd0
|
||||
branches: [nextBranchName],
|
||||
},
|
||||
{
|
||||
pattern: 'target: patch',
|
||||
branches: githubTargetBranch => {
|
||||
// If a PR is targeting the latest active version-branch through the Github UI,
|
||||
// and is also labeled with `target: patch`, then we merge it directly into the
|
||||
// branch without doing any cherry-picking. This is useful if a PR could not be
|
||||
// applied cleanly, and a separate PR for the patch branch has been created.
|
||||
if (githubTargetBranch === latestVersionBranch) {
|
||||
return [latestVersionBranch];
|
||||
}
|
||||
// Otherwise, patch changes are always merged into the next and patch branch.
|
||||
const branches = [nextBranchName, latestVersionBranch];
|
||||
// Additionally, if there is a release-candidate/feature-freeze release-train
|
||||
// currently active, also merge the PR into that version-branch.
|
||||
if (releaseCandidateBranch !== null) {
|
||||
branches.push(releaseCandidateBranch);
|
||||
}
|
||||
return branches;
|
||||
}
|
||||
},
|
||||
{
|
||||
pattern: 'target: rc',
|
||||
branches: githubTargetBranch => {
|
||||
// The `target: rc` label cannot be applied if there is no active feature-freeze
|
||||
// or release-candidate release train.
|
||||
if (releaseCandidateBranch === null) {
|
||||
throw new InvalidTargetLabelError(
|
||||
`No active feature-freeze/release-candidate branch. ` +
|
||||
`Unable to merge pull request using "target: rc" label.`);
|
||||
}
|
||||
// If the PR is targeting the active release-candidate/feature-freeze version branch
|
||||
// directly through the Github UI and has the `target: rc` label applied, merge it
|
||||
// only into the release candidate branch. This is useful if a PR did not apply cleanly
|
||||
// into the release-candidate/feature-freeze branch, and a separate PR has been created.
|
||||
if (githubTargetBranch === releaseCandidateBranch) {
|
||||
return [releaseCandidateBranch];
|
||||
}
|
||||
// Otherwise, merge into the next and active release-candidate/feature-freeze branch.
|
||||
return [nextBranchName, releaseCandidateBranch];
|
||||
},
|
||||
},
|
||||
{
|
||||
// LTS changes are rare enough that we won't worry about cherry-picking changes into all
|
||||
// active LTS branches for PRs created against any other branch. Instead, PR authors need
|
||||
// to manually create separate PRs for desired LTS branches. Additionally, active LT branches
|
||||
// commonly diverge quickly. This makes cherry-picking not an option for LTS changes.
|
||||
pattern: 'target: lts',
|
||||
branches: async githubTargetBranch => {
|
||||
if (!isReleaseTrainBranch(githubTargetBranch)) {
|
||||
throw new InvalidTargetBranchError(
|
||||
`PR cannot be merged as it does not target a long-term support ` +
|
||||
`branch: "${githubTargetBranch}"`);
|
||||
}
|
||||
if (githubTargetBranch === latestVersionBranch) {
|
||||
throw new InvalidTargetBranchError(
|
||||
`PR cannot be merged with "target: lts" into patch branch. ` +
|
||||
`Consider changing the label to "target: patch" if this is intentional.`);
|
||||
}
|
||||
if (githubTargetBranch === releaseCandidateBranch && releaseCandidateBranch !== null) {
|
||||
throw new InvalidTargetBranchError(
|
||||
`PR cannot be merged with "target: lts" into feature-freeze/release-candidate ` +
|
||||
`branch. Consider changing the label to "target: rc" if this is intentional.`);
|
||||
}
|
||||
// Assert that the selected branch is an active LTS branch.
|
||||
await assertActiveLtsBranch(repo, githubTargetBranch);
|
||||
return [githubTargetBranch];
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
80
dev-infra/pr/merge/defaults/lts-branch.ts
Normal file
80
dev-infra/pr/merge/defaults/lts-branch.ts
Normal file
@ -0,0 +1,80 @@
|
||||
/**
|
||||
* @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 fetch from 'node-fetch';
|
||||
import * as semver from 'semver';
|
||||
|
||||
import {promptConfirm, red, warn, yellow} from '../../../utils/console';
|
||||
import {InvalidTargetBranchError} from '../target-label';
|
||||
|
||||
import {getVersionOfBranch, GithubRepo} from './branches';
|
||||
|
||||
/**
|
||||
* Number of months a major version in Angular is actively supported. See:
|
||||
* https://angular.io/guide/releases#support-policy-and-schedule.
|
||||
*/
|
||||
const majorActiveSupportDuration = 6;
|
||||
|
||||
/**
|
||||
* Number of months a major version has active long-term support. See:
|
||||
* https://angular.io/guide/releases#support-policy-and-schedule.
|
||||
*/
|
||||
const majorActiveTermSupportDuration = 12;
|
||||
|
||||
/**
|
||||
* Asserts that the given branch corresponds to an active LTS version-branch that can receive
|
||||
* backported fixes. Throws an error if LTS expired or an invalid branch is selected.
|
||||
*/
|
||||
export async function assertActiveLtsBranch(repo: GithubRepo, branchName: string) {
|
||||
const version = await getVersionOfBranch(repo, branchName);
|
||||
const {'dist-tags': distTags, time} =
|
||||
await (await fetch(`https://registry.npmjs.org/${repo.npmPackageName}`)).json();
|
||||
|
||||
// LTS versions should be tagged in NPM in the following format: `v{major}-lts`.
|
||||
const ltsVersion = semver.parse(distTags[`v${version.major}-lts`]);
|
||||
|
||||
// Ensure that there is a LTS version tagged for the given version-branch major. e.g.
|
||||
// if the version branch is `9.2.x` then we want to make sure that there is a LTS
|
||||
// version tagged in NPM for `v9`, following the `v{major}-lts` tag convention.
|
||||
if (ltsVersion === null) {
|
||||
throw new InvalidTargetBranchError(`No LTS version tagged for v${version.major} in NPM.`);
|
||||
}
|
||||
|
||||
// Ensure that the correct branch is used for the LTS version. We do not want to merge
|
||||
// changes to older minor version branches that do not reflect the current LTS version.
|
||||
if (branchName !== `${ltsVersion.major}.${ltsVersion.minor}.x`) {
|
||||
throw new InvalidTargetBranchError(
|
||||
`Not using last-minor branch for v${version.major} LTS version. PR ` +
|
||||
`should be updated to target: ${ltsVersion.major}.${ltsVersion.minor}.x`);
|
||||
}
|
||||
|
||||
const today = new Date();
|
||||
const releaseDate = new Date(time[`${version.major}.0.0`]);
|
||||
const ltsEndDate = new Date(
|
||||
releaseDate.getFullYear(),
|
||||
releaseDate.getMonth() + majorActiveSupportDuration + majorActiveTermSupportDuration,
|
||||
releaseDate.getDate(), releaseDate.getHours(), releaseDate.getMinutes(),
|
||||
releaseDate.getSeconds(), releaseDate.getMilliseconds());
|
||||
|
||||
// Check if LTS has already expired for the targeted major version. If so, we do not
|
||||
// allow the merge as per our LTS guarantees. Can be forcibly overridden if desired.
|
||||
// See: https://angular.io/guide/releases#support-policy-and-schedule.
|
||||
if (today > ltsEndDate) {
|
||||
const ltsEndDateText = ltsEndDate.toLocaleDateString();
|
||||
warn(red(`Long-term support ended for v${version.major} on ${ltsEndDateText}.`));
|
||||
warn(yellow(
|
||||
`Merging of pull requests for this major is generally not ` +
|
||||
`desired, but can be forcibly ignored.`));
|
||||
if (await promptConfirm('Do you want to forcibly proceed with merging?')) {
|
||||
return;
|
||||
}
|
||||
throw new InvalidTargetBranchError(
|
||||
`Long-term supported ended for v${version.major} on ${ltsEndDateText}. ` +
|
||||
`Pull request cannot be merged into the ${branchName} branch.`);
|
||||
}
|
||||
}
|
@ -1,68 +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 * as semver from 'semver';
|
||||
import {exec} from '../../utils/shelljs';
|
||||
|
||||
/**
|
||||
* Helper function that can be used to determine merge branches based on a given
|
||||
* project version. The function determines merge branches primarily through the
|
||||
* specified version, but falls back to consulting the NPM registry when needed.
|
||||
*
|
||||
* Consulting the NPM registry for determining the patch branch may slow down merging,
|
||||
* so whenever possible, the branches are determined statically based on the current
|
||||
* version. In some cases, consulting the NPM registry is inevitable because for major
|
||||
* pre-releases, we cannot determine the latest stable minor version from the current
|
||||
* pre-release version.
|
||||
*/
|
||||
export function determineMergeBranches(
|
||||
currentVersion: string, npmPackageName: string): {minor: string, patch: string} {
|
||||
const projectVersion = semver.parse(currentVersion);
|
||||
if (projectVersion === null) {
|
||||
throw Error('Cannot parse version set in project "package.json" file.');
|
||||
}
|
||||
const {major, minor, patch, prerelease} = projectVersion;
|
||||
const isMajor = minor === 0 && patch === 0;
|
||||
const isMinor = minor !== 0 && patch === 0;
|
||||
|
||||
// If there is no prerelease, then we compute patch and minor branches based
|
||||
// on the current version major and minor.
|
||||
if (prerelease.length === 0) {
|
||||
return {minor: `${major}.x`, patch: `${major}.${minor}.x`};
|
||||
}
|
||||
|
||||
// If current version is set to a minor prerelease, we can compute the merge branches
|
||||
// statically. e.g. if we are set to `9.3.0-next.0`, then our merge branches should
|
||||
// be set to `9.x` and `9.2.x`.
|
||||
if (isMinor) {
|
||||
return {minor: `${major}.x`, patch: `${major}.${minor - 1}.x`};
|
||||
} else if (!isMajor) {
|
||||
throw Error('Unexpected version. Cannot have prerelease for patch version.');
|
||||
}
|
||||
|
||||
// If we are set to a major prerelease, we cannot statically determine the stable patch
|
||||
// branch (as the latest minor segment is unknown). We determine it by looking in the NPM
|
||||
// registry for the latest stable release that will tell us about the current minor segment.
|
||||
// e.g. if the current major is `v10.0.0-next.0`, then we need to look for the latest release.
|
||||
// Let's say this is `v9.2.6`. Our patch branch will then be called `9.2.x`.
|
||||
const latestVersion = exec(`yarn -s info ${npmPackageName} dist-tags.latest`).trim();
|
||||
if (!latestVersion) {
|
||||
throw Error('Could not determine version of latest release.');
|
||||
}
|
||||
const expectedMajor = major - 1;
|
||||
const parsedLatestVersion = semver.parse(latestVersion);
|
||||
if (parsedLatestVersion === null) {
|
||||
throw Error(`Could not parse latest version from NPM registry: ${latestVersion}`);
|
||||
} else if (parsedLatestVersion.major !== expectedMajor) {
|
||||
throw Error(
|
||||
`Expected latest release to have major version: v${expectedMajor}, ` +
|
||||
`but got: v${latestVersion}`);
|
||||
}
|
||||
|
||||
return {patch: `${expectedMajor}.${parsedLatestVersion.minor}.x`, minor: `${expectedMajor}.x`};
|
||||
}
|
@ -7,11 +7,12 @@
|
||||
*/
|
||||
|
||||
|
||||
import {getRepoBaseDir} from '../../utils/config';
|
||||
import {getConfig, getRepoBaseDir} from '../../utils/config';
|
||||
import {error, green, info, promptConfirm, red, yellow} from '../../utils/console';
|
||||
import {GithubApiRequestError} from '../../utils/git';
|
||||
import {GitClient} from '../../utils/git';
|
||||
import {GithubApiRequestError} from '../../utils/git/github';
|
||||
|
||||
import {loadAndValidateConfig, MergeConfigWithRemote} from './config';
|
||||
import {loadAndValidateConfig, MergeConfig, MergeConfigWithRemote} from './config';
|
||||
import {MergeResult, MergeStatus, PullRequestMergeTask} from './task';
|
||||
|
||||
/** URL to the Github page where personal access tokens can be generated. */
|
||||
@ -34,19 +35,7 @@ export const GITHUB_TOKEN_GENERATE_URL = `https://github.com/settings/tokens`;
|
||||
export async function mergePullRequest(
|
||||
prNumber: number, githubToken: string, projectRoot: string = getRepoBaseDir(),
|
||||
config?: MergeConfigWithRemote) {
|
||||
// If no explicit configuration has been specified, we load and validate
|
||||
// the configuration from the shared dev-infra configuration.
|
||||
if (config === undefined) {
|
||||
const {config: _config, errors} = loadAndValidateConfig();
|
||||
if (errors) {
|
||||
error(red('Invalid configuration:'));
|
||||
errors.forEach(desc => error(yellow(` - ${desc}`)));
|
||||
process.exit(1);
|
||||
}
|
||||
config = _config!;
|
||||
}
|
||||
|
||||
const api = new PullRequestMergeTask(projectRoot, config, githubToken);
|
||||
const api = await createPullRequestMergeTask(githubToken, projectRoot, config);
|
||||
|
||||
// Perform the merge. Force mode can be activated through a command line flag.
|
||||
// Alternatively, if the merge fails with non-fatal failures, the script
|
||||
@ -132,3 +121,33 @@ export async function mergePullRequest(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the pull request merge task from the given Github token, project root
|
||||
* and optional explicit configuration. An explicit configuration can be specified
|
||||
* when the merge script is used outside of a `ng-dev` configured repository.
|
||||
*/
|
||||
async function createPullRequestMergeTask(
|
||||
githubToken: string, projectRoot: string, explicitConfig?: MergeConfigWithRemote) {
|
||||
if (explicitConfig !== undefined) {
|
||||
const git = new GitClient(githubToken, {github: explicitConfig.remote}, projectRoot);
|
||||
return new PullRequestMergeTask(explicitConfig, git);
|
||||
}
|
||||
|
||||
const devInfraConfig = getConfig();
|
||||
const git = new GitClient(githubToken, devInfraConfig, projectRoot);
|
||||
const {config, errors} = await loadAndValidateConfig(devInfraConfig, git.github);
|
||||
|
||||
if (errors) {
|
||||
error(red('Invalid merge configuration:'));
|
||||
errors.forEach(desc => error(yellow(` - ${desc}`)));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Set the remote so that the merge tool has access to information about
|
||||
// the remote it intends to merge to.
|
||||
config!.remote = devInfraConfig.github;
|
||||
// We can cast this to a merge config with remote because we always set the
|
||||
// remote above.
|
||||
return new PullRequestMergeTask(config! as MergeConfigWithRemote, git);
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ import {GitClient} from '../../utils/git';
|
||||
|
||||
import {PullRequestFailure} from './failures';
|
||||
import {matchesPattern} from './string-pattern';
|
||||
import {getBranchesFromTargetLabel, getTargetLabelFromPullRequest} from './target-label';
|
||||
import {getBranchesFromTargetLabel, getTargetLabelFromPullRequest, InvalidTargetBranchError, InvalidTargetLabelError} from './target-label';
|
||||
import {PullRequestMergeTask} from './task';
|
||||
|
||||
/** Interface that describes a pull request. */
|
||||
@ -83,6 +83,20 @@ export async function loadAndValidatePullRequest(
|
||||
labels.some(name => matchesPattern(name, config.commitMessageFixupLabel));
|
||||
const hasCaretakerNote = !!config.caretakerNoteLabel &&
|
||||
labels.some(name => matchesPattern(name, config.caretakerNoteLabel!));
|
||||
let targetBranches: string[];
|
||||
|
||||
// If branches are determined for a given target label, capture errors that are
|
||||
// thrown as part of branch computation. This is expected because a merge configuration
|
||||
// can lazily compute branches for a target label and throw. e.g. if an invalid target
|
||||
// label is applied, we want to exit the script gracefully with an error message.
|
||||
try {
|
||||
targetBranches = await getBranchesFromTargetLabel(targetLabel, githubTargetBranch);
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidTargetBranchError || error instanceof InvalidTargetLabelError) {
|
||||
return new PullRequestFailure(error.failureMessage);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
url: prData.html_url,
|
||||
@ -92,8 +106,8 @@ export async function loadAndValidatePullRequest(
|
||||
githubTargetBranch,
|
||||
needsCommitMessageFixup,
|
||||
hasCaretakerNote,
|
||||
targetBranches,
|
||||
title: prData.title,
|
||||
targetBranches: getBranchesFromTargetLabel(targetLabel, githubTargetBranch),
|
||||
commitCount: prData.commits,
|
||||
};
|
||||
}
|
||||
|
@ -9,9 +9,25 @@
|
||||
import {MergeConfig, TargetLabel} from './config';
|
||||
import {matchesPattern} from './string-pattern';
|
||||
|
||||
/**
|
||||
* Unique error that can be thrown in the merge configuration if an
|
||||
* invalid branch is targeted.
|
||||
*/
|
||||
export class InvalidTargetBranchError {
|
||||
constructor(public failureMessage: string) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unique error that can be thrown in the merge configuration if an
|
||||
* invalid label has been applied to a pull request.
|
||||
*/
|
||||
export class InvalidTargetLabelError {
|
||||
constructor(public failureMessage: string) {}
|
||||
}
|
||||
|
||||
/** Gets the target label from the specified pull request labels. */
|
||||
export function getTargetLabelFromPullRequest(config: MergeConfig, labels: string[]): TargetLabel|
|
||||
null {
|
||||
export function getTargetLabelFromPullRequest(
|
||||
config: Pick<MergeConfig, 'labels'>, labels: string[]): TargetLabel|null {
|
||||
for (const label of labels) {
|
||||
const match = config.labels.find(({pattern}) => matchesPattern(label, pattern));
|
||||
if (match !== undefined) {
|
||||
@ -21,8 +37,14 @@ export function getTargetLabelFromPullRequest(config: MergeConfig, labels: strin
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Gets the branches from the specified target label. */
|
||||
export function getBranchesFromTargetLabel(
|
||||
label: TargetLabel, githubTargetBranch: string): string[] {
|
||||
return typeof label.branches === 'function' ? label.branches(githubTargetBranch) : label.branches;
|
||||
/**
|
||||
* Gets the branches from the specified target label.
|
||||
*
|
||||
* @throws {InvalidTargetLabelError} Invalid label has been applied to pull request.
|
||||
* @throws {InvalidTargetBranchError} Invalid Github target branch has been selected.
|
||||
*/
|
||||
export async function getBranchesFromTargetLabel(
|
||||
label: TargetLabel, githubTargetBranch: string): Promise<string[]> {
|
||||
return typeof label.branches === 'function' ? await label.branches(githubTargetBranch) :
|
||||
await label.branches;
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
||||
import {promptConfirm} from '../../utils/console';
|
||||
import {GitClient, GitCommandError} from '../../utils/git';
|
||||
|
||||
import {MergeConfigWithRemote} from './config';
|
||||
import {MergeConfig, MergeConfigWithRemote} from './config';
|
||||
import {PullRequestFailure} from './failures';
|
||||
import {getCaretakerNotePromptMessage} from './messages';
|
||||
import {isPullRequest, loadAndValidatePullRequest,} from './pull-request';
|
||||
@ -40,12 +40,7 @@ export interface MergeResult {
|
||||
* labels that have been resolved through the merge script configuration.
|
||||
*/
|
||||
export class PullRequestMergeTask {
|
||||
/** Git client that can be used to execute Git commands. */
|
||||
git = new GitClient(this._githubToken, {github: this.config.remote});
|
||||
|
||||
constructor(
|
||||
public projectRoot: string, public config: MergeConfigWithRemote,
|
||||
private _githubToken: string) {}
|
||||
constructor(public config: MergeConfigWithRemote, public git: GitClient) {}
|
||||
|
||||
/**
|
||||
* Merges the given pull request and pushes it upstream.
|
||||
|
@ -19,6 +19,7 @@
|
||||
"inquirer": "<from-root>",
|
||||
"minimatch": "<from-root>",
|
||||
"multimatch": "<from-root>",
|
||||
"node-fetch": "<from-root>",
|
||||
"node-uuid": "<from-root>",
|
||||
"semver": "<from-root>",
|
||||
"shelljs": "<from-root>",
|
||||
|
@ -6,13 +6,6 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
/****************************************************************************
|
||||
****************************************************************************
|
||||
** DO NOT IMPORT THE GithubClient DIRECTLY, INSTEAD IMPORT GitClient from **
|
||||
** ./index.ts and access the GithubClient via the `.github` member. **
|
||||
****************************************************************************
|
||||
****************************************************************************/
|
||||
|
||||
import {graphql} from '@octokit/graphql';
|
||||
import * as Octokit from '@octokit/rest';
|
||||
import {RequestParameters} from '@octokit/types';
|
||||
@ -28,10 +21,10 @@ export class GithubApiRequestError extends Error {
|
||||
/**
|
||||
* A Github client for interacting with the Github APIs.
|
||||
*
|
||||
* Additionally, provides convienience methods for actions which require multiple requests, or
|
||||
* Additionally, provides convenience methods for actions which require multiple requests, or
|
||||
* would provide value from memoized style responses.
|
||||
**/
|
||||
export class _GithubClient extends Octokit {
|
||||
export class GithubClient extends Octokit {
|
||||
/** The Github GraphQL (v4) API. */
|
||||
graqhql: GithubGraphqlClient;
|
||||
|
@ -11,10 +11,7 @@ import {spawnSync, SpawnSyncOptions, SpawnSyncReturns} from 'child_process';
|
||||
|
||||
import {getConfig, getRepoBaseDir, NgDevConfig} from '../config';
|
||||
import {info, yellow} from '../console';
|
||||
import {_GithubClient} from './_github';
|
||||
|
||||
// Re-export GithubApiRequestError
|
||||
export {GithubApiRequestError} from './_github';
|
||||
import {GithubClient} from './github';
|
||||
|
||||
/** Github response type extended to include the `x-oauth-scopes` headers presence. */
|
||||
type RateLimitResponseWithOAuthScopeHeader = Octokit.Response<Octokit.RateLimitGetResponse>&{
|
||||
@ -54,10 +51,8 @@ export class GitClient {
|
||||
`https://${this._githubToken}@github.com/${this.remoteConfig.owner}/${
|
||||
this.remoteConfig.name}.git`;
|
||||
/** Instance of the authenticated Github octokit API. */
|
||||
github = new _GithubClient(this._githubToken);
|
||||
github = new GithubClient(this._githubToken);
|
||||
|
||||
/** The file path of project's root directory. */
|
||||
private _projectRoot = getRepoBaseDir();
|
||||
/** The OAuth scopes available for the provided Github token. */
|
||||
private _oauthScopes: Promise<string[]>|null = null;
|
||||
/**
|
||||
@ -67,7 +62,8 @@ export class GitClient {
|
||||
private _githubTokenRegex: RegExp|null = null;
|
||||
|
||||
constructor(
|
||||
private _githubToken?: string, private _config: Pick<NgDevConfig, 'github'> = getConfig()) {
|
||||
private _githubToken?: string, private _config: Pick<NgDevConfig, 'github'> = getConfig(),
|
||||
private _projectRoot = getRepoBaseDir()) {
|
||||
// If a token has been specified (and is not empty), pass it to the Octokit API and
|
||||
// also create a regular expression that can be used for sanitizing Git command output
|
||||
// so that it does not print the token accidentally.
|
||||
|
@ -223,8 +223,23 @@
|
||||
"packages/core/src/render3/assert.ts",
|
||||
"packages/core/src/render3/interfaces/container.ts",
|
||||
"packages/core/src/render3/interfaces/node.ts",
|
||||
"packages/core/src/render3/interfaces/definition.ts",
|
||||
"packages/core/src/core.ts",
|
||||
"packages/core/src/render3/interfaces/view.ts",
|
||||
"packages/core/src/di/injector.ts",
|
||||
"packages/core/src/di/r3_injector.ts",
|
||||
"packages/core/src/render3/definition.ts",
|
||||
"packages/core/src/metadata/ng_module.ts"
|
||||
],
|
||||
[
|
||||
"packages/core/src/application_ref.ts",
|
||||
"packages/core/src/application_tokens.ts",
|
||||
"packages/core/src/linker/component_factory.ts",
|
||||
"packages/core/src/change_detection/change_detection.ts",
|
||||
"packages/core/src/change_detection/change_detector_ref.ts",
|
||||
"packages/core/src/render3/view_engine_compatibility.ts",
|
||||
"packages/core/src/render3/assert.ts",
|
||||
"packages/core/src/render3/interfaces/container.ts",
|
||||
"packages/core/src/render3/interfaces/node.ts",
|
||||
"packages/core/src/render3/interfaces/view.ts",
|
||||
"packages/core/src/metadata.ts",
|
||||
"packages/core/src/di.ts",
|
||||
"packages/core/src/di/index.ts",
|
||||
@ -247,25 +262,9 @@
|
||||
"packages/core/src/render3/assert.ts",
|
||||
"packages/core/src/render3/interfaces/container.ts",
|
||||
"packages/core/src/render3/interfaces/node.ts",
|
||||
"packages/core/src/render3/interfaces/definition.ts",
|
||||
"packages/core/src/render3/interfaces/view.ts",
|
||||
"packages/core/src/di/injector.ts",
|
||||
"packages/core/src/di/r3_injector.ts",
|
||||
"packages/core/src/render3/definition.ts",
|
||||
"packages/core/src/metadata/ng_module.ts"
|
||||
],
|
||||
[
|
||||
"packages/core/src/application_ref.ts",
|
||||
"packages/core/src/application_tokens.ts",
|
||||
"packages/core/src/linker/component_factory.ts",
|
||||
"packages/core/src/change_detection/change_detection.ts",
|
||||
"packages/core/src/change_detection/change_detector_ref.ts",
|
||||
"packages/core/src/render3/view_engine_compatibility.ts",
|
||||
"packages/core/src/render3/assert.ts",
|
||||
"packages/core/src/render3/interfaces/container.ts",
|
||||
"packages/core/src/render3/interfaces/node.ts",
|
||||
"packages/core/src/render3/interfaces/definition.ts",
|
||||
"packages/core/src/render3/interfaces/view.ts",
|
||||
"packages/core/src/core.ts",
|
||||
"packages/core/src/metadata.ts",
|
||||
"packages/core/src/di.ts",
|
||||
"packages/core/src/di/index.ts",
|
||||
@ -1766,27 +1765,25 @@
|
||||
[
|
||||
"packages/core/src/render3/interfaces/container.ts",
|
||||
"packages/core/src/render3/interfaces/node.ts",
|
||||
"packages/core/src/render3/interfaces/definition.ts",
|
||||
"packages/core/src/render3/interfaces/view.ts"
|
||||
],
|
||||
[
|
||||
"packages/core/src/render3/interfaces/definition.ts",
|
||||
"packages/core/src/render3/interfaces/node.ts"
|
||||
"packages/core/src/render3/interfaces/node.ts",
|
||||
"packages/core/src/render3/interfaces/view.ts"
|
||||
],
|
||||
[
|
||||
"packages/core/src/render3/interfaces/definition.ts",
|
||||
"packages/core/src/render3/interfaces/view.ts"
|
||||
],
|
||||
[
|
||||
"packages/core/src/render3/interfaces/definition.ts",
|
||||
"packages/core/src/render3/interfaces/view.ts",
|
||||
"packages/core/src/render3/interfaces/node.ts"
|
||||
"packages/core/src/render3/interfaces/node.ts",
|
||||
"packages/core/src/render3/interfaces/view.ts"
|
||||
],
|
||||
[
|
||||
"packages/core/src/render3/interfaces/definition.ts",
|
||||
"packages/core/src/render3/interfaces/node.ts",
|
||||
"packages/core/src/render3/interfaces/view.ts",
|
||||
"packages/core/src/render3/interfaces/query.ts",
|
||||
"packages/core/src/render3/interfaces/node.ts"
|
||||
"packages/core/src/render3/interfaces/query.ts"
|
||||
],
|
||||
[
|
||||
"packages/core/src/render3/interfaces/query.ts",
|
||||
@ -1858,11 +1855,6 @@
|
||||
"packages/forms/src/directives/ng_model.ts",
|
||||
"packages/forms/src/directives/ng_model_group.ts"
|
||||
],
|
||||
[
|
||||
"packages/forms/src/directives/normalize_validator.ts",
|
||||
"packages/forms/src/model.ts",
|
||||
"packages/forms/src/directives/shared.ts"
|
||||
],
|
||||
[
|
||||
"packages/forms/src/directives/reactive_directives/form_control_directive.ts",
|
||||
"packages/forms/src/directives/shared.ts",
|
||||
|
@ -21,9 +21,9 @@
|
||||
"master": {
|
||||
"uncompressed": {
|
||||
"runtime-es2015": 3097,
|
||||
"main-es2015": 429885,
|
||||
"main-es2015": 430239,
|
||||
"polyfills-es2015": 52195
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -49,9 +49,9 @@
|
||||
"master": {
|
||||
"uncompressed": {
|
||||
"runtime-es2015": 2289,
|
||||
"main-es2015": 221897,
|
||||
"polyfills-es2015": 36938,
|
||||
"5-es2015": 779
|
||||
"main-es2015": 221939,
|
||||
"polyfills-es2015": 36723,
|
||||
"5-es2015": 781
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -62,8 +62,8 @@
|
||||
"bundle": "TODO(i): we should define ngDevMode to false in Closure, but --define only works in the global scope.",
|
||||
"bundle": "TODO(i): (FW-2164) TS 3.9 new class shape seems to have broken Closure in big ways. The size went from 169991 to 252338",
|
||||
"bundle": "TODO(i): after removal of tsickle from ngc-wrapped / ng_package, we had to switch to SIMPLE optimizations which increased the size from 252338 to 1198917, see PR#37221 and PR#37317 for more info",
|
||||
"bundle": 1209659
|
||||
"bundle": 1213130
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -82,6 +82,7 @@
|
||||
"@types/jasminewd2": "^2.0.8",
|
||||
"@types/minimist": "^1.2.0",
|
||||
"@types/node": "^12.11.1",
|
||||
"@types/node-fetch": "^2.5.7",
|
||||
"@types/selenium-webdriver": "3.0.7",
|
||||
"@types/semver": "^6.0.2",
|
||||
"@types/shelljs": "^0.8.6",
|
||||
@ -127,6 +128,7 @@
|
||||
"materialize-css": "1.0.0",
|
||||
"minimatch": "^3.0.4",
|
||||
"minimist": "1.2.0",
|
||||
"node-fetch": "^2.6.0",
|
||||
"node-uuid": "1.4.8",
|
||||
"nodejs-websocket": "^1.7.2",
|
||||
"protractor": "^5.4.2",
|
||||
@ -185,6 +187,7 @@
|
||||
"madge": "^3.6.0",
|
||||
"multimatch": "^4.0.0",
|
||||
"mutation-observer": "^1.0.3",
|
||||
"nock": "^13.0.3",
|
||||
"rewire": "2.5.2",
|
||||
"sauce-connect": "https://saucelabs.com/downloads/sc-4.5.1-linux.tar.gz",
|
||||
"semver": "^6.3.0",
|
||||
|
@ -111,26 +111,10 @@ export class BrowserViewportScroller implements ViewportScroller {
|
||||
*/
|
||||
scrollToAnchor(anchor: string): void {
|
||||
if (this.supportScrollRestoration()) {
|
||||
// Escape anything passed to `querySelector` as it can throw errors and stop the application
|
||||
// from working if invalid values are passed.
|
||||
if (this.window.CSS && this.window.CSS.escape) {
|
||||
anchor = this.window.CSS.escape(anchor);
|
||||
} else {
|
||||
anchor = anchor.replace(/(\"|\'\ |:|\.|\[|\]|,|=)/g, '\\$1');
|
||||
}
|
||||
try {
|
||||
const elSelectedById = this.document.querySelector(`#${anchor}`);
|
||||
if (elSelectedById) {
|
||||
this.scrollToElement(elSelectedById);
|
||||
return;
|
||||
}
|
||||
const elSelectedByName = this.document.querySelector(`[name='${anchor}']`);
|
||||
if (elSelectedByName) {
|
||||
this.scrollToElement(elSelectedByName);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
this.errorHandler.handleError(e);
|
||||
const elSelected =
|
||||
this.document.getElementById(anchor) || this.document.getElementsByName(anchor)[0];
|
||||
if (elSelected) {
|
||||
this.scrollToElement(elSelected);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -165,13 +149,25 @@ export class BrowserViewportScroller implements ViewportScroller {
|
||||
*/
|
||||
private supportScrollRestoration(): boolean {
|
||||
try {
|
||||
return !!this.window && !!this.window.scrollTo;
|
||||
if (!this.window || !this.window.scrollTo) {
|
||||
return false;
|
||||
}
|
||||
// The `scrollRestoration` property could be on the `history` instance or its prototype.
|
||||
const scrollRestorationDescriptor = getScrollRestorationProperty(this.window.history) ||
|
||||
getScrollRestorationProperty(Object.getPrototypeOf(this.window.history));
|
||||
// We can write to the `scrollRestoration` property if it is a writable data field or it has a
|
||||
// setter function.
|
||||
return !!scrollRestorationDescriptor &&
|
||||
!!(scrollRestorationDescriptor.writable || scrollRestorationDescriptor.set);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getScrollRestorationProperty(obj: any): PropertyDescriptor|undefined {
|
||||
return Object.getOwnPropertyDescriptor(obj, 'scrollRestoration');
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides an empty implementation of the viewport scroller. This will
|
||||
|
@ -6,35 +6,61 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @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 {describe, expect, it} from '@angular/core/testing/src/testing_internal';
|
||||
import {BrowserViewportScroller, ViewportScroller} from '../src/viewport_scroller';
|
||||
|
||||
{
|
||||
describe('BrowserViewportScroller', () => {
|
||||
let scroller: ViewportScroller;
|
||||
let documentSpy: any;
|
||||
beforeEach(() => {
|
||||
documentSpy = jasmine.createSpyObj('document', ['querySelector']);
|
||||
scroller = new BrowserViewportScroller(documentSpy, {scrollTo: 1}, null!);
|
||||
});
|
||||
it('escapes invalid characters selectors', () => {
|
||||
const invalidSelectorChars = `"' :.[],=`;
|
||||
// Double escaped to make sure we match the actual value passed to `querySelector`
|
||||
const escapedInvalids = `\\"\\' \\:\\.\\[\\]\\,\\=`;
|
||||
scroller.scrollToAnchor(`specials=${invalidSelectorChars}`);
|
||||
expect(documentSpy.querySelector).toHaveBeenCalledWith(`#specials\\=${escapedInvalids}`);
|
||||
expect(documentSpy.querySelector)
|
||||
.toHaveBeenCalledWith(`[name='specials\\=${escapedInvalids}']`);
|
||||
describe('BrowserViewportScroller', () => {
|
||||
let scroller: ViewportScroller;
|
||||
let documentSpy: any;
|
||||
let windowSpy: any;
|
||||
|
||||
beforeEach(() => {
|
||||
windowSpy = jasmine.createSpyObj('window', ['history']);
|
||||
windowSpy.scrollTo = 1;
|
||||
windowSpy.history.scrollRestoration = 'auto';
|
||||
documentSpy = jasmine.createSpyObj('document', ['getElementById', 'getElementsByName']);
|
||||
scroller = new BrowserViewportScroller(documentSpy, windowSpy, null!);
|
||||
});
|
||||
|
||||
describe('setHistoryScrollRestoration', () => {
|
||||
it('should not crash when scrollRestoration is not writable', () => {
|
||||
Object.defineProperty(windowSpy.history, 'scrollRestoration', {
|
||||
value: 'auto',
|
||||
configurable: true,
|
||||
});
|
||||
expect(() => scroller.setHistoryScrollRestoration('manual')).not.toThrow();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe('scrollToAnchor', () => {
|
||||
const anchor = 'anchor';
|
||||
const el = document.createElement('a');
|
||||
|
||||
it('should only call getElementById when an element is found by id', () => {
|
||||
documentSpy.getElementById.and.returnValue(el);
|
||||
spyOn<any>(scroller, 'scrollToElement');
|
||||
scroller.scrollToAnchor(anchor);
|
||||
expect(documentSpy.getElementById).toHaveBeenCalledWith(anchor);
|
||||
expect(documentSpy.getElementsByName).not.toHaveBeenCalled();
|
||||
expect((scroller as any).scrollToElement).toHaveBeenCalledWith(el);
|
||||
});
|
||||
|
||||
it('should call getElementById and getElementsByName when an element is found by name', () => {
|
||||
documentSpy.getElementsByName.and.returnValue([el]);
|
||||
spyOn<any>(scroller, 'scrollToElement');
|
||||
scroller.scrollToAnchor(anchor);
|
||||
expect(documentSpy.getElementById).toHaveBeenCalledWith(anchor);
|
||||
expect(documentSpy.getElementsByName).toHaveBeenCalledWith(anchor);
|
||||
expect((scroller as any).scrollToElement).toHaveBeenCalledWith(el);
|
||||
});
|
||||
|
||||
it('should not call scrollToElement when an element is not found by its id or its name', () => {
|
||||
documentSpy.getElementsByName.and.returnValue([]);
|
||||
spyOn<any>(scroller, 'scrollToElement');
|
||||
scroller.scrollToAnchor(anchor);
|
||||
expect(documentSpy.getElementById).toHaveBeenCalledWith(anchor);
|
||||
expect(documentSpy.getElementsByName).toHaveBeenCalledWith(anchor);
|
||||
expect((scroller as any).scrollToElement).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -307,7 +307,10 @@ export class NgModuleDecoratorHandler implements
|
||||
});
|
||||
|
||||
if (this.factoryTracker !== null) {
|
||||
this.factoryTracker.track(node.getSourceFile(), analysis.factorySymbolName);
|
||||
this.factoryTracker.track(node.getSourceFile(), {
|
||||
name: analysis.factorySymbolName,
|
||||
hasId: analysis.id !== null,
|
||||
});
|
||||
}
|
||||
|
||||
this.injectableRegistry.registerInjectable(node);
|
||||
|
@ -65,6 +65,7 @@ const CORE_SUPPORTED_SYMBOLS = new Map<string, string>([
|
||||
['ɵɵInjectorDef', 'ɵɵInjectorDef'],
|
||||
['ɵɵNgModuleDefWithMeta', 'ɵɵNgModuleDefWithMeta'],
|
||||
['ɵNgModuleFactory', 'NgModuleFactory'],
|
||||
['ɵnoSideEffects', 'ɵnoSideEffects'],
|
||||
]);
|
||||
|
||||
const CORE_MODULE = '@angular/core';
|
||||
|
@ -61,10 +61,15 @@ export interface PerFileShimGenerator {
|
||||
export interface FactoryTracker {
|
||||
readonly sourceInfo: Map<string, FactoryInfo>;
|
||||
|
||||
track(sf: ts.SourceFile, factorySymbolName: string): void;
|
||||
track(sf: ts.SourceFile, moduleInfo: ModuleInfo): void;
|
||||
}
|
||||
|
||||
export interface FactoryInfo {
|
||||
sourceFilePath: string;
|
||||
moduleSymbolNames: Set<string>;
|
||||
}
|
||||
moduleSymbols: Map<string, ModuleInfo>;
|
||||
}
|
||||
|
||||
export interface ModuleInfo {
|
||||
name: string;
|
||||
hasId: boolean;
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import * as ts from 'typescript';
|
||||
|
||||
import {absoluteFromSourceFile, AbsoluteFsPath, basename} from '../../file_system';
|
||||
import {ImportRewriter} from '../../imports';
|
||||
import {FactoryInfo, FactoryTracker, PerFileShimGenerator} from '../api';
|
||||
import {FactoryInfo, FactoryTracker, ModuleInfo, PerFileShimGenerator} from '../api';
|
||||
|
||||
import {generatedModuleName} from './util';
|
||||
|
||||
@ -22,7 +22,7 @@ const STRIP_NG_FACTORY = /(.*)NgFactory$/;
|
||||
*/
|
||||
export class FactoryGenerator implements PerFileShimGenerator, FactoryTracker {
|
||||
readonly sourceInfo = new Map<string, FactoryInfo>();
|
||||
private sourceToFactorySymbols = new Map<string, Set<string>>();
|
||||
private sourceToFactorySymbols = new Map<string, Map<string, ModuleInfo>>();
|
||||
|
||||
readonly shouldEmit = true;
|
||||
readonly extensionPrefix = 'ngfactory';
|
||||
@ -85,16 +85,19 @@ export class FactoryGenerator implements PerFileShimGenerator, FactoryTracker {
|
||||
genFile.moduleName = generatedModuleName(sf.moduleName, sf.fileName, '.ngfactory');
|
||||
}
|
||||
|
||||
const moduleSymbolNames = new Set<string>();
|
||||
this.sourceToFactorySymbols.set(absoluteSfPath, moduleSymbolNames);
|
||||
this.sourceInfo.set(genFilePath, {sourceFilePath: absoluteSfPath, moduleSymbolNames});
|
||||
const moduleSymbols = new Map<string, ModuleInfo>();
|
||||
this.sourceToFactorySymbols.set(absoluteSfPath, moduleSymbols);
|
||||
this.sourceInfo.set(genFilePath, {
|
||||
sourceFilePath: absoluteSfPath,
|
||||
moduleSymbols,
|
||||
});
|
||||
|
||||
return genFile;
|
||||
}
|
||||
|
||||
track(sf: ts.SourceFile, factorySymbolName: string): void {
|
||||
track(sf: ts.SourceFile, moduleInfo: ModuleInfo): void {
|
||||
if (this.sourceToFactorySymbols.has(sf.fileName)) {
|
||||
this.sourceToFactorySymbols.get(sf.fileName)!.add(factorySymbolName);
|
||||
this.sourceToFactorySymbols.get(sf.fileName)!.set(moduleInfo.name, moduleInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -123,7 +126,7 @@ function transformFactorySourceFile(
|
||||
return file;
|
||||
}
|
||||
|
||||
const {moduleSymbolNames, sourceFilePath} = factoryMap.get(file.fileName)!;
|
||||
const {moduleSymbols, sourceFilePath} = factoryMap.get(file.fileName)!;
|
||||
|
||||
file = ts.getMutableClone(file);
|
||||
|
||||
@ -183,8 +186,24 @@ function transformFactorySourceFile(
|
||||
|
||||
// Otherwise, check if this export is a factory for a known NgModule, and retain it if so.
|
||||
const match = STRIP_NG_FACTORY.exec(decl.name.text);
|
||||
if (match !== null && moduleSymbolNames.has(match[1])) {
|
||||
transformedStatements.push(stmt);
|
||||
const module = match ? moduleSymbols.get(match[1]) : null;
|
||||
if (module) {
|
||||
// If the module can be tree shaken, then the factory should be wrapped in a
|
||||
// `noSideEffects()` call which tells Closure to treat the expression as pure, allowing
|
||||
// it to be removed if the result is not used.
|
||||
//
|
||||
// `NgModule`s with an `id` property will be lazy loaded. Google-internal lazy loading
|
||||
// infra relies on a side effect from the `new NgModuleFactory()` call, which registers
|
||||
// the module globally. Because of this, we **cannot** tree shake any module which has
|
||||
// an `id` property. Doing so would cause lazy loaded modules to never be registered.
|
||||
const moduleIsTreeShakable = !module.hasId;
|
||||
const newStmt = !moduleIsTreeShakable ?
|
||||
stmt :
|
||||
updateInitializers(
|
||||
stmt,
|
||||
(init) => init ? wrapInNoSideEffects(init) : undefined,
|
||||
);
|
||||
transformedStatements.push(newStmt);
|
||||
}
|
||||
} else {
|
||||
// Leave the statement alone, as it can't be understood.
|
||||
@ -263,3 +282,62 @@ function getFileoverviewComment(sourceFile: ts.SourceFile): string|null {
|
||||
|
||||
return commentText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the given expression in a call to `ɵnoSideEffects()`, which tells
|
||||
* Closure we don't care about the side effects of this expression and it should
|
||||
* be treated as "pure". Closure is free to tree shake this expression if its
|
||||
* result is not used.
|
||||
*
|
||||
* Example: Takes `1 + 2` and returns `i0.ɵnoSideEffects(() => 1 + 2)`.
|
||||
*/
|
||||
function wrapInNoSideEffects(expr: ts.Expression): ts.Expression {
|
||||
const noSideEffects = ts.createPropertyAccess(
|
||||
ts.createIdentifier('i0'),
|
||||
'ɵnoSideEffects',
|
||||
);
|
||||
|
||||
return ts.createCall(
|
||||
noSideEffects,
|
||||
/* typeArguments */[],
|
||||
/* arguments */
|
||||
[
|
||||
ts.createFunctionExpression(
|
||||
/* modifiers */[],
|
||||
/* asteriskToken */ undefined,
|
||||
/* name */ undefined,
|
||||
/* typeParameters */[],
|
||||
/* parameters */[],
|
||||
/* type */ undefined,
|
||||
/* body */ ts.createBlock([
|
||||
ts.createReturn(expr),
|
||||
]),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clones and updates the initializers for a given statement to use the new
|
||||
* expression provided. Does not mutate the input statement.
|
||||
*/
|
||||
function updateInitializers(
|
||||
stmt: ts.VariableStatement,
|
||||
update: (initializer?: ts.Expression) => ts.Expression | undefined,
|
||||
): ts.VariableStatement {
|
||||
return ts.updateVariableStatement(
|
||||
stmt,
|
||||
stmt.modifiers,
|
||||
ts.updateVariableDeclarationList(
|
||||
stmt.declarationList,
|
||||
stmt.declarationList.declarations.map(
|
||||
(decl) => ts.updateVariableDeclaration(
|
||||
decl,
|
||||
decl.name,
|
||||
decl.type,
|
||||
update(decl.initializer),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -79,7 +79,7 @@ export class NgTscPlugin implements TscPlugin {
|
||||
}
|
||||
|
||||
wrapHost(
|
||||
host: ts.CompilerHost&UnifiedModulesHost, inputFiles: readonly string[],
|
||||
host: ts.CompilerHost&Partial<UnifiedModulesHost>, inputFiles: readonly string[],
|
||||
options: ts.CompilerOptions): PluginCompilerHost {
|
||||
// TODO(alxhub): Eventually the `wrapHost()` API will accept the old `ts.Program` (if one is
|
||||
// available). When it does, its `ts.SourceFile`s need to be re-tagged to enable proper
|
||||
|
@ -166,8 +166,8 @@ function constructTypeCtorParameter(
|
||||
if (coercedKeys.length > 0) {
|
||||
const coercedLiteral = ts.createTypeLiteralNode(coercedKeys);
|
||||
|
||||
initType =
|
||||
initType !== null ? ts.createUnionTypeNode([initType, coercedLiteral]) : coercedLiteral;
|
||||
initType = initType !== null ? ts.createIntersectionTypeNode([initType, coercedLiteral]) :
|
||||
coercedLiteral;
|
||||
}
|
||||
|
||||
if (initType === null) {
|
||||
|
@ -179,7 +179,7 @@ TestClass.ngTypeCtor({value: 'test'});
|
||||
const typeCtor = TestClassWithCtor.members.find(isTypeCtor)!;
|
||||
const ctorText = typeCtor.getText().replace(/[ \r\n]+/g, ' ');
|
||||
expect(ctorText).toContain(
|
||||
'init: Pick<TestClass, "foo"> | { bar: typeof TestClass.ngAcceptInputType_bar; }');
|
||||
'init: Pick<TestClass, "foo"> & { bar: typeof TestClass.ngAcceptInputType_bar; }');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -3538,7 +3538,9 @@ runInEachFileSystem(os => {
|
||||
expect(factoryContents).toContain(`import * as i0 from '@angular/core';`);
|
||||
expect(factoryContents).toContain(`import { NotAModule, TestModule } from './test';`);
|
||||
expect(factoryContents)
|
||||
.toContain(`export var TestModuleNgFactory = new i0.\u0275NgModuleFactory(TestModule);`);
|
||||
.toContain(
|
||||
'export var TestModuleNgFactory = i0.\u0275noSideEffects(function () { ' +
|
||||
'return new i0.\u0275NgModuleFactory(TestModule); });');
|
||||
expect(factoryContents).not.toContain(`NotAModuleNgFactory`);
|
||||
expect(factoryContents).not.toContain('\u0275NonEmptyModule');
|
||||
|
||||
@ -3677,11 +3679,32 @@ runInEachFileSystem(os => {
|
||||
env.driveMain();
|
||||
|
||||
const factoryContents = env.getContents('test.ngfactory.js');
|
||||
expect(normalize(factoryContents)).toBe(normalize(`
|
||||
import * as i0 from "./r3_symbols";
|
||||
import { TestModule } from './test';
|
||||
export var TestModuleNgFactory = new i0.NgModuleFactory(TestModule);
|
||||
`));
|
||||
expect(factoryContents)
|
||||
.toBe(
|
||||
'import * as i0 from "./r3_symbols";\n' +
|
||||
'import { TestModule } from \'./test\';\n' +
|
||||
'export var TestModuleNgFactory = i0.\u0275noSideEffects(function () {' +
|
||||
' return new i0.NgModuleFactory(TestModule); });\n');
|
||||
});
|
||||
|
||||
it('should generate side effectful NgModuleFactory constructor when lazy loaded', () => {
|
||||
env.tsconfig({'allowEmptyCodegenFiles': true});
|
||||
|
||||
env.write('test.ts', `
|
||||
import {NgModule} from '@angular/core';
|
||||
|
||||
@NgModule({
|
||||
id: 'test', // ID to use for lazy loading.
|
||||
})
|
||||
export class TestModule {}
|
||||
`);
|
||||
|
||||
env.driveMain();
|
||||
|
||||
// Should **not** contain noSideEffects(), because the module is lazy loaded.
|
||||
const factoryContents = env.getContents('test.ngfactory.js');
|
||||
expect(factoryContents)
|
||||
.toContain('export var TestModuleNgFactory = new i0.ɵNgModuleFactory(TestModule);');
|
||||
});
|
||||
|
||||
describe('file-level comments', () => {
|
||||
|
@ -1498,6 +1498,39 @@ export declare class AnimationEvent {
|
||||
expect(diags[0].messageText)
|
||||
.toBe(`Type 'boolean' is not assignable to type 'string | number'.`);
|
||||
});
|
||||
|
||||
it('should give an error for undefined bindings into regular inputs when coercion members are present',
|
||||
() => {
|
||||
env.tsconfig({strictTemplates: true});
|
||||
env.write('test.ts', `
|
||||
import {Component, Directive, NgModule, Input} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'blah',
|
||||
template: '<input dir [regular]="undefined" [coerced]="1">',
|
||||
})
|
||||
export class FooCmp {
|
||||
invalidType = true;
|
||||
}
|
||||
|
||||
@Directive({selector: '[dir]'})
|
||||
export class CoercionDir {
|
||||
@Input() regular: string;
|
||||
@Input() coerced: boolean;
|
||||
|
||||
static ngAcceptInputType_coerced: boolean|number;
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [FooCmp, CoercionDir],
|
||||
})
|
||||
export class FooModule {}
|
||||
`);
|
||||
const diags = env.driveDiagnostics();
|
||||
expect(diags.length).toBe(1);
|
||||
expect(diags[0].messageText)
|
||||
.toBe(`Type 'undefined' is not assignable to type 'string'.`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('legacy schema checking with the DOM schema', () => {
|
||||
|
@ -292,5 +292,8 @@ export {
|
||||
ɵɵsanitizeUrl,
|
||||
ɵɵsanitizeUrlOrResourceUrl,
|
||||
} from './sanitization/sanitization';
|
||||
export {
|
||||
noSideEffects as ɵnoSideEffects,
|
||||
} from './util/closure';
|
||||
|
||||
// clang-format on
|
||||
|
@ -332,7 +332,7 @@ export interface ViewChildDecorator {
|
||||
* * A template reference variable as a string (e.g. query `<my-component #cmp></my-component>`
|
||||
* with `@ViewChild('cmp')`)
|
||||
* * Any provider defined in the child component tree of the current component (e.g.
|
||||
* `@ViewChild(SomeService) someService: SomeService`)
|
||||
* `@ViewChild(SomeComponent) someComponent: SomeComponent`)
|
||||
* * Any provider defined through a string token (e.g. `@ViewChild('someToken') someTokenVal:
|
||||
* any`)
|
||||
* * A `TemplateRef` (e.g. query `<ng-template></ng-template>` with `@ViewChild(TemplateRef)
|
||||
|
@ -28,6 +28,7 @@ export {ɵɵdefineNgModule} from './render3/definition';
|
||||
export {ɵɵFactoryDef} from './render3/interfaces/definition';
|
||||
export {setClassMetadata} from './render3/metadata';
|
||||
export {NgModuleFactory} from './render3/ng_module_ref';
|
||||
export {noSideEffects as ɵnoSideEffects} from './util/closure';
|
||||
|
||||
|
||||
|
||||
|
@ -6,6 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import '../util/ng_i18n_closure_mode';
|
||||
import '../util/ng_dev_mode';
|
||||
|
||||
import {DEFAULT_LOCALE_ID, getPluralCase} from '../i18n/localization';
|
||||
import {getTemplateContent, SRCSET_ATTRS, URI_ATTRS, VALID_ATTRS, VALID_ELEMENTS} from '../sanitization/html_sanitizer';
|
||||
@ -16,8 +17,8 @@ import {assertDataInRange, assertDefined, assertEqual} from '../util/assert';
|
||||
|
||||
import {bindingUpdated} from './bindings';
|
||||
import {attachPatchData} from './context_discovery';
|
||||
import {i18nMutateOpCodesToString, i18nUpdateOpCodesToString} from './i18n_debug';
|
||||
import {setDelayProjection} from './instructions/all';
|
||||
import {attachI18nOpCodesDebug} from './instructions/lview_debug';
|
||||
import {allocExpando, elementAttributeInternal, elementPropertyInternal, getOrCreateTNode, setInputsForProperty, setNgReflectProperties, textBindingInternal} from './instructions/shared';
|
||||
import {LContainer, NATIVE} from './interfaces/container';
|
||||
import {getDocument} from './interfaces/document';
|
||||
@ -29,6 +30,7 @@ import {isLContainer} from './interfaces/type_checks';
|
||||
import {HEADER_OFFSET, LView, RENDERER, T_HOST, TVIEW, TView} from './interfaces/view';
|
||||
import {appendChild, applyProjection, createTextNode, nativeRemoveNode} from './node_manipulation';
|
||||
import {getBindingIndex, getIsParent, getLView, getPreviousOrParentTNode, getTView, nextBindingIndex, setIsNotParent, setPreviousOrParentTNode} from './state';
|
||||
import {attachDebugGetter} from './util/debug_utils';
|
||||
import {renderStringify} from './util/misc_utils';
|
||||
import {getNativeByIndex, getNativeByTNode, getTNode, load} from './util/view_utils';
|
||||
|
||||
@ -267,6 +269,9 @@ function generateBindingUpdateOpCodes(
|
||||
str: string, destinationNode: number, attrName?: string,
|
||||
sanitizeFn: SanitizerFn|null = null): I18nUpdateOpCodes {
|
||||
const updateOpCodes: I18nUpdateOpCodes = [null, null]; // Alloc space for mask and size
|
||||
if (ngDevMode) {
|
||||
attachDebugGetter(updateOpCodes, i18nUpdateOpCodesToString);
|
||||
}
|
||||
const textParts = str.split(BINDING_REGEXP);
|
||||
let mask = 0;
|
||||
|
||||
@ -395,6 +400,9 @@ function i18nStartFirstPass(
|
||||
let parentIndexPointer = 0;
|
||||
parentIndexStack[parentIndexPointer] = parentIndex;
|
||||
const createOpCodes: I18nMutateOpCodes = [];
|
||||
if (ngDevMode) {
|
||||
attachDebugGetter(createOpCodes, i18nMutateOpCodesToString);
|
||||
}
|
||||
// If the previous node wasn't the direct parent then we have a translation without top level
|
||||
// element and we need to keep a reference of the previous element if there is one. We should also
|
||||
// keep track whether an element was a parent node or not, so that the logic that consumes
|
||||
@ -411,6 +419,9 @@ function i18nStartFirstPass(
|
||||
createOpCodes.push(previousTNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select);
|
||||
}
|
||||
const updateOpCodes: I18nUpdateOpCodes = [];
|
||||
if (ngDevMode) {
|
||||
attachDebugGetter(updateOpCodes, i18nUpdateOpCodesToString);
|
||||
}
|
||||
const icuExpressions: TIcu[] = [];
|
||||
|
||||
if (message === '' && isRootTemplateMessage(subTemplateIndex)) {
|
||||
@ -507,10 +518,6 @@ function i18nStartFirstPass(
|
||||
allocExpando(tView, lView, i18nVarsCount);
|
||||
}
|
||||
|
||||
ngDevMode &&
|
||||
attachI18nOpCodesDebug(
|
||||
createOpCodes, updateOpCodes, icuExpressions.length ? icuExpressions : null, lView);
|
||||
|
||||
// NOTE: local var needed to properly assert the type of `TI18n`.
|
||||
const tI18n: TI18n = {
|
||||
vars: i18nVarsCount,
|
||||
@ -751,6 +758,7 @@ function createDynamicNodeAtIndex(
|
||||
const previousOrParentTNode = getPreviousOrParentTNode();
|
||||
ngDevMode && assertDataInRange(lView, index + HEADER_OFFSET);
|
||||
lView[index + HEADER_OFFSET] = native;
|
||||
// FIXME(misko): Why does this create A TNode??? I would not expect this to be here.
|
||||
const tNode = getOrCreateTNode(tView, lView[T_HOST], index, type as any, name, null);
|
||||
|
||||
// We are creating a dynamic node, the previous tNode might not be pointing at this node.
|
||||
@ -780,7 +788,7 @@ function readCreateOpCodes(
|
||||
visitedNodes.push(textNodeIndex);
|
||||
setIsNotParent();
|
||||
} else if (typeof opCode == 'number') {
|
||||
switch (opCode & I18nMutateOpCode.MASK_OPCODE) {
|
||||
switch (opCode & I18nMutateOpCode.MASK_INSTRUCTION) {
|
||||
case I18nMutateOpCode.AppendChild:
|
||||
const destinationNodeIndex = opCode >>> I18nMutateOpCode.SHIFT_PARENT;
|
||||
let destinationTNode: TNode;
|
||||
@ -799,9 +807,10 @@ function readCreateOpCodes(
|
||||
appendI18nNode(tView, currentTNode!, destinationTNode, previousTNode, lView);
|
||||
break;
|
||||
case I18nMutateOpCode.Select:
|
||||
// Negative indicies indicate that a given TNode is a sibling node, not a parent node
|
||||
// Negative indices indicate that a given TNode is a sibling node, not a parent node
|
||||
// (see `i18nStartFirstPass` for additional information).
|
||||
const isParent = opCode >= 0;
|
||||
// FIXME(misko): This SHIFT_REF looks suspect as it does not have mask.
|
||||
const nodeIndex = (isParent ? opCode : ~opCode) >>> I18nMutateOpCode.SHIFT_REF;
|
||||
visitedNodes.push(nodeIndex);
|
||||
previousTNode = currentTNode;
|
||||
@ -874,7 +883,7 @@ function readCreateOpCodes(
|
||||
|
||||
function readUpdateOpCodes(
|
||||
updateOpCodes: I18nUpdateOpCodes, icus: TIcu[]|null, bindingsStartIndex: number,
|
||||
changeMask: number, tView: TView, lView: LView, bypassCheckBit = false) {
|
||||
changeMask: number, tView: TView, lView: LView, bypassCheckBit: boolean) {
|
||||
let caseCreated = false;
|
||||
for (let i = 0; i < updateOpCodes.length; i++) {
|
||||
// bit code to check if we should apply the next update
|
||||
@ -890,13 +899,10 @@ function readUpdateOpCodes(
|
||||
value += opCode;
|
||||
} else if (typeof opCode == 'number') {
|
||||
if (opCode < 0) {
|
||||
// It's a binding index whose value is negative
|
||||
// Negative opCode represent `i18nExp` values offset.
|
||||
value += renderStringify(lView[bindingsStartIndex - opCode]);
|
||||
} else {
|
||||
const nodeIndex = opCode >>> I18nUpdateOpCode.SHIFT_REF;
|
||||
let tIcuIndex: number;
|
||||
let tIcu: TIcu;
|
||||
let icuTNode: TIcuContainerNode;
|
||||
switch (opCode & I18nUpdateOpCode.MASK_OPCODE) {
|
||||
case I18nUpdateOpCode.Attr:
|
||||
const propName = updateOpCodes[++j] as string;
|
||||
@ -909,56 +915,13 @@ function readUpdateOpCodes(
|
||||
textBindingInternal(lView, nodeIndex, value);
|
||||
break;
|
||||
case I18nUpdateOpCode.IcuSwitch:
|
||||
tIcuIndex = updateOpCodes[++j] as number;
|
||||
tIcu = icus![tIcuIndex];
|
||||
icuTNode = getTNode(tView, nodeIndex) as TIcuContainerNode;
|
||||
// If there is an active case, delete the old nodes
|
||||
if (icuTNode.activeCaseIndex !== null) {
|
||||
const removeCodes = tIcu.remove[icuTNode.activeCaseIndex];
|
||||
for (let k = 0; k < removeCodes.length; k++) {
|
||||
const removeOpCode = removeCodes[k] as number;
|
||||
switch (removeOpCode & I18nMutateOpCode.MASK_OPCODE) {
|
||||
case I18nMutateOpCode.Remove:
|
||||
const nodeIndex = removeOpCode >>> I18nMutateOpCode.SHIFT_REF;
|
||||
// Remove DOM element, but do *not* mark TNode as detached, since we are
|
||||
// just switching ICU cases (while keeping the same TNode), so a DOM element
|
||||
// representing a new ICU case will be re-created.
|
||||
removeNode(tView, lView, nodeIndex, /* markAsDetached */ false);
|
||||
break;
|
||||
case I18nMutateOpCode.RemoveNestedIcu:
|
||||
const nestedIcuNodeIndex =
|
||||
removeCodes[k + 1] as number >>> I18nMutateOpCode.SHIFT_REF;
|
||||
const nestedIcuTNode =
|
||||
getTNode(tView, nestedIcuNodeIndex) as TIcuContainerNode;
|
||||
const activeIndex = nestedIcuTNode.activeCaseIndex;
|
||||
if (activeIndex !== null) {
|
||||
const nestedIcuTIndex = removeOpCode >>> I18nMutateOpCode.SHIFT_REF;
|
||||
const nestedTIcu = icus![nestedIcuTIndex];
|
||||
addAllToArray(nestedTIcu.remove[activeIndex], removeCodes);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the active caseIndex
|
||||
const caseIndex = getCaseIndex(tIcu, value);
|
||||
icuTNode.activeCaseIndex = caseIndex !== -1 ? caseIndex : null;
|
||||
if (caseIndex > -1) {
|
||||
// Add the nodes for the new case
|
||||
readCreateOpCodes(-1, tIcu.create[caseIndex], tView, lView);
|
||||
caseCreated = true;
|
||||
}
|
||||
caseCreated = icuSwitchCase(
|
||||
tView, updateOpCodes[++j] as number, nodeIndex, icus!, lView, value);
|
||||
break;
|
||||
case I18nUpdateOpCode.IcuUpdate:
|
||||
tIcuIndex = updateOpCodes[++j] as number;
|
||||
tIcu = icus![tIcuIndex];
|
||||
icuTNode = getTNode(tView, nodeIndex) as TIcuContainerNode;
|
||||
if (icuTNode.activeCaseIndex !== null) {
|
||||
readUpdateOpCodes(
|
||||
tIcu.update[icuTNode.activeCaseIndex], icus, bindingsStartIndex, changeMask,
|
||||
tView, lView, caseCreated);
|
||||
}
|
||||
icuUpdateCase(
|
||||
tView, lView, updateOpCodes[++j] as number, nodeIndex, bindingsStartIndex,
|
||||
icus!, caseCreated);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -969,6 +932,70 @@ function readUpdateOpCodes(
|
||||
}
|
||||
}
|
||||
|
||||
function icuUpdateCase(
|
||||
tView: TView, lView: LView, tIcuIndex: number, nodeIndex: number, bindingsStartIndex: number,
|
||||
tIcus: TIcu[], caseCreated: boolean) {
|
||||
const tIcu = tIcus[tIcuIndex];
|
||||
const icuTNode = getTNode(tView, nodeIndex) as TIcuContainerNode;
|
||||
if (icuTNode.activeCaseIndex !== null) {
|
||||
readUpdateOpCodes(
|
||||
tIcu.update[icuTNode.activeCaseIndex], tIcus, bindingsStartIndex, changeMask, tView, lView,
|
||||
caseCreated);
|
||||
}
|
||||
}
|
||||
|
||||
function icuSwitchCase(
|
||||
tView: TView, tIcuIndex: number, nodeIndex: number, tIcus: TIcu[], lView: LView,
|
||||
value: string): boolean {
|
||||
const tIcu = tIcus[tIcuIndex];
|
||||
const icuTNode = getTNode(tView, nodeIndex) as TIcuContainerNode;
|
||||
let caseCreated = false;
|
||||
// If there is an active case, delete the old nodes
|
||||
if (icuTNode.activeCaseIndex !== null) {
|
||||
const removeCodes = tIcu.remove[icuTNode.activeCaseIndex];
|
||||
for (let k = 0; k < removeCodes.length; k++) {
|
||||
const removeOpCode = removeCodes[k] as number;
|
||||
const nodeOrIcuIndex = removeOpCode >>> I18nMutateOpCode.SHIFT_REF;
|
||||
switch (removeOpCode & I18nMutateOpCode.MASK_INSTRUCTION) {
|
||||
case I18nMutateOpCode.Remove:
|
||||
// Remove DOM element, but do *not* mark TNode as detached, since we are
|
||||
// just switching ICU cases (while keeping the same TNode), so a DOM element
|
||||
// representing a new ICU case will be re-created.
|
||||
removeNode(tView, lView, nodeOrIcuIndex, /* markAsDetached */ false);
|
||||
break;
|
||||
case I18nMutateOpCode.RemoveNestedIcu:
|
||||
removeNestedIcu(
|
||||
tView, tIcus, removeCodes, nodeOrIcuIndex,
|
||||
removeCodes[k + 1] as number >>> I18nMutateOpCode.SHIFT_REF);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the active caseIndex
|
||||
const caseIndex = getCaseIndex(tIcu, value);
|
||||
icuTNode.activeCaseIndex = caseIndex !== -1 ? caseIndex : null;
|
||||
if (caseIndex > -1) {
|
||||
// Add the nodes for the new case
|
||||
readCreateOpCodes(
|
||||
-1 /* -1 means we don't have parent node */, tIcu.create[caseIndex], tView, lView);
|
||||
caseCreated = true;
|
||||
}
|
||||
return caseCreated;
|
||||
}
|
||||
|
||||
function removeNestedIcu(
|
||||
tView: TView, tIcus: TIcu[], removeCodes: I18nMutateOpCodes, nodeIndex: number,
|
||||
nestedIcuNodeIndex: number) {
|
||||
const nestedIcuTNode = getTNode(tView, nestedIcuNodeIndex) as TIcuContainerNode;
|
||||
const activeIndex = nestedIcuTNode.activeCaseIndex;
|
||||
if (activeIndex !== null) {
|
||||
const nestedTIcu = tIcus[nodeIndex];
|
||||
// FIXME(misko): the fact that we are adding items to parent list looks very suspect!
|
||||
addAllToArray(nestedTIcu.remove[activeIndex], removeCodes);
|
||||
}
|
||||
}
|
||||
|
||||
function removeNode(tView: TView, lView: LView, index: number, markAsDetached: boolean) {
|
||||
const removedPhTNode = getTNode(tView, index);
|
||||
const removedPhRNode = getNativeByIndex(index, lView);
|
||||
@ -1044,6 +1071,9 @@ function i18nAttributesFirstPass(lView: LView, tView: TView, index: number, valu
|
||||
const previousElement = getPreviousOrParentTNode();
|
||||
const previousElementIndex = previousElement.index - HEADER_OFFSET;
|
||||
const updateOpCodes: I18nUpdateOpCodes = [];
|
||||
if (ngDevMode) {
|
||||
attachDebugGetter(updateOpCodes, i18nUpdateOpCodesToString);
|
||||
}
|
||||
for (let i = 0; i < values.length; i += 2) {
|
||||
const attrName = values[i];
|
||||
const message = values[i + 1];
|
||||
@ -1134,7 +1164,7 @@ export function ɵɵi18nApply(index: number) {
|
||||
}
|
||||
const bindingsStartIndex = getBindingIndex() - shiftsCounter - 1;
|
||||
const lView = getLView();
|
||||
readUpdateOpCodes(updateOpCodes, icus, bindingsStartIndex, changeMask, tView, lView);
|
||||
readUpdateOpCodes(updateOpCodes, icus, bindingsStartIndex, changeMask, tView, lView, false);
|
||||
|
||||
// Reset changeMask & maskBit to default for the next update cycle
|
||||
changeMask = 0b0;
|
||||
@ -1180,9 +1210,9 @@ function getCaseIndex(icuExpression: TIcu, bindingValue: string): number {
|
||||
function icuStart(
|
||||
tIcus: TIcu[], icuExpression: IcuExpression, startIndex: number,
|
||||
expandoStartIndex: number): void {
|
||||
const createCodes = [];
|
||||
const removeCodes = [];
|
||||
const updateCodes = [];
|
||||
const createCodes: I18nMutateOpCodes[] = [];
|
||||
const removeCodes: I18nMutateOpCodes[] = [];
|
||||
const updateCodes: I18nUpdateOpCodes[] = [];
|
||||
const vars = [];
|
||||
const childIcus: number[][] = [];
|
||||
for (let i = 0; i < icuExpression.values.length; i++) {
|
||||
@ -1240,6 +1270,11 @@ function parseIcuCase(
|
||||
}
|
||||
const wrapper = getTemplateContent(inertBodyElement!) as Element || inertBodyElement;
|
||||
const opCodes: IcuCase = {vars: 0, childIcus: [], create: [], remove: [], update: []};
|
||||
if (ngDevMode) {
|
||||
attachDebugGetter(opCodes.create, i18nMutateOpCodesToString);
|
||||
attachDebugGetter(opCodes.remove, i18nMutateOpCodesToString);
|
||||
attachDebugGetter(opCodes.update, i18nUpdateOpCodesToString);
|
||||
}
|
||||
parseNodes(wrapper.firstChild, opCodes, parentIndex, nestedIcus, tIcus, expandoStartIndex);
|
||||
return opCodes;
|
||||
}
|
||||
@ -1364,6 +1399,7 @@ function parseNodes(
|
||||
3, // skip 3 opCodes if not changed
|
||||
-1 - nestedIcu.mainBinding,
|
||||
nestedIcuNodeIndex << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.IcuSwitch,
|
||||
// FIXME(misko): Index should be part of the opcode
|
||||
nestTIcuIndex,
|
||||
mask, // mask of all the bindings of this ICU expression
|
||||
2, // skip 2 opCodes if not changed
|
||||
@ -1371,6 +1407,7 @@ function parseNodes(
|
||||
nestTIcuIndex);
|
||||
icuCase.remove.push(
|
||||
nestTIcuIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.RemoveNestedIcu,
|
||||
// FIXME(misko): Index should be part of the opcode
|
||||
nestedIcuNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove);
|
||||
}
|
||||
}
|
||||
|
186
packages/core/src/render3/i18n_debug.ts
Normal file
186
packages/core/src/render3/i18n_debug.ts
Normal file
@ -0,0 +1,186 @@
|
||||
/**
|
||||
* @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 {assertNumber, assertString} from '../util/assert';
|
||||
|
||||
import {COMMENT_MARKER, ELEMENT_MARKER, getInstructionFromI18nMutateOpCode, getParentFromI18nMutateOpCode, getRefFromI18nMutateOpCode, I18nMutateOpCode, I18nMutateOpCodes, I18nUpdateOpCode, I18nUpdateOpCodes} from './interfaces/i18n';
|
||||
|
||||
/**
|
||||
* Converts `I18nUpdateOpCodes` array into a human readable format.
|
||||
*
|
||||
* This function is attached to the `I18nUpdateOpCodes.debug` property if `ngDevMode` is enabled.
|
||||
* This function provides a human readable view of the opcodes. This is useful when debugging the
|
||||
* application as well as writing more readable tests.
|
||||
*
|
||||
* @param this `I18nUpdateOpCodes` if attached as a method.
|
||||
* @param opcodes `I18nUpdateOpCodes` if invoked as a function.
|
||||
*/
|
||||
export function i18nUpdateOpCodesToString(
|
||||
this: I18nUpdateOpCodes|void, opcodes?: I18nUpdateOpCodes): string[] {
|
||||
const parser = new OpCodeParser(opcodes || (Array.isArray(this) ? this : []));
|
||||
let lines: string[] = [];
|
||||
|
||||
function consumeOpCode(value: number): string {
|
||||
const ref = value >>> I18nUpdateOpCode.SHIFT_REF;
|
||||
const opCode = value & I18nUpdateOpCode.MASK_OPCODE;
|
||||
switch (opCode) {
|
||||
case I18nUpdateOpCode.Text:
|
||||
return `(lView[${ref}] as Text).textContent = $$$`;
|
||||
case I18nUpdateOpCode.Attr:
|
||||
const attrName = parser.consumeString();
|
||||
const sanitizationFn = parser.consumeFunction();
|
||||
const value = sanitizationFn ? `(${sanitizationFn})($$$)` : '$$$';
|
||||
return `(lView[${ref}] as Element).setAttribute('${attrName}', ${value})`;
|
||||
case I18nUpdateOpCode.IcuSwitch:
|
||||
return `icuSwitchCase(lView[${ref}] as Comment, ${parser.consumeNumber()}, $$$)`;
|
||||
case I18nUpdateOpCode.IcuUpdate:
|
||||
return `icuUpdateCase(lView[${ref}] as Comment, ${parser.consumeNumber()})`;
|
||||
}
|
||||
throw new Error('unexpected OpCode');
|
||||
}
|
||||
|
||||
|
||||
while (parser.hasMore()) {
|
||||
let mask = parser.consumeNumber();
|
||||
let size = parser.consumeNumber();
|
||||
const end = parser.i + size;
|
||||
const statements: string[] = [];
|
||||
let statement = '';
|
||||
while (parser.i < end) {
|
||||
let value = parser.consumeNumberOrString();
|
||||
if (typeof value === 'string') {
|
||||
statement += value;
|
||||
} else if (value < 0) {
|
||||
// Negative numbers are ref indexes
|
||||
statement += '${lView[' + (0 - value) + ']}';
|
||||
} else {
|
||||
// Positive numbers are operations.
|
||||
const opCodeText = consumeOpCode(value);
|
||||
statements.push(opCodeText.replace('$$$', '`' + statement + '`') + ';');
|
||||
statement = '';
|
||||
}
|
||||
}
|
||||
lines.push(`if (mask & 0b${mask.toString(2)}) { ${statements.join(' ')} }`);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts `I18nMutableOpCodes` array into a human readable format.
|
||||
*
|
||||
* This function is attached to the `I18nMutableOpCodes.debug` if `ngDevMode` is enabled. This
|
||||
* function provides a human readable view of the opcodes. This is useful when debugging the
|
||||
* application as well as writing more readable tests.
|
||||
*
|
||||
* @param this `I18nMutableOpCodes` if attached as a method.
|
||||
* @param opcodes `I18nMutableOpCodes` if invoked as a function.
|
||||
*/
|
||||
export function i18nMutateOpCodesToString(
|
||||
this: I18nMutateOpCodes|void, opcodes?: I18nMutateOpCodes): string[] {
|
||||
const parser = new OpCodeParser(opcodes || (Array.isArray(this) ? this : []));
|
||||
let lines: string[] = [];
|
||||
|
||||
function consumeOpCode(opCode: number): string {
|
||||
const parent = getParentFromI18nMutateOpCode(opCode);
|
||||
const ref = getRefFromI18nMutateOpCode(opCode);
|
||||
switch (getInstructionFromI18nMutateOpCode(opCode)) {
|
||||
case I18nMutateOpCode.Select:
|
||||
lastRef = ref;
|
||||
return '';
|
||||
case I18nMutateOpCode.AppendChild:
|
||||
return `(lView[${parent}] as Element).appendChild(lView[${lastRef}])`;
|
||||
case I18nMutateOpCode.Remove:
|
||||
return `(lView[${parent}] as Element).remove(lView[${ref}])`;
|
||||
case I18nMutateOpCode.Attr:
|
||||
return `(lView[${ref}] as Element).setAttribute("${parser.consumeString()}", "${
|
||||
parser.consumeString()}")`;
|
||||
case I18nMutateOpCode.ElementEnd:
|
||||
return `setPreviousOrParentTNode(tView.data[${ref}] as TNode)`;
|
||||
case I18nMutateOpCode.RemoveNestedIcu:
|
||||
return `removeNestedICU(${ref})`;
|
||||
}
|
||||
throw new Error('Unexpected OpCode');
|
||||
}
|
||||
|
||||
let lastRef = -1;
|
||||
while (parser.hasMore()) {
|
||||
let value = parser.consumeNumberStringOrMarker();
|
||||
if (value === COMMENT_MARKER) {
|
||||
const text = parser.consumeString();
|
||||
lastRef = parser.consumeNumber();
|
||||
lines.push(`lView[${lastRef}] = document.createComment("${text}")`);
|
||||
} else if (value === ELEMENT_MARKER) {
|
||||
const text = parser.consumeString();
|
||||
lastRef = parser.consumeNumber();
|
||||
lines.push(`lView[${lastRef}] = document.createElement("${text}")`);
|
||||
} else if (typeof value === 'string') {
|
||||
lastRef = parser.consumeNumber();
|
||||
lines.push(`lView[${lastRef}] = document.createTextNode("${value}")`);
|
||||
} else if (typeof value === 'number') {
|
||||
const line = consumeOpCode(value);
|
||||
line && lines.push(line);
|
||||
} else {
|
||||
throw new Error('Unexpected value');
|
||||
}
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
|
||||
class OpCodeParser {
|
||||
i: number = 0;
|
||||
codes: any[];
|
||||
|
||||
constructor(codes: any[]) {
|
||||
this.codes = codes;
|
||||
}
|
||||
|
||||
hasMore() {
|
||||
return this.i < this.codes.length;
|
||||
}
|
||||
|
||||
consumeNumber(): number {
|
||||
let value = this.codes[this.i++];
|
||||
assertNumber(value, 'expecting number in OpCode');
|
||||
return value;
|
||||
}
|
||||
|
||||
consumeString(): string {
|
||||
let value = this.codes[this.i++];
|
||||
assertString(value, 'expecting string in OpCode');
|
||||
return value;
|
||||
}
|
||||
|
||||
consumeFunction(): Function|null {
|
||||
let value = this.codes[this.i++];
|
||||
if (value === null || typeof value === 'function') {
|
||||
return value;
|
||||
}
|
||||
throw new Error('expecting function in OpCode');
|
||||
}
|
||||
|
||||
consumeNumberOrString(): number|string {
|
||||
let value = this.codes[this.i++];
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
assertNumber(value, 'expecting number or string in OpCode');
|
||||
return value;
|
||||
}
|
||||
|
||||
consumeNumberStringOrMarker(): number|string|COMMENT_MARKER|ELEMENT_MARKER {
|
||||
let value = this.codes[this.i++];
|
||||
if (typeof value === 'string' || typeof value === 'number' || value == COMMENT_MARKER ||
|
||||
value == ELEMENT_MARKER) {
|
||||
return value;
|
||||
}
|
||||
assertNumber(value, 'expecting number, string, COMMENT_MARKER or ELEMENT_MARKER in OpCode');
|
||||
return value;
|
||||
}
|
||||
}
|
@ -15,15 +15,14 @@ import {createNamedArrayType} from '../../util/named_array_type';
|
||||
import {initNgDevMode} from '../../util/ng_dev_mode';
|
||||
import {CONTAINER_HEADER_OFFSET, HAS_TRANSPLANTED_VIEWS, LContainer, MOVED_VIEWS, NATIVE} from '../interfaces/container';
|
||||
import {DirectiveDefList, PipeDefList, ViewQueriesFunction} from '../interfaces/definition';
|
||||
import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nMutateOpCodes, I18nUpdateOpCode, I18nUpdateOpCodes, TIcu} from '../interfaces/i18n';
|
||||
import {PropertyAliases, TConstants, TContainerNode, TElementNode, TNode as ITNode, TNodeFlags, TNodeProviderIndexes, TNodeType, TViewNode} from '../interfaces/node';
|
||||
import {PropertyAliases, TConstants, TContainerNode, TElementNode, TNode as ITNode, TNodeFlags, TNodeProviderIndexes, TNodeType, TNodeTypeAsString, TViewNode} from '../interfaces/node';
|
||||
import {SelectorFlags} from '../interfaces/projection';
|
||||
import {LQueries, TQueries} from '../interfaces/query';
|
||||
import {RComment, RElement, Renderer3, RendererFactory3, RNode} from '../interfaces/renderer';
|
||||
import {getTStylingRangeNext, getTStylingRangeNextDuplicate, getTStylingRangePrev, getTStylingRangePrevDuplicate, TStylingKey, TStylingRange} from '../interfaces/styling';
|
||||
import {CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_VIEW, DestroyHookData, ExpandoInstructions, FLAGS, HEADER_OFFSET, HookData, HOST, INJECTOR, LView, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, SANITIZER, T_HOST, TData, TVIEW, TView as ITView, TView, TViewType} from '../interfaces/view';
|
||||
import {CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DebugNode, DECLARATION_VIEW, DestroyHookData, ExpandoInstructions, FLAGS, HEADER_OFFSET, HookData, HOST, INJECTOR, LContainerDebug as ILContainerDebug, LView, LViewDebug as ILViewDebug, LViewDebugRange, LViewDebugRangeContent, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, SANITIZER, T_HOST, TData, TView as ITView, TVIEW, TView, TViewType} from '../interfaces/view';
|
||||
import {attachDebugObject} from '../util/debug_utils';
|
||||
import {getTNode, unwrapRNode} from '../util/view_utils';
|
||||
import {unwrapRNode} from '../util/view_utils';
|
||||
|
||||
const NG_DEV_MODE = ((typeof ngDevMode === 'undefined' || !!ngDevMode) && initNgDevMode());
|
||||
|
||||
@ -143,7 +142,10 @@ export const TViewConstructor = class TView implements ITView {
|
||||
public firstChild: ITNode|null, //
|
||||
public schemas: SchemaMetadata[]|null, //
|
||||
public consts: TConstants|null, //
|
||||
public incompleteFirstPass: boolean //
|
||||
public incompleteFirstPass: boolean, //
|
||||
public _decls: number, //
|
||||
public _vars: number, //
|
||||
|
||||
) {}
|
||||
|
||||
get template_(): string {
|
||||
@ -335,9 +337,9 @@ export function attachLContainerDebug(lContainer: LContainer) {
|
||||
attachDebugObject(lContainer, new LContainerDebug(lContainer));
|
||||
}
|
||||
|
||||
export function toDebug(obj: LView): LViewDebug;
|
||||
export function toDebug(obj: LView|null): LViewDebug|null;
|
||||
export function toDebug(obj: LView|LContainer|null): LViewDebug|LContainerDebug|null;
|
||||
export function toDebug(obj: LView): ILViewDebug;
|
||||
export function toDebug(obj: LView|null): ILViewDebug|null;
|
||||
export function toDebug(obj: LView|LContainer|null): ILViewDebug|ILContainerDebug|null;
|
||||
export function toDebug(obj: any): any {
|
||||
if (obj) {
|
||||
const debug = (obj as any).debug;
|
||||
@ -375,7 +377,7 @@ function toHtml(value: any, includeChildren: boolean = false): string|null {
|
||||
}
|
||||
}
|
||||
|
||||
export class LViewDebug {
|
||||
export class LViewDebug implements ILViewDebug {
|
||||
constructor(private readonly _raw_lView: LView) {}
|
||||
|
||||
/**
|
||||
@ -396,10 +398,10 @@ export class LViewDebug {
|
||||
indexWithinInitPhase: flags >> LViewFlags.IndexWithinInitPhaseShift,
|
||||
};
|
||||
}
|
||||
get parent(): LViewDebug|LContainerDebug|null {
|
||||
get parent(): ILViewDebug|ILContainerDebug|null {
|
||||
return toDebug(this._raw_lView[PARENT]);
|
||||
}
|
||||
get host(): string|null {
|
||||
get hostHTML(): string|null {
|
||||
return toHtml(this._raw_lView[HOST], true);
|
||||
}
|
||||
get html(): string {
|
||||
@ -410,10 +412,9 @@ export class LViewDebug {
|
||||
}
|
||||
/**
|
||||
* The tree of nodes associated with the current `LView`. The nodes have been normalized into
|
||||
* a
|
||||
* tree structure with relevant details pulled out for readability.
|
||||
* a tree structure with relevant details pulled out for readability.
|
||||
*/
|
||||
get nodes(): DebugNode[]|null {
|
||||
get nodes(): DebugNode[] {
|
||||
const lView = this._raw_lView;
|
||||
const tNode = lView[TVIEW].firstChild;
|
||||
return toDebugNodes(tNode, lView);
|
||||
@ -437,16 +438,16 @@ export class LViewDebug {
|
||||
get sanitizer(): Sanitizer|null {
|
||||
return this._raw_lView[SANITIZER];
|
||||
}
|
||||
get childHead(): LViewDebug|LContainerDebug|null {
|
||||
get childHead(): ILViewDebug|ILContainerDebug|null {
|
||||
return toDebug(this._raw_lView[CHILD_HEAD]);
|
||||
}
|
||||
get next(): LViewDebug|LContainerDebug|null {
|
||||
get next(): ILViewDebug|ILContainerDebug|null {
|
||||
return toDebug(this._raw_lView[NEXT]);
|
||||
}
|
||||
get childTail(): LViewDebug|LContainerDebug|null {
|
||||
get childTail(): ILViewDebug|ILContainerDebug|null {
|
||||
return toDebug(this._raw_lView[CHILD_TAIL]);
|
||||
}
|
||||
get declarationView(): LViewDebug|null {
|
||||
get declarationView(): ILViewDebug|null {
|
||||
return toDebug(this._raw_lView[DECLARATION_VIEW]);
|
||||
}
|
||||
get queries(): LQueries|null {
|
||||
@ -456,11 +457,35 @@ export class LViewDebug {
|
||||
return this._raw_lView[T_HOST];
|
||||
}
|
||||
|
||||
get decls(): LViewDebugRange {
|
||||
const tView = this.tView as any as {_decls: number, _vars: number};
|
||||
const start = HEADER_OFFSET;
|
||||
return toLViewRange(this.tView, this._raw_lView, start, start + tView._decls);
|
||||
}
|
||||
|
||||
get vars(): LViewDebugRange {
|
||||
const tView = this.tView as any as {_decls: number, _vars: number};
|
||||
const start = HEADER_OFFSET + tView._decls;
|
||||
return toLViewRange(this.tView, this._raw_lView, start, start + tView._vars);
|
||||
}
|
||||
|
||||
get i18n(): LViewDebugRange {
|
||||
const tView = this.tView as any as {_decls: number, _vars: number};
|
||||
const start = HEADER_OFFSET + tView._decls + tView._vars;
|
||||
return toLViewRange(this.tView, this._raw_lView, start, this.tView.expandoStartIndex);
|
||||
}
|
||||
|
||||
get expando(): LViewDebugRange {
|
||||
const tView = this.tView as any as {_decls: number, _vars: number};
|
||||
return toLViewRange(
|
||||
this.tView, this._raw_lView, this.tView.expandoStartIndex, this._raw_lView.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalized view of child views (and containers) attached at this location.
|
||||
*/
|
||||
get childViews(): Array<LViewDebug|LContainerDebug> {
|
||||
const childViews: Array<LViewDebug|LContainerDebug> = [];
|
||||
get childViews(): Array<ILViewDebug|ILContainerDebug> {
|
||||
const childViews: Array<ILViewDebug|ILContainerDebug> = [];
|
||||
let child = this.childHead;
|
||||
while (child) {
|
||||
childViews.push(child);
|
||||
@ -470,11 +495,12 @@ export class LViewDebug {
|
||||
}
|
||||
}
|
||||
|
||||
export interface DebugNode {
|
||||
html: string|null;
|
||||
native: Node;
|
||||
nodes: DebugNode[]|null;
|
||||
component: LViewDebug|null;
|
||||
function toLViewRange(tView: TView, lView: LView, start: number, end: number): LViewDebugRange {
|
||||
let content: LViewDebugRangeContent[] = [];
|
||||
for (let index = start; index < end; index++) {
|
||||
content.push({index: index, t: tView.data[index], l: lView[index]});
|
||||
}
|
||||
return {start: start, end: end, length: end - start, content: content};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -483,7 +509,7 @@ export interface DebugNode {
|
||||
* @param tNode
|
||||
* @param lView
|
||||
*/
|
||||
export function toDebugNodes(tNode: ITNode|null, lView: LView): DebugNode[]|null {
|
||||
export function toDebugNodes(tNode: ITNode|null, lView: LView): DebugNode[] {
|
||||
if (tNode) {
|
||||
const debugNodes: DebugNode[] = [];
|
||||
let tNodeCursor: ITNode|null = tNode;
|
||||
@ -493,33 +519,32 @@ export function toDebugNodes(tNode: ITNode|null, lView: LView): DebugNode[]|null
|
||||
}
|
||||
return debugNodes;
|
||||
} else {
|
||||
return null;
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function buildDebugNode(tNode: ITNode, lView: LView, nodeIndex: number): DebugNode {
|
||||
const rawValue = lView[nodeIndex];
|
||||
const native = unwrapRNode(rawValue);
|
||||
const componentLViewDebug = toDebug(readLViewValue(rawValue));
|
||||
return {
|
||||
html: toHtml(native),
|
||||
type: TNodeTypeAsString[tNode.type],
|
||||
native: native as any,
|
||||
nodes: toDebugNodes(tNode.child, lView),
|
||||
component: componentLViewDebug,
|
||||
children: toDebugNodes(tNode.child, lView),
|
||||
};
|
||||
}
|
||||
|
||||
export class LContainerDebug {
|
||||
export class LContainerDebug implements ILContainerDebug {
|
||||
constructor(private readonly _raw_lContainer: LContainer) {}
|
||||
|
||||
get hasTransplantedViews(): boolean {
|
||||
return this._raw_lContainer[HAS_TRANSPLANTED_VIEWS];
|
||||
}
|
||||
get views(): LViewDebug[] {
|
||||
get views(): ILViewDebug[] {
|
||||
return this._raw_lContainer.slice(CONTAINER_HEADER_OFFSET)
|
||||
.map(toDebug as (l: LView) => LViewDebug);
|
||||
.map(toDebug as (l: LView) => ILViewDebug);
|
||||
}
|
||||
get parent(): LViewDebug|LContainerDebug|null {
|
||||
get parent(): ILViewDebug|null {
|
||||
return toDebug(this._raw_lContainer[PARENT]);
|
||||
}
|
||||
get movedViews(): LView[]|null {
|
||||
@ -550,206 +575,3 @@ export function readLViewValue(value: any): LView|null {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export class I18NDebugItem {
|
||||
[key: string]: any;
|
||||
|
||||
get tNode() {
|
||||
return getTNode(this._lView[TVIEW], this.nodeIndex);
|
||||
}
|
||||
|
||||
constructor(
|
||||
public __raw_opCode: any, private _lView: LView, public nodeIndex: number,
|
||||
public type: string) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns a list of "Create" & "Update" OpCodes into a human-readable list of operations for
|
||||
* debugging purposes.
|
||||
* @param mutateOpCodes mutation opCodes to read
|
||||
* @param updateOpCodes update opCodes to read
|
||||
* @param icus list of ICU expressions
|
||||
* @param lView The view the opCodes are acting on
|
||||
*/
|
||||
export function attachI18nOpCodesDebug(
|
||||
mutateOpCodes: I18nMutateOpCodes, updateOpCodes: I18nUpdateOpCodes, icus: TIcu[]|null,
|
||||
lView: LView) {
|
||||
attachDebugObject(mutateOpCodes, new I18nMutateOpCodesDebug(mutateOpCodes, lView));
|
||||
attachDebugObject(updateOpCodes, new I18nUpdateOpCodesDebug(updateOpCodes, icus, lView));
|
||||
|
||||
if (icus) {
|
||||
icus.forEach(icu => {
|
||||
icu.create.forEach(icuCase => {
|
||||
attachDebugObject(icuCase, new I18nMutateOpCodesDebug(icuCase, lView));
|
||||
});
|
||||
icu.update.forEach(icuCase => {
|
||||
attachDebugObject(icuCase, new I18nUpdateOpCodesDebug(icuCase, icus, lView));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class I18nMutateOpCodesDebug implements I18nOpCodesDebug {
|
||||
constructor(private readonly __raw_opCodes: I18nMutateOpCodes, private readonly __lView: LView) {}
|
||||
|
||||
/**
|
||||
* A list of operation information about how the OpCodes will act on the view.
|
||||
*/
|
||||
get operations() {
|
||||
const {__lView, __raw_opCodes} = this;
|
||||
const results: any[] = [];
|
||||
|
||||
for (let i = 0; i < __raw_opCodes.length; i++) {
|
||||
const opCode = __raw_opCodes[i];
|
||||
let result: any;
|
||||
if (typeof opCode === 'string') {
|
||||
result = {
|
||||
__raw_opCode: opCode,
|
||||
type: 'Create Text Node',
|
||||
nodeIndex: __raw_opCodes[++i],
|
||||
text: opCode,
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof opCode === 'number') {
|
||||
switch (opCode & I18nMutateOpCode.MASK_OPCODE) {
|
||||
case I18nMutateOpCode.AppendChild:
|
||||
const destinationNodeIndex = opCode >>> I18nMutateOpCode.SHIFT_PARENT;
|
||||
result = new I18NDebugItem(opCode, __lView, destinationNodeIndex, 'AppendChild');
|
||||
break;
|
||||
case I18nMutateOpCode.Select:
|
||||
const nodeIndex = opCode >>> I18nMutateOpCode.SHIFT_REF;
|
||||
result = new I18NDebugItem(opCode, __lView, nodeIndex, 'Select');
|
||||
break;
|
||||
case I18nMutateOpCode.ElementEnd:
|
||||
let elementIndex = opCode >>> I18nMutateOpCode.SHIFT_REF;
|
||||
result = new I18NDebugItem(opCode, __lView, elementIndex, 'ElementEnd');
|
||||
break;
|
||||
case I18nMutateOpCode.Attr:
|
||||
elementIndex = opCode >>> I18nMutateOpCode.SHIFT_REF;
|
||||
result = new I18NDebugItem(opCode, __lView, elementIndex, 'Attr');
|
||||
result['attrName'] = __raw_opCodes[++i];
|
||||
result['attrValue'] = __raw_opCodes[++i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
switch (opCode) {
|
||||
case COMMENT_MARKER:
|
||||
result = {
|
||||
__raw_opCode: opCode,
|
||||
type: 'COMMENT_MARKER',
|
||||
commentValue: __raw_opCodes[++i],
|
||||
nodeIndex: __raw_opCodes[++i],
|
||||
};
|
||||
break;
|
||||
case ELEMENT_MARKER:
|
||||
result = {
|
||||
__raw_opCode: opCode,
|
||||
type: 'ELEMENT_MARKER',
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
result = {
|
||||
__raw_opCode: opCode,
|
||||
type: 'Unknown Op Code',
|
||||
code: opCode,
|
||||
};
|
||||
}
|
||||
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
export class I18nUpdateOpCodesDebug implements I18nOpCodesDebug {
|
||||
constructor(
|
||||
private readonly __raw_opCodes: I18nUpdateOpCodes, private readonly icus: TIcu[]|null,
|
||||
private readonly __lView: LView) {}
|
||||
|
||||
/**
|
||||
* A list of operation information about how the OpCodes will act on the view.
|
||||
*/
|
||||
get operations() {
|
||||
const {__lView, __raw_opCodes, icus} = this;
|
||||
const results: any[] = [];
|
||||
|
||||
for (let i = 0; i < __raw_opCodes.length; i++) {
|
||||
// bit code to check if we should apply the next update
|
||||
const checkBit = __raw_opCodes[i] as number;
|
||||
// Number of opCodes to skip until next set of update codes
|
||||
const skipCodes = __raw_opCodes[++i] as number;
|
||||
let value = '';
|
||||
for (let j = i + 1; j <= (i + skipCodes); j++) {
|
||||
const opCode = __raw_opCodes[j];
|
||||
if (typeof opCode === 'string') {
|
||||
value += opCode;
|
||||
} else if (typeof opCode == 'number') {
|
||||
if (opCode < 0) {
|
||||
// It's a binding index whose value is negative
|
||||
// We cannot know the value of the binding so we only show the index
|
||||
value += `<EFBFBD>${- opCode - 1}<EFBFBD>`;
|
||||
} else {
|
||||
const nodeIndex = opCode >>> I18nUpdateOpCode.SHIFT_REF;
|
||||
let tIcuIndex: number;
|
||||
let tIcu: TIcu;
|
||||
switch (opCode & I18nUpdateOpCode.MASK_OPCODE) {
|
||||
case I18nUpdateOpCode.Attr:
|
||||
const attrName = __raw_opCodes[++j] as string;
|
||||
const sanitizeFn = __raw_opCodes[++j];
|
||||
results.push({
|
||||
__raw_opCode: opCode,
|
||||
checkBit,
|
||||
type: 'Attr',
|
||||
attrValue: value,
|
||||
attrName,
|
||||
sanitizeFn,
|
||||
});
|
||||
break;
|
||||
case I18nUpdateOpCode.Text:
|
||||
results.push({
|
||||
__raw_opCode: opCode,
|
||||
checkBit,
|
||||
type: 'Text',
|
||||
nodeIndex,
|
||||
text: value,
|
||||
});
|
||||
break;
|
||||
case I18nUpdateOpCode.IcuSwitch:
|
||||
tIcuIndex = __raw_opCodes[++j] as number;
|
||||
tIcu = icus![tIcuIndex];
|
||||
let result = new I18NDebugItem(opCode, __lView, nodeIndex, 'IcuSwitch');
|
||||
result['tIcuIndex'] = tIcuIndex;
|
||||
result['checkBit'] = checkBit;
|
||||
result['mainBinding'] = value;
|
||||
result['tIcu'] = tIcu;
|
||||
results.push(result);
|
||||
break;
|
||||
case I18nUpdateOpCode.IcuUpdate:
|
||||
tIcuIndex = __raw_opCodes[++j] as number;
|
||||
tIcu = icus![tIcuIndex];
|
||||
result = new I18NDebugItem(opCode, __lView, nodeIndex, 'IcuUpdate');
|
||||
result['tIcuIndex'] = tIcuIndex;
|
||||
result['checkBit'] = checkBit;
|
||||
result['tIcu'] = tIcu;
|
||||
results.push(result);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
i += skipCodes;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
export interface I18nOpCodesDebug {
|
||||
operations: any[];
|
||||
}
|
||||
|
@ -236,6 +236,13 @@ export function getOrCreateTNode(
|
||||
const tNode = tView.data[adjustedIndex] as TNode ||
|
||||
createTNodeAtIndex(tView, tHostNode, adjustedIndex, type, name, attrs);
|
||||
setPreviousOrParentTNode(tNode, true);
|
||||
if (ngDevMode) {
|
||||
// For performance reasons it is important that the tNode retains the same shape during runtime.
|
||||
// (To make sure that all of the code is monomorphic.) For this reason we seal the object to
|
||||
// prevent class transitions.
|
||||
// FIXME(misko): re-enable this once i18n code is compliant with this.
|
||||
// Object.seal(tNode);
|
||||
}
|
||||
return tNode as TElementNode & TViewNode & TContainerNode & TElementContainerNode &
|
||||
TProjectionNode & TIcuContainerNode;
|
||||
}
|
||||
@ -692,7 +699,9 @@ export function createTView(
|
||||
null, // firstChild: TNode|null,
|
||||
schemas, // schemas: SchemaMetadata[]|null,
|
||||
consts, // consts: TConstants|null
|
||||
false // incompleteFirstPass: boolean
|
||||
false, // incompleteFirstPass: boolean
|
||||
decls, // ngDevMode only: decls
|
||||
vars, // ngDevMode only: vars
|
||||
) :
|
||||
{
|
||||
type: type,
|
||||
|
@ -6,18 +6,31 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {SanitizerFn} from './sanitization';
|
||||
|
||||
/**
|
||||
* `I18nMutateOpCode` defines OpCodes for `I18nMutateOpCodes` array.
|
||||
*
|
||||
* OpCodes are efficient operations which can be applied to the DOM to update it. (For example to
|
||||
* update to a new ICU case requires that we clean up previous elements and create new ones.)
|
||||
*
|
||||
* OpCodes contain three parts:
|
||||
* 1) Parent node index offset.
|
||||
* 2) Reference node index offset.
|
||||
* 3) The OpCode to execute.
|
||||
* 1) Parent node index offset. (p)
|
||||
* 2) Reference node index offset. (r)
|
||||
* 3) The instruction to execute. (i)
|
||||
*
|
||||
* pppp pppp pppp pppp rrrr rrrr rrrr riii
|
||||
* 3322 2222 2222 1111 1111 1110 0000 0000
|
||||
* 1098 7654 3210 9876 5432 1098 7654 3210
|
||||
*
|
||||
* ```
|
||||
* var parent = lView[opCode >>> SHIFT_PARENT];
|
||||
* var refNode = lView[((opCode & MASK_REF) >>> SHIFT_REF)];
|
||||
* var instruction = opCode & MASK_OPCODE;
|
||||
* ```
|
||||
*
|
||||
* See: `I18nCreateOpCodes` for example of usage.
|
||||
*/
|
||||
import {SanitizerFn} from './sanitization';
|
||||
|
||||
export const enum I18nMutateOpCode {
|
||||
/**
|
||||
* Stores shift amount for bits 17-3 that contain reference index.
|
||||
@ -30,36 +43,61 @@ export const enum I18nMutateOpCode {
|
||||
/**
|
||||
* Mask for OpCode
|
||||
*/
|
||||
MASK_OPCODE = 0b111,
|
||||
MASK_INSTRUCTION = 0b111,
|
||||
|
||||
/**
|
||||
* OpCode to select a node. (next OpCode will contain the operation.)
|
||||
* Mask for the Reference node (bits 16-3)
|
||||
*/
|
||||
// FIXME(misko): Why is this not used?
|
||||
MASK_REF = 0b11111111111111000,
|
||||
// 11111110000000000
|
||||
// 65432109876543210
|
||||
|
||||
/**
|
||||
* Instruction to select a node. (next OpCode will contain the operation.)
|
||||
*/
|
||||
Select = 0b000,
|
||||
|
||||
/**
|
||||
* OpCode to append the current node to `PARENT`.
|
||||
* Instruction to append the current node to `PARENT`.
|
||||
*/
|
||||
AppendChild = 0b001,
|
||||
|
||||
/**
|
||||
* OpCode to remove the `REF` node from `PARENT`.
|
||||
* Instruction to remove the `REF` node from `PARENT`.
|
||||
*/
|
||||
Remove = 0b011,
|
||||
|
||||
/**
|
||||
* OpCode to set the attribute of a node.
|
||||
* Instruction to set the attribute of a node.
|
||||
*/
|
||||
Attr = 0b100,
|
||||
|
||||
/**
|
||||
* OpCode to simulate elementEnd()
|
||||
* Instruction to simulate elementEnd()
|
||||
*/
|
||||
ElementEnd = 0b101,
|
||||
|
||||
/**
|
||||
* OpCode to read the remove OpCodes for the nested ICU
|
||||
* Instruction to removed the nested ICU.
|
||||
*/
|
||||
RemoveNestedIcu = 0b110,
|
||||
}
|
||||
|
||||
export function getParentFromI18nMutateOpCode(mergedCode: number): number {
|
||||
return mergedCode >>> I18nMutateOpCode.SHIFT_PARENT;
|
||||
}
|
||||
|
||||
export function getRefFromI18nMutateOpCode(mergedCode: number): number {
|
||||
return (mergedCode & I18nMutateOpCode.MASK_REF) >>> I18nMutateOpCode.SHIFT_REF;
|
||||
}
|
||||
|
||||
export function getInstructionFromI18nMutateOpCode(mergedCode: number): number {
|
||||
return mergedCode & I18nMutateOpCode.MASK_INSTRUCTION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks that the next string is for element.
|
||||
* Marks that the next string is an element name.
|
||||
*
|
||||
* See `I18nMutateOpCodes` documentation.
|
||||
*/
|
||||
@ -71,7 +109,7 @@ export interface ELEMENT_MARKER {
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks that the next string is for comment.
|
||||
* Marks that the next string is comment text.
|
||||
*
|
||||
* See `I18nMutateOpCodes` documentation.
|
||||
*/
|
||||
@ -83,6 +121,18 @@ export interface COMMENT_MARKER {
|
||||
marker: 'comment';
|
||||
}
|
||||
|
||||
export interface I18nDebug {
|
||||
/**
|
||||
* Human readable representation of the OpCode arrays.
|
||||
*
|
||||
* NOTE: This property only exists if `ngDevMode` is set to `true` and it is not present in
|
||||
* production. Its presence is purely to help debug issue in development, and should not be relied
|
||||
* on in production application.
|
||||
*/
|
||||
debug?: string[];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Array storing OpCode for dynamically creating `i18n` blocks.
|
||||
*
|
||||
@ -92,50 +142,27 @@ export interface COMMENT_MARKER {
|
||||
* // For adding text nodes
|
||||
* // ---------------------
|
||||
* // Equivalent to:
|
||||
* // const node = lView[index++] = document.createTextNode('abc');
|
||||
* // lView[1].insertBefore(node, lView[2]);
|
||||
* 'abc', 1 << SHIFT_PARENT | 2 << SHIFT_REF | InsertBefore,
|
||||
*
|
||||
* // Equivalent to:
|
||||
* // const node = lView[index++] = document.createTextNode('xyz');
|
||||
* // lView[1].appendChild(node);
|
||||
* 'xyz', 1 << SHIFT_PARENT | AppendChild,
|
||||
* // lView[1].appendChild(lView[0] = document.createTextNode('xyz'));
|
||||
* 'xyz', 0, 1 << SHIFT_PARENT | 0 << SHIFT_REF | AppendChild,
|
||||
*
|
||||
* // For adding element nodes
|
||||
* // ---------------------
|
||||
* // Equivalent to:
|
||||
* // const node = lView[index++] = document.createElement('div');
|
||||
* // lView[1].insertBefore(node, lView[2]);
|
||||
* ELEMENT_MARKER, 'div', 1 << SHIFT_PARENT | 2 << SHIFT_REF | InsertBefore,
|
||||
*
|
||||
* // Equivalent to:
|
||||
* // const node = lView[index++] = document.createElement('div');
|
||||
* // lView[1].appendChild(node);
|
||||
* ELEMENT_MARKER, 'div', 1 << SHIFT_PARENT | AppendChild,
|
||||
* // lView[1].appendChild(lView[0] = document.createElement('div'));
|
||||
* ELEMENT_MARKER, 'div', 0, 1 << SHIFT_PARENT | 0 << SHIFT_REF | AppendChild,
|
||||
*
|
||||
* // For adding comment nodes
|
||||
* // ---------------------
|
||||
* // Equivalent to:
|
||||
* // const node = lView[index++] = document.createComment('');
|
||||
* // lView[1].insertBefore(node, lView[2]);
|
||||
* COMMENT_MARKER, '', 1 << SHIFT_PARENT | 2 << SHIFT_REF | InsertBefore,
|
||||
*
|
||||
* // Equivalent to:
|
||||
* // const node = lView[index++] = document.createComment('');
|
||||
* // lView[1].appendChild(node);
|
||||
* COMMENT_MARKER, '', 1 << SHIFT_PARENT | AppendChild,
|
||||
* // lView[1].appendChild(lView[0] = document.createComment(''));
|
||||
* COMMENT_MARKER, '', 0, 1 << SHIFT_PARENT | 0 << SHIFT_REF | AppendChild,
|
||||
*
|
||||
* // For moving existing nodes to a different location
|
||||
* // --------------------------------------------------
|
||||
* // Equivalent to:
|
||||
* // const node = lView[1];
|
||||
* // lView[2].insertBefore(node, lView[3]);
|
||||
* 1 << SHIFT_REF | Select, 2 << SHIFT_PARENT | 3 << SHIFT_REF | InsertBefore,
|
||||
*
|
||||
* // Equivalent to:
|
||||
* // const node = lView[1];
|
||||
* // lView[2].appendChild(node);
|
||||
* 1 << SHIFT_REF | Select, 2 << SHIFT_PARENT | AppendChild,
|
||||
* 1 << SHIFT_REF | Select, 2 << SHIFT_PARENT | 0 << SHIFT_REF | AppendChild,
|
||||
*
|
||||
* // For removing existing nodes
|
||||
* // --------------------------------------------------
|
||||
@ -147,18 +174,14 @@ export interface COMMENT_MARKER {
|
||||
* // --------------------------------------------------
|
||||
* // const node = lView[1];
|
||||
* // node.setAttribute('attr', 'value');
|
||||
* 1 << SHIFT_REF | Select, 'attr', 'value'
|
||||
* // NOTE: Select followed by two string (vs select followed by OpCode)
|
||||
* 1 << SHIFT_REF | Attr, 'attr', 'value'
|
||||
* ];
|
||||
* ```
|
||||
* NOTE:
|
||||
* - `index` is initial location where the extra nodes should be stored in the EXPANDO section of
|
||||
* `LVIewData`.
|
||||
*
|
||||
* See: `applyI18nCreateOpCodes`;
|
||||
*/
|
||||
export interface I18nMutateOpCodes extends Array<number|string|ELEMENT_MARKER|COMMENT_MARKER|null> {
|
||||
}
|
||||
export interface I18nMutateOpCodes extends Array<number|string|ELEMENT_MARKER|COMMENT_MARKER|null>,
|
||||
I18nDebug {}
|
||||
|
||||
export const enum I18nUpdateOpCode {
|
||||
/**
|
||||
@ -171,19 +194,19 @@ export const enum I18nUpdateOpCode {
|
||||
MASK_OPCODE = 0b11,
|
||||
|
||||
/**
|
||||
* OpCode to update a text node.
|
||||
* Instruction to update a text node.
|
||||
*/
|
||||
Text = 0b00,
|
||||
/**
|
||||
* OpCode to update a attribute of a node.
|
||||
* Instruction to update a attribute of a node.
|
||||
*/
|
||||
Attr = 0b01,
|
||||
/**
|
||||
* OpCode to switch the current ICU case.
|
||||
* Instruction to switch the current ICU case.
|
||||
*/
|
||||
IcuSwitch = 0b10,
|
||||
/**
|
||||
* OpCode to update the current ICU case.
|
||||
* Instruction to update the current ICU case.
|
||||
*/
|
||||
IcuUpdate = 0b11,
|
||||
}
|
||||
@ -197,6 +220,10 @@ export const enum I18nUpdateOpCode {
|
||||
* higher.) The OpCodes then compare its own change mask against the expression change mask to
|
||||
* determine if the OpCodes should execute.
|
||||
*
|
||||
* NOTE: 32nd bit is special as it says 32nd or higher. This way if we have more than 32 bindings
|
||||
* the code still works, but with lower efficiency. (it is unlikely that a translation would have
|
||||
* more than 32 bindings.)
|
||||
*
|
||||
* These OpCodes can be used by both the i18n block as well as ICU sub-block.
|
||||
*
|
||||
* ## Example
|
||||
@ -220,8 +247,8 @@ export const enum I18nUpdateOpCode {
|
||||
* // The following OpCodes represent: `<div i18n-title="pre{{exp1}}in{{exp2}}post">`
|
||||
* // If `changeMask & 0b11`
|
||||
* // has changed then execute update OpCodes.
|
||||
* // has NOT changed then skip `7` values and start processing next OpCodes.
|
||||
* 0b11, 7,
|
||||
* // has NOT changed then skip `8` values and start processing next OpCodes.
|
||||
* 0b11, 8,
|
||||
* // Concatenate `newValue = 'pre'+lView[bindIndex-4]+'in'+lView[bindIndex-3]+'post';`.
|
||||
* 'pre', -4, 'in', -3, 'post',
|
||||
* // Update attribute: `elementAttribute(1, 'title', sanitizerFn(newValue));`
|
||||
@ -240,8 +267,8 @@ export const enum I18nUpdateOpCode {
|
||||
* // The following OpCodes represent: `<div i18n>{exp4, plural, ... }">`
|
||||
* // If `changeMask & 0b1000`
|
||||
* // has changed then execute update OpCodes.
|
||||
* // has NOT changed then skip `4` values and start processing next OpCodes.
|
||||
* 0b1000, 4,
|
||||
* // has NOT changed then skip `2` values and start processing next OpCodes.
|
||||
* 0b1000, 2,
|
||||
* // Concatenate `newValue = lView[bindIndex -1];`.
|
||||
* -1,
|
||||
* // Switch ICU: `icuSwitchCase(lView[1], 0, newValue);`
|
||||
@ -256,7 +283,7 @@ export const enum I18nUpdateOpCode {
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
export interface I18nUpdateOpCodes extends Array<string|number|SanitizerFn|null> {}
|
||||
export interface I18nUpdateOpCodes extends Array<string|number|SanitizerFn|null>, I18nDebug {}
|
||||
|
||||
/**
|
||||
* Store information for the i18n translation block.
|
||||
|
@ -7,14 +7,11 @@
|
||||
*/
|
||||
import {KeyValueArray} from '../../util/array_utils';
|
||||
import {TStylingRange} from '../interfaces/styling';
|
||||
|
||||
import {DirectiveDef} from './definition';
|
||||
import {CssSelector} from './projection';
|
||||
import {RNode} from './renderer';
|
||||
import {LView, TView} from './view';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* TNodeType corresponds to the {@link TNode} `type` property.
|
||||
*/
|
||||
@ -45,6 +42,20 @@ export const enum TNodeType {
|
||||
IcuContainer = 5,
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts `TNodeType` into human readable text.
|
||||
* Make sure this matches with `TNodeType`
|
||||
*/
|
||||
export const TNodeTypeAsString = [
|
||||
'Container', // 0
|
||||
'Projection', // 1
|
||||
'View', // 2
|
||||
'Element', // 3
|
||||
'ElementContainer', // 4
|
||||
'IcuContainer' // 5
|
||||
] as const;
|
||||
|
||||
|
||||
/**
|
||||
* Corresponds to the TNode.flags property.
|
||||
*/
|
||||
@ -701,7 +712,9 @@ export interface TIcuContainerNode extends TNode {
|
||||
/**
|
||||
* Indicates the current active case for an ICU expression.
|
||||
* It is null when there is no active case.
|
||||
*
|
||||
*/
|
||||
// FIXME(misko): This is at a wrong location as activeCase is `LView` (not `TView`) concern
|
||||
activeCaseIndex: number|null;
|
||||
}
|
||||
|
||||
|
@ -15,10 +15,10 @@ import {Sanitizer} from '../../sanitization/sanitizer';
|
||||
import {LContainer} from './container';
|
||||
import {ComponentDef, ComponentTemplate, DirectiveDef, DirectiveDefList, HostBindingsFunction, PipeDef, PipeDefList, ViewQueriesFunction} from './definition';
|
||||
import {I18nUpdateOpCodes, TI18n} from './i18n';
|
||||
import {TConstants, TElementNode, TNode, TViewNode} from './node';
|
||||
import {TConstants, TElementNode, TNode, TNodeTypeAsString, TViewNode} from './node';
|
||||
import {PlayerHandler} from './player';
|
||||
import {LQueries, TQueries} from './query';
|
||||
import {RElement, Renderer3, RendererFactory3} from './renderer';
|
||||
import {RComment, RElement, Renderer3, RendererFactory3} from './renderer';
|
||||
import {TStylingKey, TStylingRange} from './styling';
|
||||
|
||||
|
||||
@ -69,6 +69,15 @@ export interface OpaqueViewState {
|
||||
* don't have to edit the data array based on which views are present.
|
||||
*/
|
||||
export interface LView extends Array<any> {
|
||||
/**
|
||||
* Human readable representation of the `LView`.
|
||||
*
|
||||
* NOTE: This property only exists if `ngDevMode` is set to `true` and it is not present in
|
||||
* production. Its presence is purely to help debug issue in development, and should not be relied
|
||||
* on in production application.
|
||||
*/
|
||||
debug?: LViewDebug;
|
||||
|
||||
/**
|
||||
* The host node for this LView instance, if this is a component view.
|
||||
* If this is an embedded view, HOST will be null.
|
||||
@ -826,3 +835,190 @@ export type TData =
|
||||
// Note: This hack is necessary so we don't erroneously get a circular dependency
|
||||
// failure based on types.
|
||||
export const unusedValueExportToPlacateAjd = 1;
|
||||
|
||||
/**
|
||||
* Human readable version of the `LView`.
|
||||
*
|
||||
* `LView` is a data structure used internally to keep track of views. The `LView` is designed for
|
||||
* efficiency and so at times it is difficult to read or write tests which assert on its values. For
|
||||
* this reason when `ngDevMode` is true we patch a `LView.debug` property which points to
|
||||
* `LViewDebug` for easier debugging and test writing. It is the intent of `LViewDebug` to be used
|
||||
* in tests.
|
||||
*/
|
||||
export interface LViewDebug {
|
||||
/**
|
||||
* Flags associated with the `LView` unpacked into a more readable state.
|
||||
*
|
||||
* See `LViewFlags` for the flag meanings.
|
||||
*/
|
||||
readonly flags: {
|
||||
initPhaseState: number,
|
||||
creationMode: boolean,
|
||||
firstViewPass: boolean,
|
||||
checkAlways: boolean,
|
||||
dirty: boolean,
|
||||
attached: boolean,
|
||||
destroyed: boolean,
|
||||
isRoot: boolean,
|
||||
indexWithinInitPhase: number,
|
||||
};
|
||||
|
||||
/**
|
||||
* Parent view (or container)
|
||||
*/
|
||||
readonly parent: LViewDebug|LContainerDebug|null;
|
||||
|
||||
/**
|
||||
* Next sibling to the `LView`.
|
||||
*/
|
||||
readonly next: LViewDebug|LContainerDebug|null;
|
||||
|
||||
/**
|
||||
* The context used for evaluation of the `LView`
|
||||
*
|
||||
* (Usually the component)
|
||||
*/
|
||||
readonly context: {}|null;
|
||||
|
||||
/**
|
||||
* Hierarchical tree of nodes.
|
||||
*/
|
||||
readonly nodes: DebugNode[];
|
||||
|
||||
/**
|
||||
* HTML representation of the `LView`.
|
||||
*
|
||||
* This is only approximate to actual HTML as child `LView`s are removed.
|
||||
*/
|
||||
readonly html: string;
|
||||
|
||||
/**
|
||||
* The host element to which this `LView` is attached.
|
||||
*/
|
||||
readonly hostHTML: string|null;
|
||||
|
||||
/**
|
||||
* Child `LView`s
|
||||
*/
|
||||
readonly childViews: Array<LViewDebug|LContainerDebug>;
|
||||
|
||||
/**
|
||||
* Sub range of `LView` containing decls (DOM elements).
|
||||
*/
|
||||
readonly decls: LViewDebugRange;
|
||||
|
||||
/**
|
||||
* Sub range of `LView` containing vars (bindings).
|
||||
*/
|
||||
readonly vars: LViewDebugRange;
|
||||
|
||||
/**
|
||||
* Sub range of `LView` containing i18n (translated DOM elements).
|
||||
*/
|
||||
readonly i18n: LViewDebugRange;
|
||||
|
||||
/**
|
||||
* Sub range of `LView` containing expando (used by DI).
|
||||
*/
|
||||
readonly expando: LViewDebugRange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Human readable version of the `LContainer`
|
||||
*
|
||||
* `LContainer` is a data structure used internally to keep track of child views. The `LContainer`
|
||||
* is designed for efficiency and so at times it is difficult to read or write tests which assert on
|
||||
* its values. For this reason when `ngDevMode` is true we patch a `LContainer.debug` property which
|
||||
* points to `LContainerDebug` for easier debugging and test writing. It is the intent of
|
||||
* `LContainerDebug` to be used in tests.
|
||||
*/
|
||||
export interface LContainerDebug {
|
||||
readonly native: RComment;
|
||||
/**
|
||||
* Child `LView`s.
|
||||
*/
|
||||
readonly views: LViewDebug[];
|
||||
readonly parent: LViewDebug|null;
|
||||
readonly movedViews: LView[]|null;
|
||||
readonly host: RElement|RComment|LView;
|
||||
readonly next: LViewDebug|LContainerDebug|null;
|
||||
readonly hasTransplantedViews: boolean;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* `LView` is subdivided to ranges where the actual data is stored. Some of these ranges such as
|
||||
* `decls` and `vars` are known at compile time. Other such as `i18n` and `expando` are runtime only
|
||||
* concepts.
|
||||
*/
|
||||
export interface LViewDebugRange {
|
||||
/**
|
||||
* The starting index in `LView` where the range begins. (Inclusive)
|
||||
*/
|
||||
start: number;
|
||||
|
||||
/**
|
||||
* The ending index in `LView` where the range ends. (Exclusive)
|
||||
*/
|
||||
end: number;
|
||||
|
||||
/**
|
||||
* The length of the range
|
||||
*/
|
||||
length: number;
|
||||
|
||||
/**
|
||||
* The merged content of the range. `t` contains data from `TView.data` and `l` contains `LView`
|
||||
* data at an index.
|
||||
*/
|
||||
content: LViewDebugRangeContent[];
|
||||
}
|
||||
|
||||
/**
|
||||
* For convenience the static and instance portions of `TView` and `LView` are merged into a single
|
||||
* object in `LViewRange`.
|
||||
*/
|
||||
export interface LViewDebugRangeContent {
|
||||
/**
|
||||
* Index into original `LView` or `TView.data`.
|
||||
*/
|
||||
index: number;
|
||||
|
||||
/**
|
||||
* Value from the `TView.data[index]` location.
|
||||
*/
|
||||
t: any;
|
||||
|
||||
/**
|
||||
* Value from the `LView[index]` location.
|
||||
*/
|
||||
l: any;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A logical node which comprise into `LView`s.
|
||||
*
|
||||
*/
|
||||
export interface DebugNode {
|
||||
/**
|
||||
* HTML representation of the node.
|
||||
*/
|
||||
html: string|null;
|
||||
|
||||
/**
|
||||
* Human readable node type.
|
||||
*/
|
||||
type: typeof TNodeTypeAsString[number];
|
||||
|
||||
/**
|
||||
* DOM native node.
|
||||
*/
|
||||
native: Node;
|
||||
|
||||
/**
|
||||
* Child nodes
|
||||
*/
|
||||
children: DebugNode[];
|
||||
}
|
@ -5,6 +5,37 @@
|
||||
* 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
|
||||
*/
|
||||
export function attachDebugObject(obj: any, debug: any) {
|
||||
Object.defineProperty(obj, 'debug', {value: debug, enumerable: false});
|
||||
|
||||
/**
|
||||
* Patch a `debug` property on top of the existing object.
|
||||
*
|
||||
* NOTE: always call this method with `ngDevMode && attachDebugObject(...)`
|
||||
*
|
||||
* @param obj Object to patch
|
||||
* @param debug Value to patch
|
||||
*/
|
||||
export function attachDebugObject(obj: any, debug: any): void {
|
||||
if (ngDevMode) {
|
||||
Object.defineProperty(obj, 'debug', {value: debug, enumerable: false});
|
||||
} else {
|
||||
throw new Error(
|
||||
'This method should be guarded with `ngDevMode` so that it can be tree shaken in production!');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Patch a `debug` property getter on top of the existing object.
|
||||
*
|
||||
* NOTE: always call this method with `ngDevMode && attachDebugObject(...)`
|
||||
*
|
||||
* @param obj Object to patch
|
||||
* @param debugGetter Getter returning a value to patch
|
||||
*/
|
||||
export function attachDebugGetter(obj: any, debugGetter: () => any): void {
|
||||
if (ngDevMode) {
|
||||
Object.defineProperty(obj, 'debug', {get: debugGetter, enumerable: false});
|
||||
} else {
|
||||
throw new Error(
|
||||
'This method should be guarded with `ngDevMode` so that it can be tree shaken in production!');
|
||||
}
|
||||
}
|
||||
|
@ -10,12 +10,12 @@ import {Injector} from '../../di/injector';
|
||||
import {assertLView} from '../assert';
|
||||
import {discoverLocalRefs, getComponentAtNodeIndex, getDirectivesAtNodeIndex, getLContext} from '../context_discovery';
|
||||
import {NodeInjector} from '../di';
|
||||
import {buildDebugNode, DebugNode} from '../instructions/lview_debug';
|
||||
import {buildDebugNode} from '../instructions/lview_debug';
|
||||
import {LContext} from '../interfaces/context';
|
||||
import {DirectiveDef} from '../interfaces/definition';
|
||||
import {TElementNode, TNode, TNodeProviderIndexes} from '../interfaces/node';
|
||||
import {isLView} from '../interfaces/type_checks';
|
||||
import {CLEANUP, CONTEXT, FLAGS, HEADER_OFFSET, HOST, LView, LViewFlags, T_HOST, TVIEW} from '../interfaces/view';
|
||||
import {CLEANUP, CONTEXT, DebugNode, FLAGS, HEADER_OFFSET, HOST, LView, LViewFlags, T_HOST, TVIEW} from '../interfaces/view';
|
||||
|
||||
import {stringifyForError} from './misc_utils';
|
||||
import {getLViewParent, getRootContext} from './view_traversal_utils';
|
||||
|
@ -20,6 +20,7 @@ ts_library(
|
||||
"//packages/compiler/testing",
|
||||
"//packages/core",
|
||||
"//packages/core/src/util",
|
||||
"//packages/core/test/render3:matchers",
|
||||
"//packages/core/testing",
|
||||
"//packages/localize",
|
||||
"//packages/localize/init",
|
||||
|
@ -8,13 +8,17 @@
|
||||
|
||||
import {Component} from '@angular/core';
|
||||
import {getLContext} from '@angular/core/src/render3/context_discovery';
|
||||
import {LViewDebug, toDebug} from '@angular/core/src/render3/instructions/lview_debug';
|
||||
import {LViewDebug} from '@angular/core/src/render3/instructions/lview_debug';
|
||||
import {TNodeType} from '@angular/core/src/render3/interfaces/node';
|
||||
import {HEADER_OFFSET} from '@angular/core/src/render3/interfaces/view';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
||||
import {onlyInIvy} from '@angular/private/testing';
|
||||
|
||||
describe('Debug Representation', () => {
|
||||
onlyInIvy('Ivy specific').it('should generate a human readable version', () => {
|
||||
import {matchDomElement, matchDomText, matchTI18n, matchTNode} from '../render3/matchers';
|
||||
|
||||
onlyInIvy('Ivy specific').describe('Debug Representation', () => {
|
||||
it('should generate a human readable version', () => {
|
||||
@Component({selector: 'my-comp', template: '<div id="123">Hello World</div>'})
|
||||
class MyComponent {
|
||||
}
|
||||
@ -23,11 +27,56 @@ describe('Debug Representation', () => {
|
||||
const fixture = TestBed.createComponent(MyComponent);
|
||||
fixture.detectChanges();
|
||||
|
||||
const hostView = toDebug(getLContext(fixture.componentInstance)!.lView);
|
||||
expect(hostView.host).toEqual(null);
|
||||
const hostView = getLContext(fixture.componentInstance)!.lView.debug!;
|
||||
expect(hostView.hostHTML).toEqual(null);
|
||||
const myCompView = hostView.childViews[0] as LViewDebug;
|
||||
expect(myCompView.host).toContain('<div id="123">Hello World</div>');
|
||||
expect(myCompView.hostHTML).toContain('<div id="123">Hello World</div>');
|
||||
expect(myCompView.nodes![0].html).toEqual('<div id="123">');
|
||||
expect(myCompView.nodes![0].nodes![0].html).toEqual('Hello World');
|
||||
expect(myCompView.nodes![0].children![0].html).toEqual('Hello World');
|
||||
});
|
||||
|
||||
describe('LViewDebug', () => {
|
||||
describe('range', () => {
|
||||
it('should show ranges', () => {
|
||||
@Component({selector: 'my-comp', template: '<div i18n>Hello {{name}}</div>'})
|
||||
class MyComponent {
|
||||
name = 'World';
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [MyComponent]});
|
||||
const fixture = TestBed.createComponent(MyComponent);
|
||||
fixture.detectChanges();
|
||||
|
||||
const hostView = getLContext(fixture.componentInstance)!.lView.debug!;
|
||||
const myComponentView = hostView.childViews[0] as LViewDebug;
|
||||
expect(myComponentView.decls).toEqual({
|
||||
start: HEADER_OFFSET,
|
||||
end: HEADER_OFFSET + 2,
|
||||
length: 2,
|
||||
content: [
|
||||
{index: HEADER_OFFSET + 0, t: matchTNode({tagName: 'div'}), l: matchDomElement('div')},
|
||||
{index: HEADER_OFFSET + 1, t: matchTI18n(), l: null},
|
||||
]
|
||||
});
|
||||
expect(myComponentView.vars).toEqual({
|
||||
start: HEADER_OFFSET + 2,
|
||||
end: HEADER_OFFSET + 3,
|
||||
length: 1,
|
||||
content: [{index: HEADER_OFFSET + 2, t: null, l: 'World'}]
|
||||
});
|
||||
expect(myComponentView.i18n).toEqual({
|
||||
start: HEADER_OFFSET + 3,
|
||||
end: HEADER_OFFSET + 4,
|
||||
length: 1,
|
||||
content: [{
|
||||
index: HEADER_OFFSET + 3,
|
||||
t: matchTNode({type: TNodeType.Element, tagName: null}),
|
||||
l: matchDomText('Hello World')
|
||||
}]
|
||||
});
|
||||
expect(myComponentView.expando)
|
||||
.toEqual({start: HEADER_OFFSET + 4, end: HEADER_OFFSET + 4, length: 0, content: []});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -701,9 +701,6 @@
|
||||
{
|
||||
"name": "_keyMap"
|
||||
},
|
||||
{
|
||||
"name": "_mergeErrors"
|
||||
},
|
||||
{
|
||||
"name": "_noControlError"
|
||||
},
|
||||
@ -914,6 +911,9 @@
|
||||
{
|
||||
"name": "executeTemplate"
|
||||
},
|
||||
{
|
||||
"name": "executeValidators"
|
||||
},
|
||||
{
|
||||
"name": "executeViewQueryFn"
|
||||
},
|
||||
@ -1346,6 +1346,9 @@
|
||||
{
|
||||
"name": "mergeAll"
|
||||
},
|
||||
{
|
||||
"name": "mergeErrors"
|
||||
},
|
||||
{
|
||||
"name": "mergeHostAttribute"
|
||||
},
|
||||
@ -1401,10 +1404,7 @@
|
||||
"name": "noop"
|
||||
},
|
||||
{
|
||||
"name": "normalizeAsyncValidator"
|
||||
},
|
||||
{
|
||||
"name": "normalizeValidator"
|
||||
"name": "normalizeValidators"
|
||||
},
|
||||
{
|
||||
"name": "observable"
|
||||
|
@ -11,10 +11,13 @@ ts_library(
|
||||
"**/*_perf.ts",
|
||||
"domino.d.ts",
|
||||
"load_domino.ts",
|
||||
"is_shape_of.ts",
|
||||
"jit_spec.ts",
|
||||
"matchers.ts",
|
||||
],
|
||||
),
|
||||
deps = [
|
||||
":matchers",
|
||||
"//packages:types",
|
||||
"//packages/animations",
|
||||
"//packages/animations/browser",
|
||||
@ -34,6 +37,18 @@ ts_library(
|
||||
],
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "matchers",
|
||||
testonly = True,
|
||||
srcs = [
|
||||
"is_shape_of.ts",
|
||||
"matchers.ts",
|
||||
],
|
||||
deps = [
|
||||
"//packages/core",
|
||||
],
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "domino",
|
||||
testonly = True,
|
||||
|
140
packages/core/test/render3/i18n_debug_spec.ts
Normal file
140
packages/core/test/render3/i18n_debug_spec.ts
Normal file
@ -0,0 +1,140 @@
|
||||
/**
|
||||
* @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 {i18nMutateOpCodesToString, i18nUpdateOpCodesToString} from '@angular/core/src/render3/i18n_debug';
|
||||
import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nUpdateOpCode} from '@angular/core/src/render3/interfaces/i18n';
|
||||
|
||||
describe('i18n debug', () => {
|
||||
describe('i18nUpdateOpCodesToString', () => {
|
||||
it('should print nothing', () => {
|
||||
expect(i18nUpdateOpCodesToString([])).toEqual([]);
|
||||
});
|
||||
|
||||
it('should print text opCode', () => {
|
||||
expect(i18nUpdateOpCodesToString([
|
||||
0b11,
|
||||
4,
|
||||
'pre ',
|
||||
-4,
|
||||
' post',
|
||||
1 << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Text,
|
||||
]))
|
||||
.toEqual(
|
||||
['if (mask & 0b11) { (lView[1] as Text).textContent = `pre ${lView[4]} post`; }']);
|
||||
});
|
||||
|
||||
it('should print Attribute opCode', () => {
|
||||
expect(i18nUpdateOpCodesToString([
|
||||
0b01, 8,
|
||||
'pre ', -4,
|
||||
' in ', -3,
|
||||
' post', 1 << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Attr,
|
||||
'title', null,
|
||||
0b10, 8,
|
||||
'pre ', -4,
|
||||
' in ', -3,
|
||||
' post', 1 << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Attr,
|
||||
'title', (v) => v,
|
||||
]))
|
||||
.toEqual([
|
||||
'if (mask & 0b1) { (lView[1] as Element).setAttribute(\'title\', `pre ${lView[4]} in ${lView[3]} post`); }',
|
||||
'if (mask & 0b10) { (lView[1] as Element).setAttribute(\'title\', (function (v) { return v; })(`pre ${lView[4]} in ${lView[3]} post`)); }'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should print icuSwitch opCode', () => {
|
||||
expect(i18nUpdateOpCodesToString([
|
||||
0b100, 2, -5, 12 << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.IcuSwitch,
|
||||
2 // FIXME(misko): Should be part of IcuSwitch
|
||||
])).toEqual(['if (mask & 0b100) { icuSwitchCase(lView[12] as Comment, 2, `${lView[5]}`); }']);
|
||||
});
|
||||
|
||||
it('should print icuUpdate opCode', () => {
|
||||
expect(i18nUpdateOpCodesToString([
|
||||
0b1000, 2, 13 << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.IcuUpdate,
|
||||
3 // FIXME(misko): should be part of IcuUpdate
|
||||
])).toEqual(['if (mask & 0b1000) { icuUpdateCase(lView[13] as Comment, 3); }']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('i18nMutateOpCodesToString', () => {
|
||||
it('should print nothing', () => {
|
||||
expect(i18nMutateOpCodesToString([])).toEqual([]);
|
||||
});
|
||||
|
||||
it('should print Move', () => {
|
||||
expect(i18nMutateOpCodesToString([
|
||||
1 << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select,
|
||||
2 << I18nMutateOpCode.SHIFT_PARENT | 0 << I18nMutateOpCode.SHIFT_REF |
|
||||
I18nMutateOpCode.AppendChild,
|
||||
])).toEqual(['(lView[2] as Element).appendChild(lView[1])']);
|
||||
});
|
||||
|
||||
it('should print text AppendChild', () => {
|
||||
expect(i18nMutateOpCodesToString([
|
||||
'xyz', 0,
|
||||
1 << I18nMutateOpCode.SHIFT_PARENT | 0 << I18nMutateOpCode.SHIFT_REF |
|
||||
I18nMutateOpCode.AppendChild
|
||||
]))
|
||||
.toEqual([
|
||||
'lView[0] = document.createTextNode("xyz")',
|
||||
'(lView[1] as Element).appendChild(lView[0])'
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
it('should print element AppendChild', () => {
|
||||
expect(i18nMutateOpCodesToString([
|
||||
ELEMENT_MARKER, 'xyz', 0,
|
||||
1 << I18nMutateOpCode.SHIFT_PARENT | 0 << I18nMutateOpCode.SHIFT_REF |
|
||||
I18nMutateOpCode.AppendChild
|
||||
]))
|
||||
.toEqual([
|
||||
'lView[0] = document.createElement("xyz")',
|
||||
'(lView[1] as Element).appendChild(lView[0])'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should print comment AppendChild', () => {
|
||||
expect(i18nMutateOpCodesToString([
|
||||
COMMENT_MARKER, 'xyz', 0,
|
||||
1 << I18nMutateOpCode.SHIFT_PARENT | 0 << I18nMutateOpCode.SHIFT_REF |
|
||||
I18nMutateOpCode.AppendChild
|
||||
]))
|
||||
.toEqual([
|
||||
'lView[0] = document.createComment("xyz")',
|
||||
'(lView[1] as Element).appendChild(lView[0])'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should print Remove', () => {
|
||||
expect(i18nMutateOpCodesToString([
|
||||
2 << I18nMutateOpCode.SHIFT_PARENT | 0 << I18nMutateOpCode.SHIFT_REF |
|
||||
I18nMutateOpCode.Remove
|
||||
])).toEqual(['(lView[2] as Element).remove(lView[0])']);
|
||||
});
|
||||
|
||||
it('should print Attr', () => {
|
||||
expect(i18nMutateOpCodesToString([
|
||||
1 << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Attr, 'attr', 'value'
|
||||
])).toEqual(['(lView[1] as Element).setAttribute("attr", "value")']);
|
||||
});
|
||||
|
||||
it('should print ElementEnd', () => {
|
||||
expect(i18nMutateOpCodesToString([
|
||||
1 << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.ElementEnd,
|
||||
])).toEqual(['setPreviousOrParentTNode(tView.data[1] as TNode)']);
|
||||
});
|
||||
|
||||
it('should print RemoveNestedIcu', () => {
|
||||
expect(i18nMutateOpCodesToString([
|
||||
1 << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.RemoveNestedIcu,
|
||||
])).toEqual(['removeNestedICU(1)']);
|
||||
});
|
||||
});
|
||||
});
|
@ -9,10 +9,11 @@
|
||||
import {noop} from '../../../compiler/src/render3/view/util';
|
||||
import {getTranslationForTemplate, ɵɵi18nAttributes, ɵɵi18nPostprocess, ɵɵi18nStart} from '../../src/render3/i18n';
|
||||
import {setDelayProjection, ɵɵelementEnd, ɵɵelementStart} from '../../src/render3/instructions/all';
|
||||
import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nUpdateOpCode, I18nUpdateOpCodes, TI18n} from '../../src/render3/interfaces/i18n';
|
||||
import {I18nUpdateOpCodes, TI18n, TIcu} from '../../src/render3/interfaces/i18n';
|
||||
import {HEADER_OFFSET, LView, TVIEW} from '../../src/render3/interfaces/view';
|
||||
import {getNativeByIndex} from '../../src/render3/util/view_utils';
|
||||
import {TemplateFixture} from './render_util';
|
||||
import {debugMatch} from './utils';
|
||||
|
||||
describe('Runtime i18n', () => {
|
||||
afterEach(() => {
|
||||
@ -72,25 +73,15 @@ describe('Runtime i18n', () => {
|
||||
const nbConsts = 1;
|
||||
const index = 0;
|
||||
const opCodes = getOpCodes(() => {
|
||||
ɵɵi18nStart(index, MSG_DIV);
|
||||
}, null, nbConsts, index);
|
||||
|
||||
// Check debug
|
||||
const debugOps = (opCodes as any).create.debug!.operations;
|
||||
expect(debugOps[0].__raw_opCode).toBe('simple text');
|
||||
expect(debugOps[0].type).toBe('Create Text Node');
|
||||
expect(debugOps[0].nodeIndex).toBe(1);
|
||||
expect(debugOps[0].text).toBe('simple text');
|
||||
expect(debugOps[1].__raw_opCode).toBe(1);
|
||||
expect(debugOps[1].type).toBe('AppendChild');
|
||||
expect(debugOps[1].nodeIndex).toBe(0);
|
||||
ɵɵi18nStart(index, MSG_DIV);
|
||||
}, null, nbConsts, index) as TI18n;
|
||||
|
||||
expect(opCodes).toEqual({
|
||||
vars: 1,
|
||||
create: [
|
||||
'simple text', nbConsts,
|
||||
index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild
|
||||
],
|
||||
create: debugMatch([
|
||||
'lView[1] = document.createTextNode("simple text")',
|
||||
'(lView[0] as Element).appendChild(lView[1])'
|
||||
]),
|
||||
update: [],
|
||||
icus: null
|
||||
});
|
||||
@ -102,37 +93,28 @@ describe('Runtime i18n', () => {
|
||||
// 3 consts for the 2 divs and 1 span + 1 const for `i18nStart` = 4 consts
|
||||
const nbConsts = 4;
|
||||
const index = 1;
|
||||
const elementIndex = 2;
|
||||
const elementIndex2 = 3;
|
||||
const opCodes = getOpCodes(() => {
|
||||
ɵɵi18nStart(index, MSG_DIV);
|
||||
}, null, nbConsts, index);
|
||||
|
||||
expect(opCodes).toEqual({
|
||||
vars: 5,
|
||||
create: [
|
||||
'Hello ',
|
||||
nbConsts,
|
||||
index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||||
elementIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select,
|
||||
index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||||
'world',
|
||||
nbConsts + 1,
|
||||
elementIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||||
elementIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.ElementEnd,
|
||||
' and ',
|
||||
nbConsts + 2,
|
||||
index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||||
elementIndex2 << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select,
|
||||
index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||||
'universe',
|
||||
nbConsts + 3,
|
||||
elementIndex2 << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||||
elementIndex2 << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.ElementEnd,
|
||||
'!',
|
||||
nbConsts + 4,
|
||||
index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||||
],
|
||||
create: debugMatch([
|
||||
'lView[4] = document.createTextNode("Hello ")',
|
||||
'(lView[1] as Element).appendChild(lView[4])',
|
||||
'(lView[1] as Element).appendChild(lView[2])',
|
||||
'lView[5] = document.createTextNode("world")',
|
||||
'(lView[2] as Element).appendChild(lView[5])',
|
||||
'setPreviousOrParentTNode(tView.data[2] as TNode)',
|
||||
'lView[6] = document.createTextNode(" and ")',
|
||||
'(lView[1] as Element).appendChild(lView[6])',
|
||||
'(lView[1] as Element).appendChild(lView[3])',
|
||||
'lView[7] = document.createTextNode("universe")',
|
||||
'(lView[3] as Element).appendChild(lView[7])',
|
||||
'setPreviousOrParentTNode(tView.data[3] as TNode)',
|
||||
'lView[8] = document.createTextNode("!")',
|
||||
'(lView[1] as Element).appendChild(lView[8])',
|
||||
]),
|
||||
update: [],
|
||||
icus: null
|
||||
});
|
||||
@ -146,21 +128,18 @@ describe('Runtime i18n', () => {
|
||||
ɵɵi18nStart(index, MSG_DIV);
|
||||
}, null, nbConsts, index);
|
||||
|
||||
expect((opCodes as any).update.debug.operations).toEqual([
|
||||
{__raw_opCode: 8, checkBit: 1, type: 'Text', nodeIndex: 2, text: 'Hello <20>0<EFBFBD>!'}
|
||||
expect((opCodes as any).update.debug).toEqual([
|
||||
'if (mask & 0b1) { (lView[2] as Text).textContent = `Hello ${lView[1]}!`; }'
|
||||
]);
|
||||
|
||||
expect(opCodes).toEqual({
|
||||
vars: 1,
|
||||
create:
|
||||
['', nbConsts, index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild],
|
||||
update: [
|
||||
0b1, // bindings mask
|
||||
4, // if no update, skip 4
|
||||
'Hello ',
|
||||
-1, // binding index
|
||||
'!', (index + 1) << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Text
|
||||
],
|
||||
create: debugMatch([
|
||||
'lView[2] = document.createTextNode("")',
|
||||
'(lView[1] as Element).appendChild(lView[2])',
|
||||
]),
|
||||
update: debugMatch(
|
||||
['if (mask & 0b1) { (lView[2] as Text).textContent = `Hello ${lView[1]}!`; }']),
|
||||
icus: null
|
||||
});
|
||||
});
|
||||
@ -175,14 +154,12 @@ describe('Runtime i18n', () => {
|
||||
|
||||
expect(opCodes).toEqual({
|
||||
vars: 1,
|
||||
create:
|
||||
['', nbConsts, index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild],
|
||||
update: [
|
||||
0b11, // bindings mask
|
||||
8, // if no update, skip 8
|
||||
'Hello ', -1, ' and ', -2, ', again ', -1, '!',
|
||||
(index + 1) << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Text
|
||||
],
|
||||
create: debugMatch([
|
||||
'lView[2] = document.createTextNode("")', '(lView[1] as Element).appendChild(lView[2])'
|
||||
]),
|
||||
update: debugMatch([
|
||||
'if (mask & 0b11) { (lView[2] as Text).textContent = `Hello ${lView[1]} and ${lView[2]}, again ${lView[1]}!`; }'
|
||||
]),
|
||||
icus: null
|
||||
});
|
||||
});
|
||||
@ -211,22 +188,14 @@ describe('Runtime i18n', () => {
|
||||
|
||||
expect(opCodes).toEqual({
|
||||
vars: 2,
|
||||
create: [
|
||||
'',
|
||||
nbConsts,
|
||||
index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||||
~rootTemplate << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select,
|
||||
index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||||
'!',
|
||||
nbConsts + 1,
|
||||
index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||||
],
|
||||
update: [
|
||||
0b1, // bindings mask
|
||||
3, // if no update, skip 3
|
||||
-1, // binding index
|
||||
' is rendered as: ', firstTextNode << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Text
|
||||
],
|
||||
create: debugMatch([
|
||||
'lView[3] = document.createTextNode("")', '(lView[1] as Element).appendChild(lView[3])',
|
||||
'(lView[1] as Element).appendChild(lView[16381])',
|
||||
'lView[4] = document.createTextNode("!")', '(lView[1] as Element).appendChild(lView[4])'
|
||||
]),
|
||||
update: debugMatch([
|
||||
'if (mask & 0b1) { (lView[3] as Text).textContent = `${lView[1]} is rendered as: `; }'
|
||||
]),
|
||||
icus: null
|
||||
});
|
||||
|
||||
@ -243,19 +212,15 @@ describe('Runtime i18n', () => {
|
||||
|
||||
expect(opCodes).toEqual({
|
||||
vars: 2,
|
||||
create: [
|
||||
spanElement << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select,
|
||||
index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||||
'before',
|
||||
nbConsts,
|
||||
spanElement << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||||
~bElementSubTemplate << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select,
|
||||
spanElement << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||||
'after',
|
||||
nbConsts + 1,
|
||||
spanElement << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||||
spanElement << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.ElementEnd,
|
||||
],
|
||||
create: debugMatch([
|
||||
'(lView[0] as Element).appendChild(lView[1])',
|
||||
'lView[3] = document.createTextNode("before")',
|
||||
'(lView[1] as Element).appendChild(lView[3])',
|
||||
'(lView[1] as Element).appendChild(lView[16381])',
|
||||
'lView[4] = document.createTextNode("after")',
|
||||
'(lView[1] as Element).appendChild(lView[4])',
|
||||
'setPreviousOrParentTNode(tView.data[1] as TNode)'
|
||||
]),
|
||||
update: [],
|
||||
icus: null
|
||||
});
|
||||
@ -272,14 +237,12 @@ describe('Runtime i18n', () => {
|
||||
|
||||
expect(opCodes).toEqual({
|
||||
vars: 1,
|
||||
create: [
|
||||
bElement << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select,
|
||||
index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||||
'middle',
|
||||
nbConsts,
|
||||
bElement << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||||
bElement << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.ElementEnd,
|
||||
],
|
||||
create: debugMatch([
|
||||
'(lView[0] as Element).appendChild(lView[1])',
|
||||
'lView[2] = document.createTextNode("middle")',
|
||||
'(lView[1] as Element).appendChild(lView[2])',
|
||||
'setPreviousOrParentTNode(tView.data[1] as TNode)'
|
||||
]),
|
||||
update: [],
|
||||
icus: null
|
||||
});
|
||||
@ -294,179 +257,76 @@ describe('Runtime i18n', () => {
|
||||
const nbConsts = 1;
|
||||
const index = 0;
|
||||
const opCodes = getOpCodes(() => {
|
||||
ɵɵi18nStart(index, MSG_DIV);
|
||||
}, null, nbConsts, index);
|
||||
const tIcuIndex = 0;
|
||||
const icuCommentNodeIndex = index + 1;
|
||||
const firstTextNodeIndex = index + 2;
|
||||
const bElementNodeIndex = index + 3;
|
||||
const iElementNodeIndex = index + 3;
|
||||
const spanElementNodeIndex = index + 3;
|
||||
const innerTextNode = index + 4;
|
||||
const lastTextNode = index + 5;
|
||||
|
||||
const debugOps = (opCodes as any).update.debug.operations;
|
||||
expect(debugOps[0].__raw_opCode).toBe(6);
|
||||
expect(debugOps[0].checkBit).toBe(1);
|
||||
expect(debugOps[0].type).toBe('IcuSwitch');
|
||||
expect(debugOps[0].nodeIndex).toBe(1);
|
||||
expect(debugOps[0].tIcuIndex).toBe(0);
|
||||
expect(debugOps[0].mainBinding).toBe('<27>0<EFBFBD>');
|
||||
|
||||
expect(debugOps[1].__raw_opCode).toBe(7);
|
||||
expect(debugOps[1].checkBit).toBe(3);
|
||||
expect(debugOps[1].type).toBe('IcuUpdate');
|
||||
expect(debugOps[1].nodeIndex).toBe(1);
|
||||
expect(debugOps[1].tIcuIndex).toBe(0);
|
||||
|
||||
const icuDebugOps = (opCodes as any).icus[0].create[0].debug.operations;
|
||||
let op: any;
|
||||
let i = 0;
|
||||
|
||||
op = icuDebugOps[i++];
|
||||
expect(op.__raw_opCode).toBe('no ');
|
||||
expect(op.type).toBe('Create Text Node');
|
||||
expect(op.nodeIndex).toBe(2);
|
||||
expect(op.text).toBe('no ');
|
||||
|
||||
op = icuDebugOps[i++];
|
||||
expect(op.__raw_opCode).toBe(131073);
|
||||
expect(op.type).toBe('AppendChild');
|
||||
expect(op.nodeIndex).toBe(1);
|
||||
|
||||
op = icuDebugOps[i++];
|
||||
expect(op.__raw_opCode).toEqual({marker: 'element'});
|
||||
expect(op.type).toBe('ELEMENT_MARKER');
|
||||
|
||||
op = icuDebugOps[i++];
|
||||
expect(op.__raw_opCode).toBe('b');
|
||||
expect(op.type).toBe('Create Text Node');
|
||||
expect(op.nodeIndex).toBe(3);
|
||||
expect(op.text).toBe('b');
|
||||
|
||||
op = icuDebugOps[i++];
|
||||
expect(op.__raw_opCode).toBe(131073);
|
||||
expect(op.type).toBe('AppendChild');
|
||||
expect(op.nodeIndex).toBe(1);
|
||||
|
||||
op = icuDebugOps[i++];
|
||||
expect(op.__raw_opCode).toBe(28);
|
||||
expect(op.type).toBe('Attr');
|
||||
expect(op.nodeIndex).toBe(3);
|
||||
expect(op.attrName).toBe('title');
|
||||
expect(op.attrValue).toBe('none');
|
||||
|
||||
op = icuDebugOps[i++];
|
||||
expect(op.__raw_opCode).toBe('emails');
|
||||
expect(op.type).toBe('Create Text Node');
|
||||
expect(op.nodeIndex).toBe(4);
|
||||
expect(op.text).toBe('emails');
|
||||
|
||||
op = icuDebugOps[i++];
|
||||
expect(op.__raw_opCode).toBe(393217);
|
||||
expect(op.type).toBe('AppendChild');
|
||||
expect(op.nodeIndex).toBe(3);
|
||||
|
||||
op = icuDebugOps[i++];
|
||||
expect(op.__raw_opCode).toBe('!');
|
||||
expect(op.type).toBe('Create Text Node');
|
||||
expect(op.nodeIndex).toBe(5);
|
||||
expect(op.text).toBe('!');
|
||||
|
||||
op = icuDebugOps[i++];
|
||||
expect(op.__raw_opCode).toBe(131073);
|
||||
expect(op.type).toBe('AppendChild');
|
||||
expect(op.nodeIndex).toBe(1);
|
||||
ɵɵi18nStart(index, MSG_DIV);
|
||||
}, null, nbConsts, index) as TI18n;
|
||||
|
||||
expect(opCodes).toEqual({
|
||||
vars: 5,
|
||||
create: [
|
||||
COMMENT_MARKER, 'ICU 1', icuCommentNodeIndex,
|
||||
index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild
|
||||
],
|
||||
update: [
|
||||
0b1, // mask for ICU main binding
|
||||
3, // skip 3 if not changed
|
||||
-1, // icu main binding
|
||||
icuCommentNodeIndex << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.IcuSwitch, tIcuIndex,
|
||||
0b11, // mask for all ICU bindings
|
||||
2, // skip 2 if not changed
|
||||
icuCommentNodeIndex << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.IcuUpdate, tIcuIndex
|
||||
],
|
||||
icus: [{
|
||||
update: debugMatch([
|
||||
'if (mask & 0b1) { icuSwitchCase(lView[1] as Comment, 0, `${lView[1]}`); }',
|
||||
'if (mask & 0b11) { icuUpdateCase(lView[1] as Comment, 0); }',
|
||||
]),
|
||||
create: debugMatch([
|
||||
'lView[1] = document.createComment("ICU 1")',
|
||||
'(lView[0] as Element).appendChild(lView[1])',
|
||||
]),
|
||||
icus: [<TIcu>{
|
||||
type: 1,
|
||||
vars: [4, 3, 3],
|
||||
childIcus: [[], [], []],
|
||||
cases: ['0', '1', 'other'],
|
||||
create: [
|
||||
[
|
||||
'no ',
|
||||
firstTextNodeIndex,
|
||||
icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||||
ELEMENT_MARKER,
|
||||
'b',
|
||||
bElementNodeIndex,
|
||||
icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||||
bElementNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Attr,
|
||||
'title',
|
||||
'none',
|
||||
'emails',
|
||||
innerTextNode,
|
||||
bElementNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||||
'!',
|
||||
lastTextNode,
|
||||
icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||||
],
|
||||
[
|
||||
'one ', firstTextNodeIndex,
|
||||
icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||||
ELEMENT_MARKER, 'i', iElementNodeIndex,
|
||||
icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||||
'email', innerTextNode,
|
||||
iElementNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild
|
||||
],
|
||||
[
|
||||
'', firstTextNodeIndex,
|
||||
icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||||
ELEMENT_MARKER, 'span', spanElementNodeIndex,
|
||||
icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||||
'emails', innerTextNode,
|
||||
spanElementNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild
|
||||
]
|
||||
debugMatch([
|
||||
'lView[2] = document.createTextNode("no ")',
|
||||
'(lView[1] as Element).appendChild(lView[2])',
|
||||
'lView[3] = document.createElement("b")',
|
||||
'(lView[1] as Element).appendChild(lView[3])',
|
||||
'(lView[3] as Element).setAttribute("title", "none")',
|
||||
'lView[4] = document.createTextNode("emails")',
|
||||
'(lView[3] as Element).appendChild(lView[4])',
|
||||
'lView[5] = document.createTextNode("!")',
|
||||
'(lView[1] as Element).appendChild(lView[5])'
|
||||
]),
|
||||
debugMatch([
|
||||
'lView[2] = document.createTextNode("one ")',
|
||||
'(lView[1] as Element).appendChild(lView[2])',
|
||||
'lView[3] = document.createElement("i")',
|
||||
'(lView[1] as Element).appendChild(lView[3])',
|
||||
'lView[4] = document.createTextNode("email")',
|
||||
'(lView[3] as Element).appendChild(lView[4])'
|
||||
]),
|
||||
debugMatch([
|
||||
'lView[2] = document.createTextNode("")',
|
||||
'(lView[1] as Element).appendChild(lView[2])',
|
||||
'lView[3] = document.createElement("span")',
|
||||
'(lView[1] as Element).appendChild(lView[3])',
|
||||
'lView[4] = document.createTextNode("emails")',
|
||||
'(lView[3] as Element).appendChild(lView[4])'
|
||||
])
|
||||
],
|
||||
remove: [
|
||||
[
|
||||
firstTextNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove,
|
||||
innerTextNode << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove,
|
||||
bElementNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove,
|
||||
lastTextNode << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove,
|
||||
],
|
||||
[
|
||||
firstTextNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove,
|
||||
innerTextNode << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove,
|
||||
iElementNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove,
|
||||
],
|
||||
[
|
||||
firstTextNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove,
|
||||
innerTextNode << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove,
|
||||
spanElementNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove,
|
||||
]
|
||||
debugMatch([
|
||||
'(lView[0] as Element).remove(lView[2])',
|
||||
'(lView[0] as Element).remove(lView[4])',
|
||||
'(lView[0] as Element).remove(lView[3])',
|
||||
'(lView[0] as Element).remove(lView[5])',
|
||||
]),
|
||||
debugMatch([
|
||||
'(lView[0] as Element).remove(lView[2])',
|
||||
'(lView[0] as Element).remove(lView[4])',
|
||||
'(lView[0] as Element).remove(lView[3])',
|
||||
]),
|
||||
debugMatch([
|
||||
'(lView[0] as Element).remove(lView[2])',
|
||||
'(lView[0] as Element).remove(lView[4])',
|
||||
'(lView[0] as Element).remove(lView[3])',
|
||||
])
|
||||
],
|
||||
update: [
|
||||
[], [],
|
||||
[
|
||||
0b1, // mask for the first binding
|
||||
3, // skip 3 if not changed
|
||||
-1, // binding index
|
||||
' ', // text string to concatenate to the binding value
|
||||
firstTextNodeIndex << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Text,
|
||||
0b10, // mask for the title attribute binding
|
||||
4, // skip 4 if not changed
|
||||
-2, // binding index
|
||||
bElementNodeIndex << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Attr,
|
||||
'title', // attribute name
|
||||
null // sanitize function
|
||||
]
|
||||
debugMatch([]), debugMatch([]), debugMatch([
|
||||
'if (mask & 0b1) { (lView[2] as Text).textContent = `${lView[1]} `; }',
|
||||
'if (mask & 0b10) { (lView[3] as Element).setAttribute(\'title\', `${lView[2]}`); }'
|
||||
])
|
||||
]
|
||||
}]
|
||||
});
|
||||
@ -496,19 +356,14 @@ describe('Runtime i18n', () => {
|
||||
|
||||
expect(opCodes).toEqual({
|
||||
vars: 6,
|
||||
create: [
|
||||
COMMENT_MARKER, 'ICU 1', icuCommentNodeIndex,
|
||||
index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild
|
||||
],
|
||||
update: [
|
||||
0b1, // mask for ICU main binding
|
||||
3, // skip 3 if not changed
|
||||
-1, // icu main binding
|
||||
icuCommentNodeIndex << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.IcuSwitch, tIcuIndex,
|
||||
0b11, // mask for all ICU bindings
|
||||
2, // skip 2 if not changed
|
||||
icuCommentNodeIndex << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.IcuUpdate, tIcuIndex
|
||||
],
|
||||
create: debugMatch([
|
||||
'lView[1] = document.createComment("ICU 1")',
|
||||
'(lView[0] as Element).appendChild(lView[1])'
|
||||
]),
|
||||
update: debugMatch([
|
||||
'if (mask & 0b1) { icuSwitchCase(lView[1] as Comment, 1, `${lView[1]}`); }',
|
||||
'if (mask & 0b11) { icuUpdateCase(lView[1] as Comment, 1); }'
|
||||
]),
|
||||
icus: [
|
||||
{
|
||||
type: 0,
|
||||
@ -516,28 +371,29 @@ describe('Runtime i18n', () => {
|
||||
childIcus: [[], [], []],
|
||||
cases: ['cat', 'dog', 'other'],
|
||||
create: [
|
||||
[
|
||||
'cats', nestedTextNodeIndex,
|
||||
nestedIcuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT |
|
||||
I18nMutateOpCode.AppendChild
|
||||
],
|
||||
[
|
||||
'dogs', nestedTextNodeIndex,
|
||||
nestedIcuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT |
|
||||
I18nMutateOpCode.AppendChild
|
||||
],
|
||||
[
|
||||
'animals', nestedTextNodeIndex,
|
||||
nestedIcuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT |
|
||||
I18nMutateOpCode.AppendChild
|
||||
]
|
||||
debugMatch([
|
||||
'lView[5] = document.createTextNode("cats")',
|
||||
'(lView[3] as Element).appendChild(lView[5])'
|
||||
]),
|
||||
debugMatch([
|
||||
'lView[5] = document.createTextNode("dogs")',
|
||||
'(lView[3] as Element).appendChild(lView[5])'
|
||||
]),
|
||||
debugMatch([
|
||||
'lView[5] = document.createTextNode("animals")',
|
||||
'(lView[3] as Element).appendChild(lView[5])'
|
||||
]),
|
||||
],
|
||||
remove: [
|
||||
[nestedTextNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove],
|
||||
[nestedTextNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove],
|
||||
[nestedTextNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove]
|
||||
debugMatch(['(lView[0] as Element).remove(lView[5])']),
|
||||
debugMatch(['(lView[0] as Element).remove(lView[5])']),
|
||||
debugMatch(['(lView[0] as Element).remove(lView[5])'])
|
||||
],
|
||||
update: [[], [], []]
|
||||
update: [
|
||||
debugMatch([]),
|
||||
debugMatch([]),
|
||||
debugMatch([]),
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 1,
|
||||
@ -545,48 +401,33 @@ describe('Runtime i18n', () => {
|
||||
childIcus: [[], [0]],
|
||||
cases: ['0', 'other'],
|
||||
create: [
|
||||
[
|
||||
'zero', firstTextNodeIndex,
|
||||
icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild
|
||||
],
|
||||
[
|
||||
'', firstTextNodeIndex,
|
||||
icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||||
COMMENT_MARKER, 'nested ICU 0', nestedIcuCommentNodeIndex,
|
||||
icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||||
'!', lastTextNodeIndex,
|
||||
icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild
|
||||
]
|
||||
debugMatch([
|
||||
'lView[2] = document.createTextNode("zero")',
|
||||
'(lView[1] as Element).appendChild(lView[2])'
|
||||
]),
|
||||
debugMatch([
|
||||
'lView[2] = document.createTextNode("")',
|
||||
'(lView[1] as Element).appendChild(lView[2])',
|
||||
'lView[3] = document.createComment("nested ICU 0")',
|
||||
'(lView[1] as Element).appendChild(lView[3])',
|
||||
'lView[4] = document.createTextNode("!")',
|
||||
'(lView[1] as Element).appendChild(lView[4])'
|
||||
]),
|
||||
],
|
||||
remove: [
|
||||
[firstTextNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove],
|
||||
[
|
||||
firstTextNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove,
|
||||
lastTextNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove,
|
||||
0 << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.RemoveNestedIcu,
|
||||
nestedIcuCommentNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove,
|
||||
]
|
||||
debugMatch(['(lView[0] as Element).remove(lView[2])']),
|
||||
debugMatch([
|
||||
'(lView[0] as Element).remove(lView[2])', '(lView[0] as Element).remove(lView[4])',
|
||||
'removeNestedICU(0)', '(lView[0] as Element).remove(lView[3])'
|
||||
]),
|
||||
],
|
||||
update: [
|
||||
[],
|
||||
[
|
||||
0b1, // mask for ICU main binding
|
||||
3, // skip 3 if not changed
|
||||
-1, // binding index
|
||||
' ', // text string to concatenate to the binding value
|
||||
firstTextNodeIndex << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Text,
|
||||
0b10, // mask for inner ICU main binding
|
||||
3, // skip 3 if not changed
|
||||
-2, // inner ICU main binding
|
||||
nestedIcuCommentNodeIndex << I18nUpdateOpCode.SHIFT_REF |
|
||||
I18nUpdateOpCode.IcuSwitch,
|
||||
nestedTIcuIndex,
|
||||
0b10, // mask for all inner ICU bindings
|
||||
2, // skip 2 if not changed
|
||||
nestedIcuCommentNodeIndex << I18nUpdateOpCode.SHIFT_REF |
|
||||
I18nUpdateOpCode.IcuUpdate,
|
||||
nestedTIcuIndex
|
||||
]
|
||||
debugMatch([]),
|
||||
debugMatch([
|
||||
'if (mask & 0b1) { (lView[2] as Text).textContent = `${lView[1]} `; }',
|
||||
'if (mask & 0b10) { icuSwitchCase(lView[3] as Comment, 0, `${lView[2]}`); }',
|
||||
'if (mask & 0b10) { icuUpdateCase(lView[3] as Comment, 0); }'
|
||||
]),
|
||||
]
|
||||
}
|
||||
]
|
||||
@ -623,13 +464,9 @@ describe('Runtime i18n', () => {
|
||||
ɵɵi18nAttributes(index, MSG_div_attr);
|
||||
}, null, nbConsts, index);
|
||||
|
||||
expect(opCodes).toEqual([
|
||||
0b1, // bindings mask
|
||||
6, // if no update, skip 4
|
||||
'Hello ',
|
||||
-1, // binding index
|
||||
'!', (index - 1) << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Attr, 'title', null
|
||||
]);
|
||||
expect(opCodes).toEqual(debugMatch([
|
||||
'if (mask & 0b1) { (lView[0] as Element).setAttribute(\'title\', `Hello ${lView[1]}!`); }'
|
||||
]));
|
||||
});
|
||||
|
||||
it('for multiple bindings', () => {
|
||||
@ -641,12 +478,9 @@ describe('Runtime i18n', () => {
|
||||
ɵɵi18nAttributes(index, MSG_div_attr);
|
||||
}, null, nbConsts, index);
|
||||
|
||||
expect(opCodes).toEqual([
|
||||
0b11, // bindings mask
|
||||
10, // size
|
||||
'Hello ', -1, ' and ', -2, ', again ', -1, '!',
|
||||
(index - 1) << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Attr, 'title', null
|
||||
]);
|
||||
expect(opCodes).toEqual(debugMatch([
|
||||
'if (mask & 0b11) { (lView[0] as Element).setAttribute(\'title\', `Hello ${lView[1]} and ${lView[2]}, again ${lView[1]}!`); }'
|
||||
]));
|
||||
});
|
||||
|
||||
it('for multiple attributes', () => {
|
||||
@ -658,18 +492,10 @@ describe('Runtime i18n', () => {
|
||||
ɵɵi18nAttributes(index, MSG_div_attr);
|
||||
}, null, nbConsts, index);
|
||||
|
||||
expect(opCodes).toEqual([
|
||||
0b1, // bindings mask
|
||||
6, // if no update, skip 4
|
||||
'Hello ',
|
||||
-1, // binding index
|
||||
'!', (index - 1) << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Attr, 'title', null,
|
||||
0b1, // bindings mask
|
||||
6, // if no update, skip 4
|
||||
'Hello ',
|
||||
-1, // binding index
|
||||
'!', (index - 1) << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Attr, 'aria-label', null
|
||||
]);
|
||||
expect(opCodes).toEqual(debugMatch([
|
||||
'if (mask & 0b1) { (lView[0] as Element).setAttribute(\'title\', `Hello ${lView[1]}!`); }',
|
||||
'if (mask & 0b1) { (lView[0] as Element).setAttribute(\'aria-label\', `Hello ${lView[1]}!`); }'
|
||||
]));
|
||||
});
|
||||
});
|
||||
|
||||
|
22
packages/core/test/render3/interfaces/node_spec.ts
Normal file
22
packages/core/test/render3/interfaces/node_spec.ts
Normal file
@ -0,0 +1,22 @@
|
||||
/**
|
||||
* @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 {TNodeType, TNodeTypeAsString} from '@angular/core/src/render3/interfaces/node';
|
||||
|
||||
describe('node interfaces', () => {
|
||||
describe('TNodeType', () => {
|
||||
it('should agree with TNodeTypeAsString', () => {
|
||||
expect(TNodeTypeAsString[TNodeType.Container]).toEqual('Container');
|
||||
expect(TNodeTypeAsString[TNodeType.Projection]).toEqual('Projection');
|
||||
expect(TNodeTypeAsString[TNodeType.View]).toEqual('View');
|
||||
expect(TNodeTypeAsString[TNodeType.Element]).toEqual('Element');
|
||||
expect(TNodeTypeAsString[TNodeType.ElementContainer]).toEqual('ElementContainer');
|
||||
expect(TNodeTypeAsString[TNodeType.IcuContainer]).toEqual('IcuContainer');
|
||||
});
|
||||
});
|
||||
});
|
186
packages/core/test/render3/is_shape_of.ts
Normal file
186
packages/core/test/render3/is_shape_of.ts
Normal file
@ -0,0 +1,186 @@
|
||||
/**
|
||||
* @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 {TI18n} from '@angular/core/src/render3/interfaces/i18n';
|
||||
import {TNode} from '@angular/core/src/render3/interfaces/node';
|
||||
import {TView} from '@angular/core/src/render3/interfaces/view';
|
||||
|
||||
/**
|
||||
* A type used to create a runtime representation of a shape of object which matches the declared
|
||||
* interface at compile time.
|
||||
*
|
||||
* The purpose of this type is to ensure that the object must match all of the properties of a type.
|
||||
* This is later used by `isShapeOf` method to ensure that a particular object has a particular
|
||||
* shape.
|
||||
*
|
||||
* ```
|
||||
* interface MyShape {
|
||||
* foo: string,
|
||||
* bar: number
|
||||
* }
|
||||
*
|
||||
* const myShapeObj: {foo: '', bar: 0};
|
||||
* const ExpectedPropertiesOfShape = {foo: true, bar: true};
|
||||
*
|
||||
* isShapeOf(myShapeObj, ExpectedPropertiesOfShape);
|
||||
* ```
|
||||
*
|
||||
* The above code would verify that `myShapeObj` has `foo` and `bar` properties. However if later
|
||||
* `MyShape` is refactored to change a set of properties we would like to have a compile time error
|
||||
* that the `ExpectedPropertiesOfShape` also needs to be changed.
|
||||
*
|
||||
* ```
|
||||
* const ExpectedPropertiesOfShape = <ShapeOf<MyShape>>{foo: true, bar: true};
|
||||
* ```
|
||||
* The above code will force through compile time checks that the `ExpectedPropertiesOfShape` match
|
||||
* that of `MyShape`.
|
||||
*
|
||||
* See: `isShapeOf`
|
||||
*
|
||||
*/
|
||||
export type ShapeOf<T> = {
|
||||
[P in keyof T]: true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if a particular object is of a given shape (duck-type version of `instanceof`.)
|
||||
*
|
||||
* ```
|
||||
* isShapeOf(someObj, {foo: true, bar: true});
|
||||
* ```
|
||||
*
|
||||
* The above code will be true if the `someObj` has both `foo` and `bar` property
|
||||
*
|
||||
* @param obj Object to test for.
|
||||
* @param shapeOf Desired shape.
|
||||
*/
|
||||
export function isShapeOf<T>(obj: any, shapeOf: ShapeOf<T>): obj is T {
|
||||
if (typeof obj === 'object' && obj) {
|
||||
return Object.keys(shapeOf).reduce(
|
||||
(prev, key) => prev && obj.hasOwnProperty(key), true as boolean);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if `obj` matches the shape `TI18n`.
|
||||
* @param obj
|
||||
*/
|
||||
export function isTI18n(obj: any): obj is TI18n {
|
||||
return isShapeOf<TI18n>(obj, ShapeOfTI18n);
|
||||
}
|
||||
const ShapeOfTI18n: ShapeOf<TI18n> = {
|
||||
vars: true,
|
||||
create: true,
|
||||
update: true,
|
||||
icus: true,
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Determines if `obj` matches the shape `TView`.
|
||||
* @param obj
|
||||
*/
|
||||
export function isTView(obj: any): obj is TView {
|
||||
return isShapeOf<TView>(obj, ShapeOfTView);
|
||||
}
|
||||
const ShapeOfTView: ShapeOf<TView> = {
|
||||
type: true,
|
||||
id: true,
|
||||
blueprint: true,
|
||||
template: true,
|
||||
viewQuery: true,
|
||||
node: true,
|
||||
firstCreatePass: true,
|
||||
firstUpdatePass: true,
|
||||
data: true,
|
||||
bindingStartIndex: true,
|
||||
expandoStartIndex: true,
|
||||
staticViewQueries: true,
|
||||
staticContentQueries: true,
|
||||
firstChild: true,
|
||||
expandoInstructions: true,
|
||||
directiveRegistry: true,
|
||||
pipeRegistry: true,
|
||||
preOrderHooks: true,
|
||||
preOrderCheckHooks: true,
|
||||
contentHooks: true,
|
||||
contentCheckHooks: true,
|
||||
viewHooks: true,
|
||||
viewCheckHooks: true,
|
||||
destroyHooks: true,
|
||||
cleanup: true,
|
||||
components: true,
|
||||
queries: true,
|
||||
contentQueries: true,
|
||||
schemas: true,
|
||||
consts: true,
|
||||
incompleteFirstPass: true,
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Determines if `obj` matches the shape `TI18n`.
|
||||
* @param obj
|
||||
*/
|
||||
export function isTNode(obj: any): obj is TNode {
|
||||
return isShapeOf<TNode>(obj, ShapeOfTNode);
|
||||
}
|
||||
const ShapeOfTNode: ShapeOf<TNode> = {
|
||||
type: true,
|
||||
index: true,
|
||||
injectorIndex: true,
|
||||
directiveStart: true,
|
||||
directiveEnd: true,
|
||||
directiveStylingLast: true,
|
||||
propertyBindings: true,
|
||||
flags: true,
|
||||
providerIndexes: true,
|
||||
tagName: true,
|
||||
attrs: true,
|
||||
mergedAttrs: true,
|
||||
localNames: true,
|
||||
initialInputs: true,
|
||||
inputs: true,
|
||||
outputs: true,
|
||||
tViews: true,
|
||||
next: true,
|
||||
projectionNext: true,
|
||||
child: true,
|
||||
parent: true,
|
||||
projection: true,
|
||||
styles: true,
|
||||
stylesWithoutHost: true,
|
||||
residualStyles: true,
|
||||
classes: true,
|
||||
classesWithoutHost: true,
|
||||
residualClasses: true,
|
||||
classBindings: true,
|
||||
styleBindings: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if `obj` is DOM `Node`.
|
||||
*/
|
||||
export function isDOMNode(obj: any): obj is Node {
|
||||
return obj instanceof Node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if `obj` is DOM `Text`.
|
||||
*/
|
||||
export function isDOMElement(obj: any): obj is Element {
|
||||
return obj instanceof Element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if `obj` is DOM `Text`.
|
||||
*/
|
||||
export function isDOMText(obj: any): obj is Text {
|
||||
return obj instanceof Text;
|
||||
}
|
37
packages/core/test/render3/is_shape_of_spec.ts
Normal file
37
packages/core/test/render3/is_shape_of_spec.ts
Normal file
@ -0,0 +1,37 @@
|
||||
/**
|
||||
* @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 {isShapeOf, ShapeOf} from './is_shape_of';
|
||||
|
||||
describe('isShapeOf', () => {
|
||||
const ShapeOfEmptyObject: ShapeOf<{}> = {};
|
||||
it('should not match for non objects', () => {
|
||||
expect(isShapeOf(null, ShapeOfEmptyObject)).toBeFalse();
|
||||
expect(isShapeOf(0, ShapeOfEmptyObject)).toBeFalse();
|
||||
expect(isShapeOf(1, ShapeOfEmptyObject)).toBeFalse();
|
||||
expect(isShapeOf(true, ShapeOfEmptyObject)).toBeFalse();
|
||||
expect(isShapeOf(false, ShapeOfEmptyObject)).toBeFalse();
|
||||
expect(isShapeOf(undefined, ShapeOfEmptyObject)).toBeFalse();
|
||||
});
|
||||
|
||||
it('should match on empty object', () => {
|
||||
expect(isShapeOf({}, ShapeOfEmptyObject)).toBeTrue();
|
||||
expect(isShapeOf({extra: 'is ok'}, ShapeOfEmptyObject)).toBeTrue();
|
||||
});
|
||||
|
||||
it('should match on shape', () => {
|
||||
expect(isShapeOf({required: 1}, {required: true})).toBeTrue();
|
||||
expect(isShapeOf({required: true, extra: 'is ok'}, {required: true})).toBeTrue();
|
||||
});
|
||||
|
||||
it('should not match if missing property', () => {
|
||||
expect(isShapeOf({required: 1}, {required: true, missing: true})).toBeFalse();
|
||||
expect(isShapeOf({required: true, extra: 'is ok'}, {required: true, missing: true}))
|
||||
.toBeFalse();
|
||||
});
|
||||
});
|
218
packages/core/test/render3/matchers.ts
Normal file
218
packages/core/test/render3/matchers.ts
Normal file
@ -0,0 +1,218 @@
|
||||
/**
|
||||
* @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 {TI18n} from '@angular/core/src/render3/interfaces/i18n';
|
||||
import {TNode} from '@angular/core/src/render3/interfaces/node';
|
||||
import {TView} from '@angular/core/src/render3/interfaces/view';
|
||||
|
||||
import {isDOMElement, isDOMText, isTI18n, isTNode, isTView} from './is_shape_of';
|
||||
|
||||
|
||||
/**
|
||||
* Generic matcher which asserts that an object is of a given shape (`shapePredicate`) and that it
|
||||
* contains a subset of properties.
|
||||
*
|
||||
* @param name Name of `shapePredicate` to display when assertion fails.
|
||||
* @param shapePredicate Predicate which verifies that the object is of correct shape.
|
||||
* @param expected Expected set of properties to be found on the object.
|
||||
*/
|
||||
export function matchObjectShape<T>(
|
||||
name: string, shapePredicate: (obj: any) => obj is T,
|
||||
expected: Partial<T> = {}): jasmine.AsymmetricMatcher<T> {
|
||||
const matcher = function() {};
|
||||
let _actual: any = null;
|
||||
|
||||
matcher.asymmetricMatch = function(actual: any) {
|
||||
_actual = actual;
|
||||
if (!shapePredicate(actual)) return false;
|
||||
for (const key in expected) {
|
||||
if (expected.hasOwnProperty(key) && !jasmine.matchersUtil.equals(actual[key], expected[key]))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
matcher.jasmineToString = function() {
|
||||
return `${toString(_actual, false)} != ${toString(expected, true)})`;
|
||||
};
|
||||
|
||||
function toString(obj: any, isExpected: boolean) {
|
||||
if (isExpected || shapePredicate(obj)) {
|
||||
const props =
|
||||
Object.keys(expected).map(key => `${key}: ${JSON.stringify((obj as any)[key])}`);
|
||||
if (isExpected === false) {
|
||||
// Push something to let the user know that there may be other ignored properties in actual
|
||||
props.push('...');
|
||||
}
|
||||
return `${name}({${props.length === 0 ? '' : '\n ' + props.join(',\n ') + '\n'}})`;
|
||||
} else {
|
||||
return JSON.stringify(obj);
|
||||
}
|
||||
}
|
||||
return matcher;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Asymmetric matcher which matches a `TView` of a given shape.
|
||||
*
|
||||
* Expected usage:
|
||||
* ```
|
||||
* expect(tNode).toEqual(matchTView({type: TViewType.Root}));
|
||||
* expect({
|
||||
* node: tNode
|
||||
* }).toEqual({
|
||||
* node: matchTNode({type: TViewType.Root})
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param expected optional properties which the `TView` must contain.
|
||||
*/
|
||||
export function matchTView(expected?: Partial<TView>): jasmine.AsymmetricMatcher<TView> {
|
||||
return matchObjectShape('TView', isTView, expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asymmetric matcher which matches a `TNode` of a given shape.
|
||||
*
|
||||
* Expected usage:
|
||||
* ```
|
||||
* expect(tNode).toEqual(matchTNode({type: TNodeType.Element}));
|
||||
* expect({
|
||||
* node: tNode
|
||||
* }).toEqual({
|
||||
* node: matchTNode({type: TNodeType.Element})
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param expected optional properties which the `TNode` must contain.
|
||||
*/
|
||||
export function matchTNode(expected?: Partial<TNode>): jasmine.AsymmetricMatcher<TNode> {
|
||||
return matchObjectShape('TNode', isTNode, expected);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Asymmetric matcher which matches a `T18n` of a given shape.
|
||||
*
|
||||
* Expected usage:
|
||||
* ```
|
||||
* expect(tNode).toEqual(matchT18n({vars: 0}));
|
||||
* expect({
|
||||
* node: tNode
|
||||
* }).toEqual({
|
||||
* node: matchT18n({vars: 0})
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param expected optional properties which the `TI18n` must contain.
|
||||
*/
|
||||
export function matchTI18n(expected?: Partial<TI18n>): jasmine.AsymmetricMatcher<TI18n> {
|
||||
return matchObjectShape('TI18n', isTI18n, expected);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Asymmetric matcher which matches a DOM Element.
|
||||
*
|
||||
* Expected usage:
|
||||
* ```
|
||||
* expect(div).toEqual(matchT18n('div', {id: '123'}));
|
||||
* expect({
|
||||
* node: div
|
||||
* }).toEqual({
|
||||
* node: matchT18n('div', {id: '123'})
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param expectedTagName optional DOM tag name.
|
||||
* @param expectedAttributes optional DOM element properties.
|
||||
*/
|
||||
export function matchDomElement(
|
||||
expectedTagName: string|undefined = undefined,
|
||||
expectedAttrs: {[key: string]: string|null} = {}): jasmine.AsymmetricMatcher<Element> {
|
||||
const matcher = function() {};
|
||||
let _actual: any = null;
|
||||
|
||||
matcher.asymmetricMatch = function(actual: any) {
|
||||
_actual = actual;
|
||||
if (!isDOMElement(actual)) return false;
|
||||
if (expectedTagName && (expectedTagName.toUpperCase() !== actual.tagName.toUpperCase())) {
|
||||
return false;
|
||||
}
|
||||
if (expectedAttrs) {
|
||||
for (const attrName in expectedAttrs) {
|
||||
if (expectedAttrs.hasOwnProperty(attrName)) {
|
||||
const expectedAttrValue = expectedAttrs[attrName];
|
||||
const actualAttrValue = actual.getAttribute(attrName);
|
||||
if (expectedAttrValue !== actualAttrValue) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
matcher.jasmineToString = function() {
|
||||
let actualStr = isDOMElement(_actual) ? `<${_actual.tagName}${toString(_actual.attributes)}>` :
|
||||
JSON.stringify(_actual);
|
||||
let expectedStr = `<${expectedTagName || '*'}${
|
||||
Object.keys(expectedAttrs).map(key => ` ${key}=${JSON.stringify(expectedAttrs[key])}`)}>`;
|
||||
return `[${actualStr} != ${expectedStr}]`;
|
||||
};
|
||||
|
||||
function toString(attrs: NamedNodeMap) {
|
||||
let text = '';
|
||||
for (let i = 0; i < attrs.length; i++) {
|
||||
const attr = attrs[i];
|
||||
text += ` ${attr.name}=${JSON.stringify(attr.value)}`;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
|
||||
return matcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asymmetric matcher which matches DOM text node.
|
||||
*
|
||||
* Expected usage:
|
||||
* ```
|
||||
* expect(div).toEqual(matchDomText('text'));
|
||||
* expect({
|
||||
* node: div
|
||||
* }).toEqual({
|
||||
* node: matchDomText('text')
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param expectedText optional DOM text.
|
||||
*/
|
||||
export function matchDomText(expectedText: string|undefined = undefined):
|
||||
jasmine.AsymmetricMatcher<Text> {
|
||||
const matcher = function() {};
|
||||
let _actual: any = null;
|
||||
|
||||
matcher.asymmetricMatch = function(actual: any) {
|
||||
_actual = actual;
|
||||
if (!isDOMText(actual)) return false;
|
||||
if (expectedText && (expectedText !== actual.textContent)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
matcher.jasmineToString = function() {
|
||||
let actualStr = isDOMText(_actual) ? `#TEXT: ${JSON.stringify(_actual.textContent)}` :
|
||||
JSON.stringify(_actual);
|
||||
let expectedStr = `#TEXT: ${JSON.stringify(expectedText)}`;
|
||||
return `[${actualStr} != ${expectedStr}]`;
|
||||
};
|
||||
|
||||
return matcher;
|
||||
}
|
101
packages/core/test/render3/matchers_spec.ts
Normal file
101
packages/core/test/render3/matchers_spec.ts
Normal file
@ -0,0 +1,101 @@
|
||||
/**
|
||||
* @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 {createTNode, createTView} from '@angular/core/src/render3/instructions/shared';
|
||||
import {TNodeType} from '@angular/core/src/render3/interfaces/node';
|
||||
import {TViewType} from '@angular/core/src/render3/interfaces/view';
|
||||
import {onlyInIvy} from '@angular/private/testing';
|
||||
|
||||
import {isShapeOf, ShapeOf} from './is_shape_of';
|
||||
import {matchDomElement, matchDomText, matchObjectShape, matchTNode, matchTView} from './matchers';
|
||||
import {dedent} from './utils';
|
||||
|
||||
describe('render3 matchers', () => {
|
||||
describe('matchObjectShape', () => {
|
||||
interface MyShape {
|
||||
propA: any;
|
||||
propB: any;
|
||||
}
|
||||
|
||||
const myShape: MyShape = {propA: 'value', propB: 3};
|
||||
function isMyShape(obj: any): obj is MyShape {
|
||||
return isShapeOf<MyShape>(obj, ShapeOfMyShape);
|
||||
}
|
||||
const ShapeOfMyShape: ShapeOf<MyShape> = {propA: true, propB: true};
|
||||
function matchMyShape(expected?: Partial<MyShape>): jasmine.AsymmetricMatcher<MyShape> {
|
||||
return matchObjectShape('MyShape', isMyShape, expected);
|
||||
}
|
||||
|
||||
it('should match', () => {
|
||||
expect(isMyShape(myShape)).toBeTrue();
|
||||
expect(myShape).toEqual(matchMyShape());
|
||||
expect(myShape).toEqual(matchMyShape({propA: 'value'}));
|
||||
expect({node: myShape}).toEqual({node: matchMyShape({propA: 'value'})});
|
||||
});
|
||||
|
||||
it('should produce human readable errors', () => {
|
||||
const matcher = matchMyShape({propA: 'different'});
|
||||
expect(matcher.asymmetricMatch(myShape, [])).toEqual(false);
|
||||
expect(matcher.jasmineToString!()).toEqual(dedent`
|
||||
MyShape({
|
||||
propA: "value",
|
||||
...
|
||||
}) != MyShape({
|
||||
propA: "different"
|
||||
}))`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('matchTView', () => {
|
||||
const tView = createTView(TViewType.Root, 1, null, 2, 3, null, null, null, null, null);
|
||||
it('should match', () => {
|
||||
expect(tView).toEqual(matchTView());
|
||||
expect(tView).toEqual(matchTView({type: TViewType.Root}));
|
||||
expect({node: tView}).toEqual({node: matchTView({type: TViewType.Root})});
|
||||
});
|
||||
});
|
||||
describe('matchTNode', () => {
|
||||
const tView = createTView(TViewType.Root, 1, null, 2, 3, null, null, null, null, null);
|
||||
const tNode = createTNode(tView, null, TNodeType.Element, 1, 'tagName', []);
|
||||
|
||||
it('should match', () => {
|
||||
expect(tNode).toEqual(matchTNode());
|
||||
expect(tNode).toEqual(matchTNode({type: TNodeType.Element, tagName: 'tagName'}));
|
||||
expect({node: tNode}).toEqual({node: matchTNode({type: TNodeType.Element})});
|
||||
});
|
||||
});
|
||||
|
||||
describe('matchDomElement', () => {
|
||||
const div = document.createElement('div');
|
||||
div.setAttribute('name', 'Name');
|
||||
it('should match', () => {
|
||||
expect(div).toEqual(matchDomElement());
|
||||
expect(div).toEqual(matchDomElement('div', {name: 'Name'}));
|
||||
});
|
||||
|
||||
it('should produce human readable error', () => {
|
||||
const matcher = matchDomElement('div', {name: 'other'});
|
||||
expect(matcher.asymmetricMatch(div, [])).toEqual(false);
|
||||
expect(matcher.jasmineToString!()).toEqual(`[<DIV name="Name"> != <div name="other">]`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('matchDomText', () => {
|
||||
const text = document.createTextNode('myText');
|
||||
it('should match', () => {
|
||||
expect(text).toEqual(matchDomText());
|
||||
expect(text).toEqual(matchDomText('myText'));
|
||||
});
|
||||
|
||||
it('should produce human readable error', () => {
|
||||
const matcher = matchDomText('other text');
|
||||
expect(matcher.asymmetricMatch(text, [])).toEqual(false);
|
||||
expect(matcher.jasmineToString!()).toEqual(`[#TEXT: "myText" != #TEXT: "other text"]`);
|
||||
});
|
||||
});
|
||||
});
|
73
packages/core/test/render3/utils.ts
Normal file
73
packages/core/test/render3/utils.ts
Normal file
@ -0,0 +1,73 @@
|
||||
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
/** Template string function that can be used to strip indentation from a given string literal. */
|
||||
export function dedent(strings: TemplateStringsArray, ...values: any[]) {
|
||||
let joinedString = '';
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
joinedString += `${strings[i]}${values[i]}`;
|
||||
}
|
||||
joinedString += strings[strings.length - 1];
|
||||
const lines = joinedString.split('\n');
|
||||
while (isBlank(lines[0])) {
|
||||
lines.shift();
|
||||
}
|
||||
while (isBlank(lines[lines.length - 1])) {
|
||||
lines.pop();
|
||||
}
|
||||
let minWhitespacePrefix = lines.reduce(
|
||||
(min, line) => Math.min(min, numOfWhiteSpaceLeadingChars(line)), Number.MAX_SAFE_INTEGER);
|
||||
return lines.map((line) => line.substring(minWhitespacePrefix)).join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests to see if the line is blank.
|
||||
*
|
||||
* A blank line is such which contains only whitespace.
|
||||
* @param text string to test for blank-ness.
|
||||
*/
|
||||
function isBlank(text: string): boolean {
|
||||
return /^\s*$/.test(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns number of whitespace leading characters.
|
||||
*
|
||||
* @param text
|
||||
*/
|
||||
function numOfWhiteSpaceLeadingChars(text: string): number {
|
||||
return text.match(/^\s*/)![0].length;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Jasmine AsymmetricMatcher which can be used to assert `.debug` properties.
|
||||
*
|
||||
* ```
|
||||
* expect(obj).toEqual({
|
||||
* create: debugMatch('someValue')
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* In the above example it will assert that `obj.create.debug === 'someValue'`.
|
||||
*
|
||||
* @param expected Expected value.
|
||||
*/
|
||||
export function debugMatch<T>(expected: T): any {
|
||||
const matcher = function() {};
|
||||
let actual: any = null;
|
||||
|
||||
matcher.asymmetricMatch = function(objectWithDebug: any) {
|
||||
return jasmine.matchersUtil.equals(actual = objectWithDebug.debug, expected);
|
||||
};
|
||||
matcher.jasmineToString = function() {
|
||||
return `<${JSON.stringify(actual)} != ${JSON.stringify(expected)}>`;
|
||||
};
|
||||
return matcher;
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user