Inicio / Artículos / Componentes Angular desde cero: selectores, plantillas, entradas y salidas

Componentes Angular desde cero: selectores, plantillas, entradas y salidas

Un recorrido práctico por los componentes independientes modernos de Angular: estructura de archivos, el decorador, selectores, plantillas, estilos limitados y el flujo de datos entre componentes padre e hijo mediante input() y output().

2504 palabras

Una vez que se crea y ejecuta un proyecto Angular, la siguiente pregunta es cómo se construye la propia pantalla. La respuesta son los componentes: unidades pequeñas y autónomas que cada una gestiona una parte de la interfaz. Esta guía explica de qué está compuesto un componente, cómo los componentes independientes cambian la situación en comparación con el código anterior basado en NgModule, y cómo un componente padre y uno hijo intercambian datos y eventos utilizando las API actuales input() y output(). Al final, deberías poder generar un componente, integrarlo en una página y transmitir información en ambas direcciones.

Si aún necesitas un proyecto para experimentar, la guía sobre configurar un proyecto moderno de Angular 22 abarca la configuración y la estructura de carpetas que este artículo asume.

Por qué Angular divide la interfaz de usuario en componentes

Imagínese una página web normal: una barra de navegación en la parte superior, un panel lateral, algún contenido del producto en el centro y un pie de página abajo. Se podría colocar todo ese marcado, estilo y comportamiento en un único archivo enorme, pero rápidamente se volvería difícil de leer y aún más complicado de modificar. Angular fomenta dividir la página en partes, cada una con una función específica.

Angular Application
│
├── Navbar Component
├── Sidebar Component
├── Product Component
├── Login Component
└── Footer Component

Un componente generalmente incluye cuatro elementos: una clase de TypeScript que almacena el estado y el comportamiento, un template HTML que describe la estructura del contenido, estilos para su apariencia y metadatos que indican a Angular cómo tratarlo. Dado que cada componente tiene una responsabilidad clara, se puede analizar la barra de navegación sin tener en cuenta el pie de página, reutilizar la misma tarjeta de producto en varios lugares y probar partes de la interfaz de usuario de forma aislada. Un formulario de inicio de sesión, un panel de perfil o un widget de panel de control son ejemplos adecuados.

Los componentes independientes son la opción por defecto

Muchos tutoriales de Angular, especialmente los más antiguos, organizan los componentes dentro de un NgModule, el cual declara los componentes y enumera de qué dependen. Angular moderno ha abandonado ese modelo. Ahora, por defecto, los componentes son independientes, por lo que un componente puede funcionar por sí solo sin necesidad de estar registrado en un módulo. Un componente mínimo se ve así:

import { Component } from '@angular/core';
@Component({
  selector: 'app-welcome',
  templateUrl: './welcome.html',
  styleUrl: './welcome.css'
})
export class WelcomeComponent {
}

Podría esperarse ver una marca que indique que el componente es independiente, como la línea siguiente, pero ya no es necesaria:

standalone: true

Omitirlo tiene el mismo efecto, ya que el comportamiento por defecto es independiente. Cuando un componente independiente necesita otro componente, una directiva o un pipe, enumera esa dependencia en el array imports de su propia configuración @Component. La ventaja es la localización: se puede abrir un único archivo para ver exactamente de qué depende ese componente, en lugar de buscar entre las definiciones de módulos en otras partes del proyecto.

Cómo se organiza un componente en el disco

Un componente típico reside en su propia carpeta con tres archivos uno al lado del otro:

welcome/
│
├── welcome.ts
├── welcome.html
└── welcome.css

El archivo .ts contiene la lógica, el archivo .html contiene el marcado y el archivo .css contiene los estilos. A continuación se muestra qué podría contener cada uno para un componente de bienvenida simple. Primero la clase, que expone una propiedad title al template:

import { Component } from '@angular/core';
@Component({
  selector: 'app-welcome',
  templateUrl: './welcome.html',
  styleUrl: './welcome.css'
})
export class WelcomeComponent {
  title = 'Welcome to Angular 22';
}

La plantilla lee esa propiedad entre llaves dobles:

<h1>{{ title }}</h1>
<p>
  This is my first Angular component.
</p>

Y la hoja de estilos se aplica únicamente a los elementos de esa plantilla:

h1 {
  color: #dd0031;
}
p {
  font-size: 18px;
}

El decorador @Component() es el elemento que une todo. Indica a Angular dónde se encuentran la plantilla y la hoja de estilos, de modo que los tres archivos funcionen como una sola unidad, tal como muestra el siguiente esquema:

welcome.ts
    │
    ├── Logic
    │
    ├── welcome.html
    │      ↓
    │    UI
    │
    └── welcome.css
           ↓
         Style

La guía de estilo de Angular recomienda mantener archivos estrechamente relacionados como estos en el mismo directorio, y eso es exactamente lo que hace esta estructura. Cuando un componente crece, se sabe con precisión dónde se encuentran su marcado, sus estilos y su lógica.

Qué le indica el decorador @Component() a Angular

El decorador agrega metadatos a la clase. Esos metadatos son los que permiten a Angular saber para qué sirve la clase y cómo renderizarla:

@Component({
  selector: 'app-welcome',
  templateUrl: './welcome.html',
  styleUrl: './welcome.css'
})

Tres propiedades realizan la mayor parte del trabajo aquí:

  • selector define el nombre de la etiqueta que se utiliza para colocar el componente en otras plantillas.
  • templateUrl apunta al archivo HTML que contiene el marcado del componente.
  • styleUrl apunta a la hoja de estilo del componente.

Existen otras opciones, como imports para incorporar otros componentes o características de Angular, así como alternativas integradas a templateUrl y styleUrl que se explican más adelante en este artículo. No es necesario aprenderlas todas de inmediato; se irán familiarizando a medida que construyan más componentes.

Los selectores convierten una clase en un elemento personalizado

El selector es el identificador que Angular busca dentro de las plantillas. En esta configuración está establecido en app-welcome:

@Component({
  selector: 'app-welcome',
  ...
})

Por lo tanto, el valor del selector es simplemente:

app-welcome

Dondequiera que desee que aparezca el componente, escribe ese nombre como si fuera una etiqueta HTML:

<app-welcome></app-welcome>

Cuando Angular encuentra <app-welcome> al renderizar una plantilla, crea una instancia del componente correspondiente y renderiza su plantilla dentro de ese elemento. El elemento que coincide con el selector se denomina elemento anfitrión del componente, y permanece en el DOM, lo cual es importante más adelante cuando desee aplicar estilos o comportamientos al componente en su totalidad.

Angular CLI genera selectores con el prefijo configurado para su aplicación, generalmente app-, aunque puede elegir un selector diferente al generar un componente. El prefijo no es solo estético: los nombres de los elementos personalizados deben contener un guion, y un prefijo específico del proyecto evita que sus componentes entren en conflicto con elementos nativos o con etiquetas de bibliotecas de terceros.

Las plantillas conectan el marcado con los datos del componente

La plantilla define lo que el usuario ve realmente. En su forma más simple, se trata de HTML puro:

<h1>Welcome!</h1>
<p>We are learning Angular components.</p>

Lo que hace que las plantillas de Angular sean más potentes que el HTML estático es que pueden leer datos y llamar a comportamientos definidos en la clase del componente. Supongamos que la clase declara una propiedad:

name = 'Mubbassir';

La plantilla puede entonces mostrarla mediante interpolación:

<h1>Hello {{ name }}!</h1>

Angular mantiene la salida renderizada en sincronía con esos datos: cuando el valor vinculado cambia, la página se actualiza sin que sea necesario modificar el DOM manualmente. Esta relación entre la clase de TypeScript y su plantilla es la base sobre la cual se construye todo lo demás en Angular, desde el manejo de eventos hasta los formularios.

Los estilos de los componentes permanecen limitados por defecto

Cada componente puede tener sus propios estilos:

.card {
  padding: 20px;
  border-radius: 10px;
}
h2 {
  color: #dd0031;
}

Por defecto, Angular aplica una encapsulación de vista emulada a estas reglas. En el momento de la compilación, reescribe los selectores y agrega atributos generados a los elementos del componente, de modo que una regla como h2 { ... } solo coincida con los elementos h2 de la plantilla de este componente y no con todos los encabezados de la aplicación. En una app con cientos de componentes, esta protección evita que un cambio en los estilos en un lugar afecte inadvertidamente el aspecto de otro.

