Herramientas de usuario

Herramientas del sitio


clase:daw:diw:angular-creacion-componentes

¡Esta es una revisión vieja del documento!


11. Angular: Creación de componentes

En este tema vamos a crear nuevos componentes de Angular

Crear tu nuevo componente botón

Vamos a crear nosotros un nuevo componente boton.

Para ello vamos a crear los 3 ficheros que necesitas todo componente:

  • boton/boton.ts: El código TypeScript que tiene la lógica del botón
  • boton/boton.html: El HTML correspondiente al botón
  • boton/boton.scss: Los estilos en SASS del botón. Por ahora es simplemente estilos CSS.

Los 3 ficheros los crearás dentro de la carpeta src/app/shared/ui/components.

Dónde va cada cosa

Angular no crea ninguna estructura de carpetas: la decides tú desde el principio. Nuestros componentes y layouts son reutilizables, así que van en shared, y las páginas en features, agrupadas por funcionalidad:

  • shared/ui/components: Los componentes visuales genéricos, como boton o panel.
  • shared/ui/layouts: Los layouts, los bloques l- de BEM. Solo colocan otros componentes dentro de ellos y no se ven.
  • features/<funcionalidad>/pages: Las páginas de cada funcionalidad de la aplicación.

src/
 └─ app/
     ├─ shared/
     │   └─ ui/
     │       ├─ components/
     │       │   ├─ boton/
     │       │   │   ├─ boton.ts
     │       │   │   ├─ boton.html
     │       │   │   └─ boton.scss
     │       │   └─ panel/
     │       └─ layouts/
     │           ├─ page/
     │           └─ horizontal/
     └─ features/
         └─ productos/
             ├─ components/
             │   └─ tarjeta-producto/
             └─ pages/
                 └─ productos/
                     ├─ productos.ts
                     ├─ productos.html
                     └─ productos.scss

No todo componente va en shared. Un panel genérico que recibe cualquier contenido va en shared/ui/components, pero la tarjeta que muestra la imagen, el nombre y el precio de un producto conoce los productos: va en features/productos/components.

Los ficheros del botón

El contenido de cada uno de los 3 ficheros será:

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

@Component({
  selector: 'boton',
  templateUrl: './boton.html',
  styleUrl: './boton.scss'
})
export class Boton {

}

La parte más importante es el objeto que se le pasa al decorador @Component. Veamos sus propiedades:

  • selector: Es la etiqueta que usaremos cuando queramos usar el componente. <boton></boton>
  • templateUrl: El nombre del fichero HTML
  • styleUrl: El nombre del fichero SCSS de estilos

<button class="c-boton">Aceptar</button>

.c-boton {
  font-family: sans-serif;
  font-size: 16px;
  padding: 6px;

  border-radius: 6px;
  border-width: 1px;
  border-style: solid;

  display: inline-block;
  cursor: pointer;
  text-decoration: none;


  border-color: #0056b8;
  background-color: #0056b8;
  color: #ffffff;
}

Usando un componente

Como ya hicimos en el tema Angular: Material, hay que importarlo en el .ts donde queramos usarlo y añadirlo al array de imports.

Primero lo importamos en el app.ts.

import { Component, signal } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import {MatButtonModule} from '@angular/material/button';
import { Boton } from './shared/ui/components/boton/boton';

@Component({
  selector: 'app-root',
  imports: [RouterOutlet, MatButtonModule,Boton],
  templateUrl: './app.html',
  styleUrl: './app.scss'
})
export class App {
  protected readonly title = signal('prueba');



}

Y ahora ya podemos usar la etiqueta <boton></boton> en el app.html

<style>
</style>

<h1 >Hola mundo</h1>

<button matButton="filled">
  Aceptar
</button>

<br>
<br>

<boton></boton>

<router-outlet />

Contenido de la etiqueta

Nuestros botones es muy pobre porque no permite indicar el texto del botón. Para poder hacerlo simplemente hay que usar la etiqueta <ng-content></ng-content> en el boton.html

<button class="c-boton">
  <ng-content></ng-content>
</button>

Y ahora ya podremos cambiar el texto:

<style>
</style>

<h1 >Hola mundo</h1>

<button matButton="filled">
  Aceptar
</button>

<br>
<br>

<boton>Hola mundo</boton>

<router-outlet />

Personalizar el componente: input()

Vamos ahora añadir atributos a nuestro componente para poder personalizarlo. Cada atributo es una propiedad de la clase TypeScript creada con la función input().

Nuestro objetivo es poder hacer lo siguiente:

<boton backgroundColor="#FF0000" color="#00FF00">Hola mundo</boton>

Lo primero es añadir las propiedades backgroundColor y color a la clase Boton.

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

