ng-content: los documentos ocultos

Descargo de responsabilidad: este artículo es sobre Angular, en oposición a AngularJS. Esto significa que se aplica a Angular 2.x, 4.x y, con suerte, a versiones futuras.

Si alguna vez ha intentado escribir un componente reutilizable en Angular, probablemente haya tenido que proyectar contenido dentro de él. Descubrió , encontró algunas publicaciones de blog al respecto y consiguió que su componente funcionara. Este artículo lo guiará a través de las peculiaridades y los casos de uso avanzados para la proyección de contenido para responder preguntas que siguen apareciendo en el equipo de Clarity y aparentemente también en el repositorio de GitHub de Angular.

Normalmente, comenzaría este artículo indicándole la documentación oficial de la función que estoy describiendo, pero para la proyección de contenido aún no existe ... ¡Así que entremos!

Un simple ejemplo

Utilizaremos un solo ejemplo a lo largo de este artículo, que muestra diferentes formas de proyectar contenido y varios casos extremos. Debido a que muchas de las preguntas están relacionadas con el ciclo de vida del componente en Angular, nuestro componente principal tendrá un contador que muestra la cantidad de veces que se ha instanciado:

Usaremos este componente Counter y lo proyectaremos de cualquier manera que podamos pensar en alguna variación de este componente Wrapper, que solo lo proyecta en una caja con estilo:

Solo verifiquemos este trabajo como se esperaba, colocando tres contadores dentro del contenedor:

Muestra 1, 2 y 3 como se esperaba. Hasta aquí todo bien.

De ahora en adelante, probaremos con un solo contador la simplicidad. Entonces, el HTML predeterminado de nuestra aplicación será:

Proyección dirigida

A veces desea que se proyecten diferentes elementos secundarios de su contenedor en diferentes partes de su plantilla. Para manejar esto, admite un atributo select que le permite proyectar contenido específico en lugares específicos. Este atributo toma un selector CSS (my-element, .my-class, [my-attribute], ...) para que coincida con los elementos secundarios que desea. Si incluye un ng-content sin un atributo select, servirá como un todo y recibirá todos los elementos secundarios que no coincidan con ninguno de los otros elementos ng-content. Larga historia corta:

El contador se proyecta correctamente en el segundo cuadro azul, mientras que el niño que no es un contador termina en el cuadro rojo. Tenga en cuenta que el contenido ng objetivo tiene prioridad sobre el conjunto, incluso si está detrás de él en la plantilla.

ngProjectAs

A veces su componente interno está oculto dentro de otro componente más grande. A veces solo necesita envolverlo en un recipiente adicional para aplicar ngIf o ngSwitch. Por alguna razón, a menudo sucede que su componente interno no es un hijo directo del envoltorio. Para simular eso, envuelvamos nuestro componente Contador en un y veamos qué sucede con nuestra proyección objetivo:

Nuestro componente Counter ahora se proyecta en el elemento rojo de captura general, porque el contenedor ng a su alrededor ya no coincide con select = "counter". Para remediar esto, tenemos que usar el atributo ngProjectAs, que se puede poner en absolutamente cualquier elemento y le permite "disfrazar" cualquier elemento para fines de proyección de contenido. Toma exactamente el mismo tipo de selectores que el atributo select en .

Entonces, manteniendo nuestro contenedor igual que antes (con los cuadros azul y rojo), ahora podemos usar este nuevo atributo en nuestra aplicación:

El mostrador está de vuelta en la caja azul, justo como queríamos.

Hora de empujar y pinchar

Ok, tenemos los casos más simples funcionando. Pero, ¿qué pasa si pensamos fuera de la caja (guiño guiño)? Comencemos con un experimento simple: coloque dos bloques en nuestra plantilla sin selectores. ¿Qué debería pasar? ¿Terminaremos con dos contadores o solo uno? Si terminamos con dos, ¿muestran 1 y 1 o 1 y 2?

