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 97ef75c..05c42f9 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 @@ -58,7 +58,7 @@

Community

  • Report Issues @@ -93,28 +93,41 @@

    Resources

    -

    Languages

    +

    Community translations

    - Super-powered by Google ©2010-2025. Code licensed under an + Super-powered by Google ©2010-2026. Code licensed under an MIT-style License . Documentation licensed under CC BY 4.0 - . + . Built by Angular at v{{ angularVersion }}.

    diff --git a/adev-es/src/app/core/layout/footer/footer.component.html b/adev-es/src/app/core/layout/footer/footer.component.html index bf33a6e..f39a7ad 100644 --- a/adev-es/src/app/core/layout/footer/footer.component.html +++ b/adev-es/src/app/core/layout/footer/footer.component.html @@ -41,7 +41,7 @@

    Comunidad

  • Reportar problemas diff --git a/adev-es/src/app/core/layout/navigation/navigation.component.en.html b/adev-es/src/app/core/layout/navigation/navigation.component.en.html index 8786877..00ff93d 100644 --- a/adev-es/src/app/core/layout/navigation/navigation.component.en.html +++ b/adev-es/src/app/core/layout/navigation/navigation.component.en.html @@ -3,7 +3,7 @@ [attr.id]="PRIMARY_NAV_ID" class="wrapper" (docsClickOutside)="closeMobileNav()" - [docsClickOutsideIgnore]="[SECONDARY_NAV_ID]" + [docsClickOutsideIgnore]="[SECONDARY_NAV_ID, SEARCH_DIALOG_ID]" >
    @@ -170,18 +170,13 @@ -
    @@ -451,7 +467,7 @@ - -
    + @if (!isUwu) { +
    +

    Productivity
    Meets scalability

    +

    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

    +
    +

    Features that actually
    help you solve problems

    + +
    +
    +
    +
    Signals
    +
    Control Flow
    +
    Deferrable Views
    +
    Hydration
    -
    - - - -

    Fully featured

    -

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

    + +
    +
    + @defer (on idle) { + + + + } +
    + +
    + @defer (on idle) { + + + + } +
    + +
    + @defer (on idle) { + + + + } +
    + +
    + @defer (on idle) { + + + + } +
    - +
    +

    Enabling you to build
    smarter and faster

    + +
    -
    -
    -
    -

    When performance matters

    -

    +

    +
    +
    + -
    - + + + Get started +
    - +
    +
    +

    Want to learn more about Angular?

    +
    +
    -
    -
    -

    Want to learn more?

    -
    - - - - -
    -

    New to Angular?

    -

    - Try our completely in browser tutorial lessons designed to give you hands on - experience with Angular. -

    -

    - Recommended for developers new to Angular looking to dive right into the code -

    - Start coding -
    -
    - - - - -
    -

    More of a reader?

    -

    - Our essentials guides are designed to help you understand Angular fundamentals in the - time it would take to finish a cup of coffee (or tea). -

    -

    - Recommended for developers with framework experience but want quick overview of - concepts -

    - Angular concepts -
    -
    - - -
    -

    Why Angular?

    -

    Learn about Angular, its benefits, and if it's right for you.

    -

    - Recommended for developers seeking to learn more about the Angular framework -

    - About Angular -
    -
    - - - - - - - - - +
    + +
    +
    +
    +

    Want to learn more about Angular?

    +
    diff --git a/adev-es/src/app/features/home/home.component.html b/adev-es/src/app/features/home/home.component.html index 0b95118..37d864e 100644 --- a/adev-es/src/app/features/home/home.component.html +++ b/adev-es/src/app/features/home/home.component.html @@ -1,493 +1,242 @@
    -
    - @if (!isUwu) { - -
    - - - - - - - - - - - - - - - -

    El framework para crear aplicaciones web escalables con confianza

    -
    - } @else { - -
    - Angular logo +
    +
    + - } -
    - - - -
    + @if (!isUwu) { +
    +

    Productividad
    y Escalabilidad

    +

    El framework para crear aplicaciones web escalables con confianza

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

    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

    +
    +

    Características que realmente
    te ayudan a resolver problemas

    + +
    +
    +
    +
    Signals
    +
    Control Flow
    +
    Deferrable Views
    +
    Hydration
    -
    - - - -

    Completamente equipado

    -

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

    + +
    +
    + @defer (on idle) { + + + + } +
    + +
    + @defer (on idle) { + + + + } +
    + +
    + @defer (on idle) { + + + + } +
    + +
    + @defer (on idle) { + + + + } +
    - +
    +

    Facilita el desarrollo de
    aplicaciones más inteligentes y rápidas

    +
    + +
    + auto_awesome +
    +
    +

    + Con IA integrada, recursos e integraciones para potenciar tu desarrollo + con IA +

    +
    +
    + + +
    + grid_view +
    +
    +

    + Estructurado y versátil, organizado y modular gracias a los componentes + y la inyección de dependencias de Angular +

    +
    +
    -
    -
    + +
    +
    +
    +
    +

    Donde el rendimiento importa

    +

    + Respaldado por millones para desarrollar aplicaciones rápidas y fiables que se adaptan + al tamaño de tu equipo +

    +
    - +
    +
    +

    ¿Quieres aprender más sobre Angular?

    +
    +
    -
    -
    -

    ¿Quieres aprender más?

    -
    - - - - -
    -

    ¿Nuevo en Angular?

    -

    - Prueba nuestras lecciones tutoriales completamente en el navegador, diseñadas para que adquieras - experiencia práctica con Angular. -

    -

    - Recomendado para desarrolladores nuevos en Angular que quieran sumergirse directamente en el código -

    - Comienza a programar -
    -
    - - - - -
    -

    ¿Prefieres leer?

    -

    - Nuestros guías esenciales están diseñadas para ayudarte a comprender los fundamentos de Angular - en el tiempo que tomaría terminar una taza de café (o té). -

    -

    - Recomendado para desarrolladores con experiencia en frameworks que quieren un repaso - rápido de los conceptos -

    - Conceptos de Angular -
    -
    - - -
    -

    ¿Por qué Angular?

    -

    Aprende sobre Angular, sus beneficios y si es adecuado para ti.

    -

    - Recomendado para desarrolladores que quieren aprender más sobre el framework Angular -

    - Acerca de Angular -
    -
    - - - - - - - - - +
    + +
    +
    +
    +

    ¿Quieres aprender más sobre Angular?

    +
    diff --git a/adev-es/src/app/routing/sub-navigation-data.en.ts b/adev-es/src/app/routing/sub-navigation-data.en.ts index a859ab2..43f27a2 100644 --- a/adev-es/src/app/routing/sub-navigation-data.en.ts +++ b/adev-es/src/app/routing/sub-navigation-data.en.ts @@ -1,4 +1,4 @@ -/*! +/** * @license * Copyright Google LLC All Rights Reserved. * @@ -6,20 +6,14 @@ * 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 SIGNALS_TUTORIAL_NAV_DATA from '../../../src/assets/tutorials/signals/routes.json'; -import SIGNAL_FORMS_TUTORIAL_NAV_DATA from '../../../src/assets/tutorials/signal-forms/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 {getApiNavigationItems} from '../features/references/helpers/manifest.helper'; -import {DEFAULT_PAGES} from '../core/constants/pages'; +import { + DOCS_SUB_NAVIGATION_DATA, + FOOTER_NAVIGATION_DATA, + REFERENCE_SUB_NAVIGATION_DATA, + TUTORIALS_SUB_NAVIGATION_DATA, +} from './navigation-entries'; interface SubNavigationData { docs: NavigationItem[]; @@ -28,1596 +22,6 @@ interface SubNavigationData { footer: NavigationItem[]; } -const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ - { - label: 'Introduction', - children: [ - { - label: 'What is Angular?', - path: 'overview', - contentPath: 'introduction/what-is-angular', - }, - { - label: 'Installation', - path: 'installation', - contentPath: 'introduction/installation', - }, - { - label: 'Essentials', - children: [ - { - label: 'Overview', - path: 'essentials', - contentPath: 'introduction/essentials/overview', - }, - { - label: 'Composition with components', - path: 'essentials/components', - contentPath: 'introduction/essentials/components', - }, - { - label: 'Reactivity with signals', - path: 'essentials/signals', - contentPath: 'introduction/essentials/signals', - }, - { - label: 'Dynamic interfaces with templates', - path: 'essentials/templates', - contentPath: 'introduction/essentials/templates', - }, - { - label: 'Forms with signals', - path: 'essentials/signal-forms', - contentPath: 'introduction/essentials/signal-forms', - status: 'new', - }, - { - label: 'Modular design with dependency injection', - path: 'essentials/dependency-injection', - contentPath: 'introduction/essentials/dependency-injection', - }, - { - label: 'Next Steps', - path: 'essentials/next-steps', - contentPath: 'introduction/essentials/next-steps', - }, - ], - }, - { - label: 'Start coding! 🚀', - path: 'tutorials/learn-angular', - }, - ], - }, - { - label: 'In-depth Guides', - children: [ - { - label: 'Signals', - children: [ - { - label: 'Overview', - path: 'guide/signals', - contentPath: 'guide/signals/overview', - }, - { - label: 'Dependent state with linkedSignal', - path: 'guide/signals/linked-signal', - contentPath: 'guide/signals/linked-signal', - }, - { - label: 'Async reactivity with resources', - path: 'guide/signals/resource', - contentPath: 'guide/signals/resource', - }, - ], - }, - { - label: 'Components', - children: [ - { - label: 'Anatomy of components', - path: 'guide/components', - contentPath: 'guide/components/anatomy-of-components', - }, - { - label: 'Selectors', - path: 'guide/components/selectors', - contentPath: 'guide/components/selectors', - }, - { - label: 'Styling', - path: 'guide/components/styling', - contentPath: 'guide/components/styling', - }, - { - label: 'Accepting data with input properties', - path: 'guide/components/inputs', - contentPath: 'guide/components/inputs', - }, - { - label: 'Custom events with outputs', - path: 'guide/components/outputs', - contentPath: 'guide/components/outputs', - }, - { - label: 'Content projection with ng-content', - path: 'guide/components/content-projection', - contentPath: 'guide/components/content-projection', - }, - { - label: 'Host elements', - path: 'guide/components/host-elements', - contentPath: 'guide/components/host-elements', - }, - { - label: 'Lifecycle', - path: 'guide/components/lifecycle', - contentPath: 'guide/components/lifecycle', - }, - { - label: 'Referencing component children with queries', - path: 'guide/components/queries', - contentPath: 'guide/components/queries', - }, - { - label: 'Using DOM APIs', - path: 'guide/components/dom-apis', - contentPath: 'guide/components/dom-apis', - }, - { - label: 'Inheritance', - path: 'guide/components/inheritance', - contentPath: 'guide/components/inheritance', - }, - { - label: 'Programmatically rendering components', - path: 'guide/components/programmatic-rendering', - contentPath: 'guide/components/programmatic-rendering', - }, - { - label: 'Advanced configuration', - path: 'guide/components/advanced-configuration', - contentPath: 'guide/components/advanced-configuration', - }, - { - label: 'Custom Elements', - path: 'guide/elements', - contentPath: 'guide/elements', - }, - ], - }, - { - label: 'Templates', - children: [ - { - label: 'Overview', - path: 'guide/templates', - contentPath: 'guide/templates/overview', - }, - { - label: 'Binding dynamic text, properties and attributes', - path: 'guide/templates/binding', - contentPath: 'guide/templates/binding', - }, - { - label: 'Adding event listeners', - path: 'guide/templates/event-listeners', - contentPath: 'guide/templates/event-listeners', - }, - { - label: 'Two-way binding', - path: 'guide/templates/two-way-binding', - contentPath: 'guide/templates/two-way-binding', - }, - { - label: 'Control flow', - path: 'guide/templates/control-flow', - contentPath: 'guide/templates/control-flow', - }, - { - label: 'Pipes', - path: 'guide/templates/pipes', - contentPath: 'guide/templates/pipes', - }, - { - label: 'Slotting child content with ng-content', - path: 'guide/templates/ng-content', - contentPath: 'guide/templates/ng-content', - }, - { - label: 'Create template fragments with ng-template', - path: 'guide/templates/ng-template', - contentPath: 'guide/templates/ng-template', - }, - { - label: 'Grouping elements with ng-container', - path: 'guide/templates/ng-container', - contentPath: 'guide/templates/ng-container', - }, - { - label: 'Variables in templates', - path: 'guide/templates/variables', - contentPath: 'guide/templates/variables', - }, - { - label: 'Deferred loading with @defer', - path: 'guide/templates/defer', - contentPath: 'guide/templates/defer', - }, - { - 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', - }, - ], - }, - { - label: 'Directives', - children: [ - { - label: 'Overview', - path: 'guide/directives', - contentPath: 'guide/directives/overview', - }, - { - label: 'Attribute directives', - path: 'guide/directives/attribute-directives', - contentPath: 'guide/directives/attribute-directives', - }, - { - label: 'Structural directives', - path: 'guide/directives/structural-directives', - contentPath: 'guide/directives/structural-directives', - }, - { - label: 'Directive composition API', - 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', - }, - ], - }, - { - label: 'Dependency Injection', - status: 'updated', - children: [ - { - label: 'Overview', - path: 'guide/di', - contentPath: 'guide/di/overview', - status: 'updated', - }, - { - label: 'Creating and using services', - path: 'guide/di/creating-and-using-services', - contentPath: 'guide/di/creating-and-using-services', - status: 'updated', - }, - { - label: 'Defining dependency providers', - path: 'guide/di/defining-dependency-providers', - contentPath: 'guide/di/defining-dependency-providers', - status: 'updated', - }, - { - label: 'Injection context', - path: 'guide/di/dependency-injection-context', - contentPath: 'guide/di/dependency-injection-context', - }, - { - label: 'Hierarchical injectors', - path: 'guide/di/hierarchical-dependency-injection', - contentPath: 'guide/di/hierarchical-dependency-injection', - }, - { - label: 'Optimizing injection tokens', - path: 'guide/di/lightweight-injection-tokens', - contentPath: 'guide/di/lightweight-injection-tokens', - }, - { - label: 'DI in action', - path: 'guide/di/di-in-action', - contentPath: 'guide/di/di-in-action', - }, - ], - }, - { - label: 'Routing', - status: 'updated', - children: [ - { - label: 'Overview', - path: 'guide/routing', - contentPath: 'guide/routing/overview', - }, - { - label: 'Define routes', - path: 'guide/routing/define-routes', - contentPath: 'guide/routing/define-routes', - }, - { - label: 'Show routes with Outlets', - path: 'guide/routing/show-routes-with-outlets', - contentPath: 'guide/routing/show-routes-with-outlets', - }, - { - label: 'Navigate to routes', - path: 'guide/routing/navigate-to-routes', - contentPath: 'guide/routing/navigate-to-routes', - }, - { - label: 'Read route state', - path: 'guide/routing/read-route-state', - contentPath: 'guide/routing/read-route-state', - }, - { - 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', - }, - ], - }, - { - label: 'Forms', - status: 'updated', - children: [ - { - label: 'Overview', - path: 'guide/forms', - contentPath: 'guide/forms/overview', - }, - { - label: 'Signal forms', - status: 'new', - children: [ - { - label: 'Overview', - path: 'guide/forms/signals/overview', - contentPath: 'guide/forms/signals/overview', - }, - { - label: 'Form models', - path: 'guide/forms/signals/models', - contentPath: 'guide/forms/signals/models', - }, - { - label: 'Field state management', - path: 'guide/forms/signals/field-state-management', - contentPath: 'guide/forms/signals/field-state-management', - }, - { - label: 'Validation', - path: 'guide/forms/signals/validation', - contentPath: 'guide/forms/signals/validation', - }, - { - label: 'Custom controls', - path: 'guide/forms/signals/custom-controls', - contentPath: 'guide/forms/signals/custom-controls', - }, - { - label: 'Comparison with other form systems', - path: 'guide/forms/signals/comparison', - contentPath: 'guide/forms/signals/comparison', - }, - ], - }, - { - label: 'Reactive forms', - path: 'guide/forms/reactive-forms', - contentPath: 'guide/forms/reactive-forms', - }, - { - label: 'Strictly typed reactive forms', - path: 'guide/forms/typed-forms', - contentPath: 'guide/forms/typed-forms', - }, - { - label: 'Template-driven forms', - path: 'guide/forms/template-driven-forms', - contentPath: 'guide/forms/template-driven-forms', - }, - { - label: 'Validate form input', - path: 'guide/forms/form-validation', - contentPath: 'guide/forms/form-validation', - }, - { - label: 'Building dynamic forms', - path: 'guide/forms/dynamic-forms', - contentPath: 'guide/forms/dynamic-forms', - }, - ], - }, - { - label: 'HTTP Client', - children: [ - { - label: 'Overview', - path: 'guide/http', - contentPath: 'guide/http/overview', - }, - { - label: 'Setting up HttpClient', - path: 'guide/http/setup', - contentPath: 'guide/http/setup', - }, - { - label: 'Making requests', - path: 'guide/http/making-requests', - contentPath: 'guide/http/making-requests', - }, - { - label: 'Reactive data fetching with httpResource', - path: 'guide/http/http-resource', - contentPath: 'guide/http/http-resource', - }, - { - label: 'Intercepting requests and responses', - path: 'guide/http/interceptors', - contentPath: 'guide/http/interceptors', - }, - { - label: 'Testing', - path: 'guide/http/testing', - contentPath: 'guide/http/testing', - }, - ], - }, - { - label: 'Server-side & hybrid-rendering', - children: [ - { - label: 'Overview', - path: 'guide/performance', - contentPath: 'guide/performance/overview', - }, - { - label: 'Server-side and hybrid-rendering', - path: 'guide/ssr', - contentPath: 'guide/ssr', - }, - { - label: 'Hydration', - path: 'guide/hydration', - contentPath: 'guide/hydration', - }, - { - label: 'Incremental Hydration', - path: 'guide/incremental-hydration', - contentPath: 'guide/incremental-hydration', - }, - ], - }, - { - label: 'Testing', - children: [ - { - label: 'Overview', - path: 'guide/testing', - contentPath: 'guide/testing/overview', - }, - { - label: 'Basics of testing components', - path: 'guide/testing/components-basics', - contentPath: 'guide/testing/components-basics', - }, - { - label: 'Component testing scenarios', - path: 'guide/testing/components-scenarios', - contentPath: 'guide/testing/components-scenarios', - }, - { - label: 'Testing services', - path: 'guide/testing/services', - contentPath: 'guide/testing/services', - }, - { - label: 'Testing attribute directives', - path: 'guide/testing/attribute-directives', - contentPath: 'guide/testing/attribute-directives', - }, - { - label: 'Testing pipes', - 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', - contentPath: 'guide/testing/debugging', - }, - { - label: 'Code coverage', - path: 'guide/testing/code-coverage', - contentPath: 'guide/testing/code-coverage', - }, - { - label: 'Testing utility APIs', - path: 'guide/testing/utility-apis', - contentPath: 'guide/testing/utility-apis', - }, - { - label: 'Zone.js Testing Utilities', - path: 'guide/testing/zone-js-testing-utilities', - contentPath: 'guide/testing/zone-js-testing-utilities', - }, - { - 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', - }, - { - label: 'Migrating from Karma to Vitest', - path: 'guide/testing/migrating-to-vitest', - contentPath: 'guide/testing/migrating-to-vitest', - }, - { - label: 'Testing with Karma and Jasmine', - path: 'guide/testing/karma', - contentPath: 'guide/testing/karma', - }, - ], - }, - { - label: 'Angular Aria', - status: 'new', - children: [ - { - label: 'Overview', - path: 'guide/aria/overview', - contentPath: 'guide/aria/overview', - }, - { - label: 'Accordion', - path: 'guide/aria/accordion', - contentPath: 'guide/aria/accordion', - }, - { - label: 'Autocomplete', - path: 'guide/aria/autocomplete', - contentPath: 'guide/aria/autocomplete', - }, - { - label: 'Combobox', - path: 'guide/aria/combobox', - contentPath: 'guide/aria/combobox', - }, - { - label: 'Grid', - path: 'guide/aria/grid', - contentPath: 'guide/aria/grid', - }, - { - label: 'Listbox', - path: 'guide/aria/listbox', - contentPath: 'guide/aria/listbox', - }, - { - label: 'Menu', - path: 'guide/aria/menu', - contentPath: 'guide/aria/menu', - }, - { - label: 'Menubar', - path: 'guide/aria/menubar', - contentPath: 'guide/aria/menubar', - }, - { - label: 'Multiselect', - path: 'guide/aria/multiselect', - contentPath: 'guide/aria/multiselect', - }, - { - label: 'Select', - path: 'guide/aria/select', - contentPath: 'guide/aria/select', - }, - { - label: 'Tabs', - path: 'guide/aria/tabs', - contentPath: 'guide/aria/tabs', - }, - { - label: 'Toolbar', - path: 'guide/aria/toolbar', - contentPath: 'guide/aria/toolbar', - }, - { - label: 'Tree', - path: 'guide/aria/tree', - contentPath: 'guide/aria/tree', - }, - ], - }, - { - label: 'Internationalization', - children: [ - { - label: 'Overview', - path: 'guide/i18n', - contentPath: 'guide/i18n/overview', - }, - { - label: 'Add the localize package', - path: 'guide/i18n/add-package', - contentPath: 'guide/i18n/add-package', - }, - { - label: 'Refer to locales by ID', - path: 'guide/i18n/locale-id', - contentPath: 'guide/i18n/locale-id', - }, - { - label: 'Format data based on locale', - path: 'guide/i18n/format-data-locale', - contentPath: 'guide/i18n/format-data-locale', - }, - { - label: 'Prepare component for translation', - path: 'guide/i18n/prepare', - contentPath: 'guide/i18n/prepare', - }, - { - label: 'Work with translation files', - path: 'guide/i18n/translation-files', - contentPath: 'guide/i18n/translation-files', - }, - { - label: 'Merge translations into the app', - path: 'guide/i18n/merge', - contentPath: 'guide/i18n/merge', - }, - { - label: 'Deploy multiple locales', - path: 'guide/i18n/deploy', - contentPath: 'guide/i18n/deploy', - }, - { - label: 'Import global variants of the locale data', - path: 'guide/i18n/import-global-variants', - contentPath: 'guide/i18n/import-global-variants', - }, - { - label: 'Manage marked text with custom IDs', - path: 'guide/i18n/manage-marked-text', - contentPath: 'guide/i18n/manage-marked-text', - }, - { - label: 'Example Angular application', - path: 'guide/i18n/example', - contentPath: 'guide/i18n/example', - }, - ], - }, - { - label: 'Animations', - status: 'updated', - children: [ - { - 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/routing/route-transition-animations', - contentPath: 'guide/routing/route-transition-animations', - }, - ], - }, - { - label: 'Drag and drop', - path: 'guide/drag-drop', - contentPath: 'guide/drag-drop', - }, - ], - }, - { - label: 'Build with AI', - status: 'new', - children: [ - { - 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', - }, - { - label: 'Angular AI Tutor', - path: 'ai/ai-tutor', - contentPath: 'ai/ai-tutor', - }, - ], - }, - { - label: 'Developer Tools', - children: [ - { - label: 'Angular CLI', - children: [ - { - label: 'Overview', - path: 'tools/cli', - contentPath: 'tools/cli/overview', - }, - { - label: 'Local set-up', - path: 'tools/cli/setup-local', - contentPath: 'tools/cli/setup-local', - }, - { - label: 'Building Angular apps', - path: 'tools/cli/build', - contentPath: 'tools/cli/build', - }, - { - label: 'Serving Angular apps for development', - path: 'tools/cli/serve', - contentPath: 'tools/cli/serve', - }, - { - label: 'Deployment', - path: 'tools/cli/deployment', - contentPath: 'tools/cli/deployment', - }, - { - label: 'End-to-End Testing', - path: 'tools/cli/end-to-end', - contentPath: 'tools/cli/end-to-end', - }, - { - label: 'Migrating to new build system', - path: 'tools/cli/build-system-migration', - contentPath: 'tools/cli/build-system-migration', - }, - { - label: 'Build environments', - path: 'tools/cli/environments', - contentPath: 'tools/cli/environments', - }, - { - label: 'Angular CLI builders', - path: 'tools/cli/cli-builder', - contentPath: 'tools/cli/cli-builder', - }, - { - label: 'Generating code using schematics', - path: 'tools/cli/schematics', - contentPath: 'tools/cli/schematics', - }, - { - label: 'Authoring schematics', - path: 'tools/cli/schematics-authoring', - contentPath: 'tools/cli/schematics-authoring', - }, - { - label: 'Schematics for libraries', - path: 'tools/cli/schematics-for-libraries', - contentPath: 'tools/cli/schematics-for-libraries', - }, - { - label: 'Template type checking', - path: 'tools/cli/template-typecheck', - contentPath: 'tools/cli/template-typecheck', - }, - { - label: 'Ahead-of-time (AOT) compilation', - path: 'tools/cli/aot-compiler', - contentPath: 'tools/cli/aot-compiler', - }, - { - label: 'AOT metadata errors', - path: 'tools/cli/aot-metadata-errors', - contentPath: 'tools/cli/aot-metadata-errors', - }, - ], - }, - { - label: 'Libraries', - children: [ - { - label: 'Overview', - path: 'tools/libraries', - contentPath: 'tools/libraries/overview', - }, - { - label: 'Creating Libraries', - path: 'tools/libraries/creating-libraries', - contentPath: 'tools/libraries/creating-libraries', - }, - { - label: 'Using Libraries', - path: 'tools/libraries/using-libraries', - contentPath: 'tools/libraries/using-libraries', - }, - { - label: 'Angular Package Format', - path: 'tools/libraries/angular-package-format', - contentPath: 'tools/libraries/angular-package-format', - }, - ], - }, - { - label: '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', - }, - { - label: 'Injectors', - path: 'tools/devtools/injectors', - contentPath: 'tools/devtools/injectors', - }, - // 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', - path: 'tools/language-service', - contentPath: 'tools/language-service', - }, - ], - }, - { - label: 'Best Practices', - children: [ - { - label: 'Style Guide', - path: 'style-guide', - contentPath: 'best-practices/style-guide', - status: 'updated', - }, - { - label: 'Security', - path: 'best-practices/security', - contentPath: 'guide/security', // Have not refactored due to build issues - }, - { - label: 'Accessibility', - 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: [ - { - label: 'Overview', - path: 'best-practices/runtime-performance', - contentPath: 'best-practices/runtime-performance/overview', - }, - { - label: 'Zone pollution', - path: 'best-practices/zone-pollution', - contentPath: 'best-practices/runtime-performance/zone-pollution', - }, - { - label: 'Slow computations', - path: 'best-practices/slow-computations', - contentPath: 'best-practices/runtime-performance/slow-computations', - }, - { - label: 'Skipping component subtrees', - 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'}, - ], - }, - { - label: 'Keeping up-to-date', - path: 'update', - contentPath: 'best-practices/update', - }, - ], - }, - { - label: 'Developer Events', - children: [ - { - label: 'Angular v21 Release', - path: 'events/v21', - contentPath: 'events/v21', - status: 'new', - }, - ], - }, - { - 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: [ - { - label: 'Overview', - path: 'ecosystem/service-workers', - contentPath: 'ecosystem/service-workers/overview', - }, - { - label: 'Getting started', - 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', - contentPath: 'ecosystem/service-workers/config', - }, - { - label: 'Communicating with the service worker', - path: 'ecosystem/service-workers/communications', - contentPath: 'ecosystem/service-workers/communications', - }, - { - label: 'Push notifications', - path: 'ecosystem/service-workers/push-notifications', - contentPath: 'ecosystem/service-workers/push-notifications', - }, - { - label: 'Service worker devops', - path: 'ecosystem/service-workers/devops', - contentPath: 'ecosystem/service-workers/devops', - }, - { - label: 'App shell pattern', - path: 'ecosystem/service-workers/app-shell', - contentPath: 'ecosystem/service-workers/app-shell', - }, - ], - }, - { - label: 'Web workers', - 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', - }, - { - label: 'Google Maps', - path: 'https://github.com/angular/components/tree/main/src/google-maps#readme', - }, - { - label: 'Google Pay', - path: 'https://github.com/google-pay/google-pay-button#angular', - }, - { - label: 'YouTube player', - path: 'https://github.com/angular/components/blob/main/src/youtube-player/README.md', - }, - { - label: 'Angular CDK', - path: 'https://material.angular.dev/cdk/categories', - }, - { - label: 'Angular Material', - 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, - SIGNAL_FORMS_TUTORIAL_NAV_DATA, - { - path: DEFAULT_PAGES.TUTORIALS, - contentPath: 'tutorials/home', - label: 'Tutorials', - }, -]; - -const REFERENCE_SUB_NAVIGATION_DATA: NavigationItem[] = [ - { - label: 'Roadmap', - path: 'roadmap', - contentPath: 'reference/roadmap', - }, - { - label: 'Get involved', - path: 'https://github.com/angular/angular/blob/main/CONTRIBUTING.md', - }, - { - label: 'API Reference', - children: [ - { - label: 'Overview', - path: 'api', - }, - ...getApiNavigationItems(), - ], - }, - { - label: 'CLI Reference', - children: [ - { - label: 'Overview', - path: 'cli', - contentPath: 'reference/cli', - }, - { - label: 'ng add', - path: 'cli/add', - }, - { - label: 'ng analytics', - children: [ - { - label: 'Overview', - path: 'cli/analytics', - }, - { - label: 'disable', - path: 'cli/analytics/disable', - }, - { - label: 'enable', - path: 'cli/analytics/enable', - }, - { - label: 'info', - path: 'cli/analytics/info', - }, - { - label: 'prompt', - path: 'cli/analytics/prompt', - }, - ], - }, - { - label: 'ng build', - path: 'cli/build', - }, - { - label: 'ng cache', - children: [ - { - label: 'Overview', - path: 'cli/cache', - }, - { - label: 'clean', - path: 'cli/cache/clean', - }, - { - label: 'disable', - path: 'cli/cache/disable', - }, - { - label: 'enable', - path: 'cli/cache/enable', - }, - { - label: 'info', - path: 'cli/cache/info', - }, - ], - }, - { - label: 'ng completion', - children: [ - { - label: 'Overview', - path: 'cli/completion', - }, - { - label: 'script', - path: 'cli/completion/script', - }, - ], - }, - { - label: 'ng config', - path: 'cli/config', - }, - { - label: 'ng deploy', - path: 'cli/deploy', - }, - { - label: 'ng e2e', - path: 'cli/e2e', - }, - { - label: 'ng extract-i18n', - path: 'cli/extract-i18n', - }, - { - label: 'ng generate', - children: [ - { - label: 'Overview', - path: 'cli/generate', - }, - { - label: 'ai-config', - path: 'cli/generate/ai-config', - }, - { - label: 'app-shell', - path: 'cli/generate/app-shell', - }, - { - label: 'application', - path: 'cli/generate/application', - }, - { - label: 'class', - path: 'cli/generate/class', - }, - { - label: 'component', - path: 'cli/generate/component', - }, - { - label: 'config', - path: 'cli/generate/config', - }, - { - label: 'directive', - path: 'cli/generate/directive', - }, - { - label: 'enum', - path: 'cli/generate/enum', - }, - { - label: 'environments', - path: 'cli/generate/environments', - }, - { - label: 'guard', - path: 'cli/generate/guard', - }, - { - label: 'interceptor', - path: 'cli/generate/interceptor', - }, - { - label: 'interface', - path: 'cli/generate/interface', - }, - { - label: 'library', - path: 'cli/generate/library', - }, - { - label: 'module', - path: 'cli/generate/module', - }, - { - label: 'pipe', - path: 'cli/generate/pipe', - }, - { - label: 'resolver', - path: 'cli/generate/resolver', - }, - { - label: 'service-worker', - path: 'cli/generate/service-worker', - }, - { - label: 'service', - path: 'cli/generate/service', - }, - { - label: 'web-worker', - path: 'cli/generate/web-worker', - }, - ], - }, - { - label: 'ng lint', - path: 'cli/lint', - }, - { - label: 'ng new', - path: 'cli/new', - }, - { - label: 'ng run', - path: 'cli/run', - }, - { - label: 'ng serve', - path: 'cli/serve', - }, - { - label: 'ng test', - path: 'cli/test', - }, - { - label: 'ng update', - path: 'cli/update', - }, - { - label: 'ng version', - path: 'cli/version', - }, - ], - }, - { - label: 'Error Encyclopedia', - children: [ - { - label: 'Overview', - path: 'errors', - contentPath: 'reference/errors/overview', - }, - ...ERRORS_NAV_DATA, - ], - }, - { - label: 'Extended Diagnostics', - children: [ - { - label: 'Overview', - path: 'extended-diagnostics', - contentPath: 'reference/extended-diagnostics/overview', - }, - ...EXT_DIAGNOSTICS_NAV_DATA, - ], - }, - { - label: 'Versioning and releases', - path: 'reference/releases', - contentPath: 'reference/releases', - }, - { - label: 'Version compatibility', - path: 'reference/versions', - contentPath: 'reference/versions', - }, - { - label: 'Update guide', - path: 'update-guide', - }, - { - label: 'Configurations', - children: [ - { - label: 'File structure', - path: 'reference/configs/file-structure', - contentPath: 'reference/configs/file-structure', - }, - { - label: 'Workspace configuration', - path: 'reference/configs/workspace-config', - contentPath: 'reference/configs/workspace-config', - }, - { - label: 'Angular compiler options', - path: 'reference/configs/angular-compiler-options', - contentPath: 'reference/configs/angular-compiler-options', - }, - { - label: 'npm dependencies', - path: 'reference/configs/npm-packages', - contentPath: 'reference/configs/npm-packages', - }, - ], - }, - { - label: 'Migrations', - children: [ - { - label: 'Overview', - path: 'reference/migrations', - contentPath: 'reference/migrations/overview', - }, - { - label: 'Standalone', - path: 'reference/migrations/standalone', - contentPath: 'reference/migrations/standalone', - }, - { - label: 'Control Flow Syntax', - path: 'reference/migrations/control-flow', - contentPath: 'reference/migrations/control-flow', - }, - { - label: 'inject() Function', - path: 'reference/migrations/inject-function', - contentPath: 'reference/migrations/inject-function', - }, - { - label: 'Lazy-loaded routes', - path: 'reference/migrations/route-lazy-loading', - contentPath: 'reference/migrations/route-lazy-loading', - }, - { - label: 'Signal inputs', - path: 'reference/migrations/signal-inputs', - contentPath: 'reference/migrations/signal-inputs', - }, - { - 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', - }, - { - label: 'NgClass to Class', - path: 'reference/migrations/ngclass-to-class', - contentPath: 'reference/migrations/ngclass-to-class', - status: 'new', - }, - { - label: 'NgStyle to Style', - path: 'reference/migrations/ngstyle-to-style', - contentPath: 'reference/migrations/ngstyle-to-style', - status: 'new', - }, - { - label: 'Router Testing Module Migration', - path: 'reference/migrations/router-testing-module-migration', - contentPath: 'reference/migrations/router-testing-module-migration', - status: 'new', - }, - { - label: 'CommonModule to Standalone', - path: 'reference/migrations/common-to-standalone', - contentPath: 'reference/migrations/common-to-standalone', - status: 'new', - }, - ], - }, -]; - -const FOOTER_NAVIGATION_DATA: NavigationItem[] = [ - { - label: 'Press Kit', - path: 'press-kit', - contentPath: 'reference/press-kit', - }, - { - label: 'License', - path: 'license', - contentPath: 'reference/license', - }, -]; - // Docs navigation data structure, it's used to display structure in // navigation-list component And build the routing table for content pages. export const SUB_NAVIGATION_DATA: SubNavigationData = { diff --git a/adev-es/src/content/ai/agent-skills.md b/adev-es/src/content/ai/agent-skills.md new file mode 100644 index 0000000..0ee75ff --- /dev/null +++ b/adev-es/src/content/ai/agent-skills.md @@ -0,0 +1,24 @@ +# Agent Skills + +Agent Skills are specialized, domain-specific instructions and capabilities designed for AI agents like Gemini CLI. These skills provide architectural guidance, generate idiomatic Angular code, and help scaffold new projects using modern best practices. + +By using Agent Skills, you can ensure that the AI agent you are working with has the most up-to-date information about Angular's conventions, reactivity models (like Signals), and project structure. + +## Available Skills + +The Angular team maintains a collection of official skills that are regularly updated to stay in sync with the latest framework improvements. + +| Skill | Description | +| :---------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **`angular-developer`** | Generates Angular code and provides architectural guidance. Useful for creating components, services, or obtaining best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling, testing, or CLI tooling. | +| **`angular-new-app`** | Creates a new Angular app using the Angular CLI. Provides important guidelines for effectively setting up and structuring a modern Angular application. | + +## Using Agent Skills + +Agent Skills are designed to be used with agentic coding tools like [Gemini CLI](https://geminicli.com/docs/cli/skills/), [Antigravity](https://antigravity.google/docs/skills) and more. Activating a skill loads the specific instructions and resources needed for that task. + +To use these skills in your own environment you may follow the instructions for your specific tool or use a community tool like [skills.sh](https://skills.sh/). + +```bash +npx skills add https://github.com/angular/skills +``` diff --git a/adev-es/src/content/ai/ai-tutor.en.md b/adev-es/src/content/ai/ai-tutor.en.md index fd3b01a..c38c778 100644 --- a/adev-es/src/content/ai/ai-tutor.en.md +++ b/adev-es/src/content/ai/ai-tutor.en.md @@ -97,6 +97,36 @@ If you want to learn about a specific topic out of order (e.g., jump from the ba ## **Troubleshooting** +### Setup Issues + +**"launch the Angular AI tutor" doesn't do anything?** + +Make sure you have a project open first. The tutor needs an actual Angular project to work with: + +```bash +ng new my-app +cd my-app +code . +``` + +Then ensure your MCP server is running. In VS Code, open the `.vscode/mcp.json` file and click the **"Start"** button at the top of the file. + +When you type "launch the Angular AI tutor", you should see a checkmark saying +"Reviewed .vscode/mcp.json and ran start task" and a prompt asking to +"Allow task run?" — go ahead and click Allow. + +**Still not working?** + +Try typing `#angular-cli` first to load the Angular context, then paste the tutorial URL: `https://angular.dev/ai/ai-tutor` + +**How to verify the server is running** + +Open the Command Palette (`Ctrl+Shift+P`), type "MCP: List Running Servers", and look for "angular-cli" in the list. + +--- + +### General Issues + If the tutor doesn't respond correctly or you suspect an issue with your application, here are a few things to try: 1. **Type "proceed":** This can often nudge the tutor to continue to the next step in the event it gets stuck. @@ -108,7 +138,7 @@ If the tutor doesn't respond correctly or you suspect an issue with your applica ## **Your Learning Journey: The Phased Path** -You will build your application over a four-phase journey. You can follow this path from start to finish to create a complete, fully-functional Angular application. Each module builds logically upon the last, taking you from the basics to advanced, real-world features. +You will build your application over a five-phase journey. You can follow this path from start to finish to create a complete, fully-functional Angular application. Each module builds logically upon the last, taking you from the basics to advanced, real-world features. **A Note on Automated Setup:** Some modules require a setup step, like creating interfaces or mock data. In these cases, the tutor will present you with the code and file instructions. You will be responsible for creating and modifying these files as instructed before the exercise begins. @@ -141,6 +171,13 @@ You will build your application over a four-phase journey. You can follow this p - **Module 16:** Introduction to Forms - **Module 17:** Intro to Angular Material +### **Phase 5: Signal Forms** + +- **Module 18**: **Introduction to Signal Forms** +- **Module 19**: **Submitting & Resetting** +- **Module 20**: **Validation in Signal Forms** +- **Module 21**: **Field State & Error Messages** + --- ## **A Note on AI & Feedback** diff --git a/adev-es/src/content/ai/ai-tutor.md b/adev-es/src/content/ai/ai-tutor.md index 3d1062d..c364785 100644 --- a/adev-es/src/content/ai/ai-tutor.md +++ b/adev-es/src/content/ai/ai-tutor.md @@ -97,6 +97,34 @@ Si quieres aprender sobre un tema específico fuera de orden (por ejemplo, salta ## **Solución de Problemas** +### Problemas de configuración + +**¿"iniciar el tutor de IA de Angular" no hace nada?** + +Asegúrate de tener un proyecto abierto primero. El tutor necesita un proyecto Angular real con el que trabajar: + +```bash +ng new my-app +cd my-app +code . +``` + +Luego asegúrate de que tu servidor MCP esté ejecutándose. En VS Code, abre el archivo `.vscode/mcp.json` y haz clic en el botón **"Start"** en la parte superior del archivo. + +Cuando escribas "iniciar el tutor de IA de Angular", deberías ver una marca de verificación que dice "Reviewed .vscode/mcp.json and ran start task" y un aviso preguntando "¿Permitir ejecución de tarea?" — adelante y haz clic en Permitir. + +**¿Sigue sin funcionar?** + +Intenta escribir `#angular-cli` primero para cargar el contexto de Angular, luego pega la URL del tutorial: `https://angular.dev/ai/ai-tutor` + +**Cómo verificar que el servidor está ejecutándose** + +Abre la Paleta de Comandos (`Ctrl+Shift+P`), escribe "MCP: List Running Servers" y busca "angular-cli" en la lista. + +--- + +### Problemas generales + Si el tutor no responde correctamente o sospechas un problema con tu aplicación, aquí hay algunas cosas que puedes intentar: 1. **Escribe "continuar":** Esto a menudo puede impulsar al tutor a continuar al siguiente paso en caso de que se quede atascado. @@ -108,7 +136,7 @@ Si el tutor no responde correctamente o sospechas un problema con tu aplicación ## **Tu Viaje de Aprendizaje: El Camino por Fases** -Construirás tu aplicación a lo largo de un viaje de cuatro fases. Puedes seguir este camino de principio a fin para crear una aplicación Angular completa y totalmente funcional. Cada módulo se construye lógicamente sobre el anterior, llevándote desde los básicos hasta características avanzadas del mundo real. +Construirás tu aplicación a lo largo de un viaje de cinco fases. Puedes seguir este camino de principio a fin para crear una aplicación Angular completa y totalmente funcional. Cada módulo se construye lógicamente sobre el anterior, llevándote desde los básicos hasta características avanzadas del mundo real. **Una Nota sobre la Configuración Automatizada:** Algunos módulos requieren un paso de configuración, como crear interfaces o datos de prueba. En estos casos, el tutor te presentará el código e instrucciones de archivo. Serás responsable de crear y modificar estos archivos según las instrucciones antes de que comience el ejercicio. @@ -141,6 +169,13 @@ Construirás tu aplicación a lo largo de un viaje de cuatro fases. Puedes segui - **Módulo 16:** Introducción a los Formularios - **Módulo 17:** Introducción a Angular Material +### **Fase 5: Signal Forms** + +- **Módulo 18**: **Introducción a Signal Forms** +- **Módulo 19**: **Envío y Restablecimiento** +- **Módulo 20**: **Validación en Signal Forms** +- **Módulo 21**: **Estado de Campo y Mensajes de Error** + --- ## **Una Nota sobre IA y Retroalimentación** diff --git a/adev-es/src/content/ai/design-patterns.en.md b/adev-es/src/content/ai/design-patterns.en.md index cdcd753..8a4ce4d 100644 --- a/adev-es/src/content/ai/design-patterns.en.md +++ b/adev-es/src/content/ai/design-patterns.en.md @@ -27,11 +27,14 @@ storyResource = resource({ 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 - }}); - } + return runFlow({ + url, + input: { + userInput: params, + sessionId: this.storyService.sessionId(), // Read from another signal + }, + }); + }, }); ``` @@ -56,7 +59,7 @@ storyParts = linkedSignal({ const existingStoryParts = previous?.value || []; // Return a new array with the old and new parts return [...existingStoryParts, ...newStoryParts]; - } + }, }); ``` @@ -77,14 +80,14 @@ The following example demonstrates how to create a responsive UI to dynamically
    - + } @else if (imgResource.hasValue()) { - + } @else {
    -

    Failed to load image. Click to retry.

    +

    Failed to load image. Click to retry.

    } ``` @@ -101,23 +104,23 @@ characters = resource({ // exposed by the Genkit client SDK const response = streamFlow({ url: '/streamCharacters', - input: 10 + input: 10, }); (async () => { for await (const chunk of response.stream) { data.update((prev) => { if ('value' in prev) { - return { value: `${prev.value} ${chunk}` }; + return {value: `${prev.value} ${chunk}`}; } else { - return { error: chunk as unknown as Error }; + return {error: chunk as unknown as Error}; } }); } })(); return data; - } + }, }); ``` @@ -127,47 +130,48 @@ The `characters` member is updated asynchronously and can be displayed in the te @if (characters.isLoading()) {

    Loading...

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

    {{characters.value()}}

    +

    {{ characters.value() }}

    } @else { -

    {{characters.error()}}

    +

    {{ 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"; +import {startFlowServer} from '@genkit-ai/express'; +import {genkit} from 'genkit/beta'; +import {googleAI, gemini20Flash} from '@genkit-ai/googleai'; -const ai = genkit({ plugins: [googleAI()] }); +const ai = genkit({plugins: [googleAI()]}); -export const streamCharacters = ai.defineFlow({ +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; -}); + 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.en.md b/adev-es/src/content/ai/develop-with-ai.en.md index 4641f61..ed10e7f 100644 --- a/adev-es/src/content/ai/develop-with-ai.en.md +++ b/adev-es/src/content/ai/develop-with-ai.en.md @@ -18,11 +18,11 @@ Here is a set of instructions to help LLMs generate correct code that follows An ## Rules Files -Several editors, such as Firebase Studio have rules files useful for providing critical context to LLMs. +Several editors, such as Firebase Studio have rules files useful for providing critical context to LLMs. | Environment/IDE | Rules File | Installation Instructions | | :------------------- | :--------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Firebase Studio | airules.md | Configure `airules.md` | +| Firebase Studio | airules.md | Configure `airules.md` | | Copilot powered IDEs | copilot-instructions.md | Configure `.github/copilot-instructions.md` | | Cursor | cursor.md | Configure `cursorrules.md` | | JetBrains IDEs | guidelines.md | Configure `guidelines.md` | @@ -42,7 +42,7 @@ The Angular CLI includes an experimental [Model Context Protocol (MCP) server](h - 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. -Be sure [to check out the overview page](/ai) for more information on how to integrate AI into your 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 diff --git a/adev-es/src/content/ai/mcp-server-setup.en.md b/adev-es/src/content/ai/mcp-server-setup.en.md index 148c431..f8901dd 100644 --- a/adev-es/src/content/ai/mcp-server-setup.en.md +++ b/adev-es/src/content/ai/mcp-server-setup.en.md @@ -1,153 +1,125 @@ -# Angular CLI MCP Server setup +# Angular CLI MCP Server -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. +The Angular CLI includes a Model Context Protocol (MCP) server that enables AI assistants (like Cursor, Antigravity, JetBrains AI, etc.) to interact directly with the Angular CLI. It provides tools for code generation, workspace analysis, and running builds/tests. -## 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` | -| :-------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------: | :---------: | -| `ai_tutor` | Launches an interactive AI-powered Angular tutor. Recommended to run from a new Angular project using v20 or later. [Learn more](ai/ai-tutor). | ✅ | ✅ | -| `find_examples` | Finds authoritative code examples from a curated database of official, best-practice examples, focusing on **modern, new, and recently updated** Angular features. | ✅ | ✅ | -| `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. | ✅ | ✅ | -| `onpush_zoneless_migration` | Analyzes Angular code and provides a step-by-step, iterative plan to migrate it to `OnPush` change detection, a prerequisite for a zoneless application. | ✅ | ✅ | -| `search_documentation` | Searches the official Angular documentation at . This tool should be used to answer any questions about Angular, such as for APIs, tutorials, and best practices. | ❌ | ✅ | - -### Experimental Tools - -Some tools are provided in experimental / preview status since they are new or not fully tested. Enable them individually with the [`--experimental-tool`](#command-options) option and use them with caution. - -| Name | Description | `local-only` | `read-only` | -| :---------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------: | :---------: | -| `modernize` | Performs code migrations and provides further instructions on how to modernize Angular code to align with the latest best practices and syntax. [Learn more](https://angular.dev/reference/migrations) | ✅ | ❌ | + + If your host environment supports custom Agent Skills (such as Antigravity), you can combine the Angular CLI MCP server with the official [Angular AI Skills](https://angular.dev/ai/agent-skills). While the skills provide the agent with deep instruction-level guidance and coding standards, the MCP server provides the action tools (like compiling, running tests, and analyzing workspaces) to execute those guidelines, resulting in a complete and powerful development agent. + ## Get Started -To get started, run the following command in your terminal: +To use the MCP server, you configure your host environment (IDE or CLI) to run `npx @angular/cli mcp`. -```bash -ng mcp -``` + + + Create a file named `.antigravity/mcp.json` in your project's root: -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. + ```json + { + "mcpServers": { + "angular-cli": { + "command": "npx", + "args": ["-y", "@angular/cli", "mcp"] + } + } + } + ``` -### 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`. + + Create `.cursor/mcp.json` in the project root (or globally at `~/.cursor/mcp.json`): -```json -{ - "mcpServers": { - "angular-cli": { - "command": "npx", - "args": ["-y", "@angular/cli", "mcp"] + ```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: + + Create `.vscode/mcp.json`: -```json -{ - "mcpServers": { - "angular-cli": { - "command": "npx", - "args": ["-y", "@angular/cli", "mcp"] + ```json + { + "servers": { + "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: +## Available Tools (Default) -```json -{ - "mcpServers": { - "angular-cli": { - "command": "npx", - "args": ["-y", "@angular/cli", "mcp"] - } - } -} -``` +When the MCP server is enabled, AI agents have access to the following tools: -### JetBrains IDEs +| Name | Description | +| :-------------------------- | :-------------------------------------------------------------------------------------------------------- | +| `ai_tutor` | Launches an interactive AI-powered Angular tutor. | +| `devserver.start` | Asynchronously starts a dev server (`ng serve`). Returns immediately. | +| `devserver.stop` | Stops the dev server. | +| `devserver.wait_for_build` | Returns the logs of the most recent build in a running dev server. | +| `get_best_practices` | Retrieves the Angular Best Practices Guide (crucial for standalone components, typed forms, etc.). | +| `list_projects` | Lists all applications and libraries in the workspace by reading `angular.json`. | +| `onpush_zoneless_migration` | Analyzes code and provides a plan to migrate it to `OnPush` change detection (prerequisite for zoneless). | +| `run_target` | Executes a configured target (e.g., build, test, lint, e2e, deploy). | +| `search_documentation` | Searches the official documentation at `https://angular.dev`. | -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`. Then paste the following configuration: +## Common Workflows -```json -{ - "mcpServers": { - "angular-cli": { - "command": "npx", - "args": ["-y", "@angular/cli", "mcp"] - } - } -} -``` +These workflows demonstrate how AI assistants coordinate different MCP tools to automatically achieve complex developer stories. -For the most up-to-date instructions on configuring MCP servers, please refer to the JetBrains documentation: [Connect to an MCP server](https://www.jetbrains.com/help/ai-assistant/mcp.html#connect-to-an-mcp-server). +### 1. Performance Tuning: Zoneless & OnPush Migration -### VS Code +The AI agent optimizes change detection performance and migrates components to a zoneless-ready state. -In your project's root, create a file named `.vscode/mcp.json` and add the following configuration. Note the use of the `servers` property. +1. **Discover Workspace**: The AI agent calls `list_projects` to locate components, projects, and style/test configurations in the workspace. +2. **Schematic Modernization (Prerequisite)**: The AI agent runs any prerequisite signal migrations using standard `ng generate` commands (e.g., Signal Inputs, Signal Queries). +3. **Plan Migration**: The AI agent calls `onpush_zoneless_migration` with the absolute path of the directory or component file. +4. **Apply Changes**: The AI agent automatically applies the single actionable change returned by the tool to the codebase. +5. **Verify Changes**: The AI agent runs unit tests by calling `run_target` with the target parameter set to `"test"`. +6. **Repeat**: The AI agent calls `onpush_zoneless_migration` again to retrieve the next step, repeating until the tool indicates the migration is complete. -```json -{ - "servers": { - "angular-cli": { - "command": "npx", - "args": ["-y", "@angular/cli", "mcp"] - } - } -} -``` +### 2. Feature Development & TDD Loop -### Other IDEs +The AI agent automates research, implementation, and verification when developing new features. -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. +1. **API & Syntax Research**: The AI agent uses `search_documentation` to look up Angular APIs or syntax rules (e.g., `@defer` block options). +2. **Load Coding Standards**: The AI agent calls `get_best_practices` with the workspace path to load Angular version-aligned coding rules. +3. **Start Local Dev Server**: The AI agent starts a background server by calling `devserver.start`. +4. **Monitor Build**: The AI agent uses `devserver.wait_for_build` to watch build logs and ensure compilation succeeds as it edits the code. +5. **Write and Execute Tests**: The AI agent identifies the project's test framework (e.g., Jasmine, Jest, Vitest) via `list_projects`, writes the corresponding test file, and runs the tests using `run_target` with `"test"`. +6. **Stop Dev Server**: When finished, the AI agent stops the active dev server by calling `devserver.stop`. -```json -{ - "mcpServers": { - "angular-cli": { - "command": "npx", - "args": ["-y", "@angular/cli", "mcp"] - } - } -} -``` +### 3. Developer Onboarding and Learning + +The AI agent guides the developer through Angular concepts in an interactive sandbox. + +1. **Discover Projects**: The AI agent calls `list_projects` to scan the workspace and identify the codebase structure. +2. **Launch Tutor**: The AI agent runs `ai_tutor` to load the curriculum instructions, persona, and tutoring guidelines. +3. **Follow the Curriculum**: The AI agent guides the user through the curriculum, explaining concepts and instructing them on what components to build or modify. +4. **Implement & Verify**: The AI agent helps implement the sandbox code and verifies changes using `run_target` with `"test"` or `"build"`. ## Command Options -The `mcp` command can be configured with the following options passed as arguments in your IDE's MCP configuration: +You can pass arguments to the MCP server in the `args` array of your 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` | -| `--experimental-tool`
    `-E` | `string` | Enable an [experimental tool](#experimental-tools). Separate multiple options by spaces, e.g. `-E tool_a tool_b`. | | +- `--read-only`: Only registers tools that do not modify the project. +- `--local-only`: Only registers tools that do not require an internet connection. -For example, to run the server in read-only mode in VS Code, you would update your `mcp.json` like this: +Example for read-only mode: ```json -{ - "servers": { - "angular-cli": { - "command": "npx", - "args": ["-y", "@angular/cli", "mcp", "--read-only"] - } - } -} +"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). diff --git a/adev-es/src/content/ai/overview.en.md b/adev-es/src/content/ai/overview.en.md index e2da24e..8fae0a4 100644 --- a/adev-es/src/content/ai/overview.en.md +++ b/adev-es/src/content/ai/overview.en.md @@ -38,7 +38,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.5 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/frameworks/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. @@ -59,7 +59,7 @@ Here are examples of how to build with Genkit and Angular: Here is an example of how to build with Firebase AI Logic and Angular: -- [Firebase AI Logic x Angular Starter Kit](https://github.com/angular/examples/tree/main/vertex-ai-firebase-angular-example) - Use this starter-kit to build an e-commerce application with a chat agent that can perform tasks. Start here if you do not have experience building with Firebase AI Logic and Angular. +- [Firebase AI Logic x Angular Starter Kit](https://github.com/angular/examples/tree/main/firebase-ai-logic-angular-example) - Use this starter-kit to build an e-commerce application with a chat agent that can perform tasks. Start here if you do not have experience building 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). @@ -79,7 +79,7 @@ When connecting to model providers, it is important to keep your API secrets saf 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). +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/firebase-ai-logic-angular-example). For server-side connections to model APIs that require API keys, prefer using a secrets manager or environment variable, not `environments.ts`. You should follow standard best practices for securing API keys and credentials. Firebase now provides a new secrets manager with the latest updates from Firebase App Hosting. To learn more, [check out the official documentation](https://firebase.google.com/docs/app-hosting/configure). @@ -91,7 +91,7 @@ If you want to build agentic workflows, where agents are able to act and use too Tool calling further enhances your web apps by expanding your AI integration further than a question and answer style chat bot. In fact, you can empower your model to request function calls using the function calling API of your model provider. The available tools can be used to perform more complex actions within the context of your application. -In the [e-commerce example](https://github.com/angular/examples/blob/main/vertex-ai-firebase-angular-example/src/app/ai.service.ts#L88) of the [Angular examples repository](https://github.com/angular/examples), the LLM requests to make calls to functions for inventory in order to gain the necessary context to perform more complex tasks such as calculating how much a group of items in the store will cost. The scope of the available API is up to you as a developer just as is whether or not to call a function requested by the LLM. You remain in control of the flow of execution. You can expose specific functions of a service for example but not all functions of that service. +In the [e-commerce example](https://github.com/angular/examples/blob/main/firebase-ai-logic-angular-example/src/app/ai.service.ts#L88) of the [Angular examples repository](https://github.com/angular/examples), the LLM requests to make calls to functions for inventory in order to gain the necessary context to perform more complex tasks such as calculating how much a group of items in the store will cost. The scope of the available API is up to you as a developer just as is whether or not to call a function requested by the LLM. You remain in control of the flow of execution. You can expose specific functions of a service for example but not all functions of that service. ### Handling non-deterministic responses @@ -115,4 +115,5 @@ To learn about LLM prompts and AI IDE setup, see the following guides: + diff --git a/adev-es/src/content/ai/webmcp.md b/adev-es/src/content/ai/webmcp.md new file mode 100644 index 0000000..b175a4d --- /dev/null +++ b/adev-es/src/content/ai/webmcp.md @@ -0,0 +1,259 @@ +# WebMCP + +Web Model Context Protocol (WebMCP) is an [emerging web standard](https://github.com/webmachinelearning/webmcp/) that allows web applications to expose structured tools directly to AI agents running natively in the browser. Tools defined by an application allow AI assistants to interact with it directly, providing additional capabilities to the agent and reducing the need for DOM interactions. + +For example, an application to register a new user might provide a WebMCP tool for a browser's AI agent to create the user directly rather than requiring the agent to go through a complex wizard UI via DOM interactions. + +Angular provides experimental support for WebMCP, allowing you to easily register tools tied to your application's dependency injection lifecycle and automatically turn your Signal Forms into AI-ready tools. + +IMPORTANT: The WebMCP spec is very early in its lifecycle and is undergoing frequent changes. As such, WebMCP support in Angular is currently [**experimental**](reference/releases#experimental). APIs are subject to change even outside of major versions. + +## Provide tools for the application + +Use [`provideExperimentalWebMcpTools`](api/core/provideExperimentalWebMcpTools) in your application config to register tools for the entire lifecycle of the application. Tools provided this way are automatically registered when the application initializes and unregistered when the application is destroyed. + +The `execute` callback is invoked in the injection context of the associated `Injector`, meaning you can [`inject`](api/core/inject) services directly. + +```ts {header:"main.ts"} +import {Service, inject, provideExperimentalWebMcpTools} from '@angular/core'; +import {bootstrapApplication} from '@angular/platform-browser'; +import {AppRoot} from './app-root'; + +@Service() +class Greeter { + sayHello(): string { + return 'Hello agent!'; + } +} + +bootstrapApplication(AppRoot, { + providers: [ + provideExperimentalWebMcpTools([ + { + name: 'greet', + description: 'Greets the agent.', + inputSchema: {type: 'object', properties: {}}, + execute: () => { + const greeter = inject(Greeter); + + return {content: [{type: 'text', text: greeter.sayHello()}]}; + }, + }, + ]), + ], +}); +``` + +### Define tool parameters + +When a tool requires input from the AI assistant, define the expected arguments inside `inputSchema` using [JSON Schema](https://json-schema.org/) syntax. Angular automatically infers the parameter types passed into your `execute` callback based on the schema definition. + +```ts {header:"main.ts"} +import {provideExperimentalWebMcpTools} from '@angular/core'; +import {bootstrapApplication} from '@angular/platform-browser'; +import {AppRoot} from './app-root'; + +bootstrapApplication(AppRoot, { + providers: [ + provideExperimentalWebMcpTools([ + { + name: 'searchCatalog', + description: 'Searches the store catalog for products matching a query.', + inputSchema: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'The search keywords.', + }, + maxResults: { + type: 'number', + description: 'Maximum number of results to return.', + }, + }, + required: ['query'], + additionalProperties: false, + }, + execute: ({query, maxResults}) => { + // Type of `query` is inferred as `string`. + // Type of `maxResults` is inferred as `number | undefined`. + + // Consider validating this at runtime, since inputs may not be validated to match the schema. + if (typeof query !== 'string') throw new Error(`Bad query: ${query}`); + if (typeof maxResults !== 'number' && maxResults !== undefined) + throw new Error(`Bad maxResults: ${maxResults}`); + + const limit = maxResults ?? 5; + return { + content: [{type: 'text', text: `Returning up to ${limit} results for "${query}".`}], + }; + }, + }, + ]), + ], +}); +``` + +TIP: Use `required: ['param1', 'param2', ...]` to remove `undefined` from the types of those parameters and use `additionalProperties: false` to restrict the argument object's type to only these parameters. + +## Provide tools for a route + +When building complex applications, you may only want certain tools available when the user is viewing specific routes. You can achieve this by providing tools directly in route definitions. + +```ts {header:"routes.ts"} +import {provideExperimentalWebMcpTools} from '@angular/core'; +import {Routes} from '@angular/router'; + +export const routes: Routes = [ + { + path: 'dashboard', + loadComponent: () => import('./dashboard').then((m) => m.Dashboard), + providers: [ + provideExperimentalWebMcpTools([ + { + name: 'exportDashboardReports', + description: 'Exports the current dashboard analytics.', + inputSchema: {type: 'object', properties: {}}, + execute: () => ({ + content: [{type: 'text', text: 'Dashboard export successfully triggered.'}], + }), + }, + ]), + ], + }, +]; +``` + +NOTE: When registering tools to a particular route, consider configuring the router to use [`withExperimentalAutoCleanupInjectors`](api/router/withExperimentalAutoCleanupInjectors) to ensure tools are automatically _unregistered_ when the user navigates away from the route. Without this option, WebMCP tools declared on routes will remain accessible to AI agents even after the user has navigated to a different route. + +```ts {header:"app.config.ts"} +import {ApplicationConfig} from '@angular/core'; +import {provideRouter, withExperimentalAutoCleanupInjectors} from '@angular/router'; +import {routes} from './routes'; + +export const appConfig: ApplicationConfig = { + providers: [provideRouter(routes, withExperimentalAutoCleanupInjectors())], +}; +``` + +## Provide tools within services + +For dynamic use cases, the [`declareExperimentalWebMcpTool`](api/core/declareExperimentalWebMcpTool) function registers a tool directly within an injection context and automatically unregisters it when that context is destroyed. + +```ts {header:"counter.ts"} +import {Service, declareExperimentalWebMcpTool, signal, inject} from '@angular/core'; + +@Service() +export class Counter { + readonly count = signal(0); + + constructor() { + declareExperimentalWebMcpTool({ + name: 'getCounter', + description: 'Reads the global counter.', + inputSchema: {type: 'object', properties: {}}, + execute: () => ({ + content: [{type: 'text', text: `The count is: ${this.count()}.`}], + }), + }); + } +} +``` + +While `declareExperimentalWebMcpTool` works in any injection context, watch out for [name collisions](#name-collisions) and prefer using it in root services. + +## Implicit tools in Signal Forms + +You can create a WebMCP tool implicitly from an existing Angular [Signal Form](essentials/signal-forms) with minimal configuration. Angular converts your form models into rich WebMCP tools, effectively supporting highly dynamic forms without requiring you to manually write JSON schemas or event handlers. + +### Enable the WebMCP forms feature + +First, add [`provideExperimentalWebMcpForms`](api/forms/signals/provideExperimentalWebMcpForms) to your root application providers: + +```ts {header:"main.ts"} +import {bootstrapApplication} from '@angular/platform-browser'; +import {provideExperimentalWebMcpForms} from '@angular/forms/signals'; +import {AppRoot} from './app-root'; + +bootstrapApplication(AppRoot, { + providers: [provideExperimentalWebMcpForms()], +}); +``` + +### Opt in a Signal Form + +Second, when defining a Signal Form using [`form`](api/forms/signals/form), pass the `experimentalWebMcpTool` configuration option to opt-in to an implicit WebMCP tool. Angular will inspect your form's data model and automatically generate a JSON schema for connected AI agents. + +```ts {header:"user-registration.ts"} +import {Component, signal} from '@angular/core'; +import {form, required, minLength} from '@angular/forms/signals'; + +@Component({ + selector: 'app-user-registration', + templateUrl: './user-registration.html', +}) +export class UserRegistration { + private readonly model = signal({ + firstName: '', + lastName: '', + age: 0, + hobbies: ['Web Development'], + }); + + readonly userForm = form( + this.model, + (f) => { + required(f.firstName, {message: 'First name is mandatory.'}); + required(f.lastName, {message: 'Last name is mandatory.'}); + }, + { + // Implicitly registers a WebMCP tool named `registerUser` with parameters derived from `model`. + experimentalWebMcpTool: { + name: 'registerUser', + description: 'Registers a new user.', + }, + submission: { + action: async (formValue) => { + console.log('Submitting user:', formValue); + // ... + }, + }, + }, + ); +} +``` + +In this example, Angular generates a WebMCP tool with a JSON schema which: + +1. includes `firstName`, `lastName`, `age`, and `hobbies` as parameters inferred from the initial value of the `model` signal. +2. defines `firstName` and `lastName` as _required_ fields as inferred from the [`required`](api/forms/signals/required) validator. +3. defines `hobbies` as an array of strings, allowing the agent to provide an arbitrary amount of hobbies. + +Beyond inferring the input schema, Angular also connects the WebMCP tool to the form's validation logic and submission handler. This means the agent will observe any validation errors triggered by its inputs or any failures which happen during submission, allowing it to self-correct and potentially retry. + +NOTE: Async validators are _not_ triggered and should be handled by the submission action. + +#### Constraints + +Angular infers the WebMCP schema from the initial value of your form model. This requires: + +- Concrete initial values (`''`, `0`, `false`): Angular cannot infer data types from `null` or `undefined`. +- Non-empty arrays (`['Hello!']`): Angular cannot infer data types from an empty array and requires at least one initial value. + +## Best practices + +Keep the following best practices in mind: + +### Name collisions + +WebMCP requires each tool to have a unique name and will throw an error if the same tool name is registered multiple times. This means calling `declareExperimentalWebMcpTool` or `provideExperimentalWebMcpTools` in a context where they might be registered multiple times (such as a component constructor) may lead to errors at runtime. + +Prefer placing tools on application providers, route providers, or root services where possible. When putting tools on a component, including [implicit tools in Signal Forms](#implicit-tools-in-signal-forms), ensure that component is only ever rendered on the page at most _once_ at any given time. + +### Validate tool inputs + +Angular does not provide any implicit validation that the inputs provided by an agent actually match the defined JSON schema. Consider explicitly validating arguments to the `execute` function before using them to ensure reliability. + +### Testing + +Consider using a mock WebMCP implementation like [`@mcp-b/webmcp-polyfill`](https://www.npmjs.com/package/@mcp-b/webmcp-polyfill) to effectively unit test your tools. diff --git a/adev-es/src/content/best-practices/a11y.en.md b/adev-es/src/content/best-practices/a11y.en.md index 9fb674e..2531454 100644 --- a/adev-es/src/content/best-practices/a11y.en.md +++ b/adev-es/src/content/best-practices/a11y.en.md @@ -10,10 +10,8 @@ This page discusses best practices for designing Angular applications that work ## Accessibility attributes - - Building accessible web experiences often involves setting [Accessible Rich Internet Applications \(ARIA\) attributes](https://web.dev/learn/accessibility/aria-html/) to provide semantic meaning where it might otherwise be missing. -Use attribute binding template syntax to control the values of accessibility-related attributes. +Use [attribute binding](guide/templates/binding#binding-dynamic-properties-and-attributes) template syntax to control the values of accessibility-related attributes. ### ARIA attributes and properties @@ -39,13 +37,10 @@ Some ARIA patterns expose DOM APIs or directive inputs that accept structured va

    Attention

    Please review your answers before continuing.

    -
    +
    - -`, + `, }) export class ReviewDialog {} ``` @@ -68,6 +63,9 @@ For example: For full details of these and other tools, see the [Angular CDK accessibility overview](https://material.angular.dev/cdk/a11y/overview). +For custom-styled components that need reusable WAI-ARIA interaction patterns, [Angular Aria](guide/aria/overview) provides headless directives for patterns such as accordion, combobox, listbox, menu, tabs, and toolbar. +These directives handle keyboard interaction, ARIA attributes, focus management, and screen reader support while letting you provide the HTML structure and styling for your application. + ### Augmenting native elements Native HTML elements capture several standard interaction patterns that are important to accessibility. @@ -101,7 +99,7 @@ The following example shows how to make a progress bar accessible by using host path="adev/src/content/examples/accessibility/src/app/progress-bar.component.ts" language="ts" linenums - highlight="[12, 20]"/> + highlight="[12, 19]"/> - - Home - - - About - - - Shop - + Home + About + Shop ``` diff --git a/adev-es/src/content/best-practices/error-handling.en.md b/adev-es/src/content/best-practices/error-handling.en.md index c4cd17a..8f88da5 100644 --- a/adev-es/src/content/best-practices/error-handling.en.md +++ b/adev-es/src/content/best-practices/error-handling.en.md @@ -37,10 +37,9 @@ export class GlobalErrorHandler implements ErrorHandler { description: `Screen: ${url} | ${errorMessage}`, }); - console.error(GlobalErrorHandler.name, { error }); + console.error(GlobalErrorHandler.name, {error}); } } - ``` ### `TestBed` rethrows errors by default @@ -53,7 +52,7 @@ Errors that are caught neither by the application code nor by the framework's ap ### Client-side rendering -Adding [`provideBrowserGlobalErrorListeners()`](/api/core/provideBrowserGlobalErrorListeners) to the [ApplicationConfig](guide/di/dependency-injection#at-the-application-root-level-using-applicationconfig) adds the `'error'` and `'unhandledrejection'` listeners to the browser window and forwards those errors to `ErrorHandler`. The Angular CLI generates new applications with this provider by default. The Angular team recommends handling these global errors for most applications, either with the framework's built-in listeners or with your own custom listeners. If you provide custom listeners, you can remove `provideBrowserGlobalErrorListeners`. +Adding [`provideBrowserGlobalErrorListeners()`](/api/core/provideBrowserGlobalErrorListeners) to the [ApplicationConfig](guide/di/defining-dependency-providers#application-bootstrap) adds the `'error'` and `'unhandledrejection'` listeners to the browser window and forwards those errors to `ErrorHandler`. The Angular CLI generates new applications with this provider by default. The Angular team recommends handling these global errors for most applications, either with the framework's built-in listeners or with your own custom listeners. If you provide custom listeners, you can remove `provideBrowserGlobalErrorListeners`. ### Server-side and hybrid rendering diff --git a/adev-es/src/content/best-practices/performance/overview.md b/adev-es/src/content/best-practices/performance/overview.md new file mode 100644 index 0000000..0754c74 --- /dev/null +++ b/adev-es/src/content/best-practices/performance/overview.md @@ -0,0 +1,44 @@ +# Performance + +Angular includes many optimizations out of the box, but as applications grow, you may need to fine-tune both how quickly your app loads and how responsive it feels during use. These guides cover the tools and techniques Angular provides to help you build fast applications. + +## Loading performance + +Loading performance determines how quickly your application becomes visible and interactive. Slow loading directly impacts [Core Web Vitals](https://web.dev/vitals/) like Largest Contentful Paint (LCP) and Time to First Byte (TTFB). + +| Technique | What it does | When to use it | +| :------------------------------------------------------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :-------------------------------------------------------------------------------------------- | +| [Lazy-loaded routes](best-practices/performance/lazy-loaded-routes#lazily-loaded-components-and-routes) | Defers loading route components until navigation, reducing the initial bundle size | Applications with multiple routes where not all are needed on initial load | +| [Deferred loading with `@defer`](best-practices/performance/defer) | Splits components into separate bundles that load on demand | Components not visible on initial render, heavy third-party libraries, below-the-fold content | +| [Lazy loading services with `injectAsync`](guide/di/lazy-loading-services) | Splits rarely used services into separate chunks and loads them on demand | Services backed by large libraries or infrequently used features | +| [Image optimization](best-practices/performance/image-optimization) | Prioritizes LCP images, lazy loads others, generates responsive `srcset` attributes | Any application that displays images | +| [Server-side rendering](best-practices/performance/ssr) | Renders pages on the server for faster first paint and better SEO, with [hydration](guide/hydration) to restore interactivity and [incremental hydration](guide/incremental-hydration) to defer hydrating sections until needed | Content-heavy applications, pages that need search engine indexing | + +## Runtime performance + +Runtime performance determines how responsive your application feels after it loads. Angular's change detection system keeps the DOM in sync with your data, and optimizing how and when it runs is the primary lever for improving runtime performance. + +| Technique | What it does | When to use it | +| :-------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------ | +| [Zoneless change detection](guide/zoneless) | Removes ZoneJS overhead and triggers change detection only when signals or events indicate a change | New applications (default in Angular v21+), or existing applications ready to migrate | +| [Slow computations](best-practices/slow-computations) | Identifies and optimizes expensive template expressions and lifecycle hooks | Profiling reveals specific components causing slow change detection cycles | +| [Skipping component subtrees](best-practices/skipping-subtrees) | Uses `OnPush` change detection to skip unchanged component trees | Applications that need finer control over change detection | +| [Zone pollution](best-practices/zone-pollution) | Prevents unnecessary change detection caused by third-party libraries or timers | Zone-based applications where profiling reveals excessive change detection cycles | + +## Measuring performance + +Identifying what to optimize is just as important as knowing how to optimize it. Angular integrates with browser developer tools to help you find bottlenecks. + +| Tool | What it does | +| :------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [Chrome DevTools profiling](best-practices/profiling-with-chrome-devtools) | Records Angular-specific performance data alongside browser profiling, with color-coded flame charts that show component rendering, change detection cycles, and lifecycle hooks | +| [Angular DevTools](tools/devtools) | A browser extension that provides a component tree inspector and a profiler for visualizing change detection cycles | + +## What to optimize first + +If you are unsure where to start, profile your application first using the [Chrome DevTools Angular track](best-practices/profiling-with-chrome-devtools) to identify specific bottlenecks. + +As a general starting point: + +- **Slow initial load** — Use [`@defer`](best-practices/performance/defer) to split large components out of the main bundle, [`NgOptimizedImage`](best-practices/performance/image-optimization) to prioritize above-the-fold images, and [server-side rendering](best-practices/performance/ssr) to deliver content faster. +- **Slow interactions after load** — Check whether [zoneless change detection](guide/zoneless) is enabled, look for [slow computations](best-practices/slow-computations) in templates or lifecycle hooks, and consider [`OnPush`](best-practices/skipping-subtrees) to reduce unnecessary change detection. diff --git a/adev-es/src/content/best-practices/runtime-performance/profiling-with-chrome-devtools.en.md b/adev-es/src/content/best-practices/runtime-performance/profiling-with-chrome-devtools.en.md index c78d6c2..fa21d36 100644 --- a/adev-es/src/content/best-practices/runtime-performance/profiling-with-chrome-devtools.en.md +++ b/adev-es/src/content/best-practices/runtime-performance/profiling-with-chrome-devtools.en.md @@ -25,18 +25,17 @@ You can use the Angular track to better understand how your code runs in the bro You can enable Angular profiling in one of two ways: -1. Run `ng.enableProfiling()` in Chrome's console panel, or -1. Include a call to `enableProfiling()` in your application startup code (imported from `@angular/core`). +1. Run [`ng.enableProfiling()`](api/core/enableProfiling) in Chrome's console panel, or +1. Include a call to [`enableProfiling()`](api/core/enableProfiling) in your application startup code (imported from `@angular/core`). -NOTE: -Angular profiling works exclusively in development mode. +NOTE: Angular profiling works exclusively in development mode. Here is an example of how you can enable the integration in the application bootstrap to capture all possible events: ```ts -import { enableProfiling } from '@angular/core'; -import { bootstrapApplication } from '@angular/platform-browser'; -import { MyApp } from './my-app'; +import {enableProfiling} from '@angular/core'; +import {bootstrapApplication} from '@angular/platform-browser'; +import {MyApp} from './my-app'; // Turn on profiling *before* bootstrapping your application // in order to capture all of the code run on start-up. @@ -52,6 +51,18 @@ Use the **Record** button in the Chrome DevTools performance panel: See the [Chrome DevTools documentation](https://developer.chrome.com/docs/devtools/performance#record) for more details on recording profiles. +## Open a component in Angular DevTools + +After recording a profile, select a component event in the **Angular** track. +The **Summary** tab can include a **Component** link that uses the `angular-devtools://component/...` URL scheme. + +Chrome DevTools Performance panel showing an Angular custom track with a selected _MainComponent event. The Summary tab displays a Component link that uses the angular-devtools://component URL scheme. + +Click the link to open Angular DevTools and select the matching component in the **Components** tab. +This helps you move from a browser-level profile to the component state and metadata for a selected event. + +NOTE: Opening component links requires Angular DevTools for Chrome and Chrome's experimental `chrome://flags/#enable-devtools-deep-link-via-extensibility-api` flag. + ## Interpreting a recorded profile You can use the "Angular" custom track to quickly identify and diagnose performance issues. The following sections describe some common profiling scenarios. diff --git a/adev-es/src/content/best-practices/runtime-performance/profiling-with-chrome-devtools.md b/adev-es/src/content/best-practices/runtime-performance/profiling-with-chrome-devtools.md index 3aeb46a..524d835 100644 --- a/adev-es/src/content/best-practices/runtime-performance/profiling-with-chrome-devtools.md +++ b/adev-es/src/content/best-practices/runtime-performance/profiling-with-chrome-devtools.md @@ -52,6 +52,18 @@ Usa el botón **Record** en el panel de rendimiento de Chrome DevTools: Consulta la [documentación de Chrome DevTools](https://developer.chrome.com/docs/devtools/performance#record) para más detalles sobre la grabación de perfiles. +## Abrir un componente en Angular DevTools + +Después de grabar un perfil, selecciona un evento de componente en la pista **Angular**. +La pestaña **Summary** puede incluir un enlace de **Component** que usa el esquema de URL `angular-devtools://component/...`. + +Panel de rendimiento de Chrome DevTools mostrando una pista personalizada de Angular con un evento _MainComponent seleccionado. La pestaña Summary muestra un enlace de Component que usa el esquema de URL angular-devtools://component. + +Haz clic en el enlace para abrir Angular DevTools y seleccionar el componente correspondiente en la pestaña **Components**. +Esto te ayuda a pasar de un perfil a nivel del navegador al estado y metadatos del componente para un evento seleccionado. + +NOTA: Abrir enlaces de componentes requiere Angular DevTools para Chrome y la bandera experimental `chrome://flags/#enable-devtools-deep-link-via-extensibility-api` de Chrome. + ## Interpretando un perfil grabado Puedes usar la pista personalizada "Angular" para identificar y diagnosticar rápidamente problemas de rendimiento. Las siguientes secciones describen algunos escenarios comunes de perfilado. diff --git a/adev-es/src/content/best-practices/runtime-performance/skipping-subtrees.en.md b/adev-es/src/content/best-practices/runtime-performance/skipping-subtrees.en.md index 39313fb..405836d 100644 --- a/adev-es/src/content/best-practices/runtime-performance/skipping-subtrees.en.md +++ b/adev-es/src/content/best-practices/runtime-performance/skipping-subtrees.en.md @@ -4,32 +4,20 @@ JavaScript, by default, uses mutable data structures that you can reference from Change detection is sufficiently fast for most applications. However, when an application has an especially large component tree, running change detection across the whole application can cause performance issues. You can address this by configuring change detection to only run on a subset of the component tree. -If you are confident that a part of the application is not affected by a state change, you can use [OnPush](/api/core/ChangeDetectionStrategy) to skip change detection in an entire component subtree. - ## Using `OnPush` -OnPush change detection instructs Angular to run change detection for a component subtree **only** when: +OnPush is the default change detection strategy in Angular (since v22). It instructs Angular to run change detection for a component subtree **only** when: - The root component of the subtree receives new inputs as the result of a template binding. Angular compares the current and past value of the input with `==`. - Angular handles an event _(for example using event binding, output binding, or `@HostListener` )_ in the subtree's root component or any of its children whether they are using OnPush change detection or not. -You can set the change detection strategy of a component to `OnPush` in the `@Component` decorator: - -```ts -import { ChangeDetectionStrategy, Component } from '@angular/core'; -@Component({ - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class MyComponent {} -``` - ## Common change detection scenarios This section examines several common change detection scenarios to illustrate Angular's behavior. -### An event is handled by a component with default change detection +### An event is handled by a component with `Eager` change detection -If Angular handles an event within a component without `OnPush` strategy, the framework executes change detection on the entire component tree. Angular will skip descendant component subtrees with roots using `OnPush`, which have not received new inputs. +If Angular handles an event within a component with the `Eager` strategy, the framework executes change detection on the entire component tree. Angular will skip descendant component subtrees with roots using `OnPush`, which have not received new inputs. As an example, if we set the change detection strategy of `MainComponent` to `OnPush` and the user interacts with a component outside the subtree with root `MainComponent`, Angular will check all the pink components from the diagram below (`AppComponent`, `HeaderComponent`, `SearchComponent`, `ButtonComponent`) unless `MainComponent` receives new inputs: diff --git a/adev-es/src/content/best-practices/runtime-performance/slow-computations.en.md b/adev-es/src/content/best-practices/runtime-performance/slow-computations.en.md index 744080e..b135069 100644 --- a/adev-es/src/content/best-practices/runtime-performance/slow-computations.en.md +++ b/adev-es/src/content/best-practices/runtime-performance/slow-computations.en.md @@ -19,7 +19,7 @@ For example, in the preceding screenshot, the second recorded change detection c Here are several techniques to remove slow computations: - **Optimizing the underlying algorithm**. This is the recommended approach. If you can speed up the algorithm that is causing the problem, you can speed up the entire change detection mechanism. -- **Caching using pure pipes**. You can move the heavy computation to a pure [pipe](guide/pipes). Angular reevaluates a pure pipe only if it detects that its inputs have changed, compared to the previous time Angular called it. +- **Caching using pure pipes**. You can move the heavy computation to a pure [pipe](guide/templates/pipes). Angular reevaluates a pure pipe only if it detects that its inputs have changed, compared to the previous time Angular called it. - **Using memoization**. [Memoization](https://en.wikipedia.org/wiki/Memoization) is a similar technique to pure pipes, with the difference that pure pipes preserve only the last result from the computation where memoization could store multiple results. - **Avoid repaints/reflows in lifecycle hooks**. Certain [operations](https://web.dev/avoid-large-complex-layouts-and-layout-thrashing/) cause the browser to either synchronously recalculate the layout of the page or re-render it. Since reflows and repaints are generally slow, you want to avoid performing them in every change detection cycle. diff --git a/adev-es/src/content/best-practices/runtime-performance/zone-pollution.en.md b/adev-es/src/content/best-practices/runtime-performance/zone-pollution.en.md index 37cdf73..38dfda0 100644 --- a/adev-es/src/content/best-practices/runtime-performance/zone-pollution.en.md +++ b/adev-es/src/content/best-practices/runtime-performance/zone-pollution.en.md @@ -21,38 +21,38 @@ In the image above, there is a series of change detection calls triggered by eve In such cases, you can instruct Angular to avoid calling change detection for tasks scheduled by a given piece of code using [NgZone](/api/core/NgZone). - -import { Component, NgZone, OnInit } from '@angular/core'; +```ts {header:"Run outside of the Zone" , linenums} +import { Component, NgZone, OnInit, inject } from '@angular/core'; @Component(...) class AppComponent implements OnInit { -private ngZone = inject(NgZone); + private ngZone = inject(NgZone); -ngOnInit() { -this.ngZone.runOutsideAngular(() => setInterval(pollForUpdates), 500); + ngOnInit() { + this.ngZone.runOutsideAngular(() => setInterval(pollForUpdates, 500)); + } } -} - +``` The preceding snippet instructs Angular to call `setInterval` outside the Angular Zone and skip running change detection after `pollForUpdates` runs. Third-party libraries commonly trigger unnecessary change detection cycles when their APIs are invoked within the Angular zone. This phenomenon particularly affects libraries that set up event listeners or initiate other tasks (such as timers, XHR requests, etc.). Avoid these extra cycles by calling library APIs outside the Angular zone: - -import { Component, NgZone, OnInit } from '@angular/core'; +```ts {header:"Move the plot initialization outside of the Zone" , linenums} +import { Component, NgZone, OnInit, inject } from '@angular/core'; import * as Plotly from 'plotly.js-dist-min'; @Component(...) class AppComponent implements OnInit { -private ngZone = inject(NgZone); + private ngZone = inject(NgZone); -ngOnInit() { -this.ngZone.runOutsideAngular(() => { -Plotly.newPlot('chart', data); -}); -} + ngOnInit() { + this.ngZone.runOutsideAngular(() => { + Plotly.newPlot('chart', data); + }); + } } - +``` Running `Plotly.newPlot('chart', data);` within `runOutsideAngular` instructs the framework that it shouldn’t run change detection after the execution of tasks scheduled by the initialization logic. @@ -60,24 +60,24 @@ For example, if `Plotly.newPlot('chart', data)` adds event listeners to a DOM el But sometimes, you may need to listen to events dispatched by third-party APIs. In such cases, it's important to remember that those event listeners will also execute outside of the Angular zone if the initialization logic was done there: - -import { Component, NgZone, OnInit, output } from '@angular/core'; +```ts {header:"Check whether the handler is called outside of the Zone" , linenums} +import { Component, NgZone, OnInit, output, inject } from '@angular/core'; import * as Plotly from 'plotly.js-dist-min'; @Component(...) class AppComponent implements OnInit { -private ngZone = inject(NgZone); + private ngZone = inject(NgZone); -plotlyClick = output(); + plotlyClick = output(); -ngOnInit() { -this.ngZone.runOutsideAngular(() => { -this.createPlotly(); -}); -} + ngOnInit() { + this.ngZone.runOutsideAngular(() => { + this.createPlotly(); + }); + } -private async createPlotly() { -const plotly = await Plotly.newPlot('chart', data); + private async createPlotly() { + const plotly = await Plotly.newPlot('chart', data); plotly.on('plotly_click', (event: Plotly.PlotMouseEvent) => { // This handler will be called outside of the Angular zone because @@ -86,40 +86,38 @@ const plotly = await Plotly.newPlot('chart', data); console.log(NgZone.isInAngularZone()); this.plotlyClick.emit(event); }); - + } } -} - +``` If you need to dispatch events to parent components and execute specific view update logic, you should consider re-entering the Angular zone to instruct the framework to run change detection or run change detection manually: - -import { Component, NgZone, OnInit, output } from '@angular/core'; +```ts {header:"Re-enter the Angular zone when dispatching event" , linenums} +import { Component, NgZone, OnInit, output, inject } from '@angular/core'; import * as Plotly from 'plotly.js-dist-min'; @Component(...) class AppComponent implements OnInit { -private ngZone = inject(NgZone); + private ngZone = inject(NgZone); -plotlyClick = output(); + plotlyClick = output(); -ngOnInit() { -this.ngZone.runOutsideAngular(() => { -this.createPlotly(); -}); -} + ngOnInit() { + this.ngZone.runOutsideAngular(() => { + this.createPlotly(); + }); + } -private async createPlotly() { -const plotly = await Plotly.newPlot('chart', data); + private async createPlotly() { + const plotly = await Plotly.newPlot('chart', data); plotly.on('plotly_click', (event: Plotly.PlotMouseEvent) => { this.ngZone.run(() => { this.plotlyClick.emit(event); }); }); - -} + } } - +``` The scenario of dispatching events outside of the Angular zone may also arise. It's important to remember that triggering change detection (for example, manually) may result in the creation/update of views outside of the Angular zone. diff --git a/adev-es/src/content/best-practices/style-guide.en.md b/adev-es/src/content/best-practices/style-guide.en.md index 5cffdd3..8c12a9a 100644 --- a/adev-es/src/content/best-practices/style-guide.en.md +++ b/adev-es/src/content/best-practices/style-guide.en.md @@ -110,11 +110,11 @@ When in doubt, go with the approach that leads to smaller files. ### Prefer the `inject` function over constructor parameter injection -Prefer using the `inject` function over injecting constructor parameters. The `inject` function works the same way as constructor parameter injection, but offers several style advantages: +Prefer using the [`inject`](/api/core/inject) function over injecting constructor parameters. The [`inject`](/api/core/inject) function works the same way as constructor parameter injection, but offers several style advantages: -- `inject` is generally more readable, especially when a class injects many dependencies. +- [`inject`](/api/core/inject) is generally more readable, especially when a class injects many dependencies. - It's more syntactically straightforward to add comments to injected dependencies -- `inject` offers better type inference. +- [`inject`](/api/core/inject) offers better type inference. - When targeting ES2022+ with [`useDefineForClassFields`](https://www.typescriptlang.org/tsconfig/#useDefineForClassFields), you can avoid separating field declaration and initialization when fields read on injected dependencies. [You can refactor existing code to `inject` with an automatic tool](reference/migrations/inject-function). @@ -196,7 +196,7 @@ properties initialized by `input`, `model`, `output`, and queries. The readonly ensures that the value set by Angular is not overwritten. ```ts -@Component({/* ... */}) +@Component(/* ... */) export class UserProfile { readonly userId = input(); readonly userSaved = output(); @@ -208,7 +208,7 @@ For components and directives that use the decorator-based `@Input`, `@Output`, advice applies to output properties and queries, but not input properties. ```ts -@Component({/* ... */}) +@Component(/* ... */) export class UserProfile { @Output() readonly userSaved = new EventEmitter(); @ViewChildren(PaymentMethod) readonly paymentMethods?: QueryList; @@ -219,18 +219,21 @@ export class UserProfile { Prefer `class` and `style` bindings over using the [`NgClass`](/api/common/NgClass) and [`NgStyle`](/api/common/NgStyle) directives. -```html - +```html {prefer}
    -
    - -
    -
    - +
    + +
    +
    +
    +
    +
    +``` - +```html {avoid}
    -
    +
    +
    ``` Both `class` and `style` bindings use a more straightforward syntax that aligns closely with @@ -246,11 +249,11 @@ For more details, refer to the [bindings guide](/guide/templates/binding#css-cla Prefer naming event handlers for the action they perform rather than for the triggering event: -```html - +```html {prefer} +``` - +```html {avoid} ``` @@ -268,8 +271,7 @@ single well-named handler. In these cases, it's fine to fall back to a name like then delegate to more specific behaviors based on the event details: ```ts - -@Component({/* ... */}) +@Component(/* ... */) class RichText { handleKeydown(event: KeyboardEvent) { if (event.ctrlKey) { @@ -278,7 +280,7 @@ class RichText { } else if (event.key === 'I') { this.activateItalic(); } -// ... + // ... } } } @@ -291,14 +293,14 @@ well-named methods to contain that logic and then _call those methods_ in your l Lifecycle hook names describe _when_ they run, meaning that the code inside doesn't have a meaningful name that describes what the code inside is doing. -```typescript -// PREFER +```ts {prefer} ngOnInit() { this.startLogging(); this.runBackgroundTask(); } +``` -// AVOID +```ts {avoid} ngOnInit() { this.logger.setMode('info'); this.logger.monitorErrors(); @@ -314,10 +316,11 @@ your class, import and `implement` these interfaces to ensure that the methods a ```ts import {Component, OnInit} from '@angular/core'; -@Component({/* ... */}) +@Component(/* ... */) export class UserProfile implements OnInit { - // The `OnInit` interface ensures this method is named correctly. - ngOnInit() { /* ... */ } + ngOnInit() { + /* ... */ + } } ``` diff --git a/adev-es/src/content/best-practices/update.en.md b/adev-es/src/content/best-practices/update.en.md index 5729714..1a7cd0d 100644 --- a/adev-es/src/content/best-practices/update.en.md +++ b/adev-es/src/content/best-practices/update.en.md @@ -6,9 +6,9 @@ Keeping your Angular application up-to-date enables you to take advantage of lea This document contains information and resources to help you keep your Angular applications and libraries up-to-date. -For information about our versioning policy and practices —including support and deprecation practices, as well as the release schedule— see [Angular versioning and releases](reference/releases 'Angular versioning and releases'). +For information about our versioning policy and practices — including support and deprecation practices, as well as the release schedule — see [Angular versioning and releases](reference/releases 'Angular versioning and releases'). -HELPFUL: If you are currently using AngularJS, see [Upgrading from AngularJS](https://angular.io/guide/upgrade 'Upgrading from Angular JS'). +HELPFUL: If you are currently using AngularJS, see [Upgrading from AngularJS](https://angular.io/guide/upgrade 'Upgrading from AngularJS'). _AngularJS_ is the name for all v1.x versions of Angular. ## Getting notified of new releases @@ -30,7 +30,7 @@ To check your application's version of Angular use the `ng version` command from The most recent stable released version of Angular appears [on npm](https://www.npmjs.com/package/@angular/core 'Angular on npm') under "Version." For example, `16.2.4`. You can also find the most current version of Angular by using the CLI command [`ng update`](cli/update). -By default, [`ng update`](cli/update)(without additional arguments) lists the updates that are available to you. +By default, [`ng update`](cli/update) (without additional arguments) lists the updates that are available to you. ## Updating your environment and apps @@ -43,7 +43,7 @@ It also includes troubleshooting information and any recommended manual changes For simple updates, the CLI command [`ng update`](cli/update) is all you need. Without additional arguments, [`ng update`](cli/update) lists the updates that are available to you and provides recommended steps to update your application to the most current version. -[Angular Versioning and Releases](reference/releases#versioning 'Angular Release Practices, Versioning') describes the level of change that you can expect based on a release's version number. +[Angular Versioning and Releases](reference/releases#angular-versioning 'Angular Release Practices, Versioning') describes the level of change that you can expect based on a release's version number. It also describes supported update paths. ## Resource summary diff --git a/adev-es/src/content/best-practices/update.md b/adev-es/src/content/best-practices/update.md index 1166173..f958a26 100644 --- a/adev-es/src/content/best-practices/update.md +++ b/adev-es/src/content/best-practices/update.md @@ -43,7 +43,7 @@ También incluye información de solución de problemas y cualquier cambio manua Para actualizaciones simples, el comando CLI [`ng update`](cli/update) es todo lo que necesitas. Sin argumentos adicionales, [`ng update`](cli/update) lista las actualizaciones que están disponibles para ti y proporciona pasos recomendados para actualizar tu aplicación a la versión más actual. -[Versionado y Lanzamientos de Angular](reference/releases#versioning 'Prácticas de Lanzamiento de Angular, Versionado') describe el nivel de cambio que puedes esperar basado en el número de versión de un lanzamiento. +[Versionado y Lanzamientos de Angular](reference/releases 'Prácticas de Lanzamiento de Angular, Versionado') describe el nivel de cambio que puedes esperar basado en el número de versión de un lanzamiento. También describe las rutas de actualización soportadas. ## Resumen de recursos diff --git a/adev-es/src/content/ecosystem/custom-build-pipeline.en.md b/adev-es/src/content/ecosystem/custom-build-pipeline.en.md index 6670076..d2bab6e 100644 --- a/adev-es/src/content/ecosystem/custom-build-pipeline.en.md +++ b/adev-es/src/content/ecosystem/custom-build-pipeline.en.md @@ -10,7 +10,7 @@ There are some niche use cases when you may want to maintain a custom build pipe - You have an existing app using a different toolchain and you’d like to add Angular to it - You’re strongly coupled to [module federation](https://module-federation.io/) and unable to adopt bundler-agnostic [native federation](https://www.npmjs.com/package/@angular-architects/native-federation) -- You’d like to create an short-lived experiment using your favorite build tool +- You’d like to create a short-lived experiment using your favorite build tool ## What are the options? diff --git a/adev-es/src/content/ecosystem/rxjs-interop/output-interop.en.md b/adev-es/src/content/ecosystem/rxjs-interop/output-interop.en.md index bd9faff..3a5fef2 100644 --- a/adev-es/src/content/ecosystem/rxjs-interop/output-interop.en.md +++ b/adev-es/src/content/ecosystem/rxjs-interop/output-interop.en.md @@ -8,16 +8,16 @@ The `@angular/rxjs-interop` package offers two APIs related to component and dir The `outputFromObservable` lets you create a component or directive output that emits based on an RxJS observable: -```ts {highlight:[9]} +```ts {highlight:[11]} import {Directive} from '@angular/core'; import {outputFromObservable} from '@angular/core/rxjs-interop'; -@Directive({/*...*/}) +@Directive(/* ... */) class Draggable { - pointerMoves$: Observable = listenToPointerMoves(); + pointerMoves$: Observable = listenToPointerMoves(); - // Whenever `pointerMoves$` emits, the `pointerMove` event fires. - pointerMove = outputFromObservable(this.pointerMoves$); + // Whenever `pointerMoves$` emits, the `pointerMove` event fires. + pointerMove = outputFromObservable(this.pointerMoves$); } ``` @@ -35,7 +35,7 @@ The `outputToObservable` function lets you create an RxJS observable from a comp import {outputToObservable} from '@angular/core/rxjs-interop'; @Component(/*...*/) - class CustomSlider { +class CustomSlider { valueChange = output(); } diff --git a/adev-es/src/content/ecosystem/rxjs-interop/signals-interop.en.md b/adev-es/src/content/ecosystem/rxjs-interop/signals-interop.en.md index 2ab060e..3e8fd13 100644 --- a/adev-es/src/content/ecosystem/rxjs-interop/signals-interop.en.md +++ b/adev-es/src/content/ecosystem/rxjs-interop/signals-interop.en.md @@ -7,10 +7,10 @@ The `@angular/core/rxjs-interop` package offers APIs that help you integrate RxJ Use the `toSignal` function to create a signal which tracks the value of an Observable. It behaves similarly to the `async` pipe in templates, but is more flexible and can be used anywhere in an application. ```angular-ts -import { Component } from '@angular/core'; -import { AsyncPipe } from '@angular/common'; -import { interval } from 'rxjs'; -import { toSignal } from '@angular/core/rxjs-interop'; +import {Component} from '@angular/core'; +import {AsyncPipe} from '@angular/common'; +import {interval} from 'rxjs'; +import {toSignal} from '@angular/core/rxjs-interop'; @Component({ template: `{{ counter() }}`, @@ -47,7 +47,7 @@ If you don't provide an `initialValue`, the resulting signal will return `undefi Some Observables are guaranteed to emit synchronously, such as `BehaviorSubject`. In those cases, you can specify the `requireSync: true` option. -When `requiredSync` is `true`, `toSignal` enforces that the Observable emits synchronously on subscription. This guarantees that the signal always has a value, and no `undefined` type or initial value is required. +When `requireSync` is `true`, `toSignal` enforces that the Observable emits synchronously on subscription. This guarantees that the signal always has a value, and no `undefined` type or initial value is required. ### `manualCleanup` @@ -62,20 +62,20 @@ Some observables may emit values that are **equals** even though they differ by When two emitted values are considered equal, the resulting signal **does not update**. This prevents redundant computations, DOM updates, or effects from re-running unnecessarily. ```ts -import { Component } from '@angular/core'; -import { toSignal } from '@angular/core/rxjs-interop'; -import { interval, map } from 'rxjs'; +import {Component} from '@angular/core'; +import {toSignal} from '@angular/core/rxjs-interop'; +import {interval, map} from 'rxjs'; @Component(/* ... */) export class EqualExample { temperature$ = interval(1000).pipe( - map(() => ({ temperature: Math.floor(Math.random() * 3) + 20 }) ) // 20, 21, or 22 randomly + map(() => ({temperature: Math.floor(Math.random() * 3) + 20})), // 20, 21, or 22 randomly ); // Only update if the temperature changes temperature = toSignal(this.temperature$, { - initialValue: { temperature : 20 }, - equal: (prev, curr) => prev.temperature === curr.temperature + initialValue: {temperature: 20}, + equal: (prev, curr) => prev.temperature === curr.temperature, }); } ``` @@ -91,17 +91,15 @@ If an Observable used in `toSignal` completes, the signal continues to return th Use the `toObservable` utility to create an `Observable` which tracks the value of a signal. The signal's value is monitored with an `effect` which emits the value to the Observable when it changes. ```ts -import { Component, signal } from '@angular/core'; -import { toObservable } from '@angular/core/rxjs-interop'; +import {Component, signal} from '@angular/core'; +import {toObservable} from '@angular/core/rxjs-interop'; @Component(/* ... */) export class SearchResults { query: Signal = inject(QueryService).query; query$ = toObservable(this.query); - results$ = this.query$.pipe( - switchMap(query => this.http.get('/search?q=' + query )) - ); + results$ = this.query$.pipe(switchMap((query) => this.http.get('/search?q=' + query))); } ``` @@ -119,7 +117,7 @@ Unlike Observables, signals never provide a synchronous notification of changes. ```ts const obs$ = toObservable(mySignal); -obs$.subscribe(value => console.log(value)); +obs$.subscribe((value) => console.log(value)); mySignal.set(1); mySignal.set(2); @@ -130,8 +128,6 @@ 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 @@ -146,7 +142,7 @@ export class UserProfile { protected userId = input(); private userResource = rxResource({ - params: () => ({ userId: this.userId() }), + params: () => ({userId: this.userId()}), // The `stream` property expects a factory function that returns // a data stream as an RxJS Observable. diff --git a/adev-es/src/content/ecosystem/rxjs-interop/take-until-destroyed.en.md b/adev-es/src/content/ecosystem/rxjs-interop/take-until-destroyed.en.md index 19e8a42..fc94b13 100644 --- a/adev-es/src/content/ecosystem/rxjs-interop/take-until-destroyed.en.md +++ b/adev-es/src/content/ecosystem/rxjs-interop/take-until-destroyed.en.md @@ -18,7 +18,7 @@ export class UserProfile { // 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 => { + messages.pipe(takeUntilDestroyed()).subscribe((message) => { this.popup.show(message); }); } @@ -38,7 +38,7 @@ export class UserProfile { // 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 => { + messages.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((message) => { this.popup.show(message); }); } diff --git a/adev-es/src/content/ecosystem/service-workers/app-shell.en.md b/adev-es/src/content/ecosystem/service-workers/app-shell.en.md index 31e86a0..7a7d9af 100644 --- a/adev-es/src/content/ecosystem/service-workers/app-shell.en.md +++ b/adev-es/src/content/ecosystem/service-workers/app-shell.en.md @@ -26,7 +26,7 @@ For more information about this command, see [App shell command](cli/generate/ap The command updates the application code and adds extra files to the project structure. - +```text src ├── app │ ├── app.config.server.ts # server application configuration @@ -36,7 +36,7 @@ src │ ├── app-shell.component.spec.ts │ └── app-shell.component.ts └── main.server.ts # main server application bootstrapping - +``` diff --git a/adev-es/src/content/ecosystem/service-workers/communications.en.md b/adev-es/src/content/ecosystem/service-workers/communications.en.md index 71e9ffe..22822c6 100644 --- a/adev-es/src/content/ecosystem/service-workers/communications.en.md +++ b/adev-es/src/content/ecosystem/service-workers/communications.en.md @@ -24,7 +24,7 @@ The `versionUpdates` is an `Observable` property of `SwUpdate` and emits five ev | `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. | - + ### Checking for updates @@ -56,7 +56,7 @@ Alternatively, you might want to define a different [registration strategy](api/ You can update an existing tab to the latest version by reloading the page as soon as a new version is ready. To avoid disrupting the user's progress, it is generally a good idea to prompt the user and let them confirm that it is OK to reload the page and update to the latest version: - + Calling `activateUpdate()` updates a tab to the latest version without reloading the page, but this could break the application. @@ -96,7 +96,7 @@ That particular application version is broken and there is no way to fix the sta In such cases, the service worker notifies the client by sending an `UnrecoverableStateEvent` event. Subscribe to `SwUpdate#unrecoverable` to be notified and handle these errors. - + ## More on Angular service workers diff --git a/adev-es/src/content/ecosystem/service-workers/config.en.md b/adev-es/src/content/ecosystem/service-workers/config.en.md index 8f13339..d0ea43b 100644 --- a/adev-es/src/content/ecosystem/service-workers/config.en.md +++ b/adev-es/src/content/ecosystem/service-workers/config.en.md @@ -85,7 +85,7 @@ For example, an asset group that matches `/foo.js` should appear before one that Each asset group specifies both a group of resources and a policy that governs them. This policy determines when the resources are fetched and what happens when changes are detected. -Asset groups follow the Typescript interface shown here: +Asset groups follow the TypeScript interface shown here: ```ts interface AssetGroup { @@ -179,7 +179,7 @@ The first data group that matches the requested resource handles the request. It is recommended that you put the more specific data groups higher in the list. For example, a data group that matches `/api/foo.json` should appear before one that matches `/api/*.json`. -Data groups follow this Typescript interface: +Data groups follow this TypeScript interface: ```ts export interface DataGroup { @@ -354,14 +354,12 @@ The URL query is ignored when matching. If the field is omitted, it defaults to: ```ts - [ -'/**', // Include all URLs. -'!/**/*.*', // Exclude URLs to files (containing a file extension in the last segment). -'!/**/*__*', // Exclude URLs containing `__` in the last segment. -'!/**/*__*/**', // Exclude URLs containing `__` in any other segment. -] - + '/**', // Include all URLs. + '!/**/*.*', // Exclude URLs to files (containing a file extension in the last segment). + '!/**/*__*', // Exclude URLs containing `__` in the last segment. + '!/**/*__*/**', // Exclude URLs containing `__` in any other segment. +]; ``` ### `navigationRequestStrategy` @@ -369,7 +367,6 @@ If the field is omitted, it defaults to: This optional property enables you to configure how the service worker handles navigation requests: ```json - { "navigationRequestStrategy": "freshness" } diff --git a/adev-es/src/content/ecosystem/service-workers/config.md b/adev-es/src/content/ecosystem/service-workers/config.md index e69de29..dd3e6ad 100644 --- a/adev-es/src/content/ecosystem/service-workers/config.md +++ b/adev-es/src/content/ecosystem/service-workers/config.md @@ -0,0 +1,384 @@ +# Archivo de configuración del Service Worker + +Este tema describe las propiedades del archivo de configuración del service worker. + +## Modificar la configuración + +El archivo de configuración JSON `ngsw-config.json` especifica qué archivos y URLs de datos debe cachear el service worker de Angular y cómo debe actualizar los archivos y datos cacheados. +El [Angular CLI](tools/cli) procesa este archivo de configuración durante `ng build`. + +Todas las rutas de archivos deben comenzar con `/`, lo que corresponde al directorio de despliegue — generalmente `dist/` en proyectos CLI. + +A menos que se indique lo contrario, los patrones usan un formato glob **limitado\*** que internamente se convertirá en regex: + +| Formatos glob | Detalles | +| :------------ | :-------------------------------------------------------------------------------------------------------------------- | +| `**` | Coincide con 0 o más segmentos de ruta | +| `*` | Coincide con 0 o más caracteres excluyendo `/` | +| `?` | Coincide exactamente con un carácter excluyendo `/` | +| Prefijo `!` | Marca el patrón como negativo, lo que significa que solo se incluyen archivos que no coincidan con el patrón | + + +Ten en cuenta que algunos caracteres con significado especial en una expresión regular no son escapados y el patrón tampoco se envuelve en `^`/`$` en la conversión interna de glob a regex. + +`$` es un carácter especial en regex que coincide con el final de la cadena y no se escapará automáticamente al convertir el patrón glob a una expresión regular. + +Si deseas hacer coincidir literalmente el carácter `$`, debes escaparlo tú mismo (con `\\$`). Por ejemplo, el patrón glob `/foo/bar/$value` resulta en una expresión sin coincidencia posible, porque es imposible tener una cadena que tenga caracteres después de que haya terminado. + +El patrón no se envolverá automáticamente en `^` y `$` al convertirlo a una expresión regular. Por lo tanto, los patrones coincidirán parcialmente con las URLs de solicitud. + +Si deseas que tus patrones coincidan con el principio y/o el final de las URLs, puedes agregar `^`/`$` tú mismo. Por ejemplo, el patrón glob `/foo/bar/*.js` coincidirá con archivos `.js` y `.json`. Si solo deseas coincidir con archivos `.js`, usa `/foo/bar/*.js$`. + + +Patrones de ejemplo: + +| Patrones | Detalles | +| :----------- | :---------------------------------------------- | +| `/**/*.html` | Especifica todos los archivos HTML | +| `/*.html` | Especifica solo los archivos HTML en la raíz | +| `!/**/*.map` | Excluye todos los sourcemaps | + +## Propiedades de configuración del service worker + +Las siguientes secciones describen cada propiedad del archivo de configuración. + +### `appData` + +Esta sección te permite pasar cualquier dato que quieras para describir esta versión particular de la aplicación. +El servicio `SwUpdate` incluye esos datos en las notificaciones de actualización. +Muchas aplicaciones usan esta sección para proporcionar información adicional para mostrar popups de UI, notificando a los usuarios sobre la actualización disponible. + +### `index` + +Especifica el archivo que sirve como página de índice para satisfacer solicitudes de navegación. +Generalmente es `/index.html`. + +### `assetGroups` + +Los _assets_ son recursos que forman parte de la versión de la aplicación que se actualizan junto con la aplicación. +Pueden incluir recursos cargados desde el origen de la página, así como recursos de terceros cargados desde CDNs y otras URLs externas. +Como no todas esas URLs externas pueden conocerse en el momento de la compilación, se pueden hacer coincidir patrones de URL. + +HELPFUL: Para que el service worker maneje recursos cargados desde diferentes orígenes, asegúrate de que [CORS](https://developer.mozilla.org/docs/Web/HTTP/CORS) esté correctamente configurado en el servidor de cada origen. + +Este campo contiene un array de grupos de assets, cada uno de los cuales define un conjunto de recursos de assets y la política por la cual se cachean. + +```ts +{ + "assetGroups": [ + { + … + }, + { + … + } + ] +} +``` + +HELPFUL: Cuando el ServiceWorker maneja una solicitud, verifica los grupos de assets en el orden en que aparecen en `ngsw-config.json`. +El primer grupo de assets que coincide con el recurso solicitado maneja la solicitud. + +Se recomienda colocar los grupos de assets más específicos al principio de la lista. +Por ejemplo, un grupo de assets que coincide con `/foo.js` debería aparecer antes que uno que coincida con `*.js`. + +Cada grupo de assets especifica tanto un grupo de recursos como una política que los rige. +Esta política determina cuándo se obtienen los recursos y qué sucede cuando se detectan cambios. + +Los grupos de assets siguen la interfaz TypeScript mostrada aquí: + +```ts +interface AssetGroup { + name: string; + installMode?: 'prefetch' | 'lazy'; + updateMode?: 'prefetch' | 'lazy'; + resources: { + files?: string[]; + urls?: string[]; + }; + cacheQueryOptions?: { + ignoreSearch?: boolean; + }; +} +``` + +Cada `AssetGroup` se define por las siguientes propiedades del grupo de assets. + +#### `name` + +Un `name` es obligatorio. +Identifica este grupo particular de assets entre versiones de la configuración. + +#### `installMode` + +El `installMode` determina cómo se cachean inicialmente estos recursos. +El `installMode` puede ser uno de dos valores: + +| Valores | Detalles | +| :--------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `prefetch` | Le indica al service worker de Angular que obtenga cada recurso listado mientras cachea la versión actual de la aplicación. Esto consume mucho ancho de banda pero garantiza que los recursos estén disponibles cuando se soliciten, incluso si el navegador está actualmente sin conexión. | +| `lazy` | No cachea ninguno de los recursos de antemano. En cambio, el service worker de Angular solo cachea recursos para los que recibe solicitudes. Este es un modo de caché bajo demanda. Los recursos que nunca se solicitan no se cachean. Esto es útil para cosas como imágenes en diferentes resoluciones, por lo que el service worker solo cachea los assets correctos para la pantalla y orientación específicas. | + +Por defecto es `prefetch`. + +#### `updateMode` + +Para recursos que ya están en la caché, el `updateMode` determina el comportamiento de caché cuando se descubre una nueva versión de la aplicación. +Cualquier recurso en el grupo que haya cambiado desde la versión anterior se actualiza de acuerdo con `updateMode`. + +| Valores | Detalles | +| :--------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `prefetch` | Le indica al service worker que descargue y cachee los recursos modificados de inmediato. | +| `lazy` | Le indica al service worker que no cachee esos recursos. En cambio, los trata como no solicitados y espera hasta que se soliciten nuevamente antes de actualizarlos. Un `updateMode` de `lazy` solo es válido si el `installMode` también es `lazy`. | + +Por defecto es el valor en que está configurado `installMode`. + +#### `resources` + +Esta sección describe los recursos a cachear, divididos en los siguientes grupos: + +| Grupos de recursos | Detalles | +| :----------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `files` | Lista patrones que coinciden con archivos en el directorio de distribución. Pueden ser archivos individuales o patrones tipo glob que coinciden con varios archivos. | +| `urls` | Incluye tanto URLs como patrones de URL que se hacen coincidir en tiempo de ejecución. Estos recursos no se obtienen directamente y no tienen hashes de contenido, pero se cachean según sus encabezados HTTP. Esto es más útil para CDNs como el servicio Google Fonts.
    _(Los patrones glob negativos no son compatibles y `?` se hará coincidir literalmente; es decir, no coincidirá con ningún carácter que no sea `?`.)_ | + +#### `cacheQueryOptions` + +Estas opciones se usan para modificar el comportamiento de coincidencia de las solicitudes. +Se pasan a la función `Cache#match` del navegador. +Consulta [MDN](https://developer.mozilla.org/docs/Web/API/Cache/match) para más detalles. +Actualmente, solo se admiten las siguientes opciones: + +| Opciones | Detalles | +| :------------- | :---------------------------------------------------- | +| `ignoreSearch` | Ignora los parámetros de consulta. Por defecto `false`. | + +### `dataGroups` + +A diferencia de los recursos de assets, las solicitudes de datos no están versionadas junto con la aplicación. +Se cachean según políticas configuradas manualmente que son más útiles para situaciones como solicitudes de API y otras dependencias de datos. + +Este campo contiene un array de grupos de datos, cada uno de los cuales define un conjunto de recursos de datos y la política por la cual se cachean. + +```json +{ + "dataGroups": [ + { + … + }, + { + … + } + ] +} +``` + +HELPFUL: Cuando el ServiceWorker maneja una solicitud, verifica los grupos de datos en el orden en que aparecen en `ngsw-config.json`. +El primer grupo de datos que coincide con el recurso solicitado maneja la solicitud. + +Se recomienda colocar los grupos de datos más específicos al principio de la lista. +Por ejemplo, un grupo de datos que coincide con `/api/foo.json` debería aparecer antes que uno que coincida con `/api/*.json`. + +Los grupos de datos siguen esta interfaz TypeScript: + +```ts +export interface DataGroup { + name: string; + urls: string[]; + version?: number; + cacheConfig: { + maxSize: number; + maxAge: string; + timeout?: string; + refreshAhead?: string; + strategy?: 'freshness' | 'performance'; + }; + cacheQueryOptions?: { + ignoreSearch?: boolean; + }; +} +``` + +Cada `DataGroup` se define por las siguientes propiedades del grupo de datos. + +#### `name` + +Similar a `assetGroups`, cada grupo de datos tiene un `name` que lo identifica de forma única. + +#### `urls` + +Una lista de patrones de URL. +Las URLs que coincidan con estos patrones se cachean de acuerdo con la política de este grupo de datos. +Solo se cachean las solicitudes no mutantes (GET y HEAD). + +- Los patrones glob negativos no son compatibles +- `?` se hace coincidir literalmente; es decir, coincide _solo_ con el carácter `?` + +#### `version` + +Ocasionalmente las APIs cambian sus formatos de manera que no es compatible con versiones anteriores. +Una nueva versión de la aplicación podría no ser compatible con el formato de API anterior y, por lo tanto, podría no ser compatible con los recursos cacheados existentes de esa API. + +`version` proporciona un mecanismo para indicar que los recursos que se están cacheando se han actualizado de forma incompatible con versiones anteriores, y que las entradas de caché antiguas —las de versiones anteriores— deben descartarse. + +`version` es un campo entero y por defecto es `1`. + +#### `cacheConfig` + +Las siguientes propiedades definen la política por la que se cachean las solicitudes coincidentes. + +##### `maxSize` + +El número máximo de entradas, o respuestas, en la caché. + +CRITICAL: Las cachés sin límite pueden crecer de manera ilimitada y eventualmente exceder las cuotas de almacenamiento, resultando en expulsión. + +##### `maxAge` + +El parámetro `maxAge` indica cuánto tiempo se permite que las respuestas permanezcan en la caché antes de considerarse inválidas y ser expulsadas. `maxAge` es una cadena de duración, usando los siguientes sufijos de unidad: + +| Sufijos | Detalles | +| :------ | :------------ | +| `d` | Días | +| `h` | Horas | +| `m` | Minutos | +| `s` | Segundos | +| `u` | Milisegundos | + +Por ejemplo, la cadena `3d12h` cachea contenido por hasta tres días y medio. + +##### `timeout` + +Esta cadena de duración especifica el timeout de red. +El timeout de red es cuánto tiempo espera el service worker de Angular a que la red responda antes de usar una respuesta cacheada, si está configurado para hacerlo. +`timeout` es una cadena de duración, usando los siguientes sufijos de unidad: + +| Sufijos | Detalles | +| :------ | :------------ | +| `d` | Días | +| `h` | Horas | +| `m` | Minutos | +| `s` | Segundos | +| `u` | Milisegundos | + +Por ejemplo, la cadena `5s30u` se traduce en cinco segundos y 30 milisegundos de timeout de red. + +##### `refreshAhead` + +Esta cadena de duración especifica el tiempo previo a la expiración de un recurso cacheado cuando el service worker de Angular debería intentar proactivamente actualizar el recurso desde la red. +La duración `refreshAhead` es una configuración opcional que determina cuánto tiempo antes de la expiración de una respuesta cacheada debe el service worker iniciar una solicitud para actualizar el recurso desde la red. + +| Sufijos | Detalles | +| :------ | :------------ | +| `d` | Días | +| `h` | Horas | +| `m` | Minutos | +| `s` | Segundos | +| `u` | Milisegundos | + +Por ejemplo, la cadena `1h30m` se traduce en una hora y 30 minutos antes del tiempo de expiración. + +##### `strategy` + +El service worker de Angular puede usar cualquiera de dos estrategias de caché para recursos de datos. + +| Estrategias de caché | Detalles | +| :------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `performance` | El valor por defecto, optimiza para respuestas lo más rápidas posible. Si un recurso existe en la caché, se usa la versión cacheada y no se realiza ninguna solicitud de red. Esto permite cierta obsolescencia, dependiendo del `maxAge`, a cambio de un mejor rendimiento. Es adecuado para recursos que no cambian con frecuencia; por ejemplo, imágenes de avatar de usuario. | +| `freshness` | Optimiza la actualidad de los datos, obteniendo preferentemente los datos solicitados de la red. Solo si la red agota el tiempo de espera, según `timeout`, la solicitud recurre a la caché. Esto es útil para recursos que cambian con frecuencia; por ejemplo, saldos de cuentas. | + +HELPFUL: También puedes emular una tercera estrategia, [staleWhileRevalidate](https://developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook/#stale-while-revalidate), que devuelve datos cacheados si están disponibles, pero también obtiene datos frescos de la red en segundo plano para la próxima vez. +Para usar esta estrategia, configura `strategy` como `freshness` y `timeout` como `0u` en `cacheConfig`. + +Esto esencialmente hace lo siguiente: + +1. Intentar obtener de la red primero. +2. Si la solicitud de red no se completa de inmediato, es decir, después de un timeout de 0 ms, ignorar la antigüedad de la caché y recurrir al valor cacheado. +3. Una vez que la solicitud de red se completa, actualizar la caché para solicitudes futuras. +4. Si el recurso no existe en la caché, esperar la solicitud de red de todos modos. + +##### `cacheOpaqueResponses` + +Si el service worker de Angular debe cachear respuestas opacas o no. + +Si no se especifica, el valor por defecto depende de la estrategia configurada del grupo de datos: + +| Estrategias | Detalles | +| :------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Grupos con la estrategia `freshness` | El valor por defecto es `true` y el service worker cachea respuestas opacas. Estos grupos solicitarán los datos cada vez y solo recurrirán a la respuesta cacheada cuando estén sin conexión o en una red lenta. Por lo tanto, no importa si el service worker cachea una respuesta de error. | +| Grupos con la estrategia `performance` | El valor por defecto es `false` y el service worker no cachea respuestas opacas. Estos grupos continuarían devolviendo una respuesta cacheada hasta que `maxAge` expire, incluso si el error se debió a un problema temporal de red o servidor. Por lo tanto, sería problemático que el service worker cacheara una respuesta de error. | + + + +En caso de que no estés familiarizado, una [respuesta opaca](https://fetch.spec.whatwg.org#concept-filtered-response-opaque) es un tipo especial de respuesta que se devuelve al solicitar un recurso que está en un origen diferente que no devuelve encabezados CORS. +Una de las características de una respuesta opaca es que el service worker no puede leer su estado, lo que significa que no puede verificar si la solicitud fue exitosa o no. +Consulta [Introducción a `fetch()`](https://developers.google.com/web/updates/2015/03/introduction-to-fetch#response_types) para más detalles. + +Si no puedes implementar CORS — por ejemplo, si no controlas el origen — es preferible usar la estrategia `freshness` para recursos que resulten en respuestas opacas. + + + +#### `cacheQueryOptions` + +Consulta [assetGroups](#assetgroups) para más detalles. + +### `navigationUrls` + +Esta sección opcional te permite especificar una lista personalizada de URLs que serán redirigidas al archivo de índice. + +#### Manejo de solicitudes de navegación + +El ServiceWorker redirige las solicitudes de navegación que no coinciden con ningún grupo de `asset` o `data` al [archivo de índice](#index) especificado. +Una solicitud se considera una solicitud de navegación si: + +- Su [método](https://developer.mozilla.org/docs/Web/API/Request/method) es `GET` +- Su [modo](https://developer.mozilla.org/docs/Web/API/Request/mode) es `navigation` +- Acepta una respuesta `text/html` según lo determinado por el valor del encabezado `Accept` +- Su URL coincide con los siguientes criterios: + - La URL no debe contener una extensión de archivo (es decir, un `.`) en el último segmento de ruta + - La URL no debe contener `__` + +HELPFUL: Para configurar si las solicitudes de navegación se envían a través de la red o no, consulta las secciones [navigationRequestStrategy](#navigationrequeststrategy) y [applicationMaxAge](#applicationmaxage). + +#### Hacer coincidir URLs de solicitudes de navegación + +Aunque estos criterios predeterminados están bien en la mayoría de los casos, a veces es deseable configurar reglas diferentes. +Por ejemplo, es posible que desees ignorar rutas específicas, como las que no forman parte de la aplicación Angular, y pasarlas al servidor. + +Este campo contiene un array de URLs y patrones de URL [tipo glob](#modificar-la-configuración) que se hacen coincidir en tiempo de ejecución. +Puede contener tanto patrones negativos (es decir, patrones que comienzan con `!`) como patrones y URLs no negativos. + +Solo las solicitudes cuyas URLs coincidan con _cualquiera_ de las URLs/patrones no negativos y _ninguno_ de los negativos se consideran solicitudes de navegación. +La consulta de URL se ignora al hacer la coincidencia. + +Si el campo se omite, por defecto es: + +```ts +[ + '/**', // Incluir todas las URLs. + '!/**/*.*', // Excluir URLs de archivos (que contienen una extensión de archivo en el último segmento). + '!/**/*__*', // Excluir URLs que contienen `__` en el último segmento. + '!/**/*__*/**', // Excluir URLs que contienen `__` en cualquier otro segmento. +]; +``` + +### `navigationRequestStrategy` + +Esta propiedad opcional te permite configurar cómo el service worker maneja las solicitudes de navegación: + +```json +{ + "navigationRequestStrategy": "freshness" +} +``` + +| Valores posibles | Detalles | +| :--------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `'performance'` | La configuración por defecto. Sirve el [archivo de índice](#index) especificado, que generalmente está cacheado. | +| `'freshness'` | Pasa las solicitudes a través de la red y recurre al comportamiento `performance` cuando está sin conexión. Este valor es útil cuando el servidor redirige las solicitudes de navegación a otro lugar usando un código de estado HTTP de redirección `3xx`. Las razones para usar este valor incluyen:
    • Redirigir a un sitio web de autenticación cuando la autenticación no es manejada por la aplicación
    • Redirigir URLs específicas para evitar romper enlaces/marcadores existentes después de un rediseño del sitio web
    • Redirigir a un sitio web diferente, como una página de estado del servidor, mientras una página está temporalmente inactiva
    | + +IMPORTANT: La estrategia `freshness` generalmente resulta en más solicitudes enviadas al servidor, lo que puede aumentar la latencia de respuesta. Se recomienda usar la estrategia de rendimiento por defecto siempre que sea posible. + +### `applicationMaxAge` + +Esta propiedad opcional te permite configurar cuánto tiempo el service worker cacheará cualquier solicitud. Dentro del `maxAge`, los archivos se servirán desde la caché. Más allá de eso, todas las solicitudes solo se servirán desde la red, incluidas las solicitudes de assets y datos. diff --git a/adev-es/src/content/ecosystem/service-workers/custom-service-worker-scripts.en.md b/adev-es/src/content/ecosystem/service-workers/custom-service-worker-scripts.en.md index d5fdcaa..2a5dbae 100644 --- a/adev-es/src/content/ecosystem/service-workers/custom-service-worker-scripts.en.md +++ b/adev-es/src/content/ecosystem/service-workers/custom-service-worker-scripts.en.md @@ -73,8 +73,8 @@ importScripts('./ngsw-worker.js'); 3. Configure the service worker registration to use your custom script: ```ts -import { ApplicationConfig, isDevMode } from '@angular/core'; -import { provideServiceWorker } from '@angular/service-worker'; +import {ApplicationConfig, isDevMode} from '@angular/core'; +import {provideServiceWorker} from '@angular/service-worker'; export const appConfig: ApplicationConfig = { providers: [ diff --git a/adev-es/src/content/ecosystem/service-workers/devops.en.md b/adev-es/src/content/ecosystem/service-workers/devops.en.md index fb3b0ea..498ccea 100644 --- a/adev-es/src/content/ecosystem/service-workers/devops.en.md +++ b/adev-es/src/content/ecosystem/service-workers/devops.en.md @@ -138,7 +138,7 @@ The Angular service worker exposes debugging information under the `ngsw/` virtu Currently, the single exposed URL is `ngsw/state`. Here is an example of this debug page's contents: - +```shell {hideCopy} NGSW Debug Info: @@ -160,17 +160,17 @@ Task queue: Debug log: - +``` #### Driver state The first line indicates the driver state: - +```shell {hideCopy} Driver state: NORMAL ((nominal)) - +``` `NORMAL` indicates that the service worker is operating normally and is not in a degraded state. @@ -190,21 +190,21 @@ The new instance starts in the `NORMAL` mode, regardless of the state of the pre #### Latest manifest hash - +```shell {hideCopy} Latest manifest hash: eea7f5f464f90789b621170af5a569d6be077e5c - +``` This is the SHA1 hash of the most up-to-date version of the application that the service worker knows about. #### Last update check - +```shell {hideCopy} Last update check: never - +``` This indicates the last time the service worker checked for a new version, or update, of the application. `never` indicates that the service worker has never checked for an update. @@ -213,13 +213,13 @@ In this example debug file, the update check is currently scheduled, as explaine #### Version - +```shell {hideCopy} === Version eea7f5f464f90789b621170af5a569d6be077e5c === Clients: 7b79a015-69af-4d3d-9ae6-95ba90c79486, 5bc08295-aaf2-42f3-a4cc-9e4ef9100f65 - +``` In this example, the service worker has one version of the application cached and being used to serve two different tabs. @@ -227,7 +227,7 @@ HELPFUL: This version hash is the "latest manifest hash" listed above. Both clie #### Idle task queue - +```shell {hideCopy} === Idle Task Queue === Last update tick: 1s496u @@ -236,7 +236,7 @@ Task queue: - init post-load (update, cleanup) - +``` The Idle Task Queue is the queue of all pending tasks that happen in the background in the service worker. If there are any tasks in the queue, they are listed with a description. @@ -248,11 +248,11 @@ The "Last update run" counter shows the last time idle tasks were actually execu #### Debug log - +```shell {hideCopy} Debug log: - +``` Errors that occur within the service worker are logged here. @@ -283,7 +283,7 @@ When the service worker's request for `ngsw.json` returns a `404`, then the serv -A small script, `safety-worker.js`, is also included in the `@angular/service-worker` NPM package. +A small script, `safety-worker.js`, is also included in the `@angular/service-worker` npm package. When loaded, it un-registers itself from the browser and removes the service worker caches. This script can be used as a last resort to get rid of unwanted service workers already installed on client pages. diff --git a/adev-es/src/content/ecosystem/service-workers/getting-started.en.md b/adev-es/src/content/ecosystem/service-workers/getting-started.en.md index 268a3a1..a99bb75 100644 --- a/adev-es/src/content/ecosystem/service-workers/getting-started.en.md +++ b/adev-es/src/content/ecosystem/service-workers/getting-started.en.md @@ -166,9 +166,8 @@ Angular service workers support comprehensive configuration options through the The `enabled` option controls whether the service worker will be registered and related services will attempt to communicate with it. ```ts - -import { ApplicationConfig, isDevMode } from '@angular/core'; -import { provideServiceWorker } from '@angular/service-worker'; +import {ApplicationConfig, isDevMode} from '@angular/core'; +import {provideServiceWorker} from '@angular/service-worker'; export const appConfig: ApplicationConfig = { providers: [ @@ -177,7 +176,6 @@ export const appConfig: ApplicationConfig = { }), ], }; - ``` ### Cache control with updateViaCache @@ -185,7 +183,6 @@ export const appConfig: ApplicationConfig = { 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. ```ts - export const appConfig: ApplicationConfig = { providers: [ provideServiceWorker('ngsw-worker.js', { @@ -194,7 +191,6 @@ export const appConfig: ApplicationConfig = { }), ], }; - ``` The `updateViaCache` option accepts the following values: @@ -208,7 +204,6 @@ The `updateViaCache` option accepts the following values: The `type` option enables specifying the script type when registering service workers, providing support for ES module features in your service worker scripts. ```ts - export const appConfig: ApplicationConfig = { providers: [ provideServiceWorker('ngsw-worker.js', { @@ -217,7 +212,6 @@ export const appConfig: ApplicationConfig = { }), ], }; - ``` The `type` option accepts the following values: @@ -230,7 +224,6 @@ The `type` option accepts the following values: The `scope` option defines the service worker's registration scope, determining what range of URLs it can control. ```ts - export const appConfig: ApplicationConfig = { providers: [ provideServiceWorker('ngsw-worker.js', { @@ -239,7 +232,6 @@ export const appConfig: ApplicationConfig = { }), ], }; - ``` - Controls which URLs the service worker can intercept and manage @@ -251,7 +243,6 @@ export const appConfig: ApplicationConfig = { The `registrationStrategy` option defines when the service worker will be registered with the browser, providing control over the timing of registration. ```ts - export const appConfig: ApplicationConfig = { providers: [ provideServiceWorker('ngsw-worker.js', { @@ -260,7 +251,6 @@ export const appConfig: ApplicationConfig = { }), ], }; - ``` Available registration strategies: @@ -270,7 +260,6 @@ Available registration strategies: - **`'registerWithDelay:timeout'`** - Register with a delay of the specified timeout in milliseconds ```ts - // Register immediately export const immediateConfig: ApplicationConfig = { providers: [ @@ -290,13 +279,12 @@ export const delayedConfig: ApplicationConfig = { }), ], }; - ``` You can also provide an Observable factory function for custom registration timing: ```ts -import { timer } from 'rxjs'; +import {timer} from 'rxjs'; export const customConfig: ApplicationConfig = { providers: [ @@ -306,7 +294,6 @@ export const customConfig: ApplicationConfig = { }), ], }; - ``` ## More on Angular service workers diff --git a/adev-es/src/content/ecosystem/web-workers.en.md b/adev-es/src/content/ecosystem/web-workers.en.md index 9ebd9fc..74ba773 100644 --- a/adev-es/src/content/ecosystem/web-workers.en.md +++ b/adev-es/src/content/ecosystem/web-workers.en.md @@ -26,29 +26,26 @@ The command performs the following actions. 1. Adds the following scaffold code to `src/app/app.worker.ts` to receive messages. ```ts {header:"src/app/app.worker.ts"} - - addEventListener('message', ({ data }) => { - const response = `worker response to ${data}`; - postMessage(response); - }); - + addEventListener('message', ({data}) => { + const response = `worker response to ${data}`; + postMessage(response); + }); ``` 1. Adds the following scaffold code to `src/app/app.component.ts` to use the worker. ```ts {header:"src/app/app.component.ts"} - - if (typeof Worker !== 'undefined') { - // Create a new - const worker = new Worker(new URL('./app.worker', import.meta.url)); - worker.onmessage = ({ data }) => { - console.log(`page got message: ${data}`); - }; - worker.postMessage('hello'); - } else { - // Web workers are not supported in this environment. - // You should add a fallback so that your program still executes correctly. - } + if (typeof Worker !== 'undefined') { + // Create a new + const worker = new Worker(new URL('./app.worker', import.meta.url)); + worker.onmessage = ({data}) => { + console.log(`page got message: ${data}`); + }; + worker.postMessage('hello'); + } else { + // Web workers are not supported in this environment. + // You should add a fallback so that your program still executes correctly. + } ``` After you create this initial scaffold, you must refactor your code to use the web worker by sending messages to and from the worker. diff --git a/adev-es/src/content/events/v21.md b/adev-es/src/content/events/v21.md index e1f5ace..675e931 100644 --- a/adev-es/src/content/events/v21.md +++ b/adev-es/src/content/events/v21.md @@ -4,7 +4,7 @@ ## Release Blog -**Angular v21 is live**: check out the [v21 release blog](http://goo.gle/angular-v21-blog) to learn about all of the amazing new features coming your way. +**Angular v21 is live**: check out the [v21 release blog](https://goo.gle/angular-v21-blog) to learn about all of the amazing new features coming your way. ## Experience the v21 Release @@ -22,16 +22,4 @@ Angular v21 is being delivered to you as a brand new release adventure. With mod - Your first look at Signal Forms, our new streamlined, signal-based approach to forms in Angular - Exciting new details about the Angular Aria package -
    - -
    + diff --git a/adev-es/src/content/events/v22.md b/adev-es/src/content/events/v22.md new file mode 100644 index 0000000..028563b --- /dev/null +++ b/adev-es/src/content/events/v22.md @@ -0,0 +1,30 @@ +# Angular v22: Rock solid foundation for building what's next for the web + +![A digital illustration styled like a vintage postcard with a thin white border. The top left features the words "Welcome to" written in a cursive, yellow script font. The main focus is a large, brown rocky mountain in the center. Imposed on the mountain face is a 3D white text block reading "Angular V22" along with the Angular logo icon. Dense pine trees in shades of green, yellow, and grey line the foreground at the base of the mountain. A winding path trails up the right side of the mountain, and a few birds fly in the sunset-colored purple and orange sky.](assets/images/v22-event/angular-v22-hero.png {loading: 'eager', fetchpriority: 'high'} 'Angular v22 Hero Image') + +Angular v22 is ready for you to build modern, high-performance web applications. This release introduces key stabilization updates, template enhancements, and API improvements: + +- **Stabilized APIs**: Signal Forms, Asynchronous Signals, and Angular Aria are now stable, offering a production-ready reactive foundation. +- **Template enhancements**: New template features streamline development, improve ergonomics, and enhance code clarity. +- **API improvements**: Core APIs have been updated for better performance, simpler syntax, and more robust typing. +- **Angular AI integration**: Streamlined support and updates for AI-driven development workflows. + +## Key resources + + + + Discover all the major features, community updates, and design decisions behind Angular v22. + + + Review the detailed, commit-by-commit list of new features, bug fixes, and breaking changes. + + + Get instructions on how to update your project to Angular v22. + + + +## Developer event + +[Watch the Angular v22 Developer Event online](https://goo.gle/angular-v22-yt). + + diff --git a/adev-es/src/content/examples/i18n/readme.md b/adev-es/src/content/examples/i18n/readme.md index a724b58..f72a08f 100644 --- a/adev-es/src/content/examples/i18n/readme.md +++ b/adev-es/src/content/examples/i18n/readme.md @@ -10,9 +10,9 @@ This sample comes from the Angular documentation's "[Example Angular Internation > See the scripts in `package.json` for an explanation of these commands. -## Run in Stackblitz +## Run in StackBlitz -Stackblitz compiles and runs the English version by default. +StackBlitz compiles and runs the English version by default. To see the example translate to French with Angular i18n: @@ -24,4 +24,4 @@ To see the example translate to French with Angular i18n: } ``` -1. Click the "Fork" button in the stackblitz header. That makes a new copy for you with this change and re-runs the example in French. +1. Click the "Fork" button in the StackBlitz header. That makes a new copy for you with this change and re-runs the example in French. diff --git a/adev-es/src/content/guide/animations/complex-sequences.en.md b/adev-es/src/content/guide/animations/complex-sequences.en.md index 27a69f3..0efe972 100644 --- a/adev-es/src/content/guide/animations/complex-sequences.en.md +++ b/adev-es/src/content/guide/animations/complex-sequences.en.md @@ -1,6 +1,6 @@ # Complex animation sequences -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. +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). 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. @@ -17,12 +17,12 @@ The functions that control complex animation sequences are: ## The query() function -Most complex animations rely on the `query()` function to find child elements and apply animations to them, basic examples of such are: +Most complex animations rely on the `query()` function to find child elements and apply animations to them. Basic examples include: -| Examples | Details | -| :------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `query()` followed by `animate()` | Used to query simple HTML elements and directly apply animations to them. | -| `query()` followed by `animateChild()` | Used to query child elements, which themselves have animations metadata applied to them and trigger such animation \(which would be otherwise be blocked by the current/parent element's animation\). | +| Examples | Details | +| :------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `query()` followed by `animate()` | Used to query simple HTML elements and directly apply animations to them. | +| `query()` followed by `animateChild()` | Used to query child elements, which themselves have animation metadata applied to them and trigger such animations \(which would otherwise be blocked by the current/parent element's animation\). | The first argument of `query()` is a [css selector](https://developer.mozilla.org/docs/Web/CSS/CSS_Selectors) string which can also contain the following Angular-specific tokens: @@ -43,7 +43,7 @@ You can also see an illustration of this in the animations example \(introduced ## Animate multiple elements using query() and stagger() functions -After having queried child elements via `query()`, the `stagger()` function lets you define a timing gap between each queried item that is animated and thus animates elements with a delay between them. +After querying child elements via `query()`, the `stagger()` function lets you define a timing gap between each item, animating elements with a delay between them. The following example demonstrates how to use the `query()` and `stagger()` functions to animate a list \(of heroes\) adding each in sequence, with a slight delay, from top to bottom. @@ -54,7 +54,7 @@ The following example demonstrates how to use the `query()` and `stagger()` func - Use `stagger()` to delay each animation by 30 milliseconds - Animate each element on screen for 0.5 seconds using a custom-defined easing curve, simultaneously fading it in and un-transforming it - + ## Parallel animation using group() function @@ -67,7 +67,7 @@ HELPFUL: The [`group()`](api/animations/group) function is used to group animati The following example uses [`group()`](api/animations/group)s on both `:enter` and `:leave` for two different timing configurations, thus applying two independent animations to the same element in parallel. - + ## Sequential vs. parallel animations @@ -91,11 +91,11 @@ The heroes list gradually re-enters the page as you delete each letter in the fi The HTML template contains a trigger called `filterAnimation`. - + The `filterAnimation` in the component's decorator contains three transitions. - + The code in this example performs the following tasks: diff --git a/adev-es/src/content/guide/animations/css.en.md b/adev-es/src/content/guide/animations/css.en.md index 0e68cf2..5781643 100644 --- a/adev-es/src/content/guide/animations/css.en.md +++ b/adev-es/src/content/guide/animations/css.en.md @@ -4,7 +4,7 @@ CSS offers a robust set of tools for you to create beautiful and engaging animat ## How to write animations in native CSS -If you've never written any native CSS animations, there are a number of excellent guides to get you started. Here's a few of them: +If you've never written any native CSS animations, there are a number of excellent guides to get you started. Here are a few of them: [MDN's CSS Animations guide](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_animations/Using_CSS_animations) [W3Schools CSS3 Animations guide](https://www.w3schools.com/css/css3_animations.asp) [The Complete CSS Animations Tutorial](https://www.lambdatest.com/blog/css-animations-tutorial/) @@ -14,13 +14,13 @@ and a couple of videos: [Learn CSS Animation in 9 Minutes](https://www.youtube.com/watch?v=z2LQYsZhsFw) [Net Ninja CSS Animation Tutorial Playlist](https://www.youtube.com/watch?v=jgw82b5Y2MU&list=PL4cUxeGkcC9iGYgmEd2dm3zAKzyCGDtM5) -Check some of these various guides and tutorials out, and then come back to this guide. +Check out some of these guides and tutorials, then come back to this guide. ## Creating Reusable Animations You can create reusable animations that can be shared across your application using `@keyframes`. Define keyframe animations in a shared CSS file, and you'll be able to re-use those keyframe animations wherever you want within your application. - + Adding the class `animated-class` to an element would trigger the animation on that element. @@ -28,9 +28,9 @@ Adding the class `animated-class` to an element would trigger the animation on t ### Animating State and Styles -You may want to animate between two different states, for example when an element is opened or closed. You can accomplish this by using CSS classes either using a keyframe animation or transition styling. +You may want to animate between two different states, for example when an element is opened or closed. You can accomplish this by using CSS classes, either with a keyframe animation or transition styling. - + Triggering the `open` or `closed` state is done by toggling classes on the element in your component. You can find examples of how to do this in our [template guide](guide/templates/binding#css-class-and-style-property-bindings). @@ -38,56 +38,56 @@ You can see similar examples in the template guide for [animating styles directl ### Transitions, Timing, and Easing -Animating often requires adjusting timing, delays and easeing behaviors. This can be done using several css properties or shorthand properties. +Animating often requires adjusting timing, delays, and easing behaviors. This can be done using several css properties or shorthand properties. Specify `animation-duration`, `animation-delay`, and `animation-timing-function` for a keyframe animation in CSS, or alternatively use the `animation` shorthand property. - + Similarly, you can use `transition-duration`, `transition-delay`, and `transition-timing-function` and the `transition` shorthand for animations that are not using `@keyframes`. - + ### Triggering an Animation Animations can be triggered by toggling CSS styles or classes. Once a class is present on an element, the animation will occur. Removing the class will revert the element back to whatever CSS is defined for that element. Here's an example: - - - - + + + + ## Transition and Triggers ### Animating Auto Height -You can use css-grid to animate to auto height. +You can use CSS Grid to animate to auto height. - - - - + + + + -If you don't have to worry about supporting all browsers, you can also check out `calc-size()`, which is the true solution to animating auto height. See [MDN's docs](https://developer.mozilla.org/en-US/docs/Web/CSS/calc-size) and (this tutorial)[https://frontendmasters.com/blog/one-of-the-boss-battles-of-css-is-almost-won-transitioning-to-auto/] for more information. +If you don't have to worry about supporting all browsers, you can also check out `calc-size()`, which is the true solution to animating auto height. See [MDN's docs](https://developer.mozilla.org/en-US/docs/Web/CSS/calc-size) and [this tutorial](https://frontendmasters.com/blog/one-of-the-boss-battles-of-css-is-almost-won-transitioning-to-auto/) for more information. ### 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 entering a view. We'll do this with `animate.enter`, which will apply animation classes when an element enters the view. - - - - + + + + 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. - - - - + + + + For more information on `animate.enter` and `animate.leave`, see the [Enter and Leave animations guide](guide/animations). @@ -96,10 +96,10 @@ For more information on `animate.enter` and `animate.leave`, see the [Enter and Animating on increment and decrement is a common pattern in applications. Here's an example of how you can accomplish that behavior. - - - - + + + + ### Disabling an animation or all animations @@ -147,10 +147,10 @@ Animations are often more complicated than just a simple fade in or fade out. Yo One common effect is to stagger the animations of each item in a list to create a cascade effect. This can be accomplished by utilizing `animation-delay` or `transition-delay`. Here is an example of what that CSS might look like. - - - - + + + + ### Parallel Animations @@ -159,7 +159,9 @@ You can apply multiple animations to an element at once using the `animation` sh ```css .target-element { - animation: rotate 3s, fade-in 2s; + animation: + rotate 3s, + fade-in 2s; } ``` @@ -169,10 +171,10 @@ In this example, the `rotate` and `fade-in` animations fire at the same time, bu 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. - - - - + + + + ## Programmatic control of animations diff --git a/adev-es/src/content/guide/animations/enter-and-leave.en.md b/adev-es/src/content/guide/animations/enter-and-leave.en.md index 8ab4808..7817950 100644 --- a/adev-es/src/content/guide/animations/enter-and-leave.en.md +++ b/adev-es/src/content/guide/animations/enter-and-leave.en.md @@ -1,13 +1,13 @@ # 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: +Well-designed animations can make your application more intuitive and engaging, but they aren't just cosmetic. +Animations can improve your application and the 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. +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 directly on elements and also as a host binding. ## `animate.enter` @@ -19,7 +19,7 @@ You can use `animate.enter` to animate elements as they _enter_ the DOM. You can -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. +When the animation completes, Angular removes the class or classes that you specified in `animate.enter` from the DOM. Animation classes are only 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. @@ -45,7 +45,7 @@ You can use `animate.leave` to animate elements as they _leave_ the DOM. You can 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. +NOTE: When using multiple keyframe animations or transition properties on 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. @@ -55,6 +55,16 @@ NOTE: When using multiple keyframe animations or transition properties on a an e +### Element removal order + +There is some nuance to how `animate.leave` animations are run and when an animation will occur. `animate.leave` works if it is placed on the element that is being removed, and if `animate.leave` is placed on an element that is a _descendent_ of the element being removed, those child animations will happen _before_ the parent node is removed from the DOM. This ensures that you can confidently animate away child elements without the parent node disappearing prematurely. + + + + + + + ## 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. @@ -86,7 +96,7 @@ TestBed provides built-in support for enabling or disabling animations in your t 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}); +TestBed.configureTestingModule({animationsEnabled: true}); ``` This will configure animations in your test environment to behave normally. diff --git a/adev-es/src/content/guide/animations/enter-and-leave.md b/adev-es/src/content/guide/animations/enter-and-leave.md index 2eea824..95206be 100644 --- a/adev-es/src/content/guide/animations/enter-and-leave.md +++ b/adev-es/src/content/guide/animations/enter-and-leave.md @@ -55,6 +55,16 @@ NOTA: Cuando se usan múltiples animaciones de keyframes o propiedades de transi +### Orden de eliminación de elementos + +Hay cierta sutileza en cómo se ejecutan las animaciones de `animate.leave` y cuándo ocurrirá una animación. `animate.leave` funciona si se coloca en el elemento que se está eliminando, y si `animate.leave` se coloca en un elemento que es _descendiente_ del elemento que se está eliminando, esas animaciones hijas ocurrirán _antes_ de que el nodo padre sea eliminado del DOM. Esto garantiza que puedas animar con confianza los elementos hijos sin que el nodo padre desaparezca prematuramente. + + + + + + + ## Enlaces de eventos, funciones y bibliotecas de terceros Tanto `animate.enter` como `animate.leave` soportan sintaxis de enlace de eventos que permite llamadas a funciones. Puedes usar esta sintaxis para llamar a una función en el código de tu componente o utilizar bibliotecas de animación de terceros, como [GSAP](https://gsap.com/), [anime.js](https://animejs.com/), o cualquier otra biblioteca de animación JavaScript. diff --git a/adev-es/src/content/guide/animations/migration.en.md b/adev-es/src/content/guide/animations/migration.en.md index 60890a1..7aa4258 100644 --- a/adev-es/src/content/guide/animations/migration.en.md +++ b/adev-es/src/content/guide/animations/migration.en.md @@ -4,7 +4,7 @@ The `@angular/animations` package is deprecated as of v20.2, which also introduc ## How to write animations in native CSS -If you've never written any native CSS animations, there are a number of excellent guides to get you started. Here's a few of them: +If you've never written any native CSS animations, there are a number of excellent guides to get you started. Here are a few of them: [MDN's CSS Animations guide](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_animations/Using_CSS_animations) [W3Schools CSS3 Animations guide](https://www.w3schools.com/css/css3_animations.asp) [The Complete CSS Animations Tutorial](https://www.lambdatest.com/blog/css-animations-tutorial/) @@ -14,7 +14,7 @@ and a couple of videos: [Learn CSS Animation in 9 Minutes](https://www.youtube.com/watch?v=z2LQYsZhsFw) [Net Ninja CSS Animation Tutorial Playlist](https://www.youtube.com/watch?v=jgw82b5Y2MU&list=PL4cUxeGkcC9iGYgmEd2dm3zAKzyCGDtM5) -Check some of these various guides and tutorials out, and then come back to this guide. +Check out some of these guides and tutorials, then come back to this guide. ## Creating Reusable Animations @@ -22,11 +22,11 @@ Just like with the animations package, you can create reusable animations that c #### With Animations Package - + #### With Native CSS - + Adding the class `animated-class` to an element would trigger the animation on that element. @@ -38,13 +38,13 @@ The animations package allowed you to define various states using the [`state()` #### With Animations Package - + -This same behavior can be accomplished natively by using CSS classes either using a keyframe animation or transition styling. +This same behavior can be accomplished natively by using CSS classes, either with a keyframe animation or transition styling. #### With Native CSS - + Triggering the `open` or `closed` state is done by toggling classes on the element in your component. You can find examples of how to do this in our [template guide](guide/templates/binding#css-class-and-style-property-bindings). @@ -56,11 +56,11 @@ The animations package `animate()` function allows for providing timing, like du Specify `animation-duration`, `animation-delay`, and `animation-timing-function` for a keyframe animation in CSS, or alternatively use the `animation` shorthand property. - + Similarly, you can use `transition-duration`, `transition-delay`, and `transition-timing-function` and the `transition` shorthand for animations that are not using `@keyframes`. - + ### Triggering an Animation @@ -69,17 +69,17 @@ The animations package required specifying triggers using the `trigger()` functi #### With Animations Package - - - + + + #### With Native CSS - - - - + + + + ## Transition and Triggers @@ -97,19 +97,19 @@ The animations package offers the ability to animate things that have been histo #### With Animations Package - - - + + + -You can use css-grid to animate to auto height. +You can use CSS Grid to animate to auto height. #### With Native CSS - - - - + + + + If you don't have to worry about supporting all browsers, you can also check out `calc-size()`, which is the true solution to animating auto height. See [MDN's docs](https://developer.mozilla.org/en-US/docs/Web/CSS/calc-size) and (this tutorial)[https://frontendmasters.com/blog/one-of-the-boss-battles-of-css-is-almost-won-transitioning-to-auto/] for more information. @@ -121,29 +121,25 @@ The animations package offered the previously mentioned pattern matching for ent #### With Animations Package - - - + + + -Here's how the same thing can be accomplished without the animations package using `animate.enter`. - #### With Native CSS - - - - + + + + -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 - - - - + + + + For more information on `animate.enter` and `animate.leave`, see the [Enter and Leave animations guide](guide/animations). @@ -155,17 +151,17 @@ Along with the aforementioned `:enter` and `:leave`, there's also `:increment` a #### With Animations Package - - - + + + #### With Native CSS - - - - + + + + ### Parent / Child Animations @@ -226,17 +222,17 @@ The `stagger()` function allowed you to delay the animation of each item in a li #### With Animations Package - - - + + + #### With Native CSS - - - - + + + + ### Parallel Animations @@ -245,7 +241,9 @@ The animations package has a `group()` function to play multiple animations at t ```css .target-element { - animation: rotate 3s, fade-in 2s; + animation: + rotate 3s, + fade-in 2s; } ``` @@ -255,20 +253,20 @@ In this example, the `rotate` and `fade-in` animations fire at the same time. 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< +#### With Animations Package - - - + + + #### With Native CSS - - - - + + + + ## Migrating usages of AnimationPlayer diff --git a/adev-es/src/content/guide/animations/overview.en.md b/adev-es/src/content/guide/animations/overview.en.md index e1d042d..db2a70f 100644 --- a/adev-es/src/content/guide/animations/overview.en.md +++ b/adev-es/src/content/guide/animations/overview.en.md @@ -1,10 +1,10 @@ # Introduction to Angular animations -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. +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). 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. -Animations can improve your application and user experience in a number of ways: +Animation provides the illusion of motion: HTML elements change styles over time. +Well-designed animations can make your application more intuitive and engaging, but they aren't just cosmetic. +Animations can improve your application and the 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 @@ -32,13 +32,11 @@ To get started with adding Angular animations to your project, import the animat Import `provideAnimationsAsync` from `@angular/platform-browser/animations/async` and add it to the providers list in the `bootstrapApplication` function call. - +```ts {header: "Enabling Animations", linenums} bootstrapApplication(AppComponent, { - providers: [ - provideAnimationsAsync(), - ] + providers: [provideAnimationsAsync()], }); - +``` If you need to have an animation happen immediately when your application is loaded, @@ -54,7 +52,7 @@ For `NgModule` based applications import `BrowserAnimationsModule`, which introd If you plan to use specific animation functions in component files, import those functions from `@angular/animations`. - + See all [available animation functions](guide/legacy-animations#animations-api-summary) at the end of this guide. @@ -63,7 +61,7 @@ See all [available animation functions](guide/legacy-animations#animations-api-s In the component file, add a metadata property called `animations:` within the `@Component()` decorator. You put the trigger that defines an animation within the `animations` metadata property. - + @@ -72,7 +70,7 @@ You put the trigger that defines an animation within the `animations` metadata p Let's animate a transition that changes a single HTML element from one state to another. For example, you can specify that a button displays either **Open** or **Closed** based on the user's last action. When the button is in the `open` state, it's visible and yellow. -When it's the `closed` state, it's translucent and blue. +When it's in the `closed` state, it's translucent and blue. In HTML, these attributes are set using ordinary CSS styles such as color and opacity. In Angular, use the `style()` function to specify a set of CSS styles for use with animations. @@ -86,7 +84,7 @@ Run the following command in terminal to generate the component: ng g component open-close ``` -This will create the component at `src/app/open-close.component.ts`. +This will create the component at `src/app/open-close.ts`. ### Animation state and styles @@ -101,11 +99,11 @@ Let's see how Angular's [`state()`](api/animations/state) function works with th In this code snippet, multiple style attributes are set at the same time for the state. In the `open` state, the button has a height of 200 pixels, an opacity of 1, and a yellow background color. - + In the following `closed` state, the button has a height of 100 pixels, an opacity of 0.8, and a background color of blue. - + ### Transitions and timing @@ -126,19 +124,15 @@ The `animate()` function \(second argument of the transition function\) accepts The `timings` parameter takes either a number or a string defined in three parts. - - -animate (duration) - - +```ts +animate(duration); +``` or - - -animate ('duration delay easing') - - +```ts +animate('duration delay easing'); +``` The first part, `duration`, is required. The duration can be expressed in milliseconds as a number without quotes, or in seconds with quotes and a time specifier. @@ -177,7 +171,7 @@ HELPFUL: See the Material Design website's topic on [Natural easing curves](http This example provides a state transition from `open` to `closed` with a 1-second transition between states. - + In the preceding code snippet, the `=>` operator indicates unidirectional transitions, and `<=>` is bidirectional. Within the transition, `animate()` specifies how long the transition takes. @@ -185,7 +179,7 @@ In this case, the state change from `open` to `closed` takes 1 second, expressed This example adds a state transition from the `closed` state to the `open` state with a 0.5-second transition animation arc. - + HELPFUL: Some additional notes on using styles within [`state`](api/animations/state) and `transition` functions. @@ -194,11 +188,9 @@ HELPFUL: Some additional notes on using styles within [`state`](api/animations/s - When animations are disabled, `transition()` styles can be skipped, but [`state()`](api/animations/state) styles can't - Include multiple state pairs within the same `transition()` argument: - - - transition( 'on => off, off => void' ) - - + ```ts + transition('on => off, off => void'); + ``` ### Triggering the animation @@ -220,22 +212,20 @@ However, it's possible for multiple triggers to be active at once. Animations are defined in the metadata of the component that controls the HTML element to be animated. Put the code that defines your animations under the `animations:` property within the `@Component()` decorator. - + When you've defined an animation trigger for a component, attach it to an element in that component's template by wrapping the trigger name in brackets and preceding it with an `@` symbol. Then, you can bind the trigger to a template expression using standard Angular property binding syntax as shown below, where `triggerName` is the name of the trigger, and `expression` evaluates to a defined animation state. - - -
    ; - -
    +```angular-html +
    +``` The animation is executed or triggered when the expression value changes to a new state. The following code snippet binds the trigger to the value of the `isOpen` property. - + In this example, when the `isOpen` expression evaluates to a defined state of `open` or `closed`, it notifies the trigger `openClose` of a state change. Then it's up to the `openClose` code to handle the state change and kick off a state change animation. @@ -252,9 +242,9 @@ In the HTML template file, use the trigger name to attach the defined animations Here are the code files discussed in the transition example. - - - + + + ### Summary diff --git a/adev-es/src/content/guide/animations/reusable-animations.en.md b/adev-es/src/content/guide/animations/reusable-animations.en.md index 17c2e9b..7aa3fca 100644 --- a/adev-es/src/content/guide/animations/reusable-animations.en.md +++ b/adev-es/src/content/guide/animations/reusable-animations.en.md @@ -1,15 +1,15 @@ # Reusable animations -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. +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). 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. +This topic provides examples of how to create reusable animations. ## Create reusable animations To create a reusable animation, use the [`animation()`](api/animations/animation) function to define an animation in a separate `.ts` file and declare this animation definition as a `const` export variable. -You can then import and reuse this animation in any of your application components using the [`useAnimation()`](api/animations/useAnimation) function. +You can then import and reuse this animation in your application components using the [`useAnimation()`](api/animations/useAnimation) function. - + In the preceding code snippet, `transitionAnimation` is made reusable by declaring it as an export variable. @@ -18,12 +18,12 @@ HELPFUL: The `height`, `opacity`, `backgroundColor`, and `time` inputs are repla You can also export a part of an animation. For example, the following snippet exports the animation `trigger`. - + -From this point, you can import reusable animation variables in your component class. +From this point, you can import reusable animation variables into your component class. For example, the following code snippet imports the `transitionAnimation` variable and uses it via the `useAnimation()` function. - + ## More on Angular animations diff --git a/adev-es/src/content/guide/animations/transition-and-triggers.en.md b/adev-es/src/content/guide/animations/transition-and-triggers.en.md index 13be611..7cfe759 100644 --- a/adev-es/src/content/guide/animations/transition-and-triggers.en.md +++ b/adev-es/src/content/guide/animations/transition-and-triggers.en.md @@ -1,8 +1,8 @@ # Animation transitions and triggers -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. +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). 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 guide goes into depth on special transition states such as the `*` wildcard and `void`. It also shows how these 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. ## Predefined states and wildcard matching @@ -23,11 +23,11 @@ Instead of defining each state-to-state transition pair, any transition to `clos This allows the addition of new states without having to include separate transitions for each one. - + Use a double arrow syntax to specify state-to-state transitions in both directions. - + ### Use wildcard state with multiple transition states @@ -37,7 +37,7 @@ If the button can change from `open` to either `closed` or something like `inPro wildcard state with 3 states - + The `* => *` transition applies when any change between two states takes place. @@ -52,7 +52,7 @@ To do this, list the more specific transitions _before_ `* => *`. Use the wildcard `*` with a style to tell the animation to use whatever the current style value is, and animate with that. Wildcard is a fallback value that's used if the state being animated isn't declared within the trigger. - + ### Void state @@ -76,7 +76,7 @@ Add a new behavior: - When you add a hero to the list of heroes, it appears to fly onto the page from the left - When you remove a hero from the list, it appears to fly out to the right - + In the preceding code, you applied the `void` state when the HTML element isn't attached to a view. @@ -85,12 +85,12 @@ In the preceding code, you applied the `void` state when the HTML element isn't `:enter` and `:leave` are aliases for the `void => *` and `* => void` transitions. These aliases are used by several animation functions. - +```ts {hideCopy} transition ( ':enter', [ … ] ); // alias for void => _ transition ( ':leave', [ … ] ); // alias for _ => void - +``` It's harder to target an element that is entering a view because it isn't in the DOM yet. Use the aliases `:enter` and `:leave` to target HTML elements that are inserted or removed from a view. @@ -99,17 +99,17 @@ Use the aliases `:enter` and `:leave` to target HTML elements that are inserted The `:enter` transition runs when any `*ngIf` or `*ngFor` views are placed on the page, and `:leave` runs when those views are removed from the page. -IMPORTANT: Entering/leaving behaviors can sometime be confusing. +IMPORTANT: Entering/leaving behaviors can sometimes be confusing. As a rule of thumb consider that any element being added to the DOM by Angular passes via the `:enter` transition. Only elements being directly removed from the DOM by Angular pass via the `:leave` transition. For example, an element's view is removed from the DOM because its parent is being removed from the DOM. This example has a special trigger for the enter and leave animation called `myInsertRemoveTrigger`. The HTML template contains the following code. - + In the component file, the `:enter` transition sets an initial opacity of 0. It then animates it to change that opacity to 1 as the element is inserted into the view. - + Note that this example doesn't need to use [`state()`](api/animations/state). @@ -121,13 +121,13 @@ Use these to kick off a transition when a numeric value has increased or decreas HELPFUL: The following example uses `query()` and `stagger()` methods. For more information on these methods, see the [complex sequences](guide/legacy-animations/complex-sequences) page. - + ## Boolean values in transitions If a trigger contains a Boolean value as a binding value, then this value can be matched using a `transition()` expression that compares `true` and `false`, or `1` and `0`. - + In the code snippet above, the HTML template binds a `
    ` element to a trigger named `openClose` with a status expression of `isOpen`, and with possible values of `true` and `false`. This pattern is an alternative to the practice of creating two named states like `open` and `close`. @@ -136,7 +136,7 @@ Inside the `@Component` metadata under the `animations:` property, when the stat In this case, the animation uses whatever height the element already had before the animation started. When the element is `closed`, the element gets animated to a height of 0, which makes it invisible. - + ## Multiple animation triggers @@ -156,8 +156,8 @@ When true, the `@.disabled` binding prevents all animations from rendering. The following code sample shows how to use this feature. - - + + When the `@.disabled` binding is true, the `@childAnimation` trigger doesn't kick off. @@ -177,7 +177,7 @@ Those elements can still animate. To turn off all animations for an Angular application, place the `@.disabled` host binding on the topmost Angular component. - + HELPFUL: Disabling animations application-wide is useful during end-to-end \(E2E\) testing. @@ -186,12 +186,12 @@ HELPFUL: Disabling animations application-wide is useful during end-to-end \(E2E The animation `trigger()` function emits _callbacks_ when it starts and when it finishes. The following example features a component that contains an `openClose` trigger. - + In the HTML template, the animation event is passed back via `$event`, as `@triggerName.start` and `@triggerName.done`, where `triggerName` is the name of the trigger being used. In this example, the trigger `openClose` appears as follows. - + A potential use for animation callbacks could be to cover for a slow API call, such as a database lookup. For example, an **InProgress** button can be set up to have its own looping animation while the backend system operation finishes. @@ -204,7 +204,7 @@ An animation can influence an end user to _perceive_ the operation as faster, ev Callbacks can serve as a debugging tool, for example in conjunction with `console.warn()` to view the application's progress in a browser's Developer JavaScript Console. The following code snippet creates console log output for the original example, a button with the two states of `open` and `closed`. - + ## Keyframes @@ -217,7 +217,7 @@ For example, the button, instead of fading, could change color several times ove The code for this color change might look like this. - + ### Offset @@ -233,13 +233,13 @@ Specifying an offset of 0.8 for the middle transition in the preceding example m The code with offsets specified would be as follows. - + You can combine keyframes with `duration`, `delay`, and `easing` within a single animation. ### Keyframes with a pulsation -Use keyframes to create a pulse effect in your animations by defining styles at specific offset throughout the animation. +Use keyframes to create a pulse effect in your animations by defining styles at specific offsets throughout the animation. Here's an example of using keyframes to create a pulse effect: @@ -250,11 +250,11 @@ Here's an example of using keyframes to create a pulse effect: The code snippet for this animation might look like this. - + ### Animatable properties and units -Angular animations support builds on top of web animations, so you can animate any property that the browser considers animatable. +Angular animations are built on top of web animations, so you can animate any property that the browser considers animatable. This includes positions, sizes, transforms, colors, borders, and more. The W3C maintains a list of animatable properties on its [CSS Transitions](https://www.w3.org/TR/css-transitions-1) page. @@ -285,7 +285,7 @@ In these cases, you can use a special wildcard `*` property value under `style() The following example has a trigger called `shrinkOut`, used when an HTML element leaves the page. The animation takes whatever height the element has before it leaves, and animates from that height to zero. - + ### Keyframes summary diff --git a/adev-es/src/content/guide/aria/accordion.en.md b/adev-es/src/content/guide/aria/accordion.en.md index 2588aee..b1449e8 100644 --- a/adev-es/src/content/guide/aria/accordion.en.md +++ b/adev-es/src/content/guide/aria/accordion.en.md @@ -145,13 +145,11 @@ Use the `ngAccordionContent` directive on an `ng-template` to defer rendering co ```angular-html
    - -
    + +
    - Description + Description
    @@ -161,91 +159,60 @@ Use the `ngAccordionContent` directive on an `ng-template` to defer rendering co By default, content remains in the DOM after the panel collapses. Set `[preserveContent]="false"` to remove the content from the DOM when the panel closes. -## APIs - -### AccordionGroup - -The container directive that manages keyboard navigation and expansion behavior for a group of accordion items. - -#### Inputs - -| Property | Type | Default | Description | -| ----------------- | --------- | ------- | ------------------------------------------------------------------------- | -| `disabled` | `boolean` | `false` | Disables all triggers in the group | -| `multiExpandable` | `boolean` | `true` | Whether multiple panels can be expanded simultaneously | -| `softDisabled` | `boolean` | `true` | When `true`, disabled items are focusable. When `false`, they are skipped | -| `wrap` | `boolean` | `false` | Whether keyboard navigation wraps from last to first item and vice versa | - -#### Methods - -| Method | Parameters | Description | -| ------------- | ---------- | ---------------------------------------------------------------- | -| `expandAll` | none | Expands all panels (only works when `multiExpandable` is `true`) | -| `collapseAll` | none | Collapses all panels | - -### AccordionTrigger - -The directive applied to the button element that toggles panel visibility. - -#### Inputs - -| Property | Type | Default | Description | -| ---------- | --------- | ------- | -------------------------------------------------------------- | -| `id` | `string` | auto | Unique identifier for the trigger | -| `panelId` | `string` | — | **Required.** Must match the `panelId` of the associated panel | -| `disabled` | `boolean` | `false` | Disables this trigger | -| `expanded` | `boolean` | `false` | Whether the panel is expanded (supports two-way binding) | - -#### Signals - -| Property | Type | Description | -| -------- | ----------------- | --------------------------------------- | -| `active` | `Signal` | Whether the trigger currently has focus | - -#### Methods - -| Method | Parameters | Description | -| ---------- | ---------- | --------------------------------- | -| `expand` | none | Expands the associated panel | -| `collapse` | none | Collapses the associated panel | -| `toggle` | none | Toggles the panel expansion state | - -### AccordionPanel - -The directive applied to the element containing the collapsible content. - -#### Inputs - -| Property | Type | Default | Description | -| ----------------- | --------- | ------- | ---------------------------------------------------------------- | -| `id` | `string` | auto | Unique identifier for the panel | -| `panelId` | `string` | — | **Required.** Must match the `panelId` of the associated trigger | -| `preserveContent` | `boolean` | `true` | Whether to keep content in DOM after panel collapses | - -#### Signals - -| Property | Type | Description | -| --------- | ----------------- | --------------------------------------- | -| `visible` | `Signal` | Whether the panel is currently expanded | - -#### Methods - -| Method | Parameters | Description | -| ---------- | ---------- | --------------------------- | -| `expand` | none | Expands this panel | -| `collapse` | none | Collapses this panel | -| `toggle` | none | Toggles the expansion state | - -### AccordionContent +## Testing + +Angular Aria provides component harnesses for testing accordion components. +Here is an example of how to use the harnesses in a component test: + +```typescript +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {HarnessLoader} from '@angular/cdk/testing'; +import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed'; +import {AccordionGroupHarness} from '@angular/aria/accordion/testing'; +import {MyAccordionComponent} from './my-accordion'; // Your component + +describe('MyAccordionComponent', () => { + let fixture: ComponentFixture; + let loader: HarnessLoader; + + beforeEach(async () => { + TestBed.configureTestingModule({ + imports: [MyAccordionComponent], + }); + + fixture = TestBed.createComponent(MyAccordionComponent); + await fixture.whenStable(); + loader = TestbedHarnessEnvironment.loader(fixture); + }); + + it('should allow expanding panels', async () => { + // Load the accordion group harness + const group = await loader.getHarness(AccordionGroupHarness); + + // Get all individual accordions (items) in the group + const accordions = await group.getAccordions(); + expect(accordions.length).toBe(3); + + // Verify initial state (first expanded, others collapsed) + expect(await accordions[0].isExpanded()).toBe(true); + expect(await accordions[1].isExpanded()).toBe(false); + + // Expand the second panel + await accordions[1].expand(); + + // Verify updated state + expect(await accordions[1].isExpanded()).toBe(true); + // If multiExpandable is false, the first one should now be collapsed + expect(await accordions[0].isExpanded()).toBe(false); + }); +}); +``` -The structural directive applied to an `ng-template` inside an accordion panel to enable lazy rendering. +## API reference -This directive has no inputs, outputs, or methods. Apply it to an `ng-template` element: +For detailed API documentation, inspect the following API references: -```angular-html -
    - - - -
    -``` +- [`AccordionGroup`](/api/aria/accordion/AccordionGroup) +- [`AccordionTrigger`](/api/aria/accordion/AccordionTrigger) +- [`AccordionPanel`](/api/aria/accordion/AccordionPanel) +- [`AccordionContent`](/api/aria/accordion/AccordionContent) diff --git a/adev-es/src/content/guide/aria/autocomplete.en.md b/adev-es/src/content/guide/aria/autocomplete.en.md index 73f2ab7..2b164bc 100644 --- a/adev-es/src/content/guide/aria/autocomplete.en.md +++ b/adev-es/src/content/guide/aria/autocomplete.en.md @@ -52,7 +52,7 @@ Angular's autocomplete provides a fully accessible combobox implementation with: - **Keyboard Navigation** - Navigate options with arrow keys, select with Enter, close with Escape - **Screen Reader Support** - Built-in ARIA attributes for assistive technologies -- **Three Filter Modes** - Choose between auto-select, manual selection, or highlighting behavior +- **Dynamic Highlight Behavior** - Built-in support for inline selection suggestions - **Signal-Based Reactivity** - Reactive state management using Angular signals - **Popover API Integration** - Leverages the native HTML Popover API for optimal positioning - **Bidirectional Text Support** - Automatically handles right-to-left (RTL) languages @@ -149,42 +149,79 @@ Highlight mode allows the user to navigate options with arrow keys without chang -## APIs +### Signal Forms Integration -### Combobox Directive +Angular Aria integrates seamlessly with the signal-based [Signal Forms](guide/forms/signals/overview) API. You can encapsulate complex inputs into reusable custom control components implementing `FormValueControl`. -The `ngCombobox` directive provides the container for autocomplete functionality. +The following example demonstrates a country selector component implementing `FormValueControl`, bound to the parent form using `[formField]` and protected by schema validation rules. -#### Inputs + + + + + + + + -| Property | Type | Default | Description | -| ------------ | ---------------------------------------------- | ---------- | ------------------------------------------------- | -| `filterMode` | `'auto-select'` \| `'manual'` \| `'highlight'` | `'manual'` | Controls selection behavior | -| `disabled` | `boolean` | `false` | Disables the combobox | -| `firstMatch` | `string` | - | The value of the first matching item in the popup | +## Testing -#### Outputs +The autocomplete pattern can be tested using a combination of `ComboboxHarness` and `ListboxHarness` from `@angular/aria/combobox/testing` and `@angular/aria/listbox/testing`. +Here is an example of how to use the harnesses to test an autocomplete component: -| Property | Type | Description | -| ---------- | ----------------- | ----------------------------------------------------- | -| `expanded` | `Signal` | Signal indicating whether the popup is currently open | +```typescript +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {HarnessLoader} from '@angular/cdk/testing'; +import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed'; +import {ComboboxHarness} from '@angular/aria/combobox/testing'; +import {ListboxHarness} from '@angular/aria/listbox/testing'; +import {MyAutocompleteComponent} from './my-autocomplete'; // Your component -### ComboboxInput Directive +describe('MyAutocompleteComponent', () => { + let fixture: ComponentFixture; + let loader: HarnessLoader; -The `ngComboboxInput` directive connects an input element to the combobox. + beforeEach(async () => { + TestBed.configureTestingModule({ + imports: [MyAutocompleteComponent], + }); -#### Model + fixture = TestBed.createComponent(MyAutocompleteComponent); + await fixture.whenStable(); + loader = TestbedHarnessEnvironment.loader(fixture); + }); -| Property | Type | Description | -| -------- | -------- | ------------------------------------------------------------ | -| `value` | `string` | Two-way bindable string value of the input using `[(value)]` | + it('should filter options based on input', async () => { + const combobox = await loader.getHarness(ComboboxHarness); -### ComboboxPopupContainer Directive + // Type in the input to trigger filtering + await combobox.setValue('ap'); + expect(await combobox.isOpen()).toBe(true); -The `ngComboboxPopupContainer` directive wraps the popup content and manages its display. + // Get the listbox harness from the popup + const listbox = await combobox.getPopupWidget(ListboxHarness); + const options = await listbox.getOptions(); -Must be used with `` inside a popover element. + // Verify options are filtered (e.g., 'Apple', 'Apricot') + expect(options.length).toBe(2); + expect(await options[0].getText()).toBe('Apple'); -### Related components + // Select the first option + await options[0].click(); -Autocomplete uses [Listbox](/api/aria/listbox/Listbox) and [Option](/api/aria/listbox/Option) directives to render the suggestion list. See the [Listbox documentation](/guide/aria/listbox) for additional customization options. + // Verify the input value is updated and popup is closed + expect(await combobox.isOpen()).toBe(false); + expect(await combobox.getValue()).toBe('Apple'); + }); +}); +``` + +## API reference + +For detailed API documentation, inspect the following API references: + +- [`Combobox`](/api/aria/combobox/Combobox) +- [`ComboboxPopup`](/api/aria/combobox/ComboboxPopup) +- [`ComboboxWidget`](/api/aria/combobox/ComboboxWidget) +- [`Listbox`](/api/aria/listbox/Listbox) +- [`Option`](/api/aria/listbox/Option) diff --git a/adev-es/src/content/guide/aria/combobox.en.md b/adev-es/src/content/guide/aria/combobox.en.md index 7139021..aa2aa42 100644 --- a/adev-es/src/content/guide/aria/combobox.en.md +++ b/adev-es/src/content/guide/aria/combobox.en.md @@ -8,42 +8,42 @@ ## Overview -A directive that coordinates a text input with a popup, providing the primitive directive for autocomplete, select, and multiselect patterns. +A directive that coordinates a trigger element (such as a text input, button, or `div`) with a popup, providing the primitive directive for autocomplete, select, and multiselect patterns. - - - - + + + + - - - - + + + + - - - - + + + + ## Usage -Combobox is the primitive directive that coordinates a text input with a popup. It provides the foundation for autocomplete, select, and multiselect patterns. Consider using combobox directly when: +Combobox is the primitive directive that coordinates an interactive trigger element (such as a text input, button, or `div`) with a popup. It provides the foundation for autocomplete, select, and multiselect patterns. Consider using combobox directly when: - **Building custom autocomplete patterns** - Creating specialized filtering or suggestion behavior - **Creating custom selection components** - Developing dropdowns with unique requirements - **Coordinating input with popup** - Pairing text input with listbox, tree, or dialog content -- **Implementing specific filter modes** - Using manual, auto-select, or highlight behaviors +- **Implementing custom filtering** - Filtering and orchestrating matching options in user space Use documented patterns instead when: @@ -51,14 +51,14 @@ Use documented patterns instead when: - Single-selection dropdowns are needed - See the [Select pattern](guide/aria/select) for complete dropdown implementation - Multiple-selection dropdowns are needed - See the [Multiselect pattern](guide/aria/multiselect) for multi-select with compact display -Note: The [Autocomplete](guide/aria/autocomplete), [Select](guide/aria/select), and [Multiselect](guide/aria/multiselect) guides show documented patterns that combine this directive with [Listbox](guide/aria/listbox) for specific use cases. +NOTE: The [Autocomplete](guide/aria/autocomplete), [Select](guide/aria/select), and [Multiselect](guide/aria/multiselect) guides show documented patterns that combine this directive with [Listbox](guide/aria/listbox) for specific use cases. ## Features Angular's combobox provides a fully accessible input-popup coordination system with: -- **Text Input with Popup** - Coordinates input field with popup content -- **Three Filter Modes** - Manual, auto-select, or highlight behaviors +- **Trigger Element with Popup** - Coordinates trigger element with popup content +- **Flexible Coordination** - Integrates seamlessly with standard layouts (listbox, tree, grid, or dialog) - **Keyboard Navigation** - Arrow keys, Enter, Escape handling - **Screen Reader Support** - Built-in ARIA attributes including role="combobox" and aria-expanded - **Popup Management** - Automatic show/hide based on user interaction @@ -96,7 +96,7 @@ An accessible input field that filters and suggests options as users type, helpi -The `filterMode="manual"` setting gives complete control over filtering and selection. The input updates a signal that filters the options list. Users navigate with arrow keys and select with Enter or click. This mode provides the most flexibility for custom filtering logic. See the [Autocomplete guide](guide/aria/autocomplete) for complete filtering patterns and examples. +Filtering is managed in user space by updating a signal that reactively filters the options list. Users navigate with arrow keys and select with Enter or click. This provides complete control and maximum flexibility for custom selection logic. See the [Autocomplete guide](guide/aria/autocomplete) for complete filtering patterns and examples. ### Readonly mode @@ -128,13 +128,43 @@ A pattern that combines a readonly combobox with listbox to create single-select -The `readonly` attribute prevents typing in the input field. The popup opens on click or arrow keys. Users navigate options with keyboard and select with Enter or click. +Triggering a dropdown without text input can be achieved using a button as the host trigger, or applying the native HTML `readonly` attribute to the input trigger. The popup opens on click or arrow keys. This configuration provides the foundation for the [Select](guide/aria/select) and [Multiselect](guide/aria/multiselect) patterns. See those guides for complete dropdown implementations with triggers and overlay positioning. +### Datepicker grid + +Combobox can coordinate with a two-dimensional grid to create accessible datepickers. Users navigate dates inside the calendar grid table using directional arrow keys and confirm selection with click, Enter, or Spacebar. + + + + + + + + + + + + + + + + + + + + + + + + + + + ### Dialog popup -Popups sometimes need modal behavior with a backdrop and focus trap. The combobox dialog directive provides this pattern for specialized use cases. +Dialog popups combine the combobox trigger with standard dialog layouts and focus traps (such as CDK's `cdkTrapFocus`). Use dialog popups when the overlay requires modal behavior or backdrop interaction. @@ -162,94 +192,66 @@ Popups sometimes need modal behavior with a backdrop and focus trap. The combobo -The `ngComboboxDialog` directive creates a modal popup using the native dialog element. This provides backdrop behavior and focus trapping. Use dialog popups when the selection interface requires modal interaction or when the popup content is complex enough to warrant full-screen focus. - -## APIs - -### Combobox Directive - -The `ngCombobox` directive coordinates a text input with a popup. - -#### Inputs - -| Property | Type | Default | Description | -| ---------------- | ---------------------------------------------- | ---------- | ------------------------------------------------ | -| `filterMode` | `'manual'` \| `'auto-select'` \| `'highlight'` | `'manual'` | Controls selection behavior | -| `disabled` | `boolean` | `false` | Disables the combobox | -| `readonly` | `boolean` | `false` | Makes combobox readonly (for Select/Multiselect) | -| `firstMatch` | `V` | - | Value of first matching item for auto-select | -| `alwaysExpanded` | `boolean` | `false` | Keeps popup always open | +## Testing -**Filter Modes:** +Angular Aria provides a `ComboboxHarness` for testing combobox components. +Here is an example of how to use the harness in a component test: -- **`'manual'`** - User controls filtering and selection explicitly. The popup shows options based on your filtering logic. Users select with Enter or click. This mode provides the most flexibility. -- **`'auto-select'`** - Input value automatically updates to the first matching option as users type. Requires the `firstMatch` input for coordination. See the [Autocomplete guide](guide/aria/autocomplete#auto-select-mode) for examples. -- **`'highlight'`** - Highlights matching text without changing the input value. Users navigate with arrow keys and select with Enter. +```typescript +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {HarnessLoader} from '@angular/cdk/testing'; +import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed'; +import {ComboboxHarness} from '@angular/aria/combobox/testing'; +import {MyComboboxComponent} from './my-combobox'; // Your component -#### Signals +describe('MyComboboxComponent', () => { + let fixture: ComponentFixture; + let loader: HarnessLoader; -| Property | Type | Description | -| ---------- | ----------------- | ------------------------------- | -| `expanded` | `Signal` | Whether popup is currently open | + beforeEach(async () => { + TestBed.configureTestingModule({ + imports: [MyComboboxComponent], + }); -#### Methods + fixture = TestBed.createComponent(MyComboboxComponent); + await fixture.whenStable(); + loader = TestbedHarnessEnvironment.loader(fixture); + }); -| Method | Parameters | Description | -| ---------- | ---------- | ---------------------- | -| `open` | none | Opens the combobox | -| `close` | none | Closes the combobox | -| `expand` | none | Expands the combobox | -| `collapse` | none | Collapses the combobox | + it('should allow opening and closing the popup', async () => { + const combobox = await loader.getHarness(ComboboxHarness); -### ComboboxInput Directive + // Verify initial state + expect(await combobox.isOpen()).toBe(false); -The `ngComboboxInput` directive connects an input element to the combobox. + // Open the popup + await combobox.open(); + expect(await combobox.isOpen()).toBe(true); -#### Model - -| Property | Type | Description | -| -------- | -------- | ---------------------------------------- | -| `value` | `string` | Two-way bindable value using `[(value)]` | - -The input element receives keyboard handling and ARIA attributes automatically. - -### ComboboxPopup Directive - -The `ngComboboxPopup` directive (host directive) manages popup visibility and coordination. Typically used with `ngComboboxPopupContainer` in an `ng-template` or with CDK Overlay. - -### ComboboxPopupContainer Directive - -The `ngComboboxPopupContainer` directive marks an `ng-template` as the popup content. - -```html - -
    ...
    -
    + // Close the popup + await combobox.close(); + expect(await combobox.isOpen()).toBe(false); + }); +}); ``` -Used with Popover API or CDK Overlay for positioning. - -### ComboboxDialog Directive +## API reference -The `ngComboboxDialog` directive creates a modal combobox popup. - -```html - -
    ...
    -
    -``` +For detailed API documentation, inspect the following API references: -Use for modal popup behavior with backdrop and focus trap. +- [`Combobox`](/api/aria/combobox/Combobox) +- [`ComboboxPopup`](/api/aria/combobox/ComboboxPopup) +- [`ComboboxWidget`](/api/aria/combobox/ComboboxWidget) ### Related patterns and directives Combobox is the primitive directive for these documented patterns: -- **[Autocomplete](guide/aria/autocomplete)** - Filtering and suggestions pattern (uses Combobox with filter modes) -- **[Select](guide/aria/select)** - Single selection dropdown pattern (uses Combobox with `readonly`) -- **[Multiselect](guide/aria/multiselect)** - Multiple selection pattern (uses Combobox with `readonly` + multi-enabled Listbox) +- [Autocomplete](guide/aria/autocomplete) - Filtering and suggestions pattern (coordinates input typing with options list) +- [Select](guide/aria/select) - Single selection dropdown pattern (applied directly on non-editable button triggers) +- [Multiselect](guide/aria/multiselect) - Multiple selection pattern (applied on non-editable triggers with multi-enabled Listbox) Combobox typically combines with: -- **[Listbox](guide/aria/listbox)** - Most common popup content -- **[Tree](guide/aria/tree)** - Hierarchical popup content (see Tree guide for examples) +- [Listbox](guide/aria/listbox) - Most common popup content +- [Tree](guide/aria/tree) - Hierarchical popup content (see Tree guide for examples) diff --git a/adev-es/src/content/guide/aria/grid.en.md b/adev-es/src/content/guide/aria/grid.en.md index 2b162ae..7a241fd 100644 --- a/adev-es/src/content/guide/aria/grid.en.md +++ b/adev-es/src/content/guide/aria/grid.en.md @@ -135,11 +135,13 @@ Instead of tabbing through each button, users navigate with arrow keys and only Enable selection with `[enableSelection]="true"` and configure how focus and selection interact. ```angular-html - +
    @@ -157,58 +159,63 @@ Enable selection with `[enableSelection]="true"` and configure how focus and sel - `roving`: Focus moves to cells using `tabindex` (better for simple grids) - `activedescendant`: Focus stays on grid container, `aria-activedescendant` indicates active cell (better for virtual scrolling) -## APIs - -### Grid - -The container directive that provides keyboard navigation and focus management for rows and cells. - -#### Inputs - -| Property | Type | Default | Description | -| ---------------------- | ------------------------------------ | ---------- | ------------------------------------------------------------- | -| `enableSelection` | `boolean` | `false` | Whether selection is enabled for the grid | -| `disabled` | `boolean` | `false` | Disables the entire grid | -| `softDisabled` | `boolean` | `true` | When `true`, disabled cells are focusable but not interactive | -| `focusMode` | `'roving' \| 'activedescendant'` | `'roving'` | Focus strategy used by the grid | -| `rowWrap` | `'continuous' \| 'loop' \| 'nowrap'` | `'loop'` | Navigation wrapping behavior along rows | -| `colWrap` | `'continuous' \| 'loop' \| 'nowrap'` | `'loop'` | Navigation wrapping behavior along columns | -| `multi` | `boolean` | `false` | Whether multiple cells can be selected | -| `selectionMode` | `'follow' \| 'explicit'` | `'follow'` | Whether selection follows focus or requires explicit action | -| `enableRangeSelection` | `boolean` | `false` | Enable range selections with modifier keys or dragging | - -### GridRow - -Represents a row within a grid and serves as a container for grid cells. - -#### Inputs - -| Property | Type | Default | Description | -| ---------- | -------- | ------- | ------------------------------------- | -| `rowIndex` | `number` | auto | The index of this row within the grid | - -### GridCell - -Represents an individual cell within a grid row. - -#### Inputs +## Testing + +Angular Aria provides component harnesses for testing grid components. +Here is an example of how to use the harnesses in a component test: + +```typescript +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {HarnessLoader} from '@angular/cdk/testing'; +import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed'; +import {GridHarness} from '@angular/aria/grid/testing'; +import {MyGridComponent} from './my-grid'; // Your component + +describe('MyGridComponent', () => { + let fixture: ComponentFixture; + let loader: HarnessLoader; + + beforeEach(async () => { + TestBed.configureTestingModule({ + imports: [MyGridComponent], + }); + + fixture = TestBed.createComponent(MyGridComponent); + await fixture.whenStable(); + loader = TestbedHarnessEnvironment.loader(fixture); + }); + + it('should read cell values and focus cells', async () => { + const grid = await loader.getHarness(GridHarness); + + // Get all cells text in a 2D array organized by rows + const cellTexts = await grid.getCellTextByIndex(); + expect(cellTexts).toEqual([ + ['Cell 1.1', 'Cell 1.2'], + ['Cell 2.1', 'Cell 2.2'], + ]); + + // Get a specific cell by text + const cells = await grid.getCells({text: 'Cell 1.1'}); + expect(cells.length).toBe(1); + const cell = cells[0]; + + // Verify cell state + expect(await cell.isSelected()).toBe(true); + expect(await cell.isActive()).toBe(true); + + // Focus the cell + await cell.focus(); + expect(await cell.isFocused()).toBe(true); + }); +}); +``` -| Property | Type | Default | Description | -| ------------- | ---------------------------- | -------------- | ------------------------------------------------------- | -| `id` | `string` | auto | Unique identifier for the cell | -| `role` | `string` | `'gridcell'` | Cell role: `gridcell`, `columnheader`, or `rowheader` | -| `disabled` | `boolean` | `false` | Disables this cell | -| `selected` | `boolean` | `false` | Whether the cell is selected (supports two-way binding) | -| `selectable` | `boolean` | `true` | Whether the cell can be selected | -| `rowSpan` | `number` | — | Number of rows the cell spans | -| `colSpan` | `number` | — | Number of columns the cell spans | -| `rowIndex` | `number` | — | Row index of the cell | -| `colIndex` | `number` | — | Column index of the cell | -| `orientation` | `'vertical' \| 'horizontal'` | `'horizontal'` | Orientation for widgets within the cell | -| `wrap` | `boolean` | `true` | Whether widget navigation wraps within the cell | +## API reference -#### Signals +For detailed API documentation, inspect the following API references: -| Property | Type | Description | -| -------- | ----------------- | ------------------------------------ | -| `active` | `Signal` | Whether the cell currently has focus | +- [`Grid`](/api/aria/grid/Grid) +- [`GridRow`](/api/aria/grid/GridRow) +- [`GridCell`](/api/aria/grid/GridCell) +- [`GridCellWidget`](/api/aria/grid/GridCellWidget) diff --git a/adev-es/src/content/guide/aria/listbox.en.md b/adev-es/src/content/guide/aria/listbox.en.md index a1863dc..c4a6569 100644 --- a/adev-es/src/content/guide/aria/listbox.en.md +++ b/adev-es/src/content/guide/aria/listbox.en.md @@ -72,7 +72,7 @@ Applications sometimes need selectable lists visible directly on the page rather -The `values` model signal provides two-way binding to the selected items. With `selectionMode="explicit"`, users press Space or Enter to select options. For dropdown patterns that combine listbox with combobox and overlay positioning, see the [Select](guide/aria/select) pattern. +The `value` model signal provides two-way binding to the selected items. With `selectionMode="explicit"`, users press Space or Enter to select options. For dropdown patterns that combine listbox with combobox and overlay positioning, see the [Select](guide/aria/select) pattern. ### Horizontal listbox @@ -108,86 +108,93 @@ With `orientation="horizontal"`, left and right arrow keys navigate between opti ### Selection modes -Listbox supports two selection modes that control when items become selected. Choose the mode that matches your interface's interaction pattern. +Listbox supports two selection modes that control when items become selected. - - - +The `'follow'` mode automatically selects the focused item, providing faster interaction when selection changes frequently. The `'explicit'` mode requires Space or Enter to confirm selection, preventing accidental changes while navigating. Dropdown patterns typically use `'follow'` mode for single selection. + +#### Explicit + + + + -The `'follow'` mode automatically selects the focused item, providing faster interaction when selection changes frequently. The `'explicit'` mode requires Space or Enter to confirm selection, preventing accidental changes while navigating. Dropdown patterns typically use `'follow'` mode for single selection. +#### Follow -## APIs + + + + -### Listbox Directive +| Mode | Description | +| ------------ | ------------------------------------------------------------------------------------------------------ | +| `'follow'` | Automatically selects the focused item, providing faster interaction when selection changes frequently | +| `'explicit'` | Requires Space or Enter to confirm selection, preventing accidental changes while navigating | -The `ngListbox` directive creates an accessible list of selectable options. +TIP: Dropdown patterns typically use `'follow'` mode for single selection. -#### Inputs +## Testing -| Property | Type | Default | Description | -| ---------------- | ---------------------------------- | ------------ | -------------------------------------------- | -| `id` | `string` | auto | Unique identifier for the listbox | -| `multi` | `boolean` | `false` | Enables multiple selection | -| `orientation` | `'vertical'` \| `'horizontal'` | `'vertical'` | Layout direction of the list | -| `wrap` | `boolean` | `true` | Whether focus wraps at list edges | -| `selectionMode` | `'follow'` \| `'explicit'` | `'follow'` | How selection is triggered | -| `focusMode` | `'roving'` \| `'activedescendant'` | `'roving'` | Focus management strategy | -| `softDisabled` | `boolean` | `true` | Whether disabled items are focusable | -| `disabled` | `boolean` | `false` | Disables the entire listbox | -| `readonly` | `boolean` | `false` | Makes listbox readonly | -| `typeaheadDelay` | `number` | `500` | Milliseconds before type-ahead search resets | +Angular Aria provides component harnesses for testing listbox components. +Here is an example of how to use the harnesses in a component test: -#### Model +```typescript +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {HarnessLoader} from '@angular/cdk/testing'; +import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed'; +import {ListboxHarness} from '@angular/aria/listbox/testing'; +import {MyListboxComponent} from './my-listbox'; // Your component -| Property | Type | Description | -| -------- | ----- | ----------------------------------------- | -| `values` | `V[]` | Two-way bindable array of selected values | +describe('MyListboxComponent', () => { + let fixture: ComponentFixture; + let loader: HarnessLoader; -#### Signals + beforeEach(async () => { + TestBed.configureTestingModule({ + imports: [MyListboxComponent], + }); -| Property | Type | Description | -| -------- | ------------- | ------------------------------------- | -| `values` | `Signal` | Currently selected values as a signal | + fixture = TestBed.createComponent(MyListboxComponent); + await fixture.whenStable(); + loader = TestbedHarnessEnvironment.loader(fixture); + }); -#### Methods + it('should allow selecting options', async () => { + const listbox = await loader.getHarness(ListboxHarness); -| Method | Parameters | Description | -| -------------------------- | --------------------------------- | ------------------------------------------ | -| `scrollActiveItemIntoView` | `options?: ScrollIntoViewOptions` | Scrolls the active item into view | -| `gotoFirst` | none | Navigates to the first item in the listbox | + // Verify listbox properties + expect(await listbox.isMulti()).toBe(true); -### Option Directive + // Get all options + const options = await listbox.getOptions(); + expect(options.length).toBe(2); -The `ngOption` directive marks an item within a listbox. + // Click an option + await options[0].click(); -#### Inputs + // Verify option is selected + expect(await options[0].isSelected()).toBe(true); -| Property | Type | Default | Description | -| ---------- | --------- | ------- | ------------------------------------------------ | -| `id` | `string` | auto | Unique identifier for the option | -| `value` | `V` | - | The value associated with this option (required) | -| `label` | `string` | - | Optional label for screen readers | -| `disabled` | `boolean` | `false` | Whether this option is disabled | + // Filter options by text + const bananaOption = await listbox.getOptions({text: 'Banana'}); + expect(bananaOption.length).toBe(1); + }); +}); +``` -#### Signals +## API reference -| Property | Type | Description | -| ---------- | ----------------- | ------------------------------- | -| `selected` | `Signal` | Whether this option is selected | -| `active` | `Signal` | Whether this option has focus | +For detailed API documentation, inspect the following API references: + +- [`Listbox`](/api/aria/listbox/Listbox) +- [`Option`](/api/aria/listbox/Option) ### Related patterns Listbox is used by these documented dropdown patterns: -- **[Select](guide/aria/select)** - Single-selection dropdown pattern using readonly combobox + listbox -- **[Multiselect](guide/aria/multiselect)** - Multiple-selection dropdown pattern using readonly combobox + listbox with `multi` -- **[Autocomplete](guide/aria/autocomplete)** - Filterable dropdown pattern using combobox + listbox +- [Select](guide/aria/select) - Single-selection dropdown pattern using readonly combobox + listbox +- [Multiselect](guide/aria/multiselect) - Multiple-selection dropdown pattern using readonly combobox + listbox with `multi` +- [Autocomplete](guide/aria/autocomplete) - Filterable dropdown pattern using combobox + listbox For complete dropdown patterns with trigger, popup, and overlay positioning, see those pattern guides instead of using listbox alone. - - - - - diff --git a/adev-es/src/content/guide/aria/menu.en.md b/adev-es/src/content/guide/aria/menu.en.md index 444325c..dc2ebe4 100644 --- a/adev-es/src/content/guide/aria/menu.en.md +++ b/adev-es/src/content/guide/aria/menu.en.md @@ -174,85 +174,82 @@ Disable specific menu items using the `disabled` input. Control focus behavior w When `[softDisabled]="true"`, disabled items can receive focus but cannot be activated. When `[softDisabled]="false"`, disabled items are skipped during keyboard navigation. -## APIs - -### Menu - -The container directive for menu items. - -#### Inputs - -| Property | Type | Default | Description | -| -------------- | --------- | ------- | ------------------------------------------------------------- | -| `disabled` | `boolean` | `false` | Disables all items in the menu | -| `wrap` | `boolean` | `true` | Whether keyboard navigation wraps at edges | -| `softDisabled` | `boolean` | `true` | When `true`, disabled items are focusable but not interactive | - -#### Methods - -| Method | Parameters | Description | -| ---------------- | ---------- | ---------------------------------- | -| `close` | none | Closes the menu | -| `focusFirstItem` | none | Moves focus to the first menu item | - -### MenuBar - -A horizontal container for multiple menus. - -#### Inputs - -| Property | Type | Default | Description | -| -------------- | --------- | ------- | ------------------------------------------------------------- | -| `disabled` | `boolean` | `false` | Disables the entire menubar | -| `wrap` | `boolean` | `true` | Whether keyboard navigation wraps at edges | -| `softDisabled` | `boolean` | `true` | When `true`, disabled items are focusable but not interactive | - -### MenuItem - -An individual item within a menu. - -#### Inputs - -| Property | Type | Default | Description | -| ------------ | --------- | ------- | ---------------------------------------------------- | -| `value` | `any` | — | **Required.** Value for this item | -| `disabled` | `boolean` | `false` | Disables this menu item | -| `submenu` | `Menu` | — | Reference to a submenu | -| `searchTerm` | `string` | `''` | Search term for typeahead (supports two-way binding) | - -#### Signals - -| Property | Type | Description | -| ---------- | ----------------- | ------------------------------------------ | -| `active` | `Signal` | Whether the item currently has focus | -| `expanded` | `Signal` | Whether the submenu is expanded | -| `hasPopup` | `Signal` | Whether the item has an associated submenu | - -NOTE: MenuItem does not expose public methods. Use the `submenu` input to associate submenus with menu items. - -### MenuTrigger - -A button or element that opens a menu. - -#### Inputs - -| Property | Type | Default | Description | -| -------------- | --------- | ------- | ------------------------------------------ | -| `menu` | `Menu` | — | **Required.** The menu to trigger | -| `disabled` | `boolean` | `false` | Disables the trigger | -| `softDisabled` | `boolean` | `true` | When `true`, disabled trigger is focusable | - -#### Signals - -| Property | Type | Description | -| ---------- | ----------------- | ------------------------------------------ | -| `expanded` | `Signal` | Whether the menu is currently open | -| `hasPopup` | `Signal` | Whether the trigger has an associated menu | - -#### Methods - -| Method | Parameters | Description | -| -------- | ---------- | ---------------------------- | -| `open` | none | Opens the menu | -| `close` | none | Closes the menu | -| `toggle` | none | Toggles the menu open/closed | +## Testing + +Angular Aria provides component harnesses for testing menu components. +Here is an example of how to use the harnesses in a component test: + +```typescript +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {HarnessLoader} from '@angular/cdk/testing'; +import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed'; +import {MenuHarness, MenuItemHarness} from '@angular/aria/menu/testing'; +import {MyMenuComponent} from './my-menu'; // Your component + +describe('MyMenuComponent', () => { + let fixture: ComponentFixture; + let loader: HarnessLoader; + + beforeEach(async () => { + TestBed.configureTestingModule({ + imports: [MyMenuComponent], + }); + + fixture = TestBed.createComponent(MyMenuComponent); + await fixture.whenStable(); + loader = TestbedHarnessEnvironment.loader(fixture); + }); + + it('should open menu and click item', async () => { + // Load the menu harness by its trigger text + const menu = await loader.getHarness(MenuHarness.with({triggerText: 'Open Menu'})); + + // Verify initial state + expect(await menu.isOpen()).toBe(false); + + // Open the menu + await menu.open(); + expect(await menu.isOpen()).toBe(true); + + // Get items + const items = await menu.getItems(); + expect(items.length).toBe(3); + expect(await items[0].getText()).toBe('Item 1'); + + // Click first item + await items[0].click(); + + // Menu should close after selection (depending on your implementation) + expect(await menu.isOpen()).toBe(false); + }); + + it('should interact with submenus', async () => { + const menu = await loader.getHarness(MenuHarness.with({triggerText: 'Open Menu'})); + await menu.open(); + + // Get the item that triggers a submenu + const subItem = await loader.getHarness(MenuItemHarness.with({text: 'Submenu'})); + expect(await subItem.hasSubmenu()).toBe(true); + + // Open submenu + await subItem.click(); + const submenu = await subItem.getSubmenu(); + expect(submenu).toBeTruthy(); + expect(await submenu!.isOpen()).toBe(true); + + // Interact with submenu items + const subItems = await submenu!.getItems(); + expect(subItems.length).toBe(1); + }); +}); +``` + +## API reference + +For detailed API documentation, inspect the following API references: + +- [`Menu`](/api/aria/menu/Menu) +- [`MenuBar`](/api/aria/menu/MenuBar) +- [`MenuItem`](/api/aria/menu/MenuItem) +- [`MenuTrigger`](/api/aria/menu/MenuTrigger) +- [`MenuContent`](/api/aria/menu/MenuContent) diff --git a/adev-es/src/content/guide/aria/menubar.en.md b/adev-es/src/content/guide/aria/menubar.en.md index 1abedb0..80f8221 100644 --- a/adev-es/src/content/guide/aria/menubar.en.md +++ b/adev-es/src/content/guide/aria/menubar.en.md @@ -8,7 +8,7 @@ ## Overview -The manubar is a horizontal navigation bar that provides persistent access to application menus. Menubars organize commands into logical categories like File, Edit, and View, helping users discover and execute application features through keyboard or mouse interaction. +The menubar is a horizontal navigation bar that provides persistent access to application menus. Menubars organize commands into logical categories like File, Edit, and View, helping users discover and execute application features through keyboard or mouse interaction. @@ -164,35 +164,61 @@ Menubars automatically adapt to right-to-left (RTL) languages. Arrow key navigat The `dir="rtl"` attribute enables RTL mode. Left arrow moves right, Right arrow moves left, maintaining natural navigation for RTL language users. -## APIs +## Testing -The menubar pattern uses directives from Angular's Aria library. See the [Menu guide](guide/aria/menu) for complete API documentation. +Angular Aria provides component harnesses for testing menubar components. +Here is an example of how to use the harnesses in a component test: -### MenuBar +```typescript +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {HarnessLoader} from '@angular/cdk/testing'; +import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed'; +import {MenuHarness} from '@angular/aria/menu/testing'; +import {MyMenubarComponent} from './my-menubar'; // Your component -The horizontal container for top-level menu items. +describe('MyMenubarComponent', () => { + let fixture: ComponentFixture; + let loader: HarnessLoader; -#### Inputs + beforeEach(async () => { + TestBed.configureTestingModule({ + imports: [MyMenubarComponent], + }); -| Property | Type | Default | Description | -| -------------- | --------- | ------- | ------------------------------------------------------------- | -| `disabled` | `boolean` | `false` | Disables the entire menubar | -| `wrap` | `boolean` | `true` | Whether keyboard navigation wraps from last to first item | -| `softDisabled` | `boolean` | `true` | When `true`, disabled items are focusable but not interactive | + fixture = TestBed.createComponent(MyMenubarComponent); + await fixture.whenStable(); + loader = TestbedHarnessEnvironment.loader(fixture); + }); -See the [Menu API documentation](guide/aria/menu#apis) for complete details on all available inputs and signals. + it('should interact with menubar items', async () => { + // Load the menubar harness (which is a MenuHarness with selector '[ngMenuBar]') + const menubar = await loader.getHarness(MenuHarness.with({selector: '[ngMenuBar]'})); -### MenuItem + // Menubars are persistent and always "open" + expect(await menubar.isOpen()).toBe(true); + expect(await menubar.isMenuBar()).toBe(true); -Individual items within the menubar. Same API as Menu - see [MenuItem](guide/aria/menu#menuitem). + // Get top-level items + const items = await menubar.getItems(); + expect(items.length).toBe(2); + expect(await items[0].getText()).toBe('File'); + expect(await items[1].getText()).toBe('Edit'); -**Menubar-specific behavior:** + // Click an item to open its dropdown menu + await items[0].click(); -- Left/Right arrows navigate between menubar items (vs Up/Down in vertical menus) -- First keyboard interaction or click enables hover-to-open for submenus -- Enter or Down arrow opens the submenu and focuses the first item -- `aria-haspopup="menu"` indicates items with submenus + const fileMenu = await items[0].getSubmenu(); + expect(fileMenu).toBeTruthy(); + expect(await fileMenu!.isOpen()).toBe(true); + }); +}); +``` -### MenuTrigger +## API reference -Not typically used in menubars - MenuItem handles trigger behavior directly when it has an associated submenu. See [MenuTrigger](guide/aria/menu#menutrigger) for standalone menu trigger patterns. +For detailed API documentation, inspect the following API references: + +- [`MenuBar`](/api/aria/menu/MenuBar) +- [`MenuItem`](/api/aria/menu/MenuItem) +- [`MenuTrigger`](/api/aria/menu/MenuTrigger) +- [`Menu`](/api/aria/menu/Menu) diff --git a/adev-es/src/content/guide/aria/multiselect.en.md b/adev-es/src/content/guide/aria/multiselect.en.md index dc27f4a..c430db6 100644 --- a/adev-es/src/content/guide/aria/multiselect.en.md +++ b/adev-es/src/content/guide/aria/multiselect.en.md @@ -3,7 +3,7 @@ ## Overview -A pattern that combines readonly combobox with multi-enabled listbox to create multiple-selection dropdowns with keyboard navigation and screen reader support. +The multiselect pattern combines a read-only combobox trigger with a multi-select listbox popup to create highly accessible multiple-selection dropdowns with keyboard navigation and screen reader support. @@ -157,45 +157,70 @@ Forms sometimes need to limit the number of selections or validate user choices. -This example limits selections to three items. When the limit is reached, unselected options become disabled, preventing additional selections. A message informs users about the constraint. +This example limits selections to two items. When the limit is reached, unselected options are disabled to prevent further selections, and the combobox display updates to reflect the choices. -## APIs +## Testing -The multiselect pattern uses the following directives from Angular's Aria library. See the full API documentation in the linked guides. +The multiselect pattern can be tested using a combination of `ComboboxHarness` and `ListboxHarness` from `@angular/aria/combobox/testing` and `@angular/aria/listbox/testing`. +Here is an example of how to use the harnesses to test a multiselect component: -### Combobox Directives +```typescript +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {HarnessLoader} from '@angular/cdk/testing'; +import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed'; +import {ComboboxHarness} from '@angular/aria/combobox/testing'; +import {ListboxHarness} from '@angular/aria/listbox/testing'; +import {MyMultiselectComponent} from './my-multiselect'; // Your component -The multiselect pattern uses `ngCombobox` with the `readonly` attribute to prevent text input while preserving keyboard navigation. +describe('MyMultiselectComponent', () => { + let fixture: ComponentFixture; + let loader: HarnessLoader; -#### Inputs + beforeEach(async () => { + TestBed.configureTestingModule({ + imports: [MyMultiselectComponent], + }); -| Property | Type | Default | Description | -| ---------- | --------- | ------- | ----------------------------------------- | -| `readonly` | `boolean` | `false` | Set to `true` to create dropdown behavior | -| `disabled` | `boolean` | `false` | Disables the entire multiselect | + fixture = TestBed.createComponent(MyMultiselectComponent); + await fixture.whenStable(); + loader = TestbedHarnessEnvironment.loader(fixture); + }); -See the [Combobox API documentation](guide/aria/combobox#apis) for complete details on all available inputs and signals. + it('should allow selecting multiple options', async () => { + const select = await loader.getHarness(ComboboxHarness); -### Listbox Directives + // Open the dropdown + await select.open(); -The multiselect pattern uses `ngListbox` with the `multi` attribute for multiple selection and `ngOption` for each selectable item. + // Get the listbox harness from the popup + const listbox = await select.getPopupWidget(ListboxHarness); + expect(await listbox.isMulti()).toBe(true); -#### Inputs + const options = await listbox.getOptions(); -| Property | Type | Default | Description | -| -------- | --------- | ------- | ------------------------------------------ | -| `multi` | `boolean` | `false` | Set to `true` to enable multiple selection | + // Select first and second options + await options[0].click(); + await options[1].click(); -#### Model + // Verify both options are selected + expect(await options[0].isSelected()).toBe(true); + expect(await options[1].isSelected()).toBe(true); -| Property | Type | Description | -| -------- | ------- | ----------------------------------------- | -| `values` | `any[]` | Two-way bindable array of selected values | + // Close the dropdown + await select.close(); -When `multi` is true, users can select multiple options using Space to toggle selection. The popup remains open after selection, allowing additional choices. + // Verify value is updated (e.g., comma separated list or count) + expect(await (await select.host()).text()).toContain('Option 1, Option 2'); + }); +}); +``` -See the [Listbox API documentation](guide/aria/listbox#apis) for complete details on listbox configuration, selection modes, and option properties. +## API reference -### Positioning +For detailed API documentation, inspect the following API references: -The multiselect pattern integrates with [CDK Overlay](api/cdk/overlay/CdkConnectedOverlay) for smart positioning. Use `cdkConnectedOverlay` to handle viewport edges and scrolling automatically. +- [`Combobox`](/api/aria/combobox/Combobox) +- [`ComboboxPopup`](/api/aria/combobox/ComboboxPopup) +- [`ComboboxWidget`](/api/aria/combobox/ComboboxWidget) +- [`Listbox`](/api/aria/listbox/Listbox) +- [`Option`](/api/aria/listbox/Option) diff --git a/adev-es/src/content/guide/aria/overview.en.md b/adev-es/src/content/guide/aria/overview.en.md index 9536e59..159425c 100644 --- a/adev-es/src/content/guide/aria/overview.en.md +++ b/adev-es/src/content/guide/aria/overview.en.md @@ -3,15 +3,26 @@ ## What is Angular Aria? -Building accessible components seems straightforward, but implementing them according to the W3C Accessibility Guidelines requires significant effort and accessibility expertise. +Building accessible components seems straightforward, but implementing them according to the [W3C Accessibility Guidelines](https://www.w3.org/TR/wcag/) requires significant effort and accessibility expertise. -Angular Aria is a collection of headless, accessible directives that implement common WAI-ARIA patterns. The directives handle keyboard interactions, ARIA attributes, focus management, and screen reader support. All you have to do is provide the HTML structure, CSS styling, and business logic! +Angular Aria is a collection of headless, accessible directives that implement common [WAI-ARIA patterns](https://www.w3.org/WAI/ARIA/apg/patterns/). The directives handle keyboard interactions, ARIA attributes, focus management, and screen reader support. All you have to do is provide the HTML structure, CSS styling, and business logic! ## Installation -```shell -npm install @angular/aria -``` + + + npm install @angular/aria + + + yarn add @angular/aria + + + pnpm add @angular/aria + + + bun add @angular/aria + + ## Showcase diff --git a/adev-es/src/content/guide/aria/select.en.md b/adev-es/src/content/guide/aria/select.en.md index fa7f431..b4d02cb 100644 --- a/adev-es/src/content/guide/aria/select.en.md +++ b/adev-es/src/content/guide/aria/select.en.md @@ -3,7 +3,7 @@ ## Overview -A pattern that combines readonly combobox with listbox to create single-selection dropdowns with keyboard navigation and screen reader support. +A pattern that combines a combobox with a listbox to create single-selection dropdowns with keyboard navigation and screen reader support. @@ -65,7 +65,7 @@ The select pattern combines [Combobox](guide/aria/combobox) and [Listbox](guide/ ### Basic select -Users need a standard dropdown to choose from a list of values. A readonly combobox paired with a listbox provides the familiar select experience with full accessibility support. +Users need a standard dropdown to choose from a list of values. A combobox paired with a listbox provides the familiar select experience with full accessibility support. @@ -93,7 +93,7 @@ Users need a standard dropdown to choose from a list of values. A readonly combo -The `readonly` attribute on `ngCombobox` prevents text input while preserving keyboard navigation. Users interact with the dropdown using arrow keys and Enter, just like a native select element. +Text input is prevented by applying the `ngCombobox` directive directly onto a non-interactive host element (such as a `div` or a `button`) instead of an ``. Users interact with the dropdown using arrow keys and Enter, just like a native select element. ### Select with custom display @@ -159,35 +159,69 @@ Selects can be disabled to prevent user interaction when certain form conditions When disabled, the select shows a disabled visual state and blocks all user interaction. Screen readers announce the disabled state to assistive technology users. -## APIs +## Testing -The select pattern uses the following directives from Angular's Aria library. See the full API documentation in the linked guides. +The select pattern can be tested using a combination of `ComboboxHarness` and `ListboxHarness` from `@angular/aria/combobox/testing` and `@angular/aria/listbox/testing`. +Here is an example of how to use the harnesses to test a select component: -### Combobox Directives +```typescript +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {HarnessLoader} from '@angular/cdk/testing'; +import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed'; +import {ComboboxHarness} from '@angular/aria/combobox/testing'; +import {ListboxHarness} from '@angular/aria/listbox/testing'; +import {MySelectComponent} from './my-select'; // Your component -The select pattern uses `ngCombobox` with the `readonly` attribute to prevent text input while preserving keyboard navigation. +describe('MySelectComponent', () => { + let fixture: ComponentFixture; + let loader: HarnessLoader; -#### Inputs + beforeEach(async () => { + TestBed.configureTestingModule({ + imports: [MySelectComponent], + }); -| Property | Type | Default | Description | -| ---------- | --------- | ------- | ----------------------------------------- | -| `readonly` | `boolean` | `false` | Set to `true` to create dropdown behavior | -| `disabled` | `boolean` | `false` | Disables the entire select | + fixture = TestBed.createComponent(MySelectComponent); + await fixture.whenStable(); + loader = TestbedHarnessEnvironment.loader(fixture); + }); -See the [Combobox API documentation](guide/aria/combobox#apis) for complete details on all available inputs and signals. + it('should allow selecting an option', async () => { + // Load the combobox harness (which acts as the select trigger) + const select = await loader.getHarness(ComboboxHarness); -### Listbox Directives + // Verify it is closed initially + expect(await select.isOpen()).toBe(false); -The select pattern uses `ngListbox` for the dropdown list and `ngOption` for each selectable item. + // Open the dropdown + await select.open(); + expect(await select.isOpen()).toBe(true); -#### Model + // Get the listbox harness from the popup + const listbox = await select.getPopupWidget(ListboxHarness); + const options = await listbox.getOptions(); + expect(options.length).toBe(3); -| Property | Type | Description | -| -------- | ------- | ---------------------------------------------------------------------------- | -| `values` | `any[]` | Two-way bindable array of selected values (contains single value for select) | + // Click the second option + await options[1].click(); -See the [Listbox API documentation](guide/aria/listbox#apis) for complete details on listbox configuration, selection modes, and option properties. + // Verify the dropdown closed and the value updated + expect(await select.isOpen()).toBe(false); + expect(await (await select.host()).text()).toContain('Option 2'); + }); +}); +``` + +## API reference + +For detailed API documentation, inspect the following API references: + +- [`Combobox`](/api/aria/combobox/Combobox) +- [`ComboboxPopup`](/api/aria/combobox/ComboboxPopup) +- [`ComboboxWidget`](/api/aria/combobox/ComboboxWidget) +- [`Listbox`](/api/aria/listbox/Listbox) +- [`Option`](/api/aria/listbox/Option) ### Positioning -The select pattern integrates with [CDK Overlay](api/cdk/overlay/CdkConnectedOverlay) for smart positioning. Use `cdkConnectedOverlay` to handle viewport edges and scrolling automatically. +The select pattern integrates with [CDK Overlay](https://material.angular.io/cdk/overlay/overview) for smart positioning. Use `cdkConnectedOverlay` to handle viewport edges and scrolling automatically. diff --git a/adev-es/src/content/guide/aria/tabs.en.md b/adev-es/src/content/guide/aria/tabs.en.md index 29a292c..b7d703e 100644 --- a/adev-es/src/content/guide/aria/tabs.en.md +++ b/adev-es/src/content/guide/aria/tabs.en.md @@ -224,73 +224,72 @@ Disable specific tabs to prevent user interaction. Control whether disabled tabs When `[softDisabled]="true"` on the tab list, disabled tabs can receive focus but cannot be activated. When `[softDisabled]="false"`, disabled tabs are skipped during keyboard navigation. -## APIs - -### Tabs - -The container directive that coordinates tab lists and panels. - -This directive has no inputs or outputs. It serves as the root container for `ngTabList`, `ngTab`, and `ngTabPanel` directives. - -### TabList - -The container for tab buttons that manages selection and keyboard navigation. - -#### Inputs - -| Property | Type | Default | Description | -| --------------- | ---------------------------- | -------------- | ------------------------------------------------------------------ | -| `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Tab list layout direction | -| `wrap` | `boolean` | `false` | Whether keyboard navigation wraps from last to first tab | -| `softDisabled` | `boolean` | `true` | When `true`, disabled tabs are focusable but not activatable | -| `selectionMode` | `'follow' \| 'explicit'` | `'follow'` | Whether tabs activate on focus or require explicit activation | -| `selectedTab` | `any` | — | The value of the currently selected tab (supports two-way binding) | - -### Tab - -An individual tab button. - -#### Inputs - -| Property | Type | Default | Description | -| ---------- | --------- | ------- | --------------------------------------- | -| `value` | `any` | — | **Required.** Unique value for this tab | -| `disabled` | `boolean` | `false` | Disables this tab | - -#### Signals - -| Property | Type | Description | -| ---------- | ----------------- | ------------------------------------- | -| `selected` | `Signal` | Whether the tab is currently selected | -| `active` | `Signal` | Whether the tab currently has focus | - -### TabPanel - -The content panel associated with a tab. - -#### Inputs - -| Property | Type | Default | Description | -| ----------------- | --------- | ------- | ---------------------------------------------------------- | -| `value` | `any` | — | **Required.** Must match the `value` of the associated tab | -| `preserveContent` | `boolean` | `true` | Whether to keep panel content in DOM after deactivation | - -#### Signals - -| Property | Type | Description | -| --------- | ----------------- | -------------------------------------- | -| `visible` | `Signal` | Whether the panel is currently visible | - -### TabContent +## Testing + +Angular Aria provides component harnesses for testing tabs components. +Here is an example of how to use the harnesses in a component test: + +```typescript +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed'; +import {ComponentHarness, HarnessLoader} from '@angular/cdk/testing'; +import {TabsHarness} from '@angular/aria/tabs/testing'; +import {MyTabsComponent} from './my-tabs'; // Your component + +// A simple harness to help query content inside the tab panel +class TestContentHarness extends ComponentHarness { + static hostSelector = '.test-content'; + async getText(): Promise { + return (await this.host()).text(); + } +} + +describe('MyTabsComponent', () => { + let fixture: ComponentFixture; + let loader: HarnessLoader; + + beforeEach(async () => { + TestBed.configureTestingModule({ + imports: [MyTabsComponent], + }); + + fixture = TestBed.createComponent(MyTabsComponent); + await fixture.whenStable(); + loader = TestbedHarnessEnvironment.loader(fixture); + }); + + it('should switch tabs and scope panel queries', async () => { + const tabs = await loader.getHarness(TabsHarness); + + // Get all tabs + const tabItems = await tabs.getTabs(); + expect(tabItems.length).toBe(3); + + // Verify initial selection + expect(await tabItems[0].isSelected()).toBe(true); + expect(await tabItems[1].isSelected()).toBe(false); + + // Query content inside the active tab panel + // TabHarness automatically scopes queries to its associated panel + const content = await tabItems[0].getHarness(TestContentHarness); + expect(await content.getText()).toBe('Content 1'); + + // Switch to the second tab + await tabItems[1].select(); + + // Verify selection updated + expect(await tabItems[0].isSelected()).toBe(false); + expect(await tabItems[1].isSelected()).toBe(true); + }); +}); +``` -A structural directive for lazy rendering tab panel content. +## API reference -This directive has no inputs, outputs, or methods. Apply it to an `ng-template` element inside a tab panel: +For detailed API documentation, inspect the following API references: -```angular-html -
    - - - -
    -``` +- [`Tabs`](/api/aria/tabs/Tabs) +- [`TabList`](/api/aria/tabs/TabList) +- [`Tab`](/api/aria/tabs/Tab) +- [`TabPanel`](/api/aria/tabs/TabPanel) +- [`TabContent`](/api/aria/tabs/TabContent) diff --git a/adev-es/src/content/guide/aria/toolbar.en.md b/adev-es/src/content/guide/aria/toolbar.en.md index 3f204d5..95a1bb7 100644 --- a/adev-es/src/content/guide/aria/toolbar.en.md +++ b/adev-es/src/content/guide/aria/toolbar.en.md @@ -135,22 +135,14 @@ The `multi` input controls whether multiple widgets within a group can be select ```html {highlight: [15]} -
    +
    -
    +
    @@ -222,56 +214,53 @@ Toolbars automatically support right-to-left languages. Wrap the toolbar in a co -## APIs +## Testing -### Toolbar Directive +Angular Aria provides component harnesses for testing toolbar components. +Here is an example of how to use the harnesses in a component test: -The `ngToolbar` directive provides the container for toolbar functionality. +```typescript +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {HarnessLoader} from '@angular/cdk/testing'; +import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed'; +import {ToolbarHarness} from '@angular/aria/toolbar/testing'; +import {MyToolbarComponent} from './my-toolbar'; // Your component -#### Inputs +describe('MyToolbarComponent', () => { + let fixture: ComponentFixture; + let loader: HarnessLoader; -| Property | Type | Default | Description | -| -------------- | ------------------------------ | -------------- | ------------------------------------------------------ | -| `orientation` | `'vertical'` \| `'horizontal'` | `'horizontal'` | Whether toolbar is vertically or horizontally oriented | -| `disabled` | `boolean` | `false` | Disables the entire toolbar | -| `softDisabled` | `boolean` | `true` | Whether disabled items can receive focus | -| `wrap` | `boolean` | `true` | Whether focus should wrap at the edges | + beforeEach(async () => { + TestBed.configureTestingModule({ + imports: [MyToolbarComponent], + }); -### ToolbarWidget Directive + fixture = TestBed.createComponent(MyToolbarComponent); + await fixture.whenStable(); + loader = TestbedHarnessEnvironment.loader(fixture); + }); -The `ngToolbarWidget` directive marks an element as a navigable widget within the toolbar. + it('should have widgets and allow selection', async () => { + // Load the toolbar harness + const toolbar = await loader.getHarness(ToolbarHarness); -#### Inputs + // Get all widgets + const widgets = await toolbar.getWidgets(); + expect(widgets.length).toBe(3); -| Property | Type | Default | Description | -| ---------- | --------- | ------- | ----------------------------------------------- | -| `id` | `string` | auto | Unique identifier for the widget | -| `disabled` | `boolean` | `false` | Disables the widget | -| `value` | `V` | - | The value associated with the widget (required) | + // Click the first widget + await widgets[0].click(); -#### Signals - -| Property | Type | Description | -| ---------- | ----------------- | ------------------------------------------- | -| `active` | `Signal` | Whether the widget is currently focused | -| `selected` | `Signal` | Whether the widget is selected (in a group) | - -### ToolbarWidgetGroup Directive - -The `ngToolbarWidgetGroup` directive groups related widgets together. - -#### Inputs - -| Property | Type | Default | Description | -| ---------- | --------- | ------- | ---------------------------------------- | -| `disabled` | `boolean` | `false` | Disables all widgets in the group | -| `multi` | `boolean` | `false` | Whether multiple widgets can be selected | + // Verify selection state + expect(await widgets[0].isSelected()).toBe(true); + }); +}); +``` -### Related components +## API reference -Toolbar can contain various widget types including buttons, trees, and comboboxes. See individual component documentation for specific widget implementations. +For detailed API documentation, inspect the following API references: - - - - +- [`Toolbar`](/api/aria/toolbar/Toolbar) +- [`ToolbarWidget`](/api/aria/toolbar/ToolbarWidget) +- [`ToolbarWidgetGroup`](/api/aria/toolbar/ToolbarWidgetGroup) diff --git a/adev-es/src/content/guide/aria/tree.en.md b/adev-es/src/content/guide/aria/tree.en.md index 712ab9c..9e7b934 100644 --- a/adev-es/src/content/guide/aria/tree.en.md +++ b/adev-es/src/content/guide/aria/tree.en.md @@ -157,74 +157,68 @@ Disable specific tree nodes to prevent interaction. Control whether disabled ite When `[softDisabled]="true"` on the tree, disabled items can receive focus but cannot be activated or selected. When `[softDisabled]="false"`, disabled items are skipped during keyboard navigation. -## APIs - -### Tree - -The container directive that manages hierarchical navigation and selection. - -#### Inputs - -| Property | Type | Default | Description | -| --------------- | -------------------------------- | ------------ | ------------------------------------------------------------- | -| `disabled` | `boolean` | `false` | Disables the entire tree | -| `softDisabled` | `boolean` | `true` | When `true`, disabled items are focusable but not interactive | -| `multi` | `boolean` | `false` | Whether multiple items can be selected | -| `selectionMode` | `'explicit' \| 'follow'` | `'explicit'` | Whether selection requires explicit action or follows focus | -| `nav` | `boolean` | `false` | Whether the tree is in navigation mode (uses `aria-current`) | -| `wrap` | `boolean` | `true` | Whether keyboard navigation wraps from last to first item | -| `focusMode` | `'roving' \| 'activedescendant'` | `'roving'` | Focus strategy used by the tree | -| `values` | `any[]` | `[]` | Selected item values (supports two-way binding) | - -#### Methods - -| Method | Parameters | Description | -| ---------------- | ---------- | --------------------------------------------- | -| `expandAll` | none | Expands all tree nodes | -| `collapseAll` | none | Collapses all tree nodes | -| `selectAll` | none | Selects all items (only in multi-select mode) | -| `clearSelection` | none | Clears all selection | - -### TreeItem - -An individual node in the tree that can contain child nodes. - -#### Inputs - -| Property | Type | Default | Description | -| ---------- | --------- | ------- | ------------------------------------------------------- | -| `value` | `any` | — | **Required.** Unique value for this tree item | -| `disabled` | `boolean` | `false` | Disables this item | -| `expanded` | `boolean` | `false` | Whether the node is expanded (supports two-way binding) | - -#### Signals - -| Property | Type | Description | -| ------------- | ----------------- | ------------------------------------ | -| `selected` | `Signal` | Whether the item is selected | -| `active` | `Signal` | Whether the item currently has focus | -| `hasChildren` | `Signal` | Whether the item has child nodes | - -#### Methods - -| Method | Parameters | Description | -| ---------- | ---------- | --------------------------- | -| `expand` | none | Expands this node | -| `collapse` | none | Collapses this node | -| `toggle` | none | Toggles the expansion state | - -### TreeGroup +## Testing + +Angular Aria provides component harnesses for testing tree components. +Here is an example of how to use the harnesses in a component test: + +```typescript +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {HarnessLoader} from '@angular/cdk/testing'; +import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed'; +import {TreeHarness} from '@angular/aria/tree/testing'; +import {MyTreeComponent} from './my-tree'; // Your component + +describe('MyTreeComponent', () => { + let fixture: ComponentFixture; + let loader: HarnessLoader; + + beforeEach(async () => { + TestBed.configureTestingModule({ + imports: [MyTreeComponent], + }); + + fixture = TestBed.createComponent(MyTreeComponent); + await fixture.whenStable(); + loader = TestbedHarnessEnvironment.loader(fixture); + }); + + it('should navigate and expand tree items', async () => { + const tree = await loader.getHarness(TreeHarness); + + // Get top-level structure representation + expect(await tree.getTreeStructure()).toEqual({ + children: [{text: 'public'}, {text: 'src'}, {text: 'package.json'}], + }); + + // Get all items (currently visible) + const items = await tree.getItems(); + expect(items.length).toBe(3); + + // Expand the first item ('public') + expect(await items[0].isExpanded()).toBe(false); + await items[0].click(); + expect(await items[0].isExpanded()).toBe(true); + + // Verifying tree structure updates after expansion + expect(await tree.getTreeStructure()).toEqual({ + children: [ + { + text: 'public', + children: [{text: 'index.html'}, {text: 'styles.css'}], + }, + {text: 'src'}, + {text: 'package.json'}, + ], + }); + }); +}); +``` -A container for child tree items. +## API reference -This directive has no inputs, outputs, or methods. It serves as a container to organize child `ngTreeItem` elements: +For detailed API documentation, inspect the following API references: -```angular-html -
  • - Parent Item -
      -
    • Child 1
    • -
    • Child 2
    • -
    -
  • -``` +- [`Tree`](/api/aria/tree/Tree) +- [`TreeItem`](/api/aria/tree/TreeItem) +- [`TreeItemGroup`](/api/aria/tree/TreeItemGroup) diff --git a/adev-es/src/content/guide/components/advanced-configuration.en.md b/adev-es/src/content/guide/components/advanced-configuration.en.md index 4e71034..87a47d4 100644 --- a/adev-es/src/content/guide/components/advanced-configuration.en.md +++ b/adev-es/src/content/guide/components/advanced-configuration.en.md @@ -7,12 +7,12 @@ TIP: This guide assumes you've already read the [Essentials Guide](essentials). The `@Component` decorator accepts a `changeDetection` option that controls the component's **change detection mode**. There are two change detection mode options. -**`ChangeDetectionStrategy.Default`** is, unsurprisingly, the default strategy. In this mode, +**`ChangeDetectionStrategy.Eager`/`Default`** is an optional mode. In this mode, Angular checks whether the component's DOM needs an update whenever any activity may have occurred application-wide. Activities that trigger this checking include user interaction, network response, timers, and more. -**`ChangeDetectionStrategy.OnPush`** is an optional mode that reduces the amount of checking Angular +**`ChangeDetectionStrategy.OnPush`** is the default strategy (since v22). This mode reduces the amount of checking Angular needs to perform. In this mode, the framework only checks if a component's DOM needs an update when: - A component input has changes as a result of a binding in a template, or @@ -40,7 +40,7 @@ import {Component, CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; @Component({ ..., schemas: [CUSTOM_ELEMENTS_SCHEMA], - template: '' + template: '' }) export class ComponentWithCustomElements { } ``` diff --git a/adev-es/src/content/guide/components/anatomy-of-components.en.md b/adev-es/src/content/guide/components/anatomy-of-components.en.md index 5a301b2..e08682b 100644 --- a/adev-es/src/content/guide/components/anatomy-of-components.en.md +++ b/adev-es/src/content/guide/components/anatomy-of-components.en.md @@ -11,13 +11,13 @@ Every component must have: You provide Angular-specific information for a component by adding a `@Component` [decorator](https://www.typescriptlang.org/docs/handbook/decorators.html) on top of the TypeScript class: - +```angular-ts {highlight: [1, 2, 3, 4]} @Component({ selector: 'profile-photo', - template: `Your profile photo`, + template: `Your profile photo`, }) -export class ProfilePhoto { } - +export class ProfilePhoto {} +``` For full details on writing Angular templates, including data binding, event handling, and control flow, see the [Templates guide](guide/templates). @@ -25,29 +25,33 @@ The object passed to the `@Component` decorator is called the component's **meta Components can optionally include a list of CSS styles that apply to that component's DOM: - +```angular-ts {highlight: [4]} @Component({ selector: 'profile-photo', - template: `Your profile photo`, - styles: `img { border-radius: 50%; }`, + template: `Your profile photo`, + styles: ` + img { + border-radius: 50%; + } + `, }) -export class ProfilePhoto { } - +export class ProfilePhoto {} +``` By default, a component's styles only affect elements defined in that component's template. See [Styling Components](guide/components/styling) for details on Angular's approach to styling. You can alternatively choose to write your template and styles in separate files: - +```ts {highlight: [3,4]} @Component({ selector: 'profile-photo', templateUrl: 'profile-photo.html', styleUrl: 'profile-photo.css', }) -export class ProfilePhoto { } - +export class ProfilePhoto {} +``` -This can help separate the concerns of _presentation_ from _behavior_ in your project. You can choose one approach for your entire project, or you decide which to use for each component. +This can help separate the concerns of _presentation_ from _behavior_ in your project. You can choose one approach for your entire project, or you can decide which to use for each component. Both `templateUrl` and `styleUrl` are relative to the directory in which the component resides. @@ -67,41 +71,41 @@ import {ProfilePhoto} from './profile-photo'; imports: [ProfilePhoto], /* ... */ }) -export class UserProfile { } +export class UserProfile {} ``` -By default, Angular components are _standalone_, meaning that you can directly add them to the `imports` array of other components. Components created with an earlier version of Angular may instead specify `standalone: false` in their `@Component` decorator. For these components, you instead import the `NgModule` in which the component is defined. See the full [`NgModule` guide](guide/ngmodules) for details. +By default, Angular components are _standalone_, meaning that you can directly add them to the `imports` array of other components. Components created with an earlier version of Angular may instead specify `standalone: false` in their `@Component` decorator. For these components, you instead import the `NgModule` in which the component is defined. See the full [`NgModule` guide](guide/ngmodules/overview) for details. -Important: In Angular versions before 19.0.0, the `standalone` option defaults to `false`. +IMPORTANT: In Angular versions before 19.0.0, the `standalone` option defaults to `false`. ### Showing components in a template Every component defines a [CSS selector](https://developer.mozilla.org/docs/Learn/CSS/Building_blocks/Selectors): - +```angular-ts {highlight: [2]} @Component({ selector: 'profile-photo', ... }) export class ProfilePhoto { } - +``` See [Component Selectors](guide/components/selectors) for details about which types of selectors Angular supports and guidance on choosing a selector. You show a component by creating a matching HTML element in the template of _other_ components: - +```angular-ts {highlight: [8]} @Component({ selector: 'profile-photo', }) -export class ProfilePhoto { } +export class ProfilePhoto {} @Component({ -imports: [ProfilePhoto], -template: `` + imports: [ProfilePhoto], + template: ``, }) -export class UserProfile { } - +export class UserProfile {} +``` Angular creates an instance of the component for every matching HTML element it encounters. The DOM element that matches a component's selector is referred to as that component's **host element**. The contents of a component's template are rendered inside its host element. diff --git a/adev-es/src/content/guide/components/content-projection.en.md b/adev-es/src/content/guide/components/content-projection.en.md index efa351c..202af3e 100644 --- a/adev-es/src/content/guide/components/content-projection.en.md +++ b/adev-es/src/content/guide/components/content-projection.en.md @@ -10,7 +10,9 @@ example, you may want to create a custom card component: selector: 'custom-card', template: '
    ', }) -export class CustomCard {/* ... */} +export class CustomCard { + /* ... */ +} ``` **You can use the `` element as a placeholder to mark where content should go**: @@ -20,7 +22,9 @@ export class CustomCard {/* ... */} selector: 'custom-card', template: '
    ', }) -export class CustomCard {/* ... */} +export class CustomCard { + /* ... */ +} ``` TIP: `` works similarly @@ -40,7 +44,9 @@ rendered, or **projected**, at the location of that ``:
    `, }) -export class CustomCard {/* ... */} +export class CustomCard { + /* ... */ +} ``` ```angular-html @@ -95,13 +101,13 @@ export class CardBody {} ```angular-ts -Component({ +@Component({ selector: 'custom-card', template: `
    - +
    - +
    `, }) @@ -144,10 +150,10 @@ did not match a `select` attribute: ```angular-html
    - +
    - +
    ``` @@ -216,9 +222,9 @@ placeholder, Angular compares against the `ngProjectAs` value instead of the ele ```angular-html
    - +
    - +
    ``` @@ -243,3 +249,29 @@ placeholder, Angular compares against the `ngProjectAs` value instead of the ele ``` `ngProjectAs` supports only static values and cannot be bound to dynamic expressions. + +## Caveats + +### Projected content lives in the parent's view + +Even though projected content is _rendered_ inside the receiving component, it is still owned by the component that declared it. Angular tracks it as part of the parent's view, which has a couple of side effects worth knowing about. + +**Change detection:** Projected content is checked when the _parent_ runs change detection. If the receiving component uses `OnPush`, Angular can skip checking that component's own template — but it won't skip the projected content, because that belongs to the parent. + +```angular-html + + + + + +``` + +**Dependency injection:** Projected content gets its dependencies from the parent's injector, not from the receiving component's `viewProviders`. See [Providers and viewProviders](guide/di/hierarchical-dependency-injection) for details. + +### Some library components don't support projected children + +Certain components — menus, tabs, lists — use `ContentChildren` to find their children and wire up behavior like keyboard navigation, focus management, or ARIA attributes. They're written assuming they own their children directly, so projecting external content into them tends to break things in subtle ways. + +For example, wrapping `` elements in an extra layer and projecting them into `` can silently break keyboard navigation and screen reader support. The query still finds the items, but the internal setup that makes them interactive may not work correctly when the items come from a different view context. + +If a library component manages its children's behavior, check its docs before reaching for content projection — it may not be supported. diff --git a/adev-es/src/content/guide/components/content-projection.md b/adev-es/src/content/guide/components/content-projection.md index 5d6ccd9..24bf9b3 100644 --- a/adev-es/src/content/guide/components/content-projection.md +++ b/adev-es/src/content/guide/components/content-projection.md @@ -242,3 +242,29 @@ Angular compara contra el valor de `ngProjectAs` en lugar de la identidad del el ``` `ngProjectAs` solo admite valores estáticos y no se puede enlazar a expresiones dinámicas. + +## Advertencias + +### El contenido proyectado vive en la vista del padre + +Aunque el contenido proyectado se _renderiza_ dentro del componente receptor, sigue siendo propiedad del componente que lo declaró. Angular lo rastrea como parte de la vista del padre, lo que tiene un par de efectos secundarios que vale la pena conocer. + +**Detección de cambios:** El contenido proyectado se verifica cuando el _padre_ ejecuta la detección de cambios. Si el componente receptor usa `OnPush`, Angular puede saltarse la verificación de la propia plantilla de ese componente — pero no se saltará el contenido proyectado, porque ese pertenece al padre. + +```angular-html + + + + + +``` + +**Inyección de dependencias:** El contenido proyectado obtiene sus dependencias del injector del padre, no de los `viewProviders` del componente receptor. Consulta [Providers y viewProviders](guide/di/hierarchical-dependency-injection) para más detalles. + +### Algunos componentes de librería no soportan hijos proyectados + +Ciertos componentes — menús, pestañas, listas — usan `ContentChildren` para encontrar a sus hijos y conectar comportamientos como navegación con teclado, gestión del foco o atributos ARIA. Están escritos asumiendo que son propietarios directos de sus hijos, por lo que proyectar contenido externo en ellos tiende a romper las cosas de maneras sutiles. + +Por ejemplo, envolver elementos `` en una capa extra y proyectarlos en `` puede romper silenciosamente la navegación con teclado y el soporte de lectores de pantalla. La consulta aún encuentra los elementos, pero la configuración interna que los hace interactivos puede no funcionar correctamente cuando los elementos provienen de un contexto de vista diferente. + +Si un componente de librería gestiona el comportamiento de sus hijos, consulta su documentación antes de usar proyección de contenido — puede que no sea compatible. diff --git a/adev-es/src/content/guide/components/dom-apis.en.md b/adev-es/src/content/guide/components/dom-apis.en.md index 68ac11a..9c774c0 100644 --- a/adev-es/src/content/guide/components/dom-apis.en.md +++ b/adev-es/src/content/guide/components/dom-apis.en.md @@ -7,7 +7,7 @@ directly interact with a component's DOM. Components can inject ElementRef to ge component's host element: ```ts -@Component({...}) +@Component(/* ... */) export class ProfilePhoto { constructor() { const elementRef = inject(ElementRef); @@ -23,7 +23,7 @@ You can use Angular's `afterEveryRender` and `afterNextRender` functions to regi callback** that runs when Angular has finished rendering the page. ```ts -@Component({...}) +@Component(/* ... */) export class ProfilePhoto { constructor() { const elementRef = inject(ElementRef); diff --git a/adev-es/src/content/guide/components/host-elements.en.md b/adev-es/src/content/guide/components/host-elements.en.md index 38f5080..e78c916 100644 --- a/adev-es/src/content/guide/components/host-elements.en.md +++ b/adev-es/src/content/guide/components/host-elements.en.md @@ -10,9 +10,7 @@ The contents of a component's template are rendered inside its host element. // Component source @Component({ selector: 'profile-photo', - template: ` - Your profile photo - `, + template: `Your profile photo`, }) export class ProfilePhoto {} ``` @@ -64,6 +62,8 @@ export class CustomSlider { } ``` +NOTE: The global target names that can be used to prefix an event name are `document:`, `window:` and `body:`. + ## The `@HostBinding` and `@HostListener` decorators You can alternatively bind to the host element by applying the `@HostBinding` and `@HostListener` @@ -134,14 +134,14 @@ In cases like this, the following rules determine which value wins: ## Styling with CSS custom properties Developers often rely on [CSS Custom Properties](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascading_variables/Using_CSS_custom_properties) to enable a flexible configuration of their component's styles. -You can set such custom properties on a host element with a [style binding][style binding](guide/templates/binding#css-style-properties). +You can set such custom properties on a host element with a [style binding](guide/templates/binding#css-style-properties). ```angular-ts @Component({ /* ... */ host: { '[style.--my-background]': 'color()', - } + }, }) export class MyComponent { color = signal('lightgreen'); @@ -150,14 +150,14 @@ export class MyComponent { In this example, the `--my-background` CSS custom property is bound to the `color` signal. The value of the custom property will automatically update whenever the `color` signal changes. This will affect the current component and all its children that rely on this custom property. -### Setting custom properties on children compoents +### Setting custom properties on children components Alternatively, it is also possible to set css custom properties on the host element of children components with a [style binding](guide/templates/binding#css-style-properties). ```angular-ts @Component({ selector: 'my-component', - template: ``, + template: ``, }) export class MyComponent { color = signal('lightgreen'); diff --git a/adev-es/src/content/guide/components/host-elements.md b/adev-es/src/content/guide/components/host-elements.md index 920412b..f6d0980 100644 --- a/adev-es/src/content/guide/components/host-elements.md +++ b/adev-es/src/content/guide/components/host-elements.md @@ -64,6 +64,8 @@ export class CustomSlider { } ``` +NOTA: Los nombres de destino globales que se pueden usar como prefijo del nombre de un evento son `document:`, `window:` y `body:`. + ## Los decoradores `@HostBinding` y `@HostListener` Alternativamente puedes enlazar al elemento host aplicando los decoradores `@HostBinding` y `@HostListener` diff --git a/adev-es/src/content/guide/components/inheritance.en.md b/adev-es/src/content/guide/components/inheritance.en.md index b11af7e..233db69 100644 --- a/adev-es/src/content/guide/components/inheritance.en.md +++ b/adev-es/src/content/guide/components/inheritance.en.md @@ -12,7 +12,7 @@ export class ListboxBase { value: string; } -@Component({ ... }) +@Component(/* ... */) export class CustomListbox extends ListboxBase { // CustomListbox inherits the `value` property. } @@ -27,9 +27,7 @@ host bindings, inputs, outputs, lifecycle methods. ```angular-ts @Component({ selector: 'base-listbox', - template: ` - ... - `, + template: ` ... `, host: { '(keydown)': 'handleKey($event)', }, @@ -43,9 +41,7 @@ export class ListboxBase { @Component({ selector: 'custom-listbox', - template: ` - ... - `, + template: ` ... `, host: { '(click)': 'focusActiveOption()', }, @@ -67,15 +63,29 @@ and their own. ### Forwarding injected dependencies -If a base class injects dependencies as constructor parameters, the child class must explicitly class these dependencies to `super`. +When a base class uses `inject()` as a property initializer, the child class inherits the property automatically. No `super` forwarding is needed. ```ts -@Component({ ... }) +@Component(/* ... */) export class ListboxBase { - constructor(private element: ElementRef) { } + protected element = inject(ElementRef); } -@Component({ ... }) +@Component(/* ... */) +export class CustomListbox extends ListboxBase { + // `element` is inherited from `ListboxBase`. +} +``` + +If a base class injects dependencies as constructor parameters, the child class must explicitly pass these dependencies to `super`. + +```ts +@Component(/* ... */) +export class ListboxBase { + constructor(private element: ElementRef) {} +} + +@Component(/* ... */) export class CustomListbox extends ListboxBase { constructor(element: ElementRef) { super(element); @@ -90,7 +100,7 @@ implements `ngOnInit` _overrides_ the base class's implementation. If you want t class's lifecycle method, explicitly call the method with `super`: ```ts -@Component({ ... }) +@Component(/* ... */) export class ListboxBase { protected isInitialized = false; ngOnInit() { @@ -98,7 +108,7 @@ export class ListboxBase { } } -@Component({ ... }) +@Component(/* ... */) export class CustomListbox extends ListboxBase { override ngOnInit() { super.ngOnInit(); diff --git a/adev-es/src/content/guide/components/inheritance.md b/adev-es/src/content/guide/components/inheritance.md index 51fc165..19f99a4 100644 --- a/adev-es/src/content/guide/components/inheritance.md +++ b/adev-es/src/content/guide/components/inheritance.md @@ -66,6 +66,20 @@ y los suyos propios. ### Reenviar dependencias inyectadas +Cuando una clase base usa `inject()` como inicializador de propiedad, la clase hija hereda la propiedad automáticamente. No se necesita reenvío con `super`. + +```ts +@Component(/* ... */) +export class ListboxBase { + protected element = inject(ElementRef); +} + +@Component(/* ... */) +export class CustomListbox extends ListboxBase { + // `element` es heredado de `ListboxBase`. +} +``` + Si una clase base inyecta dependencias como parámetros del constructor, la clase hija debe pasar explícitamente estas dependencias a `super`. ```ts diff --git a/adev-es/src/content/guide/components/inputs.en.md b/adev-es/src/content/guide/components/inputs.en.md index 73651d7..caaa2c3 100644 --- a/adev-es/src/content/guide/components/inputs.en.md +++ b/adev-es/src/content/guide/components/inputs.en.md @@ -7,10 +7,10 @@ TIP: If you're familiar with other web frameworks, input properties are similar When you use a component, you commonly want to pass some data to it. A component specifies the data that it accepts by declaring **inputs**: -```ts {highlight:[5]} +```ts {highlight:[6]} import {Component, input} from '@angular/core'; -@Component({/*...*/}) +@Component(/* ... */) export class CustomSlider { // Declare an input named 'value' with a default value of zero. value = input(0); @@ -26,7 +26,7 @@ This lets you bind to the property in a template: If an input has a default value, TypeScript infers the type from the default value: ```ts -@Component({/*...*/}) +@Component(/* ... */) export class CustomSlider { // TypeScript infers that this input is a number, returning InputSignal. value = input(0); @@ -38,7 +38,7 @@ You can explicitly declare a type for the input by specifying a generic paramete If an input without a default value is not set, its value is `undefined`: ```ts -@Component({/*...*/}) +@Component(/* ... */) export class CustomSlider { // Produces an InputSignal because `value` may not be set. value = input(); @@ -57,10 +57,10 @@ When extending a component class, **inputs are inherited by the child class.** The `input` function returns an `InputSignal`. You can read the value by calling the signal: -```ts {highlight:[5]} +```ts {highlight:[9]} import {Component, input, computed} from '@angular/core'; -@Component({/*...*/}) +@Component(/* ... */) export class CustomSlider { // Declare an input named 'value' with a default value of zero. value = input(0); @@ -76,8 +76,8 @@ Signals created by the `input` function are read-only. You can declare that an input is `required` by calling `input.required` instead of `input`: -```ts {highlight:[3]} -@Component({/*...*/}) +```ts {highlight:[4]} +@Component(/* ... */) export class CustomSlider { // Declare a required input named value. Returns an `InputSignal`. value = input.required(); @@ -127,7 +127,7 @@ The most common use-case for input transforms is to accept a wider range of valu When you specify an input transform, the type of the transform function's parameter determines the types of values that can be set to the input in a template. ```ts -@Component({/*...*/}) +@Component(/* ... */) export class CustomSlider { widthPx = input('', {transform: appendPx}); } @@ -146,7 +146,7 @@ Angular includes two built-in transform functions for the two most common scenar ```ts import {Component, input, booleanAttribute, numberAttribute} from '@angular/core'; -@Component({/*...*/}) +@Component(/* ... */) export class CustomSlider { disabled = input(false, {transform: booleanAttribute}); value = input(0, {transform: numberAttribute}); @@ -163,7 +163,7 @@ _presence_ of the attribute indicates a "true" value. However, Angular's `boolea You can specify the `alias` option to change the name of an input in templates. ```ts {highlight:[3]} -@Component({/*...*/}) +@Component(/* ... */) export class CustomSlider { value = input(0, {alias: 'sliderValue'}); } @@ -186,14 +186,14 @@ When creating a component, you can define a model input similarly to how you cre Both types of input allow someone to bind a value into the property. However, **model inputs allow the component author to write values into the property**. If the property is bound with a two-way binding, the new value propagates to that binding. ```ts -@Component({ /* ... */}) +@Component(/* ... */) export class CustomSlider { // Define a model input named "value". value = model(0); increment() { // Update the model input with a new value, propagating the value to any bindings. - this.value.update(oldValue => oldValue + 10); + this.value.update((oldValue) => oldValue + 10); } } @@ -212,7 +212,7 @@ export class MediaControls { In the above example, the `CustomSlider` can write values into its `value` model input, which then propagates those values back to the `volume` signal in `MediaControls`. This binding keeps the values of `value` and `volume` in sync. Notice that the binding passes the `volume` signal instance, not the _value_ of the signal. -In other respects, model inputs work similarly to standard inputs. You can read the value by calling the signal function, including in reactive contexts like `computed` and `effect`. +In other respects, model inputs work similarly to standard inputs. You can read the value by calling the signal function, including in [reactive contexts](guide/signals#reactive-contexts) like `computed` and `effect`. See [Two-way binding](guide/templates/two-way-binding) for more details on two-way binding in templates. @@ -239,7 +239,7 @@ In the example above, the `CustomSlider` can write values into its `value` model When you declare a model input in a component or directive, Angular automatically creates a corresponding [output](guide/components/outputs) for that model. The output's name is the model input's name suffixed with "Change". ```ts -@Directive({ /* ... */ }) +@Directive(/* ... */) export class CustomCheckbox { // This automatically creates an output named "checkedChange". // Can be subscribed to using `(checkedChange)="handler()"` in the template. @@ -253,7 +253,7 @@ See [Custom events with outputs](guide/components/outputs) for more details on o ### Customizing model inputs -You can mark a model input as required or provide an alias in the same way as a [standard input](guide/signals/inputs). +You can mark a model input as required or provide an alias in the same way as a [standard input](guide/components/inputs). Model inputs do not support input transforms. @@ -274,7 +274,7 @@ TIP: While the Angular team recommends using the signal-based `input` function f You can alternatively declare component inputs by adding the `@Input` decorator to a property: ```ts {highlight:[3]} -@Component({...}) +@Component(/* ... */) export class CustomSlider { @Input() value = 0; } @@ -295,7 +295,7 @@ The `@Input` decorator accepts a config object that lets you change the way that You can specify the `required` option to enforce that a given input must always have a value. ```ts {highlight:[3]} -@Component({...}) +@Component(/* ... */) export class CustomSlider { @Input({required: true}) value = 0; } @@ -326,7 +326,7 @@ function trimString(value: string | undefined) { You can specify the `alias` option to change the name of an input in templates. ```ts {highlight:[3]} -@Component({...}) +@Component(/* ... */) export class CustomSlider { @Input({alias: 'sliderValue'}) value = 0; } @@ -351,7 +351,9 @@ export class CustomSlider { return this.internalValue; } - set value(newValue: number) { this.internalValue = newValue; } + set value(newValue: number) { + this.internalValue = newValue; + } private internalValue = 0; } diff --git a/adev-es/src/content/guide/components/lifecycle.en.md b/adev-es/src/content/guide/components/lifecycle.en.md index 6f07542..4197653 100644 --- a/adev-es/src/content/guide/components/lifecycle.en.md +++ b/adev-es/src/content/guide/components/lifecycle.en.md @@ -256,13 +256,13 @@ next phase. ```ts import {Component, ElementRef, afterNextRender} from '@angular/core'; -@Component({...}) +@Component(/* ... */) export class UserProfile { private prevPadding = 0; private elementHeight = 0; constructor() { - private elementRef = inject(ElementRef); + const elementRef = inject(ElementRef); const nativeElement = elementRef.nativeElement; afterNextRender({ @@ -281,7 +281,7 @@ export class UserProfile { if (didWrite) { this.elementHeight = nativeElement.getBoundingClientRect().height; } - } + }, }); } } diff --git a/adev-es/src/content/guide/components/outputs.en.md b/adev-es/src/content/guide/components/outputs.en.md index 57a302c..e61cfed 100644 --- a/adev-es/src/content/guide/components/outputs.en.md +++ b/adev-es/src/content/guide/components/outputs.en.md @@ -4,8 +4,10 @@ TIP: This guide assumes you've already read the [Essentials Guide](essentials). Angular components can define custom events by assigning a property to the `output` function: -```ts {highlight:[3]} -@Component({/*...*/}) +```ts {highlight:[5]} +@Component({ + /*...*/ +}) export class ExpandablePanel { panelClosed = output(); } @@ -18,7 +20,7 @@ export class ExpandablePanel { The `output` function returns an `OutputEmitterRef`. You can emit an event by calling the `emit` method on the `OutputEmitterRef`: ```ts - this.panelClosed.emit(); +this.panelClosed.emit(); ``` Angular refers to properties initialized with the `output` function as **outputs**. You can use outputs to raise custom events, similar to native browser events like `click`. @@ -43,7 +45,7 @@ this.valueChanged.emit(7); this.thumbDropped.emit({ pointerX: 123, pointerY: 456, -}) +}); ``` When defining an event listener in a template, you can access the event data from the `$event` variable: @@ -71,7 +73,7 @@ export class App { The `output` function accepts a parameter that lets you specify a different name for the event in a template: ```ts -@Component({/*...*/}) +@Component(/* ... */) export class CustomSlider { changed = output({alias: 'valueChanged'}); } @@ -93,7 +95,7 @@ from the component instance. The `OutputRef` type includes a `subscribe` method: ```ts const someComponentRef: ComponentRef = viewContainerRef.createComponent(/*...*/); -someComponentRef.instance.someEventProperty.subscribe(eventData => { +someComponentRef.instance.someEventProperty.subscribe((eventData) => { console.log(eventData); }); ``` @@ -101,7 +103,7 @@ someComponentRef.instance.someEventProperty.subscribe(eventData => { Angular automatically cleans up event subscriptions when it destroys components with subscribers. Alternatively, you can manually unsubscribe from an event. The `subscribe` function returns an `OutputRefSubscription` with an `unsubscribe` method: ```ts -const eventSubscription = someComponent.someEventProperty.subscribe(eventData => { +const eventSubscription = someComponent.someEventProperty.subscribe((eventData) => { console.log(eventData); }); @@ -130,7 +132,7 @@ original decorator-based `@Output` API remains fully supported. You can alternatively define custom events by assigning a property to a new `EventEmitter` and adding the `@Output` decorator: ```ts -@Component({/*...*/}) +@Component(/* ... */) export class ExpandablePanel { @Output() panelClosed = new EventEmitter(); } @@ -143,7 +145,7 @@ You can emit an event by calling the `emit` method on the `EventEmitter`. The `@Output` decorator accepts a parameter that lets you specify a different name for the event in a template: ```ts -@Component({/*...*/}) +@Component(/* ... */) export class CustomSlider { @Output('valueChanged') changed = new EventEmitter(); } diff --git a/adev-es/src/content/guide/components/programmatic-rendering.en.md b/adev-es/src/content/guide/components/programmatic-rendering.en.md index e604e73..3992991 100644 --- a/adev-es/src/content/guide/components/programmatic-rendering.en.md +++ b/adev-es/src/content/guide/components/programmatic-rendering.en.md @@ -20,10 +20,10 @@ chunks automatically and loaded only when necessary, based on the configured tri template. ```angular-ts -@Component({ ... }) +@Component({/*...*/}) export class AdminBio { /* ... */ } -@Component({ ... }) +@Component({/*...*/}) export class StandardBio { /* ... */ } @Component({ @@ -41,6 +41,144 @@ export class CustomDialog { } ``` +### Passing inputs to dynamically rendered components + +You can pass inputs to the dynamically rendered component using the `ngComponentOutletInputs` property. This property accepts an object where keys are input names and values are the input values. + +```angular-ts +@Component({ + selector: 'user-greeting', + template: ` +
    +

    User: {{ username() }}

    +

    Role: {{ role() }}

    +
    + `, +}) +export class UserGreeting { + username = input.required(); + role = input('guest'); +} + +@Component({ + selector: 'profile-view', + imports: [NgComponentOutlet], + template: ``, +}) +export class ProfileView { + greetingComponent = UserGreeting; + greetingInputs = signal({username: 'ngAwesome', role: 'admin'}); +} +``` + +The inputs are updated whenever the `greetingInputs` signal changes, keeping the dynamic component in sync with the parent's state. + +### Providing content projection + +Use `ngComponentOutletContent` to pass projected content to the dynamically rendered component. This is useful when the dynamic component uses `` to display content. + +```angular-ts +@Component({ + selector: 'card-wrapper', + template: ` +
    + +
    + `, +}) +export class CardWrapper {} + +@Component({ + imports: [NgComponentOutlet], + template: ` + + + +

    Dynamic Content

    +

    This content is projected into the card.

    +
    + `, +}) +export class DynamicCard { + private vcr = inject(ViewContainerRef); + cardComponent = CardWrapper; + + private contentTemplate = viewChild>('contentTemplate'); + + cardContent = computed(() => { + const template = this.contentTemplate(); + if (!template) return []; + // Returns an array of projection slots. Each element represents one slot. + // CardWrapper has one , so we return an array with one element. + return [this.vcr.createEmbeddedView(template).rootNodes]; + }); +} +``` + +NOTE: Hydration does not support projecting DOM nodes created with native DOM APIs. This causes an [NG0503 error](/errors/NG0503). Use Angular APIs to create projected content or add `ngSkipHydration` to the component. + +### Providing injectors + +You can provide a custom injector to the dynamically created component using `ngComponentOutletInjector`. This is useful for providing component-specific services or configuration. + +```angular-ts +export const THEME_DATA = new InjectionToken('THEME_DATA', { + factory: () => 'light', +}); + +@Component({ + selector: 'themed-panel', + template: `
    ...
    `, +}) +export class ThemedPanel { + theme = inject(THEME_DATA); +} + +@Component({ + selector: 'dynamic-panel', + imports: [NgComponentOutlet], + template: ``, +}) +export class DynamicPanel { + panelComponent = ThemedPanel; + + customInjector = Injector.create({ + providers: [{provide: THEME_DATA, useValue: 'dark'}], + }); +} +``` + +### Accessing the component instance + +You can access the dynamically created component's instance using the directive's `exportAs` feature: + +```angular-ts +@Component({ + selector: 'counter', + template: `

    Count: {{ count() }}

    `, +}) +export class Counter { + count = signal(0); + increment() { + this.count.update((c) => c + 1); + } +} + +@Component({ + imports: [NgComponentOutlet], + template: ` + + + + `, +}) +export class CounterHost { + counterComponent = Counter; +} +``` + +NOTE: The `componentInstance` property is `null` before the component is rendered. + See the [NgComponentOutlet API reference](api/common/NgComponentOutlet) for more information on the directive's capabilities. @@ -57,9 +195,7 @@ DOM as the next sibling of the component or directive that injected the `ViewCon ```angular-ts @Component({ selector: 'leaf-content', - template: ` - This is the leaf content - `, + template: `This is the leaf content`, }) export class LeafContent {} @@ -75,9 +211,7 @@ export class OuterContainer {} @Component({ selector: 'inner-item', - template: ` - - `, + template: ``, }) export class InnerItem { private viewContainer = inject(ViewContainerRef); @@ -148,25 +282,25 @@ To simplify this, both `createComponent` and `ViewContainerRef.createComponent` ### Host view using `ViewContainerRef.createComponent` -`ViewContainerRef.createComponent` creates a component and automatically inserts its host view and host element into the container's view hierarchy at the container's location. Use this when the dynamic component should become part of the container's logical and visual structure (for example, adding list items or inline UI). +`ViewContainerRef.createComponent` creates a component and automatically inserts its host view and host element into the container’s view hierarchy at the container’s location. Use this when the dynamic component should become part of the container’s logical and visual structure (for example, adding list items or inline UI). -By contrast, the standalone `createComponent` API does not attach the new component to any existing view or DOM location — it returns a `ComponentRef` and gives you explicit control over where to place the component's host element. +By contrast, the standalone `createComponent` API does not attach the new component to any existing view or DOM location — it returns a `ComponentRef` and gives you explicit control over where to place the component’s host element. ```angular-ts -import { Component, input, model, output } from "@angular/core"; +import {Component, input, model, output} from '@angular/core'; @Component({ selector: 'app-warning', template: ` - @if(isExpanded()) { -
    -

    Warning: Action needed!

    - -
    - } - ` + @if (isExpanded()) { +
    +

    Warning: Action needed!

    + +
    + } + `, }) -export class AppWarningComponent { +export class AppWarning { readonly canClose = input.required(); readonly isExpanded = model(); readonly close = output(); @@ -174,41 +308,49 @@ export class AppWarningComponent { ``` ```ts -import { Component, ViewContainerRef, signal, inputBinding, outputBinding, twoWayBinding, inject } from '@angular/core'; -import { FocusTrap } from "@angular/cdk/a11y"; -import { ThemeDirective } from '../theme.directive'; +import { + Component, + ViewContainerRef, + signal, + inputBinding, + outputBinding, + twoWayBinding, + inject, +} from '@angular/core'; +import {FocusTrap} from '@angular/cdk/a11y'; +import {ThemeDirective} from '../theme.directive'; @Component({ - template: `` + template: ``, }) -export class HostComponent { +export class Host { private vcr = inject(ViewContainerRef); readonly canClose = signal(true); readonly isExpanded = signal(true); showWarning() { - const compRef = this.vcr.createComponent(AppWarningComponent, { + const compRef = this.vcr.createComponent(AppWarning, { bindings: [ inputBinding('canClose', this.canClose), twoWayBinding('isExpanded', this.isExpanded), outputBinding('close', (confirmed) => { console.log('Closed with result:', confirmed); - }) + }), ], directives: [ FocusTrap, - { type: ThemeDirective, bindings: [inputBinding('theme', () => 'warning')] } - ] + {type: ThemeDirective, bindings: [inputBinding('theme', () => 'warning')]}, + ], }); } } ``` -In the example above, the dynamic **AppWarningComponent** is created with its `canClose` input bound to a reactive signal, a two-way binding on its `isExpanded` state, and an output listener for `close`. The `FocusTrap` and `ThemeDirective` are attached to the host element via `directives`. +In the example above, the dynamic **AppWarning** is created with its `canClose` input bound to a reactive signal, a two-way binding on its `isExpanded` state, and an output listener for `close`. The `FocusTrap` and `ThemeDirective` are attached to the host element via `directives`. ### Popup attached to `document.body` with `createComponent` + `hostElement` -Use this when rendering outside the current view hierarchy (e.g., overlays). The provided `hostElement` becomes the component's host in the DOM, so Angular doesn't create a new element matching the selector. Lets you configure **bindings** directly. +Use this when rendering outside the current view hierarchy (e.g., overlays). The provided `hostElement` becomes the component’s host in the DOM, so Angular doesn’t create a new element matching the selector. Lets you configure **bindings** directly. ```ts import { @@ -219,10 +361,11 @@ import { Injectable, inputBinding, outputBinding, + Service, } from '@angular/core'; -import { PopupComponent } from './popup.component'; +import {Popup} from './popup'; -@Injectable({ providedIn: 'root' }) +@Service() export class PopupService { private readonly injector = inject(EnvironmentInjector); private readonly appRef = inject(ApplicationRef); @@ -232,7 +375,7 @@ export class PopupService { const host = document.createElement('popup-host'); // Create the component and bind in one call - const ref = createComponent(PopupComponent, { + const ref = createComponent(Popup, { environmentInjector: this.injector, hostElement: host, bindings: [ @@ -245,7 +388,7 @@ export class PopupService { ], }); - // Registers the component's view so it participates in change detection cycle. + // Registers the component’s view so it participates in change detection cycle. this.appRef.attachView(ref.hostView); // Inserts the provided host element into the DOM (outside the normal Angular view hierarchy). // This is what makes the popup visible on screen, typically used for overlays or modals. diff --git a/adev-es/src/content/guide/components/programmatic-rendering.md b/adev-es/src/content/guide/components/programmatic-rendering.md index 5e85df9..5559d6f 100644 --- a/adev-es/src/content/guide/components/programmatic-rendering.md +++ b/adev-es/src/content/guide/components/programmatic-rendering.md @@ -20,10 +20,10 @@ separados automáticamente y cargados solo cuando sea necesario, basado en los t plantilla. ```angular-ts -@Component({ ... }) +@Component({/*...*/}) export class AdminBio { /* ... */ } -@Component({ ... }) +@Component({/*...*/}) export class StandardBio { /* ... */ } @Component({ @@ -44,6 +44,144 @@ export class CustomDialog { Ve la [referencia de API de NgComponentOutlet](api/common/NgComponentOutlet) para más información sobre las capacidades de la directiva. +### Pasar entradas a componentes renderizados dinámicamente + +Puedes pasar entradas al componente renderizado dinámicamente usando la propiedad `ngComponentOutletInputs`. Esta propiedad acepta un objeto donde las claves son nombres de entradas y los valores son los valores de las entradas. + +```angular-ts +@Component({ + selector: 'user-greeting', + template: ` +
    +

    User: {{ username() }}

    +

    Role: {{ role() }}

    +
    + `, +}) +export class UserGreeting { + username = input.required(); + role = input('guest'); +} + +@Component({ + selector: 'profile-view', + imports: [NgComponentOutlet], + template: ``, +}) +export class ProfileView { + greetingComponent = UserGreeting; + greetingInputs = signal({username: 'ngAwesome', role: 'admin'}); +} +``` + +Las entradas se actualizan cada vez que la signal `greetingInputs` cambia, manteniendo el componente dinámico sincronizado con el estado del padre. + +### Proporcionar proyección de contenido + +Usa `ngComponentOutletContent` para pasar contenido proyectado al componente renderizado dinámicamente. Esto es útil cuando el componente dinámico usa `` para mostrar contenido. + +```angular-ts +@Component({ + selector: 'card-wrapper', + template: ` +
    + +
    + `, +}) +export class CardWrapper {} + +@Component({ + imports: [NgComponentOutlet], + template: ` + + + +

    Dynamic Content

    +

    This content is projected into the card.

    +
    + `, +}) +export class DynamicCard { + private vcr = inject(ViewContainerRef); + cardComponent = CardWrapper; + + private contentTemplate = viewChild>('contentTemplate'); + + cardContent = computed(() => { + const template = this.contentTemplate(); + if (!template) return []; + // Devuelve un array de slots de proyección. Cada elemento representa un slot . + // CardWrapper tiene un , así que devolvemos un array con un elemento. + return [this.vcr.createEmbeddedView(template).rootNodes]; + }); +} +``` + +NOTA: La hidratación no admite la proyección de nodos DOM creados con APIs nativas del DOM. Esto causa un [error NG0503](/errors/NG0503). Usa APIs de Angular para crear contenido proyectado o agrega `ngSkipHydration` al componente. + +### Proporcionar inyectores + +Puedes proporcionar un inyector personalizado al componente creado dinámicamente usando `ngComponentOutletInjector`. Esto es útil para proporcionar servicios o configuración específicos del componente. + +```angular-ts +export const THEME_DATA = new InjectionToken('THEME_DATA', { + factory: () => 'light', +}); + +@Component({ + selector: 'themed-panel', + template: `
    ...
    `, +}) +export class ThemedPanel { + theme = inject(THEME_DATA); +} + +@Component({ + selector: 'dynamic-panel', + imports: [NgComponentOutlet], + template: ``, +}) +export class DynamicPanel { + panelComponent = ThemedPanel; + + customInjector = Injector.create({ + providers: [{provide: THEME_DATA, useValue: 'dark'}], + }); +} +``` + +### Acceder a la instancia del componente + +Puedes acceder a la instancia del componente creado dinámicamente usando la característica `exportAs` de la directiva: + +```angular-ts +@Component({ + selector: 'counter', + template: `

    Count: {{ count() }}

    `, +}) +export class Counter { + count = signal(0); + increment() { + this.count.update((c) => c + 1); + } +} + +@Component({ + imports: [NgComponentOutlet], + template: ` + + + + `, +}) +export class CounterHost { + counterComponent = Counter; +} +``` + +NOTA: La propiedad `componentInstance` es `null` antes de que el componente sea renderizado. + ## Usando ViewContainerRef Un **contenedor de vista** es un nodo en el árbol de componentes de Angular que puede contener contenido. Cualquier componente @@ -219,10 +357,11 @@ import { Injectable, inputBinding, outputBinding, + Service, } from '@angular/core'; -import { PopupComponent } from './popup.component'; +import {Popup} from './popup'; -@Injectable({ providedIn: 'root' }) +@Service() export class PopupService { private readonly injector = inject(EnvironmentInjector); private readonly appRef = inject(ApplicationRef); @@ -232,7 +371,7 @@ export class PopupService { const host = document.createElement('popup-host'); // Crear el componente y enlazar en una sola llamada - const ref = createComponent(PopupComponent, { + const ref = createComponent(Popup, { environmentInjector: this.injector, hostElement: host, bindings: [ diff --git a/adev-es/src/content/guide/components/queries.en.md b/adev-es/src/content/guide/components/queries.en.md index 075d2c7..8ce4e25 100644 --- a/adev-es/src/content/guide/components/queries.en.md +++ b/adev-es/src/content/guide/components/queries.en.md @@ -7,7 +7,7 @@ A component can define **queries** that find child elements and read values from Developers most commonly use queries to retrieve references to child components, directives, DOM elements, and more. All query functions return signals that reflect the most up-to-date results. You can read the -result by calling the signal function, including in reactive contexts like `computed` and `effect`. +result by calling the signal function, including in [reactive contexts](guide/signals#reactive-contexts) like `computed` and `effect`. There are two categories of query: **view queries** and **content queries.** @@ -15,7 +15,7 @@ There are two categories of query: **view queries** and **content queries.** View queries retrieve results from the elements in the component's _view_ — the elements defined in the component's own template. You can query for a single result with the `viewChild` function. -```typescript {highlight: [14, 15]} +```angular-ts {highlight: [14, 15]} @Component({ selector: 'custom-card-header', /*...*/ @@ -40,7 +40,7 @@ If the query does not find a result, its value is `undefined`. This may occur if You can also query for multiple results with the `viewChildren` function. -```typescript {highlight: [17]} +```angular-ts {highlight: [17]} @Component({ selector: 'custom-card-action', /*...*/ @@ -51,13 +51,14 @@ export class CustomCardAction { @Component({ selector: 'custom-card', - template: `Save + template: ` + Save Cancel `, }) export class CustomCard { actions = viewChildren(CustomCardAction); - actionsTexts = computed(() => this.actions().map(action => action.text); + actionsTexts = computed(() => this.actions().map((action) => action.text)); } ``` @@ -69,7 +70,7 @@ export class CustomCard { Content queries retrieve results from the elements in the component's _content_— the elements nested inside the component in the template where it's used. You can query for a single result with the `contentChild` function. -```typescript {highlight: [14, 15]} +```angular-ts {highlight: [14, 15]} @Component({ selector: 'custom-toggle', /*...*/ @@ -88,16 +89,15 @@ export class CustomExpando { } @Component({ -/* ... */ -// CustomToggle is used inside CustomExpando as content. -template: ` + /* ... */ + // CustomToggle is used inside CustomExpando as content. + template: ` Show - ` + `, }) - -export class UserProfile { } +export class UserProfile {} ``` If the query does not find a result, its value is `undefined`. This may occur if the target element is absent or hidden by `@if`. Angular keeps the result of `contentChild` up to date as your application state changes. @@ -106,7 +106,7 @@ By default, content queries find only _direct_ children of the component and do You can also query for multiple results with the `contentChildren` function. -```typescript {highlight: [14, 16, 17, 18, 19, 20]} +```angular-ts {highlight: [14, 15]} @Component({ selector: 'custom-menu-item', /*...*/ @@ -119,10 +119,9 @@ export class CustomMenuItem { selector: 'custom-menu', /*...*/ }) - export class CustomMenu { items = contentChildren(CustomMenuItem); - itemTexts = computed(() => this.items().map(item => item.text)); + itemTexts = computed(() => this.items().map((item) => item.text)); } @Component({ @@ -132,9 +131,9 @@ export class CustomMenu { Cheese Tomato - ` + `, }) -export class UserProfile { } +export class UserProfile {} ``` `contentChildren` creates a signal with an `Array` of the query results. @@ -147,8 +146,8 @@ If a child query (`viewChild` or `contentChild`) does not find a result, its val In some cases, especially with `viewChild`, you know with certainty that a specific child is always available. In other cases, you may want to strictly enforce that a specific child is present. For these cases, you can use a _required query_. -```angular-ts -@Component({/* ... */}) +```ts +@Component(/* ... */) export class CustomCard { header = viewChild.required(CustomCardHeader); body = contentChild.required(CustomCardBody); @@ -172,7 +171,7 @@ a [template reference variable](guide/templates/variables#template-reference-var template: ` - ` + `, }) export class ActionBar { saveButton = viewChild>('save'); @@ -196,9 +195,9 @@ const SUB_ITEM = new InjectionToken('sub-item'); /*...*/ providers: [{provide: SUB_ITEM, useValue: 'special-item'}], }) -export class SpecialItem { } +export class SpecialItem {} -@Component({/*...*/}) +@Component(/* ... */) export class CustomList { subItemType = contentChild(SUB_ITEM); } @@ -215,8 +214,7 @@ All query functions accept an options object as a second parameter. These option By default, the query locator indicates both the element you're searching for and the value retrieved. You can alternatively specify the `read` option to retrieve a different value from the element matched by the locator. ```ts - -@Component({/*...*/}) +@Component(/* ... */) export class CustomExpando { toggle = contentChild(ExpandoContent, {read: TemplateRef}); } @@ -232,7 +230,7 @@ Developers most commonly use `read` to retrieve `ElementRef` and `TemplateRef`. By default, `contentChildren` queries find only _direct_ children of the component and do not traverse into descendants. `contentChild` queries do traverse into descendants by default. -```typescript {highlight: [13, 14, 15, 16]} +```angular-ts {highlight: [13, 14, 15, 16, 17]} @Component({ selector: 'custom-expando', /*...*/ @@ -244,14 +242,15 @@ export class CustomExpando { @Component({ selector: 'user-profile', - template: ` + template: ` + Show - ` + `, }) -export class UserProfile { } +export class UserProfile {} ``` In the example above, `CustomExpando` cannot find `` with `contentChildren` because it is not a direct child of ``. By setting `descendants: true`, you configure the query to traverse all descendants in the same template. Queries, however, _never_ pierce into components to traverse elements in other templates. @@ -265,11 +264,11 @@ original decorator-based query APIs remain fully supported. You can alternatively declare queries by adding the corresponding decorator to a property. Decorator-based queries behave the same way as signal-based queries except as described below. -### View queries +### View queries {#decorator-view-queries} You can query for a single result with the `@ViewChild` decorator. -```typescript {highlight: [14, 16, 17, 18]} +```angular-ts {highlight: [14, 16, 17, 18]} @Component({ selector: 'custom-card-header', /*...*/ @@ -282,7 +281,7 @@ export class CustomCardHeader { selector: 'custom-card', template: 'Visit sunny California!', }) -export class CustomCard { +export class CustomCard implements AfterViewInit { @ViewChild(CustomCardHeader) header: CustomCardHeader; ngAfterViewInit() { @@ -299,7 +298,7 @@ Angular keeps the result of `@ViewChild` up to date as your application state ch You can also query for multiple results with the `@ViewChildren` decorator. -```typescript {highlight: [17, 19, 20, 21, 22, 23]} +```angular-ts {highlight: [17, 19, 20, 21, 22, 23]} @Component({ selector: 'custom-card-action', /*...*/ @@ -315,11 +314,11 @@ export class CustomCardAction { Cancel `, }) -export class CustomCard { +export class CustomCard implements AfterViewInit { @ViewChildren(CustomCardAction) actions: QueryList; ngAfterViewInit() { - this.actions.forEach(action => { + this.actions.forEach((action) => { console.log(action.text); }); } @@ -328,11 +327,11 @@ export class CustomCard { `@ViewChildren` creates a `QueryList` object that contains the query results. You can subscribe to changes to the query results over time via the `changes` property. -### Content queries +### Content queries {#decorator-content-queries} You can query for a single result with the `@ContentChild` decorator. -```typescript {highlight: [14, 16, 17, 18, 25]} +```angular-ts {highlight: [14, 16, 17, 18]} @Component({ selector: 'custom-toggle', /*...*/ @@ -345,8 +344,7 @@ export class CustomToggle { selector: 'custom-expando', /*...*/ }) - -export class CustomExpando { +export class CustomExpando implements AfterContentInit { @ContentChild(CustomToggle) toggle: CustomToggle; ngAfterContentInit() { @@ -360,9 +358,9 @@ export class CustomExpando { Show - ` + `, }) -export class UserProfile { } +export class UserProfile {} ``` In this example, the `CustomExpando` component queries for a child `CustomToggle` and accesses the result in `ngAfterContentInit`. @@ -373,7 +371,7 @@ Angular keeps the result of `@ContentChild` up to date as your application state You can also query for multiple results with the `@ContentChildren` decorator. -```typescript {highlight: [15, 17, 18, 19, 20, 21]} +```angular-ts {highlight: [14, 16, 17, 18, 19, 20]} @Component({ selector: 'custom-menu-item', /*...*/ @@ -386,12 +384,11 @@ export class CustomMenuItem { selector: 'custom-menu', /*...*/ }) - -export class CustomMenu { +export class CustomMenu implements AfterContentInit { @ContentChildren(CustomMenuItem) items: QueryList; ngAfterContentInit() { - this.items.forEach(item => { + this.items.forEach((item) => { console.log(item.text); }); } @@ -404,9 +401,9 @@ export class CustomMenu { Cheese Tomato - ` + `, }) -export class UserProfile { } +export class UserProfile {} ``` `@ContentChildren` creates a `QueryList` object that contains the query results. You can subscribe to changes to the query results over time via the `changes` property. @@ -424,7 +421,7 @@ All query decorators accept an options object as a second parameter. These optio selector: 'custom-card', template: 'Visit sunny California!', }) -export class CustomCard { +export class CustomCard implements OnInit { @ViewChild(CustomCardHeader, {static: true}) header: CustomCardHeader; ngOnInit() { diff --git a/adev-es/src/content/guide/components/selectors.en.md b/adev-es/src/content/guide/components/selectors.en.md index b473b4d..a04926b 100644 --- a/adev-es/src/content/guide/components/selectors.en.md +++ b/adev-es/src/content/guide/components/selectors.en.md @@ -6,17 +6,17 @@ Every component defines a [CSS selector](https://developer.mozilla.org/docs/Web/CSS/CSS_selectors) that determines how the component is used: - +```angular-ts {highlight: [2]} @Component({ selector: 'profile-photo', ... }) export class ProfilePhoto { } - +``` You use a component by creating a matching HTML element in the templates of _other_ components: - +```angular-ts {highlight: [3]} @Component({ template: ` @@ -24,7 +24,7 @@ You use a component by creating a matching HTML element in the templates of _oth ..., }) export class UserProfile { } - +``` **Angular matches selectors statically at compile-time**. Changing the DOM at run-time, either via Angular bindings or with DOM APIs, does not affect the components rendered. @@ -63,13 +63,13 @@ You can append this pseudo-class to any other selector to narrow which elements selector matches. For example, you could define a `[dropzone]` attribute selector and prevent matching `textarea` elements: - +```angular-ts {highlight: [2]} @Component({ selector: '[dropzone]:not(textarea)', ... }) export class DropZone { } - +``` Angular does not support any other pseudo-classes or pseudo-elements in component selectors. @@ -78,23 +78,23 @@ Angular does not support any other pseudo-classes or pseudo-elements in componen You can combine multiple selectors by concatenating them. For example, you can match ` +
    - ` + `, }) -export class ExampleComponent { +export class Example { dataStore = inject(BasicDataStore); } ``` @@ -73,12 +138,10 @@ export class ExampleComponent { ### Injecting into another service ```ts -import { inject, Injectable } from '@angular/core'; -import { AdvancedDataStore } from './advanced-data-store'; +import {inject, Service} from '@angular/core'; +import {AdvancedDataStore} from './advanced-data-store'; -@Injectable({ - providedIn: 'root', -}) +@Service() export class BasicDataStore { private advancedDataStore = inject(AdvancedDataStore); private data: string[] = []; @@ -95,7 +158,7 @@ export class BasicDataStore { ## Next steps -While `providedIn: 'root'` covers most use cases, Angular offers additional ways to provide services for specialized scenarios: +While `providedIn: 'root'` covers most use cases, Angular also provides additional ways you can configure services for more specialized scenarios: - **Component-specific instances** - When components need their own isolated service instances - **Manual configuration** - For services that require runtime configuration diff --git a/adev-es/src/content/guide/di/creating-and-using-services.md b/adev-es/src/content/guide/di/creating-and-using-services.md index 0ad4e8e..0d38147 100644 --- a/adev-es/src/content/guide/di/creating-and-using-services.md +++ b/adev-es/src/content/guide/di/creating-and-using-services.md @@ -1,71 +1,136 @@ # Creando y usando servicios -Los servicios son piezas de código reutilizables que pueden compartirse a través de tu aplicación Angular. Típicamente manejan la obtención de datos, lógica de negocio u otra funcionalidad que múltiples componentes necesitan acceder. +Los servicios son piezas de código reutilizables que puedes compartir a través de tu aplicación Angular. Comúnmente se usan para manejar la obtención de datos, lógica de negocio u otra funcionalidad que múltiples componentes necesitan acceder. ## Creando un servicio -Puedes crear un servicio con el [Angular CLI](tools/cli) con el siguiente comando: +Puedes crear un servicio usando el [Angular CLI](tools/cli) con el siguiente comando: ```bash ng generate service CUSTOM_NAME ``` -Esto crea un archivo dedicado `CUSTOM_NAME.ts` en tu directorio `src`. +Este comando crea un archivo dedicado `CUSTOM_NAME.ts` en tu directorio `src`. -También puedes crear manualmente un servicio añadiendo el decorador `@Injectable()` a una clase TypeScript. Esto le indica a Angular que el servicio puede ser inyectado como una dependencia. +También puedes crear manualmente un servicio añadiendo el decorador `@Service()` a una clase TypeScript. Esto le indica a Angular que puedes usar esa clase como una dependencia inyectable. -Aquí hay un ejemplo de un servicio que permite a los usuarios agregar y solicitar datos: +El siguiente ejemplo define un servicio que permite a los usuarios agregar y recuperar datos: -```ts -// 📄 src/app/basic-data-store.ts -import { Injectable } from '@angular/core'; +```ts {header: "src/app/basic-data-store.ts"} +import {Service} from '@angular/core'; -@Injectable({ providedIn: 'root' }) +@Service() export class BasicDataStore { - private data: string[] = [] + private data: string[] = []; addData(item: string): void { - this.data.push(item) + this.data.push(item); } getData(): string[] { - return [...this.data] + return [...this.data]; } } ``` ## Cómo los servicios se vuelven disponibles -Cuando usas `@Injectable({ providedIn: 'root' })` en tu servicio, Angular: +Los servicios se proveen en el nivel raíz por defecto. Cuando un servicio se provee globalmente, Angular garantiza tres beneficios principales: + +- **Instancia Singleton:** Crea una única instancia compartida para toda la aplicación. +- **Disponibilidad Global:** Accesible automáticamente en cualquier parte sin registro manual de proveedores. +- **Tree-shakability:** Garantiza que el servicio se excluya del bundle de producción final si tu código nunca lo usa explícitamente. + +### Usando el decorador `@Service` vs `@Injectable` + +El decorador `@Service` sirve como una forma moderna y ergonómica del tradicional `@Injectable({ providedIn: 'root' })`. + +Usa esta referencia rápida para decidir qué decorador se adapta a tu escenario: + +| Característica / Requisito | `@Service` | `@Injectable` | +| --------------------------------------------------- | ---------- | --------------------------------------- | +| **Soporte para la función `inject()`** | Sí | Sí | +| **DI basada en constructor** | ❌ No | Sí | +| **Proveedor singleton raíz implícito** | Sí | ❌ No (requiere `{providedIn: 'root'}`) | +| **Claves de proveedor avanzadas (`useClass`, etc.)** | ❌ No | Sí | +| **Factories de inicialización personalizadas** | Sí | Sí | +| **Scopes no raíz (`platform`, etc.)** | ❌ No | Sí | + +### Reemplazar la implementación con una factory + +Si necesitas controlar cómo se crea el singleton, por ejemplo, para intercambiar una implementación diferente según el entorno, pasa una función `factory`. + +La factory se ejecuta en un [contexto de inyección](guide/di/dependency-injection-context), por lo que puedes usar [`inject()`](api/core/inject) dentro de ella para leer otras dependencias. + +El siguiente servicio `Analytics` es un no-op localmente para que los eventos no contaminen la consola durante el desarrollo. En producción, la factory lee un token `ANALYTICS_ENABLED` y retorna una subclase `GoogleAnalytics` que reenvía eventos al rastreador real: + +```ts {header: "src/app/analytics.ts"} +import {inject, InjectionToken, Service} from '@angular/core'; +import {ANALYTICS_ENABLED} from './token'; + +@Service({ + factory: () => (inject(ANALYTICS_ENABLED) ? new GoogleAnalytics() : new Analytics()), +}) +export class Analytics { + track(event: string, payload?: Record) { + // No-op por defecto. + } +} + +class GoogleAnalytics extends Analytics { + override track(event: string, payload?: Record) { + // Envía un evento de analytics a Google Analytics + } +} +``` + +NOTA: La opción `factory` reemplaza las opciones `useClass`, `useValue`, `useExisting` y `useFactory` de `@Injectable`. Si necesitas alguna de ellas, continúa usando `@Injectable`. + +### Deshabilitar el provisionamiento automático + +Por defecto, `@Service` provee la clase en el inyector raíz. Si deseas proveerla manualmente, por ejemplo, para limitarla a una ruta o componente específico, establece `autoProvided: false`: -- **Crea una única instancia** (singleton) para toda tu aplicación -- **Lo hace disponible en todas partes** sin ninguna configuración adicional -- **Habilita tree-shaking** para que el servicio solo se incluya en tu bundle de JavaScript si realmente se usa +```ts {header: "src/app/analytics-logger.ts"} +import {Service} from '@angular/core'; -Este es el enfoque recomendado para la mayoría de los servicios. +@Service({autoProvided: false}) +export class AnalyticsLogger { + trackEvent(name: string) { + console.log('event:', name); + } +} +``` + +Eres entonces responsable de agregar el servicio a un array `providers`, igual que con un `@Injectable()` normal. + +### Cuándo usar `@Service` vs `@Injectable` + +Usa `@Service` cuando estés creando una nueva clase singleton que usa `inject()` para sus dependencias. Continúa usando `@Injectable` cuando necesites cualquiera de lo siguiente: + +- **Inyección de dependencias basada en constructor.** `@Service` solo soporta la función [`inject()`](api/core/inject). +- **Configuración avanzada de proveedores** como `useClass`, `useValue`, `useExisting` o `useFactory`. `@Service` expone una única opción `factory` en su lugar. +- **Scopes no raíz** como `providedIn: 'platform'`. ## Inyectando un servicio -Una vez que has creado un servicio con `providedIn: 'root'`, puedes inyectarlo en cualquier parte de tu aplicación usando la función `inject()` de `@angular/core`. +Una vez que has creado un servicio, puedes inyectarlo en cualquier parte de tu aplicación usando la función `inject()` de `@angular/core`. ### Inyectando en un componente ```angular-ts -import { Component, inject } from '@angular/core'; -import { BasicDataStore } from './basic-data-store'; +import {Component, inject} from '@angular/core'; +import {BasicDataStore} from './basic-data-store'; @Component({ selector: 'app-example', template: `

    {{ dataStore.getData() }}

    - +
    - ` + `, }) -export class ExampleComponent { +export class Example { dataStore = inject(BasicDataStore); } ``` @@ -73,12 +138,10 @@ export class ExampleComponent { ### Inyectando en otro servicio ```ts -import { inject, Injectable } from '@angular/core'; -import { AdvancedDataStore } from './advanced-data-store'; +import {inject, Service} from '@angular/core'; +import {AdvancedDataStore} from './advanced-data-store'; -@Injectable({ - providedIn: 'root', -}) +@Service() export class BasicDataStore { private advancedDataStore = inject(AdvancedDataStore); private data: string[] = []; @@ -95,7 +158,7 @@ export class BasicDataStore { ## Próximos pasos -Aunque `providedIn: 'root'` cubre la mayoría de los casos de uso, Angular ofrece formas adicionales de proveer servicios para escenarios especializados: +Aunque `providedIn: 'root'` cubre la mayoría de los casos de uso, Angular también proporciona formas adicionales de configurar servicios para escenarios más especializados: - **Instancias específicas de componente** - Cuando los componentes necesitan sus propias instancias aisladas de servicio - **Configuración manual** - Para servicios que requieren configuración en tiempo de ejecución 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 f8b15a9..bbc4094 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 @@ -1,57 +1,62 @@ # Creating an injectable service -Service is a broad category encompassing any value, function, or feature that an application needs. -A service is typically a class with a narrow, well-defined purpose. -A component is one type of class that can use DI. +A service is a broad category that encompasses any value, function, or feature that your application needs. +A service is typically a class with a focused and well-defined purpose. +A component is one type of class that you can use with dependency injection (DI). -Angular distinguishes components from services to increase modularity and reusability. -By separating a component's view-related features from other kinds of processing, you can make your component classes lean and efficient. +Angular distinguishes components from services to improve modularity and reusability. +By separating a component's view-related features from other types of processing, you can keep your component classes lean and efficient. -Ideally, a component's job is to enable the user experience and nothing more. +Ideally, your component's responsibility is to enable the user experience and nothing more. A component should present properties and methods for data binding, to mediate between the view (rendered by the template) and the application logic (which often includes some notion of a model). -A component can delegate certain tasks to services, such as fetching data from the server, validating user input, or logging directly to the console. -By defining such processing tasks in an injectable service class, you make those tasks available to any component. -You can also make your application more adaptable by configuring different providers of the same kind of service, as appropriate in different circumstances. +You can delegate tasks from a component to services, such as fetching data from a server, validating user input, or logging to the console. +By defining such tasks in an injectable service class, you make those capabilities available to any component. +You can also make your application more adaptable by configuring different providers for the same type of service based on different circumstances. -Angular does not enforce these principles. -Angular helps you follow these principles by making it easy to factor your application logic into services and make those services available to components through DI. +Angular does not strictly enforce these principles. +Angular helps you follow these principles by making it easy to organize your application logic into services and make those services available to components through DI. ## Service examples Here's an example of a service class that logs to the browser console: - +```ts {header: "logger.service.ts (class)"} export class Logger { - log(msg: unknown) { console.log(msg); } - error(msg: unknown) { console.error(msg); } - warn(msg: unknown) { console.warn(msg); } + log(msg: unknown) { + console.log(msg); + } + error(msg: unknown) { + console.error(msg); + } + warn(msg: unknown) { + console.warn(msg); + } } - +``` Services can depend on other services. For example, here's a `HeroService` that depends on the `Logger` service, and also uses `BackendService` to get heroes. That service in turn might depend on the `HttpClient` service to fetch heroes asynchronously from a server: - -import { inject } from "@angular/core"; +```ts {header: "hero.service.ts", highlight="[7,8,12,13]"} +import {inject} from '@angular/core'; export class HeroService { -private heroes: Hero[] = []; + private heroes: Hero[] = []; -private backend = inject(BackendService); -private logger = inject(Logger); + private backend = inject(BackendService); + private logger = inject(Logger); -async getHeroes() { -// Fetch -this.heroes = await this.backend.getAll(Hero); -// Log -this.logger.log(`Fetched ${this.heroes.length} heroes.`); -return this.heroes; -} + async getHeroes() { + // Fetch + this.heroes = await this.backend.getAll(Hero); + // Log + this.logger.log(`Fetched ${this.heroes.length} heroes.`); + return this.heroes; + } } - +``` ## Creating an injectable service with the CLI @@ -61,35 +66,28 @@ To generate a new `HeroService` class in the `src/app/heroes` folder, follow the 1. Run this [Angular CLI](/tools/cli) command: - +```sh ng generate service heroes/hero - +``` This command creates the following default `HeroService`: ```ts {header: 'heroes/hero.service.ts (CLI-generated)'} -import {Injectable} from '@angular/core'; +import {Service} from '@angular/core'; -@Injectable({ - providedIn: 'root', -}) +@Service() export class HeroService {} ``` -The `@Injectable()` decorator specifies that Angular can use this class in the DI system. -The metadata, `providedIn: 'root'`, means that the `HeroService` is provided throughout the application. +The `@Service()` decorator specifies that Angular can use this class in the DI system and that the `HeroService` is available throughout your application. Add a `getHeroes()` method that returns the heroes from `mock.heroes.ts` to get the hero mock data: ```ts {header: 'hero.service.ts'} -import {Injectable} from '@angular/core'; +import {Service} from '@angular/core'; import {HEROES} from './mock-heroes'; -@Injectable({ - // declares that this service should be created - // by the root application injector. - providedIn: 'root', -}) +@Service() export class HeroService { getHeroes() { return HEROES; @@ -101,26 +99,26 @@ For clarity and maintainability, it is recommended that you define components an ## Injecting services -To inject a service as a dependency into a component, you can declare a class field representing the dependency and use Angular's `inject` function to initialize it. +To inject a service into a component, declare a class field for the dependency and use Angular's [`inject`](/api/core/inject) function to initialize it. -The following example specifies the `HeroService` in the `HeroListComponent`. +The following example specifies the `HeroService` in the `HeroList`. The type of `heroService` is `HeroService`. ```ts import {inject} from '@angular/core'; -export class HeroListComponent { +export class HeroList { private heroService = inject(HeroService); } ``` It is also possible to inject a service into a component using the component's constructor: -```ts {header: 'hero-list.component.ts (constructor signature)'} +```ts {header: 'hero-list.ts (constructor signature)'} constructor(private heroService: HeroService) ``` -The `inject` method can be used in both classes and functions, while the constructor method can naturally only be used in a class constructor. However, in either case a dependency may only be injected in a valid [injection context](guide/di/dependency-injection-context), usually in the construction or initialization of a component. +The [`inject`](/api/core/inject) method can be used in both classes and functions, while the constructor method can naturally only be used in a class constructor. However, in both cases, you can only inject a dependency within a valid [injection context](guide/di/dependency-injection-context), typically during the construction or initialization of a component. ## Injecting services in other services @@ -128,13 +126,11 @@ When a service depends on another service, follow the same pattern as injecting In the following example, `HeroService` depends on a `Logger` service to report its activities: ```ts {header: 'hero.service.ts, highlight: [[3],[9],[12]]} -import {inject, Injectable} from '@angular/core'; +import {inject, Service} from '@angular/core'; import {HEROES} from './mock-heroes'; import {Logger} from '../logger.service'; -@Injectable({ - providedIn: 'root', -}) +@Service() export class HeroService { private logger = inject(Logger); @@ -150,6 +146,6 @@ In this example, the `getHeroes()` method uses the `Logger` service by logging a ## What's next - - + + 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 8a001f8..8fd6f61 100644 --- a/adev-es/src/content/guide/di/creating-injectable-service.md +++ b/adev-es/src/content/guide/di/creating-injectable-service.md @@ -1,57 +1,62 @@ # Creando un servicio inyectable -Un servicio es una categoría amplia que abarca cualquier valor, función o característica que una aplicación necesita. +Un servicio es una categoría amplia que abarca cualquier valor, función o característica que tu aplicación necesita. Un servicio es típicamente una clase con un propósito específico y bien definido. -Un componente es un tipo de clase que puede usar DI. +Un componente es un tipo de clase que puedes usar con inyección de dependencias (DI). -Angular distingue los componentes de los servicios para aumentar la modularidad y reutilización. -Al separar las características relacionadas con la vista de un componente de otros tipos de procesamiento, puedes hacer que tus clases de componente sean eficientes y ligeras. +Angular distingue los componentes de los servicios para mejorar la modularidad y reutilización. +Al separar las características relacionadas con la vista de un componente de otros tipos de procesamiento, puedes mantener tus clases de componente eficientes y ligeras. -Idealmente, el trabajo de un componente es habilitar la experiencia del usuario y nada más. +Idealmente, la responsabilidad de tu componente es habilitar la experiencia del usuario y nada más. Un componente debe presentar propiedades y métodos para el enlace de datos, para mediar entre la vista (renderizada por la plantilla) y la lógica de la aplicación (que a menudo incluye alguna noción de un modelo). -Un componente puede delegar ciertas tareas a los servicios, como obtener datos del servidor, validar la entrada del usuario o registrar directamente en la consola. -Al definir tales tareas de procesamiento en una clase de servicio inyectable, haces que esas tareas estén disponibles para cualquier componente. -También puedes hacer que tu aplicación sea más adaptable configurando diferentes proveedores del mismo tipo de servicio, según sea apropiado en diferentes circunstancias. +Puedes delegar tareas de un componente a los servicios, como obtener datos de un servidor, validar la entrada del usuario o registrar en la consola. +Al definir tales tareas en una clase de servicio inyectable, haces que esas capacidades estén disponibles para cualquier componente. +También puedes hacer que tu aplicación sea más adaptable configurando diferentes proveedores para el mismo tipo de servicio según las circunstancias. -Angular no hace cumplir estos principios. -Angular te ayuda a seguir estos principios haciendo que sea fácil factorizar la lógica de tu aplicación en servicios y hacer que esos servicios estén disponibles para los componentes a través de DI. +Angular no hace cumplir estrictamente estos principios. +Angular te ayuda a seguir estos principios facilitando la organización de la lógica de tu aplicación en servicios y poniendo esos servicios a disposición de los componentes a través de DI. ## Ejemplos de servicios Aquí tienes un ejemplo de una clase de servicio que registra en la consola del navegador: - +```ts {header: "logger.service.ts (class)"} export class Logger { - log(msg: unknown) { console.log(msg); } - error(msg: unknown) { console.error(msg); } - warn(msg: unknown) { console.warn(msg); } + log(msg: unknown) { + console.log(msg); + } + error(msg: unknown) { + console.error(msg); + } + warn(msg: unknown) { + console.warn(msg); + } } - +``` Los servicios pueden depender de otros servicios. Por ejemplo, aquí tienes un `HeroService` que depende del servicio `Logger`, y también usa `BackendService` para obtener héroes. Ese servicio a su vez podría depender del servicio `HttpClient` para obtener héroes de forma asíncrona desde un servidor: - -import { inject } from "@angular/core"; +```ts {header: "hero.service.ts", highlight="[7,8,12,13]"} +import {inject} from '@angular/core'; export class HeroService { -private heroes: Hero[] = []; + private heroes: Hero[] = []; -private backend = inject(BackendService); -private logger = inject(Logger); + private backend = inject(BackendService); + private logger = inject(Logger); -async getHeroes() { -// Fetch -this.heroes = await this.backend.getAll(Hero); -// Log -this.logger.log(`Fetched ${this.heroes.length} heroes.`); -return this.heroes; -} + async getHeroes() { + // Fetch + this.heroes = await this.backend.getAll(Hero); + // Log + this.logger.log(`Fetched ${this.heroes.length} heroes.`); + return this.heroes; + } } - +``` ## Creando un servicio inyectable con el CLI @@ -61,35 +66,28 @@ Para generar una nueva clase `HeroService` en la carpeta `src/app/heroes`, sigue 1. Ejecuta este comando [Angular CLI](/tools/cli): - +```sh ng generate service heroes/hero - +``` Este comando crea el siguiente `HeroService` por defecto: ```ts {header: 'heroes/hero.service.ts (CLI-generated)'} -import {Injectable} from '@angular/core'; +import {Service} from '@angular/core'; -@Injectable({ - providedIn: 'root', -}) +@Service() export class HeroService {} ``` -El decorador `@Injectable()` especifica que Angular puede usar esta clase en el sistema DI. -Los metadatos, `providedIn: 'root'`, significan que el `HeroService` se provee en toda la aplicación. +El decorador `@Service()` especifica que Angular puede usar esta clase en el sistema DI y que el `HeroService` está disponible en toda tu aplicación. Agrega un método `getHeroes()` que devuelva los héroes de `mock.heroes.ts` para obtener los datos simulados de héroes: ```ts {header: 'hero.service.ts'} -import {Injectable} from '@angular/core'; +import {Service} from '@angular/core'; import {HEROES} from './mock-heroes'; -@Injectable({ - // declara que este servicio debe ser creado - // por el inyector de la aplicación raíz. - providedIn: 'root', -}) +@Service() export class HeroService { getHeroes() { return HEROES; @@ -101,40 +99,38 @@ Para claridad y mantenibilidad, se recomienda que definas componentes y servicio ## Inyectando servicios -Para inyectar un servicio como dependencia en un componente, puedes declarar un campo de clase que represente la dependencia y usar la función `inject` de Angular para inicializarlo. +Para inyectar un servicio en un componente, declara un campo de clase para la dependencia y usa la función [`inject`](/api/core/inject) de Angular para inicializarlo. -El siguiente ejemplo especifica el `HeroService` en el `HeroListComponent`. +El siguiente ejemplo especifica el `HeroService` en el `HeroList`. El tipo de `heroService` es `HeroService`. ```ts import {inject} from '@angular/core'; -export class HeroListComponent { +export class HeroList { private heroService = inject(HeroService); } ``` También es posible inyectar un servicio en un componente usando el constructor del componente: -```ts {header: 'hero-list.component.ts (constructor signature)'} +```ts {header: 'hero-list.ts (constructor signature)'} constructor(private heroService: HeroService) ``` -El método `inject` puede ser usado tanto en clases como en funciones, mientras que el método constructor naturalmente solo puede ser usado en un constructor de clase. Sin embargo, en cualquier caso una dependencia solo puede ser inyectada en un [contexto de inyección](guide/di/dependency-injection-context) válido, usualmente en la construcción o inicialización de un componente. +El método [`inject`](/api/core/inject) puede ser usado tanto en clases como en funciones, mientras que el método constructor naturalmente solo puede ser usado en un constructor de clase. Sin embargo, en ambos casos, solo puedes inyectar una dependencia dentro de un [contexto de inyección](guide/di/dependency-injection-context) válido, típicamente durante la construcción o inicialización de un componente. ## Inyectando servicios en otros servicios Cuando un servicio depende de otro servicio, sigue el mismo patrón que inyectar en un componente. En el siguiente ejemplo, `HeroService` depende de un servicio `Logger` para reportar sus actividades: -```ts {header: 'hero.service.ts', highlight: [[3],[9],[12]]} -import {inject, Injectable} from '@angular/core'; +```ts {header: 'hero.service.ts, highlight: [[3],[9],[12]]} +import {inject, Service} from '@angular/core'; import {HEROES} from './mock-heroes'; import {Logger} from '../logger.service'; -@Injectable({ - providedIn: 'root', -}) +@Service() export class HeroService { private logger = inject(Logger); @@ -150,6 +146,6 @@ En este ejemplo, el método `getHeroes()` usa el servicio `Logger` registrando u ## Próximos pasos - - + + diff --git a/adev-es/src/content/guide/di/debugging-and-troubleshooting-di.md b/adev-es/src/content/guide/di/debugging-and-troubleshooting-di.md new file mode 100644 index 0000000..6d629f2 --- /dev/null +++ b/adev-es/src/content/guide/di/debugging-and-troubleshooting-di.md @@ -0,0 +1,1015 @@ +# Debugging and troubleshooting dependency injection + +Dependency injection (DI) issues typically stem from configuration mistakes, scope problems, or incorrect usage patterns. This guide helps you identify and resolve common DI problems that developers encounter. + +## Common pitfalls and solutions + +### Services not available where expected + +One of the most common DI issues occurs when you try to inject a service but Angular cannot find it in the current injector or any parent injector. This usually happens when the service is provided in the wrong scope or not provided at all. + +#### Provider scope mismatch + +When you provide a service in a component's `providers` array, Angular creates an instance in that component's injector. This instance is only available to that component and its children. Parent components and sibling components cannot access it because they use different injectors. + +```angular-ts {header: 'child-view.ts'} +import {Component} from '@angular/core'; +import {DataStore} from './data-store'; + +@Component({ + selector: 'app-child', + template: '

    Child

    ', + providers: [DataStore], // Only available in this component and its children +}) +export class ChildView {} +``` + +```angular-ts {header: 'parent-view.ts'} +import {Component, inject} from '@angular/core'; +import {DataStore} from './data-store'; + +@Component({ + selector: 'app-parent', + template: '', +}) +export class ParentView { + private dataService = inject(DataStore); // ERROR: Not available to parent +} +``` + +Angular only searches up the hierarchy, never down. Parent components cannot access services provided in child components. + +**Solution:** Provide the service at a higher level (application or parent component). + +```ts {prefer} +import {Service} from '@angular/core'; + +@Service() +export class DataStore { + // Available everywhere +} +``` + +TIP: `@Service` makes services available everywhere and enables tree-shaking. If you don't want to scope it to the entire app, specify `autoProvided: false`. + +#### Services and lazy-loaded routes + +When you provide a service in a lazy-loaded route's `providers` array, Angular creates a child injector for that route. This injector and its services only become available after the route loads. Components in the eagerly-loaded parts of your application cannot access these services because they use different injectors that exist before the lazy-loaded injector is created. + +```ts {header: 'feature.routes.ts'} +import {Routes} from '@angular/router'; +import {FeatureClient} from './feature-client'; + +export const featureRoutes: Routes = [ + { + path: 'feature', + providers: [FeatureClient], + loadComponent: () => import('./feature-view'), + }, +]; +``` + +```angular-ts {header: 'eager-view.ts'} +import {Component, inject} from '@angular/core'; +import {FeatureClient} from './feature-client'; + +@Component({ + selector: 'app-eager', + template: '

    Eager Component

    ', +}) +export class EagerView { + private featureService = inject(FeatureClient); // ERROR: Not available yet +} +``` + +Lazy-loaded routes create child injectors that are only available after the route loads. + +NOTE: By default, route injectors and their services persist even after navigating away from the route. They are not destroyed until the application is closed. For automatic cleanup of unused route injectors, see [customizing route behavior](guide/routing/customizing-route-behavior#experimental-automatic-cleanup-of-unused-route-injectors). + +**Solution:** Use `@Service` for services that need to be shared across lazy boundaries. + +```ts {prefer, header: 'Provide at root for shared services'} +import {Service} from '@angular/core'; + +@Service() +export class FeatureClient { + // Available everywhere, including before lazy load +} +``` + +If the service should be lazy-loaded but still available to eager components, inject it only where needed and use optional injection to handle availability. + +### Multiple instances instead of singletons + +You expect one shared instance (singleton) but get separate instances in different components. + +#### Providing in component instead of root + +When you add a service to a component's `providers` array, Angular creates a new instance of that service for each instance of the component. Each component gets its own separate service instance, which means changes in one component don't affect the service instance in other components. This is often unexpected when you want shared state across your application. + +```angular-ts {avoid, header: 'Component-level provider creates multiple instances'} +import {Component, inject} from '@angular/core'; +import {UserClient} from './user-client'; + +@Component({ + selector: 'app-profile', + template: '

    Profile

    ', + providers: [UserClient], // Creates new instance per component! +}) +export class UserProfile { + private userService = inject(UserClient); +} + +@Component({ + selector: 'app-settings', + template: '

    Settings

    ', + providers: [UserClient], // Different instance! +}) +export class UserSettings { + private userService = inject(UserClient); +} +``` + +Each component gets its own `UserClient` instance. Changes in one component don't affect the other. + +**Solution:** Use `@Service` for singletons. + +```ts {prefer, header: 'Root-level singleton'} +import {Injectable} from '@angular/core'; + +@Service() +export class UserClient { + // Single instance shared across all components +} +``` + +#### When multiple instances are intentional + +Sometimes you want separate instances per component for component-specific state. + +```angular-ts {header: 'Intentional: Component-scoped state'} +import {Injectable, signal} from '@angular/core'; + +@Injectable() // No providedIn - must be provided explicitly +export class FormStateStore { + private formData = signal({}); + + setData(data: any) { + this.formData.set(data); + } + + getData() { + return this.formData(); + } +} + +@Component({ + selector: 'app-user-form', + template: '
    ...', + providers: [FormStateStore], // Each form gets its own state +}) +export class UserForm { + private formState = inject(FormStateStore); +} +``` + +This pattern is useful for: + +- Form state management (each form has isolated state) +- Component-specific caching +- Temporary data that shouldn't be shared + +### Incorrect inject() usage + +The `inject()` function only works in specific contexts during class construction and factory execution. + +#### Using inject() in lifecycle hooks + +When you call the `inject()` function inside lifecycle hooks like `ngOnInit()`, `ngAfterViewInit()`, or `ngOnDestroy()`, Angular throws an error because these methods run outside the injection context. The injection context is only available during the synchronous execution of class construction, which happens before lifecycle hooks are called. + +```angular-ts {avoid, header: 'inject() in ngOnInit'} +import {Component, inject} from '@angular/core'; +import {UserClient} from './user-client'; + +@Component({ + selector: 'app-profile', + template: '

    User: {{userName}}

    ', +}) +export class UserProfile { + userName = ''; + + ngOnInit() { + const userService = inject(UserClient); // ERROR: Not an injection context + this.userName = userService.getUser().name; + } +} +``` + +**Solution:** Capture dependencies and derive values in field initializers. + +```angular-ts {prefer, header: 'Derive values in field initializers'} +import {Component, inject} from '@angular/core'; +import {UserClient} from './user-client'; + +@Component({ + selector: 'app-profile', + template: '

    User: {{userName}}

    ', +}) +export class UserProfile { + private userService = inject(UserClient); + userName = this.userService.getUser().name; +} +``` + +#### Using the Injector for deferred injection + +When you need to retrieve services outside an injection context, use the captured `Injector` directly with `injector.get()`: + +```angular-ts +import {Component, inject, Injector} from '@angular/core'; +import {UserClient} from './user-client'; + +@Component({ + selector: 'app-profile', + template: '', +}) +export class UserProfile { + private injector = inject(Injector); + + delayedLoad() { + setTimeout(() => { + const userService = this.injector.get(UserClient); + console.log(userService.getUser()); + }, 1000); + } +} +``` + +#### Using runInInjectionContext for callbacks + +Use `runInInjectionContext()` when you need to enable **other code** to call `inject()`. This is useful when accepting callbacks that might use dependency injection: + +```angular-ts +import {Component, inject, Injector, input} from '@angular/core'; + +@Component({ + selector: 'app-data-loader', + template: '', +}) +export class DataLoader { + private injector = inject(Injector); + onLoad = input<() => void>(); + + load() { + const callback = this.onLoad(); + if (callback) { + // Enable the callback to use inject() + this.injector.runInInjectionContext(callback); + } + } +} +``` + +The `runInInjectionContext()` method creates a temporary injection context, allowing code inside the callback to call `inject()`. + +IMPORTANT: Always capture dependencies at the class level when possible. Use `injector.get()` for simple deferred retrieval, and `runInInjectionContext()` only when external code needs to call `inject()`. + +TIP: Use `assertInInjectionContext()` to verify your code is running in a valid injection context. This is useful when creating reusable functions that call `inject()`. See [Asserting the context](guide/di/dependency-injection-context#asserts-the-context) for details. + +### providers vs viewProviders confusion + +The difference between `providers` and `viewProviders` affects content projection scenarios. + +#### Understanding the difference + +**providers:** Available to the component's template AND any content projected into the component (ng-content). + +**viewProviders:** Only available to the component's template, NOT to projected content. + +```angular-ts {header: 'parent-view.ts'} +import {Component, inject} from '@angular/core'; +import {ThemeStore} from './theme-store'; + +@Component({ + selector: 'app-parent', + template: ` +
    +

    Theme: {{ themeService.theme() }}

    + +
    + `, + providers: [ThemeStore], // Available to content children +}) +export class ParentView { + protected themeService = inject(ThemeStore); +} + +@Component({ + selector: 'app-parent-view', + template: ` +
    +

    Theme: {{ themeService.theme() }}

    + +
    + `, + viewProviders: [ThemeStore], // NOT available to content children +}) +export class ParentViewOnly { + protected themeService = inject(ThemeStore); +} +``` + +```angular-ts {header: 'child-view.ts'} +import {Component, inject} from '@angular/core'; +import {ThemeStore} from './theme-store'; + +@Component({ + selector: 'app-child', + template: '

    Child theme: {{theme()}}

    ', +}) +export class ChildView { + private themeService = inject(ThemeStore, {optional: true}); + theme = () => this.themeService?.theme() ?? 'none'; +} +``` + +```angular-ts {header: 'app.ts'} +@Component({ + selector: 'app-root', + template: ` + + + + + + + + + + `, +}) +export class App {} +``` + +**When projected into `app-parent`:** The child component can inject `ThemeStore` because `providers` makes it available to projected content. + +**When projected into `app-parent-view`:** The child component cannot inject `ThemeStore` because `viewProviders` restricts it to the parent's template only. + +#### Choosing between providers and viewProviders + +Use `providers` when: + +- The service should be available to projected content +- You want content children to access the service +- You're providing general-purpose services + +Use `viewProviders` when: + +- The service should only be available to your component's template +- You want to hide implementation details from projected content +- You're providing internal services that shouldn't leak out + +**Default recommendation:** Use `providers` unless you have a specific reason to restrict access with `viewProviders`. + +### InjectionToken issues + +When using `InjectionToken` for non-class dependencies, developers often encounter problems related to token identity, type safety, and provider configuration. These issues usually stem from how JavaScript handles object identity and how TypeScript infers types. + +#### Token identity confusion + +When you create a new `InjectionToken` instance, JavaScript creates a unique object in memory. Even if you create another `InjectionToken` with the exact same description string, it's a completely different object. Angular uses the token object's identity (not its description) to match providers with injection points, so tokens with the same description but different object identities cannot access each other's values. + +```ts {header: 'config.token.ts'} +import {InjectionToken} from '@angular/core'; + +export interface AppConfig { + apiUrl: string; +} + +export const APP_CONFIG = new InjectionToken('app config'); +``` + +```ts {header: 'app.config.ts'} +import {APP_CONFIG} from './config.token'; + +export const appConfig: AppConfig = { + apiUrl: 'https://api.example.com', +}; + +bootstrapApplication(App, { + providers: [{provide: APP_CONFIG, useValue: appConfig}], +}); +``` + +```angular-ts {avoid, header: 'feature-view.ts'} +// Creating new token with same description +import {InjectionToken, inject} from '@angular/core'; +import {AppConfig} from './config.token'; + +const APP_CONFIG = new InjectionToken('app config'); + +@Component({ + selector: 'app-feature', + template: '

    Feature

    ', +}) +export class FeatureView { + private config = inject(APP_CONFIG); // ERROR: Different token instance! +} +``` + +Even though both tokens have the description `'app config'`, they are different objects. Angular compares tokens by reference, not by description. + +**Solution:** Import the same token instance. + +```angular-ts {prefer, header: 'feature-view.ts'} +import {inject} from '@angular/core'; +import {APP_CONFIG, AppConfig} from './config.token'; + +@Component({ + selector: 'app-feature', + template: '

    API: {{config.apiUrl}}

    ', +}) +export class FeatureView { + protected config = inject(APP_CONFIG); // Works: Same token instance +} +``` + +TIP: Always export tokens from a shared file and import them everywhere they're needed. Never create multiple `InjectionToken` instances with the same description. + +#### Trying to inject interfaces + +When you define a TypeScript interface, it only exists during compilation for type checking. TypeScript erases all interface definitions when it compiles to JavaScript, so at runtime there's no object for Angular to use as an injection token. If you try to inject an interface type, Angular has nothing to match against the provider configuration. + +```angular-ts {avoid, header: "Can't inject interface"} +interface UserConfig { + name: string; + email: string; +} + +@Component({ + selector: 'app-profile', + template: '

    Profile

    ', +}) +export class UserProfile { + // ERROR: Interfaces don't exist at runtime + constructor(private config: UserConfig) {} +} +``` + +**Solution:** Use `InjectionToken` for interface types. + +```angular-ts {prefer, header: 'Use InjectionToken for interfaces'} +import {InjectionToken, inject} from '@angular/core'; + +interface UserConfig { + name: string; + email: string; +} + +export const USER_CONFIG = new InjectionToken('user configuration'); + +// Provide the configuration +bootstrapApplication(App, { + providers: [ + { + provide: USER_CONFIG, + useValue: {name: 'Alice', email: 'alice@example.com'}, + }, + ], +}); + +// Inject using the token +@Component({ + selector: 'app-profile', + template: '

    User: {{config.name}}

    ', +}) +export class UserProfile { + protected config = inject(USER_CONFIG); +} +``` + +The `InjectionToken` exists at runtime and can be used for injection, while the `UserConfig` interface provides type safety during development. + +### Circular dependencies + +Circular dependencies occur when services inject each other, creating a cycle that Angular cannot resolve. For detailed explanations and code examples, see [NG0200: Circular dependency](errors/NG0200). + +**Resolution strategies** (in order of preference): + +1. **Restructure** - Extract shared logic to a third service, breaking the cycle +2. **Use events** - Replace direct dependencies with event-based communication (such as `Subject`) +3. **Lazy injection** - Use `Injector.get()` to defer one dependency (last resort) + +NOTE: Do not use `forwardRef()` for service circular dependencies—it only solves circular imports in standalone component configurations. + +## Debugging dependency resolution + +### Understanding the resolution process + +Angular resolves dependencies by walking up the injector hierarchy. When a `NullInjectorError` occurs, understanding this search order helps you identify where to add the missing provider. + +Angular searches in this order: + +1. **Element injector** - The current component or directive +2. **Parent element injectors** - Up the DOM tree through parent components +3. **Environment injector** - The route or application injector +4. **NullInjector** - Throws `NullInjectorError` if not found + +When you see a `NullInjectorError`, the service isn't provided at any level the component can access. Check that: + +- The service has `@Service()` or +- The service has `@Injectable({providedIn: 'root'})`, or +- The service is in a `providers` array the component can reach + +You can modify this search behavior with resolution modifiers like `self`, `skipSelf`, `host`, and `optional`. For complete coverage of resolution rules and modifiers, see the [Hierarchical injectors guide](guide/di/hierarchical-dependency-injection). + +### Using Angular DevTools + +Angular DevTools includes an injector tree inspector that visualizes the entire injector hierarchy and shows which providers are available at each level. For installation and general usage, see the [Angular DevTools injector documentation](tools/devtools/injectors). + +When debugging DI issues, use DevTools to answer these questions: + +- **Is the service provided?** Select the component that fails to inject and check if the service appears in the Injector section. +- **At what level?** Walk up the component tree to find where the service is actually provided (component, route, or application level). +- **Multiple instances?** If a singleton service appears in multiple component injectors, it's likely provided in component `providers` arrays instead of using `@Service` or `providedIn: 'root'`. + +If a service never appears in any injector, verify it has the `@Service` decorator or is listed in a `providers` array. + +### Logging and tracing injection + +When DevTools isn't enough, use logging to trace injection behavior. + +#### Logging service creation + +Add console logs to service constructors to see when services are created. + +```ts +import {Service} from '@angular/core'; + +@Service() +export class UserClient { + constructor() { + console.log('UserClient created'); + console.trace(); // Shows call stack + } + + getUser() { + return {name: 'Alice'}; + } +} +``` + +When the service is created, you'll see the log message and a stack trace showing where the injection occurred. + +**What to look for:** + +- How many times is the constructor called? (should be once for singletons) +- Where in the code is it being injected? (check the stack trace) +- Is it created at the expected time? (application startup vs lazy) + +#### Checking service availability + +Use optional injection with logging to determine if a service is available. + +```angular-ts +import {Component, inject} from '@angular/core'; +import {UserClient} from './user-client'; + +@Component({ + selector: 'app-debug', + template: '

    Debug Component

    ', +}) +export class DebugView { + private userService = inject(UserClient, {optional: true}); + + constructor() { + if (this.userService) { + console.log('UserClient available:', this.userService); + } else { + console.warn('UserClient NOT available'); + console.trace(); // Shows where we tried to inject + } + } +} +``` + +This pattern helps you verify if a service is available without crashing the application. + +#### Logging resolution modifiers + +Test different resolution strategies with logging. + +```angular-ts +import {Component, inject} from '@angular/core'; +import {UserClient} from './user-client'; + +@Component({ + selector: 'app-debug', + template: '

    Debug Component

    ', + providers: [UserClient], +}) +export class DebugView { + // Try to get local instance + private localService = inject(UserClient, {self: true, optional: true}); + + // Try to get parent instance + private parentService = inject(UserClient, { + skipSelf: true, + optional: true, + }); + + constructor() { + console.log('Local instance:', this.localService); + console.log('Parent instance:', this.parentService); + console.log('Same instance?', this.localService === this.parentService); + } +} +``` + +This shows you which instances are available at different injector levels. + +### Debugging workflow + +When DI fails, follow this systematic approach: + +**Step 1: Read the error message** + +- Identify the error code (NG0200, NG0203, etc.) +- Read the dependency path +- Note which token failed + +**Step 2: Check the basics** + +- Does the service have `@Service` or `@Injectable()`? +- If you use `@Injectable`, is `providedIn` set correctly? +- Are imports correct? +- Is the file included in compilation? + +**Step 3: Verify injection context** + +- Is `inject()` called in a valid context? +- Check for async issues (await, setTimeout, promises) +- Verify timing (not after destroy) + +**Step 4: Use debugging tools** + +- Open Angular DevTools +- Check injector hierarchy +- Add console logs to constructors +- Use optional injection to test availability + +**Step 5: Simplify and isolate** + +- Remove dependencies one by one +- Test in a minimal component +- Check each injector level separately +- Create a reproduction case + +## DI error reference + +This section provides detailed information about specific Angular DI error codes you may encounter. Use this as a reference when you see these errors in your console. + +### NullInjectorError: No provider for [Service] + +**Error code:** None (displayed as `NullInjectorError`) + +This error occurs when Angular cannot find a provider for a token in the injector hierarchy. The error message includes a dependency path showing where the injection was attempted. + +``` +NullInjectorError: No provider for UserClient! + Dependency path: App -> AuthClient -> UserClient +``` + +The dependency path shows that `App` injected `AuthClient`, which tried to inject `UserClient`, but no provider was found. + +#### Missing the `@Service ` or `@Injectable` decorator + +The most common cause is forgetting the `@Service` or `@Injectable()` decorator on a service class. + +```ts {avoid, header: 'Missing decorator'} +export class UserClient { + getUser() { + return {name: 'Alice'}; + } +} +``` + +Angular requires the `@Service()` decorator to generate the metadata needed for dependency injection. + +```ts {prefer, header: 'Include @Service'} +import {Service} from '@angular/core'; + +@Service() +export class UserClient { + getUser() { + return {name: 'Alice'}; + } +} +``` + +NOTE: Classes with zero-argument constructors can work without `@Service()`, but this is not recommended. Always include the decorator for consistency and to avoid issues when adding dependencies later. + +#### Missing providedIn configuration + +A service may have `@Injectable()` but not specify where it should be provided. + +```ts {avoid, header: 'No providedIn specified'} +import {Injectable} from '@angular/core'; + +@Injectable() +export class UserClient { + getUser() { + return {name: 'Alice'}; + } +} +``` + +Use the `@Service` decorator to make the service available throughout your application. + +```ts {prefer, header: 'Specify providedIn'} +import {Service} from '@angular/core'; + +@Service() +export class UserClient { + getUser() { + return {name: 'Alice'}; + } +} +``` + +The `@Service` decorator makes the service available application-wide and enables tree-shaking (the service is removed from the bundle if never injected). + +#### Standalone component missing imports + +In Angular v20+ with standalone components, you must explicitly import or provide dependencies in each component. + +```angular-ts {avoid, header: 'Missing service import'} +import {Component, inject} from '@angular/core'; +import {UserClient} from './user-client'; + +@Component({ + selector: 'app-profile', + template: '

    User: {{user().name}}

    ', +}) +export class UserProfile { + private userService = inject(UserClient); // ERROR: No provider + user = this.userService.getUser(); +} +``` + +Ensure the service uses `@Service` or add it to the component's `providers` array. + +```angular-ts {prefer, header: 'Service uses providedIn: root'} +import {Component, inject} from '@angular/core'; +import {UserClient} from './user-client'; + +@Component({ + selector: 'app-profile', + template: '

    User: {{user().name}}

    ', +}) +export class UserProfile { + private userService = inject(UserClient); // Works: providedIn: 'root' + user = this.userService.getUser(); +} +``` + +#### Debugging with the dependency path + +The dependency path in the error message shows the chain of injections that led to the failure. + +``` +NullInjectorError: No provider for LoggerStore! + Dependency path: App -> DataStore -> ApiClient -> LoggerStore +``` + +This path tells you: + +1. `App` injected `DataStore` +2. `DataStore` injected `ApiClient` +3. `ApiClient` tried to inject `LoggerStore` +4. No provider for `LoggerStore` was found + +Start your investigation at the end of the chain (`LoggerStore`) and verify it has proper configuration. + +#### Checking provider availability with optional injection + +Use optional injection to check if a provider exists without throwing an error. + +```angular-ts +import {Component, inject} from '@angular/core'; +import {UserClient} from './user-client'; + +@Component({ + selector: 'app-debug', + template: '

    Service available: {{serviceAvailable}}

    ', +}) +export class DebugView { + private userService = inject(UserClient, {optional: true}); + serviceAvailable = this.userService !== null; +} +``` + +Optional injection returns `null` if no provider is found, allowing you to handle the absence gracefully. + +### NG0203: inject() must be called from an injection context + +**Error code:** NG0203 + +This error occurs when you call `inject()` outside of a valid injection context. Angular requires `inject()` to be called synchronously during class construction or factory execution. + +``` +NG0203: inject() must be called from an injection context such as a +constructor, a factory function, a field initializer, or a function +used with `runInInjectionContext`. +``` + +#### Valid injection contexts + +Angular allows `inject()` in these locations: + +1. **Class field initializers** + + ```angular-ts + import {Component, inject} from '@angular/core'; + import {UserClient} from './user-client'; + + @Component({ + selector: 'app-profile', + template: '

    User: {{user().name}}

    ', + }) + export class UserProfile { + private userService = inject(UserClient); // Valid + user = this.userService.getUser(); + } + ``` + +2. **Class constructor** + + ```angular-ts + import {Component, inject} from '@angular/core'; + import {UserClient} from './user-client'; + + @Component({ + selector: 'app-profile', + template: '

    User: {{user().name}}

    ', + }) + export class UserProfile { + private userService: UserClient; + + constructor() { + this.userService = inject(UserClient); // Valid + } + + user = this.userService.getUser(); + } + ``` + +3. **Provider factory functions** + + ```ts + import {inject, InjectionToken} from '@angular/core'; + import {UserClient} from './user-client'; + + export const GREETING = new InjectionToken('greeting', { + factory() { + const userService = inject(UserClient); // Valid + const user = userService.getUser(); + return `Hello, ${user.name}`; + }, + }); + ``` + +4. **Inside runInInjectionContext()** + + ```angular-ts + import {Component, inject, Injector} from '@angular/core'; + import {UserClient} from './user-client'; + + @Component({ + selector: 'app-profile', + template: '', + }) + export class UserProfile { + private injector = inject(Injector); + + loadUser() { + this.injector.runInInjectionContext(() => { + const userService = inject(UserClient); // Valid + console.log(userService.getUser()); + }); + } + } + ``` + +Other injection contexts that `inject()` also works in include: + +- [provideAppInitializer](api/core/provideAppInitializer) +- [provideEnvironmentInitializer](api/core/provideEnvironmentInitializer) +- Functional [route guards](guide/routing/route-guards) +- Functional [data resolvers](guide/routing/data-resolvers) + +#### When this error occurs + +This error occurs when: + +- Calling `inject()` in lifecycle hooks (`ngOnInit`, `ngAfterViewInit`, etc.) +- Calling `inject()` after `await` in async functions +- Calling `inject()` in callbacks (`setTimeout`, `Promise.then()`, etc.) +- Calling `inject()` outside of class construction phase + +See the "Incorrect inject() usage" section for detailed examples and solutions. + +#### Solutions and workarounds + +**Solution 1:** Capture dependencies in field initializers (most common) + +```ts +private userService = inject(UserClient) // Capture at class level +``` + +**Solution 2:** Use `runInInjectionContext()` for callbacks + +```ts +private injector = inject(Injector) + +someCallback() { + this.injector.runInInjectionContext(() => { + const service = inject(MyClient) + }) +} +``` + +**Solution 3:** Pass dependencies as parameters instead of injecting them + +```ts +// Instead of injecting inside a callback +setTimeout(() => { + const service = inject(MyClient) // ERROR +}, 1000) + +// Capture first, then use +private service = inject(MyClient) + +setTimeout(() => { + this.service.doSomething() // Use captured reference +}, 1000) +``` + +### NG0200: Circular dependency detected + +**Error code:** NG0200 + +This error occurs when two or more services depend on each other, creating a circular dependency that Angular cannot resolve. + +``` +NG0200: Circular dependency in DI detected for AuthClient + Dependency path: AuthClient -> UserClient -> AuthClient +``` + +The dependency path shows the cycle: `AuthClient` depends on `UserClient`, which depends back on `AuthClient`. + +#### Understanding the error + +Angular creates service instances by calling their constructors and injecting dependencies. When services depend on each other circularly, Angular cannot determine which to create first. + +#### Common causes + +- Direct circular dependency (Service A → Service B → Service A) +- Indirect circular dependency (Service A → Service B → Service C → Service A) +- Import cycles in module files that also have service dependencies + +#### Resolution strategies + +See the "Circular dependencies" section for detailed examples and solutions: + +1. **Restructure** - Extract shared logic to a third service (recommended) +2. **Use events** - Replace direct dependencies with event-based communication +3. **Lazy injection** - Use `Injector.get()` to defer one dependency (last resort) + +Do NOT use `forwardRef()` for service circular dependencies. It only solves circular imports in component configurations. + +### Other DI error codes + +For detailed explanations and solutions for these errors, see the [Angular error reference](errors): + +| Error Code | Description | +| ----------------------- | ------------------------------------------------------------------------------------------ | +| [NG0204](errors/NG0204) | Can't resolve all parameters - missing `@Injectable()` decorator | +| [NG0205](errors/NG0205) | Injector already destroyed - accessing services after component destruction | +| [NG0207](errors/NG0207) | EnvironmentProviders in wrong context - using `provideHttpClient()` in component providers | + +## Next steps + +When you encounter DI errors, remember to: + +1. Read the error message and dependency path carefully +2. Verify basic configuration (decorators, `providedIn`, imports) +3. Check injection context and timing +4. Use DevTools and logging to investigate +5. Simplify and isolate the problem + +For a deeper understanding of specific topics on dependency injection, check out: + +- [Understanding dependency injection](guide/di) - Core DI concepts and patterns +- [Hierarchical dependency injection](guide/di/hierarchical-dependency-injection) - How the injector hierarchy works +- [Testing with dependency injection](guide/testing) - Using TestBed and mocking dependencies diff --git a/adev-es/src/content/guide/di/defining-dependency-providers.en.md b/adev-es/src/content/guide/di/defining-dependency-providers.en.md index 3702715..580ba7b 100644 --- a/adev-es/src/content/guide/di/defining-dependency-providers.en.md +++ b/adev-es/src/content/guide/di/defining-dependency-providers.en.md @@ -2,7 +2,7 @@ Angular provides two ways to make services available for injection: -1. **Automatic provision** - Using `providedIn` in the `@Injectable` decorator or by providing a factory in the `InjectionToken` configuration +1. **Automatic provision** - Using `providedIn` in the `@Injectable` decorator, the [`@Service`](guide/di/creating-and-using-services#using-the-service-vs-injectable-decorator) decorator, or by providing a factory in the `InjectionToken` configuration 2. **Manual provision** - Using the `providers` array in components, directives, routes, or application config In the [previous guide](/guide/di/creating-and-using-services), you learned how to create services using `providedIn: 'root'`, which handles most common use cases. This guide explores additional patterns for both automatic and manual provider configuration. @@ -16,7 +16,7 @@ While the `@Injectable` decorator with `providedIn: 'root'` works great for serv An `InjectionToken` is an object that Angular's dependency injection system uses to uniquely identify values for injection. Think of it as a special key that lets you store and retrieve any type of value in Angular's DI system: ```ts -import { InjectionToken } from '@angular/core'; +import {InjectionToken} from '@angular/core'; // Create a token for a string value export const API_URL = new InjectionToken('api.url'); @@ -36,11 +36,11 @@ NOTE: The string parameter (e.g., `'api.url'`) is a description purely for debug ### InjectionToken with `providedIn: 'root'` -An `InjectionToken` that has a `factory` results in `providedIn: 'root'` by default (but can be overidden via the `providedIn` prop). +An `InjectionToken` that has a `factory` results in `providedIn: 'root'` by default (but can be overridden via the `providedIn` prop). ```ts // 📁 /app/config.token.ts -import { InjectionToken } from '@angular/core'; +import {InjectionToken} from '@angular/core'; export interface AppConfig { apiUrl: string; @@ -56,17 +56,17 @@ export const APP_CONFIG = new InjectionToken('app.config', { version: '1.0.0', features: { darkMode: true, - analytics: false - } - }) + analytics: false, + }, + }), }); // No need to add to providers array - available everywhere! @Component({ selector: 'app-header', - template: `

    Version: {{ config.version }}

    ` + template: `

    Version: {{ config.version }}

    `, }) -export class HeaderComponent { +export class Header { config = inject(APP_CONFIG); // Automatically available } ``` @@ -77,8 +77,8 @@ InjectionToken with factory functions is ideal when you can't use a class but ne ```ts // 📁 /app/logger.token.ts -import { InjectionToken, inject } from '@angular/core'; -import { APP_CONFIG } from './config.token'; +import {InjectionToken, inject} from '@angular/core'; +import {APP_CONFIG} from './config.token'; // Logger function type export type LoggerFn = (level: string, message: string) => void; @@ -94,19 +94,19 @@ export const LOGGER_FN = new InjectionToken('logger.function', { console[level](`[${new Date().toISOString()}] ${message}`); } }; - } + }, }); // 📁 /app/storage.token.ts // Providing browser APIs as tokens export const LOCAL_STORAGE = new InjectionToken('localStorage', { // providedIn: 'root' is configured as the default - factory: () => window.localStorage + factory: () => window.localStorage, }); export const SESSION_STORAGE = new InjectionToken('sessionStorage', { providedIn: 'root', - factory: () => window.sessionStorage + factory: () => window.sessionStorage, }); // 📁 /app/feature-flags.token.ts @@ -125,7 +125,7 @@ export const FEATURE_FLAGS = new InjectionToken>('feature.f flags.set('newDashboard', false); return flags; - } + }, }); ``` @@ -148,7 +148,7 @@ When you need more control than `providedIn: 'root'` offers, you can manually co ### Example: Service without `providedIn` ```ts -import { Injectable, Component, inject } from '@angular/core'; +import {Injectable, Component, inject} from '@angular/core'; // Service without providedIn @Injectable() @@ -165,9 +165,9 @@ export class LocalDataStore { selector: 'app-example', // A provider is required here because the `LocalDataStore` service has no providedIn. providers: [LocalDataStore], - template: `...` + template: `...`, }) -export class ExampleComponent { +export class Example { dataStore = inject(LocalDataStore); } ``` @@ -177,9 +177,9 @@ export class ExampleComponent { Services with `providedIn: 'root'` can be overridden at the component level. This ties the instance of the service to the life of a component. As a result, when the component gets destroyed, the provided service is also destroyed as well. ```ts -import { Injectable, Component, inject } from '@angular/core'; +import {Injectable, Component, inject} from '@angular/core'; -@Injectable({ providedIn: 'root' }) +@Injectable({providedIn: 'root'}) export class DataStore { private data: ListItem[] = []; } @@ -189,9 +189,9 @@ export class DataStore { selector: 'app-isolated', // Creates new instance of `DataStore` rather than using the root-provided instance. providers: [DataStore], - template: `...` + template: `...`, }) -export class IsolatedComponent { +export class Isolated { dataStore = inject(DataStore); // Component-specific instance } ``` @@ -235,14 +235,14 @@ Think of Angular's dependency injection system as a hash map or dictionary. Each When manually providing dependencies, you typically see this shorthand syntax: ```angular-ts -import { Component } from '@angular/core'; -import { LocalService } from './local-service'; +import {Component} from '@angular/core'; +import {LocalService} from './local-service'; @Component({ selector: 'app-example', - providers: [LocalService] // Service without providedIn + providers: [LocalService], // Service without providedIn }) -export class ExampleComponent { } +export class Example {} ``` This is actually a shorthand for a more detailed provider configuration: @@ -279,19 +279,19 @@ Provider identifiers allow Angular's dependency injection (DI) system to retriev #### Class names -Class name use the imported class directly as the identifier: +Class names use the imported class directly as the identifier: ```angular-ts -import { Component } from '@angular/core'; -import { LocalService } from './local-service'; +import {Component} from '@angular/core'; +import {LocalService} from './local-service'; @Component({ selector: 'app-example', - providers: [ - { provide: LocalService, useClass: LocalService } - ] + providers: [{provide: LocalService, useClass: LocalService}], }) -export class ExampleComponent { /* ... */ } +export class Example { + /* ... */ +} ``` The class serves as both the identifier and the implementation, which is why Angular provides the shorthand `providers: [LocalService]`. @@ -302,8 +302,8 @@ Angular provides a built-in [`InjectionToken`](api/core/InjectionToken) class th ```ts // 📁 /app/tokens.ts -import { InjectionToken } from '@angular/core'; -import { DataService } from './data-service.interface'; +import {InjectionToken} from '@angular/core'; +import {DataService} from './data-service.interface'; export const DATA_SERVICE_TOKEN = new InjectionToken('DataService'); ``` @@ -313,17 +313,15 @@ NOTE: The string `'DataService'` is a description used purely for debugging purp Use the token in your provider configuration: ```angular-ts -import { Component, inject } from '@angular/core'; -import { LocalDataService } from './local-data-service'; -import { DATA_SERVICE_TOKEN } from './tokens'; +import {Component, inject} from '@angular/core'; +import {LocalDataService} from './local-data-service'; +import {DATA_SERVICE_TOKEN} from './tokens'; @Component({ selector: 'app-example', - providers: [ - { provide: DATA_SERVICE_TOKEN, useClass: LocalDataService } - ] + providers: [{provide: DATA_SERVICE_TOKEN, useClass: LocalDataService}], }) -export class ExampleComponent { +export class Example { private dataService = inject(DATA_SERVICE_TOKEN); } ``` @@ -341,10 +339,10 @@ interface DataService { // Interfaces disappear after TypeScript compilation @Component({ providers: [ - { provide: DataService, useClass: LocalDataService } // Error! - ] + {provide: DataService, useClass: LocalDataService}, // Error! + ], }) -export class ExampleComponent { +export class Example { private dataService = inject(DataService); // Error! } @@ -352,11 +350,9 @@ export class ExampleComponent { export const DATA_SERVICE_TOKEN = new InjectionToken('DataService'); @Component({ - providers: [ - { provide: DATA_SERVICE_TOKEN, useClass: LocalDataService } - ] + providers: [{provide: DATA_SERVICE_TOKEN, useClass: LocalDataService}], }) -export class ExampleComponent { +export class Example { private dataService = inject(DATA_SERVICE_TOKEN); // Works! } ``` @@ -371,25 +367,21 @@ The InjectionToken provides a runtime value that Angular's DI system can use, wh ```ts // Shorthand -providers: [DataService] +providers: [DataService]; // Full syntax -providers: [ - { provide: DataService, useClass: DataService } -] +providers: [{provide: DataService, useClass: DataService}]; // Different implementation -providers: [ - { provide: DataService, useClass: MockDataService } -] +providers: [{provide: DataService, useClass: MockDataService}]; // Conditional implementation providers: [ { provide: StorageService, - useClass: environment.production ? CloudStorageService : LocalStorageService - } -] + useClass: environment.production ? CloudStorageService : LocalStorageService, + }, +]; ``` #### Practical example: Logger substitution @@ -397,7 +389,7 @@ providers: [ You can substitute implementations to extend functionality: ```ts -import { Injectable, Component, inject } from '@angular/core'; +import {Injectable, Component, inject} from '@angular/core'; // Base logger @Injectable() @@ -431,10 +423,10 @@ export class EvenBetterLogger extends Logger { selector: 'app-example', providers: [ UserService, // EvenBetterLogger needs this - { provide: Logger, useClass: EvenBetterLogger } - ] + {provide: Logger, useClass: EvenBetterLogger}, + ], }) -export class ExampleComponent { +export class Example { private logger = inject(Logger); // Gets EvenBetterLogger instance } ``` @@ -445,10 +437,10 @@ export class ExampleComponent { ```ts providers: [ - { provide: API_URL_TOKEN, useValue: 'https://api.example.com' }, - { provide: MAX_RETRIES_TOKEN, useValue: 3 }, - { provide: FEATURE_FLAGS_TOKEN, useValue: { darkMode: true, beta: false } } -] + {provide: API_URL_TOKEN, useValue: 'https://api.example.com'}, + {provide: MAX_RETRIES_TOKEN, useValue: 3}, + {provide: FEATURE_FLAGS_TOKEN, useValue: {darkMode: true, beta: false}}, +]; ``` IMPORTANT: TypeScript types and interfaces cannot serve as dependency values. They exist only at compile-time. @@ -477,23 +469,21 @@ const appConfig: AppConfig = { appTitle: 'My Application', features: { darkMode: true, - analytics: false - } + analytics: false, + }, }; // Provide in bootstrap bootstrapApplication(AppComponent, { - providers: [ - { provide: APP_CONFIG, useValue: appConfig } - ] + providers: [{provide: APP_CONFIG, useValue: appConfig}], }); // Use in component @Component({ selector: 'app-header', - template: `

    {{ title }}

    ` + template: `

    {{ title }}

    `, }) -export class HeaderComponent { +export class Header { private config = inject(APP_CONFIG); title = this.config.appTitle; } @@ -512,15 +502,15 @@ providers: [ { provide: LoggerService, useFactory: loggerFactory, - deps: [APP_CONFIG] // Dependencies for the factory function - } -] + deps: [APP_CONFIG], // Dependencies for the factory function + }, +]; ``` You can mark factory dependencies as optional: ```ts -import { Optional } from '@angular/core'; +import {Optional} from '@angular/core'; providers: [ { @@ -528,9 +518,9 @@ providers: [ useFactory: (required: RequiredService, optional?: OptionalService) => { return new MyService(required, optional || new DefaultService()); }, - deps: [RequiredService, [new Optional(), OptionalService]] - } -] + deps: [RequiredService, [new Optional(), OptionalService]], + }, +]; ``` #### Practical example: Configuration-based API client @@ -543,7 +533,7 @@ class ApiClient { constructor( private http: HttpClient, private baseUrl: string, - private rateLimitMs: number + private rateLimitMs: number, ) {} async fetchData(endpoint: string) { @@ -554,13 +544,13 @@ class ApiClient { private async applyRateLimit() { // Simplified example - real implementation would track request timing - return new Promise(resolve => setTimeout(resolve, this.rateLimitMs)); + return new Promise((resolve) => setTimeout(resolve, this.rateLimitMs)); } } // Factory function that configures based on user tier -import { inject } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; +import {inject} from '@angular/core'; +import {HttpClient} from '@angular/common/http'; const apiClientFactory = () => { const http = inject(HttpClient); const userService = inject(UserService); @@ -575,15 +565,15 @@ const apiClientFactory = () => { // Provider configuration export const apiClientProvider = { provide: ApiClient, - useFactory: apiClientFactory + useFactory: apiClientFactory, }; // Usage in component @Component({ selector: 'app-dashboard', - providers: [apiClientProvider] + providers: [apiClientProvider], }) -export class DashboardComponent { +export class Dashboard { private apiClient = inject(ApiClient); } ``` @@ -594,9 +584,9 @@ export class DashboardComponent { ```ts providers: [ - NewLogger, // The actual service - { provide: OldLogger, useExisting: NewLogger } // The alias -] + NewLogger, // The actual service + {provide: OldLogger, useExisting: NewLogger}, // The alias +]; ``` IMPORTANT: Don't confuse `useExisting` with `useClass`. `useClass` creates separate instances, while `useExisting` ensures you get the same singleton instance. @@ -609,10 +599,10 @@ Use the `multi: true` flag when multiple providers contribute values to the same export const INTERCEPTOR_TOKEN = new InjectionToken('interceptors'); providers: [ - { provide: INTERCEPTOR_TOKEN, useClass: AuthInterceptor, multi: true }, - { provide: INTERCEPTOR_TOKEN, useClass: LoggingInterceptor, multi: true }, - { provide: INTERCEPTOR_TOKEN, useClass: RetryInterceptor, multi: true } -] + {provide: INTERCEPTOR_TOKEN, useClass: AuthInterceptor, multi: true}, + {provide: INTERCEPTOR_TOKEN, useClass: LoggingInterceptor, multi: true}, + {provide: INTERCEPTOR_TOKEN, useClass: RetryInterceptor, multi: true}, +]; ``` When you inject `INTERCEPTOR_TOKEN`, you'll receive an array containing instances of all three interceptors. @@ -636,13 +626,13 @@ Use application-level providers in `bootstrapApplication` when: ```ts // main.ts -bootstrapApplication(AppComponent, { +bootstrapApplication(App, { providers: [ - { provide: API_BASE_URL, useValue: 'https://api.example.com' }, - { provide: INTERCEPTOR_TOKEN, useClass: AuthInterceptor, multi: true }, - LoggingService, // Used throughout the app - { provide: ErrorHandler, useClass: GlobalErrorHandler } - ] + {provide: API_BASE_URL, useValue: 'https://api.example.com'}, + {provide: INTERCEPTOR_TOKEN, useClass: AuthInterceptor, multi: true}, + LoggingService, // Used throughout the app + {provide: ErrorHandler, useClass: GlobalErrorHandler}, + ], }); ``` @@ -680,20 +670,20 @@ Use component or directive providers when: @Component({ selector: 'app-advanced-form', providers: [ - FormValidationService, // Each form gets its own validator - { provide: FORM_CONFIG, useValue: { strictMode: true } } - ] + FormValidationService, // Each form gets its own validator + {provide: FORM_CONFIG, useValue: {strictMode: true}}, + ], }) -export class AdvancedFormComponent { } +export class AdvancedForm {} // Modal component with isolated state management @Component({ selector: 'app-modal', providers: [ - ModalStateService // Each modal manages its own state - ] + ModalStateService, // Each modal manages its own state + ], }) -export class ModalComponent { } +export class Modal {} ``` **Benefits:** @@ -725,22 +715,26 @@ export const routes: Routes = [ { path: 'admin', providers: [ - AdminService, // Only loaded with admin routes - { provide: FEATURE_FLAGS, useValue: { adminMode: true } } + AdminService, // Only loaded with admin routes + {provide: FEATURE_FLAGS, useValue: {adminMode: true}}, ], - loadChildren: () => import('./admin/admin.routes') + loadChildren: () => import('./admin/admin.routes'), }, { path: 'shop', providers: [ - ShoppingCartService, // Isolated shopping state - PaymentService + ShoppingCartService, // Isolated shopping state + PaymentService, ], - loadChildren: () => import('./shop/shop.routes') - } + loadChildren: () => import('./shop/shop.routes'), + }, ]; ``` +Services provided at the route level are available to all components and directives within that route, as well as to its guards and resolvers. + +Since these services are instantiated independently of the route’s components, they do not have direct access to route-specific information. + ## Library author patterns When creating Angular libraries, you often need to provide flexible configuration options for consumers while maintaining clean APIs. Angular's own libraries demonstrate powerful patterns for achieving this. @@ -751,7 +745,7 @@ Instead of requiring users to manually configure complex providers, library auth ```ts // 📁 /libs/analytics/src/providers.ts -import { InjectionToken, Provider, inject } from '@angular/core'; +import {InjectionToken, Provider, inject} from '@angular/core'; // Configuration interface export interface AnalyticsConfig { @@ -774,21 +768,18 @@ export class AnalyticsService { // Provider function for consumers export function provideAnalytics(config: AnalyticsConfig): Provider[] { - return [ - { provide: ANALYTICS_CONFIG, useValue: config }, - AnalyticsService - ]; + return [{provide: ANALYTICS_CONFIG, useValue: config}, AnalyticsService]; } // Usage in consumer app // main.ts -bootstrapApplication(AppComponent, { +bootstrapApplication(App, { providers: [ provideAnalytics({ trackingId: 'GA-12345', - enableDebugMode: !environment.production - }) - ] + enableDebugMode: !environment.production, + }), + ], }); ``` @@ -798,13 +789,13 @@ For more complex scenarios, you can combine multiple configuration approaches: ```ts // 📁 /libs/http-client/src/provider.ts -import { Provider, InjectionToken, inject } from '@angular/core'; +import {Provider, InjectionToken, inject} from '@angular/core'; // Feature flags for optional functionality export enum HttpFeatures { Interceptors = 'interceptors', Caching = 'caching', - Retry = 'retry' + Retry = 'retry', } // Configuration interfaces @@ -826,7 +817,7 @@ const HTTP_FEATURES = new InjectionToken>('http.features'); // Core service class HttpClientService { - private config = inject(HTTP_CONFIG, { optional: true }); + private config = inject(HTTP_CONFIG, {optional: true}); private features = inject(HTTP_FEATURES); get(url: string) { @@ -845,18 +836,15 @@ class CacheInterceptor { } // Main provider function -export function provideHttpClient( - config?: HttpConfig, - ...features: HttpFeature[] -): Provider[] { +export function provideHttpClient(config?: HttpConfig, ...features: HttpFeature[]): Provider[] { const providers: Provider[] = [ - { provide: HTTP_CONFIG, useValue: config || {} }, - { provide: HTTP_FEATURES, useValue: new Set(features.map(f => f.kind)) }, - HttpClientService + {provide: HTTP_CONFIG, useValue: config || {}}, + {provide: HTTP_FEATURES, useValue: new Set(features.map((f) => f.kind))}, + HttpClientService, ]; // Add feature-specific providers - features.forEach(feature => { + features.forEach((feature) => { providers.push(...feature.providers); }); @@ -872,41 +860,38 @@ export interface HttpFeature { export function withInterceptors(...interceptors: any[]): HttpFeature { return { kind: HttpFeatures.Interceptors, - providers: interceptors.map(interceptor => ({ + providers: interceptors.map((interceptor) => ({ provide: INTERCEPTOR_TOKEN, useClass: interceptor, - multi: true - })) + multi: true, + })), }; } export function withCaching(): HttpFeature { return { kind: HttpFeatures.Caching, - providers: [CacheInterceptor] + providers: [CacheInterceptor], }; } export function withRetry(config: RetryConfig): HttpFeature { return { kind: HttpFeatures.Retry, - providers: [ - { provide: RETRY_CONFIG, useValue: config }, - RetryInterceptor - ] + providers: [{provide: RETRY_CONFIG, useValue: config}, RetryInterceptor], }; } // Consumer usage with multiple features -bootstrapApplication(AppComponent, { +bootstrapApplication(App, { providers: [ provideHttpClient( - { baseUrl: 'https://api.example.com' }, + {baseUrl: 'https://api.example.com'}, withInterceptors(AuthInterceptor, LoggingInterceptor), withCaching(), - withRetry({ maxAttempts: 3, delayMs: 1000 }) - ) - ] + withRetry({maxAttempts: 3, delayMs: 1000}), + ), + ], }); ``` diff --git a/adev-es/src/content/guide/di/defining-dependency-providers.md b/adev-es/src/content/guide/di/defining-dependency-providers.md index 5663d88..028e00c 100644 --- a/adev-es/src/content/guide/di/defining-dependency-providers.md +++ b/adev-es/src/content/guide/di/defining-dependency-providers.md @@ -2,7 +2,7 @@ Angular proporciona dos formas de hacer que los servicios estén disponibles para inyección: -1. **Provisión automática** - Usando `providedIn` en el decorador `@Injectable` o proporcionando un factory en la configuración de `InjectionToken` +1. **Provisión automática** - Usando `providedIn` en el decorador `@Injectable`, el decorador [`@Service`](guide/di/creating-and-using-services#using-the-service-vs-injectable-decorator), o proporcionando un factory en la configuración de `InjectionToken` 2. **Provisión manual** - Usando el array `providers` en componentes, directivas, rutas o configuración de la aplicación En la [guía anterior](/guide/di/creating-and-using-services), aprendiste cómo crear servicios usando `providedIn: 'root'`, lo cual maneja la mayoría de los casos de uso comunes. Esta guía explora patrones adicionales para la configuración de proveedores tanto automática como manual. @@ -66,7 +66,7 @@ export const APP_CONFIG = new InjectionToken('app.config', { selector: 'app-header', template: `

    Version: {{ config.version }}

    ` }) -export class HeaderComponent { +export class Header { config = inject(APP_CONFIG); // Disponible automáticamente } ``` @@ -167,7 +167,7 @@ export class LocalDataStore { providers: [LocalDataStore], template: `...` }) -export class ExampleComponent { +export class Example { dataStore = inject(LocalDataStore); } ``` @@ -191,7 +191,7 @@ export class DataStore { providers: [DataStore], template: `...` }) -export class IsolatedComponent { +export class Isolated { dataStore = inject(DataStore); // Instancia específica del componente } ``` diff --git a/adev-es/src/content/guide/di/dependency-injection-context.en.md b/adev-es/src/content/guide/di/dependency-injection-context.en.md index e2d4c01..788bbcf 100644 --- a/adev-es/src/content/guide/di/dependency-injection-context.en.md +++ b/adev-es/src/content/guide/di/dependency-injection-context.en.md @@ -1,42 +1,44 @@ # Injection context -The dependency injection (DI) system relies internally on a runtime context where the current injector is available. +The dependency injection (DI) system relies on a runtime context where the current injector is available. -This means that injectors can only work when code is executed in such a context. +This means that injectors only work when you execute code within this context. -The injection context is available in these situations: +You have an injection context available in the following situations: -- During construction (via the `constructor`) of a class being instantiated by the DI system, such as an `@Injectable` or `@Component`. -- In the initializer for fields of such classes. +- During construction (via the `constructor`) of a class instantiated by the DI system, such as an `@Injectable` or `@Component`. +- In field initializers of such classes. - In the factory function specified for `useFactory` of a `Provider` or an `@Injectable`. - In the `factory` function specified for an `InjectionToken`. - Within a stack frame that runs in an injection context. -Knowing when you are in an injection context will allow you to use the [`inject`](api/core/inject) function to inject instances. +Knowing when you are in an injection context allows you to use the [`inject`](api/core/inject) function to retrieve dependencies. NOTE: For basic examples of using `inject()` in class constructors and field initializers, see the [overview guide](/guide/di#where-can-inject-be-used). ## Stack frame in context -Some APIs are designed to be run in an injection context. This is the case, for example, with router guards. This allows the use of [`inject`](api/core/inject) within the guard function to access a service. +Some APIs are designed to run within an injection context. This is the case, for example, with router guards. This allows you to use [`inject`](api/core/inject) within the guard function to access services. Here is an example for `CanActivateFn` ```ts {highlight: [3]} -const canActivateTeam: CanActivateFn = - (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => { - return inject(PermissionsService).canActivate(inject(UserToken), route.params.id); - }; +const canActivateTeam: CanActivateFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, +) => { + return inject(PermissionsService).canActivate(inject(UserToken), route.params.id); +}; ``` ## Run within an injection context -When you want to run a given function in an injection context without already being in one, you can do so with `runInInjectionContext`. -This requires access to a given injector, like the `EnvironmentInjector`, for example: +If you need to run a function within an injection context without already being in one, you can use `runInInjectionContext`. +This requires access to an injector, such as the `EnvironmentInjector`: -```ts {highlight: [9], header"hero.service.ts"} +```ts {highlight: [9], header:"hero.service.ts"} @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class HeroService { private environmentInjector = inject(EnvironmentInjector); @@ -49,28 +51,30 @@ export class HeroService { } ``` -Note that `inject` will return an instance only if the injector can resolve the required token. +Note that [`inject`](/api/core/inject) returns an instance only if the injector can resolve the requested token. ## Asserts the context -Angular provides the `assertInInjectionContext` helper function to assert that the current context is an injection context and throws a clear error if not. Pass a reference to the calling function so the error message points to the correct API entry point. This produces a clearer, more actionable message than the default generic injection error. +Angular provides the `assertInInjectionContext` helper function to verify that the current context is an injection context and throw a clear error if it is not. Pass a reference to the calling function so the error message points to the correct API entry point. This produces a clearer, more actionable message than the default generic injection error. ```ts -import { ElementRef, assertInInjectionContext, inject } from '@angular/core'; +import {ElementRef, assertInInjectionContext, inject} from '@angular/core'; export function injectNativeElement(): T { - assertInInjectionContext(injectNativeElement); - return inject(ElementRef).nativeElement; + assertInInjectionContext(injectNativeElement); + return inject(ElementRef).nativeElement; } ``` You can then call this helper **from an injection context** (constructor, field initializer, provider factory, or code executed via `runInInjectionContext`): ```ts -import { Component, inject } from '@angular/core'; -import { injectNativeElement } from './dom-helpers'; +import {Component, inject} from '@angular/core'; +import {injectNativeElement} from './dom-helpers'; -@Component({ /* … */ }) +@Component({ + /* … */ +}) export class PreviewCard { readonly hostEl = injectNativeElement(); // Field initializer runs in an injection context. @@ -82,4 +86,4 @@ export class PreviewCard { ## Using DI outside of a context -Calling [`inject`](api/core/inject) or calling `assertInInjectionContext` outside of an injection context will throw [error NG0203](/errors/NG0203). +If you call [`inject`](api/core/inject) or `assertInInjectionContext` outside of an injection context, Angular throws [error NG0203](/errors/NG0203). diff --git a/adev-es/src/content/guide/di/dependency-injection-context.md b/adev-es/src/content/guide/di/dependency-injection-context.md index f1aa7fe..2da8848 100644 --- a/adev-es/src/content/guide/di/dependency-injection-context.md +++ b/adev-es/src/content/guide/di/dependency-injection-context.md @@ -34,9 +34,9 @@ const canActivateTeam: CanActivateFn = Cuando quieres ejecutar una función dada en un contexto de inyección sin estar ya en uno, puedes hacerlo con `runInInjectionContext`. Esto requiere acceso a un inyector dado, como el `EnvironmentInjector`, por ejemplo: -```ts {highlight: [9], header: "hero.service.ts"} +```ts {highlight: [9], header:"hero.service.ts"} @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class HeroService { private environmentInjector = inject(EnvironmentInjector); @@ -56,21 +56,23 @@ Ten en cuenta que `inject` devolverá una instancia solo si el inyector puede re Angular proporciona la función auxiliar `assertInInjectionContext` para afirmar que el contexto actual es un contexto de inyección y lanza un error claro si no lo es. Pasa una referencia a la función que llama para que el mensaje de error apunte al punto de entrada de la API correcto. Esto produce un mensaje más claro y accionable que el error genérico de inyección predeterminado. ```ts -import { ElementRef, assertInInjectionContext, inject } from '@angular/core'; +import {ElementRef, assertInInjectionContext, inject} from '@angular/core'; export function injectNativeElement(): T { - assertInInjectionContext(injectNativeElement); - return inject(ElementRef).nativeElement; + assertInInjectionContext(injectNativeElement); + return inject(ElementRef).nativeElement; } ``` Puedes llamar a esta función auxiliar **desde un contexto de inyección** (constructor, inicializador de campo, fábrica de proveedor, o código ejecutado vía `runInInjectionContext`): ```ts -import { Component, inject } from '@angular/core'; -import { injectNativeElement } from './dom-helpers'; +import {Component, inject} from '@angular/core'; +import {injectNativeElement} from './dom-helpers'; -@Component({ /* … */ }) +@Component({ + /* … */ +}) export class PreviewCard { readonly hostEl = injectNativeElement(); // El inicializador de campo se ejecuta en un contexto de inyección. diff --git a/adev-es/src/content/guide/di/di-in-action.en.md b/adev-es/src/content/guide/di/di-in-action.en.md index 843fc1b..de0b7f5 100644 --- a/adev-es/src/content/guide/di/di-in-action.en.md +++ b/adev-es/src/content/guide/di/di-in-action.en.md @@ -1,15 +1,15 @@ # DI in action -This guide explores additional features of dependency injection in Angular. +This guide explores additional features of dependency injection (DI) in Angular. NOTE: For comprehensive coverage of InjectionToken and custom providers, see the [defining dependency providers guide](guide/di/defining-dependency-providers#injection-tokens). ## Inject the component's DOM element -Although developers strive to avoid it, some visual effects and third-party tools require direct DOM access. -As a result, you might need to access a component's DOM element. +Although developers generally avoid it, some visual effects and third-party tools require you to access the DOM directly. +In such cases, you may need to access a component's DOM element. -Angular exposes the underlying element of a `@Component` or `@Directive` via injection using the `ElementRef` injection token: +Angular exposes the underlying DOM element of a `@Component` or `@Directive` through injection using the `ElementRef` token: ```ts {highlight:[7]} import {Directive, ElementRef, inject} from '@angular/core'; @@ -24,24 +24,54 @@ export class HighlightDirective { this.element.nativeElement.style.color = 'red'; } } +``` + +## Inject the host element's tag name + +To get the tag name of a host element, inject it using the `HOST_TAG_NAME` token. + +```ts +import {Directive, HOST_TAG_NAME, inject} from '@angular/core'; + +@Directive({ + selector: '[roleButton]', +}) +export class RoleButtonDirective { + private tagName = inject(HOST_TAG_NAME); + onAction() { + switch (this.tagName) { + case 'button': + // Handle button action + break; + case 'a': + // Handle anchor action + break; + default: + // Handle other elements + break; + } + } +} ``` +NOTE: If the host element might not have a tag name (e.g., `ng-container` or `ng-template`), make the injection optional. + ## Resolve circular dependencies with a forward reference -The order of class declaration matters in TypeScript. -You can't refer directly to a class until it's been defined. +In TypeScript, the order of class declarations matters. +You cannot reference a class directly until you define it. This isn't usually a problem, especially if you adhere to the recommended _one class per file_ rule. -But sometimes circular references are unavoidable. -For example, when class 'A' refers to class 'B' and 'B' refers to 'A', one of them has to be defined first. +However, in some cases, circular references are unavoidable. +For example, if class 'A' refers to class 'B' and class 'B' refers to class 'A', one of them must be defined first. The Angular `forwardRef()` function creates an _indirect_ reference that Angular can resolve later. You face a similar problem when a class makes _a reference to itself_. For example, in its `providers` array. The `providers` array is a property of the `@Component()` decorator function, which must appear before the class definition. -You can break such circular references by using `forwardRef`. +Such circular references can be resolved using `forwardRef`. ```typescript {header: 'app.component.ts', highlight: [4]} providers: [ diff --git a/adev-es/src/content/guide/di/di-in-action.md b/adev-es/src/content/guide/di/di-in-action.md index 702c08c..cc06179 100644 --- a/adev-es/src/content/guide/di/di-in-action.md +++ b/adev-es/src/content/guide/di/di-in-action.md @@ -9,7 +9,7 @@ NOTA: Para una cobertura completa de InjectionToken y proveedores personalizados Aunque los desarrolladores se esfuerzan por evitarlo, algunos efectos visuales y herramientas de terceros requieren acceso directo al DOM. Como resultado, es posible que necesites acceder al elemento DOM de un componente. -Angular expone el elemento subyacente de un `@Component` o `@Directive` vía inyección usando el token de inyección `ElementRef`: +Angular expone el elemento DOM subyacente de un `@Component` o `@Directive` a través de inyección usando el token `ElementRef`: ```ts {highlight:[7]} import {Directive, ElementRef, inject} from '@angular/core'; @@ -24,9 +24,39 @@ export class HighlightDirective { this.element.nativeElement.style.color = 'red'; } } +``` + +## Inyectar el nombre de etiqueta del elemento host + +Para obtener el nombre de etiqueta de un elemento host, inyéctalo usando el token `HOST_TAG_NAME`. + +```ts +import {Directive, HOST_TAG_NAME, inject} from '@angular/core'; + +@Directive({ + selector: '[roleButton]', +}) +export class RoleButtonDirective { + private tagName = inject(HOST_TAG_NAME); + onAction() { + switch (this.tagName) { + case 'button': + // Manejar acción de botón + break; + case 'a': + // Manejar acción de ancla + break; + default: + // Manejar otros elementos + break; + } + } +} ``` +NOTA: Si el elemento host podría no tener un nombre de etiqueta (por ejemplo, `ng-container` o `ng-template`), haz la inyección opcional. + ## Resolver dependencias circulares con una referencia anticipada El orden de declaración de clases en TypeScript es importante. diff --git a/adev-es/src/content/guide/di/hierarchical-dependency-injection.en.md b/adev-es/src/content/guide/di/hierarchical-dependency-injection.en.md index 77dbde0..49c1689 100644 --- a/adev-es/src/content/guide/di/hierarchical-dependency-injection.en.md +++ b/adev-es/src/content/guide/di/hierarchical-dependency-injection.en.md @@ -10,7 +10,7 @@ Angular has two injector hierarchies: | Injector hierarchies | Details | | :------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `EnvironmentInjector` hierarchy | Configure an `EnvironmentInjector` in this hierarchy using `@Injectable()` or `providers` array in `ApplicationConfig`. | +| `EnvironmentInjector` hierarchy | Configure an `EnvironmentInjector` in this hierarchy using `@Service()` or `providers` array in `ApplicationConfig`. | | `ElementInjector` hierarchy | Created implicitly at each DOM element. An `ElementInjector` is empty by default unless you configure it in the `providers` property on `@Directive()` or `@Component()`. | @@ -21,12 +21,12 @@ For `NgModule` based applications, you can provide dependencies with the `Module The `EnvironmentInjector` can be configured in one of two ways by using: -- The `@Injectable()` `providedIn` property to refer to `root` or `platform` +- The `@Service()` - The `ApplicationConfig` `providers` array - + -Using the `@Injectable()` `providedIn` property is preferable to using the `ApplicationConfig` `providers` array. With `@Injectable()` `providedIn`, optimization tools can perform tree-shaking, which removes services that your application isn't using. This results in smaller bundle sizes. +Using the `@Service()` decorator is preferable to using the `ApplicationConfig` `providers` array. With `@Service`, optimization tools can perform tree-shaking, which removes services that your application isn't using. This results in smaller bundle sizes. Tree-shaking is especially useful for a library because the application which uses the library may not have a need to inject it. @@ -34,26 +34,24 @@ Tree-shaking is especially useful for a library because the application which us `EnvironmentInjector` is configured by the `ApplicationConfig.providers`. -Provide services using `providedIn` of `@Injectable()` as follows: +Provide services using `@Service()` as follows: ```ts {highlight:[4]} -import { Injectable } from '@angular/core'; +import {Service} from '@angular/core'; -@Injectable({ - providedIn: 'root' // <--provides this service in the root EnvironmentInjector -}) +@Service() // <--provides this service in the root EnvironmentInjector export class ItemService { name = 'telephone'; } ``` -The `@Injectable()` decorator identifies a service class. -The `providedIn` property configures a specific `EnvironmentInjector`, here `root`, which makes the service available in the `root` `EnvironmentInjector`. +The `@Service()` or `@Injectable()` decorators identify a service class. ### ModuleInjector In the case of `NgModule` based applications, the ModuleInjector can be configured in one of two ways by using: +- The `@Service()` decorator, - The `@Injectable()` `providedIn` property to refer to `root` or `platform` - The `@NgModule()` `providers` array @@ -68,7 +66,7 @@ There are two more injectors above `root`, an additional `EnvironmentInjector` a Consider how Angular bootstraps the application with the following in `main.ts`: ```ts -bootstrapApplication(AppComponent, appConfig); +bootstrapApplication(App, appConfig); ``` The `bootstrapApplication()` method creates a child injector of the platform injector which is configured by the `ApplicationConfig` instance. @@ -106,12 +104,10 @@ All requests forward up to the root injector, whether you configured it with the If you configure an app-wide provider in the `ApplicationConfig` of `bootstrapApplication`, it overrides one configured for `root` in the `@Injectable()` metadata. You can do this to configure a non-default provider of a service that is shared with multiple applications. -Here is an example of the case where the component router configuration includes a non-default [location strategy](guide/routing#location-strategy) by listing its provider in the `providers` list of the `ApplicationConfig`. +Here is an example of the case where the component router configuration includes a non-default [location strategy](guide/routing/common-router-tasks#locationstrategy-and-browser-url-styles) by listing its provider in the `providers` list of the `ApplicationConfig`. ```ts -providers: [ - { provide: LocationStrategy, useClass: HashLocationStrategy } -] +providers: [{provide: LocationStrategy, useClass: HashLocationStrategy}]; ``` For `NgModule` based applications, configure app-wide providers in the `AppModule` `providers`. @@ -171,7 +167,7 @@ HELPFUL: For `NgModule` based applications, Angular will search the `ModuleInjec ## Resolution modifiers Angular's resolution behavior can be modified with `optional`, `self`, `skipSelf` and `host`. -Import each of them from `@angular/core` and use each in the `inject` configuration when you inject your service. +Import each of them from `@angular/core` and use each in the [`inject`](/api/core/inject) configuration when you inject your service. ### Types of modifiers @@ -195,8 +191,8 @@ Additionally, you can combine all of the modifiers except: This way, if it can't be resolved at runtime, Angular resolves the service as `null`, rather than throwing an error. In the following example, the service, `OptionalService`, isn't provided in the service, `ApplicationConfig`, `@NgModule()`, or component class, so it isn't available anywhere in the app. -```ts {header:"src/app/optional/optional.component.ts"} -export class OptionalComponent { +```ts {header:"src/app/optional/optional.ts"} +export class Optional { public optional? = inject(OptionalService, {optional: true}); } ``` @@ -208,15 +204,15 @@ Use `self` so that Angular will only look at the `ElementInjector` for the curre A good use case for `self` is to inject a service but only if it is available on the current host element. To avoid errors in this situation, combine `self` with `optional`. -For example, in the following `SelfNoDataComponent`, notice the injected `LeafService` as a property. +For example, in the following `SelfNoData`, notice the injected `LeafService` as a property. -```ts {header: 'self-no-data.component.ts', highlight: [7]} +```ts {header: 'self-no-data.ts', highlight: [7]} @Component({ selector: 'app-self-no-data', - templateUrl: './self-no-data.component.html', - styleUrls: ['./self-no-data.component.css'] + templateUrl: './self-no-data.html', + styleUrls: ['./self-no-data.css'], }) -export class SelfNoDataComponent { +export class SelfNoData { public leaf = inject(LeafService, {optional: true, self: true}); } ``` @@ -226,15 +222,15 @@ In this example, there is a parent provider and injecting the service will retur Another example shows the component class with a provider for `FlowerService`. In this case, the injector looks no further than the current `ElementInjector` because it finds the `FlowerService` and returns the tulip 🌷. -```ts {header:"src/app/self/self.component.ts"} +```ts {header:"src/app/self/self.ts"} @Component({ selector: 'app-self', - templateUrl: './self.component.html', - styleUrls: ['./self.component.css'], + templateUrl: './self.html', + styleUrls: ['./self.css'], providers: [{provide: FlowerService, useValue: {emoji: '🌷'}}], }) -export class SelfComponent { - constructor(@Self() public flower: FlowerService) {} +export class Self { + public flower = inject(FlowerService, {self: true}); } ``` @@ -255,15 +251,15 @@ export class LeafService { Imagine that in the child component, you had a different value, maple leaf 🍁 but you wanted to use the parent's value instead. This is when you'd use `skipSelf`: -```ts {header:"skipself.component.ts" highlight:[[6],[10]]} +```ts {header:"skipself.ts" highlight:[[6],[10]]} @Component({ selector: 'app-skipself', - templateUrl: './skipself.component.html', - styleUrls: ['./skipself.component.css'], + templateUrl: './skipself.html', + styleUrls: ['./skipself.css'], // Angular would ignore this LeafService instance - providers: [{ provide: LeafService, useValue: { emoji: '🍁' } }] + providers: [{provide: LeafService, useValue: {emoji: '🍁'}}], }) -export class SkipselfComponent { +export class Skipself { // Use skipSelf as inject option public leaf = inject(LeafService, {skipSelf: true}); } @@ -280,7 +276,7 @@ In the following example, the `Person` service is injected during property initi ```ts class Person { - parent = inject(Person, {optional: true, skipSelf: true}) + parent = inject(Person, {optional: true, skipSelf: true}); } ``` @@ -293,21 +289,21 @@ class Person { Even if there is a service instance further up the tree, Angular won't continue looking. Use `host` as follows: -```ts {header:"host.component.ts" highlight:[[6],[9]]} +```ts {header:"host.ts" highlight:[[6],[9]]} @Component({ selector: 'app-host', - templateUrl: './host.component.html', - styleUrls: ['./host.component.css'], + templateUrl: './host.html', + styleUrls: ['./host.css'], // provide the service providers: [{provide: FlowerService, useValue: {emoji: '🌷'}}], }) -export class HostComponent { +export class Host { // use host when injecting the service flower = inject(FlowerService, {host: true, optional: true}); } ``` -Since `HostComponent` has the `host` option , no matter what the parent of `HostComponent` might have as a `flower.emoji` value, the `HostComponent` will use tulip 🌷. +Since `Host` has the `host` option , no matter what the parent of `Host` might have as a `flower.emoji` value, the `Host` will use tulip 🌷. ### Modifiers with constructor injection @@ -315,9 +311,9 @@ Similarly as presented before, the behavior of constructor injection can be modi Import each of them from `@angular/core` and use each in the component class constructor when you inject your service. -```ts {header:"self-no-data.component.ts" highlight:[2]} -export class SelfNoDataComponent { - constructor(@Self() @Optional() public leaf?: LeafService) { } +```ts {header:"self-no-data.ts" highlight:[2]} +export class SelfNoData { + constructor(@Self() @Optional() public leaf?: LeafService) {} } ``` @@ -330,9 +326,7 @@ Understanding the underlying logical structure of the Angular template will give Components are used in your templates, as in the following example: ```html - - ; - + ; ``` HELPFUL: Usually, you declare the components and their templates in separate files. @@ -370,7 +364,7 @@ A component class can provide services in two ways: In the examples below, you will see the logical tree of an Angular application. To illustrate how the injector works in the context of templates, the logical tree will represent the HTML structure of the application. -For example, the logical tree will show that `` is a direct children of ``. +For example, the logical tree will show that `` is a direct child of ``. In the logical tree, you will see special attributes: `@Provide`, `@Inject`, and `@ApplicationConfig`. These aren't real attributes but are here to demonstrate what is going on under the hood. @@ -385,31 +379,29 @@ These aren't real attributes but are here to demonstrate what is going on under The example application has a `FlowerService` provided in `root` with an `emoji` value of red hibiscus 🌺. -```ts {header:"lower.service.ts"} -@Injectable({ - providedIn: 'root' -}) +```ts {header:"flower.service.ts"} +@Service() export class FlowerService { emoji = '🌺'; } ``` -Consider an application with only an `AppComponent` and a `ChildComponent`. +Consider an application with only an `App` and a `Child`. The most basic rendered view would look like nested HTML elements such as the following: ```html - - - + + + ``` However, behind the scenes, Angular uses a logical view representation as follows when resolving injection requests: ```html - + <#VIEW> - + <#VIEW> @@ -425,7 +417,7 @@ Knowledge of this structure can inform how you provide and inject your services, Now, consider that `` injects the `FlowerService`: ```typescript -export class AppComponent { +export class App { flower = inject(FlowerService); } ``` @@ -438,7 +430,7 @@ Add a binding to the `` template to visualize the result: The output in the view would be: -```shell +```text {hideCopy} Emoji from FlowerService: 🌺 ``` @@ -478,17 +470,17 @@ In the example case, the constraints are: ### Using the `providers` array -Now, in the `ChildComponent` class, add a provider for `FlowerService` to demonstrate more complex resolution rules in the upcoming sections: +Now, in the `Child` class, add a provider for `FlowerService` to demonstrate more complex resolution rules in the upcoming sections: ```ts @Component({ selector: 'app-child', - templateUrl: './child.component.html', - styleUrls: ['./child.component.css'], + templateUrl: './child.html', + styleUrls: ['./child.css'], // use the providers array to provide a service providers: [{provide: FlowerService, useValue: {emoji: '🌻'}}], }) -export class ChildComponent { +export class Child { // inject the service flower = inject(FlowerService); } @@ -497,15 +489,15 @@ export class ChildComponent { Now that the `FlowerService` is provided in the `@Component()` decorator, when the `` requests the service, the injector has only to look as far as the `ElementInjector` in the ``. It won't have to continue the search any further through the injector tree. -The next step is to add a binding to the `ChildComponent` template. +The next step is to add a binding to the `Child` template. ```html

    Emoji from FlowerService: {{flower.emoji}}

    ``` -To render the new values, add `` to the bottom of the `AppComponent` template so the view also displays the sunflower: +To render the new values, add `` to the bottom of the `App` template so the view also displays the sunflower: -```shell +```text {hideCopy} Child Component Emoji from FlowerService: 🌻 ``` @@ -514,17 +506,18 @@ In the logical tree, this is represented as follows: ```html "🌺"> -<#VIEW> - -

    Emoji from FlowerService: {{flower.emoji}} (🌺)

    -"🌻"> -<#VIEW> -

    Child Component

    -

    Emoji from FlowerService: {{flower.emoji}} (🌻)

    - -
    + @Inject(FlowerService) flower=>"🌺"> + <#VIEW> + +

    Emoji from FlowerService: {{flower.emoji}} (🌺)

    + "🌻"> + <#VIEW> +

    Child Component

    +

    Emoji from FlowerService: {{flower.emoji}} (🌻)

    + +
    ``` @@ -548,20 +541,18 @@ For demonstration, we are building an `AnimalService` to demonstrate `viewProvid First, create an `AnimalService` with an `emoji` property of whale 🐳: ```typescript -import {Injectable} from '@angular/core'; +import {Service} from '@angular/core'; -@Injectable({ - providedIn: 'root', -}) +@Service() export class AnimalService { emoji = '🐳'; } ``` -Following the same pattern as with the `FlowerService`, inject the `AnimalService` in the `AppComponent` class: +Following the same pattern as with the `FlowerService`, inject the `AnimalService` in the `App` class: ```ts -export class AppComponent { +export class App { public flower = inject(FlowerService); public animal = inject(AnimalService); } @@ -575,36 +566,36 @@ Here, it has a value of dog 🐶. ```typescript @Component({ selector: 'app-child', - templateUrl: './child.component.html', - styleUrls: ['./child.component.css'], + templateUrl: './child.html', + styleUrls: ['./child.css'], // provide services providers: [{provide: FlowerService, useValue: {emoji: '🌻'}}], viewProviders: [{provide: AnimalService, useValue: {emoji: '🐶'}}], }) -export class ChildComponent { +export class Child { // inject services flower = inject(FlowerService); animal = inject(AnimalService); } ``` -Add bindings to the `ChildComponent` and the `AppComponent` templates. -In the `ChildComponent` template, add the following binding: +Add bindings to the `Child` and the `App` templates. +In the `Child` template, add the following binding: ```html

    Emoji from AnimalService: {{animal.emoji}}

    ``` -Additionally, add the same to the `AppComponent` template: +Additionally, add the same to the `App` template: ```html -

    Emoji from AnimalService: {{animal.emoji}}

    s +

    Emoji from AnimalService: {{animal.emoji}}

    ``` Now you should see both values in the browser: -```shell -AppComponent +```text {hideCopy} +App Emoji from AnimalService: 🐳 Child Component @@ -615,16 +606,17 @@ The logic tree for this example of `viewProviders` is as follows: ```html "🐳"> -<#VIEW> - -<#VIEW @Provide(AnimalService="🐶") -@Inject(AnimalService=>"🐶")> - - -

    Emoji from AnimalService: {{animal.emoji}} (🐶)

    - -
    + @Inject(AnimalService) animal=>"🐳"> + <#VIEW> + + <#VIEW @Provide(AnimalService="🐶") + @Inject(AnimalService=>"🐶")> + + +

    Emoji from AnimalService: {{animal.emoji}} (🐶)

    + +
    ``` @@ -636,63 +628,63 @@ It doesn't need to continue searching the `ElementInjector` tree, nor does it ne ### `providers` vs. `viewProviders` The `viewProviders` field is conceptually similar to `providers`, but there is one notable difference. -Configured providers in `viewProviders` are not visible to projected content that ends up as a logical children of the component. +Providers in `viewProviders` are only visible inside the component's own view — content projected into the component via `` cannot see them. -To see the difference between using `providers` and `viewProviders`, add another component to the example and call it `InspectorComponent`. -`InspectorComponent` will be a child of the `ChildComponent`. -In `inspector.component.ts`, inject the `FlowerService` and `AnimalService` during property initialization: +To see the difference between using `providers` and `viewProviders`, add another component to the example and call it `Inspector`. +`Inspector` will be a child of the `Child`. +In `inspector.ts`, inject the `FlowerService` and `AnimalService` during property initialization: ```typescript -export class InspectorComponent { +export class Inspector { flower = inject(FlowerService); animal = inject(AnimalService); } ``` You do not need a `providers` or `viewProviders` array. -Next, in `inspector.component.html`, add the same markup from previous components: +Next, in `inspector.html`, add the same markup from previous components: ```html

    Emoji from FlowerService: {{flower.emoji}}

    Emoji from AnimalService: {{animal.emoji}}

    ``` -Remember to add the `InspectorComponent` to the `ChildComponent` `imports` array. +Remember to add the `Inspector` to the `Child` `imports` array. ```ts @Component({ ... - imports: [InspectorComponent] + imports: [Inspector] }) ``` -Next, add the following to `child.component.html`: +Next, add the following to `child.html`: ```html ...

    Content projection

    - +

    Inside the view

    - + ``` -`` allows you to project content, and `` inside the `ChildComponent` template makes the `InspectorComponent` a child component of `ChildComponent`. +`` allows you to project content, and `` inside the `Child` template makes the `Inspector` a child component of `Child`. -Next, add the following to `app.component.html` to take advantage of content projection. +Next, add the following to `app.html` to take advantage of content projection. ```html - + ``` The browser now renders the following, omitting the previous examples for brevity: -```shell +```text {hideCopy} ... Content projection @@ -704,45 +696,54 @@ Emoji from AnimalService: 🐶 ``` These four bindings demonstrate the difference between `providers` and `viewProviders`. -Remember that the dog emoji 🐶 is declared inside the `<#VIEW>` of `ChildComponent` and isn't visible to the projected content. +Remember that the dog emoji 🐶 is declared inside the `<#VIEW>` of `Child` and isn't visible to the projected content. Instead, the projected content sees the whale 🐳. -However, in the next output section though, the `InspectorComponent` is an actual child component of `ChildComponent`, `InspectorComponent` is inside the `<#VIEW>`, so when it asks for the `AnimalService`, it sees the dog 🐶. +You might wonder why the projected `` can still see 🐳 from `App`'s `viewProviders`. +The reason is that Angular DI tracks **where a component was declared**, not where it ends up being rendered. +`` lives in `App`'s template — inside `App`'s `<#VIEW>` — so `App`'s `viewProviders` are fair game. +Projecting it into `Child` cuts off access to `Child`'s `viewProviders` (🐶), but `App`'s providers (🐳) are still reachable up the tree. + +However, in the next output section though, the `Inspector` is an actual child component of `Child`, `Inspector` is inside the `<#VIEW>`, so when it asks for the `AnimalService`, it sees the dog 🐶. The `AnimalService` in the logical tree would look like this: ```html "🐳"> -<#VIEW> - -<#VIEW @Provide(AnimalService="🐶") -@Inject(AnimalService=>"🐶")> - - -

    Emoji from AnimalService: {{animal.emoji}} (🐶)

    - -
    -

    Content projection

    - "🐳"> -

    Emoji from AnimalService: {{animal.emoji}} (🐳)

    -
    -
    - - - <#VIEW @Inject(AnimalService) animal=>"🐶"> -

    Emoji from AnimalService: {{animal.emoji}} (🐶)

    - -
    - -
    + @Inject(AnimalService) animal=>"🐳"> + <#VIEW> + + <#VIEW @Provide(AnimalService="🐶") + @Inject(AnimalService=>"🐶")> + + +

    Emoji from AnimalService: {{animal.emoji}} (🐶)

    + +
    +

    Content projection

    + "🐳"> +

    Emoji from AnimalService: {{animal.emoji}} (🐳)

    +
    +
    + + + <#VIEW @Inject(AnimalService) animal=>"🐶"> +

    Emoji from AnimalService: {{animal.emoji}} (🐶)

    + +
    + +
    ``` -The projected content of `` sees the whale 🐳, not the dog 🐶, because the dog 🐶 is inside the `` `<#VIEW>`. -The `` can only see the dog 🐶 if it is also within the `<#VIEW>`. +The projected `` gets 🐳 because 🐶 belongs to `Child`'s view and projected content can't reach it. +🐳 is accessible because `` was declared in `App`'s template, so it can still walk up to `App`'s `viewProviders`. + +The `` that lives directly inside `Child`'s template (not projected) gets 🐶 — it's inside the `<#VIEW>`, so no boundary to cross. ### Visibility of provided tokens @@ -750,10 +751,10 @@ Visibility decorators influence where the search for the injection token begins To do this, place visibility configuration at the point of injection, that is, when invoking `inject()`, rather than at a point of declaration. To alter where the injector starts looking for `FlowerService`, add `skipSelf` to the `` `inject()` invocation where `FlowerService` is injected. -This invocation is a property initializer the `` as shown in `child.component.ts`: +This invocation is a property initializer in `` as shown in `child.ts`: ```typescript - flower = inject(FlowerService, { skipSelf: true }) +flower = inject(FlowerService, {skipSelf: true}); ``` With `skipSelf`, the `` injector doesn't look to itself for the `FlowerService`. @@ -761,7 +762,7 @@ Instead, the injector starts looking for the `FlowerService` at the `ElementInje Then, it goes back to the `` `ModuleInjector` and finds the red hibiscus 🌺 value, which is available because `` and `` share the same `ModuleInjector`. The UI renders the following: -```shell +```text {hideCopy} Emoji from FlowerService: 🌺 ``` @@ -769,15 +770,16 @@ In a logical tree, this same idea might look like this: ```html "🌺"> -<#VIEW> - -<#VIEW @Inject(FlowerService, SkipSelf)=>"🌺"> + @Inject(FlowerService) flower=>"🌺"> + <#VIEW> + + <#VIEW @Inject(FlowerService, SkipSelf)=>"🌺"> - + - - + + ``` @@ -790,12 +792,13 @@ Here's the idea in the logical tree: ```html "🌺"> -<#VIEW> - -<#VIEW inject(FlowerService, {skipSelf: true, host: true, optional:true})=>null> - - + @Inject(FlowerService) flower=>"🌺"> + <#VIEW> + + <#VIEW inject(FlowerService, {skipSelf: true, host: true, optional:true})=>null> + + ``` @@ -824,16 +827,17 @@ The logical tree looks like this with `skipSelf` in ``: ```html "🐳")> -<#VIEW> - -<#VIEW @Provide(AnimalService="🐶") -@Inject(AnimalService, SkipSelf=>"🐳")> + @Inject(AnimalService=>"🐳")> + <#VIEW> + + <#VIEW @Provide(AnimalService="🐶") + @Inject(AnimalService, SkipSelf=>"🐳")> - + - - + + ``` @@ -843,7 +847,7 @@ With `skipSelf` in the ``, the injector begins its search for the `An ### `host` and `viewProviders` If you just use `host` for the injection of `AnimalService`, the result is dog 🐶 because the injector finds the `AnimalService` in the `` `<#VIEW>` itself. -The `ChildComponent` configures the `viewProviders` so that the dog emoji is provided as `AnimalService` value. +The `Child` configures the `viewProviders` so that the dog emoji is provided as `AnimalService` value. You can also see `host` the `inject()`: ```typescript @@ -854,7 +858,7 @@ You can also see `host` the `inject()`: { provide: AnimalService, useValue: { emoji: '🐶' } }, ] }) -export class ChildComponent { +export class Child { animal = inject(AnimalService, { host: true }) } ``` @@ -863,36 +867,37 @@ export class ChildComponent { ```html "🐳")> -<#VIEW> - -<#VIEW @Provide(AnimalService="🐶") -inject(AnimalService, {host: true}=>"🐶")> - - + @Inject(AnimalService=>"🐳")> + <#VIEW> + + <#VIEW @Provide(AnimalService="🐶") + inject(AnimalService, {host: true}=>"🐶")> + + ``` -Add a `viewProviders` array with a third animal, hedgehog 🦔, to the `app.component.ts` `@Component()` metadata: +Add a `viewProviders` array with a third animal, hedgehog 🦔, to the `app.ts` `@Component()` metadata: ```typescript @Component({ selector: 'app-root', - templateUrl: './app.component.html', - styleUrls: [ './app.component.css' ], + templateUrl: './app.html', + styleUrls: [ './app.css' ], viewProviders: [ { provide: AnimalService, useValue: { emoji: '🦔' } }, ], }) ``` -Next, add `skipSelf` along with `host` to the `inject()` for the `AnimalService` injection in `child.component.ts`. +Next, add `skipSelf` along with `host` to the `inject()` for the `AnimalService` injection in `child.ts`. Here are `host` and `skipSelf` in the `animal` property initialization: ```typescript -export class ChildComponent { - animal = inject(AnimalService, { host: true, skipSelf: true }); +export class Child { + animal = inject(AnimalService, {host: true, skipSelf: true}); } ``` @@ -903,23 +908,24 @@ export class ChildComponent { When `host` and `skipSelf` were applied to the `FlowerService`, which is in the `providers` array, the result was `null` because `skipSelf` starts its search in the `` injector, but `host` stops searching at `<#VIEW>` —where there is no `FlowerService` In the logical tree, you can see that the `FlowerService` is visible in ``, not its `<#VIEW>`. -However, the `AnimalService`, which is provided in the `AppComponent` `viewProviders` array, is visible. +However, the `AnimalService`, which is provided in the `App` `viewProviders` array, is visible. The logical tree representation shows why this is: ```html "🐳")> -<#VIEW @Provide(AnimalService="🦔") -@Inject(AnimalService, @Optional)=>"🦔"> - - - -<#VIEW @Provide(AnimalService="🐶") -inject(AnimalService, {skipSelf:true, host: true, optional: true})=>"🦔"> - - - + @Inject(AnimalService=>"🐳")> + <#VIEW @Provide(AnimalService="🦔") + @Inject(AnimalService, @Optional)=>"🦔"> + + + + <#VIEW @Provide(AnimalService="🐶") + inject(AnimalService, {skipSelf:true, host: true, optional: true})=>"🦔"> + + + ``` @@ -934,34 +940,34 @@ The ability to configure one or more providers at different levels opens up usef ### Scenario: service isolation Architectural reasons may lead you to restrict access to a service to the application domain where it belongs. -For example, consider we build a `VillainsListComponent` that displays a list of villains. +For example, consider we build a `VillainsList` that displays a list of villains. It gets those villains from a `VillainsService`. If you provide `VillainsService` in the root `AppModule`, it will make `VillainsService` visible everywhere in the application. -If you later modify the `VillainsService`, you could break something in other components that started depending this service by accident. +If you later modify the `VillainsService`, you could break something in other components that started depending on this service by accident. -Instead, you should provide the `VillainsService` in the `providers` metadata of the `VillainsListComponent` like this: +Instead, you should provide the `VillainsService` in the `providers` metadata of the `VillainsList` like this: ```typescript @Component({ selector: 'app-villains-list', - templateUrl: './villains-list.component.html', + templateUrl: './villains-list.html', providers: [VillainsService], }) -export class VillainsListComponent {} +export class VillainsList {} ``` -By providing `VillainsService` in the `VillainsListComponent` metadata and nowhere else, the service becomes available only in the `VillainsListComponent` and its subcomponent tree. +By providing `VillainsService` in the `VillainsList` metadata and nowhere else, the service becomes available only in the `VillainsList` and its subcomponent tree. -`VillainService` is a singleton with respect to `VillainsListComponent` because that is where it is declared. -As long as `VillainsListComponent` does not get destroyed it will be the same instance of `VillainService` but if there are multiple instances of `VillainsListComponent`, then each instance of `VillainsListComponent` will have its own instance of `VillainService`. +`VillainsService` is a singleton with respect to `VillainsList` because that is where it is declared. +As long as `VillainsList` does not get destroyed it will be the same instance of `VillainsService` but if there are multiple instances of `VillainsList`, then each instance of `VillainsList` will have its own instance of `VillainsService`. ### Scenario: multiple edit sessions Many applications allow users to work on several open tasks at the same time. For example, in a tax preparation application, the preparer could be working on several tax returns, switching from one to the other throughout the day. -To demonstrate that scenario, imagine a `HeroListComponent` that displays a list of super heroes. +To demonstrate that scenario, imagine a `HeroList` that displays a list of super heroes. To open a hero's tax return, the preparer clicks on a hero name, which opens a component for editing that return. Each selected hero tax return opens in its own component and multiple returns can be open at the same time. @@ -972,7 +978,7 @@ Each tax return component has the following characteristics: - Can change a tax return without affecting a return in another component - Has the ability to save the changes to its tax return or cancel them -Suppose that the `HeroTaxReturnComponent` had logic to manage and restore changes. +Suppose that the `HeroTaxReturn` had logic to manage and restore changes. That would be a straightforward task for a hero tax return. In the real world, with a rich tax return data model, the change management would be tricky. You could delegate that management to a helper service, as this example does. @@ -981,11 +987,11 @@ The `HeroTaxReturnService` caches a single `HeroTaxReturn`, tracks changes to th It also delegates to the application-wide singleton `HeroService`, which it gets by injection. ```typescript -import {inject, Injectable} from '@angular/core'; +import {inject, Service} from '@angular/core'; import {HeroTaxReturn} from './hero'; import {HeroesService} from './heroes.service'; -@Injectable() +@Service({autoProvided: false}) export class HeroTaxReturnService { private currentTaxReturn!: HeroTaxReturn; private originalTaxReturn!: HeroTaxReturn; @@ -1012,7 +1018,7 @@ export class HeroTaxReturnService { } ``` -Here is the `HeroTaxReturnComponent` that makes use of `HeroTaxReturnService`. +Here is the `HeroTaxReturn` that makes use of `HeroTaxReturnService`. ```typescript import {Component, input, output} from '@angular/core'; @@ -1021,11 +1027,11 @@ import {HeroTaxReturnService} from './hero-tax-return.service'; @Component({ selector: 'app-hero-tax-return', - templateUrl: './hero-tax-return.component.html', - styleUrls: ['./hero-tax-return.component.css'], + templateUrl: './hero-tax-return.html', + styleUrls: ['./hero-tax-return.css'], providers: [HeroTaxReturnService], }) -export class HeroTaxReturnComponent { +export class HeroTaxReturn { message = ''; close = output(); @@ -1073,13 +1079,13 @@ The component also asks the service to save and restore this tax return. This won't work if the service is an application-wide singleton. Every component would share the same service instance, and each component would overwrite the tax return that belonged to another hero. -To prevent this, configure the component-level injector of `HeroTaxReturnComponent` to provide the service, using the `providers` property in the component metadata. +To prevent this, configure the component-level injector of `HeroTaxReturn` to provide the service, using the `providers` property in the component metadata. ```typescript - providers: [HeroTaxReturnService] +providers: [HeroTaxReturnService]; ``` -The `HeroTaxReturnComponent` has its own provider of the `HeroTaxReturnService`. +The `HeroTaxReturn` has its own provider of the `HeroTaxReturnService`. Recall that every component _instance_ has its own injector. Providing the service at the component level ensures that _every_ instance of the component gets a private instance of the service. This makes sure that no tax return gets overwritten. @@ -1164,5 +1170,5 @@ style RootInjector fill:#BDD7EE,color:#000 ## More on dependency injection - + diff --git a/adev-es/src/content/guide/di/hierarchical-dependency-injection.md b/adev-es/src/content/guide/di/hierarchical-dependency-injection.md index f424898..31a1fc3 100644 --- a/adev-es/src/content/guide/di/hierarchical-dependency-injection.md +++ b/adev-es/src/content/guide/di/hierarchical-dependency-injection.md @@ -10,7 +10,7 @@ Angular tiene dos jerarquías de inyectores: | Jerarquías de inyectores | Detalles | |:------------------------------- |:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Jerarquía `EnvironmentInjector` | Configura un `EnvironmentInjector` en esta jerarquía usando `@Injectable()` o array `providers` en `ApplicationConfig`. | +| Jerarquía `EnvironmentInjector` | Configura un `EnvironmentInjector` en esta jerarquía usando `@Service()` o array `providers` en `ApplicationConfig`. | | Jerarquía `ElementInjector` | Creada implícitamente en cada elemento DOM. Un `ElementInjector` está vacío por defecto a menos que lo configures en la propiedad `providers` en `@Directive()` o `@Component()`. | @@ -21,12 +21,12 @@ Para aplicaciones basadas en `NgModule`, puedes proveer dependencias con la jera El `EnvironmentInjector` puede ser configurado de una de dos maneras usando: -- La propiedad `providedIn` de `@Injectable()` para referirse a `root` o `platform` +- El `@Service()` - El array `providers` de `ApplicationConfig` - + -Usar la propiedad `providedIn` de `@Injectable()` es preferible a usar el array `providers` de `ApplicationConfig`. Con `providedIn` de `@Injectable()`, las herramientas de optimización pueden realizar tree-shaking, que elimina servicios que tu aplicación no está usando. Esto resulta en tamaños de paquete más pequeños. +Usar el decorador `@Service()` es preferible a usar el array `providers` de `ApplicationConfig`. Con `@Service`, las herramientas de optimización pueden realizar tree-shaking, que elimina servicios que tu aplicación no está usando. Esto resulta en tamaños de paquete más pequeños. El tree-shaking es especialmente útil para una biblioteca porque la aplicación que usa la biblioteca puede no tener necesidad de inyectarla. @@ -34,26 +34,24 @@ El tree-shaking es especialmente útil para una biblioteca porque la aplicación `EnvironmentInjector` está configurado por `ApplicationConfig.providers`. -Provee servicios usando `providedIn` de `@Injectable()` de la siguiente manera: +Provee servicios usando `@Service()` de la siguiente manera: ```ts {highlight:[4]} -import { Injectable } from '@angular/core'; +import {Service} from '@angular/core'; -@Injectable({ - providedIn: 'root' // <--provee este servicio en el EnvironmentInjector raíz -}) +@Service() // <--provee este servicio en el EnvironmentInjector raíz export class ItemService { name = 'telephone'; } ``` -El decorador `@Injectable()` identifica una clase de servicio. -La propiedad `providedIn` configura un `EnvironmentInjector` específico, en este caso `root`, que hace que el servicio esté disponible en el `EnvironmentInjector` `root`. +Los decoradores `@Service()` o `@Injectable()` identifican una clase de servicio. ### ModuleInjector En el caso de aplicaciones basadas en `NgModule`, el ModuleInjector puede ser configurado de una de dos maneras usando: +- El decorador `@Service()`, - La propiedad `providedIn` de `@Injectable()` para referirse a `root` o `platform` - El array `providers` de `@NgModule()` @@ -386,9 +384,7 @@ Estos no son atributos reales pero están aquí para demostrar lo que está pasa La aplicación de ejemplo tiene un `FlowerService` proveído en `root` con un valor `emoji` de hibisco rojo 🌺. ```ts {header:"flower.service.ts"} -@Injectable({ - providedIn: 'root' -}) +@Service() export class FlowerService { emoji = '🌺'; } @@ -547,12 +543,8 @@ Si puedes configurarlo por tu cuenta, salta adelante a [Modificando la disponibi Para demostración, estamos construyendo un `AnimalService` para demostrar `viewProviders`. Primero, crea un `AnimalService` con una propiedad `emoji` de ballena 🐳: -```typescript -import {Injectable} from '@angular/core'; - -@Injectable({ - providedIn: 'root', -}) +```ts +@Service() export class AnimalService { emoji = '🐳'; } diff --git a/adev-es/src/content/guide/di/lazy-loading-services.md b/adev-es/src/content/guide/di/lazy-loading-services.md new file mode 100644 index 0000000..8425033 --- /dev/null +++ b/adev-es/src/content/guide/di/lazy-loading-services.md @@ -0,0 +1,85 @@ +# Lazy loading services + +IMPORTANT: For lazy loading to work, the service you load must be auto-provided. Decorate it with either `@Injectable({providedIn: 'root'})` or [`@Service()`](guide/di/creating-and-using-services#using-the-service-vs-injectable-decorator). Without auto-provisioning, Angular has no way to construct the service after it loads. + +Angular's `injectAsync` function lets you load a service on demand, only when it's actually needed. This is useful when a service depends on a large library or rarely used feature, and you don't want to pay for it on the initial page load. + +When you use `injectAsync`, the service's code is split out by your bundler into a separate JavaScript chunk and downloaded the first time you ask for the instance. Once loaded, Angular resolves the service through the regular DI system, so it can still depend on other injectables and behaves like any other singleton. + +## Lazily injecting a service + +Imagine a `ReportExporter` that depends on a heavy spreadsheet library. Most users open the report; only a few click **Export**. Load the exporter on demand: + +```angular-ts +import {Component, injectAsync} from '@angular/core'; + +@Component({ + selector: 'app-report', + template: ``, +}) +export class Report { + private exporter = injectAsync(() => import('./report-exporter').then((m) => m.ReportExporter)); + + async export() { + const exporter = await this.exporter(); + exporter.export(); + } +} +``` + +The first call to `this.exporter()` triggers the dynamic import and resolves the service from DI. Subsequent calls reuse the same promise, so the chunk is only fetched once. + +If the lazy-loaded service is the [default export](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/export#using_the_default_export), pass the dynamic import directly, Angular unwraps the `default` for you: + +```ts {header: report-exporter.ts} +@Service() +export default class ReportExporter { + /* … */ +} +``` + +```ts {header: report.ts} +private exporter = injectAsync(() => import('./report-exporter')); +``` + +## Prefetching the dependency + +By default, the lazy chunk is only fetched when you invoke the returned function. You can start the download earlier by passing a `prefetch` trigger in the options. A trigger is any function that returns a `Promise`, when it resolves, Angular kicks off the loader. + +Angular ships with `onIdle`, a built-in trigger that waits until the browser becomes idle: + +```ts +import {Component, injectAsync, onIdle} from '@angular/core'; + +@Component({ + /* … */ +}) +export class Report { + private exporter = injectAsync(() => import('./report-exporter').then((m) => m.ReportExporter), { + prefetch: onIdle, + }); +} +``` + +You can also configure `onIdle` with a maximum wait time so the prefetch always happens within a known window, even on busy pages: + +```ts +injectAsync(loader, {prefetch: () => onIdle({timeout: 1_000})}); +``` + +NOTE: Prefetching is opportunistic. If the user invokes the feature before the prefetch fires, Angular still loads the dependency immediately and resolves your `await` as soon as it's ready. + +## Provide a custom prefetch trigger + +A `PrefetchTrigger` is just a function that returns a promise, the loader runs as soon as the promise resolves. Use this to align prefetching with your own signals, such as a hover or a scheduler tick: + +```ts +import {PrefetchTrigger} from '@angular/core'; + +export function onHover(target: HTMLElement): PrefetchTrigger { + return () => + new Promise((resolve) => { + target.addEventListener('pointerenter', () => resolve(), {once: true}); + }); +} +``` diff --git a/adev-es/src/content/guide/di/lightweight-injection-tokens.en.md b/adev-es/src/content/guide/di/lightweight-injection-tokens.en.md index bb28b41..314580e 100644 --- a/adev-es/src/content/guide/di/lightweight-injection-tokens.en.md +++ b/adev-es/src/content/guide/di/lightweight-injection-tokens.en.md @@ -22,52 +22,51 @@ To prevent the retention of unused components, your library should use the light To better explain the condition under which token retention occurs, consider a library that provides a library-card component. This component contains a body and can contain an optional header: -```angular-html - -; -; -; +```html + + + ``` -In a likely implementation, the `` component uses `@ContentChild()` or `@ContentChildren()` to get `` and ``, as in the following: +In a likely implementation, the `` component uses `contentChild` or `contentChildren` to get `` and ``, as in the following: ```ts {highlight: [14]} -import {Component, ContentChild} from '@angular/core'; +import {Component, contentChild} from '@angular/core'; @Component({ selector: 'lib-header', …, }) -class LibHeaderComponent {} +class LibHeader {} @Component({ selector: 'lib-card', …, }) -class LibCardComponent { - @ContentChild(LibHeaderComponent) header: LibHeaderComponent | null = null; +class LibCard { + readonly header = contentChild(LibHeader); } ``` -Because `` is optional, the element can appear in the template in its minimal form, ``. +Because `` is optional, the element can appear in the template in its minimal form, ``. In this case, `` is not used and you would expect it to be tree-shaken, but that is not what happens. -This is because `LibCardComponent` actually contains two references to the `LibHeaderComponent`: +This is because `LibCard` actually contains two references to the `LibHeader`: ```ts -@ContentChild(LibHeaderComponent) header: LibHeaderComponent; +readonly header = contentChild(LibHeader); ``` -- One of these reference is in the _type position_-- that is, it specifies `LibHeaderComponent` as a type: `header: LibHeaderComponent;`. -- The other reference is in the _value position_-- that is, LibHeaderComponent is the value of the `@ContentChild()` parameter decorator: `@ContentChild(LibHeaderComponent)`. +- One of these references is in the _type position_-- that is, it specifies `LibHeader` as a type: `readonly header: Signal`. +- The other reference is in the _value position_-- that is, `LibHeader` is the value passed into the `contentChild` function: `contentChild(LibHeader)`. The compiler handles token references in these positions differently: - The compiler erases _type position_ references after conversion from TypeScript, so they have no impact on tree-shaking. - The compiler must keep _value position_ references at runtime, which **prevents** the component from being tree-shaken. -In the example, the compiler retains the `LibHeaderComponent` token that occurs in the value position. +In the example, the compiler retains the `LibHeader` token that occurs in the value position. This prevents the referenced component from being tree-shaken, even if the application does not actually use `` anywhere. -If `LibHeaderComponent` 's code, template, and styles combine to become too large, including it unnecessarily can significantly increase the size of the client application. +If `LibHeader` 's code, template, and styles combine to become too large, including it unnecessarily can significantly increase the size of the client application. ## When to use the lightweight injection token pattern @@ -75,63 +74,63 @@ The tree-shaking problem arises when a component is used as an injection token. There are two cases when that can happen: - The token is used in the value position of a [content query](guide/components/queries#content-queries). -- The token is used as a type specifier for constructor injection. +- The token is used with the `inject` function. -In the following example, both uses of the `OtherComponent` token cause retention of `OtherComponent`, preventing it from being tree-shaken when it is not used: +In the following example, both uses of the `CustomOther` token cause retention of `CustomOther`, preventing it from being tree-shaken when it is not used: ```ts {highlight: [[2],[4]]} -class MyComponent { - constructor(@Optional() other: OtherComponent) {} +class App { + private readonly other = inject(CustomOther, {optional: true}); - @ContentChild(OtherComponent) other: OtherComponent | null; + readonly header = contentChild(CustomOther); } ``` Although tokens used only as type specifiers are removed when converted to JavaScript, all tokens used for dependency injection are needed at runtime. -These effectively change `constructor(@Optional() other: OtherComponent)` to `constructor(@Optional() @Inject(OtherComponent) other)`. +When using `inject(CustomOther)`, `CustomOther` is passed as a value argument. The token is now in a value position, which causes the tree-shaker to keep the reference. -HELPFUL: Libraries should use [tree-shakable providers](guide/di/dependency-injection#providing-dependency) for all services, providing dependencies at the root level rather than in components or modules. +HELPFUL: Libraries should use [tree-shakable providers](guide/di/defining-dependency-providers) for all services, providing dependencies at the root level rather than in components or modules. ## Using lightweight injection tokens The lightweight injection token design pattern consists of using a small abstract class as an injection token, and providing the actual implementation at a later stage. The abstract class is retained, not tree-shaken, but it is small and has no material impact on the application size. -The following example shows how this works for the `LibHeaderComponent`: +The following example shows how this works for the `LibHeader`: ```ts {highlight: [[1],[5], [15]]} abstract class LibHeaderToken {} @Component({ selector: 'lib-header', - providers: [{provide: LibHeaderToken, useExisting: LibHeaderComponent}], + providers: [{provide: LibHeaderToken, useExisting: LibHeader}], …, }) -class LibHeaderComponent extends LibHeaderToken {} +class LibHeader extends LibHeaderToken {} @Component({ selector: 'lib-card', …, }) -class LibCardComponent { - @ContentChild(LibHeaderToken) header: LibHeaderToken | null = null; +class LibCard { + readonly header = contentChild(LibHeaderToken); } ``` -In this example, the `LibCardComponent` implementation no longer refers to `LibHeaderComponent` in either the type position or the value position. -This lets full tree-shaking of `LibHeaderComponent` take place. +In this example, the `LibCard` implementation no longer refers to `LibHeader` in either the type position or the value position. +This lets full tree-shaking of `LibHeader` take place. The `LibHeaderToken` is retained, but it is only a class declaration, with no concrete implementation. It is small and does not materially impact the application size when retained after compilation. -Instead, `LibHeaderComponent` itself implements the abstract `LibHeaderToken` class. +Instead, `LibHeader` itself implements the abstract `LibHeaderToken` class. You can safely use that token as the provider in the component definition, allowing Angular to correctly inject the concrete type. To summarize, the lightweight injection token pattern consists of the following: 1. A lightweight injection token that is represented as an abstract class. 2. A component definition that implements the abstract class. -3. Injection of the lightweight pattern, using `@ContentChild()` or `@ContentChildren()`. +3. Injection of the lightweight pattern, using `contentChild` or `contentChildren`. 4. A provider in the implementation of the lightweight injection token which associates the lightweight injection token with the implementation. ### Use the lightweight injection token for API definition @@ -141,19 +140,19 @@ The token is now an abstract class. Since the injectable component implements th The implementation of the method, with all its code overhead, resides in the injectable component that can be tree-shaken. This lets the parent communicate with the child, if it is present, in a type-safe manner. -For example, the `LibCardComponent` now queries `LibHeaderToken` rather than `LibHeaderComponent`. -The following example shows how the pattern lets `LibCardComponent` communicate with the `LibHeaderComponent` without actually referring to `LibHeaderComponent`: +For example, the `LibCard` now queries `LibHeaderToken` rather than `LibHeader`. +The following example shows how the pattern lets `LibCard` communicate with the `LibHeader` without actually referring to `LibHeader`: -```ts {highlight: [[2],[9],[11],[19]]} +```ts {highlight: [[2],[7],[11],[19]]} abstract class LibHeaderToken { abstract doSomething(): void; } @Component({ selector: 'lib-header', - providers: [{provide: LibHeaderToken, useExisting: LibHeaderComponent}], + providers: [{provide: LibHeaderToken, useExisting: LibHeader}], }) -class LibHeaderComponent extends LibHeaderToken { +class LibHeader extends LibHeaderToken { doSomething(): void { // Concrete implementation of `doSomething` } @@ -162,12 +161,12 @@ class LibHeaderComponent extends LibHeaderToken { @Component({ selector: 'lib-card', }) -class LibCardComponent implements AfterContentInit { - @ContentChild(LibHeaderToken) header: LibHeaderToken | null = null; +class LibCard implements AfterContentInit { + readonly header = contentChild(LibHeaderToken); ngAfterContentInit(): void { - if (this.header !== null) { - this.header?.doSomething(); + if (this.header() !== undefined) { + this.header()!.doSomething(); } } } @@ -180,8 +179,8 @@ If the child component has been tree-shaken, there is no runtime reference to it ### Naming your lightweight injection token Lightweight injection tokens are only useful with components. -The Angular style guide suggests that you name components using the "Component" suffix. -The example "LibHeaderComponent" follows this convention. +The [Angular Style Guide](style-guide) suggests that you name components without the suffix `Component`. +The example `LibHeader` follows this convention. You should maintain the relationship between the component and its token while still distinguishing between them. -The recommended style is to use the component base name with the suffix "`Token`" to name your lightweight injection tokens: "`LibHeaderToken`." +The recommended style is to use the component base name with the suffix `Token` to name your lightweight injection tokens: `LibHeaderToken`. diff --git a/adev-es/src/content/guide/di/overview.en.md b/adev-es/src/content/guide/di/overview.en.md index 03de6b1..7142858 100644 --- a/adev-es/src/content/guide/di/overview.en.md +++ b/adev-es/src/content/guide/di/overview.en.md @@ -1,38 +1,38 @@ -Dependency Injection (DI) is a design pattern used to organize and share code across an application. +Dependency Injection (DI) is a design pattern you use to organize and share code across your application by supplying dependencies to a class instead of creating them inside it. TIP: Check out Angular's [Essentials](essentials/dependency-injection) before diving into this comprehensive guide. -As an application grows, developers often need to reuse and share features across different parts of the codebase. [Dependency Injection (DI)](https://en.wikipedia.org/wiki/Dependency_injection) is a design pattern used to organize and share code across an application by allowing you to "inject" features into different parts. +As an application grows, developers often need to reuse and share functionality across different parts of the codebase. [Dependency Injection (DI)](https://en.wikipedia.org/wiki/Dependency_injection) helps you achieve this by allowing you to provide dependencies to a class instead of creating them directly inside it. This makes different parts of the application more reusable and easier to manage. Dependency injection is a popular pattern because it allows developers to address common challenges such as: -- **Improved code maintainability**: Dependency injection allows cleaner separation of concerns which enables easier refactoring and reducing code duplication. -- **Scalability**: Modular functionality can be reused across multiple contexts and allows for easier scaling. -- **Better testing**: DI allows unit tests to easily use [test doubles](https://en.wikipedia.org/wiki/Test_double) for situations when using a real implementation is not practical. +- **Improved code maintainability**: Dependency injection promotes a clear separation of concerns, making code easier to refactor and reducing duplication. +- **Scalability**: You can reuse modular functionality across different parts of an application, making it easier to scale. +- **Better testing**: DI allows unit tests to use [test doubles](https://en.wikipedia.org/wiki/Test_double) in place of real implementations when needed. ## How does dependency injection work in Angular? -A dependency is any object, value, function or service that a class needs to work but does not create itself. In other words, it creates a relationship between different parts of your application since it wouldn't work without the dependency. +A dependency is any object, value, function, or service that a class requires to work but does not create itself. Instead, you provide it from the outside, creating a clear relationship between different parts of the application. -There are two ways that code interacts with any dependency injection system: +You interact with a dependency injection system in two main ways: -- Code can _provide_, or make available, values. -- Code can _inject_, or ask for, those values as dependencies. +- You can _provide_, or make available, values. +- You can _inject_, or ask for, those values as dependencies. -"Values," in this context, can be any JavaScript value, including objects and functions. Common types of injected dependencies include: +In this context, "values" can refer to any JavaScript value, including objects, functions, or class instances. Common types of injected dependencies include: - **Configuration values**: Environment-specific constants, API URLs, feature flags, etc. - **Factories**: Functions that create objects or values based on runtime conditions - **Services**: Classes that provide common functionality, business logic, or state -Angular components and directives automatically participate in DI, meaning that they can inject dependencies _and_ they are available to be injected. +Angular components and directives automatically participate in DI, meaning that you can inject dependencies into them and make them available for injection. ## What are services? -An Angular _service_ is a TypeScript class decorated with `@Injectable`, which makes an instance of the class available to be injected as a dependency. Services are the most common way of sharing data and functionality across an application. +An Angular _service_ is a TypeScript class decorated with `@Service`, which allows you to inject an instance of the class as a dependency. Services are the most common way of sharing data and functionality across an application. Common types of services include: @@ -46,21 +46,23 @@ Common types of services include: The following example declares a service named `AnalyticsLogger`: ```ts -import { Injectable } from '@angular/core'; +import {Service} from '@angular/core'; -@Injectable({ providedIn: 'root' }) +@Service() export class AnalyticsLogger { trackEvent(category: string, value: string) { console.log('Analytics event logged:', { category, value, - timestamp: new Date().toISOString() - }) + timestamp: new Date().toISOString(), + }); } } ``` -NOTE: The `providedIn: 'root'` option makes this service available throughout your entire application as a singleton. This is the recommended approach for most services. +NOTE: The `@Service` makes this service available throughout your entire application as a singleton. This is the recommended approach for most services. + +HELPFUL: The [`@Service`](guide/di/creating-and-using-services#using-the-service-vs-injectable-decorator) decorator is an ergonomic shorthand for `@Injectable({providedIn: 'root'})`. ## Injecting dependencies with `inject()` @@ -69,17 +71,15 @@ You can inject dependencies using Angular's `inject()` function. Here is an example of a navigation bar that injects `AnalyticsLogger` and Angular `Router` service to allow users to navigate to a different page while tracking the event. ```angular-ts -import { Component, inject } from '@angular/core'; -import { Router } from '@angular/router'; -import { AnalyticsLogger } from './analytics-logger'; +import {Component, inject} from '@angular/core'; +import {Router} from '@angular/router'; +import {AnalyticsLogger} from './analytics-logger'; @Component({ selector: 'app-navbar', - template: ` -
    Detail Page - `, + template: `Detail Page`, }) -export class NavbarComponent { +export class Navbar { private router = inject(Router); private analytics = inject(AnalyticsLogger); @@ -93,10 +93,10 @@ export class NavbarComponent { ### Where can `inject()` be used? -You can inject dependencies during construction of a component, directive, or service. The call to `inject` can appear in either the `constructor` or in a field initializer. Here are some common examples: +You can inject dependencies during construction of a component, directive, or service. The call to [`inject`](/api/core/inject) can appear in either the `constructor` or in a field initializer. Here are some common examples: ```ts -@Component({...}) +@Component(/* ... */) export class MyComponent { // ✅ In class field initializer private service = inject(MyService); @@ -119,10 +119,10 @@ export class MyDirective { ``` ```ts -import { Injectable, inject } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; +import {Service, inject} from '@angular/core'; +import {HttpClient} from '@angular/common/http'; -@Injectable({ providedIn: 'root' }) +@Service() export class MyService { // ✅ In a service private http = inject(HttpClient); @@ -134,10 +134,10 @@ export const authGuard = () => { // ✅ In a route guard const auth = inject(AuthService); return auth.isAuthenticated(); -} +}; ``` -Angular uses the term "injection context" to describe any place in your code where you can call `inject`. While component, directive, and service construction is the most common, see [injection contexts](/guide/di/dependency-injection-context) for more details. +Angular uses the term "injection context" to describe any place in your code where you can call [`inject`](/api/core/inject). While component, directive, and service construction is the most common, see [injection contexts](/guide/di/dependency-injection-context) for more details. For more information, see the [inject API docs](api/core/inject#usage-notes). diff --git a/adev-es/src/content/guide/di/overview.md b/adev-es/src/content/guide/di/overview.md index 2c8d3a0..3431a01 100644 --- a/adev-es/src/content/guide/di/overview.md +++ b/adev-es/src/content/guide/di/overview.md @@ -32,7 +32,7 @@ Los componentes y directivas de Angular participan automáticamente en DI, lo qu ## ¿Qué son los servicios? -Un _servicio_ de Angular es una clase TypeScript decorada con `@Injectable`, que hace que una instancia de la clase esté disponible para ser inyectada como dependencia. Los servicios son la forma más común de compartir datos y funcionalidad a través de una aplicación. +Un _servicio_ de Angular es una clase TypeScript decorada con `@Service`, que te permite inyectar una instancia de la clase como dependencia. Los servicios son la forma más común de compartir datos y funcionalidad a través de una aplicación. Los tipos comunes de servicios incluyen: @@ -46,21 +46,23 @@ Los tipos comunes de servicios incluyen: El siguiente ejemplo declara un servicio llamado `AnalyticsLogger`: ```ts -import { Injectable } from '@angular/core'; +import {Service} from '@angular/core'; -@Injectable({ providedIn: 'root' }) +@Service() export class AnalyticsLogger { trackEvent(category: string, value: string) { console.log('Analytics event logged:', { category, value, - timestamp: new Date().toISOString() - }) + timestamp: new Date().toISOString(), + }); } } ``` -NOTA: La opción `providedIn: 'root'` hace que este servicio esté disponible en toda tu aplicación como un singleton. Este es el enfoque recomendado para la mayoría de los servicios. +NOTA: El `@Service` hace que este servicio esté disponible en toda tu aplicación como un singleton. Este es el enfoque recomendado para la mayoría de los servicios. + +ÚTIL: El decorador [`@Service`](guide/di/creating-and-using-services#using-the-service-vs-injectable-decorator) es una abreviatura ergonómica para `@Injectable({providedIn: 'root'})`. ## Inyectando dependencias con `inject()` @@ -79,7 +81,7 @@ import { AnalyticsLogger } from './analytics-logger'; Detail Page `, }) -export class NavbarComponent { +export class Navbar { private router = inject(Router); private analytics = inject(AnalyticsLogger); @@ -96,7 +98,7 @@ export class NavbarComponent { Puedes inyectar dependencias durante la construcción de un componente, directiva o servicio. La llamada a `inject` puede aparecer en el `constructor` o en un inicializador de campo. Aquí hay algunos ejemplos comunes: ```ts -@Component({...}) +@Component(/* ... */) export class MyComponent { // ✅ En inicializador de campo de clase private service = inject(MyService); @@ -119,10 +121,10 @@ export class MyDirective { ``` ```ts -import { Injectable, inject } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; +import {Service, inject} from '@angular/core'; +import {HttpClient} from '@angular/common/http'; -@Injectable({ providedIn: 'root' }) +@Service() export class MyService { // ✅ En un servicio private http = inject(HttpClient); diff --git a/adev-es/src/content/guide/directives/attribute-directives.en.md b/adev-es/src/content/guide/directives/attribute-directives.en.md index 4d39ac9..ad50cc2 100644 --- a/adev-es/src/content/guide/directives/attribute-directives.en.md +++ b/adev-es/src/content/guide/directives/attribute-directives.en.md @@ -1,137 +1,81 @@ # Attribute directives -Change the appearance or behavior of DOM elements and Angular components with attribute directives. +Attribute directives change the appearance or behavior of DOM elements and Angular components. -## Building an attribute directive +## Use template bindings for one-off behavior -This section walks you through creating a highlight directive that sets the background color of the host element to yellow. +Angular's template syntax already covers changing a single element's classes, styles, properties, and events: -1. To create a directive, use the CLI command [`ng generate directive`](tools/cli/schematics). +- [Class and style bindings](guide/templates/binding#css-class-and-style-property-bindings) add and remove CSS classes and inline styles. +- [Property and attribute bindings](guide/templates/binding) set DOM properties and HTML attributes. +- [Event listeners](guide/templates/event-listeners) respond to user interaction. - ```shell - ng generate directive highlight - ``` +Attribute directives are useful when you want to package this kind of behavior into a reusable unit that you can apply to any element or component. - The CLI creates `src/app/highlight.directive.ts`, a corresponding test file `src/app/highlight.directive.spec.ts`. +## Building an attribute directive - +A custom attribute directive is a JavaScript class with the `@Directive()` decorator. The decorator's `selector` defines the attribute that applies the directive. The square brackets make this an attribute selector, so the directive matches elements that carry the attribute. By convention, use a prefix such as `app` to avoid naming collisions: - The `@Directive()` decorator's configuration property specifies the directive's CSS attribute selector, `[appHighlight]`. +```ts +import {Directive} from '@angular/core'; -1. Import `ElementRef` from `@angular/core`. - `ElementRef` grants direct access to the host DOM element through its `nativeElement` property. +@Directive({ + selector: '[appHighlight]', +}) +export class HighlightDirective {} +``` -1. Add `ElementRef` in the directive's `constructor()` to [inject](guide/di) a reference to the host DOM element, the element to which you apply `appHighlight`. +HELPFUL: The CLI command [`ng generate directive`](tools/cli/schematics) scaffolds a directive along with its test file. -1. Add logic to the `HighlightDirective` class that sets the background to yellow. +A directive can change its host declaratively through host bindings or imperatively through a reference to the host element. This example [injects](guide/di) [`ElementRef`](api/core/ElementRef) and accesses the element through its `nativeElement` property to set the background to yellow: -HELPFUL: Directives _do not_ support namespaces. +IMPORTANT: Directives _do not_ support namespaces. - +```angular-html {avoid} +

    This is invalid

    +``` ## Applying an attribute directive -1. To use the `HighlightDirective`, add a `

    ` element to the HTML template with the directive as an attribute. +To apply the directive, add its selector as an attribute on an element: - + -Angular creates an instance of the `HighlightDirective` class and injects a reference to the `

    ` element into the directive's constructor, which sets the `

    ` element's background style to yellow. +Angular creates an instance of `HighlightDirective` for that `

    ` element, injects a reference to the element, and sets its background to yellow. ## Handling user events -This section shows you how to detect when a user mouses into or out of the element and to respond by setting or clearing the highlight color. - -1. Configure host event bindings using the `host` property in the `@Directive()` decorator. - - - -1. Add two event handler methods, and map host element events to them via the `host` property. - - - -Subscribe to events of the DOM element that hosts an attribute directive (the `

    ` in this case) by configuring event listeners on the directive's [`host` property](guide/components/host-elements#binding-to-the-host-element). - -HELPFUL: The handlers delegate to a helper method, `highlight()`, that sets the color on the host DOM element, `el`. - -The complete directive is as follows: +To respond to user interaction, bind host element events to handler methods through the `host` property of the `@Directive()` decorator. The following directive highlights the host element while the pointer is over it and clears the highlight when the pointer leaves: -The background color appears when the pointer hovers over the paragraph element and disappears as the pointer moves out. - -Second Highlight - -## Passing values into an attribute directive - -This section walks you through setting the highlight color while applying the `HighlightDirective`. - -1. In `highlight.directive.ts`, import `Input` from `@angular/core`. - - - -2. Add an `appHighlight` `input` property. - - - - The `input()` function adds metadata to the class that makes the directive's `appHighlight` property available for binding. +The `host` property maps the `mouseenter` and `mouseleave` events to the `onMouseEnter()` and `onMouseLeave()` methods, which delegate to a `highlight()` helper that sets the background color on the host element. For more on host event bindings, see [binding to the host element](guide/components/host-elements#binding-to-the-host-element). -3. In `app.component.ts`, add a `color` property to the `AppComponent`. +## Accepting input values - +Like components, directives accept inputs through the [`input()`](guide/components/inputs) function. Give an input the same name as the selector so that a single binding both applies the directive and passes a value to it: -4. To simultaneously apply the directive and the color, use property binding with the `appHighlight` directive selector, setting it equal to `color`. + - +Read the input by calling it as a signal, and fall back to a default when no color is set: - The `[appHighlight]` attribute binding performs two tasks: - - Applies the highlighting directive to the `

    ` element - - Sets the directive's highlight color with a property binding + -### Setting the value with user input +In the template, bind the value to the selector. Because the input shares the selector's name, `[appHighlight]` both applies the directive and sets its value. Here the bound `color` is a property on the component: -This section guides you through adding radio buttons to bind your color choice to the `appHighlight` directive. + -1. Add markup to `app.component.html` for choosing a color as follows: + - +A directive can declare more than one input. The following directive adds a `defaultColor` input, then falls back through `appHighlight`, `defaultColor`, and finally `red`: -1. Revise the `AppComponent.color` so that it has no initial value. + - +Bind both inputs on the same element. Because `defaultColor` takes a static string rather than a dynamic expression, it doesn't need square brackets: -1. In `highlight.directive.ts`, revise `onMouseEnter` method so that it first tries to highlight with `appHighlight` and falls back to `red` if `appHighlight` is `undefined`. - - - -1. Serve your application to verify that the user can choose the color with the radio buttons. - -Animated gif of the refactored highlight directive changing color according to the radio button the user selects - -## Binding to a second property - -This section guides you through configuring your application so the developer can set the default color. - -1. Add a second `input()` property to `HighlightDirective` called `defaultColor`. - - - -1. Revise the directive's `onMouseEnter` so that it first tries to highlight with the `appHighlight`, then with the `defaultColor`, and falls back to `red` if both properties are `undefined`. - - - -1. To bind to the `AppComponent.color` and fall back to "violet" as the default color, add the following HTML. - In this case, the `defaultColor` binding doesn't use square brackets, `[]`, because it is static. - - - - As with components, you can add multiple directive property bindings to a host element. - -The default color is red if there is no default color binding. -When the user chooses a color the selected color becomes the active highlight color. - -Animated gif of final highlight directive that shows red color with no binding and violet with the default color set. When user selects color, the selection takes precedence. + ## Deactivating Angular processing with `NgNonBindable` @@ -140,12 +84,19 @@ To prevent expression evaluation in the browser, add `ngNonBindable` to the host In the following example, the expression `{{ 1 + 1 }}` renders just as it does in your code editor, and does not display `2`. - + Applying `ngNonBindable` to an element stops binding for that element's child elements. However, `ngNonBindable` still lets directives work on the element where you apply `ngNonBindable`. In the following example, the `appHighlight` directive is still active but Angular does not evaluate the expression `{{ 1 + 1 }}`. - + If you apply `ngNonBindable` to a parent element, Angular disables interpolation and binding of any sort, such as property binding or event binding, for the element's children. + +## What's next + + + + + diff --git a/adev-es/src/content/guide/directives/attribute-directives.md b/adev-es/src/content/guide/directives/attribute-directives.md index e782f2b..241d8aa 100644 --- a/adev-es/src/content/guide/directives/attribute-directives.md +++ b/adev-es/src/content/guide/directives/attribute-directives.md @@ -2,6 +2,16 @@ Cambia la apariencia o comportamiento de elementos DOM y componentes Angular con directivas de atributo. +## Usar bindings de plantilla para comportamiento puntual + +La sintaxis de plantilla de Angular ya cubre cambiar las clases, estilos, propiedades y eventos de un solo elemento: + +- Los [bindings de clase y estilo](guide/templates/binding#css-class-and-style-property-bindings) agregan y eliminan clases CSS y estilos en línea. +- Los [bindings de propiedad y atributo](guide/templates/binding) establecen propiedades DOM y atributos HTML. +- Los [escuchadores de eventos](guide/templates/event-listeners) responden a la interacción del usuario. + +Las directivas de atributo son útiles cuando quieres empaquetar este tipo de comportamiento en una unidad reutilizable que puedes aplicar a cualquier elemento o componente. + ## Construyendo una directiva de atributo Esta sección te guía a través de la creación de una directiva de resaltado que establece el color de fondo del elemento host en amarillo. @@ -149,3 +159,10 @@ En el siguiente ejemplo, la directiva `appHighlight` sigue activa pero Angular n Si aplicas `ngNonBindable` a un elemento padre, Angular desactiva la interpolación y el enlace de cualquier tipo, como enlace de propiedad o enlace de evento, para los hijos del elemento. + +## Siguientes pasos + + + + + diff --git a/adev-es/src/content/guide/directives/directive-composition-api.en.md b/adev-es/src/content/guide/directives/directive-composition-api.en.md index 53694c0..47b132a 100644 --- a/adev-es/src/content/guide/directives/directive-composition-api.en.md +++ b/adev-es/src/content/guide/directives/directive-composition-api.en.md @@ -1,137 +1,122 @@ # Directive composition API -Angular directives offer a great way to encapsulate reusable behaviors— directives can apply -attributes, CSS classes, and event listeners to an element. +Angular directives offer a great way to encapsulate reusable behaviors. Directives can apply attributes, CSS classes, and event listeners to an element. -The _directive composition API_ lets you apply directives to a component's host element from -_within_ the component TypeScript class. +The _directive composition API_ lets you apply directives to a component's host element from _within_ the component TypeScript class. ## Adding directives to a component -You apply directives to a component by adding a `hostDirectives` property to a component's -decorator. We call such directives _host directives_. +You apply directives to a component by adding a `hostDirectives` property to a component's decorator. Such directives are called _host directives_. -In this example, we apply the directive `MenuBehavior` to the host element of `AdminMenu`. This -works similarly to applying the `MenuBehavior` to the `` element in a template. +In this example, the `MenuBehavior` directive is applied to the host element of `AdminMenu`. This works similarly to applying the `MenuBehavior` to the `` element in a template. -```typescript +```ts @Component({ selector: 'admin-menu', - template: 'admin-menu.html', + templateUrl: './admin-menu.html', hostDirectives: [MenuBehavior], }) -export class AdminMenu { } +export class AdminMenu {} ``` -When the framework renders a component, Angular also creates an instance of each host directive. The -directives' host bindings apply to the component's host element. By default, host directive inputs -and outputs are not exposed as part of the component's public API. See -[Including inputs and outputs](#including-inputs-and-outputs) below for more information. +When the framework renders a component, Angular also creates an instance of each host directive. The directives' host bindings apply to the component's host element. By default, host directive inputs and outputs are not exposed as part of the component's public API. See [Including inputs and outputs](#including-inputs-and-outputs) below for more information. -**Angular applies host directives statically at compile time.** You cannot dynamically add -directives at runtime. +Host directives come with the following constraints: -**Directives used in `hostDirectives` may not specify `standalone: false`.** - -**Angular ignores the `selector` of directives applied in the `hostDirectives` property.** +- **Angular applies host directives statically at compile time.** You cannot dynamically add + directives at runtime. +- **Directives used in `hostDirectives` may not specify `standalone: false`.** +- **Angular ignores the `selector` of directives applied in the `hostDirectives` property.** ## Including inputs and outputs -When you apply `hostDirectives` to your component, the inputs and outputs from the host directives -are not included in your component's API by default. You can explicitly include inputs and outputs -in your component's API by expanding the entry in `hostDirectives`: +When you apply `hostDirectives` to your component, the inputs and outputs from the host directives are not included in your component's API by default. You can explicitly include inputs and outputs in your component's API by expanding the entry in `hostDirectives`: -```typescript +```ts @Component({ selector: 'admin-menu', - template: 'admin-menu.html', - hostDirectives: [{ - directive: MenuBehavior, - inputs: ['menuId'], - outputs: ['menuClosed'], - }], + templateUrl: './admin-menu.html', + hostDirectives: [ + { + directive: MenuBehavior, + inputs: ['menuId'], + outputs: ['menuClosed'], + }, + ], }) -export class AdminMenu { } +export class AdminMenu {} ``` -By explicitly specifying the inputs and outputs, consumers of the component with `hostDirective` can -bind them in a template: +By explicitly specifying the inputs and outputs, consumers of the component with `hostDirectives` can bind them in a template: ```angular-html - - + ``` -Furthermore, you can alias inputs and outputs from `hostDirective` to customize the API of your -component: +Furthermore, you can alias inputs and outputs from a host directive to customize the API of your component: -```typescript +```ts @Component({ selector: 'admin-menu', - template: 'admin-menu.html', - hostDirectives: [{ - directive: MenuBehavior, - inputs: ['menuId: id'], - outputs: ['menuClosed: closed'], - }], + templateUrl: './admin-menu.html', + hostDirectives: [ + { + directive: MenuBehavior, + inputs: ['menuId: id'], + outputs: ['menuClosed: closed'], + }, + ], }) -export class AdminMenu { } +export class AdminMenu {} ``` ```angular-html - - + ``` ## Adding directives to another directive -You can also add `hostDirectives` to other directives, in addition to components. This enables the -transitive aggregation of multiple behaviors. +You can also add `hostDirectives` to other directives, in addition to components. This enables the transitive aggregation of multiple behaviors. -In the following example, we define two directives, `Menu` and `Tooltip`. We then compose the behavior -of these two directives in `MenuWithTooltip`. Finally, we apply `MenuWithTooltip` -to `SpecializedMenuWithTooltip`. +The following example defines two directives, `Menu` and `Tooltip`, then composes their behavior in `MenuWithTooltip`. Finally, it applies `MenuWithTooltip` to `SpecializedMenuWithTooltip`. -When `SpecializedMenuWithTooltip` is used in a template, it creates instances of all of `Menu` -, `Tooltip`, and `MenuWithTooltip`. Each of these directives' host bindings apply to the host -element of `SpecializedMenuWithTooltip`. +When `SpecializedMenuWithTooltip` is used in a template, it creates instances of all of `Menu`, `Tooltip`, and `MenuWithTooltip`. Each of these directives' host bindings apply to the host element of `SpecializedMenuWithTooltip`. -```typescript -@Directive({...}) -export class Menu { } +```ts +@Directive({/* ... */}) +export class Menu {} -@Directive({...}) -export class Tooltip { } +@Directive({/* ... */}) +export class Tooltip {} // MenuWithTooltip can compose behaviors from multiple other directives @Directive({ hostDirectives: [Tooltip, Menu], }) -export class MenuWithTooltip { } +export class MenuWithTooltip {} // CustomWidget can apply the already-composed behaviors from MenuWithTooltip @Directive({ hostDirectives: [MenuWithTooltip], }) -export class SpecializedMenuWithTooltip { } +export class SpecializedMenuWithTooltip {} ``` ## Host directive semantics ### Directive execution order -Host directives go through the same lifecycle as components and directives used directly in a -template. However, host directives always execute their constructor, lifecycle hooks, and bindings _before_ the component or directive on which they are applied. +Host directives go through the same lifecycle as components and directives used directly in a template. However, host directives always execute their constructor, lifecycle hooks, and bindings _before_ the component or directive on which they are applied. The following example shows minimal use of a host directive: -```typescript +```ts @Component({ selector: 'admin-menu', - template: 'admin-menu.html', + templateUrl: './admin-menu.html', hostDirectives: [MenuBehavior], }) -export class AdminMenu { } +export class AdminMenu {} ``` The order of execution here is: @@ -143,13 +128,11 @@ The order of execution here is: 5. `MenuBehavior` applies host bindings 6. `AdminMenu` applies host bindings -This order of operations means that components with `hostDirectives` can override any host bindings -specified by a host directive. +This order of operations means that components with `hostDirectives` can override any host bindings specified by a host directive. -This order of operations extends to nested chains of host directives, as shown in the following -example. +This order of operations extends to nested chains of host directives, as shown in the following example. -```typescript +```ts @Directive({...}) export class Tooltip { } @@ -178,12 +161,113 @@ In the example above, the order of execution is: ### Dependency injection -A component or directive that specifies `hostDirectives` can inject the instances of those host -directives and vice versa. +A component or directive that specifies `hostDirectives` can inject the instances of those host directives and vice versa. + +When applying host directives to a component, both the component and host directives can define providers. + +If a component or directive with `hostDirectives` and those host directives both provide the same injection token, the providers defined by class with `hostDirectives` take precedence over providers defined by the host directives. + +### Host directive de-duplication + +When the same directive appears more than once in the resolved host directive tree, it is automatically de-duplicated rather than throwing an error. Two deterministic rules are used to decide which match survives. + +#### Template match takes precedence + +If a directive matches an element once through a **template selector** and also appears as a **host directive**, Angular keeps only the template match and discards all host directive matches. + +The mental model is that a host directive match represents `Partial`, a partial application where only the inputs and outputs explicitly listed in `hostDirectives` are exposed, while a template match represents the full directive with its complete public API. + +@Directive({selector: '[hoverable]'}) +export class Hoverable {} + +```ts +@Component({ + selector: 'app-button', + hostDirectives: [Hoverable], +}) +export class Button {} +``` + +```angular-html + + + +``` + +#### Multiple host directive matches are merged + +If the same directive appears **more than once as a host directive**, for example, when two directives both declare a common dependency in their `hostDirectives`, Angular merges all instances into a single directive instance. The input and output mappings from all instances are combined. + +This resolves the classic [diamond problem](https://en.wikipedia.org/wiki/Multiple_inheritance#The_diamond_problem) in host directive composition: + +```ts +// A shared behavior that both triggers need +@Directive({ + host: { + '[attr.data-trigger-id]': 'triggerId()', + }, +}) +export class TriggerRef { + readonly triggerId = input(`trigger-${crypto.randomUUID()}`); +} + +// Two separate triggers, each declaring TriggerRef as a host directive +@Directive({ + selector: '[popoverTrigger]', + hostDirectives: [TriggerRef], +}) +export class PopoverTrigger { + readonly triggerRef = inject(TriggerRef); +} + +@Directive({ + selector: '[dropdownTrigger]', + hostDirectives: [TriggerRef], +}) +export class DropdownTrigger { + readonly triggerRef = inject(TriggerRef); +} +``` + +```angular-html + + +``` + +HELPFUL: Because Angular produces only one instance of the shared directive, both `PopoverTrigger` and `DropdownTrigger` receive the same `TriggerRef` instance when they inject it. + +#### Conflicting aliases + +When Angular merges duplicate host directive matches it also merges their input and output mappings. + +If two instances of the same host directive expose the **same input or output under different aliases**, Angular throws an error at compile time ([NG8024](errors/NG8024)). + +```ts +@Directive({ + selector: '[popoverTrigger]', + hostDirectives: [{directive: TriggerRef, inputs: ['triggerId: popoverTriggerId']}], +}) +export class PopoverTrigger {} + +@Directive({ + selector: '[dropdownTrigger]', + hostDirectives: [ + {directive: TriggerRef, inputs: ['triggerId: dropdownTriggerId']}, // different alias! + ], +}) +export class DropdownTrigger {} +``` + +```angular-html + + +``` + +To resolve this, ensure that both paths expose the shared input or output under the same alias, or do not expose it at all. -When applying host directives to a component, both the component and host directives can define -providers. +## What's next -If a component or directive with `hostDirectives` and those host directives both provide the same -injection token, the providers defined by class with `hostDirectives` take precedence over providers -defined by the host directives. + + + + diff --git a/adev-es/src/content/guide/directives/directive-composition-api.md b/adev-es/src/content/guide/directives/directive-composition-api.md index 992c64f..7bae6b1 100644 --- a/adev-es/src/content/guide/directives/directive-composition-api.md +++ b/adev-es/src/content/guide/directives/directive-composition-api.md @@ -187,3 +187,108 @@ pueden definir providers. Si un componente o directiva con `hostDirectives` y esas directivas host proveen el mismo token de inyección, los providers definidos en la clase con `hostDirectives` tienen precedencia sobre los providers definidos en las directivas host. + +### Deduplicación de directivas host + +Cuando la misma directiva aparece más de una vez en el árbol de directivas host resuelto, se deduplica automáticamente en lugar de lanzar un error. Se usan dos reglas deterministas para decidir qué coincidencia prevalece. + +#### La coincidencia de plantilla tiene precedencia + +Si una directiva coincide con un elemento una vez a través de un **selector de plantilla** y también aparece como **directiva host**, Angular conserva solo la coincidencia de plantilla y descarta todas las coincidencias de directiva host. + +El modelo mental es que una coincidencia de directiva host representa `Partial`, una aplicación parcial donde solo los inputs y outputs explícitamente enumerados en `hostDirectives` están expuestos, mientras que una coincidencia de plantilla representa la directiva completa con su API pública completa. + +@Directive({selector: '[hoverable]'}) +export class Hoverable {} + +```ts +@Component({ + selector: 'app-button', + hostDirectives: [Hoverable], +}) +export class Button {} +``` + +```angular-html + + + +``` + +#### Las coincidencias múltiples de directiva host se fusionan + +Si la misma directiva aparece **más de una vez como directiva host**, por ejemplo, cuando dos directivas declaran una dependencia común en sus `hostDirectives`, Angular fusiona todas las instancias en una sola instancia de directiva. Los mapeos de input y output de todas las instancias se combinan. + +Esto resuelve el clásico [problema del diamante](https://en.wikipedia.org/wiki/Multiple_inheritance#The_diamond_problem) en la composición de directivas host: + +```ts +// Un comportamiento compartido que ambos triggers necesitan +@Directive({ + host: { + '[attr.data-trigger-id]': 'triggerId()', + }, +}) +export class TriggerRef { + readonly triggerId = input(`trigger-${crypto.randomUUID()}`); +} + +// Dos triggers separados, cada uno declarando TriggerRef como directiva host +@Directive({ + selector: '[popoverTrigger]', + hostDirectives: [TriggerRef], +}) +export class PopoverTrigger { + readonly triggerRef = inject(TriggerRef); +} + +@Directive({ + selector: '[dropdownTrigger]', + hostDirectives: [TriggerRef], +}) +export class DropdownTrigger { + readonly triggerRef = inject(TriggerRef); +} +``` + +```angular-html + + +``` + +HELPFUL: Dado que Angular produce solo una instancia de la directiva compartida, tanto `PopoverTrigger` como `DropdownTrigger` reciben la misma instancia de `TriggerRef` cuando la inyectan. + +#### Alias en conflicto + +Cuando Angular fusiona coincidencias de directiva host duplicadas, también fusiona sus mapeos de input y output. + +Si dos instancias de la misma directiva host exponen el **mismo input u output bajo alias diferentes**, Angular lanza un error en tiempo de compilación ([NG8024](errors/NG8024)). + +```ts +@Directive({ + selector: '[popoverTrigger]', + hostDirectives: [{directive: TriggerRef, inputs: ['triggerId: popoverTriggerId']}], +}) +export class PopoverTrigger {} + +@Directive({ + selector: '[dropdownTrigger]', + hostDirectives: [ + {directive: TriggerRef, inputs: ['triggerId: dropdownTriggerId']}, // ¡alias diferente! + ], +}) +export class DropdownTrigger {} +``` + +```angular-html + + +``` + +Para resolver esto, asegúrate de que ambas rutas expongan el input u output compartido bajo el mismo alias, o no lo expongan en absoluto. + +## Siguientes pasos + + + + + diff --git a/adev-es/src/content/guide/directives/overview.en.md b/adev-es/src/content/guide/directives/overview.en.md index 2ea7047..0a9050d 100644 --- a/adev-es/src/content/guide/directives/overview.en.md +++ b/adev-es/src/content/guide/directives/overview.en.md @@ -1,124 +1,69 @@ - -Directives are classes that add additional behavior to elements in your Angular applications. + +Directives add behavior to elements and components in your Angular applications. -Use Angular's built-in directives to manage forms, lists, styles, and what users see. +A directive can change how an element looks, how it behaves, or how it fits into the DOM. Angular ships with several built-in directives, and you can write your own. -The different types of Angular directives are as follows: +## When to use a 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. | +Directives are most effective when they encapsulate **reusable** behavior that you want to apply to an existing element or component. -This guide covers built-in [attribute directives](#built-in-attribute-directives). +Common examples include: -## Built-in attribute directives +- Applying the same appearance or behavior across many elements, such as autofocus or a tooltip. +- Reading from or writing to the host element's DOM, attributes, or classes. +- Adding behavior to a component you don't own without changing its source. -Attribute directives listen to and modify the behavior of other HTML elements, attributes, properties, and components. +If you need to render your own markup or manage a piece of UI with its own template, reach for a [component](guide/components), a specialized directive with its own template. -The most common attribute directives are as follows: +## A quick example -| 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. | +Suppose you want elements to highlight when the user hovers over them with a mouse, changing their background color to yellow. Rather than repeat the same event-handling logic on every element, you can package that behavior in a directive and apply it wherever you need it. -HELPFUL: Built-in directives use only public APIs. They do not have special access to any private APIs that other directives can't access. +The following `appHighlight` directive sets the host element's background color when the mouse enters and clears it when the mouse leaves: -## Adding and removing classes with `NgClass` +```ts +import {Directive, signal} from '@angular/core'; -Add or remove multiple CSS classes simultaneously with `ngClass`. +@Directive({ + selector: '[appHighlight]', + host: { + '(mouseenter)': 'isHovered.set(true)', + '(mouseleave)': 'isHovered.set(false)', + '[style.background-color]': 'isHovered() ? "yellow" : null', + }, +}) +export class HighlightDirective { + protected isHovered = signal(false); +} +``` -HELPFUL: To add or remove a _single_ class, use [class binding](guide/templates/class-binding) rather than `NgClass`. +The `host` metadata listens for mouse events to update the `isHovered` signal, and binds the host element's `background-color` style to the signal's value. -### Import `NgClass` in the component +Apply the directive by adding its selector as an attribute on an element: -To use `NgClass`, add it to the component's `imports` list. +```angular-html +

    Highlight me!

    +``` - +Every element that carries the `appHighlight` attribute gains the same hover behavior, with the logic defined in one place. -### Using `NgClass` with an expression +## Types of directives -On the element you'd like to style, add `[ngClass]` and set it equal to an expression. -In this case, `isSpecial` is a boolean set to `true` in `app.component.ts`. -Because `isSpecial` is true, `ngClass` applies the class of `special` to the `
    `. +Angular has three primary types of directives: - - -### Using `NgClass` with a method - -1. To use `NgClass` with a method, add the method to the component class. - In the following example, `setCurrentClasses()` sets the property `currentClasses` with an object that adds or removes three classes based on the `true` or `false` state of three other component properties. - - Each key of the object is a CSS class name. - If a key is `true`, `ngClass` adds the class. - If a key is `false`, `ngClass` removes the class. - - - -1. In the template, add the `ngClass` property binding to `currentClasses` to set the element's classes: - - - -For this use case, Angular applies the classes on initialization and in case of changes caused by reassigning the `currentClasses` object. -The full example calls `setCurrentClasses()` initially with `ngOnInit()` when the user clicks on the `Refresh currentClasses` button. -These steps are not necessary to implement `ngClass`. - -## Setting inline styles with `NgStyle` - -HELPFUL: To add or remove a _single_ style, use [style bindings](guide/templates/binding#css-class-and-style-property-bindings) rather than `NgStyle`. - -### Import `NgStyle` in the component - -To use `NgStyle`, add it to the component's `imports` list. - - - -Use `NgStyle` to set multiple inline styles simultaneously, based on the state of the component. - -1. To use `NgStyle`, add a method to the component class. - - In the following example, `setCurrentStyles()` sets the property `currentStyles` with an object that defines three styles, based on the state of three other component properties. - - - -1. To set the element's styles, add an `ngStyle` property binding to `currentStyles`. - - - -For this use case, Angular applies the styles upon initialization and in case of changes. -To do this, the full example calls `setCurrentStyles()` initially with `ngOnInit()` and when the dependent properties change through a button click. -However, these steps are not necessary to implement `ngStyle` on its own. - -## Hosting a directive without a DOM element - -The Angular `` is a grouping element that doesn't interfere with styles or layout because Angular doesn't put it in the DOM. - -Use `` when there's no single element to host the directive. - -Here's a conditional paragraph using ``. - - - -ngcontainer paragraph with proper style - -1. Import the `ngModel` directive from `FormsModule`. - -1. Add `FormsModule` to the imports section of the relevant Angular module. - -1. To conditionally exclude an `
    Cell 1 Cell 2