Componentes y databinding en Angular: Guía avanzada | Parte #2

Retomamos esta saga sobre el uso avanzado de componentes y databinding en Angular, después de ver la parte #1. Habíamos dejado nuestra demo app con este aspecto tan majo:

ejemplo event emitter

Breve historia del View Encapsulation

Si te fijas, antes de dividir nuestra app en componentes, cuando generábamos contenido añadiendo un blueprint, el texto de la card era de color azul. El párrafo "Add new servers or blueprints!" también era azul previamente. Esto sucedía porque el archivo CSS del AppComponent así lo especifica:

p {
  color: blue;
}

Pero ahora, al haber dividido en componentes nuestro código, el template del AppComponent no contiene ningún <p> al que aplicarle el color de su archivo CSS. Los <p> están ahora repartidos entre el ServerElementComponent y el ControlPanelComponent.

Teniendo en cuenta que CSS se aplica en cascada, sin importar en qué archivo CSS definamos una regla, en nuestro caso debería aplicarse a todas las etiquetas <p> de nuestra app, buscando cualquier <p> siguiendo un patrón de AppComponent  elementos del AppComponent como el <app-control-panel> elementos del <app-control-panel> ➡ hasta finalmente llegar a un <p> dentro del <app-control-panel> y aplicar ese color azul. Pero esto no es lo que sucede. ¿Por qué? 🤨

Porque Angular sobrescribe el comportamiento por defecto del navegador, encapsulando los estilos CSS y aplicándolos únicamente al componente al que pertenecen. 💊

Es el mismo principio que aplica Angular a los archivos .ts y .html de un componente: por defecto, lo que declaremos en dichos archivos sólo puede ser accedido desde dentro del mismo componente, y si queremos acceder desde fuera, tendremos que implementar una lógica de comunicación entre componentes como la que hablábamos en la parte #1 de esta serie

Esto significa que, si quisiéramos colorear todos los <p> de color azul, deberíamos aplicar una regla de CSS en el archivo CSS de cada componente donde haya etiquetas <p>. Por ejemplo, podemos cortar esa regla de nuestro app.component.css añadirla al archivo server-element.component.cssque es donde la queremos. Y ahora, cuando generamos contenido pulsando el botón "Add Server Blueprint", éste vuelve a ser azul.

Cuando Angular fuerza esta encapsulación de estilos (en inglés, styles encapsulation view encapsulation), lo hace aplicando un atributo distinto por componente. Inspecciona tu app con las dev tools para comprobarlo: verás que, como por arte de magia, el párrafo que ahora recibe la clase que lo pinta de color azul también contiene esa extraña adición subrayada en amarillo, que no tiene que ser igual en tu caso que en el mío. 

atributo angular css class

Si inspeccionas todas las etiquetas HTML de ese componente, verás, que todas comparten ese atributo: _ngcontent-syk-c2 en mi caso.

Si inspeccionas el primer párrafo de nuestra app ("Add new Servers or blueprints!"), verás que Angular le ha añadido otro atributo distinto a _ngcontent-syk-c2, porque ese <p> está dentro de otro componente (al ControlPanel).

Y esta es la manera que tiene Angular de decir: "tú, <p>, perteneces a este componente X, porque he creado un atributo único para identificar ese componente. Así que cuando desde el archivo CSS del componente X se defina un estilo para un <p>, sólo lo aplicaré a los <p> que estén dentro de ese componente X". 🧙‍♂️ 

Angular fuerza el "view encapsulation" dando un mismo atributo a todos los elementos HTML de un componente.

Este comportamiento de Angular emula la tecnología del shadow DOM, que no es compatible con todos los navegadores, y que consiste precisamente en encapsular cada nodo de HTML, haciéndolos así independientes entre sí. Pero al no ser compatible con todos lo navegadores, Angular utiliza este otro enfoque de view encapsulation para conseguir el mismo resultado.

Configuración del View Encapsulation 

