Aprende Javascript con MentoringJS - Step 11
En esta serie de artículos voy a explicar el prototipado de mi proyecto de aplicación web pero basado en React y la descomposición en Componentes que he decidido hacer y el por qué de ellos. Cualquier consejo o corrección es bienvenido.
Esta es una primera versión, con muchas horas detrás, pero obviamente no es la definitiva. Es una aplicación en constante revisión y progresión.
Como comentaba, este proyecto está basado en mi anterior mockup de aplicación móvil. Pero, en este caso se ha adaptado a una aplicación web con React.
Antes de empezar a explicar cómo he ido realizando esta aplicación, quería indicar que esta es una versión estática y sin efectos, que se dejan para una versión posterior. Además, también quería indicar algunos recursos que me han ayudado en este proceso.
- El artículo de David Tang sobre descomposición de Componentes
- El workshop de Leonardo García sobre React
- Y sobre todo, de nuevo, el tutorial de Devin Abbott React Express
- Añadir también otros muchos recursos que he podido ir utilizando para hacer algunas cosas concretas.
ACTUALIZACIÓN
Después de pensar y repensar cómo podría hacer para poder hacer el índex algo dinámico sin muchas modificaciones con eventos y demás, llegué a la conclusión que se podía hacer con React Router.
Además, encontré varios recursos para adaptar mi código y el que me resultó más claro y funcional fue el de la web Kirupa.com.
Esto, ha hecho cambiar un poco el código que había hecho, por lo que voy a resaltar el contenido que he modificado y voy a explicar por qué otro lo he sustituido. Indicar también que solo se han visto modificados dos archivos y componentes: App y Home. Además, se ha instalado el paquete de npm para React Router.
Empezando con el código, en primer lugar, se tiene que realizar la configuración del entorno para poder crear una aplicación React. En esto seguí los pasos que ya realicé en mi último proyecto React sobre la revisión de React express.
Para crear e inicializar una app React y poder empezar a programar hacemos:
create-react-app kine-proyect
cd kine-proyect
npm start
Después, también he añadido varios paquetes que me han ayudado con los estilos y con los iconos. El primero ha sido Bootstrap.
npm install bootstrap
El segundo ha sido Font Awesome. Este paquete tiene una instalación más específica. De todos modos, en su propia web explican cómo hacerlo de manera sencilla.
npm i --save @fortawesome/fontawesome-svg-core \
npm i --save @fortawesome/free-solid-svg-icons \
npm i --save @fortawesome/react-fontawesome
ACTUALIZACIÓN
Este paquete es necesario instalarlo para poder utilizar React Router.
npm install react-router-dom
Una vez se configura el entorno empezamos con la configuración de los archivos básicos, como el index, etc.
Index.html
El index es casi el mismo que trae React por defecto, tan solo cambiando el título.
<title>Kine Proyect</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="app"></div>
</body>
</html>
Index.js
import { render } from "react-dom";
import React from "react";
import App from './App';
import 'bootstrap/dist/css/bootstrap.css';
import { library } from '@fortawesome/fontawesome-svg-core'
import { faCog } from '@fortawesome/free-solid-svg-icons'
import { faSearch } from '@fortawesome/free-solid-svg-icons'
import { faForward} from '@fortawesome/free-solid-svg-icons'
import { faBackward} from '@fortawesome/free-solid-svg-icons'
import { faTimes} from '@fortawesome/free-solid-svg-icons'
library.add(faCog)
library.add(faSearch)
library.add(faForward)
library.add(faBackward)
library.add(faTimes)
render (<App />, document.querySelector("#app"));
En este index se añaden el import para renderizar y el de React y el componente principal App.
Además, se importan el archivo de CSS que trae Bootstrap y todos los iconos que se van a utilizar. Debajo de los imports se añaden las librerías de cada icono. Todo esto se explica cómo hacerlo en la propia documentación de Font Awesome.
Como se puede ver, los iconos son diferentes al del tutorial, ya que los he intentado adaptar a mi propia aplicación.
Por último, se renderiza el componente App en el atributo app del div del index.
App.js
EL CÓDIGO ANTERIOR DE ESTE ARCHIVO ERA:
import React, { Component } from 'react';
import Index from './components/home';
import Services from './components/Services';
import AboutUs from './components/AboutUs';
import Appointment from './components/Appointment';
import Blog from './components/Blog';
import Gallery from './components/Gallery';
export default class App extends Component {
render() {
return (
<div>
<Index />
<Services />
<AboutUs />
<Appointment />
<Blog />
<Gallery />
</div>
)
}
}
Este es el componente principal, desde dónde se hacen todos los imports de los demás componentes.
Antes de seguir, voy a hacer una pequeña aclaración.
Como se ve en este componente, hay 6 componentes diferentes que se exportan. A continuación iré explicándolos pero quiero hacer un inciso para explicar la estructura que he pensado hacer y que finalmente he hecho.
Cada componente representa una “página”. Es decir, el componente “Index” sería la página principal desde dónde se podría ir accediendo a las demás secciones, que serían la de los servicios ofrecidos; la que contiene información sobre los trabajadores; en la que podrías reservar cita; en la que podrías acceder al blog y la última a la galería de imágenes.
Seguramente existe otra manera de realizar un proyecto React, como por ejemplo hacerlo todo en una página con la información mostrada en vertical. Pero basándome en el proyecto inicial y en la información que quería mostrar, finalmente he decidido realizarlo así.
Huelga decir que el resultado no es el más vistoso posible, ya que aparece todo seguido y sin una lógica, pero eso intentaré ajustarlo cuando se añadan los efectos.
En la parte principal hay una clase App que se exporta al componente App anterior y que es la que renderiza todos los demás componentes.
EL NUEVO CÓDIGO CON LOS CAMBIOS CON REACT ROUTER ES EL SIGUIENTE:
import React, { Component } from 'react';
import Index from './components/home';
export default class App extends Component {
render() {
return (
<div>
<Index />
</div>
)
}
}
Como se ve, básicamente el cambio que ha habido es que la llamada a todos los componentes se hace ahora desde el archivo Home.js, por lo que solo se importa el archivo indicado y también solo se llama al componente Index de dicho archivo en el return que se llamará desde App.js.
Home.js
EL CÓDIGO ANTERIOR DE ESTE ARCHIVO ERA:
Como este componente contiene bastante información iré poniendo el código en pequeñas partes. Para empezar, los imports.
import React, { Component } from 'react';
import NavBar from './NavBar';
import Styles from './Styles'
En este componente se importa el componente React y otros personalizados. El primero es Navbar que es un componente que renderiza la barra de navegación en todos los componentes. Más adelante, al revisar el propio componente, lo explicaré en detalle.
El segundo, es otro componente personalizado, pero este es sobre los estilos que he ido utilizando en todos los demás componentes.
export default class Index extends Component {
render() {
const list = [
{ 'id': 1, 'name': 'Servicios y especialidades', 'url': './Services'},
{ 'id': 2, 'name': 'Nosotros', 'url': './AboutUs' },
{ 'id': 3, 'name': 'Pedir cita', 'url': './Appointment' },
{ 'id': 4, 'name': 'Blog', 'url': './Blog' },
{ 'id': 5, 'name': 'Galería', 'url': './Gallery' },
];
let imgIndex= "https://images.unsplash.com/photo-1514672013381-c6d0df1c8b18?ixlib=rb-0.3.5&s=2d6c4262761e367e15cc4c0675591fad&auto=format&fit=crop&w=1500&q=80"
return (
<div>
<NavBar icontwice= "search" title= "Centro de Fisioterapia" icon="cog" />
<div className="card">
<Styles
imageIndex ="true"
src={imgIndex}
>
</Styles>
<ul className="list-unstyled list-group ">
{list.map(option =>
<li
key={option}
className="list-group-item list-group-item-primary"
>
<Styles title="true">
<h2>
<a href={option.url}>{option.name}</a>
</h2>
</Styles>
</li>) }
</ul>
</div>
<Search />
<Settings />
<Language />
</div>
);
}
}
Este es la clase principal y la que se exportará al componente App. En la parte del render se declaran un array de objetos y una variable. En el array se añaden todas las secciones de la aplicación, con su ID y con el acceso a su path dentro de la aplicación. En la variable se añade la imagen de la página principal.
En la parte que devuelve la clase se añade el componente Navbar con varios props
<NavBar icontwice= "search" title= "Centro de Fisioterapia" icon="cog" />
Este componente siempre se llama con varios props. Aquí hay tres, icontwice, title e icon. El primero es para comprobar (con un condicional) si la página tiene uno o dos iconos (este tiene dos). El segundo es el título y el tercero es el tipo de ícono a mostrar.
Después se añade un div con una clase de Bootstrap.
<div className="card">
Como en React las clases CSS se llaman con className y no con class, hay que adaptar el código de Bootstrap para cada ocasión. En todos los componentes se utilizan clases bootstrap para adaptarlo a los estilos que vienen en esa librería.
A continuación, añado otro componente personalizado, Styles.
<Styles
imageIndex ="true"
src={imgIndex}
>
</Styles>
Este componente también acepta props. El primero, imageIndex es un condicional que comprueba si lo que hay que renderizar es una imagen, un h3, etc. El segundo es el que renderiza la imagen pasada como variable anteriormente.
A continuación se hace un mapeo del array list con el objeto map y extrayendo otro array option. Se hace además con una arrow_function.
En todos los componentes se utiliza esta misma lógica de mapeo sacando un listado y con una arrow function.
Tanto en el tag ul como en el li se pasan varias clases de Bootstrap. Además, dentro del mapeo, en el tag li se llama al componente Styles para coger ciertos a mostrar en el h2.
<ul className="list-unstyled list-group ">
{list.map(option =>
<li
key={option}
className="list-group-item list-group-item-primary"
>
<Styles title="true">
<h2>
<a href={option.url}>{option.name}</a>
</h2>
</Styles>
</li>) }
</ul>
Por último, se llama a los otros componentes que mostrarían el apartado de búsqueda (al cuál se accede desde el icono de la parte superior izquierda), al de ajustes (se accedería desde el icono de la parte superior derecha) y al de los idiomas, que se accedería desde dentro de Ajustes.
<Search />
<Settings />
<Language />
EL NUEVO CÓDIGO CON LOS CAMBIOS CON REACT ROUTER ES EL SIGUIENTE:
import React, { Component } from 'react';
import NavBar from './NavBar';
import Styles from './Styles'
import { BrowserRouter as HashRouter, Route, NavLink } from "react-router-dom";
import Services from './Services';
import AboutUs from './AboutUs';
import Appointment from './Appointment';
import Blog from './Blog';
import Gallery from './Gallery';
En este archivo se añaden todos los imports de los demás componentes, ya que ahora se van a renderizar aquí y no en el archivo App.js.
Además, se añade el import de React Router, tal y como se indica en la documentación y en el artículo que he utilizado para adaptar el código.
El resto de cambios se introducen en el return del componente:
return (
<div>
<NavBar icontwice= "search" title= "Centro de Fisioterapia" icon="cog" />
<HashRouter>
<div className="card">
<Styles
imageIndex ="true"
src={imgIndex}
>
</Styles>
<ul className="list-unstyled list-group ">
{list.map(option =>
<li
key={option}
className="list-group-item list-group-item-primary"
>
<Styles title="true">
<NavLink to={option.url}>{option.name}</NavLink>
</Styles>
</li>) }
</ul>
<div className="content">
<Route path="/Services" component={Services}/>
<Route path="/AboutUs" component={AboutUs}/>
<Route path="/Appointment" component={Appointment}/>
<Route path="/Blog" component={Blog}/>
<Route path="/Gallery" component={Gallery}/>
</div>
</div>
</HashRouter>
</div>
Para empezar, se añaden los tags de HashRouter necesarios para crear la estructura de links y acceso a las rutas de los componentes indicados.
Un ejemplo de dicha estructura sería:
function BasicExample() {
return (
<Router>
<div>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
<li>
<Link to="/topics">Topics</Link>
</li>
</ul>
<hr />
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/topics" component={Topics} />
</div>
</Router>
);
}
Yo lo he adaptado tal y como indicaba el artículo enlazado y he utilizado HashRouter y NavLink en vez de Router y Link. En mi código lo he adaptado en el mapeo del listado que hacía y ahí he añadido el tag de NavLink con el contenido dinámico del mapeo del array.
<ul className="list-unstyled list-group ">
{list.map(option =>
<li
key={option}
className="list-group-item list-group-item-primary"
>
<Styles title="true">
<NavLink to={option.url}>{option.name}</NavLink>
</Styles>
</li>) }
</ul>
Esta parte no ha cambiado mucho, ya que se ha cambiado el tag h2 por el NavLink.
Más abajo, he añadiendo el código necesario que se utilizar para indicar que cuándo se pinche en un enlace de los anteriores se vaya a un componente de una ruta indicada. Esto se hace con el código siguiente:
<div className="content">
<Route path="/Services" component={Services}/>
<Route path="/AboutUs" component={AboutUs}/>
<Route path="/Appointment" component={Appointment}/>
<Route path="/Blog" component={Blog}/>
<Route path="/Gallery" component={Gallery}/>
</div>
Y esto es todo. Con estas pocas modificaciones el resultado ha quedado algo más dinámico que pronto se podrá ver en un deploy que voy a realizar.
El resultado quedaría así:
Dentro de este archivo Home hay tres funciones más, una para la opción de búsqueda, la otra para los ajustes y la última para los idiomas.
Función búsqueda
function Search (){
return(
<div>
<NavBar title= "Buscar" icon="times" right= "true" />
<div className="card">
<form>
<div className="form-group">
<input className="form-control " type="text" placeholder="Búsquedas más comunes" readOnly></input>
</div>
<div className="form-group">
<button type="button" className="btn btn-info btn-lg ">Contractura</button>
<button type="button" className="btn btn-info btn-lg ml-5 ">Animales</button>
<button type="button" className="btn btn-info btn-lg ml-5 ">Cita</button>
<button type="button" className="btn btn-info btn-lg ml-5 ">Ganchos</button>
</div>
<div className="form-group">
<label htmlFor="exampleFormControlTextarea1">Busca lo que quieras</label>
<textarea className="form-control" id="exampleFormControlTextarea1"></textarea>
</div>
<div className="form-group">
<button className="btn btn-outline-success btn-block my-2 my-sm-0" type="submit">Buscar</button>
</div>
</form>
</div>
</div>
)
}
Estas función no tiene parámetros y devuelve código JSX. En esta, se pasa inicialmente el componente Navbar para la barra de navegación con un icono y que se muestre a la derecha.
Después, hay un div con la clase card (la mayoría son así) en el que hay un formulario dentro con varias opciones: un input, varios botones, un text Area y un botón final de buscar.
El resultado quedaría así:
Función ajustes
function Settings () {
let settings = [
'Idioma',
'Sonido',
'Cerrar sesión',
]
function buttonsSettings (button) {
if (button===settings[0]) {
return ( <button
className="btn btn-info ml-5" >Cambiar
</button>
);
}
else if (button===settings[1]){
return (
<button className="btn btn-primary ml-5" data-toggle="button" aria-pressed="false" autoComplete="off">
OFF
</button>)
}
else if (button===settings[2]){
return (
<button className="btn btn-primary ml-5" data-toggle="button" aria-pressed="false" autoComplete="off">
ON
</button>)
}
}
return(
<div>
<NavBar title= "Ajustes" icon="times" right= "true" />
<div className="card">
<ul className="list-group list-group-flush">
{settings.map(setting =>
<li
key={setting}
className="list-group-item"
>
{ setting }
{buttonsSettings(setting)}
</li>) }
</ul>
<button className="btn btn-outline-success btn-block my-2 my-sm-0" type="submit">Guardar</button>
</div>
</div>
)
}
La función principal es una función simple sin parámetros. Dentro de ella se declara un array con las secciones que tendrá la sección ajustes. Dentro hay otra función con un parámetro que se le pasará como argumento en el return final con el código JSX para discernir qué mostrar.
En esta función interna se hacen varios condicionales que, en función de lo que se reciba, devuelve cierto contenido. Esto se hace sobre todo para mostrar un botón específico en el mapeo. Es decir, al realizar el mapeo del array se mostrará un botón concreto al lado en función del parámetro que esté mapeando el objeto map del array inicial.
El resultado quedaría así:
Función idiomas
function Language (){
let languages = [
'Español',
'Català',
'Galego',
'Euskera',
'English',
'Française'
]
return(
<div>
<NavBar left="true" title= "Idiomas" icon="backward" />
<div className="card">
<ul className="list-group list-group-flush">
{languages.map(language =>
<li
key={language}
className= "list-group-item"
>
<Styles title="true">
{language}
</Styles>
</li>) }
</ul>
<button className="btn btn-outline-success btn-block my-2 my-sm-0" type="submit">Seleccionar Idioma</button>
</div>
</div>
);
}
Esta es una función simple con un array que se mapeará en el return sacando un listado. Ahí se le ponen ciertos estilos (pasado a través del componnete Styles) y un botón final. Además, se añade el componente Navbar al principio de todo con las props concretas para este componente.
El resultado quedaría así:
Services.js
Este componente es el que aglutina toda la parte de los servicios ofrecidos en la web, tanto la parte genérica como un ejemplo concreto que, posiblemente, se podría hacer un componente aislado como modelo para todos los demás servicios.
La clase principal sería la siguiente:
import React, { Component } from 'react';
import NavBar from './NavBar';
import Styles from './Styles'
export default class Services extends Component {
render () {
let imageUrl = 'https://picsum.photos/100/?random';
const servicesContent= [
{
id: 1,
title: "Tratamiento con ganchos y agujas",
content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt",
price: "30€"
}
,
{
id: 2,
title: "Rehabilitación",
content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt",
price: "40€"
},
{
id: 3,
title: "Fisioterapia acuática",
content: "labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat",
price: "25"
},
{
id: 4,
title: "Fisioterapia asistida con animales",
content: "labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat",
price: "25"
}
]
return(
<div>
<NavBar left="true" title= "Servicios y especialidades" icon="backward" />
{servicesContent.map(service =>
<li
key={service.id}
className="list-group-item"
>
<Styles title="true">
{service.title}
</Styles>
<Styles
imageCenter ="true"
src={imageUrl}
>
</Styles>
<h3>{service.content}</h3>
<h4>{service.price}</h4>
</li>) }
<ServicesExample />
</div>
)
}
}
Al principio se hacen los imports habituales, de React y de los dos componentes personalizados.
Dentro del render se añade una variable de una web que muestra imágenes aleatorias.
Después se añade un array de objetos, un objeto por cada servicio.
En la parte del return se añade el componente Navbar con sus especificaciones para este componente y, posteriormente, se realiza un mapeo del array con map sacando un listado. Ahí se le añaden varios estilos cogidos del componente Styles, uno para el título y otro para la imagen. Después se añaden otro contenido dinámico extraído de los objetos del array para mostrar el contenido del servicio y el precio específico para cada servicio mapeado.
Finalmente, se carga el componente de ejemplo que veremos a continuación.
El resultado quedaría así:
Función servicio ejemplo
function ServicesExample(){
let imageService= 'https://www.efisioterapia.net/sites/default/files/imagen_cursos/curso-de-ganchos.jpg'
return (
<div>
<NavBar left="true" title= "Técnica de ganchos y agujas" icon="backward" />
<div className="card">
<Styles title="true">
Características
</Styles>
<Styles
imageServices ="true"
src={imageService}
>
</Styles>
<h3>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. </h3>
<h3>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. </h3>
</div>
</div>
)
}
Esta función sería más simple ya que tiene una imagen y devuelve cierto contenido JSX. En este, se llamaría a los componentes Navbar y Styles y después se añadiría una explicación más detallada. Esta información se podría coger de algún archivo concreto o de algún array de objetos que serviría para esta función y para la clase principal del Componente.
El resultado quedaría así:
AboutUs.js
import React, { Component } from 'react';
import NavBar from './NavBar';
import Styles from './Styles'
export default class AboutUs extends Component {
render (){
const workers = [
{
id: 1,
name: "Don Juan Snow",
content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat"
},
{
id: 2,
name: "Doña Laura Casas",
content: "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat",
},
{
id: 3,
name: "Doña Rosa López",
content: "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo"
}
]
let imageUrl= 'https://picsum.photos/100/?random'
let imgLocalization ='https://cdn.vox-cdn.com/thumbor/6fuJjC-vr4m-Yaf4mkmU7ZfdNbY=/0x0:1161x713/1200x800/filters:focal(435x243:619x427)/cdn.vox-cdn.com/uploads/chorus_image/image/58662765/Screen_Shot_2018_02_12_at_9.34.36_AM.0.png'
return (
<div>
<NavBar left="true" title= "Nosotros" icon="backward" />
<div className="card" >
{workers.map(worker =>
<li
key={worker.id}
className="list-group-item"
>
<Styles title="true">
{worker.name}
</Styles>
<Styles
imageCenter ="true"
src={imageUrl}
>
</Styles>
<h2>{worker.content}</h2>
</li>) }
</div>
<div>
<Styles title="true">
Localización
</Styles>
<Styles
imageIndex ="true"
src={imgLocalization}
>
</Styles>
</div>
<div>
<h4>Teléfono: 6000000</h4>
<h4>Twitter: @nosotros</h4>
</div>
</div>
)
}
}
Este componente es el que muestra la información de los trabajadores y de la clínica.
El componente tiene los tres imports iniciales comunes a otros componentes.
Dentro de la clase se añade un array de objetos con toda la información de los trabajadores. Además, dos variables más con imágenes de los trabajadores y de la localización.
En la parte del return se añade el componente Navbar presonalizado. También se hace un mapeo del array y se devuelve en un listado con la información de cada trabajador: nombre, foto y explicación detallada. Ahí se le añaden los estilos establecido en Styles. Debajo de este mapeo se añade la información de la localización y también información de contacto y de redes sociales.
El resultado quedaría así:
Hasta aquí la primera parte de este artículo. En breve estará el siguiente y último artículo sobre el prototipado de mi aplicación con React.