From fe1b77d4a649b4ac374ee89010768f59d66fcb48 Mon Sep 17 00:00:00 2001 From: Ricardo Chavarria Date: Mon, 6 Oct 2025 18:09:49 -0600 Subject: [PATCH] feat: update origin to Angular v20.3.3 Contributing guide updated - Add prerequisites section with required software versions - Improve translation guidelines with clear examples - Add Git best practices using English conventions - Include troubleshooting section for common issues - Add detailed pnpm installation instructions - Update code examples section with do's and don'ts - Add commit message and branch naming conventions --- .github/workflows/ci.yml | 4 + CONTRIBUTING.md | 511 +++++++++-- adev-es/src/app/core/constants/links.ts | 16 +- .../layout/footer/footer.component.en.html | 14 +- .../core/layout/footer/footer.component.html | 6 +- .../core/layout/footer/footer.component.ts | 10 +- .../navigation/navigation.component.en.html | 232 ++--- .../navigation/navigation.component.html | 251 +++-- .../home-animation.component.html | 154 ---- .../app/features/home/home.component.en.html | 489 ++++++++++ .../src/app/features/home/home.component.html | 499 +++++++++- .../{ => routing}/sub-navigation-data.en.ts | 159 +++- .../app/{ => routing}/sub-navigation-data.ts | 854 ++++++++---------- adev-es/src/content/ai/design-patterns.md | 172 ++++ adev-es/src/content/ai/develop-with-ai.md | 11 +- adev-es/src/content/ai/mcp-server-setup.md | 141 +++ adev-es/src/content/ai/overview.md | 70 +- .../src/content/best-practices/style-guide.md | 14 +- .../ecosystem/rxjs-interop/signals-interop.md | 35 +- .../rxjs-interop/take-until-destroyed.md | 46 + .../service-workers/communications.md | 3 +- .../custom-service-worker-scripts.md | 111 +++ .../service-workers/getting-started.md | 153 ++++ .../ecosystem/service-workers/overview.md | 2 + .../guide/animations/complex-sequences.md | 13 +- adev-es/src/content/guide/animations/css.md | 17 +- .../guide/animations/enter-and-leave.md | 99 ++ .../src/content/guide/animations/migration.md | 12 +- .../src/content/guide/animations/overview.md | 15 +- .../guide/animations/reusable-animations.md | 11 +- .../animations/transition-and-triggers.md | 15 +- .../guide/components/content-projection.md | 51 +- .../content/guide/components/host-elements.md | 4 +- .../src/content/guide/components/inputs.md | 8 +- .../src/content/guide/components/outputs.md | 14 + .../src/content/guide/components/selectors.md | 3 +- .../content/guide/components/styling.en.md | 9 +- .../di/creating-injectable-service.en.md | 2 +- .../guide/di/creating-injectable-service.md | 2 +- .../content/guide/directives/overview.en.md | 19 +- .../src/content/guide/directives/overview.md | 19 +- adev-es/src/content/guide/drag-drop.en.md | 23 + adev-es/src/content/guide/drag-drop.md | 23 + .../content/guide/http/http-resource.en.md | 14 +- .../src/content/guide/http/http-resource.md | 10 +- .../src/content/guide/http/interceptors.en.md | 33 + .../src/content/guide/http/interceptors.md | 33 + .../content/guide/http/making-requests.en.md | 315 ++++++- .../src/content/guide/http/making-requests.md | 315 ++++++- adev-es/src/content/guide/http/testing.en.md | 2 +- adev-es/src/content/guide/http/testing.md | 2 +- adev-es/src/content/guide/i18n/overview.md | 2 +- .../content/guide/i18n/translation-files.md | 2 +- .../routing/customizing-route-behavior.md | 473 ++++++++++ .../content/guide/routing/data-resolvers.md | 298 ++++++ .../content/guide/routing/define-routes.md | 24 +- .../guide/routing/lifecycle-and-events.md | 243 +++++ adev-es/src/content/guide/routing/overview.md | 18 - .../guide/routing/rendering-strategies.md | 139 +++ .../routing/route-transition-animations.md | 203 +++++ adev-es/src/content/guide/routing/testing.md | 341 +++++++ .../content/guide/signals/linked-signal.en.md | 2 +- .../content/guide/signals/linked-signal.md | 2 +- .../src/content/guide/signals/resource.en.md | 2 +- adev-es/src/content/guide/signals/resource.md | 2 +- adev-es/src/content/guide/ssr.md | 6 +- adev-es/src/content/guide/tailwind.md | 69 ++ .../src/content/guide/templates/binding.md | 67 +- adev-es/src/content/guide/templates/defer.md | 4 + .../guide/templates/expression-syntax.md | 88 +- .../content/guide/templates/ng-template.md | 11 +- .../guide/templates/two-way-binding.md | 2 +- .../src/content/guide/templates/variables.md | 2 +- .../guide/testing/components-scenarios.md | 177 +--- .../guide/testing/experimental-unit-test.md | 22 +- .../testing/using-component-harnesses.md | 6 + adev-es/src/content/guide/zoneless.md | 6 +- .../introduction/essentials/next-steps.en.md | 6 +- .../introduction/essentials/next-steps.md | 4 +- .../introduction/essentials/signals.en.md | 2 +- .../introduction/essentials/signals.md | 2 +- .../introduction/essentials/templates.en.md | 2 +- .../introduction/essentials/templates.md | 2 +- .../content/introduction/installation.en.md | 4 +- .../src/content/introduction/installation.md | 2 +- .../introduction/what-is-angular.en.md | 4 +- .../content/introduction/what-is-angular.md | 4 +- adev-es/src/content/kitchen-sink.md | 72 +- .../content/reference/configs/npm-packages.md | 6 +- .../reference/configs/workspace-config.md | 430 ++++----- .../src/content/reference/errors/NG02802.md | 41 + .../src/content/reference/errors/NG0401.md | 18 + .../src/content/reference/errors/NG05104.md | 2 +- .../src/content/reference/errors/NG0913.md | 4 +- .../src/content/reference/errors/NG3003.md | 25 +- .../src/content/reference/errors/overview.md | 2 + .../reference/extended-diagnostics/NG8114.md | 2 +- .../reference/extended-diagnostics/NG8117.md | 65 ++ .../extended-diagnostics/overview.md | 33 +- .../migrations/route-lazy-loading.md | 2 +- .../reference/migrations/signal-queries.md | 4 +- adev-es/src/content/reference/roadmap.md | 4 +- adev-es/src/content/reference/versions.md | 19 +- adev-es/src/content/tools/cli/aot-compiler.md | 2 +- .../tools/cli/build-system-migration.md | 7 +- adev-es/src/content/tools/cli/build.md | 4 +- adev-es/src/content/tools/cli/serve.md | 11 - .../src/content/tools/devtools/component.md | 79 ++ .../src/content/tools/devtools/overview.md | 45 + .../src/content/tools/devtools/profiler.md | 77 ++ adev-es/src/content/tools/language-service.md | 4 +- .../first-app/steps/01-hello-world/README.md | 2 +- .../first-app/steps/02-Home/README.md | 2 +- .../steps/03-HousingLocation/README.md | 2 +- .../first-app/steps/04-interfaces/README.md | 4 +- .../first-app/steps/05-inputs/README.md | 30 +- .../steps/06-property-binding/README.md | 2 +- .../07-dynamic-template-values/README.md | 6 +- .../first-app/steps/08-ngFor/README.md | 36 +- .../first-app/steps/09-services/README.md | 4 +- .../first-app/steps/10-routing/README.md | 4 +- .../first-app/steps/11-details-page/README.md | 33 +- .../first-app/steps/12-forms/README.md | 4 +- .../first-app/steps/13-search/README.md | 19 +- .../first-app/steps/14-http/README.md | 243 +++-- adev-es/src/content/tutorials/home.md | 3 + .../steps/11-optimizing-images/README.md | 5 +- .../steps/12-enable-routing/README.md | 9 +- .../steps/13-define-a-route/README.md | 12 +- .../learn-angular/steps/15-forms/README.md | 4 +- .../steps/16-form-control-values/README.md | 6 +- .../README.md | 4 +- .../steps/20-inject-based-di/README.md | 2 +- .../learn-angular/steps/22-pipes/README.md | 14 +- .../steps/23-pipes-format-data/README.md | 12 +- .../steps/24-create-a-pipe/README.md | 21 +- .../steps/4-control-flow-if/README.md | 2 +- .../steps/6-property-binding/README.md | 6 +- .../steps/7-event-handling/README.md | 20 +- .../learn-angular/steps/8-input/README.md | 2 +- .../learn-angular/steps/9-output/README.md | 15 +- .../content/tutorials/signals/intro/README.md | 13 + .../1-creating-your-first-signal/README.md | 130 +++ .../README.md | 110 +++ .../signals/steps/11-next-steps/README.md | 12 + .../README.md | 109 +++ .../README.md | 95 ++ .../README.md | 110 +++ .../README.md | 105 +++ .../README.md | 128 +++ .../7-using-signals-with-services/README.md | 103 +++ .../8-using-signals-with-directives/README.md | 110 +++ .../README.md | 66 ++ origin | 2 +- tools/lib/common.mjs | 7 +- tools/update-origin.mjs | 2 +- 156 files changed, 7831 insertions(+), 2040 deletions(-) delete mode 100644 adev-es/src/app/features/home/components/home-animation/home-animation.component.html create mode 100644 adev-es/src/app/features/home/home.component.en.html rename adev-es/src/app/{ => routing}/sub-navigation-data.en.ts (88%) rename adev-es/src/app/{ => routing}/sub-navigation-data.ts (69%) create mode 100644 adev-es/src/content/ai/design-patterns.md create mode 100644 adev-es/src/content/ai/mcp-server-setup.md create mode 100644 adev-es/src/content/ecosystem/rxjs-interop/take-until-destroyed.md create mode 100644 adev-es/src/content/ecosystem/service-workers/custom-service-worker-scripts.md create mode 100644 adev-es/src/content/guide/animations/enter-and-leave.md create mode 100644 adev-es/src/content/guide/routing/customizing-route-behavior.md create mode 100644 adev-es/src/content/guide/routing/data-resolvers.md create mode 100644 adev-es/src/content/guide/routing/lifecycle-and-events.md create mode 100644 adev-es/src/content/guide/routing/rendering-strategies.md create mode 100644 adev-es/src/content/guide/routing/route-transition-animations.md create mode 100644 adev-es/src/content/guide/routing/testing.md create mode 100644 adev-es/src/content/guide/tailwind.md create mode 100644 adev-es/src/content/reference/errors/NG02802.md create mode 100644 adev-es/src/content/reference/errors/NG0401.md create mode 100644 adev-es/src/content/reference/extended-diagnostics/NG8117.md create mode 100644 adev-es/src/content/tools/devtools/component.md create mode 100644 adev-es/src/content/tools/devtools/overview.md create mode 100644 adev-es/src/content/tools/devtools/profiler.md create mode 100644 adev-es/src/content/tutorials/signals/intro/README.md create mode 100644 adev-es/src/content/tutorials/signals/steps/1-creating-your-first-signal/README.md create mode 100644 adev-es/src/content/tutorials/signals/steps/10-reacting-to-signal-changes-with-effect/README.md create mode 100644 adev-es/src/content/tutorials/signals/steps/11-next-steps/README.md create mode 100644 adev-es/src/content/tutorials/signals/steps/2-deriving-state-with-computed-signals/README.md create mode 100644 adev-es/src/content/tutorials/signals/steps/3-deriving-state-with-linked-signals/README.md create mode 100644 adev-es/src/content/tutorials/signals/steps/4-managing-async-data-with-signals/README.md create mode 100644 adev-es/src/content/tutorials/signals/steps/5-component-communication-with-signals/README.md create mode 100644 adev-es/src/content/tutorials/signals/steps/6-two-way-binding-with-model-signals/README.md create mode 100644 adev-es/src/content/tutorials/signals/steps/7-using-signals-with-services/README.md create mode 100644 adev-es/src/content/tutorials/signals/steps/8-using-signals-with-directives/README.md create mode 100644 adev-es/src/content/tutorials/signals/steps/9-query-child-elements-with-signal-queries/README.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4150f81..6a6372c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,5 +23,9 @@ jobs: repository-cache: true - name: Install Dependencies run: npm ci + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.17.1 - name: Build Docs run: npm run build \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1ee3fe4..67fc1e0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,138 +1,473 @@ -# Contribución al proyecto angular-docs-es +# Guía de Contribución - angular-docs-es -## Configuración de origin +¡Gracias por tu interés en contribuir a la documentación de Angular en español! Esta guía te ayudará a empezar. -- `origin`: Manejo del repositorio `angular/angular`como un submódulo. -- `adev-es`: Se administra el contenido de la documentación de Angular, aquí se encuentran los - archivos traducidos y no traducidos en formato markdown. +## Tabla de Contenidos -## Configuración del directorio de adev +- [Introducción](#introducción) +- [Glosario de Términos de GitHub](#glosario-de-términos-de-github) +- [Requisitos Previos](#requisitos-previos) +- [Estructura del Proyecto](#estructura-del-proyecto) +- [Flujo de Trabajo para Contribuciones](#flujo-de-trabajo-para-contribuciones) +- [Configuración del Entorno Local](#configuración-del-entorno-local) +- [Directrices para la Traducción](#directrices-para-la-traducción) +- [Comandos Disponibles](#comandos-disponibles) +- [Solución de Problemas](#solución-de-problemas) +- [Recomendaciones](#recomendaciones) - Básicamente, traduce archivos de Markdown en el directorio `content`. Si es necesario, también - puede agregar el código fuente de la aplicación. +## Introducción + +Este proyecto es una traducción colaborativa de la documentación oficial de Angular al español, con el objetivo de hacer que los recursos de aprendizaje sean más accesibles para la comunidad hispanohablante. + +## Glosario de Términos de GitHub + +Si no estás familiarizado con GitHub, aquí te explicamos los términos más comunes que usaremos: + +| Término | Significado | +|---------|-------------| +| **Fork** | Crear una copia completa de un repositorio en tu cuenta de GitHub. Te permite trabajar en tu propia versión sin afectar el proyecto original. | +| **Clone** | Descargar una copia del repositorio a tu computadora local. | +| **Branch** | Una rama es una versión paralela del código. Te permite trabajar en cambios sin afectar la rama principal (`main`). | +| **Commit** | Guardar cambios en tu repositorio local con un mensaje descriptivo. Es como una "fotografía" del estado de tu código en un momento específico. | +| **Push** | Enviar tus commits locales al repositorio remoto en GitHub. | +| **Pull** | Traer cambios desde el repositorio remoto a tu repositorio local. | +| **Pull Request (PR)** | Una solicitud para que tus cambios sean revisados e incorporados al proyecto original. Es donde se revisa y discute tu contribución. | +| **Issue** | Un problema, sugerencia o tarea pendiente. Se usa para rastrear el trabajo que debe hacerse. | +| **Merge** | Combinar cambios de una rama en otra. Por ejemplo, incorporar tu pull request a la rama `main`. | +| **Upstream** | El repositorio original del cual hiciste fork. Se usa para mantener tu fork actualizado. | +| **Remote** | Una versión del repositorio alojada en un servidor (como GitHub), a diferencia de tu copia local. | + +> **Consejo:** No te preocupes si estos términos son nuevos para ti. Los irás aprendiendo mientras contribuyes al proyecto. + +## Requisitos Previos + +Antes de comenzar, asegúrate de tener instalado: + +- **Git** (versión 2.0 o superior) +- **Node.js** (versión 16.13 o superior - recomendado 20+) +- **npm** (incluido con Node.js) +- **pnpm** (versión específica - ver [paso 4](#4-instalar-pnpm)) +- Conocimientos básicos de: + - Git y GitHub (fork, clone, commit, pull request) + - Markdown + - Angular (preferiblemente) + +## Estructura del Proyecto + +El proyecto está organizado en dos directorios principales: + +### `origin/` +Submódulo de Git que apunta al repositorio oficial de `angular/angular`. Contiene el código fuente y documentación original en inglés. + +### `adev-es/` +Contiene los archivos traducidos y personalizados para la versión en español: ``` -origin/adev/ -├── README.md -├── content # Archivos de recursos de documentación escritos en Markdown, HTML, etc. Traduzca principalmente los archivos aquí. -│ ├── best-pratices -│ ├── ecosystem -│ ├── guide -│ ├── introduction -│ ├── references -│ ├── tools -│ ├── error.md -│ └── kitchen-sink.md -├── app # Contenido de la aplicación de angular.dev -... +adev-es/ +├── src/ +│ ├── app/ # Archivos personalizados de la app (navegación, footer, etc.) +│ └── content/ # 📝 Documentación en Markdown (¡traduce aquí!) +│ ├── best-practices/ +│ ├── ecosystem/ +│ ├── guide/ +│ ├── introduction/ +│ ├── reference/ +│ ├── tools/ +│ ├── error.md +│ └── kitchen-sink.md ``` -## Creación de un problema de traducción +### `build/` +Directorio generado automáticamente que combina los archivos de `origin/` con las traducciones de `adev-es/`. - Antes de comenzar el trabajo de traducción, vamos a comprobar si no hay nadie que está tratando de - traducir el mismo archivo. Si miras la etiqueta de [docs-translation](https://github.com/angular-hispano/angular-docs-es/labels/docs-translation), puedes ver el área en - la que se está trabajando actualmente en la traducción. Si desea hacer una nueva traducción, - primero cree un [problema en Github](https://github.com/angular-hispano/angular-docs-es/issues) y complete la información de acuerdo con la plantilla. - -## Crear una solicitud de extracción de traducción +## Flujo de Trabajo para Contribuciones - Empuja los cambios al repositorio donde bifurcaste `angular/angular-docs-es` y envía una solicitud - de extracción a la fuente de bifurcación. Las solicitudes de extracción se revisan y luego se - fusionan si no hay problemas. - -## Directrices para la traducción +### 1. Verificar trabajo en progreso - Para traducciones al español, por favor sigue las siguientes pautas. - -### Guarda el texto original como un archivo.en.md +Antes de comenzar a traducir, revisa los [issues con la etiqueta `docs-translation`](https://github.com/angular-hispano/angular-docs-es/labels/docs-translation) para asegurarte de que nadie más esté trabajando en el mismo archivo. - Para gestionar el `diff` del texto original después de actualizar el origen, guarda el original en - el momento de la traducción como un archivo `xx.en.md`. Si desea traducir un documento nuevo, - copie el archivo `xx.md` escrito en inglés al archivo `xx.en.md` y sobrescriba el archivo `xx.md` - con la traducción al español. +### 2. Crear un issue - Si la traducción es parcial y no del documento completo, no necesitas duplicar/crear el archivo - `xx.en.md` simplemente puedes trabajar sobre el archivo `xx.md` con la traducción al español. +Si deseas traducir un documento nuevo: -### Alinear la posición de salto de línea con la original +1. Ve a [GitHub Issues](https://github.com/angular-hispano/angular-docs-es/issues) +2. Crea un nuevo issue usando la plantilla de traducción +3. Indica qué archivo vas a traducir +4. Espera la asignación antes de comenzar - Siempre que sea posible, alinear el número de líneas en la fuente y la traducción, y ayudar a hacer - la comprobación de diferencias más fácil al actualizar. +### 3. Hacer fork del repositorio -### Recomendaciones +1. Haz clic en el botón "Fork" en la parte superior derecha del repositorio +2. Esto crea una copia del proyecto en tu cuenta de GitHub -- Bifurca este repositorio en tu espacio de trabajo de Github para poder colaborar de una manera más - limpia y ordenada. +### 4. Trabajar en tu traducción -- Se recomienda encarecidamente firmar todos los commits que envíes al repositorio. Esto ayudará a - mantener la integridad y la seguridad del código base, y a fomentar un entorno de colaboración más - confiable entre los desarrolladores. +1. Clona tu fork (ver [Configuración del Entorno Local](#configuración-del-entorno-local)) +2. Crea un branch para tu traducción: + ```bash + git checkout -b translate-components-guide + ``` +3. Realiza tu traducción siguiendo las [Directrices para la Traducción](#directrices-para-la-traducción) +4. Prueba tus cambios localmente con `npm start` -## Configura tu entorno local +### 5. Enviar Pull Request -### 1. Clonación del repositorio +1. Haz push de los cambios a tu fork: + ```bash + git add . + git commit -m "translate: complete translation of components guide" + git push origin translate-components-guide + ``` +2. Ve a tu fork en GitHub +3. Haz clic en "Compare & pull request" +4. Completa la descripción del PR con detalles de tu traducción +5. Espera la revisión del equipo +## Configuración del Entorno Local + +### 1. Clonar tu fork + +```bash +git clone git@github.com:TU-USUARIO/angular-docs-es.git +cd angular-docs-es ``` -$ git clone git@github.com:angular-hispano/angular-docs-es.git + +**Nota:** Reemplaza `TU-USUARIO` con tu nombre de usuario de GitHub. + +### 2. Agregar el repositorio original como upstream (opcional pero recomendado) + +```bash +git remote add upstream git@github.com:angular-hispano/angular-docs-es.git ``` -### 2. Sincronizar el repositorio de origen +Esto te permitirá mantener tu fork actualizado: -Este repositorio utiliza submódulos para integrarse con el repositorio de origen (`angular/angular`). +```bash +git fetch upstream +git merge upstream/main +``` +### 3. Sincronizar el submódulo de origin + +Este repositorio utiliza submódulos de Git para integrarse con el repositorio original de Angular: + +```bash +git submodule sync +git submodule update --init ``` -$ git submodule sync -$ git submodule update --init + +### 4. Instalar pnpm + +Este proyecto requiere `pnpm` en una versión específica. La versión exacta se encuentra definida en `origin/package.json` bajo el campo `packageManager`. + +**Opción A: Instalación con npm** + +```bash +npm install -g pnpm@10.17.1 ``` -### 3. Instalar dependencias +**Opción B: Instalación con Corepack (recomendado para Node.js 16.13+)** +```bash +corepack enable +corepack prepare pnpm@10.17.1 --activate ``` -$ npm install + +**Verificar instalación:** + +```bash +pnpm --version +# Debería mostrar: 10.17.1 ``` -### 4. Servidor de desarrollo - - Al iniciar el servidor local de desarrollo, puede traducir mientras comprueba el resultado de la - compilación. +**⚠️ Importante:** Es crucial usar la versión exacta especificada en `origin/package.json` para garantizar la compatibilidad con el lockfile de pnpm y evitar errores durante la compilación. +### 5. Instalar dependencias + +```bash +npm install ``` -$ npm start + +Este comando instala las dependencias del proyecto raíz definidas en `package.json`. + +### 6. Iniciar el servidor de desarrollo + +```bash +npm start ``` - El navegador se iniciará automáticamente cuando el servidor local esté listo `adev-es` será - reconstruido automáticamente al cambiar un archivo en el directorio. +Este comando: +- Inicia el servidor local de desarrollo +- Abre automáticamente el navegador cuando esté listo +- Reconstruye automáticamente `adev-es` cuando cambias un archivo +- Te permite traducir mientras ves el resultado en tiempo real - Toma en cuenta que, si es la primera vez que están iniciando el servidor, debes pasar como - argumento `-- --init` lo cual hará que se inicilice de nuevo el folder `build` y así evitar - problemas de caché. Puedes pasar este argumento cada vez que necesites inicializar nuevamente el - folder `build`. +**Primera vez o problemas de caché:** - - ### 5. Construye para producción - - Construye el proyecto desplegable con el siguiente comando. +Si es la primera vez que inicias el servidor, o si experimentas problemas de caché, ejecuta: +```bash +npm start -- --init ``` -$ npm run build + +La opción `--init` reinicializa el directorio `build` desde cero. + +### 7. Compilar para producción + +Para generar una versión optimizada de producción: + +```bash +npm run build ``` -Cuando se completa la compilación, el resultado de la compilación se envía al directorio -`build/dist/bin/adev/build`. Puedes configurar un servidor de desarrollo con tu herramienta -favorita y comprobar los sitios construidos. +El resultado de la compilación se genera en la carpeta `build/dist`. + +## Directrices para la Traducción + +### Guarda el original como archivo `.en.md` + +Para facilitar la gestión de cambios (diff) después de actualizar el submódulo `origin`: + +1. **Para traducciones completas:** + - Copia el archivo original `xx.md` a `xx.en.md` (versión en inglés) + - Sobrescribe `xx.md` con tu traducción al español + +2. **Para traducciones parciales:** + - No necesitas crear el archivo `xx.en.md` + - Trabaja directamente sobre `xx.md` -Construye el proyecto en CI +**Ejemplo:** +```bash +# Traducir guide/components.md +cd adev-es/src/content/guide + +# Copiar el original +cp components.md components.en.md + +# Ahora edita components.md con la traducción en español ``` -$ npm run build:ci + +### Alinear saltos de línea + +Siempre que sea posible, mantén el mismo número de líneas entre el archivo original y la traducción. Esto facilita: +- La comparación con herramientas de diff +- La detección de cambios cuando se actualiza el origen +- La revisión del código + +**Ejemplo:** + +```markdown + +Angular is a platform and framework for building +single-page client applications using HTML and +TypeScript. + + +Angular es una plataforma y marco de trabajo para +construir aplicaciones de cliente de una sola página +usando HTML y TypeScript. +``` + +### Términos técnicos + +Algunos términos es mejor mantenerlos en inglés por ser ampliamente reconocidos en la comunidad de desarrollo: + +**Mantener en inglés:** +- Component, Service, Directive, Pipe, Decorator, Signal +- TypeScript, JavaScript, HTML, CSS +- Framework +- Build, Deploy +- Términos de Git/GitHub: commit, push, pull, merge, branch, fork, issue, pull request + +**Traducir al español:** +- Application → Aplicación +- Development → Desarrollo +- Tutorial → Tutorial +- Guide → Guía +- Feature → Característica/Funcionalidad +- Testing → Pruebas +- Debug → Depurar + +> **Nota:** Cuando tengas dudas sobre un término, revisa traducciones existentes en el proyecto para mantener la consistencia. + +### Ejemplos de código + +- **SÍ traduce** las descripciones y explicaciones alrededor del código +- **SÍ traduce** los comentarios explicativos dentro del código (que explican qué hace algo) +- **NO traduzcas** nombres de variables, funciones, clases o propiedades en el código +- **NO traduzcas** comentarios técnicos con términos específicos de Angular (Component, Service, etc.) + +**Ejemplo:** + +```typescript +// ❌ NO traducir así: +export class HeroeComponent { + // Este es el nombre del héroe + nombreHeroe = 'Superman'; +} + +// ✅ SÍ traducir así: +export class HeroComponent { + // El nombre del héroe que se muestra en la interfaz + heroName = 'Superman'; +} +``` + +> **Consejo:** Traduce los comentarios que ayudan a entender la lógica, pero mantén los términos técnicos en inglés. + +## Comandos Disponibles + +| Comando | Descripción | +|---------|-------------| +| `npm start` | Inicia servidor de desarrollo con hot-reload | +| `npm start -- --init` | Reinicializa el build y luego inicia servidor | +| `npm run build` | Compila el proyecto para producción | +| `npm run update-origin` | Actualiza el submódulo origin a la última versión | +| `npm run deploy:staging` | Despliega a Firebase Hosting (staging) | +| `npm run deploy:prod` | Despliega a Firebase Hosting (producción) | + +## Solución de Problemas + +### Error: "No se puede encontrar el módulo pnpm" + +**Problema:** El comando `npm run build` falla porque no encuentra pnpm. + +**Solución:** +```bash +# Verifica que pnpm esté instalado +pnpm --version + +# Si no está instalado o tiene versión incorrecta +npm install -g pnpm@10.17.1 + +# Verifica nuevamente +pnpm --version # Debe mostrar 10.17.1 +``` + +### Error: Submódulo no inicializado + +**Problema:** El directorio `origin/` está vacío o falta contenido. + +**Solución:** +```bash +git submodule sync +git submodule update --init --recursive +``` + +### Error: Caché corrupto o construcción inconsistente + +**Problema:** Los cambios no se reflejan o aparecen errores extraños al compilar. + +**Solución:** +```bash +# Limpia todo y reinicia +rm -rf build/ node_modules/ +npm install +npm start -- --init ``` -La compilación del proyecto en CI funciona de manera diferente, primero ejecutamos el comando -`npm run build:ci`, este comando realizará un `pre-built` del proyecto, es decir que no construirá -la documentación y solo creará el directorio `build` y copiará los archivos necesarios. La -construcción de la documentación se realizará directamente en la configuración de CI. +### El navegador no se abre automáticamente + +**Problema:** El servidor inicia pero el navegador no se abre. + +**Solución:** +Abre manualmente tu navegador en `http://localhost:4201` + +## Recomendaciones + +### Buenas prácticas de Git + +- **Haz fork del repositorio** para mantener tu espacio de trabajo limpio y ordenado +- **Crea branches descriptivos** en inglés y con guiones: + - ✅ `translate-directives-guides` + - ✅ `translate-http-client` + - ✅ `fix-typos-signals` + - ❌ `traduccion/guide-components` (evitar español) + - ❌ `fix` o `update` (muy genérico) + +- **Commits atómicos**: Un commit por concepto/archivo traducido + +- **Mensajes de commit en inglés**: Seguimos las convenciones internacionales de open source + ```bash + # Formato: : + + # Para traducciones completas + git commit -m "translate: translations for directives guides" + git commit -m "translate: complete translation of components guide" + + # Para traducciones parciales + git commit -m "translate: partial translation of pipes guide (basics section)" + + # Para correcciones + git commit -m "fix: typos in signals overview" + git commit -m "fix: broken links in routing guide" + + # Para actualizaciones + git commit -m "docs: update contributing guidelines" + ``` + +**Tipos de commit comunes:** +- `translate:` - Nuevas traducciones +- `fix:` - Correcciones de errores, typos, enlaces rotos +- `docs:` - Cambios en documentación del proyecto (como CONTRIBUTING.md) +- `chore:` - Tareas de mantenimiento, actualizaciones de dependencias + +**Convenciones adicionales:** +- Usa minúsculas en el mensaje del commit +- Mantén el mensaje corto y descriptivo (idealmente menos de 72 caracteres) +- No termines el mensaje con punto +- Si necesitas más detalles, agrégalos en el cuerpo del commit: + ```bash + git commit -m "translate: complete translation of pipes guide" -m "Includes all examples and code snippets. Added translations for built-in pipes section." + ``` + +> **Nota:** Mantenemos los commits en inglés para seguir las convenciones internacionales de proyectos open source y facilitar la colaboración con la comunidad global. + +### Firma tus commits (recomendado) + +Firmar commits ayuda a mantener la integridad y seguridad del código base: + +```bash +# Configurar GPG (una sola vez) +git config --global user.signingkey TU_GPG_KEY_ID +git config --global commit.gpgsign true + +# Los commits futuros se firmarán automáticamente +git commit -m "translate: complete translation of overview guide" +``` + +[Guía para configurar GPG en GitHub](https://docs.github.com/es/authentication/managing-commit-signature-verification) + +### Calidad de la traducción + +- **Lee el contexto completo** antes de traducir +- **Mantén el tono técnico** pero accesible +- **Revisa tu ortografía y gramática** +- **Prueba los ejemplos** si es posible +- **Pide feedback** cuando tengas dudas + +### Comunicación + +- **Comenta en el issue** si tienes dudas o necesitas ayuda +- **Actualiza el issue** con tu progreso +- **Sé receptivo al feedback** en las revisiones del PR +- **Agradece a los revisores** - son voluntarios como tú + +## Recursos Útiles + +- [Documentación oficial de Angular](https://angular.dev) +- [Repositorio angular/angular](https://github.com/angular/angular) +- [Guía de Markdown](https://www.markdownguide.org/basic-syntax/) +- [Convenciones para commits](https://www.conventionalcommits.org/es/) +- [Cómo usar Git y GitHub](https://docs.github.com/es/get-started) + +--- + +## ¡Gracias por tu Colaboración! 🎉 + +Esperamos que te unas a nosotros en este esfuerzo por hacer que la documentación de Angular sea más accesible para la comunidad hispanohablante. Cada traducción, por pequeña que sea, hace una gran diferencia. -## Gracias por tu colaboración! +**¿Preguntas?** Abre un [issue](https://github.com/angular-hispano/angular-docs-es/issues) o únete a la conversación. -Esperamos que te unas a nosotros en este esfuerzo por hacer que la documentación de Angular sea más -accesible para la comunidad hispanohablante. +¡Feliz traducción! 🚀 diff --git a/adev-es/src/app/core/constants/links.ts b/adev-es/src/app/core/constants/links.ts index d5b65c0..2fa07f5 100644 --- a/adev-es/src/app/core/constants/links.ts +++ b/adev-es/src/app/core/constants/links.ts @@ -1,6 +1,10 @@ -export const GITHUB = 'https://github.com/angular-hispano/angular-docs-es'; -export const X = 'https://x.com/AngularHispana'; -export const DISCORD = 'https://discord.gg/4jgk3ddgAx'; -export const MEDIUM = ''; -export const YOUTUBE = ''; -export const BLUESKY = ''; \ No newline at end of file +export const ANGULAR_LINKS = { + GITHUB: 'https://github.com/angular-hispano/angular-docs-es', + X: 'https://x.com/AngularHispana', + MEDIUM: 'https://blog.angular.dev', + YOUTUBE: '', + DISCORD: 'https://discord.gg/4jgk3ddgAx', + BLUESKY: '', + STACKOVERFLOW: '', + } as const; + \ No newline at end of file diff --git a/adev-es/src/app/core/layout/footer/footer.component.en.html b/adev-es/src/app/core/layout/footer/footer.component.en.html index b72fd77..97ef75c 100644 --- a/adev-es/src/app/core/layout/footer/footer.component.en.html +++ b/adev-es/src/app/core/layout/footer/footer.component.en.html @@ -4,31 +4,31 @@

Social Media

