State Management: Gestión de estados - Práctica

Práctica orientada al proyecto real que estamos trabajando desde el capítulo de estilos, donde implementaremos mediante el uso de la gestión de estados el apartado de capítulos que estaba pendiente de implementarlo.

Anteriormente hemos obtenido conocimientos acerca de estos conceptos:

Estos nos van a proporcionar las herramientas necesarias para poder hacer un componente tipo accordion donde vamos a visualizar la lista de capítulos que vamos a proporcionar.

No vamos a utilizarlos todos, pero podéis experimentar con las diferentes opciones ya que se puede implementar haciendo uso de todas ellas.

Contenido del capítulo

Lo que aprenderemos en este capítulo se engloba en estos puntos importantes.

Requisitos antes de empezar

Esto será lo que necesitamos para poder abordar con éxito este capítulo práctico:

export const chaptersData = [
    {
        "question": "1.- Introducción a Qwik",
        "answer": "Lorem ipsum dolor, sit amet consectetur adipisicing elit. ...",
        "open": true
    },
    {
        "question": "2.- Enrutamiento",
        "answer": "Lorem ipsum dolor, sit amet consectetur adipisicing elit. ...",
        "open": false
    },
    {
        "question": "3.- Componentes",
        "answer": "Lorem ipsum dolor, sit amet consectetur adipisicing elit. ...",
        "open": false
    },
    {
        "question": "4.- Layouts / Plantillas",
        "answer": "Lorem ipsum dolor, sit amet consectetur adipisicing elit. ...",
        "open": false
    },
    {
        "question": "5.- Estilos",
        "answer": "Lorem ipsum dolor, sit amet consectetur adipisicing elit. ...",
        "open": false
    },
    {
        "question": "6.- SSR - Server Side Rendering",
        "answer": "Lorem ipsum dolor, sit amet consectetur adipisicing elit. ...",
        "open": false
    },
    {
        "question": "7.- Gestión de Estado",
        "answer": "Lorem ipsum dolor, sit amet consectetur adipisicing elit. ...",
        "open": false
    },
    {
        "question": "8.- Consumiendo APIs REST / GraphQL",
        "answer": "Lorem ipsum dolor, sit amet consectetur adipisicing elit. ...",
        "open": false
    }
];

Resultado final

Una vez completado este capítulo, conseguiremos mostrar la lista de todos los capítulos con el componente <Accordion />, que mostrará todos los títulos de cada uno de los capítulos y dentro del contenido oculto, información de lo que trata cada uno de ellos.

El resultado para verlo con más detalle, lo podéis encontrar en la siguiente URL:

https://shorten-up.vercel.app/MMjKgcH_e-

Acerca del contenido final de los capítulos

En las capturas se ve la información que existe actualmente, y puede que estos capítulos o se amplie o cambien de orden.

LO MÁS IMPORTANTE aquí no será el propio contenido, si no el modo de mostrarlo y proceso que hay que llevar para dejar algo tan presentable como el accordion que se crea en este capítulo