Por defecto, Angular aplica el comportamiento del view encapsulation como hemos visto en el apartado anterior, pero podemos cambiar dicho comportamiento. A continuación explicamos cómo sobrescribir ese comportamiento por defecto.

1. En el server-element.component.ts, añadimos a nuestro decorador una propiedad llamada encapsulationy le damos el valor ViewEncapsulation, que nos permite elegir una de estas 3 opciones:

  • Emulated ➡ valor por defecto
  • None ➡ desactiva el comportamiento del view encapsulation
  • ShadowDOM ➡ usa la tecnología del shadow DOM. Esta opción sólo funcionará en los navegadores compatibles con dicha tecnología.

No olvides importar este paquete desde angular/core.

Vamos a probar a utilizar la propiedad None.

@Component({
  selector: 'app-server-element',
  templateUrl: './server-element.component.html',
  styleUrls: ['./server-element.component.css'],
  encapsulation: ViewEncapsulation.None
})

Al utilizar None, desactivamos el comportamiento de la encapsulación, por tanto ahora, si inspeccionamos nuestra app, verás que Angular ha dejado de aplicar sus propios atributos a los elementos HTML del componente ServerElement.

Con este cambio, ahora cualquier estilo CSS que definamos en el server-element.component.css se aplicará a toda nuestra app. De hecho, ya deberías poder ver que el primer párrafo de la app es azul otra vez.

2. Si ahora añadimos otro estilo cualquiera dentro del mismo CSS (server-element.component.css), verás que se sigue aplicando globalmente. Por ejemplo, le cambiamos el color a la etiqueta <label>, que no está en este componente, sino en el ControlPanelComponent.

label {
  color: darkmagenta;
}

Guarda los cambios y observa como ahora tus labels han cambiado de color. 

Y así es como se configura el comportamiento del view encapsulation. Lo más común es usar la opción por defecto que trae Angular, así que vamos a eliminar la propiedad encapsulation del archivo server-element.component.ts para dejarlo como estaba previamente. 

Cómo usar local references en templates HTML

