Saltar al contenido
Angular flujos de datos de componentes mediante @Output y EventEmitter

Angular flujos de datos de componentes mediante @Output y EventEmitter

A diferencia de AngularJS, Angular no tiene enlace de datos bidireccional. Cuando digo que Angular no tiene un enlace de datos bidireccional, no significa que no pueda lograrlo.

15min read

Ningún marco web moderno puede existir sin el soporte del enlace de datos bidireccional. Angular proporciona esta función mediante la directiva ngModel. Para usarlo, debe importar FormsModule y luego puede usarlo de varias maneras, como:

  • ngModel  
  • [ngModel] 
  • [(ngModel)] 

Quizás se pregunte por qué Angular no tiene un enlace de datos bidireccional como AngularJS. AngularJS una vez implementó el enlace de datos bidireccional utilizando el ciclo de resumen, algoritmos de verificación sucios y otros, y estos enfoques tenían sus propios problemas. En este artículo, nos centraremos en los problemas con el enlace de datos bidireccional de AngularJS para que comprenda por qué se maneja de manera diferente en Angular.

Volviendo a Angular, el enlace de datos bidireccional se puede representar como se muestra a continuación:

 

El enlace de datos bidireccional se puede representar como se muestra

 

Los corchetes representan el enlace de propiedades, los corchetes pequeños representan el enlace de eventos y la combinación de ambos representa el enlace de datos bidireccional. La sintaxis del enlace de datos bidireccional también se denomina sintaxis banana in the box. Obtenga más información sobre los enlaces de datos aquí.

Ahora, tiene cierta comprensión de los diferentes tipos de encuadernación en Angular; Veamos cómo pueden fluir los datos entre Angular componentes. Los datos pueden ser de tipos primitivos, matrices, objetos o eventos. Para hacer fluir datos entre componentes, Angular proporciona:

  1. @Input decorator  
  2. @Output decorator  

Ambos decoradores son parte de @angular/núcleo.

@Input() decorator marks a class field as an input property and supplies configuration metadata. It declares a data-bound input property, which Angular automatically updates during change detection. 

@Output decorador marca un campo de clase como una propiedad de salida y proporciona metadatos de configuración. Declara una propiedad de salida enlazada a datos, que Angular se actualiza automáticamente durante la detección de cambios.

Ambos decoradores son parte de @angular/core

 

Estos dos decoradores se utilizan para hacer fluir los datos entre los componentes. Tenga en cuenta que, además de estos dos decoradores, también puede utilizar Angular servicios para hacer fluir los datos entre los componentes. Si tiene que pasar datos a un componente, use @Input decorador, y si tiene que emitir datos desde un componente, use @Output decorador.

Ambos decoradores trabajan entre componentes cuando están relacionados entre sí.  Por ejemplo, si usa un componente denominado ChildComponent dentro de otro componente denominado AppComponent, están relacionados entre sí.  Puede ver esto en la imagen siguiente, y cualquier propiedad ChildComponent decorada con @Input decorador puede recibir datos de AppComponent.

Ambos decoradores trabajan entre componentes cuando están relacionados entre sí

Para comprenderlo mejor, considere ChildComponent como se indica a continuación:

import {
    Component,
    OnInit,
    Input
}

from '@angular/core';

@Component({
    selector: 'app-child',
    template: ` <p>count= {
            {
            count
        }
    }
    </p> `
}) export class ChildComponent implements OnInit {
    constructor() {}
    @Input() count: number;
    ngOnInit() {}
}

Si observa detenidamente el fragmento de código anterior, estamos usando el decorador @Input() con la propiedad count, lo que implica que el valor de la propiedad count se establecerá desde fuera de ChildComponent.  Podemos usar ChildComponent dentro de AppComponent y podemos usar el enlace de propiedades para pasar el valor de la propiedad count, como se muestra en la siguiente lista:

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

@Component({
  selector: "app-root",
  template: `<h2>{{ title }}</h2>

    <app-child [count]="count"></app-child> `,
})
export class AppComponent {
  count = 9;
}

Estamos usando ChildComponent dentro de AppComponent y usando el enlace de propiedades que pasa datos en ChildComponent.  En este momento, los datos se pasan en un flujo direccional unidireccional al ChildComponent.  Tenga en cuenta que cada vez que se actualiza el valor de una propiedad enlazada a la entrada, Angular ejecuta el detector de cambios. Sin embargo, puede configurar cómo debe ejecutarse el detector de cambios en @Input decorador. Además, cada vez que se actualiza @input propiedad del decorador, Angular ejecuta el enlace del ciclo de vida ngOnChanges(), para que pueda leer el valor actualizado de la propiedad vinculada a la entrada en el enlace del ciclo de vida ngOnChanges().