Cargar y visualizar el contenido en useSignal {#load-content-in-use-signal}

Comenzamos con este apartado en el que vamos a introducir los datos que os he proporcionado en un fichero, como fuente de datos para poder usarlo en nuestro proyecto.

Cogemos la información de los capítulos y lo añadimos tal cual en un fichero que lo llamaremos chapters.ts introduciéndolo en src/data:

src/
└── data/
|    └── chapters.ts
|
└── routes/
    └── ...

Una vez que ya hemos añadido la información en el fichero mencionado, vamos al fichero src/routes/chapters/index.tsx y añadimos esa información dentro de un useSignal, con el objetivo de poder controlar el estado de los desplegables, si están abiertos o no, dependiendo de lo que se especifique en la propiedad open de cada elemento.

Antes de empezar a renderizar con la estructura deseada, comprobamos que los datos se visualizan correctamente.

Implementamos estos cambios partiendo de esta base:

import { component$ } from "@builder.io/qwik";
import type { DocumentHead } from "@builder.io/qwik-city";

export default component$(() => {
  return (
    <>
      <div class="container mt-2">
        <h1 class="page-title">Listado de cap&iacute;tulos</h1>
        <hr />
      </div>
    </>
  );
});

export const head: DocumentHead = {
  ...(SIN CAMBIOS)
};

Dejando de la siguiente forma el código:

import { component$, useSignal } from "@builder.io/qwik";
import type { DocumentHead } from "@builder.io/qwik-city";
import { chaptersData } from "~/data/chapters";

export default component$(() => {
  const chapters = useSignal(chaptersData);
  return (
    <>
      <div class="container mt-2">
        <h1 class="page-title">Listado de cap&iacute;tulos</h1>
        <hr />
        { JSON.stringify(chapters.value)}
      </div>
    </>
  );
});

export const head: DocumentHead = {
  ...(SIN CAMBIOS)
};

Guardamos los cambios y se visualizarán de esta manera:

Como se puede observar, no queda muy bonito, pero ahora lo importante es ver que funciona y como se puede ver, obtiene y renderiza esos datos, por lo que pasamos al siguiente paso donde vamos a crear el componente Accordion.

Componente Accordion {#accordion-component-html-to-qwik}

Para poder crear nuestro componente, vamos a usar como ejemplo la estructura de la plantilla HTML (recurso mostrado al principio del capítulo) para ver como se construye con todo cerrado y abierto.

Accedemos a la página de Capítulos y esto es lo que visualizamos:

Lo que nos encontramos es una estructura principal Accordion que está compuesta por un número determinado de elementos accordion en dos estados:

Una vez que tenemos más contexto, vamos a ver como está formado en lo que se refiere a la estructura HTML.

Vamos a Herramientas del desarrollador de nuestro navegador haciendo click (1) sobre la estructura principal donde se encuentra el elemento Accordion y seleccionamos Inspeccionar:

Y dentro de estas opciones seleccionamos Elements (Elementos) (1), ponemos el cursor sobre <section class="accordion-container mb-5"... (2) y en el momento que hacemos, se estará seleccionando toda la estructura del contenedor (3) donde tenemos los capítulos.

Hacemos click derecho sobre <section class="accordion-container mb-5"... y seleccionamos la opción Edit as HTML (lo que sería Editar como HTML):

Seleccionamos todo el contenido que se ha habilitado para editarlo y lo copiamos:

Y el contenido copiado sera lo siguiente (se añaden los dos primeros capítulos, que son las dos variantes, la primera desplegado y la segunda cerrado):

<section class="accordion-container mb-5" id="accordion-container">
    <div class="accordion-1">
        <h1 class="accordion-page item-active">1.- Introducción a Qwik</h1>
        <div class="accordion-body" style="display: block;">
            <p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. ...</p>
        </div>
    </div>
    <hr class="hr-line">
    <div class="accordion-2">
        <h1 class="accordion-page">2.- Enrutamiento</h1>
        <div class="accordion-body">
            <p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. ...</p>
        </div>
    </div>
    <hr class="hr-line">
    ...
</section>

Lo implementamos en Qwik en src/routes/chapters/index.tsx:

import { component$, useSignal } from "@builder.io/qwik";
import type { DocumentHead } from "@builder.io/qwik-city";
import { chaptersData } from "~/data/chapters";


export default component$(() => {
  const chapters = useSignal(chaptersData);
  return (
    <>
      <div class="container mt-2">
        <h1 class="page-title">Listado de cap&iacute;tulos</h1>
        <hr />
        <section class="accordion-container mb-5" id="accordion-container">
          <div class="accordion-1">
              <h1 class="accordion-page item-active">1.- Introducción a Qwik</h1>
              <div class="accordion-body" style="display: block;">
                  <p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. ...</p>
              </div>
          </div>
          <hr class="hr-line" />
          <div class="accordion-2">
        <h1 class="accordion-page">2.- Enrutamiento</h1>
        <div class="accordion-body">
            <p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. ...</p>
        </div>
    </div>
    <hr class="hr-line" />
        </section>
      </div>
    </>
  );
});

export const head: DocumentHead = {
  ...(SIN CAMBIOS)
};

Y esta será la apariencia:

Todavía nos quedan unos pequeños ajustes para que se vaya pareciendo más a la plantilla original, mostrando los estados desplegado (Capítulo 1) y cerrado (Capítulo 2) asociado al código HTML que hemos copiado.

Para realizar esto debemos añadir los estilos asociados a esa ruta, que podemos encontrar en el fichero chapters.css:

.accordion-heading {
    border-bottom: #777;
    padding: 20px 60px;
}

.accordion-container {
    display: flex;
    justify-content: center;
    flex-direction: column;
}

.hr-line {
    width: 100%;
    margin: auto;
}

/* Style the buttons that are used to open and close the accordion-page body */
.accordion-page {
    /* background-color: #eee; */
    color: #444;
    cursor: pointer;
    padding: 30px 20px;
    width: 100%;
    border: none;
    outline: none;
    transition: 0.4s;
    margin: auto;
    font-size: 1rem;
}

.accordion-body {
    margin: auto;
    /* text-align: center; */
    width: 100%;
    padding: auto;
}

/* Add a background color to the button if it is clicked on (add the .active class with JS), and when you move the mouse over it (hover) */
.item-active,
.accordion-page:hover {
    background-color: #F9F9F9;
}

/* Style the accordion-page panel. Note: hidden by default */
.accordion-body {
    padding: 0 18px;
    background-color: white;
    display: none;
    overflow: hidden;
}

.accordion-page:after {
    content: '\02795';
    /* Unicode character for "plus" sign (+) */
    font-size: 13px;
    color: #777;
    float: right;
    margin-left: 5px;
}

.item-active:after {
    content: "\2796";
    /* Unicode character for "minus" sign (-) */
}

.open {
    display: block;
}

Creamos src/routes/chapters/chapters.css, pegamos todo el contenido ahí (sin realizar ningún cambio).

src/
└── routes/
    └── chapters
            └── index.tsx
            └── chapters.css
...

Y ahora, para usarlo en la ruta, en el fichero src/routes/chapters/index.tsx debemos de añadir lo siguiente:

import { component$, useSignal, useStyles$ } from "@builder.io/qwik";
...
import chaptersStyles from './chapters.css?inline';

export default component$(() => {
  ...
  useStyles$(chaptersStyles);
  return (
    <>
      ....
    </>
  );
});

...

Y una vez que guardemos los cambios, como se puede apreciar ya se aplican correctamente los estilos, mostrando el capítulo 1 desplegado por usar la clase item-active y el capítulo 2 cerrado POR no disponer de esa clase que si está en el capítulo 1.

Llegados a este punto, ya sabemos las características que tiene que tener un elemento del accordion, donde mostraremos la información de los capítulos uno a uno, para mostrar toda su información (desplegado) o la básica (cerrado).

Antes de implementar la funcionalidad para desplegar / cerrar la información vamos a conseguir que se renderice todo el contenido con los 8 capítulos que os he proporcionado de manera dinámica, mediante un bucle.

Tenemos que tener en cuenta estos detalles:

Vamos paso por paso:

Renderizar los elementos de manera básica

Generamos los elementos de 1 a n, teniendo en cuenta la información de los capítulos, con lo que tenemos 8 elementos:

Por lo tanto, cambiamos lo actual:

<section class="accordion-container mb-5" id="accordion-container">
  <div class="accordion-1">
      <h1 class="accordion-page item-active">1.- Introducción a Qwik</h1>
      <div class="accordion-body" style="display: block;">
          <p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. ...</p>
      </div>
  </div>
  <hr class="hr-line" />
  <div class="accordion-2">
    <h1 class="accordion-page">2.- Enrutamiento</h1>
    <div class="accordion-body">
        <p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. ...</p>
    </div>
  </div>
  <hr class="hr-line" />
</section>

Por lo siguiente:

<section class="accordion-container mb-5" id="accordion-container">
  {chapters.value.map((chapter, index) => {
    return (
      <div key={`accordion-${index + 1}_${new Date().toISOString()}`}>
        <div class={`accordion-${index + 1}`}>
          <h1
            class={`accordion-page ${
              chapter.open ? 'item-active' : ''
            }`}
          >
            {chapter.question}
          </h1>
          <div class={`accordion-body ${chapter.open ? 'open' : ''}`}>
            <p>{chapter.answer}</p>
          </div>
        </div>
        <hr class="hr-line" />
      </div>
    );
  })}
</section>

Y el resultado será el siguiente, teniendo en cuenta que el capítulo 1 en la fuente de datos es el único que tiene el valor true en la propiedad open debería de mostrarse únicamente este capítulo desplegado:

Ahora lo que nos falta es darle "vida" a este elemento accordion, dando opción a desplegar / cerrar las opciones bajo nuestra demanda.

Desplegar / Cerrar mediante la gestión del estado

Para efectuar la acción de desplegar / cerrar la información extra de un capítulo lo pondremos en marcha, siempre y cuando hagamos click sobre el elemento <h1> donde tenemos los títulos de los capítulos.

Vamos a implementar el evento de cuando hacemos click mediante onClick$.

Más detalles sobre eventos

Veremos todo sobre los eventos en el Capítulo 14 - Eventos donde se aprenderá que son los eventos, como funcionan y como trabajar con los diferentes en Qwik, por lo que paciencia.

Cambiamos el apartado donde tenemos el <h1>:

<h1 class={`accordion-page ${
  chapter.open ? "item-active" : ""
}`}
>
    {chapter.question}
</h1>

Por lo siguiente, donde añadimos un mensaje para que se muestre en la consola del navegador:

<h1 class={`accordion-page ${
  chapter.open ? "item-active" : ""
}`}
onClick$={()=> console.log(`accordion-${index + 1}`)}
>
    {chapter.question}
</h1>

Y una vez guardados los cambios, en apariencia no ha cambiado nada, pero si hacemos click en el capítulo 1, 2 y 3, debemos de ver lo siguiente:

Esto se muestra debido a que hemos definido que muestre ese texto asociado al elemento:

onClick$={()=> console.log(`accordion-${index + 1}`)}

Modificando el estado en open del item seleccionado

Ahora que ya tenemos claro que estamos ejecutando la acción relacionada al elemento donde hacemos la acción del click, debemos de modificar el estado con la actualización en open de lo seleccionado pasando de true a false y viceversa para posteriormente actualizar el contenedor principal chapters.

Aplicamos en el código estos cambios, dentro de onClick$:

onClick$={()=> {
  // Modificamos la propiedad `open` al valor inverso
  chapter.open = !chapter.open;
  
  // Actualizamos el contenedor donde almacenamos todos los capítulos
  chapters.value = chapters.value.map((item) => {
    if (item.question === chapter.question) {
      return chapter;
    }
    return item;
  });
}}

Guardamos los cambios y lo que va a pasar es que siempre que hagamoc click en un título que corresponde a cualquier capítulo, se va a desplegar / cerrar dependiendo de su estado.

Accedemos al proyecto y este sería el estado en base a los datos iniciales:

Ahora haciendo click en el Capítulo 1:

Conseguimos que la información extra se cierre:

Aparte de esto, si nos fijamos en el registro de la consola del navegador, se aprecia el cambio en el renderizado debido a este cambio:

Si hacemos click de nuevo, se despliega (aparte de mostrar un nuevo registro en el log):

Os dejo que probéis con las otras opciones. Analizad los mismos aspectos que os he mencionado.

Refactorización

Ahora que nuestro contenido de los capítulos funciona tal como debería, ya estamos preparados para mejorar la calidad del código y hacerlo más reutilizable.

Para poder usar este contenido en cualquiera de las rutas de nuestro proyecto, lo primero que vamos a hacer es crear un fichero donde almacenaremos toda la información del componente Accordion y esto lo haremos en src/components/shared/accordion/index.tsx junto con otro que llamaremos accordion.css que servirá para definir los estilos (aquí copiamos lo que hay en src/routes/chapters/chapters.css):

src/
└── components/
      └── shared
          └── accordion
                └── index.tsx
                └── accordion.css 
...

Y lo que hacemos en este componente es añadir el siguiente código, que explicaré a continuación:

// 1
import { Signal, component$, useStyles$ } from '@builder.io/qwik';

// 2
import accordionStyles from './accordion.css?inline';

// 3
export interface IAccordionItem {
  answer: string;
  question: string;
  open: boolean;
}
export interface AccordionProps {
  data: Signal<IAccordionItem[]>;
}

// 4
export default component$<AccordionProps>((props: AccordionProps) => {
  // 5
  const { data: accordionItems } = props;

  // 6
  useStyles$(accordionStyles);

  // 7
  return (
    <section class='accordion-container mb-5' id='accordion-container'>
      {accordionItems.value.map((chapter, index) => {
        return (
          <div key={`accordion-${index + 1}_${new Date().toISOString()}`}>
            <div class={`accordion-${index + 1}`}>
              <h1
                class={`accordion-page ${chapter.open ? 'item-active' : ''}`}
                onClick$={() => {
                  // Modificamos la propiedad `open` al valor inverso
                  chapter.open = !chapter.open;

                  // Actualizamos el contenedor donde almacenamos todos los capítulos
                  accordionItems.value = accordionItems.value.map((item) => {
                    if (item.question === chapter.question) {
                      return chapter;
                    }
                    return item;
                  });
                }}
              >
                {chapter.question}
              </h1>
              <div class={`accordion-body ${chapter.open ? 'open' : ''}`}>
                <p>{chapter.answer}</p>
              </div>
            </div>
            <hr class='hr-line' />
          </div>
        );
      })}
    </section>
  );
});

Os detallo a continuación los diferentes apartados definidos:

  1. Importamos los elementos necesarios como Signal y useStyles, donde el primero lo usamos como propiedad del AccordionProps, que será la información que pasemos desde el padre (en este caso desde la ruta /chapters) y el segundo será para especificar los estilos, tal y como se ha realizado en src/routes/chapters/index.tsx aunque ahora hará referencia al fichero de estilos que se encuentre en el directorio de este componente.
  2. Importamos los estilos en formato inline para añadirlos en el hook useStyles$.
  3. Definimos la estructura de los datos, del elemento individual del Acccordion con IAccordionItem y la estructura principal de los props con AccordionProps, donde pasaremos un Array de tipo IAccordionItem dentro de un useSignal, para obtener los cambios de estado (desplegar / cerrar la información extra).
  4. Componente principal, donde definimos que recibirá datos mediante props con la definición AccordionProps.
  5. Desestructuramos la propiedad data de los props y la renombramos como accordionItems para poder usarlo desde ahora con ese nombre.
  6. Añadimos los estilos cargados que se han mencionado en (2).
  7. Código JSX que teníamos añadido dentro de src/routes/chapters/index.tsx con algunas pequeñas modificaciones, añadiendo accordionItems en vez de chapters.

Una vez analizados estos cambios, debemos de ir al directorio src/routes/chapters y realizar estas acciones:

import { component$, useSignal } from "@builder.io/qwik";
import type { DocumentHead } from "@builder.io/qwik-city";
import { chaptersData } from "~/data/chapters";
import Accordion from "~/components/shared/accordion";

export default component$(() => {
  const chapters = useSignal(chaptersData);
  return (
    <>
      <div class="container mt-2">
        <h1 class="page-title">Listado de cap&iacute;tulos</h1>
        <hr />
        <Accordion data={chapters}/>
      </div>
    </>
  );
});

export const head: DocumentHead = {
  title: "Capítulos",
  meta: [
    {
      name: "description",
      content: "Listado de capítulos que contiene el libro de Qwik",
    },
  ],
};

Guardamos los cambios y ahora probando, deberíamos de tener el mismo comportamiento. Si no es así, comprobad los pasos dados.

Con esto, lo único que nos queda es actualizar los datos de los capítulos con la información real.

Más sobre refactorizar

Lo que corresponde a refactorizar más, podríais llevar AccordionProps y IAccordionItem al apartado models dentro de un fichero que se podría llamar accordion.tsx aunque estos ya depende de si queréis o no.

Añadir los datos reales

Ahora que ya hemos implementado con los datos mock que os he proporcionado con la estructura de los capítulos, os añado el contenido completo desde el siguiente enlace:

https://shorten-up.vercel.app/6pSUJu1MZ-

Lo que tenemos que hacer es acceder al siguiente contenido, copiar la información y añadirla en src/data/chapters.tsx sustituyendo completamente la información de ese fichero.

Guardamos y deberíamos de ver la siguiente apariencia, con la información de los capítulos real y al completo:

Propuestas de mejora

En este apartado os muestro algunas ideas de cosas que podéis implementar en el proyecto con los conceptos que hemos aprendido hasta este momento:

Seguramente se os ocurran más cosas y espero que os animéis a implementarlas, ya que de esta manera váis a aprender un montón, y si las compartís, mejor que mejor.

Resultado de lo trabajado en este capítulo

El código que encontráis es el resultado final de todo el proceso realizado durante el capítulo. Os recomiendo que vayáis haciendo los pasos poco a poco y paso a paso para ir interiorizando todos los conceptos y en este caso, pienso que os vendría bien realizar pruebas con más ejemplos.

El enlace lo tenéis a continuación:
https://shorten-up.vercel.app/mKj8hwKMxq

¿Qué hemos aprendido en este capítulo?

Las habilidades que hemos obtenido aquí son:

Conclusión

Llegados a este momento, hemos ido implementando los conceptos aprendidos durante el libro junto con el repaso de los últimos conocimientos adquiridos para finalizar la conversión de la plantilla HTML a un proyecto Qwik.

Seguramente antes de iniciar la práctica del capítulo Qwik: Adaptando proyecto desde Plantilla HTML, teniáis bastantes dudas de como hacerlo.

Ahora mismo, estoy seguro que si cogéis cualquier plantilla HTML, la analizáis como os he enseñado, vais a poder adaptarla a Qwik sin ningún problema.

Finalizamos el capítulo y comenzamos con un tema super interesante, el que corresponde a consumir información de servidores remotos, es decir, consumir APIs donde lo haremos tanto con APIs REST como GraphQL.