Compartir a través de



Noviembre de 2017

Volumen 33, número 11

El programador ocupado: cómo dominar MEAN: formularios de Angular

Por Ted Neward | Noviembre de 2017

Ted Neward

Bienvenidos de nuevo, usuarios de MEAN.
Toda esta serie se centra en Angular; en especial, en el uso de Angular para mostrar los datos y la ruta en varias páginas. Aunque el explorador web está integrado para poder mostrar datos, también pretende poder recopilar datos y pasarlos al servidor y este es un tema que aún no se ha tratado hasta el momento.

Desde luego, Angular puede capturar datos a través de formularios, pero hacerlo es un poco difícil, no tanto en la sintaxis para crear y definir el formulario del explorador, sino en un aspecto particular del comportamiento subyacente, que explicaré más adelante. No pongamos el carro delante de los bueyes. Es hora de hablar de la definición y captura de formularios simples.

Componentes formados

En columnas anteriores, he explicado cómo puede enlazar "instrucciones de plantilla" (para usar la terminología de Angular) a los eventos que ofrece un componente de Angular. El ejemplo más común es la captura del evento de clic en un elemento <button> para invocar un método en un componente de Angular:

<button (click)="console.log('Clicked')">Push me!</button>

De por sí, está bien, pero no proporciona ninguna característica para capturar la entrada. Para que resulte útil para la entrada de datos, debe darse una de las dos condiciones siguientes: Que el código del componente tenga la capacidad de hacer referencia a los controles en la página desde el componente (que es lo que hará ASP.NET Web Forms, entre otros marcos), o que el código que se invoca sea capaz de recibir los datos introducidos como un parámetro. No obstante, puede que se necesiten dos formularios diferentes.

El primero y más genérico es la instrucción de plantilla de Angular, que puede hacer referencia al objeto $event, que es fundamentalmente el objeto de evento de DOM generado durante la sesión del usuario. Solo requiere la referencia al parámetro como parte de la instrucción, por ejemplo:

<button (click)="capture($event)" >Save</button>

Sin embargo, el inconveniente es que el objeto pasado es el evento de DOM que representa la acción del usuario; en este caso, un evento MouseEvent que realiza el seguimiento de la ubicación en la pantalla donde se hizo clic con el mouse y que no captura realmente el estado de los demás elementos de la página. Aunque sin duda sería posible navegar por la jerarquía de DOM para buscar elementos de control y extraer los valores de estos, tampoco es realmente el método de Angular. Los componentes deberían aislarse de DOM, y la instrucción de plantilla debería poder obtener los datos que necesita y pasarlos a un método en el componente para su uso.

Este enfoque sugiere que Angular necesita alguna manera de identificar los campos de entrada en la página, de manera que la instrucción de plantilla pueda extraer y pasar los valores. La necesidad de poder identificar el elemento de formulario toma la forma de un "identificador" en el propio elemento <input>, que Angular denomina "variable de referencia de plantilla". Como sucede con otra sintaxis de Angular, usa de forma deliberada sintaxis que no parece HTML:

<input #firstName>

Se creará un campo Input en HTML, de acuerdo con la etiqueta HTML normal con el mismo nombre, pero se introducirá una nueva variable en el ámbito de la plantilla, denominada firstName, a la que se puede hacer referencia desde la instrucción de plantilla enlazada a un evento desde un campo, de la siguiente manera:

<button (click)="addSpeaker(firstName, lastName)" >Save</button>

Es bastante descriptivo: un clic en un botón invoca el método addSpeaker del componente, y pasa las variables firstName y lastName en consecuencia, como se muestra en la Figura 1.

Figura 1 Invocación del método addSpeaker

import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-speaker-edit',
templateUrl: './speaker-edit.component.html',
styleUrls: ['./speaker-edit.component.css']
})
export class SpeakerEditComponent implements OnInit {
constructor() { }
ngOnInit() { }
addSpeaker(fname: string, lname: string) {
console.log("addSpeaker(", fname, ",", lname, ")")
}
}

No obstante, escrito de esta manera, lo que se muestra en la consola del explorador no son las cadenas previstas de la entrada; en su lugar, aparecen valores como <input _ngcontent-crf-2> que reemplazan cada uno de estos valores. La razón es sencilla: La consola del explorador devuelve las representaciones de Angular reales del elemento DOM, en lugar de los datos de entrada que se han escrito. La solución es igual de simple: usar la propiedad "value" en cada lado de la instrucción de plantilla para obtener los datos que el usuario ha escrito.

Así, si necesito construir un componente para crear nuevos oradores, debo crear un componente que muestre dos campos <input>, un elemento <button> que tenga un elemento (click) que llame a addSpeaker, y que pase firstName.value y lastName.value, y usar ese método para invocar el objeto SpeakerService (de un artículo anterior) para guardarlo en la base de datos. No obstante, esta idea de "crear un nuevo objeto Speaker" es conceptualmente muy parecida a la de "editar un objeto Speaker existente", tanto que algunas bases de datos modernas han empezado a considerar las operaciones inserción y actualización como si fueran una sola: upsert. Sería de agradecer, además de estar orientado a los componentes, que el componente SpeakerEdit pudiera servir para ambas operaciones de creación o edición en el mismo componente.