La respuesta es que tenemos un único contador en el último , ¡el otro está vacío! Experimentemos un poco más antes de intentar explicar por qué. Estamos llegando al ejemplo que generó la mayoría de las preguntas en GitHub, con mucho: ¿qué pasa si envuelvo mi en un * ngIf?

A primera vista, parece funcionar bien. Pero si lo activa y desactiva con el botón, notará que el contador no aumenta. Esto significa que nuestro componente Contador se instancia una sola vez, nunca se destruye ni se recrea. ¿No es eso lo contrario de lo que * ngIf se supone que debe hacer? Verifiquemos con * ngFor para ver si tenemos el mismo problema:

Lo mismo que nuestro caso múltiple , ¡solo el último obtiene un contador! ¿Por qué no funciona como esperábamos?

La explicación

no "produce" contenido, simplemente proyecta contenido existente. Piense en ello como una variación de node.appendChild (el) o la conocida versión de JQuery $ (node) .append (el): con estos métodos, el nodo no se clona, ​​simplemente se mueve a su nueva ubicación. Debido a esto, el ciclo de vida del contenido proyectado está vinculado a donde se declara, no donde se muestra.

Hay dos razones para este comportamiento: la coherencia de las expectativas y el rendimiento. Lo que significa "coherencia de expectativas" es que, como desarrollador, puedo leer el código de mi aplicación y adivinar su comportamiento en función del código que he escrito. Digamos que escribí este código:

Obviamente, el contador se instanciará una vez. Pero ahora, digamos que en lugar de mi envoltorio estático, uso uno de una biblioteca de terceros:

Si la biblioteca de terceros tuviera la capacidad de controlar el ciclo de vida de mi contador, no tendría forma de saber cuántas veces se ha instanciado. La única manera de saberlo sería mirar el código de la biblioteca de terceros y estar a merced de los cambios internos que realicen. Hacer cumplir el ciclo de vida para que se vincule al componente de mi aplicación en lugar del envoltorio significa que puedo asumir con seguridad que mi contador se instanciará una sola vez, sin saber nada sobre el código real de la biblioteca de terceros.

La parte de rendimiento es mucho más obvia. Debido a que ng-content solo mueve elementos, se puede hacer en tiempo de compilación en lugar de tiempo de ejecución, lo que reduce significativamente el trabajo de la aplicación real (especialmente cuando se compila con anticipación, lo que angular-cli hace por defecto).

La solución

Para permitir que el contenedor controle la creación de instancias de sus elementos secundarios, debemos proporcionarle una plantilla para el contenido, en lugar del contenido en sí. Esto se puede hacer de dos maneras: usando el elemento alrededor de nuestro contenido, o usando una directiva estructural con la sintaxis "star", como * myContent. Para simplificar, usaremos la sintaxis en nuestros ejemplos, pero aquí puede encontrar toda la información que necesita sobre el prefijo de estrella. Nuestra nueva aplicación se ve así:

El contenedor ya no puede usar , ya que recibe una plantilla. Necesita acceder a la plantilla con @ContentChild y usar ngTemplateOutlet para mostrarla:

¡Nuestro contador ahora se incrementa correctamente cada vez que lo ocultamos y lo mostramos nuevamente! Ahora intentemos de nuevo con * ngFor:

Un contador en cada cuadro, que muestra 1, 2 y 3. ¡Exactamente lo que estábamos buscando!

Esperemos que estas explicaciones estén pronto en la documentación Angular, pero mientras tanto espero que esta inmersión profunda en haya respondido la mayoría de sus preguntas. En particular, explica por qué las bibliotecas angulares solicitan plantillas con tanta frecuencia en lugar de solo proyectar plantillas. Hace que la API sea un poco más detallada, pero les abre muchas más posibilidades: cargar de forma diferida el contenido de las pestañas, duplicar títulos en diferentes lugares de un componente, etc.

Si tiene curiosidad y se siente como un científico loco hoy, continúe e intente experimentos más avanzados como proyectar contenido dentro de una . Todos los resultados son consistentes con la explicación anterior, pero algunos de estos patrones complicados tienen aplicaciones útiles. ¡Quizás lleguemos a ellos en una futura publicación de blog!