Ahora que sabemos cómo pasar datos de un componente principal a un componente secundario, cambiemos nuestro enfoque a cómo se pueden emitir datos y eventos de un componente secundario a un componente principal.  Para emitir datos y eventos desde un componente, necesitamos

  • Para decorar la propiedad con @output decorador. Al hacerlo, esta propiedad se convierte en una propiedad de datos salientes.
  • Cree una instancia de EventEmitter y decórela con @Output decorador.

Tenga en cuenta en la siguiente imagen que los datos o eventos salen de un componente mediante @Output decorador.

Tenga en cuenta en la imagen a continuación que los datos o eventos salen de un componente mediante @Output decorador

 

Antes de explicar cómo funciona EventEmitter en detalle, revise el siguiente ejemplo de código.  Modifique ChildComponent con un botón como se muestra en la lista a continuación:

import {
    Component,
    OnInit,
    Input
}

from '@angular/core';

@Component({
    selector: 'app-child',
    template: ` <p>count= {
            {
            count
        }
    }

    </p> <button (click)='updateCount()' >update count </button> `

}) 
export class ChildComponent implements OnInit {

    constructor() {}
    @Input() count: number;
    ngOnInit() {}
    updateCount() {
        this.count=this.count+1;
    }

}

Al hacer clic en el botón en el ChildComponent, actualiza el valor de recuento. Súper simple.  Hasta ahora, el evento click se controla dentro del propio ChildComponent.

Ahora, modifiquemos un poco el requisito. ¿Qué sucede si desea ejecutar una función de AppComponent en el evento de clic de un botón dentro  de ChildComponent?

Para hacer esto, deberá emitir el evento de clic del botón de ChildComponent. Con event emit también puede enviar datos desde ChildComponent.  Modifiquemos ChildComponent para que emita datos y eventos que se capturarán en AppComponent.

import {
    Component,
    OnInit,
    Input,
    Output,
    EventEmitter
}

from '@angular/core';

@Component({

    selector: 'app-child',

    template: ` <p>count= {
            {
            count
        }
    }
    </p><button (click)='updateCount()' >update count </button> `

}) 
export class ChildComponent implements OnInit {

    constructor() {}

    @Input() count: number;

    @Output() countChange=new EventEmitter();

    ngOnInit() {}

    updateCount() {

        this.count=this.count+1;

        this.countChange.emit(this.count);

    }

}

En este momento, estamos realizando las siguientes tareas en ChildComponent

  1. Instancia creada de EventEmitter llamada countChange, que se emitirá al componente primario en el evento de clic del botón.
  2. Se creó una función llamada updateCount(). Esta función se llama en el evento click del botón, y dentro de la función se emite el evento countChange.
  3. Al emitir el evento countChange, el valor de la propiedad count también se envía fuera del componente.

Antes de continuar, entendamos EventEmitter. Se usa en las directivas y componentes para emitir eventos personalizados de forma sincrónica o asincrónica.  Cualquier controlador puede controlar estos eventos suscribiéndose a una instancia de la clase EventEmitter.  Es diferente a un evento HTML normal, ya que utiliza RxJS observable de modo que el controlador puede suscribirse al evento.

Si examina la implementación de la clase EventEmitter, extiende la clase Subject.

Si examina la implementación de la clase EventEmitter, extiende la clase Subject

  

Dado que la clase EventEmitter extiende la clase RxJs Subject, esto significa que es observable y se puede multidifundir a muchos observadores.  No es lo mismo que un evento DOM, ¿cuál? no puede ser muticasto y observado.

En AppComponent, puede controlar el evento emitido desde ChildComponent como se muestra en la siguiente lista de código:

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

@Component({ 
  selector: 'app-root', 
  template: `<h2>{{title}}</h2> 
  <app-child [count]='count' (countChange)=changeCount($event)></app-child>` 
}) 

export class AppComponent { 
   count = 9; 
   changeCount(data) { 
      console.log(data); 
   } 
}

En este momento, estamos realizando las siguientes tareas en la clase AppComponent:

  1. Usando <app-child> en la plantilla.
  2. En el elemento <app-child>, se usa el enlace de eventos para usar el evento countChange.
  3. Llamar a la función changeCount en el evento countChange.
  4. En la función changeCount, imprime el valor del recuento pasado desde ChildComponent.