Vamos a refactorizar un poco el código de nuestra demo app y de paso aprender un par de cosillas nuevas 🤓. Si nos vamos a nuestro componente ControlPanel, verás que estamos utilizando two way data binding (mediante el ngModelpara obtener el valor del newServerName y del newServerContent. No hay nada malo en este método, pero es innecesario, ya que podemos sustituirlo por algo que optimizará nuestro código: las local references. 

En este caso, únicamente necesitamos obtener el valor de las propiedades newServerName y newServerContent en el momento en el que el usuario hace click en uno de los botones, no antes. Así que este es un buen caso para usar una local reference.

Para usar una local reference simplemente tenemos que añadir la palabra que queramos usar como local reference en el elemento HTML donde la necesitemos. 

Sintaxis:

<elementoHTML #nombreLocalReference> </elementoHTML>

Una local reference hace referencia a todas las propiedades del elemento HTML al que se añada, a las que podemos tener acceso a través de esa misma local reference.

🧐 Podemos usar local references a lo largo de toda nuestra template, pero únicamente ahí, no en ningún otro archivo.

Vamos a añadir una local reference a nuestro primer <input> del control-panel.component.html.

1. Comentamos el código que contiene la ngModel porque ya no lo vamos a necesitar. De hecho, puedes duplicar todo el <input> y dejar uno comentado como referencia, y trabajar con el no comentado.

2. Le quitamos el ngModel y le añadimos una local reference usando la sintaxis de arriba. Le damos el nombre que nos parezca más adecuado, por ejemplo, #serverNameInputAhora esta local reference contiene una referencia a todo el elemento <input>, con sus métodos y propiedades. Es como tener un chivato a nuestra disposición. 👀

Podemos pasar la local reference como parámetro al método onAddServer. Al pasársela como parámetro, no incluimos la almohadilla  👉  # .

    <!-- <input type="text" class="form-control" [(ngModel)]="newServerName"> -->
    <input type="text" class="form-control" #serverNameInput>
    <label>Server Content</label>
    <input type="text" class="form-control" [(ngModel)]="newServerContent">
    <br>
    <button class="btn btn-primary" (click)="onAddServer(serverNameInput)">Add Server</button>

Al incluir la local reference como parámetro del método, ya la estamos conectando al archivo TypeScript (en adelante, TS).

3. Vamos al archivo control-panel.component.ts e informamos al método onAddServer de que va a recibir un parámetro (la local reference que le hemos pasado en la template). Hacemos un console.log del parámetro en el método para ver qué se imprime en la consola cuando hacemos click en el botón "Add server".

  onAddServer(nameInput) {
    console.log(nameInput);
    this.serverCreated.emit({
      serverName: this.newServerName,
      serverContent: this.newServerContent
    });
  }

De vuelta a tu navegador, verás que, al añadir algo en el campo del input y hacer click en "Add server", en la consola se imprime todo el elemento <input>. ¡Genial!

Con esto hemos demostrado que una local reference tiene acceso al elemento HTML completo, así que si quisiéramos acceder al value del <input>, no tendríamos más que usar esa propiedad (.value). 

4. Prueba a pasarle la propiedad value al console.log que hemos hecho en el método onAddServer, y verás que ahora sólo te imprime el valor del input, es decir, lo que has escrito en el campo del input. 

    console.log(nameInput.value);

5. Ahora que hemos visto las posibilidades de las local references, vamos a quitar el console.log y a utilizar nuestra local reference para obtener el nombre que el usuario introduzca en el campo input. Para eso, a la key "serverName" podemos darle el valor del parámetro.value.

Es conveniente también que seamos explícitos con el data type de nuestro parámetro del método onAddServer, siendo el data type un HTMLInputElement. 

  onAddServer(nameInput: HTMLInputElement) {
    this.serverCreated.emit({
      serverName: nameInput.value,
      serverContent: this.newServerContent
    });
  }

6. Replicamos la misma lógica de los puntos anteriores sobre el método onAddBlueprint, tanto en el template:​​

    <button class="btn btn-primary mx-2"
            (click)="onAddBlueprint(serverNameInput)">
            Add Server Blueprint
    </button>

como en el archivo TS:

  onAddBlueprint(nameInput: HTMLInputElement) {
    this.blueprintCreated.emit({
      blueprintName: nameInput.value,
      blueprintContent: this.newServerContent
    });
  }

Y con esto, podemos olvidarnos de la propiedad newServerName, ya que no nos hará falta más. 👋

  // newServerName = '';

Y con esta optimización, tu código debería funcionar igual que antes, añadiendo un bloque con la información del nombre del server o blueprint y su contenido según hagas click en un botón o en otro.

Explicando local references

@ViewChild(): otra manera de acceder al template desde el código TS

Existe otra manera de acceder a una local reference (o a cualquier elemento) del template a través del código del archivo TS. Esta forma se llama ViewChild, ​que es un decorador de TSVeamos cómo funciona.

Utilizamos @ViewChild cuando queremos tener acceso a una propiedad antes de que cierto método sea ejecutado.

Por ejemplo, hasta ahora, en el control-panel.component.html, estamos pasándole una local reference  (serverNameInput) a los métodos onAddServer onAddBlueprint. Así es como obtenemos el acceso a la local reference, en ese momento concreto. Pero imaginemos que, por alguna razón, queremos tener acceso a la local reference en un momento anterior a que los métodos se ejecuten.

Vamos a aplicar este sistema sobre el input:

  <input type="text" class="form-control" [(ngModel)]="newServerContent">

1. En control-panel.component.html, duplicamos ese <input> y comentamos uno para dejarlo como referencia. En el otro <input>, nos deshacemos del ngModel y le añadimos una local referencea la que llamamos #serverContentInput.

    <!-- <input type="text" class="form-control" [(ngModel)]="newServerContent"> -->
    <input type="text" class="form-control" #serverContentInput>

2. En el archivo control-panel.component.ts, comentamos la propiedad newServerContent porque no la vamos a necesitar más y añadimos una propiedad nueva, llamada serverContentInput.

3. Delante de dicha propiedad le añadimos el decorador @ViewChild, que por cierto, debemos importarlo desde angular/core. 

Para que el decorador funcione, necesita recibir un argumento (en inglés, argument), que debe ser sencillamente el elemento que queramos seleccionar (en inglés, el selector). ¿Y cómo lo seleccionamos?  Utilizando nuestra local reference recién creada (serverContentInput), pasándosela como string

Por un tema relacionado con el Angular lifecycle, debemos añadir al viewChild un segundo argumento (un objeto: {static: true} ). De los Angular lifecycles hablaremos en la tercera parte de esta serie. 🤓

Esta no es la única manera de pasarle información al @ViewChild por medio de sus arguments, pero sí la más común. En este ejemplo estamos pasándole una local reference como un string, pero también podríamos pasarle un componente completo, por ejemplo, @ViewChild(ControlPanelComponent).

Con estos cambios, ya tenemos acceso a la local reference del template.

4. Comentamos el código de dentro de los métodos onAddServer onAddBlueprint para que no rompa nuestra app, y hacemos un console.log de nuestra propiedad nueva (serverContentInput) en el método onAddServer

  // newServerContent = '';
  @ViewChild('serverContentInput', { static: true }) serverContentInput;

  constructor() { }

  ngOnInit() {
  }

  onAddServer(nameInput: HTMLInputElement) {
    console.log(this.serverContentInput);
    // this.serverCreated.emit({
    //   serverName: nameInput.value,
    //   serverContent: this.newServerContent
    // });
  }

  onAddBlueprint(nameInput: HTMLInputElement) {
  //   this.blueprintCreated.emit({
  //     blueprintName: nameInput.value,
  //     blueprintContent: this.newServerContent
  //   });
  }

Verás que, si ahora guardas y escribes algo en el input del Server Content, al hacer click en el botón "Add server", la consola te devuelve un elemento de tipo ElementRef.

|   Elements    Console    Sources    Performance    Network    ...

> ElementRef {nativeElement: input.form-control}


Fíjate que esto es distinto de lo que pasaba cuando accedíamos al elemento a través de una local reference, porque la local reference nos devolvía el elemento entero al que estaba ligada, es decir, un elemento de tipo HTMLInputElement. Mientras que ahora la consola nos devuelve un ElementRef.

5. Como ya sabemos de qué tipo es nuestra propiedad serverContentInput, se lo añadimos para ser más explícitos. De hecho, ElementRef es un data type de Angular, así que debemos importarlo desde angular/core.

ElementRef tiene una propiedad muy útil que usaremos:  nativeElementEsta propiedad nos da acceso al elemento subyacente (en inglés, underlying element), que en nuestro caso es el <input>, y como todos los <input>, tiene una propiedad llamada .value, a la que podemos tener acceso gracias a ese nativeElement. 

viewChild esquema

6. Nos deshacemos del console.log y habilitamos el código del método onAddServer. A nuestra propiedad serverContent le atribuimos el valor del value del nativeElement del <input>, qué será el contenido que el usuario escriba en el campo <input> del Server Content.

Replicamos el mismo proceso para el método onAddBlueprint.

  @ViewChild('serverContentInput', { static: true }) serverContentInput: ElementRef;

  constructor() { }

  ngOnInit() {
  }

  onAddServer(nameInput: HTMLInputElement) {
    this.serverCreated.emit({
      serverName: nameInput.value,
      serverContent: this.serverContentInput.nativeElement.value
    });
  }

  onAddBlueprint(nameInput: HTMLInputElement) {
    this.blueprintCreated.emit({
      blueprintName: nameInput.value,
      blueprintContent: this.serverContentInput.nativeElement.value
    });
  }

Y con estos cambios, tu demo app debería funcionar perfectamente igual que antes. 👌

👮‍♀️ Cuidadito con usar esta técnica para manipular el DOM desde el archivo TS, porque está totalmente desaconsejado 🚫. Por ejemplo, si le damos un valor hardcoded al serverContentInput así:

onAddServer(nameInput: HTMLInputElement) {
    this.serverContentInput.nativeElement.value = 'this is hardcoded!';
    this.serverCreated.emit({
      serverName: nameInput.value,
      serverContent: this.serverContentInput.nativeElement.value
    });
  }

Verás que en el navegador, cualquier cosa que escribas en el input del Server Content tomará el valor que le has dado en el archivo TS, lo cual seguramente no es el comportamiento deseado. Así que aléjate de esta técnica, porque Angular ofrece maneras mucho más apropiadas de acceder al DOM.

Cómo proyectar contenido con la directiva ngContent

Existe otra manera (¡sí, otra más!🤯) de pasar información de un sitio a otro. Para esta parte vamos a trabajar con el componente ServerElement. En su template vemos que tenemos un <p> que mostrará un elemento HTML u otro (mediante ngIf) dependiendo de si es un server o un blueprint lo que el usuario ha añadido. 

Un bloque de código como ese <p>, que contiene otros elementos HTML dentro de él, podríamos considerarlo como un bloque de HTML complejo, porque no es únicamente una línea de código. En ocasiones necesitaremos pasar ese bloque de código cuando utilizamos el componente que lo contiene (el ServerElementComponent) dentro de otro componente (el AppComponent). Para eso podemos usar al directiva ngContent

1. Para entender bien este ejemplo, cortamos el <p> del ServerElementComponent y lo pegamos en la template del AppComponent, dentro del selector del componente ServerElement.

      <app-server-element *ngFor="let serverElement of serverElements"
                          [srvElement]="serverElement">
        <p class="card-text">
          <strong *ngIf="element.type === 'server'" style="color: red">
                  {{ element.content }}
          </strong>
          <em *ngIf="element.type === 'blueprint'">{{ element.content }}</em>
        </p>
      </app-server-element>

2. Si guardas y vas a tu navegador, verás que la consola te devuelve un error. Y es que para hacerlo funcionar, tenemos que reemplazar la variable local anterior (element) por la usada en este template (serverElement).

<strong *ngIf="serverElement.type === 'server'" style="color: red">
        {{ serverElement.content }}
</strong>
<em *ngIf="serverElement.type === 'blueprint'">{{ serverElement.content }}</em>

Con este cambio, los errores en la consola deberían desaparecer. Esto no es suficiente para que nuestro código funcione, ya que ahora si vas al navegador, verás que Angular no te lanza ningún error, pero ahora ignora todo contenido que añadamos.

ngContent sin aplicar

Ése es el comportamiento de Angular por defecto: cualquier cosa que vaya entre las etiquetas de apertura y cierre de tus HTML custom elements será ignorado por Angular y por tanto no incluido en el DOM  <app-tu-selector> contenido ignorado aquí </app-tu-selector> ) . Pero esto tiene solución: ¡ngContent al rescate! 🚁

