Tabla de Contenidos
¡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ónboton/boton.html: El HTML correspondiente al botónboton/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, comobotonopanel.shared/ui/layouts: Los layouts, los bloquesl-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
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 HTMLstyleUrl: 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 creainput()youtput().
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>
[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
funcioncon 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
funcioncon las clases CSS en el HTML de la siguiente forma:
<button class="c-boton funcion--{{funcion()}}">
<ng-content></ng-content>
</button>
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
hrefa la claseBoton - 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)".
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.tsy 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.
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.
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.