Como puede ver, se llama a la función de AppComponent en el evento de clic del botón colocado en ChildComponent. Esto se puede hacer con @Output y EventEmitter.  En este momento, los datos fluyen entre los componentes utilizando los decoradores @Input() y @Output().

 

En este momento, los datos fluyen entre los componentes utilizando los decoradores @Input() y @Output().

Como puede ver, creamos un enlace de datos bidireccional entre dos componentes. Si observa el código, es una combinación de enlace de propiedades y enlace de eventos.

Si observa el código, es una combinación de enlace de propiedades y enlace de eventos

Un ejemplo en tiempo real

Tomemos un ejemplo en tiempo real para descubrir cómo @Output y EventEmitter son más útiles. Tenga en cuenta que AppComponent representa una lista de productos en forma de tabla, como se muestra en la imagen siguiente:

tome un ejemplo en tiempo real para descubrir cómo @Output y EventEmitter son más útiles

Para crear la tabla de productos anterior, tenemos una clase AppComponent muy simple con una sola función: devolver una lista de productos.

export class AppComponent implements OnInit { 
  products = []; 
  title = 'Products'; 
  ngOnInit() { 
    this.products = this.getProducts(); 
  } 

  getProducts() { 
    return [ 
      { 'id': '1', 'title': 'Screw Driver', 'price': 400, 'stock': 11 }, 
      { 'id': '2', 'title': 'Nut Volt', 'price': 200, 'stock': 5 }, 
      { 'id': '3', 'title': 'Resistor', 'price': 78, 'stock': 45 }, 
      { 'id': '4', 'title': 'Tractor', 'price': 20000, 'stock': 1 }, 
      { 'id': '5', 'title': 'Roller', 'price': 62, 'stock': 15 }, 
    ]; 
  } 
}

En el enlace del ciclo de vida ngOnInit, llamamos a la función getPrdoducts() y asignamos los datos devueltos a la variable products para que se puedan usar en la plantilla. Allí, estamos usando la directiva *ngFor para iterar a través de la matriz y mostrar los productos. Vea el código a continuación:

<div class="container"> 
  <br/> 
  <h1 class="text-center">{{title}}</h1> 
  <table class="table"> 
    <thead> 
      <th>Id</th> 
      <th>Title</th> 
      <th>Price</th> 
      <th>Stock</th> 
    </thead> 
    <tbody> 
      <tr *ngFor="let p of products"> 
        <td>{{p.id}}</td> 
        <td>{{p.title}}</td> 
        <td>{{p.price}}</td> 
        <td>{{p.stock}}</td> 
      </tr> 
    </tbody> 
  </table> 
</div>

Con este código, los productos se representan en una tabla como se muestra en la imagen siguiente:

Este código, los productos se representan en una tabla como se muestra en la imagen

Ahora digamos que queremos agregar una nueva columna con un botón y un cuadro de entrada como se muestra en la imagen a continuación:

Digamos que queremos agregar una nueva columna con un botón y un cuadro de entrada como se muestra

 

Nuestros requisitos son los siguientes:

  1. Si el valor de la acción es superior a 10, el color del botón debe ser verde.
  2. Si el valor de la acción es inferior a 10, el color del botón debe ser rojo.
  3. El usuario puede ingresar un número en el cuadro de entrada, que se agregará a ese valor de acciones en particular.
  4. El color del botón debe actualizarse en función del valor modificado del stock del producto.

Para lograr esta tarea, vamos a crear un nuevo componente secundario llamado StockStausComponent. Esencialmente, en la plantilla de StockStatusComponent, hay un botón y un cuadro de entrada numérico. En StockStatusComponent:

  1. Necesitamos leer el valor de las acciones pasadas desde AppComponnet. Para ello, necesitamos usar @Input
  2. Necesitamos emitir un evento para que se pueda llamar a una función en AppComponent al hacer clic en el botón StockStatusComponent. Para esto, necesitamos usar @Output y EventEmitter.

 

Consider the code below: 

stockstatus.component.ts

import { Component, Input, EventEmitter, Output, OnChanges } from '@angular/core'; 

@Component({ 
    selector: 'app-stock-status', 
    template: `<input type='number' [(ngModel)]='updatedstockvalue'/> <button class='btn btn-primary' 
     [style.background]='color' 
     (click)="stockValueChanged()">Change Stock Value</button> ` 
}) 

export class StockStatusComponent implements OnChanges { 
    @Input() stock: number; 
    @Input() productId: number; 
    @Output() stockValueChange = new EventEmitter(); 
    color = ''; 
    updatedstockvalue: number; 