@Component({
  selector: 'boton',
  templateUrl: './boton.html',
  styleUrl: './boton.scss'
})
export class Boton {
  readonly backgroundColor = input('#0B5CD5');
  readonly color = input('#F5F7FF');
}

  • input('#0B5CD5'): Crea el atributo con su valor por defecto. El tipo (string) lo deduce TypeScript del valor por defecto.
  • El valor se lee llamándolo como una función: color(). Es un signal de solo lectura: lo cambia quien usa el componente, no el propio componente.
  • readonly: La propiedad no se puede reasignar desde el componente (this.color = …). La guía de estilo de Angular lo pide para todo lo que crea input() y output().

Y en el boton.html se usan con [style]:

<button class="c-boton" [style.color]="color()" [style.background-color]="backgroundColor()">
  <ng-content></ng-content>
</button>

No olvides los paréntesis. Si escribes [style.color]="color" o {{color}}, Angular solo da un warning al compilar y en el HTML aparece el texto [Input Signal: #F5F7FF].

Al usar el componente, el atributo se puede escribir de dos formas:

  • color="#00FF00": Sin corchetes, el valor es un texto literal.
  • [porcentaje]="70": Con corchetes, el valor es una expresión de TypeScript. Hace falta para números, booleanos y para pasar datos del componente padre: [color]="colorElegido()".
porcentaje="70" pasa el texto "70", no el número 70, y da el error Type 'string' is not assignable to type 'number'. Si el atributo es un número o un booleano, siempre con corchetes.

Lo ideal es usar clases CSS y no style, pero entonces los atributos ya no pueden valer cualquier color: hay que limitar los valores. Es lo que vemos en el siguiente apartado.

Limitando los valores

En el apartado anterior hemos visto como definir los colores de los botones pero como vimos en Diseño Web: Sistema de diseño, realmente hay únicamente unos valores de colores que puede haber

Así que realmente lo que queremos es solo definir la función del botón. De forma que se use de la siguiente manera:

<boton funcion="peligrosa">Hola mundo</boton>

  • Por lo tanto el atributo del botón va a ser funcion con los posibles valores de 'normal' | 'alternativa' | 'peligrosa'. El tipo se indica entre <>:

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

@Component({
  selector: 'boton',
  templateUrl: './boton.html',
  styleUrl: './boton.scss'
})
export class Boton {
  readonly funcion = input<'normal' | 'alternativa' | 'peligrosa'>('normal');
}

  • Ahora hay que definir las clases CSS en el archivo boton.scss

.c-boton {
  font-family: sans-serif;
  font-size: 16px;
  padding: 6px;

  border-radius: 6px;
  border-width: 1px;
  border-style: solid;

  display: inline-block;
  cursor: pointer;
  text-decoration: none;


  border-color: #0056b8;
  background-color: #0056b8;
  color: #ffffff;
}

.funcion--normal {
  border-color: #0056b8;
  background-color: #0056b8;
  color: #ffffff;
}

.funcion--alternativa {
  border-color: #ed8936;
  background-color: #ed8936;
  color: #ffffff;
}

.funcion--peligrosa {
  border-color: #c53030;
  background-color: #c53030;
  color: #ffffff;
}


  • Y por último relacionamos la propiedad TypeScript funcion con las clases CSS en el HTML de la siguiente forma:

<button class="c-boton funcion--{{funcion()}}">
  <ng-content></ng-content>
</button>

Notar el truco de class="funcion--{{funcion()}}"

Acción de href

Por último vamos a añadir acciones al botón, como el href y el click

Para crear el atributo href hay que hacer 3 cambios:

  • Añadir la propiedad href a la clase Boton
  • Usar la propiedad en el HTML
  • Cambiar de tipo <button> a <a>

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

@Component({
  selector: 'boton',
  templateUrl: './boton.html',
  styleUrl: './boton.scss'
})
export class Boton {
  readonly funcion = input<'normal' | 'alternativa' | 'peligrosa'>('normal');
  readonly href = input('');
}

<a class="c-boton funcion--{{funcion()}}" [attr.href]="href() || null">
  <ng-content></ng-content>
</a>

Hay que destacar que además de usar ahora href también hemos cambiado la etiqueta a <a>.

Hemos puesto [attr.href]="href() || null" en vez de simplemente href="{{href()}}" porque así, si no hay nada en href, no se añade el atributo: con el valor null Angular quita el atributo.

Eventos: output()

Queremos que se ejecute la función alerta() de nuestra aplicación al pulsar en el botón.

import { Component, signal } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { Boton } from './shared/ui/components/boton/boton';

@Component({
  selector: 'app-root',
  imports: [RouterOutlet, MatButtonModule, Boton],
  templateUrl: './app.html',
  styleUrl: './app.scss'
})
export class App {
  protected readonly title = signal('prueba');

  alerta() : void {
    alert('Hola mundo');
  }

}

En el HTML lo indicaremos así

<style>
</style>

<h1 >Hola mundo</h1>

<button matButton="filled">
  Aceptar
</button>

<br>
<br>

<boton funcion="peligrosa" (pulsado)="alerta()">Hola mundo</boton>

<router-outlet />

Para ello tenemos que hacer 2 cosas en el botón:

  • Declarar el evento con la función output()
  • Emitir el evento con emit() cuando se haga click en el <a>

import { Component, input, output } from '@angular/core';

@Component({
  selector: 'boton',
  templateUrl: './boton.html',
  styleUrl: './boton.scss'
})
export class Boton {
  readonly funcion = input<'normal' | 'alternativa' | 'peligrosa'>('normal');
  readonly href = input('');
  readonly pulsado = output();
}

<a class="c-boton funcion--{{funcion()}}" [attr.href]="href() || null" (click)="pulsado.emit()">
  <ng-content></ng-content>
</a>

Si el evento lleva un dato, se indica su tipo y se pasa en emit(): output<number>() y this.cambiado.emit(70). Quien lo escucha lo recibe en $event: (cambiado)="guardar($event)".

No llames al evento click ni con el nombre de otro evento del navegador. El click de dentro del <a> sube hasta <boton> y (click)="alerta()" se ejecutaría dos veces: una por el evento del navegador y otra por el nuestro.

Datos que cambian: signal()

Los datos de un componente que cambian, por ejemplo un contador o la lista de productos que llega del servidor, se guardan en un signal. Es lo que ya trae el app.ts en title.

import { Component, signal } from '@angular/core';
import { Boton } from './shared/ui/components/boton/boton';

@Component({
  selector: 'app-root',
  imports: [Boton],
  templateUrl: './app.html',
  styleUrl: './app.scss'
})
export class App {
  protected readonly title = signal('prueba');

  contador = signal(0);

  incrementar(): void {
    this.contador.update(valor => valor + 10);
  }
}

<p>Contador: {{contador()}}</p>

<boton (pulsado)="incrementar()">Incrementar 10</boton>

  • signal(0): Crea el signal con su valor inicial.
  • contador(): Lee el valor, en el .ts y en el HTML.
  • contador.set(5): Le da un valor nuevo.
  • contador.update(valor ⇒ valor + 10): Le da un valor nuevo calculado a partir del actual.
Angular solo vuelve a pintar un componente cuando cambia uno de sus signals o cuando atiende un evento de su HTML, como un click. Si guardas los datos en una propiedad normal (contador = 0), el ejemplo anterior funciona porque el cambio viene de un click, pero cuando los datos llegan de un fetch o de un setTimeout la pantalla no se actualiza. Los datos que cambian, siempre en un signal.

Filtros (pipes)

Angular permite modificar los datos cuando se interpolan en el html.

  • currency : Permite mostrar un número como una moneda. En nuestro caso mostrará el símbolo del "€"

<p>{{precio() | currency}}</p>

  • date : Permite formatear una fecha

<p>{{fechaCompra() | date:'shortDate'}}</p>

Los formatos predefinidos de fecha se pueden ver en Pre-defined format options

Cada filtro hay que importarlo en el imports del componente que lo usa, como un componente más. Si no, el error es No pipe found with name 'currency'.

import { Component, input } from '@angular/core';
import { CurrencyPipe, DatePipe } from '@angular/common';

@Component({
  selector: 'articulo',
  imports: [CurrencyPipe, DatePipe],
  templateUrl: './articulo.html',
  styleUrl: './articulo.scss'
})
export class Articulo {
  readonly precio = input(0);
  readonly fechaCompra = input(new Date());
}

Ejercicios

Ejercicio 1

Usando el elemento HTML de <progress>.

<label for="file">File progress:</label>

<progress id="file" max="100" value="70">70%</progress>

Crea el componente <progreso>

<progreso titulo="File progress:" [porcentajeRealizado]="porcentaje()" />

<boton (pulsado)="incrementar()">Incrementar 10</boton>

y que muestre:

Añade en la página un botón de forma que al pulsarlo se incremente en 10 el valor.

Para generar el id que une el <label> con el <progress> se usa crypto.randomUUID(), que ya trae el navegador:

export class Progreso {
  id = crypto.randomUUID();
}

Ejercicio 2

Crea un componente llamado panel que permita hacer los siguientes paneles

Y crea una página HTML donde se vean los paneles.

Ejercicio 3

Crea ahora un componente para crear los siguientes botones:

Importancia
Función Primaria Secundaria Terciaria
Normal
Alternativa
Peligrosa
clase/daw/diw/angular-creacion-componentes.1792878034.txt.gz · Última modificación: por claude