¿Cómo comunicar controladores Stimulus a través de Outlets?

Maximiliano Mendivil
Unagi
Published in
4 min readApr 26, 2024
¿Cómo comunicar controladores Stimulus a través de Outlets?

Stimulus nos provee de dos mecanismos para comunicar controladores entre sí: a través del lanzamiento de eventos personalizados y a través del uso de Outlets.

Ya hablamos en este artículo de la comunicación entre controladores a través del lanzamiento de eventos personalizados, por lo cual ahora nos vamos a centrar en la comunicación entre controladores a través de Outlets.

Los Outlets son conceptualmente similares a los Targets con la diferencia de que nos permiten acceder a una instancia de otro controlador (además de al elemento HTML al cual está asociado) a través de un selector CSS.

Veamos a través de un ejemplo concreto cómo se definen los Outlets y cómo funciona la comunicación entre los controladores.

Supongamos que tenemos un listado de películas y queremos realizar una búsqueda a través de su nombre.

Listado de películas con un buscador

El código HTML para construir el listado y el buscador se ve como lo siguiente:

<input type="text" class="form-control" placeholder="Buscar película">

<p class="border-bottom mb-0 py-3 result">
El secreto de sus ojos
</p>
<p class="border-bottom mb-0 py-3 result">
Nueve Reinas
</p>
<p class="border-bottom mb-0 py-3 result">
Esperando a la carroza
</p>
<p class="border-bottom mb-0 py-3 result">
Papá se volvió loco
</p>
<p class="border-bottom mb-0 py-3 result">
Un novio para mi mujer
</p>

Para mostrar u ocultar una película en base a la búsqueda realizada, vamos a definir un controlador Stimulus. El mismo lo definimos de la siguiente manera:

class MovieController extends Controller {
static targets = [ "title" ]

showOrHide(query) {
const matches = this.titleTarget.innerText.toLowerCase().includes(query)

this.titleTarget.classList.toggle("d-block", matches)
this.titleTarget.classList.toggle("d-none", !matches)
}
}

El controlador tiene como target el título de la película y además provee un método que se va a encargar de mostrar u ocultar la película en base a la búsqueda realizada.

Aclaración: se decidió tener una instancia de MovieController por cada película con fines de mostrar que a través de los outlets se puede acceder a más de una instancia de un mismo controlador. Probablemente, en un caso real, sea mejor tener un único controlador para manejar el filtrado de todas las películas.

Para conectar los títulos de las películas con el controlador, agregamos los data attributes correspondientes en la vista:

<input type="text" class="form-control" placeholder="Buscar película">

<p ... data-controller="movie" data-movie-target="title">
El secreto de sus ojos
</p>
<p ... data-controller="movie" data-movie-target="title">
Nueve Reinas
</p>
<p ... data-controller="movie" data-movie-target="title">
Esperando a la carroza
</p>
<p ... data-controller="movie" data-movie-target="title">
Papá se volvió loco
</p>
<p ... data-controller="movie" data-movie-target="title">
Un novio para mi mujer
</p>

Ahora bien, vamos a definir un controlador para manejar el buscador de las películas. Lo que queremos hacer es delegarle a cada película la responsabilidad para que se muestre u oculte en el listado en base a la búsqueda realizada.

class SearchBarController extends Controller {
static targets = [ "input" ]

search() {
/* Delegar llamado a cada una de las películas para que se muestre u oculte
en base al contenido de la barra de búsqueda */
}
}

Para poder delegar el llamado, necesitamos definir como outlet cada uno de las películas. Esto lo podemos hacer añadiendo un data attribute data-search-bar-movie-outlet a nuestro input de la siguiente manera:

<input 
...
data-controller="search-bar"
data-search-bar-target="input"
data-action="search-bar#search"
data-search-bar-movie-outlet=".movie">

<p class="... movie" data-controller="movie" data-movie-target="title">
El secreto de sus ojos
</p>
<p class="... movie" data-controller="movie" data-movie-target="title">
Nueve Reinas
</p>
<p class="... movie" data-controller="movie" data-movie-target="title">
Esperando a la carroza
</p>
<p class="... movie" data-controller="movie" data-movie-target="title">
Papá se volvió loco
</p>
<p class="... movie" data-controller="movie" data-movie-target="title">
Un novio para mi mujer
</p>
class SearchBarController extends Controller {
static targets = [ "input" ]
static outlets = [ "movie" ]

search() {
this.movieOutlets.forEach(element => {
element.showOrHide(this.inputTarget.value.toLowerCase());
});
}
}

De esta manera quedaron conectados ambos controladores y delegadas correctamente las responsabilidades.

Dejo algunas observaciones acerca de la definición de los outlets:

  • El nombre del outlet debe ser el mismo que el nombre del controlador del elemento asociado. Para el caso del ejemplo, debe ser movie.
  • El valor del outlet es un selector CSS. Puede hacer referencia a uno o varios elementos. Para el caso del ejemplo, hacemos referencia a todos los elementos que tienen la clase movie.
  • Los outlets se deben definir dentro de la variable estática outlets, similar a cómo se definen los targets.

El resultado final se ve de la siguiente manera:

Y así es cómo podemos comunicar diferentes controladores a través de los Outlets y mantener nuestros controladores simples y con una única responsabilidad. ¿Se te ocurre algún caso donde puedas aplicar los Outlets? Compartilo con nosotros en los comentarios.

Unagi brinda servicios de desarrollo de software en Ruby y React. Podés conocer más de nosotros en cualquiera de nuestros canales.

--

--