    stockValueChanged() { 
        this.stockValueChange.emit({ id: this.productId, updatdstockvalue: this.updatedstockvalue }); 
        this.updatedstockvalue = null; 
    } 

    ngOnChanges() { 
        if (this.stock > 10) { 
            this.color = 'green'; 
        } else { 
            this.color = 'red'; 
        } 
    } 
}

Exploremos la clase anterior línea por línea.

  1. En la primera línea estamos importando todo lo necesario: @Input, @Output etc.
  2. En la plantilla, hay un cuadro de entrada numérico que está enlazado a la propiedad updatedStockValue using [(ngModel)]. Necesitamos pasar este valor con un evento a AppComponent.
  3. En la plantilla, hay un botón. En el evento de clic del botón, se emite un evento a AppComponent.
  4. Necesitamos establecer el color del botón en función del valor del stock del producto. Por lo tanto, debemos usar el enlace de propiedades para establecer el fondo del botón. El valor de la propiedad color se actualiza en la clase.
  5. Estamos creando dos propiedades decoradas @Input() –stock y productId– porque el valor de estas dos propiedades se pasará desde AppComponent.
  6. Estamos creando un evento llamado stockValueChange. Este evento se emitirá a AppComponent al hacer clic en el botón.
  7. En la función stockValueChanged, emitimos el evento stockValueChange y también pasamos el identificador del producto que se actualizará y el valor que se agregará en el valor de stock del producto.
  8. Estamos actualizando el valor de la propiedad de color en el enlace de ciclo de vida ngOnChanges() porque cada vez que se actualiza el valor de stock en AppComponent, se debe actualizar el valor de la propiedad color.

Aquí estamos usando el decorador @Input para leer datos de la clase AppComponent, que resulta ser la clase principal en este caso. Para pasar datos de la clase de componente principal a la clase de componente secundaria, use @Input decorador.

Además, usamos @Output con EventEmitter para emitir un evento a AppComponent. Por lo tanto, para emitir un evento de la clase de componente secundario a la clase de componente principal, use EventEmitter con el decorador @Output().

Por lo tanto, StockStatusComponent usa @Input y @Output para leer datos de AppComponent y emitir un evento a AppComponent.

Modify AppComponent to use StockStatusComponent 

Primero modifiquemos la plantilla. En la plantilla, agregue una nueva columna de tabla. Dentro de la columna, se utiliza el componente <app-stock-status>.

<div class="container"> 
  <br/> 
  <h1 class="text-center">{{title}}</h1> 
  <table class="table"> 
    <thead> 
      <th>Id</th> 
      <th>Title</th> 
      <th>Price</th> 
      <th>Stock</th> 
    </thead> 
    <tbody> 
      <tr *ngFor="let p of products"> 
        <td>{{p.id}}</td> 
        <td>{{p.title}}</td> 
        <td>{{p.price}}</td> 
        <td>{{p.stock}}</td> 
        <td><app-stock-status [productId]='p.id' [stock]='p.stock' (stockValueChange)='changeStockValue($event)'></app-stock-status></td> 
      </tr> 
    </tbody> 
  </table> 
</div>

Estamos pasando el valor a productId y stock mediante el enlace de propiedades (recuerde, estas dos propiedades están decoradas con @Input() en StockStatusComponent) y el enlace de eventos para controlar el evento stockValueChange (recuerde, este evento está decorado con @Output() en StockStatusComponent).

A continuación, debemos agregar la función changeStockValue en AppComponent. Agregue el código siguiente en la clase AppComponent:

productToUpdate: any; 

changeStockValue(p) { 
    this.productToUpdate = this.products.find(this.findProducts, [p.id]); 
    this.productToUpdate.stock = this.productToUpdate.stock + p.updatdstockvalue; 
  } 

  findProducts(p) { 
    return p.id === this[0]; 
  }

En la función, usamos el método JavaScript Array.prototype.find para encontrar un producto con un productId coincidente y, a continuación, actualizamos el recuento de existencias del producto coincidente.  Al ejecutar la aplicación, obtendrá el siguiente resultado:

Cuando ingresa un número en el cuadro numérico y hace clic en el botón, realiza una tarea en el componente secundario que actualiza el valor de la operación en el componente principal. Además, en función del valor del componente principal, el estilo se cambia en el componente secundario. Todo esto es posible utilizando Angular @Input, @Output y EventEmitter.

¡Estén atentos para futuros artículos donde profundizaremos en otras características de Angular!

Ignite UI for Angular beneficios

 

Solicitar una demostración