-
+
@@ -486,16 +513,17 @@ - @if (activeRouteItem() === DOCS_ROUTE || activeRouteItem() === REFERENCE_ROUTE) { -
- -
+ @if (activeRouteItem() === PAGE_PREFIX.DOCS || activeRouteItem() === PAGE_PREFIX.REFERENCE) { +
+ +
}
diff --git a/adev-es/src/app/core/layout/navigation/navigation.component.html b/adev-es/src/app/core/layout/navigation/navigation.component.html index 2eef9c3..69530a6 100644 --- a/adev-es/src/app/core/layout/navigation/navigation.component.html +++ b/adev-es/src/app/core/layout/navigation/navigation.component.html @@ -1,6 +1,7 @@
@@ -26,6 +27,7 @@ @@ -166,8 +175,11 @@ -
  • - +
  • +
  • - - - - - -
  • - +
    -
    + -
    +
    @@ -382,16 +374,17 @@ - @if (activeRouteItem() === DOCS_ROUTE || activeRouteItem() === REFERENCE_ROUTE) { -
    - -
    + @if (activeRouteItem() === PAGE_PREFIX.DOCS || activeRouteItem() === PAGE_PREFIX.REFERENCE) { +
    + +
    } -
    \ No newline at end of file +
    diff --git a/adev-es/src/app/features/home/components/home-animation/home-animation.component.html b/adev-es/src/app/features/home/components/home-animation/home-animation.component.html deleted file mode 100644 index 18366f7..0000000 --- a/adev-es/src/app/features/home/components/home-animation/home-animation.component.html +++ /dev/null @@ -1,154 +0,0 @@ -
    - - - - @if (!isUwu()) { - -
    - - - - - - - - - - - - - - - -
    - } @else { - -
    - Angular logo -
    - } - - -
    -

    Trabaja a cualquier escala

    -

    - Angular te permite empezar poco a poco en un camino bien iluminado y - te apoya a medida que tu equipo y tus aplicaciones crecen. -

    -
    - - - @if (meteorFieldData(); as meteorFieldData) { -
    -
    - @for (type of meteors(); track $index) { -
    - } -
    -
    - } - - -
    -

    Amaado por millones

    -

    - Únete a los millones de desarrolladores de todo el mundo que - construyen con Angular en una comunidad próspera y amigable. -

    -
    - - -
    -

    Construye para todos

    -

    - Confía en la hidración, internacionalización, seguridad y accesibilidad - incorporadas de Angular para construir para todos en todo el mundo. -

    -
    -
    - \ No newline at end of file diff --git a/adev-es/src/app/features/home/home.component.en.html b/adev-es/src/app/features/home/home.component.en.html new file mode 100644 index 0000000..5267f29 --- /dev/null +++ b/adev-es/src/app/features/home/home.component.en.html @@ -0,0 +1,489 @@ +
    + +
    + @if (!isUwu) { + +
    + + + + + + + + + + + + + + + +

    The framework for building scalable web apps with confidence

    +
    + } @else { + +
    + Angular logo +
    + } + + +
    + +
    +

    Productivity meets scalability

    +
    +
    + + + + + + + + + + +

    AI-forward

    +

    Resources and integrations to supercharge your development with AI

    +
    +
    + + + +

    Opinionated & versatile

    +

    Organized yet modular thanks to Angular components and dependency injection

    +
    +
    + + + +

    Reactive

    +

    Fast state updates with fine-grained reactivity based on Angular Signals

    +
    +
    + + + +

    Fully featured

    +

    + Everything works together with Angular's first-party modules for forms, routing, and more +

    +
    +
    +
    + + + +
    +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    + + + +
    + +
    +
    diff --git a/adev-es/src/app/features/home/home.component.html b/adev-es/src/app/features/home/home.component.html index 77e4458..b5b398c 100644 --- a/adev-es/src/app/features/home/home.component.html +++ b/adev-es/src/app/features/home/home.component.html @@ -1,17 +1,488 @@ - +
    +
    + +
    +
    + @if (!isUwu) { + +
    + + + + + + + + + + + + + + + +

    El framework para crear aplicaciones web escalables con confianza

    +
    + } @else { + +
    + Angular logo +
    + } - -@if (!animationReady()) { -
    -} + +
    -
    -
    - @defer (when showEditor(); prefetch when prefetchEditor()) { - - } @loading { - Code editor - } -
    +
    +

    Productividad y escalabilidad

    +
    +
    + + + + + + + + + + +

    Impulsado por IA

    +

    Recursos e integraciones para potenciar tu desarrollo con IA

    +
    +
    + + + +

    Estructurado & versátil

    +

    Organizado y modular gracias a los componentes y la inyección de dependencias de Angular

    +
    +
    + + + +

    Reactivo

    +

    Actualizaciones de estado rápidas con reactividad basada en Angular Signals

    +
    +
    + + + +

    Completamente equipado

    +

    + Todo funciona en conjunto con los módulos nativos de Angular para formularios, ruteo y más +

    +
    +
    +
    + + + +
    +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    + + + +
    + +
    diff --git a/adev-es/src/app/sub-navigation-data.en.ts b/adev-es/src/app/routing/sub-navigation-data.en.ts similarity index 88% rename from adev-es/src/app/sub-navigation-data.en.ts rename to adev-es/src/app/routing/sub-navigation-data.en.ts index fe20e9e..ad1a5e2 100644 --- a/adev-es/src/app/sub-navigation-data.en.ts +++ b/adev-es/src/app/routing/sub-navigation-data.en.ts @@ -6,17 +6,19 @@ * found in the LICENSE file at https://angular.dev/license */ +import {isDevMode} from '@angular/core'; import {NavigationItem} from '@angular/docs'; // These 2 imports are expected to be red because they are generated a build time -import FIRST_APP_TUTORIAL_NAV_DATA from '../../src/assets/tutorials/first-app/routes.json'; -import LEARN_ANGULAR_TUTORIAL_NAV_DATA from '../../src/assets/tutorials/learn-angular/routes.json'; -import DEFERRABLE_VIEWS_TUTORIAL_NAV_DATA from '../../src/assets/tutorials/deferrable-views/routes.json'; -import ERRORS_NAV_DATA from '../../src/assets/content/reference/errors/routes.json'; -import EXT_DIAGNOSTICS_NAV_DATA from '../../src/assets/content/reference/extended-diagnostics/routes.json'; +import FIRST_APP_TUTORIAL_NAV_DATA from '../../../src/assets/tutorials/first-app/routes.json'; +import LEARN_ANGULAR_TUTORIAL_NAV_DATA from '../../../src/assets/tutorials/learn-angular/routes.json'; +import DEFERRABLE_VIEWS_TUTORIAL_NAV_DATA from '../../../src/assets/tutorials/deferrable-views/routes.json'; +import SIGNALS_TUTORIAL_NAV_DATA from '../../../src/assets/tutorials/signals/routes.json'; +import ERRORS_NAV_DATA from '../../../src/assets/content/reference/errors/routes.json'; +import EXT_DIAGNOSTICS_NAV_DATA from '../../../src/assets/content/reference/extended-diagnostics/routes.json'; -import {DefaultPage} from './core/enums/pages'; -import {getApiNavigationItems} from './features/references/helpers/manifest.helper'; +import {getApiNavigationItems} from '../features/references/helpers/manifest.helper'; +import {DEFAULT_PAGES} from '../core/constants/pages'; interface SubNavigationData { docs: NavigationItem[]; @@ -325,6 +327,7 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ }, { label: 'Routing', + status: 'updated', children: [ { label: 'Overview', @@ -361,6 +364,22 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ path: 'guide/routing/route-guards', contentPath: 'guide/routing/route-guards', }, + { + label: 'Route data resolvers', + path: 'guide/routing/data-resolvers', + contentPath: 'guide/routing/data-resolvers', + }, + { + label: 'Lifecycle and events', + path: 'guide/routing/lifecycle-and-events', + contentPath: 'guide/routing/lifecycle-and-events', + }, + { + label: 'Testing routing and navigation', + path: 'guide/routing/testing', + contentPath: 'guide/routing/testing', + status: 'new', + }, { label: 'Other routing tasks', path: 'guide/routing/common-router-tasks', @@ -371,11 +390,28 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ path: 'guide/routing/routing-with-urlmatcher', contentPath: 'guide/routing/routing-with-urlmatcher', }, + { + label: 'Rendering strategies', + path: 'guide/routing/rendering-strategies', + contentPath: 'guide/routing/rendering-strategies', + status: 'new', + }, + { + label: 'Customizing route behavior', + path: 'guide/routing/customizing-route-behavior', + contentPath: 'guide/routing/customizing-route-behavior', + status: 'new', + }, { label: 'Router reference', path: 'guide/routing/router-reference', contentPath: 'guide/routing/router-reference', }, + { + label: 'Route transition animations', + path: 'guide/routing/route-transition-animations', + contentPath: 'guide/routing/route-transition-animations', + }, ], }, { @@ -511,6 +547,12 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ path: 'guide/testing/pipes', contentPath: 'guide/testing/pipes', }, + { + label: 'Testing routing and navigation', + path: 'guide/routing/testing', + contentPath: 'guide/routing/testing', + status: 'new', + }, { label: 'Debugging tests', path: 'guide/testing/debugging', @@ -610,16 +652,23 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ }, { label: 'Animations', + status: 'updated', children: [ { - label: 'Animating your content', + label: 'Enter and Leave animations', + path: 'guide/animations', + contentPath: 'guide/animations/enter-and-leave', + status: 'new', + }, + { + label: 'Complex Animations with CSS', path: 'guide/animations/css', contentPath: 'guide/animations/css', }, { label: 'Route transition animations', - path: 'guide/animations/route-animations', - contentPath: 'guide/animations/route-animations', + path: 'guide/routing/route-transition-animations', + contentPath: 'guide/routing/route-transition-animations', }, ], }, @@ -632,6 +681,7 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ }, { label: 'Build with AI', + status: 'new', children: [ { label: 'Get Started', @@ -643,6 +693,16 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ path: 'ai/develop-with-ai', contentPath: 'ai/develop-with-ai', }, + { + label: 'Design Patterns', + path: 'ai/design-patterns', + contentPath: 'ai/design-patterns', + }, + { + label: 'Angular CLI MCP Server setup', + path: 'ai/mcp', + contentPath: 'ai/mcp-server-setup', + }, ], }, { @@ -755,8 +815,35 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ }, { label: 'DevTools', - path: 'tools/devtools', - contentPath: 'tools/devtools', + children: [ + { + label: 'Overview', + path: 'tools/devtools', + contentPath: 'tools/devtools/overview', + }, + { + label: 'Components', + path: 'tools/devtools/component', + contentPath: 'tools/devtools/component', + }, + { + label: 'Profiler', + path: 'tools/devtools/profiler', + contentPath: 'tools/devtools/profiler', + }, + // TODO: create those guides + // The signal debugging docs should also be added to the signal section + // { + // label: 'Signals', + // path: 'tools/devtools/signals', + // contentPath: 'tools/devtools/signals', + // }, + // { + // label: 'Router', + // path: 'tools/devtools/router', + // contentPath: 'tools/devtools/router', + // } + ], }, { label: 'Language Service', @@ -772,6 +859,7 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ label: 'Style Guide', path: 'style-guide', contentPath: 'best-practices/style-guide', + status: 'updated', }, { label: 'Security', @@ -835,26 +923,26 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ contentPath: 'guide/ngmodules/overview', }, { - label: 'Animations', + label: 'Legacy Animations', children: [ { label: 'Overview', - path: 'guide/animations', + path: 'guide/legacy-animations', contentPath: 'guide/animations/overview', }, { label: 'Transition and Triggers', - path: 'guide/animations/transition-and-triggers', + path: 'guide/legacy-animations/transition-and-triggers', contentPath: 'guide/animations/transition-and-triggers', }, { label: 'Complex Sequences', - path: 'guide/animations/complex-sequences', + path: 'guide/legacy-animations/complex-sequences', contentPath: 'guide/animations/complex-sequences', }, { label: 'Reusable Animations', - path: 'guide/animations/reusable-animations', + path: 'guide/legacy-animations/reusable-animations', contentPath: 'guide/animations/reusable-animations', }, { @@ -877,6 +965,11 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ path: 'ecosystem/rxjs-interop/output-interop', contentPath: 'ecosystem/rxjs-interop/output-interop', }, + { + label: 'Unsubscribing with takeUntilDestroyed', + path: 'ecosystem/rxjs-interop/take-until-destroyed', + contentPath: 'ecosystem/rxjs-interop/take-until-destroyed', + }, ], }, { @@ -892,6 +985,11 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ path: 'ecosystem/service-workers/getting-started', contentPath: 'ecosystem/service-workers/getting-started', }, + { + label: 'Custom service worker scripts', + path: 'ecosystem/service-workers/custom-service-worker-scripts', + contentPath: 'ecosystem/service-workers/custom-service-worker-scripts', + }, { label: 'Configuration file', path: 'ecosystem/service-workers/config', @@ -929,6 +1027,12 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ path: 'ecosystem/custom-build-pipeline', contentPath: 'ecosystem/custom-build-pipeline', }, + { + label: 'Tailwind', + path: 'guide/tailwind', + contentPath: 'guide/tailwind', + status: 'new', + }, { label: 'Angular Fire', path: 'https://github.com/angular/angularfire#readme', @@ -955,14 +1059,29 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ }, ], }, + ...(isDevMode() + ? [ + { + label: 'Adev Dev Guide', + children: [ + { + label: 'Kitchen Sink', + path: 'kitchen-sink', + contentPath: 'kitchen-sink', + }, + ], + }, + ] + : []), ]; export const TUTORIALS_SUB_NAVIGATION_DATA: NavigationItem[] = [ FIRST_APP_TUTORIAL_NAV_DATA, LEARN_ANGULAR_TUTORIAL_NAV_DATA, DEFERRABLE_VIEWS_TUTORIAL_NAV_DATA, + SIGNALS_TUTORIAL_NAV_DATA, { - path: DefaultPage.TUTORIALS, + path: DEFAULT_PAGES.TUTORIALS, contentPath: 'tutorials/home', label: 'Tutorials', }, @@ -1090,6 +1209,10 @@ const REFERENCE_SUB_NAVIGATION_DATA: NavigationItem[] = [ label: 'Overview', path: 'cli/generate', }, + { + label: 'ai-config', + path: 'cli/generate/ai-config', + }, { label: 'app-shell', path: 'cli/generate/app-shell', diff --git a/adev-es/src/app/sub-navigation-data.ts b/adev-es/src/app/routing/sub-navigation-data.ts similarity index 69% rename from adev-es/src/app/sub-navigation-data.ts rename to adev-es/src/app/routing/sub-navigation-data.ts index 402e770..fb15704 100644 --- a/adev-es/src/app/sub-navigation-data.ts +++ b/adev-es/src/app/routing/sub-navigation-data.ts @@ -6,14 +6,19 @@ * found in the LICENSE file at https://angular.dev/license */ +import {isDevMode} from '@angular/core'; import {NavigationItem} from '@angular/docs'; // These 2 imports are expected to be red because they are generated a build time -import FIRST_APP_TUTORIAL_NAV_DATA from '../../src/assets/tutorials/first-app/routes.json'; -import LEARN_ANGULAR_TUTORIAL_NAV_DATA from '../../src/assets/tutorials/learn-angular/routes.json'; +import FIRST_APP_TUTORIAL_NAV_DATA from '../../../src/assets/tutorials/first-app/routes.json'; +import LEARN_ANGULAR_TUTORIAL_NAV_DATA from '../../../src/assets/tutorials/learn-angular/routes.json'; +import DEFERRABLE_VIEWS_TUTORIAL_NAV_DATA from '../../../src/assets/tutorials/deferrable-views/routes.json'; +import SIGNALS_TUTORIAL_NAV_DATA from '../../../src/assets/tutorials/signals/routes.json'; +import ERRORS_NAV_DATA from '../../../src/assets/content/reference/errors/routes.json'; +import EXT_DIAGNOSTICS_NAV_DATA from '../../../src/assets/content/reference/extended-diagnostics/routes.json'; -import {DefaultPage} from './core/enums/pages'; -import {getApiNavigationItems} from './features/references/helpers/manifest.helper'; +import {getApiNavigationItems} from '../features/references/helpers/manifest.helper'; +import {DEFAULT_PAGES} from '../core/constants/pages'; interface SubNavigationData { docs: NavigationItem[]; @@ -37,7 +42,6 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ contentPath: 'introduction/installation', }, { - label: 'Esenciales', children: [ { @@ -81,6 +85,26 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ { label: 'Guías Detalladas', children: [ + { + label: 'Signals', + children: [ + { + label: 'Visión general', + path: 'guide/signals', + contentPath: 'guide/signals/overview', + }, + { + label: 'Estado dependiente con linkedSignal', + path: 'guide/signals/linked-signal', + contentPath: 'guide/signals/linked-signal', + }, + { + label: 'Reactividad asíncrona con resource', + path: 'guide/signals/resource', + contentPath: 'guide/signals/resource', + }, + ], + }, { label: 'Componentes', children: [ @@ -89,11 +113,6 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ path: 'guide/components', contentPath: 'guide/components/anatomy-of-components', }, - { - label: 'Importing and using components', - path: 'guide/components/importing', - contentPath: 'guide/components/importing', - }, { label: 'Selectores', path: 'guide/components/selectors', @@ -114,11 +133,6 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ path: 'guide/components/outputs', contentPath: 'guide/components/outputs', }, - { - label: 'output() function', - path: 'guide/components/output-fn', - contentPath: 'guide/components/output-function', - }, { label: 'Content projection with ng-content', path: 'guide/components/content-projection', @@ -167,7 +181,7 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ ], }, { - label: 'Template Syntax', + label: 'Templates', children: [ { label: 'Overview', @@ -175,99 +189,64 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ contentPath: 'guide/templates/overview', }, { - label: 'Text interpolation', - path: 'guide/templates/interpolation', - contentPath: 'guide/templates/interpolation', + label: 'Binding dynamic text, properties and attributes', + path: 'guide/templates/binding', + contentPath: 'guide/templates/binding', }, { - label: 'Template statements', - path: 'guide/templates/template-statements', - contentPath: 'guide/templates/template-statements', + label: 'Adding event listeners', + path: 'guide/templates/event-listeners', + contentPath: 'guide/templates/event-listeners', }, { - label: 'Understanding binding', - path: 'guide/templates/binding', - contentPath: 'guide/templates/binding', + label: 'Two-way binding', + path: 'guide/templates/two-way-binding', + contentPath: 'guide/templates/two-way-binding', }, { - label: 'Property binding', - path: 'guide/templates/property-binding', - contentPath: 'guide/templates/property-binding', + label: 'Control flow', + path: 'guide/templates/control-flow', + contentPath: 'guide/templates/control-flow', }, { - label: 'Property binding best practices', - path: 'guide/templates/property-binding-best-practices', - contentPath: 'guide/templates/property-binding-best-practices', + label: 'Pipes', + path: 'guide/templates/pipes', + contentPath: 'guide/templates/pipes', }, { - label: 'Attribute binding', - path: 'guide/templates/attribute-binding', - contentPath: 'guide/templates/attribute-binding', + label: 'Slotting child content with ng-content', + path: 'guide/templates/ng-content', + contentPath: 'guide/templates/ng-content', }, { - label: 'Class and style binding', - path: 'guide/templates/class-binding', - contentPath: 'guide/templates/class-binding', + label: 'Create template fragments with ng-template', + path: 'guide/templates/ng-template', + contentPath: 'guide/templates/ng-template', }, { - label: 'Event binding', - path: 'guide/templates/event-binding', - contentPath: 'guide/templates/event-binding', + label: 'Grouping elements with ng-container', + path: 'guide/templates/ng-container', + contentPath: 'guide/templates/ng-container', }, { - label: 'Two-way binding', - path: 'guide/templates/two-way-binding', - contentPath: 'guide/templates/two-way-binding', + label: 'Variables in templates', + path: 'guide/templates/variables', + contentPath: 'guide/templates/variables', }, { - label: 'Control flow', - path: 'guide/templates/control-flow', - contentPath: 'guide/templates/control-flow', + label: 'Deferred loading with @defer', + path: 'guide/templates/defer', + contentPath: 'guide/templates/defer', }, { - label: 'Pipes', - children: [ - { - label: 'Overview', - path: 'guide/pipes', - contentPath: 'guide/pipes/overview', - }, - { - label: 'Using a pipe in a template', - path: 'guide/pipes/template', - contentPath: 'guide/pipes/template', - }, - { - label: 'Custom pipes', - path: 'guide/pipes/transform-data', - contentPath: 'guide/pipes/transform-data', - }, - { - label: 'Pipe precedence in expressions', - path: 'guide/pipes/precedence', - contentPath: 'guide/pipes/precedence', - }, - { - label: 'Change detection with pipes', - path: 'guide/pipes/change-detection', - contentPath: 'guide/pipes/change-detection', - }, - { - label: 'Unwrapping data from an observable', - path: 'guide/pipes/unwrapping-data-observables', - contentPath: 'guide/pipes/unwrapping-data-observables', - }, - ], - }, - { - label: 'Template reference variables', - path: 'guide/templates/reference-variables', - contentPath: 'guide/templates/reference-variables', - }, - { - label: 'SVG as templates', - path: 'guide/templates/svg-in-templates', - contentPath: 'guide/templates/svg-in-templates', + label: 'Expression syntax', + path: 'guide/templates/expression-syntax', + contentPath: 'guide/templates/expression-syntax', + }, + { + label: 'Whitespace in templates', + path: 'guide/templates/whitespace', + contentPath: 'guide/templates/whitespace', }, ], }, @@ -294,6 +273,11 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ path: 'guide/directives/directive-composition-api', contentPath: 'guide/directives/directive-composition-api', }, + { + label: 'Optimizing images with NgOptimizedImage', + path: 'guide/image-optimization', + contentPath: 'guide/image-optimization', + }, ], }, { @@ -342,53 +326,92 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ ], }, { - label: 'Signals', + label: 'Routing', + status: 'updated', children: [ { - label: 'Visión general', - path: 'guide/signals', - contentPath: 'guide/signals/overview', + label: 'Overview', + path: 'guide/routing', + contentPath: 'guide/routing/overview', }, { - label: 'Estado dependiente con linkedSignal', - path: 'guide/signals/linked-signal', - contentPath: 'guide/signals/linked-signal', + label: 'Define routes', + path: 'guide/routing/define-routes', + contentPath: 'guide/routing/define-routes', }, { - label: 'Reactividad asíncrona con resource', - path: 'guide/signals/resource', - contentPath: 'guide/signals/resource', + label: 'Show routes with Outlets', + path: 'guide/routing/show-routes-with-outlets', + contentPath: 'guide/routing/show-routes-with-outlets', }, - ], - }, - { - label: 'Routing', - children: [ { - label: 'Overview', - path: 'guide/routing', - contentPath: 'guide/routing/overview', + label: 'Navigate to routes', + path: 'guide/routing/navigate-to-routes', + contentPath: 'guide/routing/navigate-to-routes', }, { - label: 'Common routing tasks', - path: 'guide/routing/common-router-tasks', - contentPath: 'guide/routing/common-router-tasks', + label: 'Read route state', + path: 'guide/routing/read-route-state', + contentPath: 'guide/routing/read-route-state', }, { - label: 'Routing in single-page applications', - path: 'guide/routing/router-tutorial', - contentPath: 'guide/routing/router-tutorial', + label: 'Redirecting routes', + path: 'guide/routing/redirecting-routes', + contentPath: 'guide/routing/redirecting-routes', + }, + { + label: 'Control route access with guards', + path: 'guide/routing/route-guards', + contentPath: 'guide/routing/route-guards', + }, + { + label: 'Route data resolvers', + path: 'guide/routing/data-resolvers', + contentPath: 'guide/routing/data-resolvers', + }, + { + label: 'Lifecycle and events', + path: 'guide/routing/lifecycle-and-events', + contentPath: 'guide/routing/lifecycle-and-events', + }, + { + label: 'Testing routing and navigation', + path: 'guide/routing/testing', + contentPath: 'guide/routing/testing', + status: 'new', + }, + { + label: 'Other routing tasks', + path: 'guide/routing/common-router-tasks', + contentPath: 'guide/routing/common-router-tasks', }, { label: 'Creating custom route matches', path: 'guide/routing/routing-with-urlmatcher', contentPath: 'guide/routing/routing-with-urlmatcher', }, + { + label: 'Rendering strategies', + path: 'guide/routing/rendering-strategies', + contentPath: 'guide/routing/rendering-strategies', + status: 'new', + }, + { + label: 'Customizing route behavior', + path: 'guide/routing/customizing-route-behavior', + contentPath: 'guide/routing/customizing-route-behavior', + status: 'new', + }, { label: 'Router reference', path: 'guide/routing/router-reference', contentPath: 'guide/routing/router-reference', }, + { + label: 'Route transition animations', + path: 'guide/routing/route-transition-animations', + contentPath: 'guide/routing/route-transition-animations', + }, ], }, { @@ -462,33 +485,28 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ ], }, { - label: 'Performance', + label: 'Server-side & hybrid-rendering', children: [ { - label: 'Deferrable views', - path: 'guide/defer', - contentPath: 'guide/defer', - }, - { - label: 'Image Optimization', - path: 'guide/image-optimization', - contentPath: 'guide/image-optimization', + label: 'Overview', + path: 'guide/performance', + contentPath: 'guide/performance/overview', }, { - label: 'Server-side Rendering', + label: 'Server-side and hybrid-rendering', path: 'guide/ssr', contentPath: 'guide/ssr', }, - { - label: 'Build-time prerendering', - path: 'guide/prerendering', - contentPath: 'guide/prerendering', - }, { label: 'Hydration', path: 'guide/hydration', contentPath: 'guide/hydration', }, + { + label: 'Incremental Hydration', + path: 'guide/incremental-hydration', + contentPath: 'guide/incremental-hydration', + }, ], }, { @@ -529,6 +547,12 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ path: 'guide/testing/pipes', contentPath: 'guide/testing/pipes', }, + { + label: 'Testing routing and navigation', + path: 'guide/routing/testing', + contentPath: 'guide/routing/testing', + status: 'new', + }, { label: 'Debugging tests', path: 'guide/testing/debugging', @@ -539,6 +563,31 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ path: 'guide/testing/utility-apis', contentPath: 'guide/testing/utility-apis', }, + { + label: 'Experimental unit testing integration', + path: 'guide/testing/unit-tests', + contentPath: 'guide/testing/experimental-unit-test', + }, + { + label: 'Component harnesses overview', + path: 'guide/testing/component-harnesses-overview', + contentPath: 'guide/testing/component-harnesses-overview', + }, + { + label: 'Using component harnesses in tests', + path: 'guide/testing/using-component-harnesses', + contentPath: 'guide/testing/using-component-harnesses', + }, + { + label: 'Creating harnesses for your components', + path: 'guide/testing/creating-component-harnesses', + contentPath: 'guide/testing/creating-component-harnesses', + }, + { + label: 'Adding harness support for additional testing environments', + path: 'guide/testing/component-harnesses-testing-environments', + contentPath: 'guide/testing/component-harnesses-testing-environments', + }, ], }, { @@ -603,31 +652,23 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ }, { label: 'Animations', + status: 'updated', children: [ { - label: 'Overview', + label: 'Enter and Leave animations', path: 'guide/animations', - contentPath: 'guide/animations/overview', + contentPath: 'guide/animations/enter-and-leave', + status: 'new', }, { - label: 'Transition and Triggers', - path: 'guide/animations/transition-and-triggers', - contentPath: 'guide/animations/transition-and-triggers', - }, - { - label: 'Complex Sequences', - path: 'guide/animations/complex-sequences', - contentPath: 'guide/animations/complex-sequences', - }, - { - label: 'Reusable Animations', - path: 'guide/animations/reusable-animations', - contentPath: 'guide/animations/reusable-animations', + label: 'Complex Animations with CSS', + path: 'guide/animations/css', + contentPath: 'guide/animations/css', }, { label: 'Route transition animations', - path: 'guide/animations/route-animations', - contentPath: 'guide/animations/route-animations', + path: 'guide/routing/route-transition-animations', + contentPath: 'guide/routing/route-transition-animations', }, ], }, @@ -636,11 +677,31 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ path: 'guide/drag-drop', contentPath: 'guide/drag-drop', }, + ], + }, + { + label: 'Build with AI', + status: 'new', + children: [ { - label: 'Experimental features', - children: [ - {label: 'Zoneless', path: 'guide/experimental/zoneless', contentPath: 'guide/zoneless'}, - ], + label: 'Get Started', + path: 'ai', + contentPath: 'ai/overview', + }, + { + label: 'LLM prompts and AI IDE setup', + path: 'ai/develop-with-ai', + contentPath: 'ai/develop-with-ai', + }, + { + label: 'Design Patterns', + path: 'ai/design-patterns', + contentPath: 'ai/design-patterns', + }, + { + label: 'Angular CLI MCP Server setup', + path: 'ai/mcp', + contentPath: 'ai/mcp-server-setup', }, ], }, @@ -754,8 +815,35 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ }, { label: 'DevTools', - path: 'tools/devtools', - contentPath: 'tools/devtools', + children: [ + { + label: 'Overview', + path: 'tools/devtools', + contentPath: 'tools/devtools/overview', + }, + { + label: 'Components', + path: 'tools/devtools/component', + contentPath: 'tools/devtools/component', + }, + { + label: 'Profiler', + path: 'tools/devtools/profiler', + contentPath: 'tools/devtools/profiler', + }, + // TODO: create those guides + // The signal debugging docs should also be added to the signal section + // { + // label: 'Signals', + // path: 'tools/devtools/signals', + // contentPath: 'tools/devtools/signals', + // }, + // { + // label: 'Router', + // path: 'tools/devtools/router', + // contentPath: 'tools/devtools/router', + // } + ], }, { label: 'Language Service', @@ -771,6 +859,7 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ label: 'Style Guide', path: 'style-guide', contentPath: 'best-practices/style-guide', + status: 'updated', }, { label: 'Security', @@ -782,6 +871,11 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ path: 'best-practices/a11y', contentPath: 'best-practices/a11y', }, + { + label: 'Unhandled errors in Angular', + path: 'best-practices/error-handling', + contentPath: 'best-practices/error-handling', + }, { label: 'Performance', children: [ @@ -805,6 +899,12 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ path: 'best-practices/skipping-subtrees', contentPath: 'best-practices/runtime-performance/skipping-subtrees', }, + { + label: 'Profiling with the Chrome DevTools', + path: 'best-practices/profiling-with-chrome-devtools', + contentPath: 'best-practices/runtime-performance/profiling-with-chrome-devtools', + }, + {label: 'Zoneless', path: 'guide/zoneless', contentPath: 'guide/zoneless'}, ], }, { @@ -817,6 +917,61 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ { label: 'Extended Ecosystem', children: [ + { + label: 'NgModules', + path: 'guide/ngmodules/overview', + contentPath: 'guide/ngmodules/overview', + }, + { + label: 'Legacy Animations', + children: [ + { + label: 'Overview', + path: 'guide/legacy-animations', + contentPath: 'guide/animations/overview', + }, + { + label: 'Transition and Triggers', + path: 'guide/legacy-animations/transition-and-triggers', + contentPath: 'guide/animations/transition-and-triggers', + }, + { + label: 'Complex Sequences', + path: 'guide/legacy-animations/complex-sequences', + contentPath: 'guide/animations/complex-sequences', + }, + { + label: 'Reusable Animations', + path: 'guide/legacy-animations/reusable-animations', + contentPath: 'guide/animations/reusable-animations', + }, + { + label: 'Migrating to Native CSS Animations', + path: 'guide/animations/migration', + contentPath: 'guide/animations/migration', + }, + ], + }, + { + label: 'Using RxJS with Angular', + children: [ + { + label: 'Signals interop', + path: 'ecosystem/rxjs-interop', + contentPath: 'ecosystem/rxjs-interop/signals-interop', + }, + { + label: 'Component output interop', + path: 'ecosystem/rxjs-interop/output-interop', + contentPath: 'ecosystem/rxjs-interop/output-interop', + }, + { + label: 'Unsubscribing with takeUntilDestroyed', + path: 'ecosystem/rxjs-interop/take-until-destroyed', + contentPath: 'ecosystem/rxjs-interop/take-until-destroyed', + }, + ], + }, { label: 'Service Workers & PWAs', children: [ @@ -830,6 +985,11 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ path: 'ecosystem/service-workers/getting-started', contentPath: 'ecosystem/service-workers/getting-started', }, + { + label: 'Custom service worker scripts', + path: 'ecosystem/service-workers/custom-service-worker-scripts', + contentPath: 'ecosystem/service-workers/custom-service-worker-scripts', + }, { label: 'Configuration file', path: 'ecosystem/service-workers/config', @@ -862,6 +1022,17 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ path: 'ecosystem/web-workers', contentPath: 'ecosystem/web-workers', }, + { + label: 'Custom build pipeline', + path: 'ecosystem/custom-build-pipeline', + contentPath: 'ecosystem/custom-build-pipeline', + }, + { + label: 'Tailwind', + path: 'guide/tailwind', + contentPath: 'guide/tailwind', + status: 'new', + }, { label: 'Angular Fire', path: 'https://github.com/angular/angularfire#readme', @@ -880,21 +1051,37 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ }, { label: 'Angular CDK', - path: 'https://material.angular.io/cdk/categories', + path: 'https://material.angular.dev/cdk/categories', }, { label: 'Angular Material', - path: 'https://material.angular.io/', + path: 'https://material.angular.dev/', }, ], }, + ...(isDevMode() + ? [ + { + label: 'Adev Dev Guide', + children: [ + { + label: 'Kitchen Sink', + path: 'kitchen-sink', + contentPath: 'kitchen-sink', + }, + ], + }, + ] + : []), ]; export const TUTORIALS_SUB_NAVIGATION_DATA: NavigationItem[] = [ FIRST_APP_TUTORIAL_NAV_DATA, LEARN_ANGULAR_TUTORIAL_NAV_DATA, + DEFERRABLE_VIEWS_TUTORIAL_NAV_DATA, + SIGNALS_TUTORIAL_NAV_DATA, { - path: DefaultPage.TUTORIALS, + path: DEFAULT_PAGES.TUTORIALS, contentPath: 'tutorials/home', label: 'Tutorials', }, @@ -969,7 +1156,7 @@ const REFERENCE_SUB_NAVIGATION_DATA: NavigationItem[] = [ path: 'cli/cache', }, { - label: 'clear', + label: 'clean', path: 'cli/cache/clean', }, { @@ -1022,6 +1209,10 @@ const REFERENCE_SUB_NAVIGATION_DATA: NavigationItem[] = [ label: 'Overview', path: 'cli/generate', }, + { + label: 'ai-config', + path: 'cli/generate/ai-config', + }, { label: 'app-shell', path: 'cli/generate/app-shell', @@ -1042,6 +1233,10 @@ const REFERENCE_SUB_NAVIGATION_DATA: NavigationItem[] = [ label: 'config', path: 'cli/generate/config', }, + { + label: 'directive', + path: 'cli/generate/directive', + }, { label: 'enum', path: 'cli/generate/enum', @@ -1130,196 +1325,7 @@ const REFERENCE_SUB_NAVIGATION_DATA: NavigationItem[] = [ path: 'errors', contentPath: 'reference/errors/overview', }, - { - label: 'NG0100: Expression Changed After Checked', - path: 'errors/NG0100', - contentPath: 'reference/errors/NG0100', - }, - { - label: 'NG01101: Wrong Async Validator Return Type', - path: 'errors/NG01101', - contentPath: 'reference/errors/NG01101', - }, - { - label: 'NG01203: Missing value accessor', - path: 'errors/NG01203', - contentPath: 'reference/errors/NG01203', - }, - { - label: 'NG0200: Circular Dependency in DI', - path: 'errors/NG0200', - contentPath: 'reference/errors/NG0200', - }, - { - label: 'NG0201: No Provider Found', - path: 'errors/NG0201', - contentPath: 'reference/errors/NG0201', - }, - { - label: 'NG0203: `inject()` must be called from an injection context', - path: 'errors/NG0203', - contentPath: 'reference/errors/NG0203', - }, - { - label: 'NG0209: Invalid multi provider', - path: 'errors/NG0209', - contentPath: 'reference/errors/NG0209', - }, - { - label: 'NG02200: Missing Iterable Differ', - path: 'errors/NG02200', - contentPath: 'reference/errors/NG02200', - }, - { - label: 'NG02800: JSONP support in HttpClient configuration', - path: 'errors/NG02800', - contentPath: 'reference/errors/NG02800', - }, - { - label: 'NG0300: Selector Collision', - path: 'errors/NG0300', - contentPath: 'reference/errors/NG0300', - }, - { - label: 'NG0301: Export Not Found', - path: 'errors/NG0301', - contentPath: 'reference/errors/NG0301', - }, - { - label: 'NG0302: Pipe Not Found', - path: 'errors/NG0302', - contentPath: 'reference/errors/NG0302', - }, - { - label: `NG0403: Bootstrapped NgModule doesn't specify which component to initialize`, - path: 'errors/NG0403', - contentPath: 'reference/errors/NG0403', - }, - { - label: 'NG0500: Hydration Node Mismatch', - path: 'errors/NG0500', - contentPath: 'reference/errors/NG0500', - }, - { - label: 'NG0501: Hydration Missing Siblings', - path: 'errors/NG0501', - contentPath: 'reference/errors/NG0501', - }, - { - label: 'NG0502: Hydration Missing Node', - path: 'errors/NG0502', - contentPath: 'reference/errors/NG0502', - }, - { - label: 'NG0503: Hydration Unsupported Projection of DOM Nodes', - path: 'errors/NG0503', - contentPath: 'reference/errors/NG0503', - }, - { - label: 'NG0504: Skip hydration flag is applied to an invalid node', - path: 'errors/NG0504', - contentPath: 'reference/errors/NG0504', - }, - { - label: 'NG0505: No hydration info in server response', - path: 'errors/NG0505', - contentPath: 'reference/errors/NG0505', - }, - { - label: 'NG0506: NgZone remains unstable', - path: 'errors/NG0506', - contentPath: 'reference/errors/NG0506', - }, - { - label: 'NG0507: HTML content was altered after server-side rendering', - path: 'errors/NG0507', - contentPath: 'reference/errors/NG0507', - }, - { - label: 'NG0602: HTML content was altered after server-side rendering', - path: 'errors/NG0602', - contentPath: 'reference/errors/NG0602', - }, - { - label: 'NG05104: Root element was not found', - path: 'errors/NG05104', - contentPath: 'reference/errors/NG05104', - }, - { - label: 'NG0910: Unsafe bindings on an iframe element', - path: 'errors/NG0910', - contentPath: 'reference/errors/NG0910', - }, - { - label: 'NG0912: Component ID generation collision', - path: 'errors/NG0912', - contentPath: 'reference/errors/NG0912', - }, - { - label: 'NG0950: Required input is accessed before a value is set.', - path: 'errors/NG0950', - contentPath: 'reference/errors/NG0950', - }, - { - label: 'NG0951: Child query result is required but no value is available.', - path: 'errors/NG0951', - contentPath: 'reference/errors/NG0951', - }, - { - label: 'NG0955: Track expression resulted in duplicated keys for a given collection', - path: 'errors/NG0955', - contentPath: 'reference/errors/NG0955', - }, - { - label: 'NG0956: Tracking expression caused re-creation of the DOM structure', - path: 'errors/NG0956', - contentPath: 'reference/errors/NG0956', - }, - { - label: 'NG1001: Argument Not Literal', - path: 'errors/NG1001', - contentPath: 'reference/errors/NG1001', - }, - { - label: 'NG2003: Missing Token', - path: 'errors/NG2003', - contentPath: 'reference/errors/NG2003', - }, - { - label: 'NG2009: Invalid Shadow DOM selector', - path: 'errors/NG2009', - contentPath: 'reference/errors/NG2009', - }, - { - label: 'NG3003: Import Cycle Detected', - path: 'errors/NG3003', - contentPath: 'reference/errors/NG3003', - }, - { - label: 'NG05000: Hydration with unsupported Zone.js instance.', - path: 'errors/NG05000', - contentPath: 'reference/errors/NG05000', - }, - { - label: 'NG6100: NgModule.id Set to module.id anti-pattern', - path: 'errors/NG6100', - contentPath: 'reference/errors/NG6100', - }, - { - label: 'NG8001: Invalid Element', - path: 'errors/NG8001', - contentPath: 'reference/errors/NG8001', - }, - { - label: 'NG8002: Invalid Attribute', - path: 'errors/NG8002', - contentPath: 'reference/errors/NG8002', - }, - { - label: 'NG8003: Missing Reference Target', - path: 'errors/NG8003', - contentPath: 'reference/errors/NG8003', - }, + ...ERRORS_NAV_DATA, ], }, { @@ -1330,51 +1336,7 @@ const REFERENCE_SUB_NAVIGATION_DATA: NavigationItem[] = [ path: 'extended-diagnostics', contentPath: 'reference/extended-diagnostics/overview', }, - { - label: 'NG8101: Invalid Banana-in-Box', - path: 'extended-diagnostics/NG8101', - contentPath: 'reference/extended-diagnostics/NG8101', - }, - { - label: 'NG8102: Nullish coalescing not nullable', - path: 'extended-diagnostics/NG8102', - contentPath: 'reference/extended-diagnostics/NG8102', - }, - { - label: 'NG8103: Missing control flow directive', - path: 'extended-diagnostics/NG8103', - contentPath: 'reference/extended-diagnostics/NG8103', - }, - { - label: 'NG8104: Text attribute not binding', - path: 'extended-diagnostics/NG8104', - contentPath: 'reference/extended-diagnostics/NG8104', - }, - { - label: 'NG8105: Missing `let` keyword in an *ngFor expression', - path: 'extended-diagnostics/NG8105', - contentPath: 'reference/extended-diagnostics/NG8105', - }, - { - label: 'NG8106: Suffix not supported', - path: 'extended-diagnostics/NG8106', - contentPath: 'reference/extended-diagnostics/NG8106', - }, - { - label: 'NG8107: Optional chain not nullable', - path: 'extended-diagnostics/NG8107', - contentPath: 'reference/extended-diagnostics/NG8107', - }, - { - label: 'NG8108: ngSkipHydration should be a static attribute', - path: 'extended-diagnostics/NG8108', - contentPath: 'reference/extended-diagnostics/NG8108', - }, - { - label: 'NG8109: Signals must be invoked in template interpolations', - path: 'extended-diagnostics/NG8109', - contentPath: 'reference/extended-diagnostics/NG8109', - }, + ...EXT_DIAGNOSTICS_NAV_DATA, ], }, { @@ -1430,94 +1392,44 @@ const REFERENCE_SUB_NAVIGATION_DATA: NavigationItem[] = [ contentPath: 'reference/migrations/standalone', }, { - label: 'ModuleWithProviders', - path: 'reference/migrations/module-with-providers', - contentPath: 'reference/migrations/module-with-providers', + label: 'Control Flow Syntax', + path: 'reference/migrations/control-flow', + contentPath: 'reference/migrations/control-flow', }, { - label: 'Typed Forms', - path: 'reference/migrations/typed-forms', - contentPath: 'reference/migrations/typed-forms', + label: 'inject() Function', + path: 'reference/migrations/inject-function', + contentPath: 'reference/migrations/inject-function', }, { - label: 'Control Flow Syntax', - path: 'reference/migrations/control-flow', - contentPath: 'reference/migrations/control-flow', + label: 'Lazy-loaded routes', + path: 'reference/migrations/route-lazy-loading', + contentPath: 'reference/migrations/route-lazy-loading', }, - ], - }, - { - label: 'Concepts', - children: [ { - label: 'Overview', - path: 'reference/concepts', - contentPath: 'reference/concepts/overview', + label: 'Signal inputs', + path: 'reference/migrations/signal-inputs', + contentPath: 'reference/migrations/signal-inputs', }, { - label: 'NgModule', - children: [ - { - label: 'Overview', - path: 'guide/ngmodules', - contentPath: 'guide/ngmodules/overview', - }, - { - label: 'JS Modules vs NgModules', - path: 'guide/ngmodules/vs-jsmodule', - contentPath: 'guide/ngmodules/vs-jsmodule', - }, - { - label: 'Launching your app with a root module', - path: 'guide/ngmodules/bootstrapping', - contentPath: 'guide/ngmodules/bootstrapping', - }, - { - label: 'Sharing NgModules', - path: 'guide/ngmodules/sharing', - contentPath: 'guide/ngmodules/sharing', - }, - { - label: 'Frequently used NgModules', - path: 'guide/ngmodules/frequent', - contentPath: 'guide/ngmodules/frequent', - }, - { - label: 'Feature modules', - path: 'guide/ngmodules/feature-modules', - contentPath: 'guide/ngmodules/feature-modules', - }, - { - label: 'Types of feature modules', - path: 'guide/ngmodules/module-types', - contentPath: 'guide/ngmodules/module-types', - }, - { - label: 'Providing dependencies', - path: 'guide/ngmodules/providers', - contentPath: 'guide/ngmodules/providers', - }, - { - label: 'Singleton services', - path: 'guide/ngmodules/singleton-services', - contentPath: 'guide/ngmodules/singleton-services', - }, - { - label: 'Lazy-loading feature modules', - path: 'guide/ngmodules/lazy-loading', - contentPath: 'guide/ngmodules/lazy-loading', - }, - { - label: 'NgModule API', - path: 'guide/ngmodules/api', - contentPath: 'guide/ngmodules/api', - }, - { - label: 'NgModule FAQs', - path: 'guide/ngmodules/faq', - contentPath: 'guide/ngmodules/faq', - }, - ], + label: 'Outputs', + path: 'reference/migrations/outputs', + contentPath: 'reference/migrations/outputs', + }, + { + label: 'Signal queries', + path: 'reference/migrations/signal-queries', + contentPath: 'reference/migrations/signal-queries', + }, + { + label: 'Clean up unused imports', + path: 'reference/migrations/cleanup-unused-imports', + contentPath: 'reference/migrations/cleanup-unused-imports', + }, + { + label: 'Self-closing tags', + path: 'reference/migrations/self-closing-tags', + contentPath: 'reference/migrations/self-closing-tags', }, ], }, @@ -1543,4 +1455,4 @@ export const SUB_NAVIGATION_DATA: SubNavigationData = { reference: REFERENCE_SUB_NAVIGATION_DATA, tutorials: TUTORIALS_SUB_NAVIGATION_DATA, footer: FOOTER_NAVIGATION_DATA, -}; \ No newline at end of file +}; diff --git a/adev-es/src/content/ai/design-patterns.md b/adev-es/src/content/ai/design-patterns.md new file mode 100644 index 0000000..75ae54a --- /dev/null +++ b/adev-es/src/content/ai/design-patterns.md @@ -0,0 +1,172 @@ +# Design patterns for AI SDKs and signal APIs + +Interacting with AI and Large Language Model (LLM) APIs introduces unique challenges, such as managing asynchronous operations, handling streaming data, and designing a responsive user experience for potentially slow or unreliable network requests. Angular [signals](guide/signals) and the [`resource`](guide/signals/resource) API provide powerful tools to solve these problems elegantly. + +## Triggering requests with signals + +A common pattern when working with user-provided prompts is to separate the user's live input from the submitted value that triggers the API call. + +1. Store the user's raw input in one signal as they type +2. When the user submits (e.g., by clicking a button), update a second signal with contents of the first signal. +3. Use the second signal in the **`params`** field of your `resource`. + +This setup ensures the resource's **`loader`** function only runs when the user explicitly submits their prompt, not on every keystroke. You can use additional signal parameters, like a `sessionId` or `userId` (which can be useful for creating persistent LLM sessions), in the `loader` field. This way, the request always uses these parameters' current values without re-triggering the asynchronous function defined in the `loader` field. + +Many AI SDKs provide helper methods for making API calls. For example, the Genkit client library exposes a `runFlow` method for calling Genkit flows, which you can call from a resource's `loader`. For other APIs, you can use the [`httpResource`](guide/signals/resource#reactive-data-fetching-with-httpresource). + +The following example shows a `resource` that fetches parts of an AI-generated story. The `loader` is triggered only when the `storyInput` signal changes. + +```ts +// A resource that fetches three parts of an AI generated story +storyResource = resource({ + // The default value to use before the first request or on error + defaultValue: DEFAULT_STORY, + // The loader is re-triggered when this signal changes + params: () => this.storyInput(), + // The async function to fetch data + loader: ({params}): Promise => { + // The params value is the current value of the storyInput signal + const url = this.endpoint(); + return runFlow({ url, input: { + userInput: params, + sessionId: this.storyService.sessionId() // Read from another signal + }}); + } +}); +``` + +## Preparing LLM data for templates + +You can configure LLM APIs to return structured data. Strongly typing your `resource` to match the expected output from the LLM provides better type safety and editor autocompletion. + +To manage state derived from a resource, use a `computed` signal or `linkedSignal`. Because `linkedSignal` [provides access to prior values](guide/signals/linked-signal), it can serve a variety of AI-related use cases, including + * building a chat history + * preserving or customizing data that templates display while LLMs generate content + +In the example below, `storyParts` is a `linkedSignal` that appends the latest story parts returned from `storyResource` to the existing array of story parts. + +```ts +storyParts = linkedSignal({ + // The source signal that triggers the computation + source: () => this.storyResource.value().storyParts, + // The computation function + computation: (newStoryParts, previous) => { + // Get the previous value of this linkedSignal, or an empty array + const existingStoryParts = previous?.value || []; + // Return a new array with the old and new parts + return [...existingStoryParts, ...newStoryParts]; + } +}); +``` + +## Performance and user experience + +LLM APIs may be slower and more error-prone than conventional, more deterministic APIs. You can use several Angular features to build a performant and user-friendly interface. + +* **Scoped Loading:** place the `resource` in the component that directly uses the data. This helps limit change detection cycles (especially in zoneless applications) and prevents blocking other parts of your application. If data needs to be shared across multiple components, provide the `resource` from a service. +* **SSR and Hydration:** use Server-Side Rendering (SSR) with incremental hydration to render the initial page content quickly. You can show a placeholder for the AI-generated content and defer fetching the data until the component hydrates on the client. +* **Loading State:** use the `resource` `LOADING` [status](guide/signals/resource#resource-status) to show an indicator, like a spinner, while the request is in flight. This status covers both initial loads and reloads. +* **Error Handling and Retries:** use the `resource` [**`reload()`**](guide/signals/resource#reloading) method as a simple way for users to retry failed requests, may be more prevalent when relying on AI generated content. + +The following example demonstrates how to create a responsive UI to dynamically display an AI generated image with loading and retry functionality. + +```angular-html + +@if (imgResource.isLoading()) { +
    + +
    + +} @else if (imgResource.hasValue()) { + + +} @else { +
    + +

    Failed to load image. Click to retry.

    +
    +} +``` + + +## AI patterns in action: streaming chat responses +Interfaces often display partial results from LLM-based APIs incrementally as response data arrives. Angular's resource API provides the ability to stream responses to support this type of pattern. The `stream` property of `resource` accepts an asynchronous function you can use to apply updates to a signal value over time. The signal being updated represents the data being streamed. + +```ts +characters = resource({ + stream: async () => { + const data = signal>({value: ''}); + // Calls a Genkit streaming flow using the streamFlow method + // exposed by the Genkit client SDK + const response = streamFlow({ + url: '/streamCharacters', + input: 10 + }); + + (async () => { + for await (const chunk of response.stream) { + data.update((prev) => { + if ('value' in prev) { + return { value: `${prev.value} ${chunk}` }; + } else { + return { error: chunk as unknown as Error }; + } + }); + } + })(); + + return data; + } +}); +``` + +The `characters` member is updated asynchronously and can be displayed in the template. + +```angular-html +@if (characters.isLoading()) { +

    Loading...

    +} @else if (characters.hasValue()) { +

    {{characters.value()}}

    +} @else { +

    {{characters.error()}}

    +} +``` + +On the server side, in `server.ts` for example, the defined endpoint sends the data to be streamed to the client. The following code uses Gemini with the Genkit framework but this technique is applicable to other APIs that support streaming responses from LLMs: + +```ts +import { startFlowServer } from '@genkit-ai/express'; +import { genkit } from "genkit/beta"; +import { googleAI, gemini20Flash } from "@genkit-ai/googleai"; + +const ai = genkit({ plugins: [googleAI()] }); + +export const streamCharacters = ai.defineFlow({ + name: 'streamCharacters', + inputSchema: z.number(), + outputSchema: z.string(), + streamSchema: z.string(), + }, + async (count, { sendChunk }) => { + const { response, stream } = ai.generateStream({ + model: gemini20Flash, + config: { + temperature: 1, + }, + prompt: `Generate ${count} different RPG game characters.`, + }); + + (async () => { + for await (const chunk of stream) { + sendChunk(chunk.content[0].text!); + } + })(); + + return (await response).text; +}); + +startFlowServer({ + flows: [streamCharacters], +}); + +``` diff --git a/adev-es/src/content/ai/develop-with-ai.md b/adev-es/src/content/ai/develop-with-ai.md index 8c75f8e..fa7e252 100644 --- a/adev-es/src/content/ai/develop-with-ai.md +++ b/adev-es/src/content/ai/develop-with-ai.md @@ -26,11 +26,20 @@ Several editors, such as .instructions.md | Configure `.instructions.md` | | Windsurf | guidelines.md | Configure `guidelines.md` | +## Angular CLI MCP Server setup +The Angular CLI includes an experimental [Model Context Protocol (MCP) server](https://modelcontextprotocol.io/) that allows AI assistants in your development environment to interact with the Angular CLI. + +[**Learn how to set up the Angular CLI MCP Server**](/ai/mcp) + ## Providing Context with `llms.txt` `llms.txt` is a proposed standard for websites designed to help LLMs better understand and process their content. The Angular team has developed two versions of this file to help LLMs and tools that use LLMs for code generation to create better modern Angular code. * llms.txt - an index file providing links to key files and resources. -* llms-full.txt - a more robust compiled set of resources describing how Angular works and how to build Angular applications. +* llms-full.txt - a more robust compiled set of resources describing how Angular works and how to build Angular applications. Be sure [to check out the overview page](/ai) for more information on how to integrate AI into your Angular applications. + +## Web Codegen Scorer +The Angular team developed and open-sourced the [Web Codegen Scorer](https://github.com/angular/web-codegen-scorer), a tool to evaluate and score the quality of AI generated web code. You can use this tool to make evidence-based decisions relating to AI-generated code, such as fine-tuning prompts to improve the accuracy of LLM-generated code for Angular. These prompts can be included as system instructions for your AI tooling or as context with your prompt. You can also use this tool to compare the quality of code produced by different models and monitor quality over time as models and agents evolve. + diff --git a/adev-es/src/content/ai/mcp-server-setup.md b/adev-es/src/content/ai/mcp-server-setup.md new file mode 100644 index 0000000..d51831b --- /dev/null +++ b/adev-es/src/content/ai/mcp-server-setup.md @@ -0,0 +1,141 @@ +# Angular CLI MCP Server setup + +The Angular CLI includes an experimental [Model Context Protocol (MCP) server](https://modelcontextprotocol.io/) enabling AI assistants in your development environment to interact with the Angular CLI. We've included support for CLI powered code generation, adding packages, and more. + +## Available Tools + +The Angular CLI MCP server provides several tools to assist you in your development workflow. By default, the following tools are enabled: + +| Name | Description | `local-only` | `read-only` | +| :--------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------: | :---------: | +| `get_best_practices` | Retrieves the Angular Best Practices Guide. This guide is essential for ensuring that all code adheres to modern standards, including standalone components, typed forms, and modern control flow. | ✅ | ✅ | +| `list_projects` | Lists the names of all applications and libraries defined within an Angular workspace. It reads the `angular.json` configuration file to identify the projects. | ✅ | ✅ | +| `search_documentation` | Searches the official Angular documentation at https://angular.dev. This tool should be used to answer any questions about Angular, such as for APIs, tutorials, and best practices. | ❌ | ✅ | + +## Get Started + +To get started, run the following command in your terminal: + +```bash +ng mcp +``` + +When run from an interactive terminal, this command displays instructions on how to configure a host environment to use the MCP server. The following sections provide example configurations for several popular editors and tools. + +### Cursor + +Create a file named `.cursor/mcp.json` in your project's root and add the following configuration. You can also configure it globally in `~/.cursor/mcp.json`. + +```json +{ + "mcpServers": { + "angular-cli": { + "command": "npx", + "args": ["-y", "@angular/cli", "mcp"] + } + } +} +``` + +### Firebase Studio + +Create a file named `.idx/mcp.json` in your project's root and add the following configuration: + +```json +{ + "mcpServers": { + "angular-cli": { + "command": "npx", + "args": ["-y", "@angular/cli", "mcp"] + } + } +} +``` + +### Gemini CLI + +Create a file named `.gemini/settings.json` in your project's root and add the following configuration: + +```json +{ + "mcpServers": { + "angular-cli": { + "command": "npx", + "args": ["-y", "@angular/cli", "mcp"] + } + } +} +``` + +### JetBrains IDEs + +In JetBrains IDEs (like IntelliJ IDEA or WebStorm), after installing the JetBrains AI Assistant plugin, go to `Settings | Tools | AI Assistant | Model Context Protocol (MCP)`. Add a new server and select `As JSON`. Paste the following configuration, which does not use a top-level property for the server list. + +```json +{ + "name": "Angular CLI", + "command": "npx", + "args": [ + "-y", + "@angular/cli", + "mcp" + ] +} +``` + +### VS Code + +In your project's root, create a file named `.vscode/mcp.json` and add the following configuration. Note the use of the `servers` property. + +```json +{ + "servers": { + "angular-cli": { + "command": "npx", + "args": ["-y", "@angular/cli", "mcp"] + } + } +} +``` + +### Other IDEs + +For other IDEs, check your IDE's documentation for the proper location of the MCP configuration file (often `mcp.json`). The configuration should contain the following snippet. + +```json +{ + "mcpServers": { + "angular-cli": { + "command": "npx", + "args": ["-y", "@angular/cli", "mcp"] + } + } +} +``` + +## Command Options + +The `mcp` command can be configured with the following options passed as arguments in your IDE's MCP configuration: + +| Option | Type | Description | Default | +| :------------- | :-------- | :--------------------------------------------------------------------------------------------------------- | :------ | +| `--read-only` | `boolean` | Only register tools that do not make changes to the project. Your editor or coding agent may still perform edits. | `false` | +| `--local-only` | `boolean` | Only register tools that do not require an internet connection. Your editor or coding agent may still send data over the network. | `false` | + + +For example, to run the server in read-only mode in VS Code, you would update your `mcp.json` like this: + +```json +{ + "servers": { + "angular-cli": { + "command": "npx", + "args": ["-y", "@angular/cli", "mcp", "--read-only"] + } + } +} +``` + +## Feedback and New Ideas + +The Angular team welcomes your feedback on the existing MCP capabilities and any ideas you have for new tools or features. Please share your thoughts by opening an issue on the [angular/angular GitHub repository](https://github.com/angular/angular/issues). \ No newline at end of file diff --git a/adev-es/src/content/ai/overview.md b/adev-es/src/content/ai/overview.md index d816855..33314b8 100644 --- a/adev-es/src/content/ai/overview.md +++ b/adev-es/src/content/ai/overview.md @@ -35,7 +35,7 @@ Here are examples of how to build with Genkit and Angular: * [Agentic Apps with Genkit and Angular starter-kit](https://github.com/angular/examples/tree/main/genkit-angular-starter-kit)— New to building with AI? Start here with a basic app that features an agentic workflow. Perfect place to start for your first AI building experience. -* [Use Genkit in an Angular app](https://genkit.dev/docs/angular/)— Build a basic application that uses Genkit Flows, Angular and Gemini 2.0 Flash. This step-by-step walkthrough guides you through creating a full-stack Angular application with AI features. +* [Use Genkit in an Angular app](https://genkit.dev/docs/angular/)— Build a basic application that uses Genkit Flows, Angular and Gemini 2.5 Flash. This step-by-step walkthrough guides you through creating a full-stack Angular application with AI features. * [Dynamic Story Generator app](https://github.com/angular/examples/tree/main/genkit-angular-story-generator)— Learn to build an agentic Angular app powered by Genkit, Gemini and Imagen 3 to dynamically generate a story based on user interaction featuring beautiful image panels to accompany the events that take place. Start here if you'd like to experiment with a more advanced use-case. @@ -54,71 +54,17 @@ Here is an example of how to build with Firebase AI Logic and Angular: This example includes an [in-depth video walkthrough explaining the functionality and demonstrates how to add new features](https://youtube.com/live/4vfDz2al_BI). ### Build AI-powered applications with Gemini API and Angular -The [Gemini API](https://ai.google.dev/gemini-api/docs) provides access to state-of-the-art models from Google that supports audio, images, video, and text input. The models that are optimized for specific use cases, [learn more on the Gemini API documentation site](https://ai.google.dev/gemini-api/docs/models). +The [Gemini API](https://ai.google.dev/gemini-api/docs) provides access to state-of-the-art models from Google that support audio, images, video, and text input. These models that are optimized for specific use cases, [learn more on the Gemini API documentation site](https://ai.google.dev/gemini-api/docs/models). * [AI Text Editor Angular app template](https://github.com/FirebaseExtended/firebase-framework-tools/tree/main/starters/angular/ai-text-editor) - Use this template to start with a fully functioning text editor with AI-powered features like refining text, expanding text and formalizing text. This is a good starting point to gain experience with calling the Gemini API via HTTP. * [AI Chatbot app template](https://github.com/FirebaseExtended/firebase-framework-tools/tree/main/starters/angular/ai-chatbot) - This template starts with a chatbot user interface that communicates with the Gemini API via HTTP. -## AI patterns in action: Streaming chat responses -Having text appear as the response is received from the model is a common UI pattern for web apps using AI. You can achieve this asynchronous task with Angular's `resource` API. The `stream` property of `resource` accepts an asynchronous function you can use to apply updates to a signal value over time. The signal being updated represents the data being streamed. - -```ts -characters = resource({ - stream: async () => { - const data = signal<{ value: string } | { error: unknown }>({ - value: "", - }); - - fetch(this.url).then(async (response) => { - if (!response.body) return; - - for await (const chunk of response.body) { - const chunkText = this.decoder.decode(chunk); - data.update((prev) => { - if ("value" in prev) { - return { value: `${prev.value} ${chunkText}` }; - } else { - return { error: chunkText }; - } - }); - } - }); - - return data; - }, - }); - -``` - -The `characters` member is updated asynchronously and can be displayed in the template. - -```html -

    {{ characters.value() }}

    -``` - -On the server side, in `server.ts` for example, the defined endpoint sends the data to be streamed to the client. The following code uses the Gemini API but this technique is applicable to other tools and frameworks that support streaming responses from LLMs: - -```ts - app.get("/api/stream-response", async (req, res) => { - ai.models.generateContentStream({ - model: "gemini-2.0-flash", - contents: "Explain how AI works", - }).then(async (response) => { - for await (const chunk of response) { - res.write(chunk.text); - } - }); - }); - -``` -This example connects to the Gemini API but other APIs that support streaming responses can be used here as well. [You can find the complete example on the Angular Github](https://github.com/angular/examples/tree/main/streaming-example). - ## Best Practices ### Connecting to model providers and keeping your API Credentials Secure When connecting to model providers, it is important to keep your API secrets safe. *Never put your API key in a file that ships to the client, such as `environments.ts`*. -Your application's architecture determines which AI APIs and tools to choose. Specifically, choose based on whether or not your application is client-side or server-side. Tools such as Firebase AI Logic provide a secure connection to the model APIs for client-side code. If you want to use a different API than Firerbase AI Logic or prefer to use a different model provider, consider creating a proxy-server or even [Cloud Functions for Firebase](https://firebase.google.com/docs/functions) to serve as a proxy and not expose your API keys. +Your application's architecture determines which AI APIs and tools to choose. Specifically, choose based on whether or not your application is client-side or server-side. Tools such as Firebase AI Logic provide a secure connection to the model APIs for client-side code. If you want to use a different API than Firebase AI Logic or prefer to use a different model provider, consider creating a proxy-server or even [Cloud Functions for Firebase](https://firebase.google.com/docs/functions) to serve as a proxy and not expose your API keys. For an example of connecting using a client-side app, see the code: [Firebase AI Logic Angular example repository](https://github.com/angular/examples/tree/main/vertex-ai-firebase-angular-example). @@ -142,6 +88,14 @@ Because models can return non-deterministic results, your applications should be Even considering these strategies and techniques, sensible fallbacks should be incorporated in your application design. Follow existing standards of application resiliency. For example, it is not acceptable for an application to crash if a resource or API is not available. In that scenario, an error message is displayed to the user and, if applicable, options for next steps are also displayed. Building AI-powered applications requires the same consideration. Confirm that the response is aligned with the expected output and provide a "safe landing" in case it is not aligned by way of [graceful degradation](https://developer.mozilla.org/en-US/docs/Glossary/Graceful_degradation). This also applies to API outages for LLM providers. Consider this example: The LLM provider is not responding. A potential strategy to handle the outage is: -* Save the response from the user to used in a retry scenario (now or at a later time) +* Save the response from the user to use in a retry scenario (now or at a later time) * Alert the user to the outage with an appropriate message that doesn't reveal sensitive information * Resume the conversation at a later time once the services are available again. + +## Next steps + +To learn about LLM prompts and AI IDE setup, see the following guides: + + + + diff --git a/adev-es/src/content/best-practices/style-guide.md b/adev-es/src/content/best-practices/style-guide.md index fe9c834..63128d8 100644 --- a/adev-es/src/content/best-practices/style-guide.md +++ b/adev-es/src/content/best-practices/style-guide.md @@ -76,7 +76,7 @@ tests into a single `tests` directory. ### Organize your project by feature areas -Organize your project into subdirectories based on the features or your application or common themes +Organize your project into subdirectories based on the features of your application or common themes to the code in those directories. For example, the project structure for a movie theater site, MovieReel, might look like this: @@ -95,7 +95,7 @@ Avoid creating subdirectories based on the type of code that lives in those dire example, avoid creating directories like `components`, `directives`, and `services`. Avoid putting so many files into one directory that it becomes hard to read or navigate. As the -number files in a directory grows, consider splitting further into additional sub-directories. +number of files in a directory grows, consider splitting further into additional sub-directories. ### One concept per file @@ -164,8 +164,7 @@ accommodate [JavaScript-like expressions](guide/templates/expression-syntax). You should take advantage of these expressions to capture relatively straightforward logic directly in template expressions. -When the code in a template gets too complex, though, refactor logic into the TypeScript code ( -typically with a [computed](guide/signals#computed-signals)). +When the code in a template gets too complex, though, refactor logic into the TypeScript code (typically with a [computed](guide/signals#computed-signals)). There's no one hard-and-fast rule that determines what constitutes "complex". Use your best judgement. @@ -190,7 +189,7 @@ export class UserProfile { } ``` -### Use `readonly` on properties that are initialized by Angular +### Use `readonly` for properties that shouldn't change Mark component and directive properties initialized by Angular as `readonly`. This includes properties initialized by `input`, `model`, `output`, and queries. The readonly access modifier @@ -201,6 +200,7 @@ ensures that the value set by Angular is not overwritten. export class UserProfile { readonly userId = input(); readonly userSaved = output(); + readonly userName = model(); } ``` @@ -217,7 +217,7 @@ export class UserProfile { ### Prefer `class` and `style` over `ngClass` and `ngStyle` -Prefer `class` and `style` bindings over using the `NgClass` and `NgStyle` directives. +Prefer `class` and `style` bindings over using the [`NgClass`](/api/common/NgClass) and [`NgStyle`](/api/common/NgStyle) directives. ```html @@ -237,6 +237,8 @@ developers familiar with basic HTML. Additionally, the `NgClass` and `NgStyle` directives incur an additional performance cost compared to the built-in `class` and `style` binding syntax. +For more details, refer to the [bindings guide](/guide/templates/binding#css-class-and-style-property-bindings) + ### Name event handlers for what they _do_, not for the triggering event Prefer naming event handlers for the action they perform rather than for the triggering event: diff --git a/adev-es/src/content/ecosystem/rxjs-interop/signals-interop.md b/adev-es/src/content/ecosystem/rxjs-interop/signals-interop.md index 8e8a318..210311c 100644 --- a/adev-es/src/content/ecosystem/rxjs-interop/signals-interop.md +++ b/adev-es/src/content/ecosystem/rxjs-interop/signals-interop.md @@ -1,6 +1,6 @@ # RxJS interop with Angular signals -The `@angular/rxjs-interop` package offers APIs that help you integrate RxJS and Angular signals. +The `@angular/core/rxjs-interop` package offers APIs that help you integrate RxJS and Angular signals. ## Create a signal from an RxJs Observable with `toSignal` @@ -69,7 +69,7 @@ Use the `toObservable` utility to create an `Observable` which tracks the value import { Component, signal } from '@angular/core'; import { toObservable } from '@angular/core/rxjs-interop'; -@Component(...) +@Component(/* ... */) export class SearchResults { query: Signal = inject(QueryService).query; query$ = toObservable(this.query); @@ -102,3 +102,34 @@ mySignal.set(3); ``` Here, only the last value (3) will be logged. + +## Using `rxResource` for async data + +IMPORTANT: `rxResource` is [experimental](reference/releases#experimental). It's ready for you to try, but it might change before it is stable. + +Angular's [`resource` function](/guide/signals/resource) gives you a way to incorporate async data into your application's signal-based code. Building on top of this pattern, `rxResource` lets you define a resource where the source of your data is defined in terms of an RxJS `Observable`. Instead of accepting a `loader` function, `rxResource` accepts a `stream` function that accepts an RxJS `Observable`. + +```typescript +import {Component, inject} from '@angular/core'; +import {rxResource} from '@angular/core/rxjs-interop'; + +@Component(/* ... */) +export class UserProfile { + // This component relies on a service that exposes data through an RxJS Observable. + private userData = inject(MyUserDataClient); + + protected userId = input(); + + private userResource = rxResource({ + params: () => this.userId(), + + // The `stream` property expects a factory function that returns + // a data stream as an RxJS Observable. + stream: ({params}) => this.userData.load(params.userId), + }); +} +``` + +The `stream` property accepts a factory function for an RxJS `Observable`. This factory function is passed the resource's `params` value and returns an `Observable`. The resource calls this factory function every time the `params` computation produces a new value. See [Resource loaders](/guide/signals/resource#resource-loaders) for more details on the parameters passed to the factory function. + +In all other ways, `rxResource` behaves like and provides the same APIs as `resource` for specifying parameters, reading values, checking loading state, and examining errors. diff --git a/adev-es/src/content/ecosystem/rxjs-interop/take-until-destroyed.md b/adev-es/src/content/ecosystem/rxjs-interop/take-until-destroyed.md new file mode 100644 index 0000000..17dc37a --- /dev/null +++ b/adev-es/src/content/ecosystem/rxjs-interop/take-until-destroyed.md @@ -0,0 +1,46 @@ +# Unsubscribing with `takeUntilDestroyed` + +TIP: This guide assumes you're familiar with [component and directive lifecycle](guide/components/lifecycle). + +The `takeUntilDestroyed` operator, from `@angular/core/rxjs-interop`, provides a concise and reliable way to automatically unsubscribe from an Observable when a component or directive is destroyed. This prevents common memory leaks with RxJS subscriptions. It works similarly to the RxJS [`takeUntil`](https://rxjs.dev/api/operators/takeUntil) operator but without the need for a separate Subject. + +```typescript +import {Component, inject} from '@angular/core'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {NotificationDispatcher, CustomPopupShower} from './some-shared-project-code'; + +@Component(/* ... */) +export class UserProfile { + private dispatcher = inject(NotificationDispatcher); + private popup = inject(CustomPopupShower); + + constructor() { + // This subscription the 'notifications' Observable is automatically + // unsubscribed when the 'UserProfile' component is destroyed. + const messages: Observable = this.dispatcher.notifications; + messages.pipe(takeUntilDestroyed()).subscribe(message => { + this.popup.show(message); + }); + } +} +``` + +The `takeUntilDestroyed` operator accepts a single optional [`DestroyRef`](https://angular.dev/api/core/DestroyRef) argument. The operator uses `DestroyRef` to know when the component or directive has been destroyed. You can omit this argument when calling `takeUntilDestroyed` in an [injection context](https://angular.dev/guide/di/dependency-injection-context), typically the constructor of a component or directive. Always provide a `DestroyRef` if your code may call `takeUntilDestroyed` outside of an injection context. + +```typescript +@Component(/* ... */) +export class UserProfile { + private dispatcher = inject(NotificationDispatcher); + private popup = inject(CustomPopupShower); + private destroyRef = inject(DestroyRef); + + startListeningToNotifications() { + // Always pass a `DestroyRef` if you call `takeUntilDestroyed` outside + // of an injection context. + const messages: Observable = this.dispatcher.notifications; + messages.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(message => { + this.popup.show(message); + }); + } +} +``` diff --git a/adev-es/src/content/ecosystem/service-workers/communications.md b/adev-es/src/content/ecosystem/service-workers/communications.md index ea08f24..59ab2db 100644 --- a/adev-es/src/content/ecosystem/service-workers/communications.md +++ b/adev-es/src/content/ecosystem/service-workers/communications.md @@ -14,7 +14,7 @@ The `SwUpdate` service supports three separate operations: ### Version updates -The `versionUpdates` is an `Observable` property of `SwUpdate` and emits four event types: +The `versionUpdates` is an `Observable` property of `SwUpdate` and emits five event types: | Event types | Details | |:--- |:--- | @@ -22,6 +22,7 @@ The `versionUpdates` is an `Observable` property of `SwUpdate` and emits four ev | `NoNewVersionDetectedEvent` | Emitted when the service worker has checked the version of the app on the server and did not find a new version. | | `VersionReadyEvent` | Emitted when a new version of the app is available to be activated by clients. It may be used to notify the user of an available update or prompt them to refresh the page. | | `VersionInstallationFailedEvent` | Emitted when the installation of a new version failed. It may be used for logging/monitoring purposes. | +| `VersionFailedEvent` | Emitted when a version encounters a critical failure (such as broken hash errors) that affects all clients using that version. Provides error details for debugging and transparency. | diff --git a/adev-es/src/content/ecosystem/service-workers/custom-service-worker-scripts.md b/adev-es/src/content/ecosystem/service-workers/custom-service-worker-scripts.md new file mode 100644 index 0000000..8bafac8 --- /dev/null +++ b/adev-es/src/content/ecosystem/service-workers/custom-service-worker-scripts.md @@ -0,0 +1,111 @@ +# Custom service worker scripts + +While the Angular service worker provides excellent capabilities, you may need to add custom functionality such as handling push notifications, background sync, or other service worker events. You can create a custom service worker script that imports and extends the Angular service worker. + +## Creating a custom service worker + +To create a custom service worker that extends Angular's functionality: + +1. Create a custom service worker file (e.g., `custom-sw.js`) in your `src` directory: + + + +// Import the Angular service worker +importScripts('./ngsw-worker.js'); + +(function () { + 'use strict'; + + // Add custom notification click handler + self.addEventListener('notificationclick', (event) => { + console.log('Custom notification click handler'); + console.log('Notification details:', event.notification); + + // Handle notification click - open URL if provided + if (clients.openWindow && event.notification.data.url) { + event.waitUntil(clients.openWindow(event.notification.data.url)); + console.log('Opening URL:', event.notification.data.url); + } + }); + + // Add custom background sync handler + self.addEventListener('sync', (event) => { + console.log('Custom background sync handler'); + + if (event.tag === 'background-sync') { + event.waitUntil(doBackgroundSync()); + } + }); + + function doBackgroundSync() { + // Implement your background sync logic here + return fetch('https://example.com/api/sync') + .then(response => response.json()) + .then(data => console.log('Background sync completed:', data)) + .catch(error => console.error('Background sync failed:', error)); + } +})(); + + + +2. Update your `angular.json` file to use the custom service worker: + + + +{ + "projects": { + "your-app": { + "architect": { + "build": { + "options": { + "assets": [ + { + "glob": "**/*", + "input": "public" + }, + "app/src/custom-sw.js" + ] + }, + } + } + } + } +} + + + +3. Configure the service worker registration to use your custom script: + + + +import { ApplicationConfig, isDevMode } from '@angular/core'; +import { provideServiceWorker } from '@angular/service-worker'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideServiceWorker('custom-sw.js', { + enabled: !isDevMode(), + registrationStrategy: 'registerWhenStable:30000' + }), + ], +}; + + + +### Best practices for custom service workers + +When extending the Angular service worker: + +- **Always import the Angular service worker first** using `importScripts('./ngsw-worker.js')` to ensure you get all the caching and update functionality +- **Wrap your custom code in an IIFE** (Immediately Invoked Function Expression) to avoid polluting the global scope +- **Use `event.waitUntil()`** for asynchronous operations to ensure they complete before the service worker is terminated +- **Test thoroughly** in both development and production environments +- **Handle errors gracefully** to prevent your custom code from breaking the Angular service worker functionality + +### Common use cases + +Custom service workers are commonly used for: + +- **Push notifications**: Handle incoming push messages and display notifications +- **Background sync**: Sync data when the network connection is restored +- **Custom navigation**: Handle special routing or offline page scenarios diff --git a/adev-es/src/content/ecosystem/service-workers/getting-started.md b/adev-es/src/content/ecosystem/service-workers/getting-started.md index c75039a..aedaeb7 100644 --- a/adev-es/src/content/ecosystem/service-workers/getting-started.md +++ b/adev-es/src/content/ecosystem/service-workers/getting-started.md @@ -159,6 +159,159 @@ Now look at how the browser and service worker handle the updated application. The service worker installed the updated version of your application _in the background_, and the next time the page is loaded or reloaded, the service worker switches to the latest version. +## Service worker configuration + +Angular service workers support comprehensive configuration options through the `SwRegistrationOptions` interface, providing fine-grained control over registration behavior, caching, and script execution. + +### Enabling and disabling service workers + +The `enabled` option controls whether the service worker will be registered and related services will attempt to communicate with it. + + + +import { ApplicationConfig, isDevMode } from '@angular/core'; +import { provideServiceWorker } from '@angular/service-worker'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideServiceWorker('ngsw-worker.js', { + enabled: !isDevMode(), // Disable in development, enable in production + }), + ], +}; + + + + +### Cache control with updateViaCache + +The `updateViaCache` option controls how the browser consults the HTTP cache during service worker updates. This provides fine-grained control over when the browser fetches updated service worker scripts and imported modules. + + + +export const appConfig: ApplicationConfig = { + providers: [ + provideServiceWorker('ngsw-worker.js', { + enabled: !isDevMode(), + updateViaCache: 'imports', + }), + ], +}; + + + +The `updateViaCache` option accepts the following values: + +* **`'imports'`** - The HTTP cache is consulted for the service worker script's imported scripts, but not for the service worker script itself +* **`'all'`** - The HTTP cache is consulted for both the service worker script and its imported scripts +* **`'none'`** - The HTTP cache is not consulted for the service worker script or its imported scripts + +### ES Module support with type option + +The `type` option enables specifying the script type when registering service workers, providing support for ES module features in your service worker scripts. + + + +export const appConfig: ApplicationConfig = { + providers: [ + provideServiceWorker('ngsw-worker.js', { + enabled: !isDevMode(), + type: 'module', // Enable ES module features + }), + ], +}; + + + +The `type` option accepts the following values: + +* **`'classic'`** (default) - Traditional service worker script execution. ES module features such as `import` and `export` are NOT allowed in the script +* **`'module'`** - Registers the script as an ES module. Allows use of `import`/`export` syntax and module features + +### Registration scope control + +The `scope` option defines the service worker's registration scope, determining what range of URLs it can control. + + + +export const appConfig: ApplicationConfig = { + providers: [ + provideServiceWorker('ngsw-worker.js', { + enabled: !isDevMode(), + scope: '/app/', // Service worker will only control URLs under /app/ + }), + ], +}; + + + +* Controls which URLs the service worker can intercept and manage +* By default, the scope is the directory containing the service worker script +* Used when calling `ServiceWorkerContainer.register()` + +### Registration strategy configuration + +The `registrationStrategy` option defines when the service worker will be registered with the browser, providing control over the timing of registration. + + + +export const appConfig: ApplicationConfig = { + providers: [ + provideServiceWorker('ngsw-worker.js', { + enabled: !isDevMode(), + registrationStrategy: 'registerWhenStable:30000', + }), + ], +}; + + + +Available registration strategies: + +* **`'registerWhenStable:timeout'`** (default: `'registerWhenStable:30000'`) - Register as soon as the application stabilizes (no pending micro-/macro-tasks) but no later than the specified timeout in milliseconds +* **`'registerImmediately'`** - Register the service worker immediately +* **`'registerWithDelay:timeout'`** - Register with a delay of the specified timeout in milliseconds + + + +// Register immediately +export const immediateConfig: ApplicationConfig = { + providers: [ + provideServiceWorker('ngsw-worker.js', { + enabled: !isDevMode(), + registrationStrategy: 'registerImmediately', + }), + ], +}; + +// Register with a 5-second delay +export const delayedConfig: ApplicationConfig = { + providers: [ + provideServiceWorker('ngsw-worker.js', { + enabled: !isDevMode(), + registrationStrategy: 'registerWithDelay:5000', + }), + ], +}; + + + +You can also provide an Observable factory function for custom registration timing: + + +import { timer } from 'rxjs'; + +export const customConfig: ApplicationConfig = { + providers: [ + provideServiceWorker('ngsw-worker.js', { + enabled: !isDevMode(), + registrationStrategy: () => timer(10_000), // Register after 10 seconds + }), + ], +}; + + + ## More on Angular service workers You might also be interested in the following: diff --git a/adev-es/src/content/ecosystem/service-workers/overview.md b/adev-es/src/content/ecosystem/service-workers/overview.md index 3ce87df..6e0fe6e 100644 --- a/adev-es/src/content/ecosystem/service-workers/overview.md +++ b/adev-es/src/content/ecosystem/service-workers/overview.md @@ -1,5 +1,7 @@ # Angular service worker overview +IMPORTANT: The Angular Service Worker is a basic caching utility for simple offline support with a limited featureset. We will not be accepting any new features other than security fixes. For more advanced caching and offline capabilities, we recommend exploring native browser APIs directly. + Service workers augment the traditional web deployment model and empower applications to deliver a user experience with the reliability and performance on par with code that is written to run on your operating system and hardware. Adding a service worker to an Angular application is one of the steps for turning an application into a [Progressive Web App](https://web.dev/progressive-web-apps/) (also known as a PWA). diff --git a/adev-es/src/content/guide/animations/complex-sequences.md b/adev-es/src/content/guide/animations/complex-sequences.md index de743b5..4eb1da6 100644 --- a/adev-es/src/content/guide/animations/complex-sequences.md +++ b/adev-es/src/content/guide/animations/complex-sequences.md @@ -1,6 +1,6 @@ # Complex animation sequences -IMPORTANT: The Angular team recommends using native CSS for animations instead of the Animations package for all new code. Use this guide to understand existing code built with the Animations Package. See [Migrating away from Angular's Animations package](guide/animations/migration#complex-sequences) to learn how you can start using pure CSS animations in your apps. +IMPORTANT: The `@angular/animations` package is now deprecated. The Angular team recommends using native CSS with `animate.enter` and `animate.leave` for animations for all new code. Learn more at the new enter and leave [animation guide](guide/animations/enter-and-leave). Also see [Migrating away from Angular's Animations package](guide/animations/migration) to learn how you can start migrating to pure CSS animations in your apps. So far, we've learned simple animations of single HTML elements. Angular also lets you animate coordinated sequences, such as an entire grid or list of elements as they enter and leave a page. @@ -37,7 +37,7 @@ The first argument of `query()` is a [css selector](https://developer.mozilla.or Not all child elements are actually considered as entering/leaving; this can, at times, be counterintuitive and confusing. Please see the [query api docs](api/animations/query#entering-and-leaving-elements) for more information. -You can also see an illustration of this in the animations example \(introduced in the animations [introduction section](guide/animations#about-this-guide)\) under the Querying tab. +You can also see an illustration of this in the animations example \(introduced in the animations [introduction section](guide/legacy-animations#about-this-guide)\) under the Querying tab. @@ -137,8 +137,9 @@ The remaining functions, `stagger()`, [`group()`](api/animations/group), and `se You might also be interested in the following: - - - - + + + + + diff --git a/adev-es/src/content/guide/animations/css.md b/adev-es/src/content/guide/animations/css.md index b94a2a6..64e6635 100644 --- a/adev-es/src/content/guide/animations/css.md +++ b/adev-es/src/content/guide/animations/css.md @@ -74,7 +74,7 @@ If you don't have to worry about supporting all browsers, you can also check out ### Animate entering and leaving a view -You can create animations for when an item enters a view or leaves a view. Let's start by looking at how to animate an element leaving a view. +You can create animations for when an item enters a view or leaves a view. Let's start by looking at how to animate an element entering a view. We'll do this with `animate.enter`, which will apply animation classes when an element enters the view. @@ -82,7 +82,7 @@ You can create animations for when an item enters a view or leaves a view. Let's -Leaving a view is slightly more complex. The element removal needs to be delayed until the exit animation is complete. This requires a bit of extra code in your component class to accomplish. +Animating an element when it leaves the view is similar to animating when entering a view. Use `animate.leave` to specify which CSS classes to apply when the element leaves the view. @@ -90,6 +90,8 @@ Leaving a view is slightly more complex. The element removal needs to be delayed +For more information on `animate.enter` and `animate.leave`, see the [Enter and Leave animations guide](guide/animations). + ### Animating increment and decrement Animating on increment and decrement is a common pattern in applications. Here's an example of how you can accomplish that behavior. @@ -165,7 +167,7 @@ In this example, the `rotate` and `fade-in` animations fire at the same time, bu ### Animating the items of a reordering list -Items in a `@for` loop will be removed and re-added, which will fire off animations using `@starting-styles` for entry animations. Removal animations will require additional code to add the event listener, as seen in the example above. +Items in a `@for` loop will be removed and re-added, which will fire off animations using `@starting-styles` for entry animations. Alternatively, you can use `animate.enter` for this same behavior. Use `animate.leave` to animate elements as they are removed, as seen in the example below. @@ -176,3 +178,12 @@ Items in a `@for` loop will be removed and re-added, which will fire off animati ## Programmatic control of animations You can retrieve animations off an element directly using [`Element.getAnimations()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/getAnimations). This returns an array of every [`Animation`](https://developer.mozilla.org/en-US/docs/Web/API/Animation) on that element. You can use the `Animation` API to do much more than you could with what the `AnimationPlayer` from the animations package offered. From here you can `cancel()`, `play()`, `pause()`, `reverse()` and much more. This native API should provide everything you need to control your animations. + +## More on Angular animations + +You might also be interested in the following: + + + + + diff --git a/adev-es/src/content/guide/animations/enter-and-leave.md b/adev-es/src/content/guide/animations/enter-and-leave.md new file mode 100644 index 0000000..5059af9 --- /dev/null +++ b/adev-es/src/content/guide/animations/enter-and-leave.md @@ -0,0 +1,99 @@ +# Animating your applications with `animate.enter` and `animate.leave` + +Well-designed animations can make your application more fun and straightforward to use, but they aren't just cosmetic. +Animations can improve your application and user experience in a number of ways: + +* Without animations, web page transitions can seem abrupt and jarring +* Motion greatly enhances the user experience, so animations give users a chance to detect the application's response to their actions +* Good animations can smoothly direct the user's attention throughout a workflow + +Angular provides `animate.enter` and `animate.leave` to animate your application's elements. These two features apply enter and leave CSS classes at the appropriate times or call functions to apply animations from third party libraries. `animate.enter` and `animate.leave` are not directives. They are special API supported directly by the Angular compiler. They can be used on elements directly and can also be used as a host binding. + +## `animate.enter` + +You can use `animate.enter` to animate elements as they _enter_ the DOM. You can define enter animations using CSS classes with either transitions or keyframe animations. + + + + + + + +When the animation completes, Angular removes the class or classes that you specified in `animate.enter` from the DOM. Animation classes are only be present while the animation is active. + +NOTE: When using multiple keyframe animations or transition properties on an element, Angular removes all classes only _after_ the longest animation has completed. + +You can use `animate.enter` with any other Angular features, such as control flow or dynamic expressions. `animate.enter` accepts both a single class string (with multiple classes separated by spaces), or an array of class strings. + +A quick note about using CSS transitions: If you choose to use transitions instead of keyframe animations, the classes added to the element with `animate.enter` represent the state that the transition will animate _to_. Your base element CSS is what the element will look like when no animations run, which is likely similar to the end state of the CSS transition. So you would still need to pair it with `@starting-style` to have an appropriate _from_ state for your transition to work. + + + + + + + +## `animate.leave` + +You can use `animate.leave` to animate elements as they _leave_ the DOM. You can define leave animations using CSS classes with either transforms or keyframe animations. + + + + + + + +When the animation completes, Angular automatically removes the animated element from the DOM. + +NOTE: When using multiple keyframe animations or transition properties on a an element, Angular waits to remove the element only _after_ the longest of those animations has completed. + +`animate.leave` can also be used with signals, and other bindings. You can use `animate.leave` with a single class or multiple classes. Either specify it as a simple string with spaces or a string array. + + + + + + + +## Event Bindings, Functions, and Third-party Libraries + +Both `animate.enter` and `animate.leave` support event binding syntax that allows for function calls. You can use this syntax to call a function in your component code or utilize third-party animation libraries, like [GSAP](https://gsap.com/), [anime.js](https://animejs.com/), or any other JavaScript animation library. + + + + + + + +The `$event` object has the type `AnimationCallbackEvent`. It includes the element as the `target` and provides an `animationComplete()` function to notify the framework when the animation finishes. + +IMPORTANT: You **must** call the `animationComplete()` function when using `animate.leave` for Angular to remove the element. + +If you don't call `animationComplete()` when using `animate.leave`, Angular calls the function automatically after a four-second delay. You can configure the duration of the delay by providing the token `MAX_ANIMATION_TIMEOUT` in milliseconds. + +```typescript + { provide: MAX_ANIMATION_TIMEOUT, useValue: 6000 } +``` + +## Testing + +TestBed provides built-in support for enabling or disabling animations in your test environment. CSS animations require a browser to run, and many of the APIs are not available in a test environment. By default, TestBed disables animations for you in your test environments. + +If you want to test that the animations are animating in a browser test, for example an end-to-end test, you can configure TestBed to enable animations by specifying `animationsEnabled: true` in your test configuration. + +```typescript + TestBed.configureTestingModule({animationsEnabled: true}); +``` + +This will configure animations in your test environment to behave normally. + +NOTE: Some test environments do not emit animation events like `animationstart`, `animationend` and their transition event equivalents. + +## More on Angular animations + +You might also be interested in the following: + + + + + diff --git a/adev-es/src/content/guide/animations/migration.md b/adev-es/src/content/guide/animations/migration.md index c62ec4f..8ca357a 100644 --- a/adev-es/src/content/guide/animations/migration.md +++ b/adev-es/src/content/guide/animations/migration.md @@ -1,6 +1,6 @@ # Migrating away from Angular's Animations package -Almost all the features supported by `@angular/animations` have simpler alternatives with native CSS. Consider removing the Angular Animations package from your application, as the package can contribute around 60 kilobytes to your JavaScript bundle. Native CSS animations offer superior performance, as they can benefit from hardware acceleration. Animations defined in the animations package lack that ability. This guide walks through the process of refactoring your code from `@angular/animations` to native CSS animations. +The `@angular/animations` package is deprecated as of v20.2, which also introduced the new `animate.enter` and `animate.leave` feature to add animations to your application. Using these new features, you can replace all animations based on `@angular/animations` with plain CSS or JS animation libraries. Removing `@angular/animations` from your application can significantly reduce the size of your JavaScript bundle. Native CSS animations generally offer superior performance, as they can benefit from hardware acceleration. This guide walks through the process of refactoring your code from `@angular/animations` to native CSS animations. ## How to write animations in native CSS @@ -117,7 +117,7 @@ The animations package offered the previously mentioned pattern matching for ent -Here's how the same thing can be accomplished without the animations package. +Here's how the same thing can be accomplished without the animations package using `animate.enter`. #### With Native CSS @@ -126,7 +126,7 @@ Here's how the same thing can be accomplished without the animations package. -Leaving a view is slightly more complex. The element removal needs to be delayed until the exit animation is complete. This requires a bit of extra code in your component class to accomplish. +Use `animate.leave` to animate elements as they leave the view, which will apply the specified CSS classes to the element as it leaves the view. #### With Native CSS @@ -135,6 +135,8 @@ Leaving a view is slightly more complex. The element removal needs to be delayed +For more information on `animate.enter` and `animate.leave`, see the [Enter and Leave animations guide](guide/animations). + ### Animating increment and decrement Along with the aforementioned `:enter` and `:leave`, there's also `:increment` and `:decrement`. You can animate these also by adding and removing classes. Unlike the animation package built-in aliases, there is no automatic application of classes when the values go up or down. You can apply the appropriate classes programmatically. Here's an example: @@ -236,7 +238,7 @@ In this example, the `rotate` and `fade-in` animations fire at the same time. ### Animating the items of a reordering list -Items reordering in a list works out of the box using the previously described techniques. No additional special work is required. Items in a `@for` loop will be removed and re-added properly, which will fire off animations using `@starting-styles` for entry animations. Removal animations will require additional code to add the event listener, as seen in the example above. +Items reordering in a list works out of the box using the previously described techniques. No additional special work is required. Items in a `@for` loop will be removed and re-added properly, which will fire off animations using `@starting-styles` for entry animations. Alternatively, you can use `animate.enter` for this same behavior. Use `animate.leave` to animate elements as they are removed, as seen in the example above. #### With Animations Package< @@ -261,4 +263,4 @@ You can retrieve animations off an element directly using [`Element.getAnimation ## Route Transitions -You can use view transitions to animate between routes. See the [Route Transition Animations Guide](guide/animations/route-animations) to get started. \ No newline at end of file +You can use view transitions to animate between routes. See the [Route Transition Animations Guide](guide/routing/route-transition-animations) to get started. \ No newline at end of file diff --git a/adev-es/src/content/guide/animations/overview.md b/adev-es/src/content/guide/animations/overview.md index 1403f21..bb6991e 100644 --- a/adev-es/src/content/guide/animations/overview.md +++ b/adev-es/src/content/guide/animations/overview.md @@ -1,6 +1,6 @@ # Introduction to Angular animations -IMPORTANT: The Angular team recommends using native CSS for animations instead of the Animations package for all new code. Use this guide to understand existing code built with the Animations Package. See [Migrating away from Angular's Animations package](guide/animations/migration) to learn how you can start using pure CSS animations in your apps. +IMPORTANT: The `@angular/animations` package is now deprecated. The Angular team recommends using native CSS with `animate.enter` and `animate.leave` for animations for all new code. Learn more at the new enter and leave [animation guide](guide/animations/enter-and-leave). Also see [Migrating away from Angular's Animations package](guide/animations/migration) to learn how you can start migrating to pure CSS animations in your apps. Animation provides the illusion of motion: HTML elements change styling over time. Well-designed animations can make your application more fun and straightforward to use, but they aren't just cosmetic. @@ -56,7 +56,7 @@ If you plan to use specific animation functions in component files, import those -See all [available animation functions](guide/animations#animations-api-summary) at the end of this guide. +See all [available animation functions](guide/legacy-animations#animations-api-summary) at the end of this guide. @@ -263,7 +263,7 @@ Here are the code files discussed in the transition example. You learned to add animation to a transition between two states, using `style()` and [`state()`](api/animations/state) along with `animate()` for the timing. -Learn about more advanced features in Angular animations under the Animation section, beginning with advanced techniques in [transition and triggers](guide/animations/transition-and-triggers). +Learn about more advanced features in Angular animations under the Animation section, beginning with advanced techniques in [transition and triggers](guide/legacy-animations/transition-and-triggers). ## Animations API summary @@ -295,8 +295,9 @@ HELPFUL: Check out this [presentation](https://www.youtube.com/watch?v=rnTK9meY5 You might also be interested in the following: - - - - + + + + + diff --git a/adev-es/src/content/guide/animations/reusable-animations.md b/adev-es/src/content/guide/animations/reusable-animations.md index 76f02b9..d8d024f 100644 --- a/adev-es/src/content/guide/animations/reusable-animations.md +++ b/adev-es/src/content/guide/animations/reusable-animations.md @@ -1,6 +1,6 @@ # Reusable animations -IMPORTANT: The Angular team recommends using native CSS for animations instead of the Animations package for all new code. Use this guide to understand existing code built with the Animations Package. See [Migrating away from Angular's Animations package](guide/animations/migration#creating-reusable-animations) to learn how you can start using pure CSS animations in your apps. +IMPORTANT: The `@angular/animations` package is now deprecated. The Angular team recommends using native CSS with `animate.enter` and `animate.leave` for animations for all new code. Learn more at the new enter and leave [animation guide](guide/animations/enter-and-leave). Also see [Migrating away from Angular's Animations package](guide/animations/migration) to learn how you can start migrating to pure CSS animations in your apps. This topic provides some examples of how to create reusable animations. @@ -30,8 +30,9 @@ For example, the following code snippet imports the `transitionAnimation` variab You might also be interested in the following: - - - - + + + + + diff --git a/adev-es/src/content/guide/animations/transition-and-triggers.md b/adev-es/src/content/guide/animations/transition-and-triggers.md index 3b77802..cc35138 100644 --- a/adev-es/src/content/guide/animations/transition-and-triggers.md +++ b/adev-es/src/content/guide/animations/transition-and-triggers.md @@ -1,6 +1,6 @@ # Animation transitions and triggers -IMPORTANT: The Angular team recommends using native CSS for animations instead of the Animations package for all new code. Use this guide to understand existing code built with the Animations Package. See [Migrating away from Angular's Animations package](guide/animations/migration#transition-and-triggers) to learn how you can start using pure CSS animations in your apps. +IMPORTANT: The `@angular/animations` package is now deprecated. The Angular team recommends using native CSS with `animate.enter` and `animate.leave` for animations for all new code. Learn more at the new enter and leave [animation guide](guide/animations/enter-and-leave). Also see [Migrating away from Angular's Animations package](guide/animations/migration) to learn how you can start migrating to pure CSS animations in your apps. This guide goes into depth on special transition states such as the `*` wildcard and `void`. It shows how these special states are used for elements entering and leaving a view. This section also explores multiple animation triggers, animation callbacks, and sequence-based animation using keyframes. @@ -57,7 +57,7 @@ Wildcard is a fallback value that's used if the state being animated isn't decla ### Void state Use the `void` state to configure transitions for an element that is entering or leaving a page. -See [Animating entering and leaving a view](guide/animations/transition-and-triggers#aliases-enter-and-leave). +See [Animating entering and leaving a view](guide/legacy-animations/transition-and-triggers#aliases-enter-and-leave). ### Combine wildcard and void states @@ -119,7 +119,7 @@ The `transition()` function takes other selector values, `:increment` and `:decr Use these to kick off a transition when a numeric value has increased or decreased in value. HELPFUL: The following example uses `query()` and `stagger()` methods. -For more information on these methods, see the [complex sequences](guide/animations/complex-sequences) page. +For more information on these methods, see the [complex sequences](guide/legacy-animations/complex-sequences) page. @@ -296,8 +296,9 @@ The `keyframes()` function in Angular allows you to specify multiple interim sty You might also be interested in the following: - - - - + + + + + diff --git a/adev-es/src/content/guide/components/content-projection.md b/adev-es/src/content/guide/components/content-projection.md index 6667486..7a5ae1a 100644 --- a/adev-es/src/content/guide/components/content-projection.md +++ b/adev-es/src/content/guide/components/content-projection.md @@ -68,7 +68,7 @@ placeholder that tells Angular where to render content. Angular's compiler proce all `` elements at build-time. You cannot insert, remove, or modify `` at run time. You cannot add directives, styles, or arbitrary attributes to ``. -You should not conditionally include `` with `@if`, `@for`, or `@switch`. Angular always +IMPORTANT: You should not conditionally include `` with `@if`, `@for`, or `@switch`. Angular always instantiates and creates DOM nodes for content rendered to a `` placeholder, even if that `` placeholder is hidden. For conditional rendering of component content, see [Template fragments](api/core/ng-template). @@ -79,21 +79,48 @@ Angular supports projecting multiple different elements into different `card-title`, +}) +export class CardTitle {} + +@Component({ + selector: 'card-body', + template: `card-body`, +}) +export class CardBody {} +``` + +```angular-ts -
    - -
    - -
    +Component({ + selector: 'custom-card', + template: ` +
    + +
    + +
    + `, +}) +export class CustomCard {} ``` -```angular-html +```angular-ts - - Hello - Welcome to the example - +@Component({ + selector: 'app-root', + imports: [CustomCard, CardTitle, CardBody], + template: ` + + Hello + Welcome to the example + +`, +}) +export class App {} ``` ```angular-html diff --git a/adev-es/src/content/guide/components/host-elements.md b/adev-es/src/content/guide/components/host-elements.md index 8401ed4..b9566e0 100644 --- a/adev-es/src/content/guide/components/host-elements.md +++ b/adev-es/src/content/guide/components/host-elements.md @@ -67,7 +67,7 @@ export class CustomSlider { You can alternatively bind to the host element by applying the `@HostBinding` and `@HostListener` decorator to class members. -`@HostBinding` lets you bind host properties and attributes to properties and methods: +`@HostBinding` lets you bind host properties and attributes to properties and getters: ```angular-ts @Component({ @@ -78,7 +78,7 @@ export class CustomSlider { value: number = 0; @HostBinding('tabIndex') - getTabIndex() { + get tabIndex() { return this.disabled ? -1 : 0; } diff --git a/adev-es/src/content/guide/components/inputs.md b/adev-es/src/content/guide/components/inputs.md index b532054..77cceea 100644 --- a/adev-es/src/content/guide/components/inputs.md +++ b/adev-es/src/content/guide/components/inputs.md @@ -349,9 +349,10 @@ export class CustomSlider { return this.internalValue; } -set value(newValue: number) { this.internalValue = newValue; } + set value(newValue: number) { this.internalValue = newValue; } -private internalValue = 0; } + private internalValue = 0; +}
    You can even create a _write-only_ input by only defining a public setter: @@ -363,7 +364,8 @@ export class CustomSlider { this.internalValue = newValue; } -private internalValue = 0; } + private internalValue = 0; +}
    **Prefer using input transforms instead of getters and setters** if possible. diff --git a/adev-es/src/content/guide/components/outputs.md b/adev-es/src/content/guide/components/outputs.md index ad8bbca..16593b4 100644 --- a/adev-es/src/content/guide/components/outputs.md +++ b/adev-es/src/content/guide/components/outputs.md @@ -52,6 +52,20 @@ When defining an event listener in a template, you can access the event data fro ``` +Receive the event data in the parent component: + + +@Component({ + /*...*/ +}) +export class App { + logValue(value: number) { + ... + } +} + + + ## Customizing output names The `output` function accepts a parameter that lets you specify a different name for the event in a template: diff --git a/adev-es/src/content/guide/components/selectors.md b/adev-es/src/content/guide/components/selectors.md index 5e53d9c..fb1b509 100644 --- a/adev-es/src/content/guide/components/selectors.md +++ b/adev-es/src/content/guide/components/selectors.md @@ -118,8 +118,7 @@ prefijar tus componentes con `yt-`, con componentes como `yt-menu`, `yt-player`, a tus selectores de esta manera hace inmediatamente claro de dónde viene un componente particular. Por defecto, el Angular CLI usa `app-`. -Angular usa el prefijo de selector `ng` para sus propias APIs del framework. Nunca uses `ng` como prefijo de -selector para tus propios componentes personalizados. +IMPORTANTE: Angular usa el prefijo de selector `ng` para sus propias APIs del framework. Nunca uses `ng` como prefijo de selector para tus propios componentes personalizados. ### Cuándo usar un selector de atributo diff --git a/adev-es/src/content/guide/components/styling.en.md b/adev-es/src/content/guide/components/styling.en.md index d2bdd21..25d69d5 100644 --- a/adev-es/src/content/guide/components/styling.en.md +++ b/adev-es/src/content/guide/components/styling.en.md @@ -59,10 +59,11 @@ global styles defined outside of a component may still affect elements inside a emulated encapsulation. In emulated mode, Angular supports -the [`:host`](https://developer.mozilla.org/docs/Web/CSS/:host) -and [`:host-context()`](https://developer.mozilla.org/docs/Web/CSS/:host-context) pseudo -classes without -using [Shadow DOM](https://developer.mozilla.org/docs/Web/Web_Components/Using_shadow_DOM). +the [`:host`](https://developer.mozilla.org/docs/Web/CSS/:host) pseudo-class. +While the [`:host-context()`](https://developer.mozilla.org/docs/Web/CSS/:host-context) pseudo-class +is deprecated in modern browsers, Angular's compiler provides full support for it. Both pseudo-classes +can be used without relying on native +[Shadow DOM](https://developer.mozilla.org/docs/Web/Web_Components/Using_shadow_DOM). During compilation, the framework transforms these pseudo classes into attributes so it doesn't comply with these native pseudo classes' rules at runtime (e.g. browser compatibility, specificity). Angular's emulated encapsulation mode does not support any other pseudo classes related to Shadow DOM, such diff --git a/adev-es/src/content/guide/di/creating-injectable-service.en.md b/adev-es/src/content/guide/di/creating-injectable-service.en.md index ebe5b6a..ca3a9bd 100644 --- a/adev-es/src/content/guide/di/creating-injectable-service.en.md +++ b/adev-es/src/content/guide/di/creating-injectable-service.en.md @@ -53,7 +53,7 @@ export class HeroService { }
    -## Creating an injectable service +## Creating an injectable service with the CLI The Angular CLI provides a command to create a new service. In the following example, you add a new service to an existing application. diff --git a/adev-es/src/content/guide/di/creating-injectable-service.md b/adev-es/src/content/guide/di/creating-injectable-service.md index 1f18927..3f5a011 100644 --- a/adev-es/src/content/guide/di/creating-injectable-service.md +++ b/adev-es/src/content/guide/di/creating-injectable-service.md @@ -53,7 +53,7 @@ export class HeroService { } -## Creando un servicio inyectable +## Creando un servicio inyectable con el CLI El Angular CLI proporciona un comando para crear un nuevo servicio. En el siguiente ejemplo, agregas un nuevo servicio a una aplicación existente. diff --git a/adev-es/src/content/guide/directives/overview.en.md b/adev-es/src/content/guide/directives/overview.en.md index 9b87981..07a62d2 100644 --- a/adev-es/src/content/guide/directives/overview.en.md +++ b/adev-es/src/content/guide/directives/overview.en.md @@ -6,10 +6,11 @@ Use Angular's built-in directives to manage forms, lists, styles, and what users The different types of Angular directives are as follows: -| Directive Types | Details | -| :------------------------------------------------------- | :-------------------------------------------------------------------------------- | -| [Components](guide/components) | Used with a template. This type of directive is the most common directive type. | -| [Attribute directives](#built-in-attribute-directives) | Change the appearance or behavior of an element, component, or another directive. | +| Directive Types | Details | +| :--------------------------------------------------------------- | :-------------------------------------------------------------------------------- | +| [Components](guide/components) | Used with a template. This type of directive is the most common directive type. | +| [Attribute directives](#built-in-attribute-directives) | Change the appearance or behavior of an element, component, or another directive. | +| [Structural directives](/guide/directives/structural-directives) | Change the DOM layout by adding and removing DOM elements. | This guide covers built-in [attribute directives](#built-in-attribute-directives). @@ -19,11 +20,11 @@ Attribute directives listen to and modify the behavior of other HTML elements, a The most common attribute directives are as follows: -| Common directives | Details | -| :------------------------------------------------------------ | :------------------------------------------------- | -| [`NgClass`](#adding-and-removing-classes-with-ngclass) | Adds and removes a set of CSS classes. | -| [`NgStyle`](#setting-inline-styles-with-ngstyle) | Adds and removes a set of HTML styles. | -| [`NgModel`](guide/forms/template-driven-forms) | Adds two-way data binding to an HTML form element. | +| Common directives | Details | +| :----------------------------------------------------- | :------------------------------------------------- | +| [`NgClass`](#adding-and-removing-classes-with-ngclass) | Adds and removes a set of CSS classes. | +| [`NgStyle`](#setting-inline-styles-with-ngstyle) | Adds and removes a set of HTML styles. | +| [`NgModel`](guide/forms/template-driven-forms) | Adds two-way data binding to an HTML form element. | HELPFUL: Built-in directives use only public APIs. They do not have special access to any private APIs that other directives can't access. diff --git a/adev-es/src/content/guide/directives/overview.md b/adev-es/src/content/guide/directives/overview.md index 7fd0b5f..7c09775 100644 --- a/adev-es/src/content/guide/directives/overview.md +++ b/adev-es/src/content/guide/directives/overview.md @@ -6,10 +6,11 @@ Usa las directivas integradas de Angular para gestionar formularios, listas, est Los diferentes tipos de directivas de Angular son los siguientes: -| Tipos de Directivas | Detalles | -| :------------------------------------------------------- | :-------------------------------------------------------------------------------- | -| [Componentes](guide/components) | Se usan con una plantilla. Este tipo de directiva es el tipo de directiva más común. | -| [Directivas de atributo](#directivas-de-atributo-integradas) | Cambian la apariencia o comportamiento de un elemento, componente u otra directiva. | +| Tipos de Directivas | Detalles | +| :--------------------------------------------------------------- | :----------------------------------------------------------------------------------- | +| [Componentes](guide/components) | Se usan con una plantilla. Este tipo de directiva es el tipo de directiva más común. | +| [Directivas de atributo](#directivas-de-atributo-integradas) | Cambian la apariencia o comportamiento de un elemento, componente u otra directiva. | +| [Structural directives](/guide/directives/structural-directives) | Cambia el diseño del DOM agregando y eliminando elementos del DOM.. Esta guía cubre las [directivas de atributo integradas](#directivas-de-atributo-integradas). @@ -19,11 +20,11 @@ Las directivas de atributo escuchan y modifican el comportamiento de otros eleme Las directivas de atributo más comunes son las siguientes: -| Directivas comunes | Detalles | -| :------------------------------------------------------------ | :------------------------------------------------- | -| [`NgClass`](#añadiendo-y-eliminando-clases-con-ngclass) | Añade y elimina un conjunto de clases CSS. | -| [`NgStyle`](#configurando-estilos-en-línea-con-ngstyle) | Añade y elimina un conjunto de estilos HTML. | -| [`NgModel`](guide/forms/template-driven-forms) | Añade enlace de datos bidireccional a un elemento de formulario HTML. | +| Directivas comunes | Detalles | +| :------------------------------------------------------------- | :-------------------------------------------------------------------- | +| [`NgClass`](#añadiendo-y-eliminando-clases-con-ngclass) | Añade y elimina un conjunto de clases CSS. | +| [`NgStyle`](#configurando-estilos-en-línea-con-ngstyle) | Añade y elimina un conjunto de estilos HTML. | +| [`NgModel`](guide/forms/template-driven-forms) | Añade enlace de datos bidireccional a un elemento de formulario HTML. | ÚTIL: Las directivas integradas usan solo APIs públicas. No tienen acceso especial a ninguna API privada que otras directivas no puedan acceder. diff --git a/adev-es/src/content/guide/drag-drop.en.md b/adev-es/src/content/guide/drag-drop.en.md index 0e97557..50f89e3 100644 --- a/adev-es/src/content/guide/drag-drop.en.md +++ b/adev-es/src/content/guide/drag-drop.en.md @@ -312,6 +312,20 @@ There are cases where draggable elements can be dragged out of one `cdkDropList` Alternatively, you can modify the `CDK_DRAG_CONFIG` injection token to update sortingDisabled within the config. For more information see the [dependency injection guide](https://angular.dev/guide/di), [drag config injection token API](api/cdk/drag-drop/CDK_DRAG_CONFIG), and the [drag drop config API](api/cdk/drag-drop/DragDropConfig). +### Copying items between lists + +By default, when an item is dragged from one list to another, it is moved out of its original list. However, you can configure the directives to copy the item, leaving the original item in the source list. + +To enable copying, you can set the `cdkDropListHasAnchor` input. This tells the `cdkDropList` to create an "anchor" element that stays in the original container and doesn't move with the item. If the user moves the item back into the original container, the anchor is removed automatically. The anchor element can be styled by targeting the `.cdk-drag-anchor` CSS class. + +Combining `cdkDropListHasAnchor` with `cdkDropListSortingDisabled` makes it possible to construct a list from which a user can copy items without being able to reorder the source list (e.g. a product list and a shopping cart). + + + + + + + ## Customize animations Drag and drop supports animations for both: @@ -341,3 +355,12 @@ Both `cdkDrag` and `cdkDropList` directives only apply essential styles needed f | .cdk-drop-list-dragging | Selector for `cdkDropList` container element that has a draggable element currently being dragged. | | .cdk-drop-list-disabled | Selector for `cdkDropList` container elements that are disabled. | | .cdk-drop-list-receiving | Selector for `cdkDropList` container element that has a draggable element it can receive from a connected drop list that is currently being dragged. | +| .cdk-drag-anchor | Selector for the anchor element that is created when `cdkDropListHasAnchor` is enabled. This element indicates the position from which the dragged item started. | + +## Dragging in a scrollable container + +If your draggable items are inside a scrollable container (e.g., a `div` with `overflow: auto`), automatic scrolling will not work unless the scrollable container has the `cdkScrollable` directive. Without it, the CDK cannot detect or control the scroll behavior of the container during drag operations. + +## Integrations with other components + +The CDK's drag-and-drop functionality can be integrated with different components. Common use cases include sortable `MatTable` components and sortable `MatTabGroup` components. diff --git a/adev-es/src/content/guide/drag-drop.md b/adev-es/src/content/guide/drag-drop.md index 24f4cee..54320bc 100644 --- a/adev-es/src/content/guide/drag-drop.md +++ b/adev-es/src/content/guide/drag-drop.md @@ -312,6 +312,20 @@ Hay casos donde los elementos arrastrables pueden ser arrastrados de un `cdkDrop Alternativamente, puedes modificar el token de inyección `CDK_DRAG_CONFIG` para actualizar `sortingDisabled` dentro de la configuración. Para más información consulta la [guía de inyección de dependencias](https://angular.dev/guide/di), la [API del token de inyección de configuración de arrastre](api/cdk/drag-drop/CDK_DRAG_CONFIG), y la [API de configuración de drag and drop](api/cdk/drag-drop/DragDropConfig). +### Copying items between lists + +By default, when an item is dragged from one list to another, it is moved out of its original list. However, you can configure the directives to copy the item, leaving the original item in the source list. + +To enable copying, you can set the `cdkDropListHasAnchor` input. This tells the `cdkDropList` to create an "anchor" element that stays in the original container and doesn't move with the item. If the user moves the item back into the original container, the anchor is removed automatically. The anchor element can be styled by targeting the `.cdk-drag-anchor` CSS class. + +Combining `cdkDropListHasAnchor` with `cdkDropListSortingDisabled` makes it possible to construct a list from which a user can copy items without being able to reorder the source list (e.g. a product list and a shopping cart). + + + + + + + ## Personalizar animaciones Drag and drop soporta animaciones tanto para: @@ -341,3 +355,12 @@ Tanto las directivas `cdkDrag` como `cdkDropList` solo aplican estilos esenciale | .cdk-drop-list-dragging | Selector para el elemento contenedor de `cdkDropList` que tiene un elemento arrastrable actualmente siendo arrastrado. | | .cdk-drop-list-disabled | Selector para elementos contenedores de `cdkDropList` que están deshabilitados. | | .cdk-drop-list-receiving | Selector para el elemento contenedor de `cdkDropList` que tiene un elemento arrastrable que puede recibir de una lista de soltar conectada que está actualmente siendo arrastrado. | +| .cdk-drag-anchor | Selector para elemento de aclaje que se crea cuado `cdkDropListHasAnchor` está habilitado. Este elemento indica la posición desde la cual comenzó el arrastre del elemento | + +## Arrastrado en un contenedor desplazable + +Si tus elementos arrastrables están dentro de un contenedor desplazable (por ejemplo, un `div` con `overflow: auto`), el desplazamiento automático no funcionará a menos que el contenedor desplazable tenga la directiva `cdkScrollable`. Sin esta directiva, el CDK no puede detectar ni controlar el comportamiento de desplazamiento del contenedor durante las operaciones de arrastre. + +## Integraciones con otros componentes + +La funcionalidad de arrastrar y soltar del CDK puede integrarse con diferentes componentes. Los casos de uso más comunes incluyen componentes `MatTable` ordenables y componentes `MatTabGroup` ordenables. diff --git a/adev-es/src/content/guide/http/http-resource.en.md b/adev-es/src/content/guide/http/http-resource.en.md index 40d4950..4a0844b 100644 --- a/adev-es/src/content/guide/http/http-resource.en.md +++ b/adev-es/src/content/guide/http/http-resource.en.md @@ -19,7 +19,7 @@ userId = input.required(); user = httpResource(() => `/api/user/${userId()}`); // A reactive function as argument ``` -`httResource` is reactive, meaning that whenever one of the signal it depends on changes (like `userId`), the resource will emit a new http request. +`httpResource` is reactive, meaning that whenever one of the signal it depends on changes (like `userId`), the resource will emit a new http request. If a request is already pending, the resource cancels the outstanding request before issuing a new one. HELPFUL: `httpResource` differs from the `HttpClient` as it initiates the request _eagerly_. In contrast, the `HttpClient` only initiates requests upon subscription to the returned `Observable`. @@ -38,9 +38,15 @@ user = httpResource(() => ({ 'fast': 'yes', }, reportProgress: true, - withCredentials: true, transferCache: true, - keepalive: true, + keepalive: true, + mode: 'cors', + redirect: 'error', + priority: 'high', + cache : 'force-cache', + credentials: 'include', + referrer: 'no-referrer', + integrity: 'sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GhEXAMPLEKEY=' })); ``` @@ -50,7 +56,7 @@ The signals of the `httpResource` can be used in the template to control which e ```angular-html @if(user.hasValue()) { - + } @else if (user.error()) {
    Could not load user information
    } @else if (user.isLoading()) { diff --git a/adev-es/src/content/guide/http/http-resource.md b/adev-es/src/content/guide/http/http-resource.md index bfe2ace..d856520 100644 --- a/adev-es/src/content/guide/http/http-resource.md +++ b/adev-es/src/content/guide/http/http-resource.md @@ -38,9 +38,15 @@ user = httpResource(() => ({ 'fast': 'yes', }, reportProgress: true, - withCredentials: true, transferCache: true, - keepalive: true, + keepalive: true, + mode: 'cors', + redirect: 'error', + priority: 'high', + cache : 'force-cache', + credentials: 'include', + referrer: 'no-referrer', + integrity: 'sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GhEXAMPLEKEY=' })); ``` diff --git a/adev-es/src/content/guide/http/interceptors.en.md b/adev-es/src/content/guide/http/interceptors.en.md index aef9a01..86ef23c 100644 --- a/adev-es/src/content/guide/http/interceptors.en.md +++ b/adev-es/src/content/guide/http/interceptors.en.md @@ -164,6 +164,39 @@ const resp = new HttpResponse({ }); +## Working with redirect information + +When using `HttpClient` with the `withFetch` provider, responses include a `redirected` property that indicates whether the response was the result of a redirect. This property aligns with the native Fetch API specification and can be useful in interceptors for handling redirect scenarios. + +An interceptor can access and act upon the redirect information: + + +export function redirectTrackingInterceptor(req: HttpRequest, next: HttpHandlerFn): Observable> { + return next(req).pipe(tap(event => { + if (event.type === HttpEventType.Response && event.redirected) { + console.log('Request to', req.url, 'was redirected to', event.url); + // Handle redirect logic - maybe update analytics, security checks, etc. + } + })); +} + + +You can also use the redirect information to implement conditional logic in your interceptors: + + +export function authRedirectInterceptor(req: HttpRequest, next: HttpHandlerFn): Observable> { + return next(req).pipe(tap(event => { + if (event.type === HttpEventType.Response && event.redirected) { + // Check if we were redirected to a login page + if (event.url?.includes('/login')) { + // Handle authentication redirect + handleAuthRedirect(); + } + } + })); +} + + ## DI-based interceptors `HttpClient` also supports interceptors which are defined as injectable classes and configured through the DI system. The capabilities of DI-based interceptors are identical to those of functional interceptors, but the configuration mechanism is different. diff --git a/adev-es/src/content/guide/http/interceptors.md b/adev-es/src/content/guide/http/interceptors.md index 5216b66..585d9ed 100644 --- a/adev-es/src/content/guide/http/interceptors.md +++ b/adev-es/src/content/guide/http/interceptors.md @@ -164,6 +164,39 @@ const resp = new HttpResponse({ }); +## Trabajando con información de redirección + +Cuando se usa `HttpClient` con el proveedor `withFetch`, las respuestas incluyen una propiedad `redirected` que indica si la respuesta fue resultado de una redirección. Esta propiedad está alineada con la especificación nativa de la API Fetch y puede ser útil en interceptores para manejar escenarios de redirección. + +Un interceptor puede acceder y actuar en base a la información de redirección: + + +export function redirectTrackingInterceptor(req: HttpRequest, next: HttpHandlerFn): Observable> { + return next(req).pipe(tap(event => { + if (event.type === HttpEventType.Response && event.redirected) { + console.log('La solicitud a', req.url, 'fue redirigida a', event.url); + // Manejar la lógica de redirección - por ejemplo actualizar analíticas, verificaciones de seguridad, etc. + } + })); +} + + +También puedes usar la información de redirección para implementar lógica condicional en tus interceptores: + + +export function authRedirectInterceptor(req: HttpRequest, next: HttpHandlerFn): Observable> { + return next(req).pipe(tap(event => { + if (event.type === HttpEventType.Response && event.redirected) { + // Verificar si fuimos redirigidos a una página de inicio de sesión + if (event.url?.includes('/login')) { + // Manejar la redirección de autenticación + handleAuthRedirect(); + } + } + })); +} + + ## Interceptores basados en DI (Inyección de Dependencias) `HttpClient` también soporta interceptores que se definen como clases inyectables y se configuran a través del sistema DI. Las capacidades de los interceptores basados en DI son idénticas a las de los interceptores funcionales, pero el mecanismo de configuración es diferente. diff --git a/adev-es/src/content/guide/http/making-requests.en.md b/adev-es/src/content/guide/http/making-requests.en.md index 5b52869..180f9f3 100644 --- a/adev-es/src/content/guide/http/making-requests.en.md +++ b/adev-es/src/content/guide/http/making-requests.en.md @@ -104,6 +104,49 @@ http.get('/api/config', { You can instantiate `HttpParams` with a custom `HttpParameterCodec` that determines how `HttpClient` will encode the parameters into the URL. +### Custom parameter encoding + +By default, `HttpParams` uses the built-in [`HttpUrlEncodingCodec`](api/common/http/HttpUrlEncodingCodec) to encode and decode parameter keys and values. + +You can provide your own implementation of [`HttpParameterCodec`](api/common/http/HttpParameterCodec) to customize how encoding and decoding are applied. + +```ts +import { HttpClient, HttpParams, HttpParameterCodec } from '@angular/common/http'; +import { inject } from '@angular/core'; + +export class CustomHttpParamEncoder implements HttpParameterCodec { + encodeKey(key: string): string { + return encodeURIComponent(key); + } + + encodeValue(value: string): string { + return encodeURIComponent(value); + } + + decodeKey(key: string): string { + return decodeURIComponent(key); + } + + decodeValue(value: string): string { + return decodeURIComponent(value); + } +} + +export class ApiService { + private http = inject(HttpClient); + + search() { + const params = new HttpParams({ + encoder: new CustomHttpParamEncoder(), + }) + .set('email', 'dev+alerts@example.com') + .set('q', 'a & b? c/d = e'); + + return this.http.get('/api/items', { params }); + } +} +``` + ## Setting request headers Specify request headers that should be included in the request using the `headers` option. @@ -198,12 +241,13 @@ Each `HttpEvent` reported in the event stream has a `type` which distinguishes w ## Handling request failure -There are two ways an HTTP request can fail: +There are three ways an HTTP request can fail: * A network or connection error can prevent the request from reaching the backend server. +* A request didn't respond in time when the timeout option was set. * The backend can receive the request but fail to process it, and return an error response. -`HttpClient` captures both kinds of errors in an `HttpErrorResponse` which it returns through the `Observable`'s error channel. Network errors have a `status` code of `0` and an `error` which is an instance of [`ProgressEvent`](https://developer.mozilla.org/docs/Web/API/ProgressEvent). Backend errors have the failing `status` code returned by the backend, and the error response as the `error`. Inspect the response to identify the error's cause and the appropriate action to handle the error. +`HttpClient` captures all of the above kinds of errors in an `HttpErrorResponse` which it returns through the `Observable`'s error channel. Network and timeout errors have a `status` code of `0` and an `error` which is an instance of [`ProgressEvent`](https://developer.mozilla.org/docs/Web/API/ProgressEvent). Backend errors have the failing `status` code returned by the backend, and the error response as the `error`. Inspect the response to identify the error's cause and the appropriate action to handle the error. The [RxJS library](https://rxjs.dev/) offers several operators which can be useful for error handling. @@ -211,6 +255,273 @@ You can use the `catchError` operator to transform an error response into a valu Sometimes transient errors such as network interruptions can cause a request to fail unexpectedly, and simply retrying the request will allow it to succeed. RxJS provides several *retry* operators which automatically re-subscribe to a failed `Observable` under certain conditions. For example, the `retry()` operator will automatically attempt to re-subscribe a specified number of times. +### Timeouts + +To set a timeout for a request, you can set the `timeout` option to a number of milliseconds along other request options. If the backend request does not complete within the specified time, the request will be aborted and an error will be emitted. + +NOTE: The timeout will only apply to the backend HTTP request itself. It is not a timeout for the entire request handling chain. Therefore, this option is not affected by any delay introduced by interceptors. + + +http.get('/api/config', { + timeout: 3000, +}).subscribe({ + next: config => { + console.log('Config fetched successfully:', config); + }, + error: err => { + // If the request times out, an error will have been emitted. + } +}); + + +## Advanced fetch options + +When using the `withFetch()` provider, Angular's `HttpClient` provides access to advanced fetch API options that can improve performance and user experience. These options are only available when using the fetch backend. + +### Fetch options + +The following options provide fine-grained control over request behavior when using the fetch backend. + +#### Keep-alive connections + +The `keepalive` option allows a request to outlive the page that initiated it. This is particularly useful for analytics or logging requests that need to complete even if the user navigates away from the page. + + +http.post('/api/analytics', analyticsData, { + keepalive: true +}).subscribe(); + + +#### HTTP caching control + +The `cache` option controls how the request interacts with the browser's HTTP cache, which can significantly improve performance for repeated requests. + + +// Use cached response regardless of freshness +http.get('/api/config', { + cache: 'force-cache' +}).subscribe(config => { + // ... +}); + +// Always fetch from network, bypass cache +http.get('/api/live-data', { + cache: 'no-cache' +}).subscribe(data => { + // ... +}); + +// Use cached response only, fail if not in cache +http.get('/api/static-data', { + cache: 'only-if-cached' +}).subscribe(data => { + // ... +}); + + +#### Request priority for Core Web Vitals + +The `priority` option allows you to indicate the relative importance of a request, helping browsers optimize resource loading for better Core Web Vitals scores. + + +// High priority for critical resources +http.get('/api/user-profile', { + priority: 'high' +}).subscribe(profile => { + // ... +}); + +// Low priority for non-critical resources +http.get('/api/recommendations', { + priority: 'low' +}).subscribe(recommendations => { + // ... +}); + +// Auto priority (default) lets the browser decide +http.get('/api/settings', { + priority: 'auto' +}).subscribe(settings => { + // ... +}); + + +Available `priority` values: +- `'high'`: High priority, loaded early (e.g., critical user data, above-the-fold content) +- `'low'`: Low priority, loaded when resources are available (e.g., analytics, prefetch data) +- `'auto'`: Browser determines priority based on request context (default) + +TIP: Use `priority: 'high'` for requests that affect Largest Contentful Paint (LCP) and `priority: 'low'` for requests that don't impact initial user experience. + +#### Request mode + +The `mode` option controls how the request handles cross-origin requests and determines the response type. + + +// Same-origin requests only +http.get('/api/local-data', { + mode: 'same-origin' +}).subscribe(data => { + // ... +}); + +// CORS-enabled cross-origin requests +http.get('https://api.external.com/data', { + mode: 'cors' +}).subscribe(data => { + // ... +}); + +// No-CORS mode for simple cross-origin requests +http.get('https://external-api.com/public-data', { + mode: 'no-cors' +}).subscribe(data => { + // ... +}); + + +Available `mode` values: +- `'same-origin'`: Only allow same-origin requests, fail for cross-origin requests +- `'cors'`: Allow cross-origin requests with CORS (default) +- `'no-cors'`: Allow simple cross-origin requests without CORS, response is opaque + +TIP: Use `mode: 'same-origin'` for sensitive requests that should never go cross-origin. + +#### Redirect handling + +The `redirect` option specifies how to handle redirect responses from the server. + + +// Follow redirects automatically (default behavior) +http.get('/api/resource', { + redirect: 'follow' +}).subscribe(data => { + // ... +}); + +// Prevent automatic redirects +http.get('/api/resource', { + redirect: 'manual' +}).subscribe(response => { + // Handle redirect manually +}); + +// Treat redirects as errors +http.get('/api/resource', { + redirect: 'error' +}).subscribe({ + next: data => { + // Success response + }, + error: err => { + // Redirect responses will trigger this error handler + } +}); + + +Available `redirect` values: +- `'follow'`: Automatically follow redirects (default) +- `'error'`: Treat redirects as errors +- `'manual'`: Don't follow redirects automatically, return redirect response + +TIP: Use `redirect: 'manual'` when you need to handle redirects with custom logic. + +#### Credentials handling + +The `credentials` option controls whether cookies, authorization headers, and other credentials are sent with cross-origin requests. This is particularly important for authentication scenarios. + + +// Include credentials for cross-origin requests +http.get('https://api.example.com/protected-data', { + credentials: 'include' +}).subscribe(data => { + // ... +}); + +// Never send credentials (default for cross-origin) +http.get('https://api.example.com/public-data', { + credentials: 'omit' +}).subscribe(data => { + // ... +}); + +// Send credentials only for same-origin requests +http.get('/api/user-data', { + credentials: 'same-origin' +}).subscribe(data => { + // ... +}); + +// withCredentials overrides credentials setting +http.get('https://api.example.com/data', { + credentials: 'omit', // This will be ignored + withCredentials: true // This forces credentials: 'include' +}).subscribe(data => { + // Request will include credentials despite credentials: 'omit' +}); + +// Legacy approach (still supported) +http.get('https://api.example.com/data', { + withCredentials: true +}).subscribe(data => { + // Equivalent to credentials: 'include' +}); + + +IMPORTANT: The `withCredentials` option takes precedence over the `credentials` option. If both are specified, `withCredentials: true` will always result in `credentials: 'include'`, regardless of the explicit `credentials` value. + +Available `credentials` values: +- `'omit'`: Never send credentials +- `'same-origin'`: Send credentials only for same-origin requests (default) +- `'include'`: Always send credentials, even for cross-origin requests + +TIP: Use `credentials: 'include'` when you need to send authentication cookies or headers to a different domain that supports CORS. Avoid mixing `credentials` and `withCredentials` options to prevent confusion. + +#### Referrer + +The `referrer` option allows you to control what referrer information is sent with the request. This is important for privacy and security considerations. + + +// Send a specific referrer URL +http.get('/api/data', { + referrer: 'https://example.com/page' +}).subscribe(data => { + // ... +}); + +// Use the current page as referrer (default behavior) +http.get('/api/analytics', { + referrer: 'about:client' +}).subscribe(data => { + // ... +}); + + +The `referrer` option accepts: +- A valid URL string: Sets the specific referrer URL to send +- An empty string `''`: Sends no referrer information +- `'about:client'`: Uses the default referrer (current page URL) + +TIP: Use `referrer: ''` for sensitive requests where you don't want to leak the referring page URL. + +#### Integrity + +The `integrity` option allows you to verify that the response hasn't been tampered with by providing a cryptographic hash of the expected content. This is particularly useful for loading scripts or other resources from CDNs. + + +// Verify response integrity with SHA-256 hash +http.get('/api/script.js', { + integrity: 'sha256-ABC123...', + responseType: 'text' +}).subscribe(script => { + // Script content is verified against the hash +}); + + +IMPORTANT: The `integrity` option requires an exact match between the response content and the provided hash. If the content doesn't match, the request will fail with a network error. + +TIP: Use subresource integrity when loading critical resources from external sources to ensure they haven't been modified. Generate hashes using tools like `openssl`. + ## Http `Observable`s Each request method on `HttpClient` constructs and returns an `Observable` of the requested response type. Understanding how these `Observable`s work is important when using `HttpClient`. diff --git a/adev-es/src/content/guide/http/making-requests.md b/adev-es/src/content/guide/http/making-requests.md index 636ebcb..de9436c 100644 --- a/adev-es/src/content/guide/http/making-requests.md +++ b/adev-es/src/content/guide/http/making-requests.md @@ -104,6 +104,49 @@ http.get('/api/config', { Puedes instanciar `HttpParams` con un `HttpParameterCodec` personalizado que determine cómo `HttpClient` codificará los parámetros en la URL. +### Codificación personalizada de parámetros + +Por defecto, `HttpParams` utiliza el [`HttpUrlEncodingCodec`](api/common/http/HttpUrlEncodingCodec) incorporado para codificar y decodificar las claves y valores de los parámetros. + +Puedes proporcionar tu propia implementación de [`HttpParameterCodec`](api/common/http/HttpParameterCodec) para personalizar cómo se aplica la codificación y decodificación. + +```ts +import { HttpClient, HttpParams, HttpParameterCodec } from '@angular/common/http'; +import { inject } from '@angular/core'; + +export class CustomHttpParamEncoder implements HttpParameterCodec { + encodeKey(key: string): string { + return encodeURIComponent(key); + } + + encodeValue(value: string): string { + return encodeURIComponent(value); + } + + decodeKey(key: string): string { + return decodeURIComponent(key); + } + + decodeValue(value: string): string { + return decodeURIComponent(value); + } +} + +export class ApiService { + private http = inject(HttpClient); + + search() { + const params = new HttpParams({ + encoder: new CustomHttpParamEncoder(), + }) + .set('email', 'dev+alerts@example.com') + .set('q', 'a & b? c/d = e'); + + return this.http.get('/api/items', { params }); + } +} +``` + ## Configurando encabezados de solicitud Especifica los encabezados de solicitud que deben incluirse en la solicitud usando la opción `headers`. @@ -198,12 +241,13 @@ Cada `HttpEvent` reportado en la secuencia de eventos tiene un `type` que distin ## Manejando fallos de solicitud -Hay dos formas en que una solicitud HTTP puede fallar: +Hay tres formas en que una solicitud HTTP puede fallar: * Un error de red o conexión puede impedir que la solicitud llegue al servidor backend. +* Una solicitud no respondió a tiempo cuando se configuró la opción de tiempo de espera (timeout). * El backend puede recibir la solicitud pero fallar al procesarla, y devolver una respuesta de error. -`HttpClient` captura ambos tipos de errores en un `HttpErrorResponse` que devuelve a través del canal de error del `Observable`. Los errores de red tienen un código de `status` de `0` y un `error` que es una instancia de [`ProgressEvent`](https://developer.mozilla.org/docs/Web/API/ProgressEvent). Los errores del backend tienen el código de `status` fallido devuelto por el backend, y la respuesta de error como `error`. Inspecciona la respuesta para identificar la causa del error y la acción apropiada para manejar el error. +`HttpClient` captura todos tipos de errores mencionados anteriormente en un `HttpErrorResponse` que devuelve a través del canal de error del `Observable`. Los errores de red tienen un código de `status` de `0` y un `error` que es una instancia de [`ProgressEvent`](https://developer.mozilla.org/docs/Web/API/ProgressEvent). Los errores del backend tienen el código de `status` fallido devuelto por el backend, y la respuesta de error como `error`. Inspecciona la respuesta para identificar la causa del error y la acción apropiada para manejar el error. La [librería RxJS](https://rxjs.dev/) ofrece varios operadores que pueden ser útiles para el manejo de errores. @@ -211,6 +255,273 @@ Puedes usar el operador `catchError` para transformar una respuesta de error en A veces, errores transitorios como interrupciones de red pueden hacer que una solicitud falle inesperadamente, y simplemente reintentar la solicitud permitirá que tenga éxito. RxJS proporciona varios operadores de *reintento* que automáticamente se re-suscriben a un `Observable` fallido bajo ciertas condiciones. Por ejemplo, el operador `retry()` automáticamente intentará re-suscribirse un número especificado de veces. +### Tiempos de esperar (Timeouts) + +Para establecer un tiempo de espera para una solicitud, puedes configurar la opción `timeout` con un número de milisegundos junto con otras opciones de la solicitud. Si la solicitud al backend no se completa dentro del tiempo especificado, la solicitud se abortará y se emitirá un error. + +NOTA: El timeout solo se aplica a la solicitud HTTP al backend en sí. No es un tiempo de espera para toda la cadena de manejo de la solicitud. Por lo tanto, esta opción no se ve afectada por ningún retraso introducido por los interceptores. + + +http.get('/api/config', { + timeout: 3000, +}).subscribe({ + next: config => { + console.log('Configuración obtenida con éxito:', config); + }, + error: err => { + // Si la solicitud supera el tiempo de espera, se habrá emitido un error. + } +}); + + +## Opciones avanzadas de Fetch + +Al usar el proveedor `withFetch()`, el `HttpClient` de Angular proporciona acceso a opciones avanzadas de la API Fetch que pueden mejorar el rendimiento y la experiencia del usuario. Estas opciones solo están disponibles cuando se utiliza el backend Fetch. + +### Opciones de Fetch + +Las siguientes opciones permiten un control detallado sobre el comportamiento de la solicitud al usar el backend Fetch. + +#### Conexiones Keep-alive + +La opción `keepalive` permite que una solicitud continúe incluso si la página que la inició se cierra. Esto es útil para solicitudes de analíticas o registro que deben completarse aunque el usuario navegue fuera de la página. + + +http.post('/api/analytics', analyticsData, { + keepalive: true +}).subscribe(); + + +#### Control de caché HTTP + +La opción `cache` controla cómo interactúa la solicitud con la caché HTTP del navegador, lo que puede mejorar significativamente el rendimiento en solicitudes repetidas. + + +// Usar respuesta en caché sin importar su antigüedad +http.get('/api/config', { + cache: 'force-cache' +}).subscribe(config => { + // ... +}); + +// Siempre obtener del network, ignorando la caché +http.get('/api/live-data', { + cache: 'no-cache' +}).subscribe(data => { + // ... +}); + +// Usar solo la respuesta en caché, falla si no está en caché +http.get('/api/static-data', { + cache: 'only-if-cached' +}).subscribe(data => { + // ... +}); + + +#### Prioridad de solicitud para Core Web Vitals + +La opción `priority` permite indicar la importancia relativa de una solicitud, ayudando a los navegadores a optimizar la carga de recursos para mejorar los Core Web Vitals. + + +// Alta prioridad para recursos críticos +http.get('/api/user-profile', { + priority: 'high' +}).subscribe(profile => { + // ... +}); + +// Baja prioridad para recursos no críticos +http.get('/api/recommendations', { + priority: 'low' +}).subscribe(recommendations => { + // ... +}); + +// Prioridad automática (por defecto), el navegador decide +http.get('/api/settings', { + priority: 'auto' +}).subscribe(settings => { + // ... +}); + + +Valores disponibles para `priority`: +- `'high'`: Prioridad alta, se carga pronto (por ejemplo, datos críticos del usuario, contenido que se ve sin hacer scroll) +- `'low'`: Prioridad baja, se carga cuando los recursos están disponibles (por ejemplo, análisis, datos de precarga) +- `'auto'`: El navegador determina la prioridad basándose en el contexto de la solicitud (valor por defecto) + +TIP: Usa `priority: 'high'` para solicitudes que afectan Largest Contentful Paint (LCP) y `priority: 'low'` para solicitudes que no impactan la experiencia inicial del usuario. + +#### Modo de solicitud + +La opción `mode` controla cómo se manejan las solicitudes cross-origin y determina el tipo de respuesta. + + +// Solo solicitudes same-origin +http.get('/api/local-data', { + mode: 'same-origin' +}).subscribe(data => { + // ... +}); + +// Solicitudes CORS habilitadas cross-origin +http.get('https://api.external.com/data', { + mode: 'cors' +}).subscribe(data => { + // ... +}); + +// Modo no-CORS para solicitudes simples cross-origin +http.get('https://external-api.com/public-data', { + mode: 'no-cors' +}).subscribe(data => { + // ... +}); + + +Valores disponibles para `mode`: +- `'same-origin'`: Solo permite solicitudes same-origin, falla si es cross-origin +- `'cors'`: Permite solicitudes cross-origin con CORS (por defecto) +- `'no-cors'`: Permite solicitudes simples cross-origin sin CORS, la respuesta es opaca + +TIP: Usa `mode: 'same-origin'` para solicitudes sensibles que nunca deberían ser cross-origin. + +#### Manejo de redirecciones + +La opción `redirect` especifica cómo manejar respuestas de redirección del servidor. + + +// Seguir redirecciones automáticamente (comportamiento por defecto) +http.get('/api/resource', { + redirect: 'follow' +}).subscribe(data => { + // ... +}); + +// Prevenir redirecciones automáticas +http.get('/api/resource', { + redirect: 'manual' +}).subscribe(response => { + // Manejar la redirección manualmente +}); + +// Tratar redirecciones como errores +http.get('/api/resource', { + redirect: 'error' +}).subscribe({ + next: data => { + // Respuesta exitosa + }, + error: err => { + // Las redirecciones activarán este manejador de errores + } +}); + + +Valores disponibles para `redirect`: +- `'follow'`: Seguir redirecciones automáticamente (por defecto) +- `'error'`: Tratar redirecciones como errores +- `'manual'`: No seguir redirecciones automáticamente, devolver la respuesta de redirección + +TIP: Usa `redirect: 'manual'` cuando necesites manejar redirecciones con lógica personalizada. + +#### Manejo de credenciales + +La opción `credentials` controla si se envían cookies, encabezados de autorización y otras credenciales en solicitudes cross-origin. Esto es importante para escenarios de autenticación. + + +// Incluir credenciales en solicitudes cross-origin +http.get('https://api.example.com/protected-data', { + credentials: 'include' +}).subscribe(data => { + // ... +}); + +// Nunca enviar credenciales (por defecto en cross-origin) +http.get('https://api.example.com/public-data', { + credentials: 'omit' +}).subscribe(data => { + // ... +}); + +// Enviar credenciales solo para solicitudes same-origin +http.get('/api/user-data', { + credentials: 'same-origin' +}).subscribe(data => { + // ... +}); + +// withCredentials sobrescribe la opción credentials +http.get('https://api.example.com/data', { + credentials: 'omit', // Será ignorado + withCredentials: true // Fuerza credentials: 'include' +}).subscribe(data => { + // La solicitud incluirá credenciales aunque credentials sea 'omit' +}); + +// Enfoque legado (todavía soportado) +http.get('https://api.example.com/data', { + withCredentials: true +}).subscribe(data => { + // Equivalente a credentials: 'include' +}); + + +IMPORTANTE: La opción `withCredentials` tiene prioridad sobre `credentials`. Si se especifican ambas, `withCredentials: tru`e` siempre resultará en `credentials: 'include'`, sin importar el valor explícito de `credentials`. + +Valores disponivles para `credentials`: +- `'omit'`: Nunca enviar credenciales +- `'same-origin'`: Enviar credenciales solo para solicitudes same-origin (por defecto) +- `'include'`: Siempre enviar credenciales, incluso en solicitudes cross-origin + +TIP: Usa `credentials: 'include'` cuando necesites enviar cookies o encabezados de autenticación a un dominio diferente que soporte CORS. Evita mezclar `credentials` y `withCredentials` para no generar confusión. + +#### Referente (Referrer) + +La opción `referrer` permite controlar qué información de referencia se envía con la solicitud, importante por motivos de privacidad y seguridad. + + +// Enviar una URL específica como referrer +http.get('/api/data', { + referrer: 'https://example.com/page' +}).subscribe(data => { + // ... +}); + +// Usar la página actual como referrer (por defecto) +http.get('/api/analytics', { + referrer: 'about:client' +}).subscribe(data => { + // ... +}); + + +La opción `referrer` acepta: +- Una cadena de URL válida: establece la URL de referrer a enviar +- Una cadena vacía `''`: No envía información de referrer +- `'about:client'`: Utiliza el referente referrer (la URL de la página actual). + +TIP: Usa `referrer: ''` para solicitudes sensibles donde no quieres filtrar la URL de la página que refirió la solicitud. + +#### Integridad (Integrity) + +La opción `integrity` permite verificar que la respuesta no ha sido alterada, proporcionando un hash criptográfico del contenido esperado. Esto es útil al cargar scripts u otros recursos desde CDNs. + + +// Verificar integridad de la respuesta con hash SHA-256 +http.get('/api/script.js', { + integrity: 'sha256-ABC123...', + responseType: 'text' +}).subscribe(script => { + // El contenido del script se verifica contra el hash +}); + + +IMPORTANTE: La opción `integrity` requiere una coincidencia exacta entre el contenido de la respuesta y el hash proporcionado. Si el contenido no coincide, la solicitud fallará con un error de red. + +TIP: Usa integridad de subrecursos (subresource integrity) al cargar recursos críticos de fuentes externas para asegurar que no han sido modificados. Genera los hashes usando herramientas como `openssl` + ## `Observable`s Http Cada método de solicitud en `HttpClient` construye y devuelve un `Observable` del tipo de respuesta solicitado. Entender cómo funcionan estos `Observable`s es importante al usar `HttpClient`. diff --git a/adev-es/src/content/guide/http/testing.en.md b/adev-es/src/content/guide/http/testing.en.md index 20fc8cf..1942ebd 100644 --- a/adev-es/src/content/guide/http/testing.en.md +++ b/adev-es/src/content/guide/http/testing.en.md @@ -43,7 +43,7 @@ const httpTesting = TestBed.inject(HttpTestingController); // Load `ConfigService` and request the current configuration. const service = TestBed.inject(ConfigService); -const config$ = this.configService.getConfig(); +const config$ = service.getConfig(); // `firstValueFrom` subscribes to the `Observable`, which makes the HTTP request, // and creates a `Promise` of the response. diff --git a/adev-es/src/content/guide/http/testing.md b/adev-es/src/content/guide/http/testing.md index 887a17e..975cafc 100644 --- a/adev-es/src/content/guide/http/testing.md +++ b/adev-es/src/content/guide/http/testing.md @@ -43,7 +43,7 @@ const httpTesting = TestBed.inject(HttpTestingController); // Cargar `ConfigService` y solicitar la configuración actual. const service = TestBed.inject(ConfigService); -const config$ = this.configService.getConfig(); +const config$ = service.getConfig(); // `firstValueFrom` se suscribe al `Observable`, lo que hace la solicitud HTTP, // y crea una `Promise` de la respuesta. diff --git a/adev-es/src/content/guide/i18n/overview.md b/adev-es/src/content/guide/i18n/overview.md index 0ad4076..d504e1a 100644 --- a/adev-es/src/content/guide/i18n/overview.md +++ b/adev-es/src/content/guide/i18n/overview.md @@ -1,4 +1,4 @@ -# Angular Internationalization +# Angular Internationalization (i18n) *Internationalization*, sometimes referenced as i18n, is the process of designing and preparing your project for use in different locales around the world. *Localization* is the process of building versions of your project for different locales. diff --git a/adev-es/src/content/guide/i18n/translation-files.md b/adev-es/src/content/guide/i18n/translation-files.md index 6ee0a25..5b90e60 100644 --- a/adev-es/src/content/guide/i18n/translation-files.md +++ b/adev-es/src/content/guide/i18n/translation-files.md @@ -245,7 +245,7 @@ The following example displays both translation units after translating. [JsonMain]: https://www.json.org "Introducing JSON | JSON" -[OasisOpenDocsXliffXliffCoreXliffCoreHtml]: http://docs.oasis-open.org/xliff/xliff-core/xliff-core.html "XLIFF Version 1.2 Specification | Oasis Open Docs" +[OasisOpenDocsXliffXliffCoreXliffCoreHtml]: https://docs.oasis-open.org/xliff/v1.2/os/xliff-core.html "XLIFF Version 1.2 Specification | Oasis Open Docs" [OasisOpenDocsXliffXliffCoreV20Cos01XliffCoreV20Cose01Html]: http://docs.oasis-open.org/xliff/xliff-core/v2.0/cos01/xliff-core-v2.0-cos01.html "XLIFF Version 2.0 | Oasis Open Docs" [UnicodeCldrDevelopmentDevelopmentProcessDesignProposalsXmb]: http://cldr.unicode.org/development/development-process/design-proposals/xmb "XMB | CLDR - Unicode Common Locale Data Repository | Unicode" diff --git a/adev-es/src/content/guide/routing/customizing-route-behavior.md b/adev-es/src/content/guide/routing/customizing-route-behavior.md new file mode 100644 index 0000000..6ae475f --- /dev/null +++ b/adev-es/src/content/guide/routing/customizing-route-behavior.md @@ -0,0 +1,473 @@ +# Customizing route behavior + +Angular Router provides powerful extension points that allow you to customize how routes behave in your application. While the default routing behavior works well for most applications, specific requirements often demand custom implementations for performance optimization, specialized URL handling, or complex routing logic. + +Route customization can become valuable when your application needs: + +- **Component state preservation** across navigations to avoid re-fetching data +- **Strategic lazy module loading** based on user behavior or network conditions +- **External URL integration** or handling Angular routes alongside legacy systems +- **Dynamic route matching** based on runtime conditions beyond simple path + patterns + +NOTE: Before implementing custom strategies, ensure the default router behavior doesn't meet your needs. Angular's default routing is optimized for common use cases and provides the best balance of performance and simplicity. Customizing route strategies can create additional code complexity and have performance implications on memory usage if not carefully managed. + +Angular Router exposes four main areas for customization: + + + + + + + + +## Route reuse strategy + +Route reuse strategy controls whether Angular destroys and recreates components during navigation or preserves them for reuse. By default, Angular destroys component instances when navigating away from a route and creates new instances when navigating back. + +### When to implement route reuse + +Custom route reuse strategies benefit applications that need: + +- **Form state preservation** - Keep partially completed forms when users navigate away and return +- **Expensive data retention** - Avoid re-fetching large datasets or complex calculations +- **Scroll position maintenance** - Preserve scroll positions in long lists or infinite scroll implementations +- **Tab-like interfaces** - Maintain component state when switching between tabs + +### Creating a custom route reuse strategy + +Angular's `RouteReuseStrategy` class allows you to customize navigation behavior through the concept of "detached route handles." + +"Detached route handles" are Angular's way of storing component instances and their entire view hierarchy. When a route is detached, Angular preserves the component instance, its child components, and all associated state in memory. This preserved state can later be reattached when navigating back to the route. + +The `RouteReuseStrategy` class provides five methods that control the lifecycle of route components: + +| Method | Description | +| -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | +| [`shouldDetach`](api/router/RouteReuseStrategy#shouldDetach) | Determines if a route should be stored for later reuse when navigating away | +| [`store`](api/router/RouteReuseStrategy#store) | Stores the detached route handle when `shouldDetach` returns true | +| [`shouldAttach`](api/router/RouteReuseStrategy#shouldAttach) | Determines if a stored route should be reattached when navigating to it | +| [`retrieve`](api/router/RouteReuseStrategy#retrieve) | Returns the previously stored route handle for reattachment | +| [`shouldReuseRoute`](api/router/RouteReuseStrategy#shouldReuseRoute) | Determines if the router should reuse the current route instance instead of destroying it during navigation | + +The following example demonstrates a custom route reuse strategy that selectively preserves component state based on route metadata: + +```ts +import { RouteReuseStrategy, ActivatedRouteSnapshot, DetachedRouteHandle } from '@angular/router'; +import { Injectable } from '@angular/core'; + +@Injectable() +export class CustomRouteReuseStrategy implements RouteReuseStrategy { + private handlers = new Map(); + + shouldDetach(route: ActivatedRouteSnapshot): boolean { + // Determines if a route should be stored for later reuse + return route.data['reuse'] === true; + } + + store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle | null): void { + // Stores the detached route handle when shouldDetach returns true + if (handle && route.data['reuse'] === true) { + const key = this.getRouteKey(route); + this.handlers.set(key, handle); + } + } + + shouldAttach(route: ActivatedRouteSnapshot): boolean { + // Checks if a stored route should be reattached + const key = this.getRouteKey(route); + return route.data['reuse'] === true && this.handlers.has(key); + } + + retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null { + // Returns the stored route handle for reattachment + const key = this.getRouteKey(route); + return route.data['reuse'] === true ? this.handlers.get(key) ?? null : null; + } + + shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean { + // Determines if the router should reuse the current route instance + return future.routeConfig === curr.routeConfig; + } + + private getRouteKey(route: ActivatedRouteSnapshot): string { + return route.routeConfig ?? ''; + } +} +``` + +### Configuring a route to use a custom route reuse strategy + +Routes can opt into reuse behavior through route configuration metadata. This approach keeps the reuse logic separate from component code, making it easy to adjust behavior without modifying components: + +```ts +export const routes: Routes = [ + { + path: 'products', + component: ProductListComponent, + data: { reuse: true } // Component state persists across navigations + }, + { + path: 'products/:id', + component: ProductDetailComponent, + // No reuse flag - component recreates on each navigation + }, + { + path: 'search', + component: SearchComponent, + data: { reuse: true } // Preserves search results and filter state + } +]; +``` + +You can also configure a custom route reuse strategy at the application level through Angular's dependency injection system. In this case, Angular creates a single instance of the strategy that manages all route reuse decisions throughout the application: + +```ts +export const appConfig: ApplicationConfig = { + providers: [ + provideRouter(routes), + { provide: RouteReuseStrategy, useClass: CustomRouteReuseStrategy } + ] +}; +``` + +## Preloading strategy + +Preloading strategies determine when Angular loads lazy-loaded route modules in the background. While lazy loading improves initial load time by deferring module downloads, users still experience a delay when first navigating to a lazy route. Preloading strategies eliminate this delay by loading modules before users request them. + +### Built-in preloading strategies + +Angular provides two preloading strategies out of the box: + +| Strategy | Description | +| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| [`NoPreloading`](api/router/NoPreloading) | The default strategy that disables all preloading. In other words, modules only load when users navigate to them | +| [`PreloadAllModules`](api/router/PreloadAllModules) | Loads all lazy-loaded modules immediately after the initial navigation | + +The `PreloadAllModules` strategy can be configured as follows: + +```ts +import { ApplicationConfig } from '@angular/core'; +import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router'; +import { routes } from './app.routes'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideRouter( + routes, + withPreloading(PreloadAllModules) + ) + ] +}; +``` + +The `PreloadAllModules` strategy works well for small to medium applications where downloading all modules doesn't significantly impact performance. However, larger applications with many feature modules might benefit from more selective preloading. + +### Creating a custom preloading strategy + +Custom preloading strategies implement the `PreloadingStrategy` interface, which requires a single `preload` method. This method receives the route configuration and a function that triggers the actual module load. The strategy returns an Observable that emits when preloading completes or an empty Observable to skip preloading: + +```ts +import { Injectable } from '@angular/core'; +import { PreloadingStrategy, Route } from '@angular/router'; +import { Observable, of, timer } from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; + +@Injectable() +export class SelectivePreloadingStrategy implements PreloadingStrategy { + preload(route: Route, load: () => Observable): Observable { + // Only preload routes marked with data: { preload: true } + if (route.data?.['preload']) { + return load(); + } + return of(null); + } +} +``` + +This selective strategy checks route metadata to determine preloading behavior. Routes can opt into preloading through their configuration: + +```ts +import { Routes } from '@angular/router'; + +export const routes: Routes = [ + { + path: 'dashboard', + loadChildren: () => import('./dashboard/dashboard.routes'), + data: { preload: true } // Preload immediately after initial navigation + }, + { + path: 'reports', + loadChildren: () => import('./reports/reports.routes'), + data: { preload: false } // Only load when user navigates to reports + }, + { + path: 'admin', + loadChildren: () => import('./admin/admin.routes') + // No preload flag - won't be preloaded + } +]; +``` + +### Performance considerations for preloading + +Preloading impacts both network usage and memory consumption. Each preloaded module consumes bandwidth and increases the application's memory footprint. Mobile users on metered connections might prefer minimal preloading, while desktop users on fast networks can handle aggressive preloading strategies. + +The timing of preloading also matters. Immediate preloading after initial load might compete with other critical resources like images or API calls. Strategies should consider the application's post-load behavior and coordinate with other background tasks to avoid performance degradation. + +Browser resource limits also affect preloading behavior. Browsers limit concurrent HTTP connections, so aggressive preloading might queue behind other requests. Service workers can help by providing fine-grained control over caching and network requests, complementing the preloading strategy. + +## URL handling strategy + +URL handling strategies determine which URLs the Angular router processes versus which ones it ignores. By default, Angular attempts to handle all navigation events within the application, but real-world applications often need to coexist with other systems, handle external links, or integrate with legacy applications that manage their own routes. + +The `UrlHandlingStrategy` class gives you control over this boundary between Angular-managed routes and external URLs. This becomes essential when migrating applications to Angular incrementally or when Angular applications need to share URL space with other frameworks. + +### Implementing a custom URL handling strategy + +Custom URL handling strategies extend the `UrlHandlingStrategy` class and implement three methods. The `shouldProcessUrl` method determines whether Angular should handle a given URL, `extract` returns the portion of the URL that Angular should process, and `merge` combines the URL fragment with the rest of the URL: + +```ts +import { Injectable } from '@angular/core'; +import { UrlHandlingStrategy, UrlTree } from '@angular/router'; + +@Injectable() +export class CustomUrlHandlingStrategy implements UrlHandlingStrategy { + shouldProcessUrl(url: UrlTree): boolean { + // Only handle URLs that start with /app or /admin + return url.toString().startsWith('/app') || + url.toString().startsWith('/admin'); + } + + extract(url: UrlTree): UrlTree { + // Return the URL unchanged if we should process it + return url; + } + + merge(newUrlPart: UrlTree, rawUrl: UrlTree): UrlTree { + // Combine the URL fragment with the rest of the URL + return newUrlPart; + } +} +``` + +This strategy creates clear boundaries in the URL space. Angular handles `/app` and `/admin` paths while ignoring everything else. This pattern works well when migrating legacy applications where Angular controls specific sections while the legacy system maintains others. + +### Configuring a custom URL handling strategy + +You can register a custom strategy through Angular's dependency injection system: + +```ts +import { ApplicationConfig } from '@angular/core'; +import { provideRouter } from '@angular/router'; +import { UrlHandlingStrategy } from '@angular/router'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideRouter(routes), + { provide: UrlHandlingStrategy, useClass: CustomUrlHandlingStrategy } + ] +}; +``` + +## Custom route matchers + +By default, Angular's router iterates through routes in the order they're defined, attempting to match the URL path against each route's path pattern. It supports static segments, parameterized segments (`:id`), and wildcards (`**`). The first route that matches wins, and the router stops searching. + +When applications require more sophisticated matching logic based on runtime conditions, complex URL patterns, or other custom rules, custom matchers provide this flexibility without compromising the simplicity of standard routes. + +The router evaluates custom matchers during the route matching phase, before path matching occurs. When a matcher returns a successful match, it can also extract parameters from the URL, making them available to the activated component just like standard route parameters. + +### Creating a custom matcher + +A custom matcher is a function that receives URL segments and returns either a match result with consumed segments and parameters, or null to indicate no match. The matcher function runs before Angular evaluates the route's path property: + +```ts +import { Route, UrlSegment, UrlSegmentGroup, UrlMatchResult } from '@angular/router'; + +export function customMatcher( + segments: UrlSegment[], + group: UrlSegmentGroup, + route: Route +): UrlMatchResult | null { + // Matching logic here + if (matchSuccessful) { + return { + consumed: segments, + posParams: { + paramName: new UrlSegment('paramValue', {}) + } + }; + } + return null; +} +``` + +### Implementing version-based routing + +Consider an API documentation site that needs to route based on version numbers in the URL. Different versions might have different component structures or feature sets: + +```ts +import { Routes, UrlSegment, UrlMatchResult } from '@angular/router'; + +export function versionMatcher(segments: UrlSegment[]): UrlMatchResult | null { + // Match patterns like /v1/docs, /v2.1/docs, /v3.0.1/docs + if (segments.length >= 2 && segments[0].path.match(/^v\d+(\.\d+)*$/)) { + return { + consumed: segments.slice(0, 2), // Consume version and 'docs' + posParams: { + version: segments[0], // Make version available as a parameter + section: segments[1] // Make section available too + } + }; + } + return null; +} + +// Route configuration +export const routes: Routes = [ + { + matcher: versionMatcher, + component: DocumentationComponent + }, + { + path: 'latest/docs', + redirectTo: 'v3/docs' + } +]; +``` + +The component receives the extracted parameters through route inputs: + +```ts +import { Component, input, inject } from '@angular/core'; +import { resource } from '@angular/core'; + +@Component({ + selector: 'app-documentation', + template: ` + @if (documentation.isLoading()) { +
    Loading documentation...
    + } @else if (documentation.error()) { +
    Error loading documentation
    + } @else if (documentation.value(); as docs) { +
    {{ docs.content }}
    + } + ` +}) +export class DocumentationComponent { + // Route parameters are automatically bound to signal inputs + version = input.required(); // Receives the version parameter + section = input.required(); // Receives the section parameter + + private docsService = inject(DocumentationService); + + // Resource automatically loads documentation when version or section changes + documentation = resource({ + params: () => { + if (!this.version() || !this.section()) return; + + return { + version: this.version(), + section: this.section() + } + }, + loader: ({ params }) => { + return this.docsService.loadDocumentation(params.version, params.section); + } + }) +} +``` + +### Locale-aware routing + +International applications often encode locale information in URLs. A custom matcher can extract locale codes and route to appropriate components while making the locale available as a parameter: + +```ts +// Supported locales +const locales = ['en', 'es', 'fr', 'de', 'ja', 'zh']; + +export function localeMatcher(segments: UrlSegment[]): UrlMatchResult | null { + if (segments.length > 0) { + const potentialLocale = segments[0].path; + + if (locales.includes(potentialLocale)) { + // This is a locale prefix, consume it and continue matching + return { + consumed: [segments[0]], + posParams: { + locale: segments[0] + } + }; + } else { + // No locale prefix, use default locale + return { + consumed: [], // Don't consume any segments + posParams: { + locale: new UrlSegment('en', {}) + } + }; + } + } + + return null; +} +``` + +### Complex business logic matching + +Custom matchers excel at implementing business rules that would be awkward to express in path patterns. Consider an e-commerce site where product URLs follow different patterns based on product type: + +```ts +export function productMatcher(segments: UrlSegment[]): UrlMatchResult | null { + if (segments.length === 0) return null; + + const firstSegment = segments[0].path; + + // Books: /isbn-1234567890 + if (firstSegment.startsWith('isbn-')) { + return { + consumed: [segments[0]], + posParams: { + productType: new UrlSegment('book', {}), + identifier: new UrlSegment(firstSegment.substring(5), {}) + } + }; + } + + // Electronics: /sku/ABC123 + if (firstSegment === 'sku' && segments.length > 1) { + return { + consumed: segments.slice(0, 2), + posParams: { + productType: new UrlSegment('electronics', {}), + identifier: segments[1] + } + }; + } + + // Clothing: /style/BRAND/ITEM + if (firstSegment === 'style' && segments.length > 2) { + return { + consumed: segments.slice(0, 3), + posParams: { + productType: new UrlSegment('clothing', {}), + brand: segments[1], + identifier: segments[2] + } + }; + } + + return null; +} +``` + +### Performance considerations for custom matchers + +Custom matchers run for every navigation attempt until a match is found. As a result, complex matching logic can impact navigation performance, especially in applications with many routes. Keep matchers focused and efficient: + +- Return early when a match is impossible +- Avoid expensive operations like API calls or complex regular expressions +- Consider caching results for repeated URL patterns + +While custom matchers solve complex routing requirements elegantly, overuse can make route configuration harder to understand and maintain. Reserve custom matchers for scenarios where standard path matching genuinely falls short. diff --git a/adev-es/src/content/guide/routing/data-resolvers.md b/adev-es/src/content/guide/routing/data-resolvers.md new file mode 100644 index 0000000..4407598 --- /dev/null +++ b/adev-es/src/content/guide/routing/data-resolvers.md @@ -0,0 +1,298 @@ +# Data resolvers + +Data resolvers allow you to fetch data before navigating to a route, ensuring that your components receive the data they need before rendering. This can help prevent the need for loading states and improve the user experience by pre-loading essential data. + +## What are data resolvers? + +A data resolver is a service that implements the [`ResolveFn`](api/router/ResolveFn) function. It runs before a route activates and can fetch data from APIs, databases, or other sources. The resolved data becomes available to the component through the [`ActivatedRoute`](api/router/ActivatedRoute). + +## Why use data resolvers? + +Data resolvers solve common routing challenges: + +- **Prevent empty states**: Components receive data immediately upon loading +- **Better user experience**: No loading spinners for critical data +- **Error handling**: Handle data fetching errors before navigation +- **Data consistency**: Ensure required data is available before rendering which is important for SSR + +## Creating a resolver + +You create a resolver by writing a function with the [`ResolveFn`](api/router/ResolveFn) type. + +It receives the [`ActivatedRouteSnapshot`](api/router/ActivatedRouteSnapshot) and [`RouterStateSnapshot`](api/router/RouterStateSnapshot) as parameters. + +Here is a resolver that gets the user information before rendering a route using the [`inject`](api/core/inject) function: + +```ts +import { inject } from '@angular/core'; +import { UserStore, SettingsStore } from './user-store'; +import type { ActivatedRouteSnapshot, ResolveFn, RouterStateSnapshot } from '@angular/router'; +import type { User, Settings } from './types'; + +export const userResolver: ResolveFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => { + const userStore = inject(UserStore); + const userId = route.paramMap.get('id')!; + return userStore.getUser(userId); +}; + +export const settingsResolver: ResolveFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => { + const settingsStore = inject(SettingsStore); + const userId = route.paramMap.get('id')!; + return settingsStore.getUserSettings(userId); +}; +``` + +## Configuring routes with resolvers + +When you want to add one or more data resolvers to a route, you can add it under the `resolve` key in the route configuration. The [`Routes`](api/router/Routes) type defines the structure for route configurations: + +```ts +import { Routes } from '@angular/router'; + +export const routes: Routes = [ + { + path: 'user/:id', + component: UserDetail, + resolve: { + user: userResolver, + settings: settingsResolver + } + } +]; +``` + +You can learn more about the [`resolve` configuration in the API docs](api/router/Route#resolve). + +## Accessing resolved data in components + +### Using ActivatedRoute + +You can access the resolved data in a component by accessing the snapshot data from the [`ActivatedRoute`](api/router/ActivatedRoute) using the [`signal`](api/core/signal) function: + +```angular-ts +import { Component, inject, computed } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { toSignal } from '@angular/core/rxjs-interop'; +import type { User, Settings } from './types'; + +@Component({ + template: ` +

    {{ user().name }}

    +

    {{ user().email }}

    +
    Theme: {{ settings().theme }}
    + ` +}) +export class UserDetail { + private route = inject(ActivatedRoute); + private data = toSignal(this.route.data); + user = computed(() => this.data().user as User); + settings = computed(() => this.data().settings as Settings); +} +``` + +### Using withComponentInputBinding + +A different approach to accessing the resolved data is to use [`withComponentInputBinding()`](api/router/withComponentInputBinding) when configuring your router with [`provideRouter`](api/router/provideRouter). This allows resolved data to be passed directly as component inputs: + +```ts +import { bootstrapApplication } from '@angular/platform-browser'; +import { provideRouter, withComponentInputBinding } from '@angular/router'; +import { routes } from './app.routes'; + +bootstrapApplication(App, { + providers: [ + provideRouter(routes, withComponentInputBinding()) + ] +}); +``` + +With this configuration, you can define inputs in your component that match the resolver keys using the [`input`](api/core/input) function and [`input.required`](api/core/input#required) for required inputs: + +```angular-ts +import { Component, input } from '@angular/core'; +import type { User, Settings } from './types'; + +@Component({ + template: ` +

    {{ user().name }}

    +

    {{ user().email }}

    +
    Theme: {{ settings().theme }}
    + ` +}) +export class UserDetail { + user = input.required(); + settings = input.required(); +} +``` + +This approach provides better type safety and eliminates the need to inject `ActivatedRoute` just to access resolved data. + +## Error handling in resolvers + +In the event of navigation failures, it is important to handle errors gracefully in your data resolvers. Otherwise, a `NavigationError` will occur and the navigation to the current route will fail which will lead to a poor experience for your users. + +There are three primary ways to handle errors with data resolvers: + +1. [Centralizing error handling in `withNavigationErrorHandler`](#centralize-error-handling-in-withnavigationerrorhandler) +2. [Managing errors through a subscription to router events](#managing-errors-through-a-subscription-to-router-events) +3. [Handling errors directly in the resolver](#handling-errors-directly-in-the-resolver) + +### Centralize error handling in `withNavigationErrorHandler` + +The [`withNavigationErrorHandler`](api/router/withNavigationErrorHandler) feature provides a centralized way to handle all navigation errors, including those from failed data resolvers. This approach keeps error handling logic in one place and prevents duplicate error handling code across resolvers. + +```ts +import { bootstrapApplication } from '@angular/platform-browser'; +import { provideRouter, withNavigationErrorHandler } from '@angular/router'; +import { inject } from '@angular/core'; +import { Router } from '@angular/router'; +import { routes } from './app.routes'; + +bootstrapApplication(App, { + providers: [ + provideRouter(routes, withNavigationErrorHandler((error) => { + const router = inject(Router); + + if (error?.message) { + console.error('Navigation error occurred:', error.message) + } + + router.navigate(['/error']); + })) + ] +}); +``` + +With this configuration, your resolvers can focus on data fetching while letting the centralized handler manage error scenarios: + +```ts +export const userResolver: ResolveFn = (route) => { + const userStore = inject(UserStore); + const userId = route.paramMap.get('id')!; + // No need for explicit error handling - let it bubble up + return userStore.getUser(userId); +}; +``` + +### Managing errors through a subscription to router events + +You can also handle resolver errors by subscribing to router events and listening for [`NavigationError`](api/router/NavigationError) events. This approach gives you more granular control over error handling and allows you to implement custom error recovery logic. + +```angular-ts +import { Component, inject, signal } from '@angular/core'; +import { Router, NavigationError } from '@angular/router'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { filter, map } from 'rxjs'; + +@Component({ + selector: 'app-root', + template: ` + @if (errorMessage()) { +
    + {{ errorMessage() }} + +
    + } + + ` +}) +export class App { + private router = inject(Router); + private lastFailedUrl = signal(''); + + private navigationErrors = toSignal( + this.router.events.pipe( + filter((event): event is NavigationError => event instanceof NavigationError), + map(event => { + this.lastFailedUrl.set(event.url); + + if (event.error) { + console.error('Navigation error', event.error) + } + + return 'Navigation failed. Please try again.'; + }) + ), + { initialValue: '' } + ); + + errorMessage = this.navigationErrors; + + retryNavigation() { + if (this.lastFailedUrl()) { + this.router.navigateByUrl(this.lastFailedUrl()); + } + } +} +``` + +This approach is particularly useful when you need to: + +- Implement custom retry logic for failed navigation +- Show specific error messages based on the type of failure +- Track navigation failures for analytics purposes + +### Handling errors directly in the resolver + +Here's an updated example of the `userResolver` that logs the error and navigates back to the generic `/users` page using the [`Router`](api/router/Router) service: + +```ts +import { inject } from '@angular/core'; +import { ResolveFn, RedirectCommand, Router } from '@angular/router'; +import { catchError, of, EMPTY } from 'rxjs'; +import { UserStore } from './user-store'; +import type { User } from './types'; + +export const userResolver: ResolveFn = (route) => { + const userStore = inject(UserStore); + const router = inject(Router); + const userId = route.paramMap.get('id')!; + + return userStore.getUser(userId).pipe( + catchError(error => { + console.error('Failed to load user:', error); + return of(new RedirectCommand(router.parseUrl('/users'))); + }) + ); +}; +``` + +## Navigation loading considerations + +While data resolvers prevent loading states within components, they introduce a different UX consideration: navigation is blocked while resolvers execute. Users may experience delays between clicking a link and seeing the new route, especially with slow network requests. + +### Providing navigation feedback + +To improve user experience during resolver execution, you can listen to router events and show loading indicators: + +```angular-ts +import { Component, inject } from '@angular/core'; +import { Router } from '@angular/router'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { map } from 'rxjs'; + +@Component({ + selector: 'app-root', + template: ` + @if (isNavigating()) { +
    Loading...
    + } + + ` +}) +export class App { + private router = inject(Router); + isNavigating = computed(() => !!this.router.currentNavigation()); +} +``` + +This approach ensures users receive visual feedback that navigation is in progress while resolvers fetch data. + +## Best practices + +- **Keep resolvers lightweight**: Resolvers should fetch essential data only and not everything the page could possibly need +- **Handle errors**: Always remember to handle errors gracefully to provide the best experience possible to users +- **Use caching**: Consider caching resolved data to improve performance +- **Consider navigation UX**: Implement loading indicators for resolver execution since navigation is blocked during data fetching +- **Set reasonable timeouts**: Avoid resolvers that could hang indefinitely and block navigation +- **Type safety**: Use TypeScript interfaces for resolved data diff --git a/adev-es/src/content/guide/routing/define-routes.md b/adev-es/src/content/guide/routing/define-routes.md index a596c98..3c7257b 100644 --- a/adev-es/src/content/guide/routing/define-routes.md +++ b/adev-es/src/content/guide/routing/define-routes.md @@ -216,11 +216,11 @@ export const routes: Routes = [ // their corresponding routes become active. { path: 'login', - loadComponent: () => import('./components/auth/login-page') + loadComponent: () => import('./components/auth/login-page').then(m => m.LoginPage) }, { path: '', - loadComponent: () => import('./components/home/home-page') + loadComponent: () => import('./components/home/home-page').then(m => m.HomePage) } ] ``` @@ -262,7 +262,7 @@ If you modify or remove a route, some users may still click on out-of-date links You can associate a **title** with each route. Angular automatically updates the [page title](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/title) when a route activates. Always define appropriate page titles for your application, as these titles are necessary to create an accessible experience. -```angular-ts +```ts import { Routes } from '@angular/router'; import { HomeComponent } from './home/home.component'; import { AboutComponent } from './about/about.component'; @@ -279,14 +279,26 @@ const routes: Routes = [ component: AboutComponent, title: 'About Us' }, +]; +``` + +The page `title` property can be set dynamincally to a resolver function using [`ResolveFn`](/api/router/ResolveFn). + +```ts +const titleResolver: ResolveFn = (route) => route.queryParams['id']; +const routes: Routes = [ + ... { path: 'products', component: ProductsComponent, - title: 'Our Products' + title: titleResolver, } ]; + ``` +Route titles can also be set via a service extending the [`TitleStrategy`](/api/router/TitleStrategy) abstract class. By default, Angular uses the [`DefaultTitleStrategy`](/api/router/DefaultTitleStrategy). + ## Route-level providers for dependency injection Each route has a `providers` property that lets you provide dependencies to that route's content via [dependency injection](/guide/di). @@ -351,7 +363,7 @@ You can read this static data by injecting the `ActivatedRoute`. See [Reading ro ### Dynamic data with data resolvers -When you need to provide dynamic data to a route, check out the [guide on route data resolvers](/guide/router/route-data-resolvers). +When you need to provide dynamic data to a route, check out the [guide on route data resolvers](/guide/routing/data-resolvers). ## Nested Routes @@ -365,7 +377,7 @@ You can add child routes to any route definition with the `children` property: const routes: Routes = [ { path: 'product/:id', - component: 'ProductComponent', + component: ProductComponent, children: [ { path: 'info', diff --git a/adev-es/src/content/guide/routing/lifecycle-and-events.md b/adev-es/src/content/guide/routing/lifecycle-and-events.md new file mode 100644 index 0000000..6e3676f --- /dev/null +++ b/adev-es/src/content/guide/routing/lifecycle-and-events.md @@ -0,0 +1,243 @@ +# Router Lifecycle and Events + +Angular Router provides a comprehensive set of lifecycle hooks and events that allow you to respond to navigation changes and execute custom logic during the routing process. + +## Common router events + +The Angular Router emits navigation events that you can subscribe to in order to track the navigation lifecycle. These events are available through the `Router.events` observable. This section covers common routing lifecycle events for navigation and error tracking (in chronological order). + +| Events | Description | +| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | +| [`NavigationStart`](api/router/NavigationStart) | Occurs when navigation begins and contains the requested URL. | +| [`RoutesRecognized`](api/router/RoutesRecognized) | Occurs after the router determines which route matches the URL and contains the route state information. | +| [`GuardsCheckStart`](api/router/GuardsCheckStart) | Begins the route guard phase. The router evaluates route guards like `canActivate` and `canDeactivate`. | +| [`GuardsCheckEnd`](api/router/GuardsCheckEnd) | Signals completion of guard evaluation. Contains the result (allowed/denied). | +| [`ResolveStart`](api/router/ResolveStart) | Begins the data resolution phase. Route resolvers start fetching data. | +| [`ResolveEnd`](api/router/ResolveEnd) | Data resolution completes. All required data becomes available. | +| [`NavigationEnd`](api/router/NavigationEnd) | Final event when navigation completes successfully. The router updates the URL. | +| [`NavigationSkipped`](api/router/NavigationSkipped) | Occurs when the router skips navigation (e.g., same URL navigation). | + +The following are common error events: + +| Event | Description | +| ------------------------------------------------- | -------------------------------------------------------------------------------- | +| [`NavigationCancel`](api/router/NavigationCancel) | Occurs when the router cancels navigation. Often due to a guard returning false. | +| [`NavigationError`](api/router/NavigationError) | Occurs when navigation fails. Could be due to invalid routes or resolver errors. | + +For a list of all lifecycle events, check out the [complete table of this guide](#all-router-events). + +## How to subscribe to router events + +When you want to run code during specific navigation lifecycle events, you can do so by subscribing to the `router.events` and checking the instance of the event: + +```ts +// Example of subscribing to router events +import { Component, inject, signal, effect } from '@angular/core'; +import { Event, Router, NavigationStart, NavigationEnd } from '@angular/router'; + +@Component({ ... }) +export class RouterEventsComponent { + private readonly router = inject(Router); + + constructor() { + // Subscribe to router events and react to events + this.router.events.pipe(takeUntilDestroyed()).subscribe((event: Event) => { + if (event instanceof NavigationStart) { + // Navigation starting + console.log('Navigation starting:', event.url); + } + if (event instanceof NavigationEnd) { + // Navigation completed + console.log('Navigation completed:', event.url); + } + }); + } +} +``` + +Note: The [`Event`](api/router/Event) type from `@angular/router` is named the same as the regular global [`Event`](https://developer.mozilla.org/en-US/docs/Web/API/Event) type, but it is different from the [`RouterEvent`](api/router/RouterEvent) type. + +## How to debug routing events + +Debugging router navigation issues can be challenging without visibility into the event sequence. Angular provides a built-in debugging feature that logs all router events to the console, helping you understand the navigation flow and identify where issues occur. + +When you need to inspect a Router event sequence, you can enable logging for internal navigation events for debugging. You can configure this by passing a configuration option (`withDebugTracing()`) that enables detailed console logging of all routing events. + +```ts +import { provideRouter, withDebugTracing } from '@angular/router'; + +const appRoutes: Routes = []; +bootstrapApplication(AppComponent, + { + providers: [ + provideRouter(appRoutes, withDebugTracing()) + ] + } +); +``` + +For more information, check out the official docs on [`withDebugTracing`](api/router/withDebugTracing). + +## Common use cases + +Router events enable many practical features in real-world applications. Here are some common patterns that are used with router events. + +### Loading indicators + +Show loading indicators during navigation: + +```angular-ts +import { Component, inject } from '@angular/core'; +import { Router } from '@angular/router'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { map } from 'rxjs/operators'; + +@Component({ + selector: 'app-loading', + template: ` + @if (loading()) { +
    Loading...
    + } + ` +}) +export class AppComponent { + private router = inject(Router); + + readonly loading = toSignal( + this.router.events.pipe( + map(() => !!this.router.getCurrentNavigation()) + ), + { initialValue: false } + ); +} +``` + +### Analytics tracking + +Track page views for analytics: + +```typescript +import { Component, inject, signal, effect } from '@angular/core'; +import { Router, NavigationEnd } from '@angular/router'; + +@Injectable({ providedIn: 'root' }) +export class AnalyticsService { + private router = inject(Router); + private destroyRef = inject(DestroyRef); + + startTracking() { + this.router.events.pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(event => { + // Track page views when URL changes + if (event instanceof NavigationEnd) { + // Send page view to analytics + this.analytics.trackPageView(url); + } + }); + } + + private analytics = { + trackPageView: (url: string) => { + console.log('Page view tracked:', url); + } + }; +} +``` + +### Error handling + +Handle navigation errors gracefully and provide user feedback: + +```angular-ts +import { Component, inject, signal } from '@angular/core'; +import { Router, NavigationStart, NavigationError, NavigationCancel, NavigationCancellationCode } from '@angular/router'; + +@Component({ + selector: 'app-error-handler', + template: ` + @if (errorMessage()) { +
    + {{ errorMessage() }} + +
    + } + ` +}) +export class ErrorHandlerComponent { + private router = inject(Router); + readonly errorMessage = signal(''); + + constructor() { + this.router.events.pipe(takeUntilDestroyed()).subscribe(event => { + if (event instanceof NavigationStart) { + this.errorMessage.set(''); + } else if (event instanceof NavigationError) { + console.error('Navigation error:', event.error); + this.errorMessage.set('Failed to load page. Please try again.'); + } else if (event instanceof NavigationCancel) { + console.warn('Navigation cancelled:', event.reason); + if (event.reason === NavigationCancellationCode.GuardRejected) { + this.errorMessage.set('Access denied. Please check your permissions.'); + } + } + }); + } + + dismissError() { + this.errorMessage.set(''); + } +} +``` + +## All router events + +For reference, here is the complete list of all router events available in Angular. These events are organized by category and listed in the order they typically occur during navigation. + +### Navigation events + +These events track the core navigation process from start through route recognition, guard checks, and data resolution. They provide visibility into each phase of the navigation lifecycle. + +| Event | Description | +| --------------------------------------------------------- | --------------------------------------------------------------- | +| [`NavigationStart`](api/router/NavigationStart) | Occurs when navigation starts | +| [`RouteConfigLoadStart`](api/router/RouteConfigLoadStart) | Occurs before lazy loading a route configuration | +| [`RouteConfigLoadEnd`](api/router/RouteConfigLoadEnd) | Occurs after a lazy-loaded route configuration loads | +| [`RoutesRecognized`](api/router/RoutesRecognized) | Occurs when the router parses the URL and recognizes the routes | +| [`GuardsCheckStart`](api/router/GuardsCheckStart) | Occurs at the start of the guard phase | +| [`GuardsCheckEnd`](api/router/GuardsCheckEnd) | Occurs at the end of the guard phase | +| [`ResolveStart`](api/router/ResolveStart) | Occurs at the start of the resolve phase | +| [`ResolveEnd`](api/router/ResolveEnd) | Occurs at the end of the resolve phase | + +### Activation events + +These events occur during the activation phase when route components are being instantiated and initialized. Activation events fire for each route in the route tree, including parent and child routes. + +| Event | Description | +| --------------------------------------------------------- | --------------------------------------------- | +| [`ActivationStart`](api/router/ActivationStart) | Occurs at the start of route activation | +| [`ChildActivationStart`](api/router/ChildActivationStart) | Occurs at the start of child route activation | +| [`ActivationEnd`](api/router/ActivationEnd) | Occurs at the end of route activation | +| [`ChildActivationEnd`](api/router/ChildActivationEnd) | Occurs at the end of child route activation | + +### Navigation completion events + +These events represent the final outcome of a navigation attempt. Every navigation will end with exactly one of these events, indicating whether it succeeded, was cancelled, failed, or was skipped. + +| Event | Description | +| --------------------------------------------------- | ------------------------------------------------------------------- | +| [`NavigationEnd`](api/router/NavigationEnd) | Occurs when navigation ends successfully | +| [`NavigationCancel`](api/router/NavigationCancel) | Occurs when the router cancels navigation | +| [`NavigationError`](api/router/NavigationError) | Occurs when navigation fails due to an unexpected error | +| [`NavigationSkipped`](api/router/NavigationSkipped) | Occurs when the router skips navigation (e.g., same URL navigation) | + +### Other events + +There is one additional event that occurs outside the main navigation lifecycle, but it is still part of the router's event system. + +| Event | Description | +| ----------------------------- | ----------------------- | +| [`Scroll`](api/router/Scroll) | Occurs during scrolling | + +## Next steps + +Learn more about [route guards](/guide/routing/route-guards) and [common router tasks](/guide/routing/common-router-tasks). diff --git a/adev-es/src/content/guide/routing/overview.md b/adev-es/src/content/guide/routing/overview.md index 57faec7..e219535 100644 --- a/adev-es/src/content/guide/routing/overview.md +++ b/adev-es/src/content/guide/routing/overview.md @@ -4,24 +4,6 @@ Routing helps you change what the user sees in a single-page app. Angular Router (`@angular/router`) is the official library for managing navigation in Angular applications and a core part of the framework. It is included by default in all projects created by Angular CLI. -## Installation - -Angular Router is included by default in all Angular projects setup with the Angular CLI `ng new` command. - -### Prerequisite - -- Angular CLI - -### Add to an existing project - -If your project does not have routing, you can install it manually with the following command: - -```bash -ng add @angular/router -``` - -The Angular CLI will then install all the necessary dependencies. - ## Why is routing necessary in a SPA? When you navigate to a URL in your web browser, the browser normally makes a network request to a web server and displays the returned HTML page. When you navigate to a different URL, such as clicking a link, the browser makes another network request and replaces the entire page with a new one. diff --git a/adev-es/src/content/guide/routing/rendering-strategies.md b/adev-es/src/content/guide/routing/rendering-strategies.md new file mode 100644 index 0000000..621968d --- /dev/null +++ b/adev-es/src/content/guide/routing/rendering-strategies.md @@ -0,0 +1,139 @@ +# Rendering strategies in Angular + +This guide helps you choose the right rendering strategy for different parts of your Angular application. + +## What are rendering strategies? + +Rendering strategies determine when and where your Angular application's HTML content is generated. Each strategy offers different trade-offs between initial page load performance, interactivity, SEO capabilities, and server resource usage. + +Angular supports three primary rendering strategies: + +- **Client-Side Rendering (CSR)** - Content is rendered entirely in the browser +- **Static Site Generation (SSG/Prerendering)** - Content is pre-rendered at build time +- **Server-Side Rendering (SSR)** - Content is rendered on the server for the initial request for a route + +## Client-Side Rendering (CSR) + +**CSR is Angular's default.** Content renders entirely in the browser after JavaScript loads. + +### When to use CSR + +✅ It can be a good fit for: + +- Interactive applications (dashboards, admin panels) +- Real-time applications +- Internal tools where SEO doesn't matter +- Single-page applications with complex client-side state + +❌ When possible, consider avoiding it for: + +- Public-facing content that needs SEO +- Pages where initial load performance is critical + +### CSR trade-offs + +| Aspect | Impact | +| :---------------- | :------------------------------------------------------- | +| **SEO** | Poor - content not visible to crawlers until JS executes | +| **Initial load** | Slower - must download and execute JavaScript first | +| **Interactivity** | Immediate once loaded | +| **Server needs** | Minimal outside of some configuration | +| **Complexity** | Simplest because it works with minimum configuration | + +## Static Site Generation (SSG/Prerendering) + +**SSG pre-renders pages at build time** into static HTML files. The server sends pre-built HTML for the initial page load. After hydration, your app runs entirely in the browser like a traditional SPA - subsequent navigation, route changes, and API calls all happen client-side without server rendering. + +### When to use SSG + +✅ It can be a good fit for: + +- Marketing pages and landing pages +- Blog posts and documentation +- Product catalogs with stable content +- Content that doesn't change per-user + +❌ When possible, consider avoiding it for: + +- User-specific content +- Frequently changing data +- Real-time information + +### SSG trade-offs + +| Aspect | Impact | +| :------------------ | :------------------------------------------ | +| **SEO** | Excellent - full HTML available immediately | +| **Initial load** | Fastest - pre-generated HTML | +| **Interactivity** | After hydration completes | +| **Server needs** | None for serving (CDN-friendly) | +| **Build time** | Longer - generates all pages upfront | +| **Content updates** | Requires rebuild and redeploy | + +📖 **Implementation:** See [Customizing build-time prerendering](guide/ssr#customizing-build-time-prerendering-ssg) in the SSR guide. + +## Server-Side Rendering (SSR) + +**SSR generates HTML on the server for the initial request for a route**, providing dynamic content with good SEO. The server renders HTML and sends it to the client. + +Once the client renders the page, Angular [hydrates](/guide/hydration#what-is-hydration) the app and it then runs entirely in the browser like a traditional SPA - subsequent navigation, route changes, and API calls all happen client-side without additional server rendering. + +### When to use SSR + +✅ It can be a good fit for: + +- E-commerce product pages (dynamic pricing/inventory) +- News sites and social media feeds +- Personalized content that changes frequently + +❌ When possible, consider avoiding it for: + +- Static content (use SSG instead) +- When server costs are a concern + +### SSR trade-offs + +| Aspect | Impact | +| :------------------ | :-------------------------------------------------- | +| **SEO** | Excellent - full HTML for crawlers | +| **Initial load** | Fast - immediate content visibility | +| **Interactivity** | Delayed until hydration | +| **Server needs** | Requires server | +| **Personalization** | Full access to user context | +| **Server costs** | Higher - renders on the initial request for a route | + +📖 **Implementation:** See [Server routing](guide/ssr#server-routing) and [Authoring server-compatible components](guide/ssr#authoring-server-compatible-components) in the SSR guide. + +## Choosing the Right Strategy + +### Decision matrix + +| If you need... | Use this strategy | Why | +| :------------------------- | :---------------- | :----------------------------------------------- | +| **SEO + Static content** | SSG | Pre-rendered HTML, fastest load | +| **SEO + Dynamic content** | SSR | Fresh content on the initial request for a route | +| **No SEO + Interactivity** | CSR | Simplest, no server needed | +| **Mixed requirements** | Hybrid | Different strategies per route | + +## Making SSR/SSG Interactive with Hydration + +When using SSR or SSG, Angular "hydrates" the server-rendered HTML to make it interactive. + +**Available strategies:** + +- **Full hydration** - Entire app becomes interactive at once (default) +- **Incremental hydration** - Parts become interactive as needed (better performance) +- **Event replay** - Captures clicks before hydration completes + +📖 **Learn more:** + +- [Hydration guide](guide/hydration) - Complete hydration setup +- [Incremental hydration](guide/incremental-hydration) - Advanced hydration with `@defer` blocks + +## Next steps + + + + + + diff --git a/adev-es/src/content/guide/routing/route-transition-animations.md b/adev-es/src/content/guide/routing/route-transition-animations.md new file mode 100644 index 0000000..a9aa290 --- /dev/null +++ b/adev-es/src/content/guide/routing/route-transition-animations.md @@ -0,0 +1,203 @@ +# Route transition animations + +Route transition animations enhance user experience by providing smooth visual transitions when navigating between different views in your Angular application. [Angular Router](/guide/routing/overview) includes built-in support for the browser's View Transitions API, enabling seamless animations between route changes in supported browsers. + +HELPFUL: The Router's native View Transitions integration is currently in [developer preview](/reference/releases#developer-preview). Native View Transitions are a relatively new browser feature with limited support across all browsers. + +## How View Transitions work + +View transitions use the browser's native [`document.startViewTransition` API](https://developer.mozilla.org/en-US/docs/Web/API/Document/startViewTransition) to create smooth animations between different states of your application. The API works by: + +1. **Capturing the current state** - The browser takes a screenshot of the current page +2. **Executing the DOM update** - Your callback function runs to update the DOM +3. **Capturing the new state** - The browser captures the updated page state +4. **Playing the transition** - The browser animates between the old and new states + +Here's the basic structure of the `startViewTransition` API: + +```ts +document.startViewTransition(async () => { + await updateTheDOMSomehow(); +}); +``` + +For more details about the browser API, see the [Chrome Explainer](https://developer.chrome.com/docs/web-platform/view-transitions). + +## How the Router uses view transitions + +Angular Router integrates view transitions into the navigation lifecycle to create seamless route changes. During navigation, the Router: + +1. **Completes navigation preparation** - Route matching, [lazy loading](/guide/routing/define-routes#lazily-loaded-components), [guards](/guide/routing/route-guards), and [resolvers](/guide/routing/data-resolvers) execute +2. **Initiates the view transition** - Router calls `startViewTransition` when routes are ready for activation +3. **Updates the DOM** - Router activates new routes and deactivates old ones within the transition callback +4. **Finalizes the transition** - The transition Promise resolves when Angular completes rendering + +The Router's view transition integration acts as a [progressive enhancement](https://developer.mozilla.org/en-US/docs/Glossary/Progressive_Enhancement). When browsers don't support the View Transitions API, the Router performs normal DOM updates without animation, ensuring your application works across all browsers. + +## Enabling View Transitions in the Router + +Enable view transitions by adding the `withViewTransitions` feature to your [router configuration](/guide/routing/define-routes#adding-the-router-to-your-application). Angular supports both standalone and NgModule bootstrap approaches: + +### Standalone bootstrap + +```ts +import { bootstrapApplication } from '@angular/platform-browser'; +import { provideRouter, withViewTransitions } from '@angular/router'; +import { routes } from './app.routes'; + +bootstrapApplication(MyApp, { + providers: [ + provideRouter(routes, withViewTransitions()), + ] +}); +``` + +### NgModule bootstrap + +```ts +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +@NgModule({ + imports: [RouterModule.forRoot(routes, {enableViewTransitions: true})] +}) +export class AppRouting {} +``` + +[Try the "count" example on StackBlitz](https://stackblitz.com/edit/stackblitz-starters-2dnvtm?file=src%2Fmain.ts) + +This example demonstrates how router navigation can replace direct `startViewTransition` calls for counter updates. + +## Customizing transitions with CSS + +You can customize view transitions using CSS to create unique animation effects. The browser creates separate transition elements that you can target with CSS selectors. + +To create custom transitions: + +1. **Add view-transition-name** - Assign unique names to elements you want to animate +2. **Define global animations** - Create CSS animations in your global styles +3. **Target transition pseudo-elements** - Use `::view-transition-old()` and `::view-transition-new()` selectors + +Here's an example that adds a rotation effect to a counter element: + +```css +/* Define keyframe animations */ +@keyframes rotate-out { + to { + transform: rotate(90deg); + } +} + +@keyframes rotate-in { + from { + transform: rotate(-90deg); + } +} + +/* Target view transition pseudo-elements */ +::view-transition-old(count), +::view-transition-new(count) { + animation-duration: 200ms; + animation-name: -ua-view-transition-fade-in, rotate-in; +} + +::view-transition-old(count) { + animation-name: -ua-view-transition-fade-out, rotate-out; +} +``` + +IMPORTANT: Define view transition animations in your global styles file, not in component styles. Angular's [view encapsulation](/guide/components/styling#view-encapsulation) scopes component styles, which prevents them from targeting the transition pseudo-elements correctly. + +[Try the updated “count” example on StackBlitz](https://stackblitz.com/edit/stackblitz-starters-fwn4i7?file=src%2Fmain.ts) + +## Advanced transition control with onViewTransitionCreated + +The `withViewTransitions` feature accepts an options object with an `onViewTransitionCreated` callback for advanced control over view transitions. This callback: + +- Runs in an [injection context](/guide/di/dependency-injection-context#run-within-an-injection-context) +- Receives a [`ViewTransitionInfo`](/api/router/ViewTransitionInfo) object containing: + - The `ViewTransition` instance from `startViewTransition` + - The [`ActivatedRouteSnapshot`](/api/router/ActivatedRouteSnapshot) for the route being navigated from + - The [`ActivatedRouteSnapshot`](/api/router/ActivatedRouteSnapshot) for the route being navigated to + +Use this callback to customize transition behavior based on navigation context. For example, you can skip transitions for specific navigation types: + +```ts +import { inject } from '@angular/core'; +import { Router, withViewTransitions } from '@angular/router'; + +withViewTransitions({ + onViewTransitionCreated: ({transition}) => { + const router = inject(Router); + const targetUrl = router.getCurrentNavigation()!.finalUrl!; + + // Skip transition if only fragment or query params change + const config = { + paths: 'exact', + matrixParams: 'exact', + fragment: 'ignored', + queryParams: 'ignored', + }; + + if (router.isActive(targetUrl, config)) { + transition.skipTransition(); + } + }, +}) +``` + +This example skips the view transition when navigation only changes the [URL fragment or query parameters](/guide/routing/read-route-state#query-parameters) (such as anchor links within the same page). The `skipTransition()` method prevents the animation while still allowing the navigation to complete. + +## Examples from the Chrome explainer adapted to Angular + +The following examples demonstrate various view transition techniques adapted from the Chrome team's documentation for use with Angular Router: + +### Transitioning elements don't need to be the same DOM element + +Elements can transition smoothly between different DOM elements as long as they share the same `view-transition-name`. + +- [Chrome Explainer](https://developer.chrome.com/docs/web-platform/view-transitions/same-document#transitioning_elements_dont_need_to_be_the_same_dom_element) +- [Angular Example on StackBlitz](https://stackblitz.com/edit/stackblitz-starters-dh8npr?file=src%2Fmain.ts) + +### Custom entry and exit animations + +Create unique animations for elements entering and leaving the viewport during route transitions. + +- [Chrome Explainer](https://developer.chrome.com/docs/web-platform/view-transitions/same-document#custom_entry_and_exit_transitions) +- [Angular Example on StackBlitz](https://stackblitz.com/edit/stackblitz-starters-8kly3o) + +### Async DOM updates and waiting for content + +Angular Router prioritizes immediate transitions over waiting for additional content to load. + +- [Chrome Explainer](https://developer.chrome.com/docs/web-platform/view-transitions/same-document#async_dom_updates_and_waiting_for_content) + +NOTE: Angular Router does not provide a way to delay view transitions. This design choice prevents pages from becoming non-interactive while waiting for additional content. As the Chrome documentation notes: "During this time, the page is frozen, so delays here should be kept to a minimum…in some cases it's better to avoid the delay altogether, and use the content you already have." + +### Handle multiple view transition styles with view transition types + +Use view transition types to apply different animation styles based on navigation context. + +- [Chrome Explainer](https://developer.chrome.com/docs/web-platform/view-transitions/same-document#view-transition-types) +- [Angular Example on StackBlitz](https://stackblitz.com/edit/stackblitz-starters-vxzcam) + +### Handle multiple view transition styles with a class name on the view transition root (deprecated) + +This approach uses CSS classes on the transition root element to control animation styles. + +- [Chrome Explainer](https://developer.chrome.com/docs/web-platform/view-transitions/same-document#changing-on-navigation-type) +- [Angular Example on StackBlitz](https://stackblitz.com/edit/stackblitz-starters-nmnzzg?file=src%2Fmain.ts) + +### Transitioning without freezing other animations + +Maintain other page animations during view transitions to create more dynamic user experiences. + +- [Chrome Explainer](https://developer.chrome.com/docs/web-platform/view-transitions/same-document#transitioning-without-freezing) +- [Angular Example on StackBlitz](https://stackblitz.com/edit/stackblitz-starters-76kgww) + +### Animating with JavaScript + +Control view transitions programmatically using JavaScript APIs for complex animation scenarios. + +- [Chrome Explainer](https://developer.chrome.com/docs/web-platform/view-transitions/same-document#animating-with-javascript) +- [Angular Example on StackBlitz](https://stackblitz.com/edit/stackblitz-starters-cklnkm) diff --git a/adev-es/src/content/guide/routing/testing.md b/adev-es/src/content/guide/routing/testing.md new file mode 100644 index 0000000..5a1f2fe --- /dev/null +++ b/adev-es/src/content/guide/routing/testing.md @@ -0,0 +1,341 @@ +# Testing routing and navigation + +Testing routing and navigation is essential to ensure your application behaves correctly when users navigate between different routes. This guide covers various strategies for testing routing functionality in Angular applications. + +## Prerequisites + +This guide assumes you are familiar with the following tools and libraries: + +- **[Jasmine](https://jasmine.github.io/)** - JavaScript testing framework that provides the testing syntax (`describe`, `it`, `expect`) +- **[Karma](https://karma-runner.github.io/)** - Test runner that executes tests in browsers +- **[Angular Testing Utilities](guide/testing)** - Angular's built-in testing tools ([`TestBed`](api/core/testing/TestBed), [`ComponentFixture`](api/core/testing/ComponentFixture)) +- **[`RouterTestingHarness`](api/router/testing/RouterTestingHarness)** - Test harness for testing routed components with built-in navigation and component testing capabilities + +## Testing scenarios + +### Route parameters + +Components often rely on route parameters from the URL to fetch data, like a user ID for a profile page. + +The following example shows how to test a `UserProfile` component that displays a user ID from the route. + +```ts +// user-profile.component.spec.ts +import { TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; +import { RouterTestingHarness } from '@angular/router/testing'; +import { provideRouter } from '@angular/router'; +import { UserProfile } from './user-profile'; + +describe('UserProfile', () => { + it('should display user ID from route parameters', async () => { + TestBed.configureTestingModule({ + imports: [UserProfile], + providers: [ + provideRouter([ + { path: 'user/:id', component: UserProfile } + ]) + ] + }); + + const harness = await RouterTestingHarness.create(); + await harness.navigateByUrl('/user/123', UserProfile); + + expect(harness.routeNativeElement?.textContent).toContain('User Profile: 123'); + }); +}); +``` + +```ts +// user-profile.component.ts +import { Component, inject } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; + +@Component({ + template: '

    User Profile: {{userId}}

    ' +}) +export class UserProfile { + private route = inject(ActivatedRoute); + userId: string | null = this.route.snapshot.paramMap.get('id'); +} +``` + +### Route guards + +Route guards control access to routes based on conditions like authentication or permissions. When testing guards, focus on mocking dependencies and verifying navigation outcomes. + +The following example tests an `authGuard` that allows navigation for authenticated users and redirects unauthenticated users to a login page. + +```ts +// auth.guard.spec.ts +import { RouterTestingHarness } from '@angular/router/testing'; +import { provideRouter, Router } from '@angular/router'; +import { authGuard } from './auth.guard'; +import { AuthStore } from './auth-store'; +import { Component } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; + +@Component({ template: '

    Protected Page

    ' }) +class ProtectedComponent {} + +@Component({ template: '

    Login Page

    ' }) +class LoginComponent {} + +describe('authGuard', () => { + let authStore: jasmine.SpyObj; + let harness: RouterTestingHarness; + + async function setup(isAuthenticated: boolean) { + authStore = jasmine.createSpyObj('AuthStore', ['isAuthenticated']); + authStore.isAuthenticated.and.returnValue(isAuthenticated); + + TestBed.configureTestingModule({ + providers: [ + { provide: AuthStore, useValue: authStore }, + provideRouter([ + { path: 'protected', component: ProtectedComponent, canActivate: [authGuard] }, + { path: 'login', component: LoginComponent }, + ]), + ], + }); + + harness = await RouterTestingHarness.create(); + } + + it('allows navigation when user is authenticated', async () => { + await setup(true); + await harness.navigateByUrl('/protected', ProtectedComponent); + // The protected component should render when authenticated + expect(harness.routeNativeElement?.textContent).toContain('Protected Page'); + }); + + it('redirects to login when user is not authenticated', async () => { + await setup(false); + await harness.navigateByUrl('/protected', LoginComponent); + // The login component should render after redirect + expect(harness.routeNativeElement?.textContent).toContain('Login Page'); + }); +}); +``` + +```ts +// auth.guard.ts +import { inject } from '@angular/core'; +import { CanActivateFn, Router } from '@angular/router'; +import { AuthStore } from './auth-store'; + +export const authGuard: CanActivateFn = () => { + const authStore = inject(AuthStore); + const router = inject(Router); + return authStore.isAuthenticated() ? true : router.parseUrl('/login'); +}; +``` + +### Router outlets + +Router outlet tests are more of an integration test since you're essentially testing the integration between the [`Router`](api/router/Router), the outlet, and the components being displayed. + +Here's an example of how to set up a test that verifies different components are displayed for different routes: + +```ts +// app.component.spec.ts +import { TestBed } from '@angular/core/testing'; +import { RouterTestingHarness } from '@angular/router/testing'; +import { provideRouter } from '@angular/router'; +import { Component } from '@angular/core'; +import { App } from './app'; + +@Component({ + template: '

    Home Page

    ' +}) +class MockHome {} + +@Component({ + template: '

    About Page

    ' +}) +class MockAbout {} + +describe('App Router Outlet', () => { + let harness: RouterTestingHarness; + + beforeEach(async () => { + TestBed.configureTestingModule({ + imports: [App], + providers: [ + provideRouter([ + { path: '', component: MockHome }, + { path: 'about', component: MockAbout } + ]) + ] + }); + + harness = await RouterTestingHarness.create(); + }); + + it('should display home component for default route', async () => { + await harness.navigateByUrl(''); + + expect(harness.routeNativeElement?.textContent).toContain('Home Page'); + }); + + it('should display about component for about route', async () => { + await harness.navigateByUrl('/about'); + + expect(harness.routeNativeElement?.textContent).toContain('About Page'); + }); +}); +``` + +```ts +// app.component.ts +import { Component } from '@angular/core'; +import { RouterOutlet, RouterLink } from '@angular/router'; + +@Component({ + imports: [RouterOutlet, RouterLink], + template: ` + + + ` +}) +export class App {} +``` + +### Nested routes + +Testing nested routes ensures that both the parent and child components render correctly when navigating to nested URLs. This is important because nested routes involve multiple layers. + +You need to verify that: + +1. The parent component renders properly. +2. The child component renders within it. +3. Ensure that both components can access their respective route data. + +Here's an example of testing a parent-child route structure: + +```ts +// nested-routes.spec.ts +import { TestBed } from '@angular/core/testing'; +import { RouterTestingHarness } from '@angular/router/testing'; +import { provideRouter } from '@angular/router'; +import { Parent, Child } from './nested-components'; + +describe('Nested Routes', () => { + let harness: RouterTestingHarness; + + beforeEach(async () => { + TestBed.configureTestingModule({ + imports: [Parent, Child], + providers: [ + provideRouter([ + { + path: 'parent', + component: Parent, + children: [ + { path: 'child', component: Child } + ] + } + ]) + ] + }); + + harness = await RouterTestingHarness.create(); + }); + + it('should render parent and child components for nested route', async () => { + await harness.navigateByUrl('/parent/child'); + + expect(harness.routeNativeElement?.textContent).toContain('Parent Component'); + expect(harness.routeNativeElement?.textContent).toContain('Child Component'); + }); +}); +``` + +```ts +// nested-components.ts +import { Component } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; + +@Component({ + imports: [RouterOutlet], + template: ` +

    Parent Component

    + + ` +}) +export class Parent {} + +@Component({ + template: '

    Child Component

    ' +}) +export class Child {} +``` + +### Query parameters and fragments + +Query parameters (like `?search=angular&category=web`) and URL fragments (like `#section1`) provide additional data through the URL that doesn't affect which component loads, but does affect how the component behaves. Components that read query parameters through [`ActivatedRoute.queryParams`](api/router/ActivatedRoute#queryParams) need to be tested to ensure they handle different parameter scenarios correctly. + +Unlike route parameters that are part of the route definition, query parameters are optional and can change without triggering route navigation. This means you need to test both the initial loading and the reactive updates when query parameters change. + +Here's an example of how to test query parameters and fragments: + +```ts +// search.component.spec.ts +import { TestBed } from '@angular/core/testing'; +import { Router, provideRouter } from '@angular/router'; +import { RouterTestingHarness } from '@angular/router/testing'; +import { Search } from './search'; + +describe('Search', () => { + let component: Search; + let harness: RouterTestingHarness; + + beforeEach(async () => { + TestBed.configureTestingModule({ + imports: [Search], + providers: [ + provideRouter([ + { path: 'search', component: Search } + ]) + ] + }); + + harness = await RouterTestingHarness.create(); + }); + + it('should read search term from query parameters', async () => { + component = await harness.navigateByUrl('/search?q=angular', Search); + + expect(component.searchTerm()).toBe('angular'); + }); +}); +``` + +```ts +// search.component.ts +import { Component, inject, computed } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { toSignal } from '@angular/core/rxjs-interop'; + +@Component({ + template: '
    Search term: {{searchTerm()}}
    ' +}) +export class Search { + private route = inject(ActivatedRoute); + private queryParams = toSignal(this.route.queryParams, { initialValue: {} }); + + searchTerm = computed(() => this.queryParams()['q'] || null); +} +``` + +## Best practices for router testing + +1. **Use RouterTestingHarness** - For testing routed components, use [`RouterTestingHarness`](api/router/testing/RouterTestingHarness) which provides a cleaner API and eliminates the need for test host components. It offers direct component access, built-in navigation, and better type safety. However, it isn't as suitable for some scenarios, such as testing named outlets, where you may need to create custom host components. +2. **Handle external dependencies thoughtfully** - Prefer real implementations when possible for more realistic tests. If real implementations aren't feasible (e.g., external APIs), use fakes that approximate the real behavior. Use mocks or stubs only as a last resort, as they can make tests brittle and less reliable. +3. **Test navigation state** - Verify both the navigation action and the resulting application state, including URL changes and component rendering. +4. **Handle asynchronous operations** - Router navigation is asynchronous. Use `async/await` or [`fakeAsync`](api/core/testing/fakeAsync) to properly handle timing in your tests. +5. **Test error scenarios** - Include tests for invalid routes, failed navigation, and guard rejections to ensure your application handles edge cases gracefully. +6. **Do not mock Angular Router** - Instead, provide real route configurations and use the harness to navigate. This makes your tests more robust and less likely to break on internal Angular updates, while also ensuring you catch real issues when the router updates since mocks can hide breaking changes. diff --git a/adev-es/src/content/guide/signals/linked-signal.en.md b/adev-es/src/content/guide/signals/linked-signal.en.md index 1b758f6..ac13187 100644 --- a/adev-es/src/content/guide/signals/linked-signal.en.md +++ b/adev-es/src/content/guide/signals/linked-signal.en.md @@ -106,7 +106,7 @@ When you create a `linkedSignal`, you can pass an object with separate `source` The `source` can be any signal, such as a `computed` or component `input`. When the value of `source` changes, `linkedSignal` updates its value to the result of the provided `computation`. -The `computation` is a function that receives the new value of `source` and a `previous` object. The `previous` object has two properties— `previous.source` is the previous value of `source`, and `previous.value` is the previous result of the `computation`. You can use these previous values to decide the new result of the computation. +The `computation` is a function that receives the new value of `source` and a `previous` object. The `previous` object has two properties— `previous.source` is the previous value of `source`, and `previous.value` is the previous value of the `linkedSignal`. You can use these previous values to decide the new result of the computation. HELPFUL: When using the `previous` parameter, it is necessary to provide the generic type arguments of `linkedSignal` explicitly. The first generic type corresponds with the type of `source` and the second generic type determines the output type of `computation`. diff --git a/adev-es/src/content/guide/signals/linked-signal.md b/adev-es/src/content/guide/signals/linked-signal.md index 518b853..d097573 100644 --- a/adev-es/src/content/guide/signals/linked-signal.md +++ b/adev-es/src/content/guide/signals/linked-signal.md @@ -106,7 +106,7 @@ Cuando creas un `linkedSignal`, puedes pasar un objeto con propiedades separadas La `source` puede ser cualquier signal, como un `computed` o `input` de componente. Cuando el valor de `source` cambia, `linkedSignal` actualiza su valor al resultado del `cómputo` proporcionado. -El `computation` es una función que recibe el nuevo valor de `source` y un objeto `previous`. El objeto `previous` tiene dos propiedades — `previous.source` es el valor anterior de `source`, y `previous.value` es el resultado anterior del `cómputo`. Puedes usar estos valores anteriores para decidir el nuevo resultado del cómputo. +El `computation` es una función que recibe el nuevo valor de `source` y un objeto `previous`. El objeto `previous` tiene dos propiedades, `previous.source` es el valor anterior de `source`, y `previous.value` es el valor de anterior de `linkedSignal`. Puedes usar estos valores anteriores para decidir el nuevo resultado del cómputo. ÚTIL: Cuando uses el parámetro `previous`, es necesario proporcionar explícitamente los argumentos de tipo genérico de `linkedSignal`. El primer tipo genérico corresponde con el tipo de `source` y el segundo tipo genérico determina el tipo de salida del `cómputo`. diff --git a/adev-es/src/content/guide/signals/resource.en.md b/adev-es/src/content/guide/signals/resource.en.md index 53e8db7..44c5efb 100644 --- a/adev-es/src/content/guide/signals/resource.en.md +++ b/adev-es/src/content/guide/signals/resource.en.md @@ -115,7 +115,7 @@ The `status` signal provides a specific `ResourceStatus` that describes the stat | ------------- | :---------------- | ---------------------------------------------------------------------------- | | `'idle'` | `undefined` | The resource has no valid request and the loader has not run. | | `'error'` | `undefined` | The loader has encountered an error. | -| `'loading'` | `undefined` | The loader is running as a result of the `request` value changing. | +| `'loading'` | `undefined` | The loader is running as a result of the `params` value changing. | | `'reloading'` | Previous value | The loader is running as a result calling of the resource's `reload` method. | | `'resolved'` | Resolved value | The loader has completed. | | `'local'` | Locally set value | The resource's value has been set locally via `.set()` or `.update()` | diff --git a/adev-es/src/content/guide/signals/resource.md b/adev-es/src/content/guide/signals/resource.md index 187442b..7d466a6 100644 --- a/adev-es/src/content/guide/signals/resource.md +++ b/adev-es/src/content/guide/signals/resource.md @@ -115,7 +115,7 @@ El `status` de una signal proporciona un `ResourceStatus` específico que descri | ------------- | :---------------- | ---------------------------------------------------------------------------- | | `'idle'` | `undefined` | El resource no tiene una peticion válida y el loader no se ha ejecutado. | | `'error'` | `undefined` | El loader ha encontrado un error. | -| `'loading'` | `undefined` | El loader se está ejecutando como resultado del valor de `request` cambiando. | +| `'loading'` | `undefined` | El loader se está ejecutando como resultado de que el valor de `params` haya cambiando. | | `'reloading'` | Valor anterior | El loader se está ejecutando como resultado de llamar al método `reload` del resource. | | `'resolved'` | Valor resuelto | El loader ha completado. | | `'local'` | Valor establecido localmente | El valor del resource ha sido establecido localmente vía `.set()` o `.update()` | diff --git a/adev-es/src/content/guide/ssr.md b/adev-es/src/content/guide/ssr.md index 1d03b5b..aad8870 100644 --- a/adev-es/src/content/guide/ssr.md +++ b/adev-es/src/content/guide/ssr.md @@ -4,7 +4,7 @@ Angular ships all applications as client-side rendered (CSR) by default. While t ## What is hybrid rendering? -Hybrid rendering allows developers to leverage the benefits of server-side rendering (SSR), pre-rendering (also known as "static site generation" or SSG) and client-side rendering (CSR) to optimize your Angular application. It gives you fine-grained control over how your different parts of your app is rendered to give your users the best experience possible. +Hybrid rendering allows developers to leverage the benefits of server-side rendering (SSR), pre-rendering (also known as "static site generation" or SSG) and client-side rendering (CSR) to optimize your Angular application. It gives you fine-grained control over how the different parts of your app are rendered to give your users the best experience possible. ## Setting up hybrid rendering @@ -98,7 +98,7 @@ The server routing configuration lets you specify how each route in your applica Each rendering mode has different benefits and drawbacks. You can choose rendering modes based on the specific needs of your application. -##### Client-side rendering +##### Client-side rendering (CSR) Client-side rendering has the simplest development model, as you can write code that assumes it always runs in a web browser. This lets you use a wide range of client-side libraries that also assume they run in a browser. @@ -110,7 +110,7 @@ When client-side rendering, the server does not need to do any work to render a Applications that support installable, offline experiences with [service workers](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) can rely on client-side rendering without needing to communicate with a server. -##### Server-side rendering +##### Server-side rendering (SSR) Server-side rendering offers faster page loads than client-side rendering. Instead of waiting for JavaScript to download and run, the server directly renders an HTML document upon receiving a request from the browser. The user experiences only the latency necessary for the server to fetch data and render the requested page. This mode also eliminates the need for additional network requests from the browser, as your code can fetch data during rendering on the server. diff --git a/adev-es/src/content/guide/tailwind.md b/adev-es/src/content/guide/tailwind.md new file mode 100644 index 0000000..b5b8094 --- /dev/null +++ b/adev-es/src/content/guide/tailwind.md @@ -0,0 +1,69 @@ +# Using Tailwind CSS with Angular + +[Tailwind CSS](https://tailwindcss.com/) is a utility-first CSS framework that can be used to build modern websites without ever leaving your HTML. This guide will walk you through setting up Tailwind CSS in your Angular project. + +## Setting up Tailwind CSS + +### 1. Create an Angular project + +First, create a new Angular project if you don't have one set up already. + + +ng new my-project +cd my-project + + +### 2. Install Tailwind CSS + +Next, open a terminal in your Angular project's root directory and run the following command to install Tailwind CSS and its peer dependencies: + + +npm install tailwindcss @tailwindcss/postcss postcss + + +### 3. Configure PostCSS Plugins + +Next, add a `.postcssrc.json` file in the file root of the project. +Add the `@tailwindcss/postcss` plugin into your PostCSS configuration. + + +{ + "plugins": { + "@tailwindcss/postcss": {} + } +} + + +### 4. Import Tailwind CSS + +Add an `@import` to `./src/styles.css` that imports Tailwind CSS. + + +@import "tailwindcss"; + + +If you're using SCSS, add `@use` to `./src/styles.scss`. + + +@use "tailwindcss"; + + +### 5. Start using Tailwind in your project + +You can now start using Tailwind's utility classes in your component templates to style your application. + +For example, you can add the following to your `app.html` file: + + +

    + Hello world! +

    +
    + +### 6. Start using Tailwind in your project + +Run your build process with `ng serve` and you should see the styled heading. + +## Additional Resources + +- [Tailwind CSS Documentation](https://tailwindcss.com/docs) \ No newline at end of file diff --git a/adev-es/src/content/guide/templates/binding.md b/adev-es/src/content/guide/templates/binding.md index 3a39e48..cd7eb30 100644 --- a/adev-es/src/content/guide/templates/binding.md +++ b/adev-es/src/content/guide/templates/binding.md @@ -25,9 +25,26 @@ In this example, when the snippet is rendered to the page, Angular will replace

    Your color preference is dark.

    ``` -In addition to evaluating the expression at first render, Angular also updates the rendered content when the expression's value changes. +Bindings that change over time should read values from [signals](/guide/signals). Angular tracks the signals read in the template, and updates the rendered page when those signal values change. -Continuing the theme example, if a user clicks on a button that changes the value of `theme` to `'light'` after the page loads, the page updates accordingly to: +```angular-ts +@Component({ + template: ` + +

    {{ welcomeMessage }}

    + +

    Your color preference is {{ theme() }}.

    + ` + ... +}) +export class AppComponent { + welcomeMessage = "Welcome, enjoy this app that we built for you"; + theme = signal('dark'); +} +``` +For more details, see the [Signals guide](/guide/signals). + +Continuing the theme example, if a user clicks on a button that updates the `theme` signal to `'light'` after the page loads, the page updates accordingly to: ```angular-html @@ -50,7 +67,7 @@ Every HTML element has a corresponding DOM representation. For example, each ` - + ``` In this example, every time `isFormValid` changes, Angular automatically sets the `disabled` property of the `HTMLButtonElement` instance. @@ -61,7 +78,7 @@ When an element is an Angular component, you can use property bindings to set co ```angular-html - + ``` In this example, every time `mySelection` changes, Angular automatically sets the `value` property of the `MyListbox` instance. @@ -70,7 +87,7 @@ You can bind to directive properties as well. ```angular-html -The current user's profile photo +The current user's profile photo ``` ### Attributes @@ -79,7 +96,7 @@ When you need to set HTML attributes that do not have corresponding DOM properti ```angular-html -
      +
        ``` In this example, every time `listRole` changes, Angular automatically sets the `role` attribute of the `
          ` element by calling `setAttribute`. @@ -92,13 +109,13 @@ You can also use text interpolation syntax in properties and attributes by using ```angular-html -Profile photo of {{ firstName }} +Profile photo of {{ firstName() }} ``` To bind to an attribute with the text interpolation syntax, prefix the attribute name with `attr.` ```angular-html - +
          ...
          + `, ... }) export class UserProfile { listClasses = 'full-width outlined'; - sectionClasses = ['expandable', 'elevated']; - buttonClasses = { + sectionClasses = signal(['expandable', 'elevated']); + buttonClasses = signal({ highlighted: true, embiggened: false, - }; + }); } ``` @@ -155,12 +172,12 @@ When using static CSS classes, directly binding `class`, and binding specific cl ```angular-ts @Component({ - template: `
            ...`, + template: `
              ...`, ... }) export class Listbox { - listType = 'box'; - isExpanded = true; + listType = signal('box'); + isExpanded = signal(true); } ``` @@ -176,20 +193,22 @@ When binding `class` to an array or an object, Angular compares the previous val If an element has multiple bindings for the same CSS class, Angular resolves collisions by following its style precedence order. +NOTE: Class bindings do not support space-separated class names in a single key. They also don't support mutations on objects as the reference of the binding remains the same. If you need one or the other, use the [ngClass](/api/common/NgClass) directive. + ### CSS style properties You can also bind to CSS style properties directly on an element. ```angular-html -
              +
              ``` You can further specify units for CSS properties that accept units. ```angular-html -
              +
              ``` You can also set multiple style values in one binding. Angular accepts the following types of value: @@ -202,17 +221,17 @@ You can also set multiple style values in one binding. Angular accepts the follo ```angular-ts @Component({ template: ` -
                ...
              -
              ...
              +
                ...
              +
              ...
              `, ... }) export class UserProfile { - listStyles = 'display: flex; padding: 8px'; - sectionStyles = { + listStyles = signal('display: flex; padding: 8px'); + sectionStyles = signal({ border: '1px solid black', 'font-weight': 'bold', - }; + }); } ``` diff --git a/adev-es/src/content/guide/templates/defer.md b/adev-es/src/content/guide/templates/defer.md index 994ea32..badcf76 100644 --- a/adev-es/src/content/guide/templates/defer.md +++ b/adev-es/src/content/guide/templates/defer.md @@ -321,6 +321,10 @@ it('should render a defer block in different states', async () => { `@defer` blocks are compatible with both standalone and NgModule-based components, directives and pipes. However, **only standalone components, directives and pipes can be deferred**. NgModule-based dependencies are not deferred and are included in the eagerly loaded bundle. +## Compatibility between `@defer` blocks and Hot Module Reload (HMR) + +When Hot Module Replacement (HMR) is active, all `@defer` block chunks are fetched eagerly, overriding any configured triggers. To restore the standard trigger behavior, you must disable HMR by serving your application with the `--no-hmr` flag. + ## How does `@defer` work with server-side rendering (SSR) and static-site generation (SSG)? By default, when rendering an application on the server (either using SSR or SSG), defer blocks always render their `@placeholder` (or nothing if a placeholder is not specified) and triggers are not invoked. On the client, the content of the `@placeholder` is hydrated and triggers are activated. diff --git a/adev-es/src/content/guide/templates/expression-syntax.md b/adev-es/src/content/guide/templates/expression-syntax.md index d3b497a..edf9154 100644 --- a/adev-es/src/content/guide/templates/expression-syntax.md +++ b/adev-es/src/content/guide/templates/expression-syntax.md @@ -8,22 +8,27 @@ Angular supports a subset of [literal values](https://developer.mozilla.org/en-U ### Supported value literals -| Literal type | Example values | -| ---------------------- | ------------------------------- | -| String | `'Hello'`, `"World"` | -| Boolean | `true`, `false` | -| Number | `123`, `3.14` | -| Object | `{name: 'Alice'}` | -| Array | `['Onion', 'Cheese', 'Garlic']` | -| null | `null` | -| Template string | `` `Hello ${name}` `` | -| Tagged template string | `` tag`Hello ${name}` `` | +| Literal type | Example values | +| --------------- | ------------------------------- | +| String | `'Hello'`, `"World"` | +| Boolean | `true`, `false` | +| Number | `123`, `3.14` | +| Object | `{name: 'Alice'}` | +| Array | `['Onion', 'Cheese', 'Garlic']` | +| null | `null` | +| Template string | `` `Hello ${name}` `` | ### Unsupported literals -| Literal type | Example value | -| ---------------------- | ------------------------ | -| RegExp | `/\d+/` | +| Literal type | Example value | +| ------------ | ------------- | +| RegExp | `/\d+/` | + +### Unsupported value literals + +| Literal type | Example values | +|--------------|----------------| +| BigInt | `1n` | ## Globals @@ -46,33 +51,40 @@ For example, `@for` blocks make several local variables corresponding to informa Angular supports the following operators from standard JavaScript. -| Operator | Example(s) | -| --------------------- | ---------------------------------------- | -| Add / Concatenate | `1 + 2` | -| Subtract | `52 - 3` | -| Multiply | `41 * 6` | -| Divide | `20 / 4` | -| Remainder (Modulo) | `17 % 5` | -| Exponentiation | `10 ** 3` | -| Parenthesis | `9 * (8 + 4)` | -| Conditional (Ternary) | `a > b ? true : false` | -| And (Logical) | `&&` | -| Or (Logical) | `\|\|` | -| Not (Logical) | `!` | -| Nullish Coalescing | `possiblyNullValue ?? 'default'` | -| Comparison Operators | `<`, `<=`, `>`, `>=`, `==`, `===`, `!==` | -| Unary Negation | `-x` | -| Unary Plus | `+y` | -| Property Accessor | `person['name']` | -| typeof | `typeof 42` | -| void | `void 1` | -| in | `'model' in car` | +| Operator | Example(s) | +| ----------------------------- | ---------------------------------------------- | +| Add / Concatenate | `1 + 2` | +| Subtract | `52 - 3` | +| Multiply | `41 * 6` | +| Divide | `20 / 4` | +| Remainder (Modulo) | `17 % 5` | +| Exponentiation | `10 ** 3` | +| Parenthesis | `9 * (8 + 4)` | +| Conditional (Ternary) | `a > b ? true : false` | +| And (Logical) | `&&` | +| Or (Logical) | `\|\|` | +| Not (Logical) | `!` | +| Nullish Coalescing | `possiblyNullValue ?? 'default'` | +| Comparison Operators | `<`, `<=`, `>`, `>=`, `==`, `===`, `!==`, `!=` | +| Unary Negation | `-x` | +| Unary Plus | `+y` | +| Property Accessor | `person['name']` | +| Assignment | `a = b` | +| Addition Assignment | `a += b` | +| Subtraction Assignment | `a -= b` | +| Multiplication Assignment | `a *= b` | +| Division Assignment | `a /= b` | +| Remainder Assignment | `a %= b` | +| Exponentiation Assignment | `a **= b` | +| Logical AND Assignment | `a &&= b` | +| Logical OR Assignment | `a \|\|= b` | +| Nullish Coalescing Assignment | `a ??= b` | Angular expressions additionally also support the following non-standard operators: | Operator | Example(s) | | ------------------------------- | ------------------------------ | -| [Pipe](/guide/templates/pipes) | `{{ total \| currency }}` | +| [Pipe](/guide/templates/pipes) | `{{ total \| currency }}` | | Optional chaining\* | `someObj.someProp?.nestedProp` | | Non-null assertion (TypeScript) | `someObj!.someProp` | @@ -83,13 +95,9 @@ NOTE: Optional chaining behaves differently from the standard JavaScript version | Operator | Example(s) | | --------------------- | --------------------------------- | | All bitwise operators | `&`, `&=`, `~`, `\|=`, `^=`, etc. | -| Assignment operators | `=` | | Object destructuring | `const { name } = person` | | Array destructuring | `const [firstItem] = items` | | Comma operator | `x = (x++, x)` | -| in | `'model' in car` | -| typeof | `typeof 42` | -| void | `void 1` | | instanceof | `car instanceof Automobile` | | new | `new Car()` | @@ -112,7 +120,7 @@ Generally speaking, declarations are not supported in Angular expressions. This # Event listener statements -Event handlers are **statements** rather than expressions. While they support all of the same syntax as Angular expressions, the are two key differences: +Event handlers are **statements** rather than expressions. While they support all of the same syntax as Angular expressions, there are two key differences: 1. Statements **do support** assignment operators (but not destructing assignments) 1. Statements **do not support** pipes diff --git a/adev-es/src/content/guide/templates/ng-template.md b/adev-es/src/content/guide/templates/ng-template.md index 527cda4..b3118b5 100644 --- a/adev-es/src/content/guide/templates/ng-template.md +++ b/adev-es/src/content/guide/templates/ng-template.md @@ -60,7 +60,7 @@ You can then reference this fragment anywhere else in the template via the `myFr You can get a reference to a template fragment using any [component or directive query API](/guide/components/queries). -For example, if your template has exactly one template fragment, you can query directly for the `TemplateRef` object with a `@ViewChild` query: +You can query the `TemplateRef` object directly using a `viewChild` query. ```angular-ts @Component({ @@ -74,7 +74,7 @@ For example, if your template has exactly one template fragment, you can query d `, }) export class ComponentWithFragment { - @ViewChild(TemplateRef) myFragment: TemplateRef | undefined; + templateRef = viewChild>(TemplateRef); } ``` @@ -98,10 +98,8 @@ If a template contains multiple fragments, you can assign a name to each fragmen `, }) export class ComponentWithFragment { - // When querying by name, you can use the `read` option to specify that you want to get the - // TemplateRef object associated with the element. - @ViewChild('fragmentOne', {read: TemplateRef}) fragmentOne: TemplateRef | undefined; - @ViewChild('fragmentTwo', {read: TemplateRef}) fragmentTwo: TemplateRef | undefined; + fragmentOne = viewChild>('fragmentOne'); + fragmentTwo = viewChild>('fragmentTwo'); } ``` @@ -137,6 +135,7 @@ Once you have a reference to a template fragment's `TemplateRef` object, you can The `NgTemplateOutlet` directive from `@angular/common` accepts a `TemplateRef` and renders the fragment as a **sibling** to the element with the outlet. You should generally use `NgTemplateOutlet` on an [`` element](/guide/templates/ng-container). First, import `NgTemplateOutlet`: + ```typescript import { NgTemplateOutlet } from '@angular/common'; ``` diff --git a/adev-es/src/content/guide/templates/two-way-binding.md b/adev-es/src/content/guide/templates/two-way-binding.md index bfc141a..f102f3d 100644 --- a/adev-es/src/content/guide/templates/two-way-binding.md +++ b/adev-es/src/content/guide/templates/two-way-binding.md @@ -99,7 +99,7 @@ Here is a simplified example: // './counter/counter.component.ts'; import { Component, model } from '@angular/core'; -@Component({ // Omitted for brevity }) +@Component({ /* Omitted for brevity */ }) export class CounterComponent { count = model(0); diff --git a/adev-es/src/content/guide/templates/variables.md b/adev-es/src/content/guide/templates/variables.md index 9b3ce1e..9860581 100644 --- a/adev-es/src/content/guide/templates/variables.md +++ b/adev-es/src/content/guide/templates/variables.md @@ -14,7 +14,7 @@ Use `@let` to declare a variable whose value is based on the result of a templat @let name = user.name; @let greeting = 'Hello, ' + name; @let data = data$ | async; -@let pi = 3.1459; +@let pi = 3.14159; @let coordinates = {x: 50, y: 100}; @let longExpression = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit ' + 'sed do eiusmod tempor incididunt ut labore et dolore magna ' + diff --git a/adev-es/src/content/guide/testing/components-scenarios.md b/adev-es/src/content/guide/testing/components-scenarios.md index de855e8..7efc281 100644 --- a/adev-es/src/content/guide/testing/components-scenarios.md +++ b/adev-es/src/content/guide/testing/components-scenarios.md @@ -531,9 +531,8 @@ The setup for the `test-host` tests is similar to the setup for the stand-alone -This testing module configuration shows three important differences: +This testing module configuration shows two important differences: -* It *imports* both the `DashboardHeroComponent` and the `TestHostComponent` * It *creates* the `TestHostComponent` instead of the `DashboardHeroComponent` * The `TestHostComponent` sets the `DashboardHeroComponent.hero` with a binding @@ -588,23 +587,6 @@ The component has to *subscribe* to the `ActivatedRoute.paramMap` observable and Tests can explore how the `HeroDetailComponent` responds to different `id` parameter values by navigating to different routes. -### Testing with the `RouterTestingHarness` - -Here's a test demonstrating the component's behavior when the observed `id` refers to an existing hero: - - - -HELPFUL: In the following section, the `createComponent()` method and `page` object are discussed. -Rely on your intuition for now. - -When the `id` cannot be found, the component should re-route to the `HeroListComponent`. - -The test suite setup provided the same router harness [described above](#routing-component). - -This test expects the component to try to navigate to the `HeroListComponent`. - - - ## Nested component tests Component templates often have nested components, whose templates might contain more components. @@ -637,17 +619,15 @@ In the first technique, you create and declare stub versions of the components a The stub selectors match the selectors for the corresponding real components. But their templates and classes are empty. -Then declare them in the `TestBed` configuration next to the components, directives, and pipes that need to be real. +Then declare them by overriding the `imports` of your component using `TestBed.overrideComponent`. -The `AppComponent` is the test subject, so of course you declare the real version. - -The rest are stubs. +HELPFUL: The `set` key in this example replaces all the exisiting imports on your component, make sure to imports all dependencies, not only the stubs. Alternatively you can use the `remove`/`add` keys to selectively remove and add imports. ### `NO_ERRORS_SCHEMA` -In the second approach, add `NO_ERRORS_SCHEMA` to the `TestBed.schemas` metadata. +In the second approach, add `NO_ERRORS_SCHEMA` to the metadata overrides of your component. @@ -732,155 +712,6 @@ Here are a few more `HeroDetailComponent` tests to reinforce the point. -## Calling `compileComponents()` - -HELPFUL: Ignore this section if you *only* run tests with the CLI `ng test` command because the CLI compiles the application before running the tests. - -If you run tests in a **non-CLI environment**, the tests might fail with a message like this one: - - - -Error: This test module uses the component BannerComponent -which is using a "templateUrl" or "styleUrls", but they were never compiled. -Please call "TestBed.compileComponents" before your test. - - - -The root of the problem is at least one of the components involved in the test specifies an external template or CSS file as the following version of the `BannerComponent` does. - - - -The test fails when the `TestBed` tries to create the component. - - - -Recall that the application hasn't been compiled. -So when you call `createComponent()`, the `TestBed` compiles implicitly. - -That's not a problem when the source code is in memory. -But the `BannerComponent` requires external files that the compiler must read from the file system, an inherently *asynchronous* operation. - -If the `TestBed` were allowed to continue, the tests would run and fail mysteriously before the compiler could finish. - -The preemptive error message tells you to compile explicitly with `compileComponents()`. - -### `compileComponents()` is async - -You must call `compileComponents()` within an asynchronous test function. - -CRITICAL: If you neglect to make the test function async (for example, forget to use `waitForAsync()` as described), you'll see this error message - - - -Error: ViewDestroyedError: Attempt to use a destroyed view - - - -A typical approach is to divide the setup logic into two separate `beforeEach()` functions: - -| Functions | Details | -| :-------------------------- | :--------------------------- | -| Asynchronous `beforeEach()` | Compiles the components | -| Synchronous `beforeEach()` | Performs the remaining setup | - -### The async `beforeEach` - -Write the first async `beforeEach` like this. - - - -The `TestBed.configureTestingModule()` method returns the `TestBed` class so you can chain calls to other `TestBed` static methods such as `compileComponents()`. - -In this example, the `BannerComponent` is the only component to compile. -Other examples configure the testing module with multiple components and might import application modules that hold yet more components. -Any of them could require external files. - -The `TestBed.compileComponents` method asynchronously compiles all components configured in the testing module. - -IMPORTANT: Do not re-configure the `TestBed` after calling `compileComponents()`. - -Calling `compileComponents()` closes the current `TestBed` instance to further configuration. -You cannot call any more `TestBed` configuration methods, not `configureTestingModule()` nor any of the `override...` methods. -The `TestBed` throws an error if you try. - -Make `compileComponents()` the last step before calling `TestBed.createComponent()`. - -### The synchronous `beforeEach` - -The second, synchronous `beforeEach()` contains the remaining setup steps, which include creating the component and querying for elements to inspect. - - - -Count on the test runner to wait for the first asynchronous `beforeEach` to finish before calling the second. - -### Consolidated setup - -You can consolidate the two `beforeEach()` functions into a single, async `beforeEach()`. - -The `compileComponents()` method returns a promise so you can perform the synchronous setup tasks *after* compilation by moving the synchronous code after the `await` keyword, where the promise has been resolved. - - - -### `compileComponents()` is harmless - -There's no harm in calling `compileComponents()` when it's not required. - -The component test file generated by the CLI calls `compileComponents()` even though it is never required when running `ng test`. - -The tests in this guide only call `compileComponents` when necessary. - -## Setup with module imports - -Earlier component tests configured the testing module with a few `declarations` like this: - - - -The `DashboardComponent` is simple. -It needs no help. -But more complex components often depend on other components, directives, pipes, and providers and these must be added to the testing module too. - -Fortunately, the `TestBed.configureTestingModule` parameter parallels the metadata passed to the `@NgModule` decorator which means you can also specify `providers` and `imports`. - -The `HeroDetailComponent` requires a lot of help despite its small size and simple construction. -In addition to the support it receives from the default testing module `CommonModule`, it needs: - -* `NgModel` and friends in the `FormsModule` to enable two-way data binding -* The `TitleCasePipe` from the `shared` folder -* The Router services -* The Hero data access services - -One approach is to configure the testing module from the individual pieces as in this example: - - - -HELPFUL: Notice that the `beforeEach()` is asynchronous and calls `TestBed.compileComponents` because the `HeroDetailComponent` has an external template and css file. - -As explained in [Calling `compileComponents()`](#calling-compilecomponents), these tests could be run in a non-CLI environment where Angular would have to compile them in the browser. - -### Import a shared module - -Because many application components need the `FormsModule` and the `TitleCasePipe`, the developer created a `SharedModule` to combine these and other frequently requested parts. - -The test configuration can use the `SharedModule` too as seen in this alternative setup: - - - -It's a bit tighter and smaller, with fewer import statements, which are not shown in this example. - -### Import a feature module - -The `HeroDetailComponent` is part of the `HeroModule` [Feature Module](guide/ngmodules/feature-modules) that aggregates more of the interdependent pieces including the `SharedModule`. -Try a test configuration that imports the `HeroModule` like this one: - - - -Only the *test doubles* in the `providers` remain. -Even the `HeroDetailComponent` declaration is gone. - -In fact, if you try to declare it, Angular will throw an error because `HeroDetailComponent` is declared in both the `HeroModule` and the `DynamicTestModule` created by the `TestBed`. - -HELPFUL: Importing the component's feature module can be the best way to configure tests when there are many mutual dependencies within the module and the module is small, as feature modules tend to be. - ## Override component providers The `HeroDetailComponent` provides its own `HeroDetailService`. diff --git a/adev-es/src/content/guide/testing/experimental-unit-test.md b/adev-es/src/content/guide/testing/experimental-unit-test.md index 2a85c16..ccca5dc 100644 --- a/adev-es/src/content/guide/testing/experimental-unit-test.md +++ b/adev-es/src/content/guide/testing/experimental-unit-test.md @@ -5,6 +5,24 @@ The Angular CLI provides an experimental unit test system that can use [Vitest]( IMPORTANT: This experimental unit testing system requires the use of the `application` build system. The `application` build system is the default for all newly created projects. +## Installing dependencies + +Some packages are required for the new builder to work. In order to install the new packages, run the following command : + + + +npm install vitest jsdom --save-dev + + + +If no other projects in your workspace use Karma, run the following command to uninstall the corresponding packages : + + + +npm uninstall karma karma-chrome-launcher karma-coverage karma-jasmine karma-jasmine-html-reporter --save-dev + + + ## Set up testing The Angular CLI includes the test system within a new project but must be configured before it can be used. @@ -18,7 +36,7 @@ To change to the experimental unit test system, update the `test` target as foll "options": { "tsConfig": "tsconfig.spec.json", "runner": "vitest", - "buildTarget": "::development", + "buildTarget": "::development" } } @@ -62,4 +80,4 @@ Directly customizing the underlying test runner configuration is currently not s Report issues and feature requests on [GitHub](https://github.com/angular/angular-cli/issues). -Please provide a minimal reproduction where possible to aid the team in addressing issues. \ No newline at end of file +Please provide a minimal reproduction where possible to aid the team in addressing issues. diff --git a/adev-es/src/content/guide/testing/using-component-harnesses.md b/adev-es/src/content/guide/testing/using-component-harnesses.md index 5873913..03cbe63 100644 --- a/adev-es/src/content/guide/testing/using-component-harnesses.md +++ b/adev-es/src/content/guide/testing/using-component-harnesses.md @@ -71,6 +71,12 @@ const myComponentHarness = await loader.getHarness(MyComponent); const myComponentHarnesses = await loader.getHarnesses(MyComponent); +In addition to `getHarness` and `getAllHarnesses`, `HarnessLoader` has several other useful methods for querying for harnesses: + +- `getHarnessAtIndex(...)`: Gets the harness for a component that matches the given criteria at a specific index. +- `countHarnesses(...)`: Counts the number of component instances that match the given criteria. +- `hasHarness(...)`: Checks if at least one component instance matches the given criteria. + As an example, consider a reusable dialog-button component that opens a dialog on click. It contains the following components, each with a corresponding harness: - `MyDialogButton` (composes the `MyButton` and `MyDialog` with a convenient API) diff --git a/adev-es/src/content/guide/zoneless.md b/adev-es/src/content/guide/zoneless.md index 5cf090c..0d5877a 100644 --- a/adev-es/src/content/guide/zoneless.md +++ b/adev-es/src/content/guide/zoneless.md @@ -11,7 +11,7 @@ The main advantages to removing ZoneJS as a dependency are: ## Enabling Zoneless in an application -The API for enabling Zoneless is currently in developer preview. The shape of the API and underlying behavior can change in patch versions. + ```typescript // standalone bootstrap @@ -99,7 +99,7 @@ taskService.run(async () => { }); ``` -For more complicated use-cases, you can manuall add and remove a pending tasks: +For more complicated use-cases, you can manually add and remove a pending task: ```typescript const taskService = inject(PendingTasks); @@ -114,7 +114,7 @@ try { ``` In addition, the [pendingUntilEvent](/api/core/rxjs-interop/pendingUntilEvent#) helper in `rxjs-interop` ensures -the application remains unstable until the observable emits, complets, errors, or is unsubscribed. +the application remains unstable until the observable emits, completes, errors, or is unsubscribed. ```typescript readonly myObservableState = someObservable.pipe(pendingUntilEvent()); diff --git a/adev-es/src/content/introduction/essentials/next-steps.en.md b/adev-es/src/content/introduction/essentials/next-steps.en.md index 10a86f8..2c5c921 100644 --- a/adev-es/src/content/introduction/essentials/next-steps.en.md +++ b/adev-es/src/content/introduction/essentials/next-steps.en.md @@ -1,16 +1,20 @@ -Now that you have been introduced to core concepts of Angular - you're ready to put what you learned into practices with our interactive tutorials and learn more with our in-depth guides. +Now that you have been introduced to the main concepts of Angular - you're ready to put what you learned into practice with our interactive tutorials and learn more with our in-depth guides. ## Playground +Try out Angular in an interactive code editor to further explore the concepts you've learned. + ## Tutorials +Put these main concepts into practice with our in-browser tutorial or build your first app locally with the Angular CLI. + diff --git a/adev-es/src/content/introduction/essentials/next-steps.md b/adev-es/src/content/introduction/essentials/next-steps.md index 4400b74..fc8038c 100644 --- a/adev-es/src/content/introduction/essentials/next-steps.md +++ b/adev-es/src/content/introduction/essentials/next-steps.md @@ -1,7 +1,7 @@ -Ahora que has sido introducido a los conceptos principales de Angular - estás listo para poner en práctica lo que has aprendido con nuestros tutoriales interactivos y aprender más con nuestras guías detalladas. +Ahora que has sido introducido a los conceptos principales de Angular, estás listo para poner en práctica lo que has aprendido con nuestros tutoriales interactivos y aprender más con nuestras guías detalladas. ## Playground @@ -13,7 +13,7 @@ Prueba Angular en un editor de código interactivo para explorar más a fondo lo ## Tutoriales -Pon estos conceptos principales en práctica con nuestro tutorial en el navegador o construye tu primera aplicación localmente con Angular CLI. +Pon en práctica estos conceptos principales con nuestro tutorial en el navegador o construye tu primera aplicación localmente con Angular CLI. diff --git a/adev-es/src/content/introduction/essentials/signals.en.md b/adev-es/src/content/introduction/essentials/signals.en.md index 0da1016..f397649 100644 --- a/adev-es/src/content/introduction/essentials/signals.en.md +++ b/adev-es/src/content/introduction/essentials/signals.en.md @@ -77,4 +77,4 @@ Now that you have learned how to declare and manage dynamic data, it's time to l - \ No newline at end of file + diff --git a/adev-es/src/content/introduction/essentials/signals.md b/adev-es/src/content/introduction/essentials/signals.md index 4c44cc7..476875a 100644 --- a/adev-es/src/content/introduction/essentials/signals.md +++ b/adev-es/src/content/introduction/essentials/signals.md @@ -77,4 +77,4 @@ Ahora que has aprendido cómo declarar y manejar datos dinámicos, es momento de - \ No newline at end of file + diff --git a/adev-es/src/content/introduction/essentials/templates.en.md b/adev-es/src/content/introduction/essentials/templates.en.md index 238d0f3..2fba376 100644 --- a/adev-es/src/content/introduction/essentials/templates.en.md +++ b/adev-es/src/content/introduction/essentials/templates.en.md @@ -61,7 +61,7 @@ You can also bind to HTML _attributes_ by prefixing the attribute name with `att
                ``` -Angular automatically updates DOM properties and attribute when the bound value changes. +Angular automatically updates DOM properties and attributes when the bound value changes. ## Handling user interaction diff --git a/adev-es/src/content/introduction/essentials/templates.md b/adev-es/src/content/introduction/essentials/templates.md index f004776..9e76560 100644 --- a/adev-es/src/content/introduction/essentials/templates.md +++ b/adev-es/src/content/introduction/essentials/templates.md @@ -61,7 +61,7 @@ También puedes hacer binding a *atributos* HTML prefijando el nombre del atribu
                  ``` -Angular actualiza automáticamente las propiedades del DOM y atributos cuando el valor vinculado cambia. +Angular actualiza automáticamente las propiedades y atributos del DOM cuando el valor vinculado cambia. ## Manejar la interacción del usuario diff --git a/adev-es/src/content/introduction/installation.en.md b/adev-es/src/content/introduction/installation.en.md index 6d4c595..4201488 100644 --- a/adev-es/src/content/introduction/installation.en.md +++ b/adev-es/src/content/introduction/installation.en.md @@ -19,7 +19,7 @@ If you're starting a new project, you'll most likely want to create a local proj ### Prerequisites -- **Node.js** - [v20.11.1 or newer](/reference/versions) +- **Node.js** - [v20.19.0 or newer](/reference/versions) - **Text editor** - We recommend [Visual Studio Code](https://code.visualstudio.com/) - **Terminal** - Required for running Angular CLI commands - **Development Tool** - To improve your development workflow, we recommend the [Angular Language Service](/tools/language-service) @@ -91,7 +91,7 @@ cd my-first-angular-app -All of your dependencies should be installed at this point (which you can verify by checking for the existent for a `node_modules` folder in your project), so you can start your project by running the command: +All of your dependencies should be installed at this point (which you can verify by checking for the existence of a `node_modules` folder in your project), so you can start your project by running the command: diff --git a/adev-es/src/content/introduction/installation.md b/adev-es/src/content/introduction/installation.md index c67c736..88866c5 100644 --- a/adev-es/src/content/introduction/installation.md +++ b/adev-es/src/content/introduction/installation.md @@ -19,7 +19,7 @@ Si estás comenzando un nuevo proyecto, lo más probable es que quieras crear un ### Prerrequisitos -- **Node.js** - [v20.11.1 o más reciente](/reference/versions) +- **Node.js** - [v20.19.0 o más reciente](/reference/versions) - **Editor de texto** - Recomendamos [Visual Studio Code](https://code.visualstudio.com/) - **Terminal** - Requerido para ejecutar comandos de Angular CLI - **Herramienta de desarrollo** - Para mejorar tu flujo de trabajo de desarrollo, recomendamos el [Angular Language Service](/tools/language-service) diff --git a/adev-es/src/content/introduction/what-is-angular.en.md b/adev-es/src/content/introduction/what-is-angular.en.md index bfb75fc..42b0540 100644 --- a/adev-es/src/content/introduction/what-is-angular.en.md +++ b/adev-es/src/content/introduction/what-is-angular.en.md @@ -1,9 +1,9 @@ - +

                  Angular is a web framework that empowers developers to build fast, reliable applications. - +

                  Maintained by a dedicated team at Google, Angular provides a broad suite of tools, APIs, and libraries to simplify and streamline your development workflow. Angular gives you diff --git a/adev-es/src/content/introduction/what-is-angular.md b/adev-es/src/content/introduction/what-is-angular.md index fa948dc..2c38520 100644 --- a/adev-es/src/content/introduction/what-is-angular.md +++ b/adev-es/src/content/introduction/what-is-angular.md @@ -1,9 +1,9 @@ - +

                  Angular es un framework que permite a los desarrolladores crear aplicaciones rápidas y fiables. - +

                  Mantenido por un equipo dedicado en Google, Angular ofrece un amplio conjunto de herramientas, API y librerias para simplificar y optimizar su flujo de trabajo de desarrollo. Angular te da diff --git a/adev-es/src/content/kitchen-sink.md b/adev-es/src/content/kitchen-sink.md index 7f51896..fb2f194 100644 --- a/adev-es/src/content/kitchen-sink.md +++ b/adev-es/src/content/kitchen-sink.md @@ -1,4 +1,4 @@ - + This is a visual list of all custom components and styles for Angular.dev. @@ -146,14 +146,13 @@ export class ComponentOverviewComponent {} Here's a code example fully styled: + visibleLines="[3,10]"> We also have styling for the terminal, just set the language as `shell`: @@ -176,34 +175,36 @@ We also have styling for the terminal, just set the language as `shell`: | `visibleLines` | `string of number[]` | range of lines for collapse mode | | `visibleRegion` | `string` | **DEPRECATED** FOR `visibleLines` | | `preview` | `boolean` | (False) display preview | +| `hideCode` | `boolean` | (False) Whether to collapse code example by default. | ### Multifile examples You can create multifile examples by wrapping the examples inside a ``. + path="adev/src/content/examples/hello-world/src/app/app.component.ts" + diff="adev/src/content/examples/hello-world/src/app/app.component-old.ts" + linenums + visibleLines="[3, 11]"/> + path="adev/src/content/examples/hello-world/src/app/app.component.css" /> #### `` Attributes -| Attributes | Type | Details | -|:--- |:--- |:--- | -| body contents | `string` | nested tabs of `docs-code` examples | -| `path` | `string` | Path to code example for preview and external link | -| `preview` | `boolean` | (False) display preview | +| Attributes | Type | Details | +|:--- |:--- |:--- | +| body contents | `string` | nested tabs of `docs-code` examples | +| `path` | `string` | Path to code example for preview and external link | +| `preview` | `boolean` | (False) display preview | +| `hideCode` | `boolean` | (False) Whether to collapse code example by default. | ### Adding `preview` to your code example @@ -211,35 +212,6 @@ Adding the `preview` flag builds a running example of the code below the code sn NOTE: `preview` only works with standalone. -#### built-in-template-functions - - - - - - -#### user-input - - - - - - - - ## Workflow Style numbered steps using ``. Numbering is created using CSS (handy!). @@ -316,12 +288,12 @@ Steps must start on a new line, and can contain `docs-code`s and other nested el You can add images using the semantic Markdown image: -![Rhubarb the cat](./images/kitchen-sink/rhubarb.jpg "Optional title") +![Rhubarb the cat](assets/images/kitchen-sink/rhubarb.jpg "Optional title") ### Add `#small` and `#medium` to change the image size -![Rhubarb the small cat](./images/kitchen-sink/rhubarb.jpg#small) -![Rhubarb the medium cat](./images/kitchen-sink/rhubarb.jpg#medium) +![Rhubarb the small cat](assets/images/kitchen-sink/rhubarb.jpg#small) +![Rhubarb the medium cat](assets/images/kitchen-sink/rhubarb.jpg#medium) Embedded videos are created with `docs-video` and just need a `src` and `alt`: diff --git a/adev-es/src/content/reference/configs/npm-packages.md b/adev-es/src/content/reference/configs/npm-packages.md index 7046bda..8e40d95 100644 --- a/adev-es/src/content/reference/configs/npm-packages.md +++ b/adev-es/src/content/reference/configs/npm-packages.md @@ -27,7 +27,7 @@ For a complete list of Angular packages, see the [API reference](api). | Package name | Details | |:--- |:--- | -| [`@angular/animations`](api#animations) | Angular's animations library makes it easy to define and apply animation effects such as page and list transitions. For more information, see the [Animations guide](guide/animations). | +| [`@angular/animations`](api#animations) | Angular's legacy animations library makes it easy to define and apply animation effects such as page and list transitions. For more information, see the [Legacy Animations guide](guide/legacy-animations). | | [`@angular/common`](api#common) | The commonly-needed services, pipes, and directives provided by the Angular team. | | `@angular/compiler` | Angular's template compiler. It understands Angular templates and can convert them to code that makes the application run. | | `@angular/compiler-cli` | Angular's compiler which is invoked by the Angular CLI's `ng build` and `ng serve` commands. It processes Angular templates with `@angular/compiler` inside a standard TypeScript compilation. | @@ -37,7 +37,7 @@ For a complete list of Angular packages, see the [API reference](api). | [`@angular/platform-browser-dynamic`](api#platform-browser-dynamic) | Includes [providers](api/core/Provider) and methods to compile and run the application on the client using the [JIT compiler](tools/cli/aot-compiler#choosing-a-compiler). | | [`@angular/router`](api#router) | The router module navigates among your application pages when the browser URL changes. For more information, see [Routing and Navigation](guide/routing). | | [`@angular/cli`](https://github.com/angular/angular-cli) | Contains the Angular CLI binary for running `ng` commands. | -| [`@angular-devkit/build-angular`](https://github.com/angular/angular-cli) | Contains default CLI builders for bundling, testing, and serving Angular applications and libraries. | -| `rxjs` | A library for reactive programming using `Observables`. | +| [`@angular-devkit/build-angular`](https://www.npmjs.com/package/@angular-devkit/build-angular) | Contains default CLI builders for bundling, testing, and serving Angular applications and libraries. | +| [`rxjs`](https://www.npmjs.com/package/rxjs) | A library for reactive programming using `Observables`. | | [`zone.js`](https://github.com/angular/zone.js) | Angular relies on `zone.js`` to run Angular's change detection processes when native JavaScript operations raise events. | | [`typescript`](https://www.npmjs.com/package/typescript) | The TypeScript compiler, language server, and built-in type definitions. | diff --git a/adev-es/src/content/reference/configs/workspace-config.md b/adev-es/src/content/reference/configs/workspace-config.md index 1dd1eb0..4160d33 100644 --- a/adev-es/src/content/reference/configs/workspace-config.md +++ b/adev-es/src/content/reference/configs/workspace-config.md @@ -12,7 +12,7 @@ You can also override defaults set at the project level using the command line. The following properties, at the top-level of the file, configure the workspace. | Properties | Details | -|:--- |:--- | +| :--------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `version` | The configuration-file version. | | `newProjectRoot` | Path where new projects are created through tools like `ng generate application` or `ng generate library`. Path can be absolute or relative to the workspace directory. Defaults to `projects` | | `cli` | A set of options that customize the [Angular CLI](tools/cli). See [Angular CLI configuration options](#angular-cli-configuration-options) below. | @@ -24,9 +24,11 @@ The initial application that you create with `ng new app-name` is listed under " When you create a library project with `ng generate library`, the library project is also added to the `projects` section. HELPFUL: The `projects` section of the configuration file does not correspond exactly to the workspace file structure. + -* The initial application created by `ng new` is at the top level of the workspace file structure. -* Other applications and libraries are under the `projects` directory by default. + +- The initial application created by `ng new` is at the top level of the workspace file structure. +- Other applications and libraries are under the `projects` directory by default. For more information, see [Workspace and project file structure](reference/configs/file-structure). @@ -34,26 +36,26 @@ For more information, see [Workspace and project file structure](reference/confi The following properties are a set of options that customize the Angular CLI. -| Property | Details | Value type | Default value | -|:--- |:--- |:--- |:--- | -| `analytics` | Share anonymous usage data with the Angular Team. A boolean value indicates whether or not to share data, while a UUID string shares data using a pseudonymous identifier. | `boolean` \| `string` | `false` | -| `cache` | Control [persistent disk cache](cli/cache) used by [Angular CLI Builders](tools/cli/cli-builder). | [Cache options](#cache-options) | `{}` | -| `schematicCollections`| List schematics collections to use in `ng generate`. | `string[]` | `[]` | -| `packageManager` | The preferred package manager tool to use. | `npm` \| `cnpm` \| `pnpm` \| `yarn`\| `bun` | `npm` | -| `warnings` | Control Angular CLI specific console warnings. | [Warnings options](#warnings-options) | `{}` | +| Property | Details | Value type | Default value | +| :--------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------ | :------------ | +| `analytics` | Share anonymous usage data with the Angular Team. A boolean value indicates whether or not to share data, while a UUID string shares data using a pseudonymous identifier. | `boolean` \| `string` | `false` | +| `cache` | Control [persistent disk cache](cli/cache) used by [Angular CLI Builders](tools/cli/cli-builder). | [Cache options](#cache-options) | `{}` | +| `schematicCollections` | List schematics collections to use in `ng generate`. | `string[]` | `[]` | +| `packageManager` | The preferred package manager tool to use. | `npm` \| `cnpm` \| `pnpm` \| `yarn`\| `bun` | `npm` | +| `warnings` | Control Angular CLI specific console warnings. | [Warnings options](#warnings-options) | `{}` | ### Cache options -| Property | Details | Value type | Default value | -|:--- |:--- |:--- |:--- | -| `enabled` | Configure whether disk caching is enabled for builds. | `boolean` | `true` | -| `environment` | Configure in which environment disk cache is enabled.

                  * `ci` enables caching only in continuous integration (CI) environments.
                  * `local` enables caching only *outside* of CI environments.
                  * `all` enables caching everywhere. | `local` \| `ci` \| `all` | `local` | -| `path` | The directory used to stored cache results. | `string` | `.angular/cache` | +| Property | Details | Value type | Default value | +| :------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------- | :--------------- | +| `enabled` | Configure whether disk caching is enabled for builds. | `boolean` | `true` | +| `environment` | Configure in which environment disk cache is enabled.

                  _ `ci` enables caching only in continuous integration (CI) environments.
                  _ `local` enables caching only _outside_ of CI environments.
                  \* `all` enables caching everywhere. | `local` \| `ci` \| `all` | `local` | +| `path` | The directory used to stored cache results. | `string` | `.angular/cache` | ### Warnings options | Property | Details | Value type | Default value | -|:--- |:--- |:--- |:--- | +| :---------------- | :------------------------------------------------------------------------------ | :--------- | :------------ | | `versionMismatch` | Show a warning when the global Angular CLI version is newer than the local one. | `boolean` | `true` | ## Project configuration options @@ -61,7 +63,7 @@ The following properties are a set of options that customize the Angular CLI. The following top-level configuration properties are available for each project, under `projects['project-name']`. | Property | Details | Value type | Default value | -|:--- |:--- |:--- |:--- | +| :------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------- | :-------------- | | `root` | The root directory for this project's files, relative to the workspace directory. Empty for the initial application, which resides at the top level of the workspace. | `string` | None (required) | | `projectType` | One of "application" or "library" An application can run independently in a browser, while a library cannot. | `application` \| `library` | None (required) | | `sourceRoot` | The root directory for this project's source files. | `string` | `''` | @@ -84,15 +86,15 @@ You can update your workspace schema file to set a different default for a sub-c { - "projects": { - "my-app": { - "schematics": { - "@schematics/angular:component": { - "standalone": false - } - } - } - } +"projects": { +"my-app": { +"schematics": { +"@schematics/angular:component": { +"standalone": false +} +} +} +} } @@ -121,7 +123,7 @@ Each target also has an `options` section that configures default options for th See the example in [Build target](#build-target) below. | Property | Details | -|:--- |:--- | +| :------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `build` | Configures defaults for options of the `ng build` command. See the [Build target](#build-target) section for more information. | | `serve` | Overrides build defaults and supplies extra serve defaults for the `ng serve` command. Besides the options available for the `ng build` command, it adds options related to serving the application. | | `e2e` | Overrides build defaults for building end-to-end testing applications using the `ng e2e` command. | @@ -135,29 +137,29 @@ HELPFUL: All options in the configuration file must use `camelCase`, rather than Each target under `architect` has the following properties: -| Property | Details | -|:--- |:--- | -| `builder` | The CLI builder used to create this target in the form of `:`. | -| `options` | Build target default options. | -| `configurations`| Alternative configurations for executing the target. Each configuration sets the default options for that intended environment, overriding the associated value under `options`. See [Alternate build configurations](#alternate-build-configurations) below. | +| Property | Details | +| :--------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `builder` | The CLI builder used to create this target in the form of `:`. | +| `options` | Build target default options. | +| `configurations` | Alternative configurations for executing the target. Each configuration sets the default options for that intended environment, overriding the associated value under `options`. See [Alternate build configurations](#alternate-build-configurations) below. | For example, to configure a build with optimizations disabled: { - "projects": { - "my-app": { - "architect": { - "build": { - "builder": "@angular-devkit/build-angular:application", - "options": { - "optimization": false - } - } - } - } - } +"projects": { +"my-app": { +"architect": { +"build": { +"builder": "@angular-devkit/build-angular:application", +"options": { +"optimization": false +} +} +} +} +} } @@ -167,10 +169,10 @@ For example, to configure a build with optimizations disabled: Angular CLI comes with two build configurations: `production` and `development`. By default, the `ng build` command uses the `production` configuration, which applies several build optimizations, including: -* Bundling files -* Minimizing excess whitespace -* Removing comments and dead code -* Minifying code to use short, mangled names +- Bundling files +- Minimizing excess whitespace +- Removing comments and dead code +- Minifying code to use short, mangled names You can define and name extra alternate configurations (such as `staging`, for instance) appropriate to your development process. You can select an alternate configuration by passing its name to the `--configuration` command line flag. @@ -180,23 +182,23 @@ For example, to configure a build where optimization is enabled only for product { - "projects": { - "my-app": { - "architect": { - "build": { - "builder": "@angular-devkit/build-angular:application", - "options": { - "optimization": false - }, - "configurations": { - "production": { - "optimization": true - } - } - } - } - } - } +"projects": { +"my-app": { +"architect": { +"build": { +"builder": "@angular-devkit/build-angular:application", +"options": { +"optimization": false +}, +"configurations": { +"production": { +"optimization": true +} +} +} +} +} +} } @@ -209,18 +211,26 @@ In this example, if both `staging` and `french` configurations set the output pa ### Extra build and test options -The configurable options for a default or targeted build generally correspond to the options available for the [`ng build`](cli/build), [`ng serve`](cli/serve), and [`ng test`](cli/test) commands. +The configurable options for a default or targeted build generally correspond to the options available for the [`ng build`](cli/build), and [`ng test`](cli/test) commands. For details of those options and their possible values, see the [Angular CLI Reference](cli). | Options properties | Details | -|:--- |:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `assets` | An object containing paths to static assets to serve with the application. The default paths point to the project's `public` directory. See more in the [Assets configuration](#assets-configuration) section. | +| :------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `assets` | An object containing paths to static assets to serve with the application. The default paths point to the project's `public` directory. See more in the [Assets configuration](#assets-configuration) section. | | `styles` | An array of CSS files to add to the global context of the project. Angular CLI supports CSS imports and all major CSS preprocessors. See more in the [Styles and scripts configuration](#styles-and-scripts-configuration) section. | | `stylePreprocessorOptions` | An object containing option-value pairs to pass to style preprocessors. See more in the [Styles and scripts configuration](#styles-and-scripts-configuration) section. | | `scripts` | An object containing JavaScript files to add to the application. The scripts are loaded exactly as if you had added them in a `