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:
SSR
y aplicación en Qwik.useSignal
useStore
useComputed$
Context API
con useSignal
, useStore
y useComputed$
.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.
Lo que aprenderemos en este capítulo se engloba en estos puntos importantes.
useSignal
.useComputed$
.Esto será lo que necesitamos para poder abordar con éxito este capítulo práctico:
Descargar el proyecto
que hemos dejado desarrollado en la práctica del capítulo 5, donde hemos adaptado todo excepto el apartado de capítulos: https://shorten-up.vercel.app/5ms1wrP_vv.proyecto HTML
(https://shorten-up.vercel.app/uPDeBdhmIY) desde el que hemos empezado a trabajar.Datos de los capítulos
de ejemplo (al final del capítulo os añado todos y con la información real para que quede más real todo):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
}
];
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-
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
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í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í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
.
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:
Elemento desplegado (1)
: Donde se visualizan el título y el body con la información extendida y como se puede apreciar, el icono al estar extendido tenemos un -
por estar en estado abierto.Elemento cerrado (2)
: Será el estado base y hasta que no hagamos click sobre ese elemento, el icono no cambiará a -
y seguirá con +
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í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:
A
: Elemento número del accordion, empieza desde 1 hasta n. En este caso tenemos dos elementos, por lo que tenemos accordion-1
y accordion-2
.B
: En el título, si este elemento está con el valor true
en la propiedad open
se añade item-active
. Si es false
NO se añade nada.C
: Añade la opción de mostrar el bloque (display: block
) siempre y cuando la propiedad open
sea su valor true
. Si es false
como en el título, no añade nada. Este valor lo sustituimos englobando esos estilos dentro de una clase open
que se añade en este elemento.Vamos paso por paso:
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.
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$
.
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.
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:
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.inline
para añadirlos en el hook useStyles$
.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).props
con la definición AccordionProps
.data
de los props
y la renombramos como accordionItems
para poder usarlo desde ahora con ese nombre.(2)
.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:
src/routes/chapters/chapters.css
ya que no es necesario, por tener los estilos definidos en src/components/shared/accordion/accordion.css
y también la referencia a el para usar como estilos.Lista de capítulos
añadiendo el componente <Accordion />
pasándole chapters
como prop
para renderizarse en el nuevo componente creado: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í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.
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.
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:
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:
Proyectos
, título principal de cada ruta junto con la línea horizontal... hay trabajo que hacer en ello, ahora que ya tenemos el proyecto estable podemos abordar este punto.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.
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
Las habilidades que hemos obtenido aquí son:
<Accordion />
donde podremos añadir un título y una descripción para usarlo en muchos propósitos.useSignal
, aunque como os he mencionado, podemos hacerlo de diversas formas.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.