Gracias a la eficacia de las directivas @Input y @Output mostradas anteriormente, esto es en realidad bastante común. Agregue un campo @Input para el orador que se deba pasar y un campo @Output para que la gente sepa cuándo el usuario hace clic en Save (Guardar). (Este último no es necesario si decide que el componente SpeakerEdit siempre se guarde en la base de datos y no existe ninguna otra acción que un cliente del componente pueda querer llevar a cabo. Esta sería una conversación altamente contextual en una reunión del equipo).

Esto me lleva al contenido de la Figura 2 para el código del componente SpeakerEdit.

Figura 2 El componente SpeakerEdit

export class SpeakerEditComponent implements OnInit {
@Input() speaker: Speaker;
@Output() onSave = new EventEmitter<Speaker>();
constructor(private speakerService: SpeakerService) { }
ngOnInit() {
if (this.speaker === undefined) {
this.speaker = new Speaker();
}
}
save(fn: string, ln: string) {
this.speaker.firstName = fn;
this.speaker.lastName = ln;
this.onSave.emit(this.speaker);
}
}

Como podría prever teniendo en cuenta mis aptitudes de diseño, la plantilla es bastante simple, aunque funcional:

<div>
Speaker Details: <br>
FirstName: <input #firstName><br>
LastName:  <input #lastName><br>
<button (click)="save(firstName.value, lastName.value)">Save</button>
</div>

De nuevo, observe que uso ".value" para extraer los valores de cadena de los campos de entrada firstName y lastName.

El uso de este componente (en este ejercicio, desde el componente App­Component principal) es bastante sencillo:

<h3>Create a new Speaker</h3>
<app-speaker-edit (onSave)="speakerSvc.save($event)"></app-speaker-edit>

En este caso, opté por no guardar el objeto Speaker desde el componente, sino forzar que lo hicieran los clientes desde el evento emitido. El objeto speakerSvc es un objeto SpeakerService insertado por dependencias del artículo anterior sobre la creación de servicios en Angular (msdn.microsoft.com/magazine/mt826349).

Esto es lo que la componentización le compra: La capacidad de crear "controles" de interfaz de usuario que puede colocar y usar, en lugar de tener que pensar cómo funcionan internamente.

Activación de eventos

Estaría bien terminar aquí, pero existe un problema relacionado con los eventos en Angular que merece atención. Es bastante común para los desarrolladores querer interceptar cosas, como pulsaciones de teclas, y reaccionar a los datos introducidos, por ejemplo, mostrar algunos valores de sugerencias de autocompletar. Los eventos DOM tradicionales para esto son métodos como onBlur u onKeyUp. Por ejemplo, estaría bien realizar el seguimiento de cada pulsación de tecla y mostrarla a medida que el usuario escribe. Los desarrolladores nuevos en Angular pueden esperar que esto funcione:

@Component({
  selector: 'loop-back',
  template: `
    <input #box>
    <p>{{box.value}}</p>
  `
})
export class LoopbackComponent { }

Sin embargo, cuando este código se ejecute, no mostrará cada carácter a medida que se escriba. De hecho, no hará nada. Esto se debe a que Angular no activará ningún evento a menos que el explorador active uno. Por ese motivo, Angular necesita que se active un evento, aunque el código que se active en este sea totalmente inoperativo, como la instrucción de plantilla 0 en:

@Component({
  selector: 'loop-back',
  template: `
    <input #box (keyup)="0">
    <p>{{box.value}}</p>
  `
})
export class LoopbackComponent { }

Observe el enlace "keyup". Indica a Angular que registre eventos clave en el elemento Input, lo que proporciona a Angular la oportunidad de desencadenar eventos que, luego, actualizarán la vista. Es un poco incómodo de usar al principio, pero, de este modo, Angular no sondea ningún tipo de evento constantemente y, por tanto, no tiene que consumir tantos ciclos de CPU.

Resumen

A algunos desarrolladores web experimentados les podría parecer un poco confuso o incómodo: "¿Por qué no podemos volver al práctico formulario HTML antiguo?" El método Angular no siempre es obvio, pero en muchos aspectos, una vez que el razonamiento y la lógica están claros, suele ser comprensible y razonable. En este caso concreto, el método Angular consiste en adoptar el concepto de los componentes y pensar en términos de creación de "construcciones" útiles que sepan cómo capturar y procesar la entrada. Esto permite un grado de flexibilidad que no se daba realmente en el pensamiento "web antiguo", como la inclusión de varios de estos componentes en una sola página. (A la hora de actualizar la página completa para mostrar los resultados, se acaba teniendo un ciclo de entrada-validación-almacenamiento-representación por requisito de entrada).

No obstante, existen otros problemas, como conocer la manera de actualizar otros componentes del sistema cuando un usuario edita un objeto Speaker existente. Angular tiene una solución a este problema, pero aún son necesarios algunos pasos más para poder entrar en el mundo de la programación reactiva. La recta final está cerca, así que quédese conmigo un poco más. Mientras tanto… ¡Que disfrute programando!


Ted Neward es un asesor politecnológico, orador y mentor con sede en Seattle. Actualmente, trabaja como director de relaciones con desarrolladores en Smartsheet.com. Ha escrito una gran cantidad de artículos, es autor y coautor de una docena de libros y trabaja por todo el mundo. Puede ponerse en contacto con él en ted@tedneward.com o leer su blog en blogs.tedneward.com.

Gracias al siguiente experto técnico de Microsoft por revisar este artículo: James Bender


Discuta sobre este artículo en el foro de MSDN Magazine