La encapsulación no excluye el uso compartido de estilos. Las reglas que deben aplicarse en todas partes, como la tipografía, los valores por defecto o las herramientas de maquetación, deben encontrarse en la hoja de estilo global de la aplicación, mientras que los archivos de componentes albergan los estilos específicos de ese componente.

Generación de un componente con Angular CLI

Crear los archivos a mano funciona, pero la CLI lo hace más rápido y con una nomenclatura consistente. Desde la raíz del proyecto, ejecute:

ng generate component user-card

o la forma abreviada:

ng g c user-card

El generador crea una carpeta como esta (el = extra después del nombre del archivo de especificaciones es un error tipográfico en la lista, no forma parte del nombre):

user-card/
├── user-card.ts
├── user-card.html
├── user-card.css
└── user-card.spec.ts=

El estilo más reciente para nombrar archivos es más breve que la convención anterior de user-card.component.ts. La creación de un archivo de pruebas .spec.ts depende de la configuración de pruebas del proyecto. Las versiones recientes de la CLI que siguen la guía de estilo actualizada también tienden a generar nombres de clase sin el sufijo Component (por ejemplo, UserCard); los ejemplos a continuación mantienen el sufijo, y cualquiera de los nombres funciona siempre que las importaciones coincidan.

Cómo se comunican entre sí los componentes padre e hijo

Las pantallas reales son árboles de componentes, y necesitan compartir información. Considere un componente padre que conoce los datos de un usuario y un componente hijo cuya función es mostrar a ese usuario. El padre necesita una forma de pasar datos hacia abajo, y el hijo necesita una forma de informar cuando ocurre algo, como un clic en un botón.

Angular maneja la dirección hacia abajo con los inputs y la dirección hacia arriba con los outputs:

Parent Component
      │
      │  Data
      │
      ▼
Child Component
      │
      │  Event
      │
      ▼
Parent Component

Para código nuevo, Angular recomienda la función input() basada en señales y la función output(). Las API @Input() y @Output() basadas en decoradores siguen teniendo soporte total, por lo que aún se encontrarán en bases de código existentes.

Transmisión de datos hacia abajo con input()

Comience con la clase del componente hijo:

import { Component, input } from '@angular/core';
@Component({
  selector: 'app-user-card',
  templateUrl: './user-card.html',
  styleUrl: './user-card.css'
})
export class UserCardComponent {
  name = input.required<string>();
}

La línea importante es la declaración de input:

name = input.required<string>();

Declara que UserCardComponent espera una cadena name por parte de quien lo utilice. Al usar input.required(), esa expectativa se vuelve estricta: si un padre olvida vincular name, Angular reporta un error en lugar de renderizar silenciosamente un valor vacío. Para campos opcionales, input() por sí solo acepta un valor predeterminado.

Dado que name es una señal y no una propiedad común, se lee llamándola. En la clase se escribe:

this.name()

y en el template:

{{ name() }}

Olvidarse de los paréntesis es un error común al principio; sin ellos, el template renderiza la función de señal en sí en lugar de su valor. El template del hijo utiliza el campo de entrada de esta manera:

<div class="card">
  <h2>{{ name() }}</h2>
  <p>Welcome to Angular!</p>
</div>

En el lado del padre, se vincula un valor al campo de entrada al insertar al hijo:

<app-user-card [name]="userName" />

Si la clase padre contiene:

userName = 'Yuvaraj';

el hijo recibe esa cadena y la muestra:

Yuvaraj

Los corchetes son lo que convierten esto en un enlace de propiedad:

[name]="userName"

Indican a Angular que evalúe userName como una expresión en el padre y que proporcione el resultado al campo name del hijo. Sin los corchetes, Angular pasaría en su lugar el texto literal userName. Dado que el campo es una señal, el hijo se vuelve a renderizar automáticamente cada vez que cambia el valor del padre.

Envío de eventos hacia arriba con output()

Ahora demos al hijo un botón y permitamos que el padre sepa cuándo se selecciona un usuario. El hijo declara una salida y un método que la utiliza para emitir eventos:

import { Component, input, output } from '@angular/core';
@Component({
  selector: 'app-user-card',
  templateUrl: './user-card.html',
  styleUrl: './user-card.css'
})
export class UserCardComponent {
  name = input.required<string>();
  selected = output<string>();
  selectUser() {
    this.selected.emit(this.name());
  }
}

La declaración de salida define un evento personalizado que transporta una cadena:

selected = output<string>();

La llamada a emit dispara ese evento y pasa el nombre actual como su carga:

this.selected.emit(this.name());

La plantilla del hijo llama a selectUser() cuando se hace clic en el botón. Observe la sintaxis (click), que corresponde al enlace de eventos de Angular:

<div class="card">
 <h2>{{ name() }}</h2>
  <button (click)="selectUser()">
    Select User
  </button>
</div>

El padre escucha el evento personalizado utilizando la misma sintaxis entre paréntesis, esta vez con el nombre del resultado:

<app-user-card
  [name]="userName"
  (selected)="userSelected($event)"
/>

Los corchetes redondos significan “escuchar este evento”, y $event almacena el valor que emitió el hijo; en este caso, el nombre del usuario. El padre lo maneja en un método normal:

userSelected(name: string) {
  console.log('Selected user:', name);
}

Esta división del trabajo mantiene al hijo reutilizable. No sabe ni le importa qué hace el padre con una selección; simplemente anuncia que ocurrió una. El padre decide si registrarla, navegar o actualizar su propio estado.

Colocar un componente en la página

El ejemplo final une todos los elementos. Esta versión del componente de bienvenida utiliza template e styles en línea dentro del decorador en lugar de archivos separados, lo cual es útil para componentes muy pequeños:

import { Component } from '@angular/core';

@Component({
  selector: 'app-welcome',
  template: `
    <div class="welcome-card">
      <h2>Welcome Component</h2>
      <p>This component was created using the <strong>app-welcome</strong> selector.</p>
    </div>
  `,
  styles: `
    .welcome-card {
      padding: 25px;
      margin-top: 20px;
      border-radius: 12px;
      background: #f5f5f5;
      text-align: center;
      font-family: Arial, sans-serif;
    }
    h2 {
      color: #dd0031;
    }
    p {
      font-size: 18px;
    }
  `
})
export class WelcomeComponent {}

El componente raíz importa WelcomeComponent en su array imports, y eso es lo que hace que el selector app-welcome sea usable en su plantilla. Omitirlo es una de las causas más frecuentes del error “no es un elemento conocido” en aplicaciones independientes:

import { Component } from '@angular/core';
import { WelcomeComponent } from './welcome';

@Component({
  selector: 'app-root',
  imports: [WelcomeComponent],
  templateUrl: './app.html',
  styleUrl: './app.css'
})
export class App {}

Finalmente, la plantilla raíz coloca el componente utilizando su selector:

<div class="container">
<h1>Angular Components</h1>
  <p>
    Below is our custom Angular component:
  </p>
  <app-welcome></app-welcome>
</div>

Los templates incrustados mantienen todo en un único archivo, pero se vuelven poco prácticos cuando el marcado supera unas pocas líneas. Una regla razonable es comenzar con templates incrustados para pequeñas partes presentacionales y pasar a archivos separados .html y .css en cuanto el template necesite una estructura real.

Puntos clave

  • Un componente combina una clase de TypeScript, un template y estilos opcionales, unidos por el decorador @Component().
  • Standalone es la opción predeterminada en Angular moderno; se declaran las dependencias en el array imports de cada componente en lugar de en un NgModule.
  • El selector es la etiqueta personalizada que se utiliza para colocar un componente, y el elemento correspondiente se convierte en su anfitrión.
  • Los templates se vinculan a los datos de la clase, y Angular actualiza la vista cuando esos datos cambian.
  • Los estilos de los componentes se definen mediante una encapsulación emulada, mientras que las reglas aplicables a toda la aplicación se incluyen en la hoja de estilo global.
  • Utilice input() (o input.required()) para pasar datos hacia abajo y output() para enviar eventos hacia arriba; lea las entradas llamándolas como señales.
  • Visto de esta manera, una aplicación Angular es un árbol de componentes, cada uno responsable de su propia parte de la interfaz y comunicándose a través de entradas y salidas explícitas. Esa estructura es lo que mantiene comprensibles las aplicaciones Angular grandes a medida que crecen.