⚓ ngContent es una directiva especial de Angular que tiene aspecto de etiqueta HTML y que se coloca en el lugar donde queremos representar nuestro contenido. Es como colocar un ancla que informa a Angular de que desde ese lugar queremos proyectar cierto contenido.

3. Así que para hacerlo funcionar vamos a nuestro archivo server-element.component.html y añadimos la directiva ngContent donde antes estaba nuestro <p>, porque ése es el lugar donde queremos representar (en inglés, render) nuestro contenido.

<div class="card">
  <div class="card-header">{{ element.name }}</div>
  <div class="card-body">
    <ng-content></ng-content>
  </div>
</div>

Así, esa Directiva en forma de etiqueta HTML, nos hace de ancla para que Angular, desde el app.component.html, sepa de dónde viene el contenido incluido entre la etiqueta de apertura y cierre del ServerElementComponent.

En tu navegador, verás que el contenido del blueprint ha perdido su color azul. Esto es así por el tema que hemos visto en secciones anteriores sobre encapsulación de estilos. Lo voy a dejar así, pero si quisiéramos que el párrafo volviese a ser azul, tendríamos que migrar los estilos del server-element.component.css (los que se aplican al <p>al app.component.css.

👉 El ngContent es especialmente útil para construir widgets re-utilizables, como pestañas (en inglés, tabs)

Especificidad con el atributo select

Si queremos, podemos ser más específicos con el ngContent y añadirle un atributo llamado select y darle el valor que queramos, algo así como un identificador reforzado. Puede tomar forma de atributo HTML, elemento HTML o clase de CSS.

    <ng-content select="[my-specific-content]"></ng-content>
    <ng-content select="my-specific-content"></ng-content>
    <ng-content select=".my-specific-content"></ng-content>

Y ahora ya podríamos añadirlo al contenido proyectado, en la modalidad que hayamos elegido.

¡BRAVO! 

Y hasta aquí la parte #2 de esta saga sobre el uso avanzado de componentes y databinding en Angular. Si quieres seguir aprendiendo, aquí tienes la parte #3. Si te queda alguna duda, ¡nos vemos en los comentarios!

Sobre la autora de este post

Soy Rocío, una abogada reconvertida en programadora. Soy una apasionada de aprender cosas nuevas y ferviente defensora de que la única manera de ser feliz es alcanzando un equilibrio entre lo que te encanta hacer y lo que te saque de pobre. Mi historia completa, aquí. 

Más recursos de aprendizaje

En mi experiencia, la manera más eficaz para aprender Angular es combinando varias vías de aprendizaje. Uno de mis métodos favoritos son los vídeo-cursos y mi plataforma predilecta para eso es Udemy. He hecho varios cursos pero sólo recomiendo aquellos que verdaderamente me han sido útiles. Aquí van:

angular the complete guide - curso Max S.

  Max Schwarzmüller

curso angular fernando herrera

   Fernando Herrera

Si necesitas apoyo en forma de libro, puede que éstos te sirvan de ayuda:

libro 1 angular
libro 2 angular

La programación es un mundo que evoluciona a una velocidad de vértigo. Los autores de estos libros lo saben, por eso suelen encargarse de actualizar su contenido regularmente. Asegúrate de que así sea antes de adquirirlos 😌.

Participo en el programa de afiliados de Udemy y Amazon, lo que significa que, si compras alguno de estos cursos y/o libros, yo me llevaré una pequeña comisión y a ti no costará nada extra. Vamos, lo que se dice un win-win 😊.

Otros artículos que pueden interesarte

Guía de iniciación al data binding en Angular
¿Qué es el databinding?El databinding es la forma que tiene Angular para permitirnos mostrar contenido dinámico en lugar de estático (en inglés, hardcoded). Podríamos[...]
Cómo aprendí a programar cuando estaba «programada» para ser de letras
[tcb-script src="https://player.vimeo.com/api/player.js"][/tcb-script]A nadie le gusta su trabajo. Eso es lo que me decía a mí misma cuando conseguí mi primer[...]
Cómo construir un pop-up con vanilla JavaScript
¿Qué vamos a construir? Vamos a construir una ventana emergente, popup, overlay, dialog o como quieras llamarla, pero esos 3 nombres son[...]
Si crees que este post puede serle útil a alguien, por favor, ¡compártelo!:

Deja un comentario

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Esta web utiliza cookies para asegurar que se da la mejor experiencia al usuario. Si continúas utilizando este sitio se asume que estás de acuerdo. más información

Los ajustes de cookies en esta web están configurados para «permitir las cookies» y ofrecerte la mejor experiencia de navegación posible. Si sigues usando esta web sin cambiar tus ajustes de cookies o haces clic en «Aceptar», estarás dando tu consentimiento a esto.

Cerrar