ReactJS + LoopbackJS + MySQL [Parte 3]
Continuando el tutorial que empezamos aquí
Vamos a crear los archivos que realizarán las funciones CRUD de nuestra aplicación para el modelo Materias.
El arbol de archivos al finalizar, quedará mas o menos de esta manera:
- src
- components
- App
- App.css
- App.js
- Main.js
- Navbar.js
- Routes.js
- Apuntes
- Materias
- subcomps
- MateriasCard.js
- AddMateria.js [Nuevo]
- EditMateria.js [Nuevo]
- ViewMateria.js [Nuevo]
- subcomps
- App
- index.css
- index.js
- registerServiceWorker.js
- components
Creamos el archivo AddMateria.js, en el cual pondremos un formulario para crear la nueva materia. En el fichero importamos las librerías necesarias. En el constructor definimos la array state con una asociación uuid vacía, que se llenará más tarde.
Con componentWillMount() cuando cargue el archivo ejecutará la funcion genUUID(), la cual a su vez generará un Universally unique identifier para el nuevo registro, y guardará el UUID generado en this.state.uuid para su posterior uso.
La función onSubmit() recogerá los datos que estén como valores en los campos del formulario y en this.state, y los enviará a addMateria() que se encargará, mediante la librería axios, de comunicarse con la api de loopback y guardar en la BDD la información.
El resto del código es un formulario que se puede personalizar con el framework que uséis. Aun así debéis tener en cuenta el onSubmit de la etiqueta <form>, y en las etiquetas <input> y <textarea> la propiedad ref.
import React, { Component } from 'react'; import axios from 'axios'; import { Link } from 'react-router-dom'; export default class addMateria extends Component{ constructor(){ super(); this.state = { uuid:'' } } componentWillMount(){ this.genUUID(); } genUUID(){ function fd(){ return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1); } var tempUUID = fd() + fd() + '-' + fd() + '-' + fd() + '-' + fd() + '-' + fd() + fd() + fd(); this.setState( { uuid: tempUUID } ); } onSubmit(e){ e.preventDefault(); var date = new Date(); date.setHours(date.getHours() +1); const nuevaMateria = { id: this.state.uuid, nombre: this.refs.nombre.value, descripcion: this.refs.desc.value, created_at: date, updated_at: date } this.addMateria(nuevaMateria); } addMateria(nuevaMateria){ axios.request({ method:'post', url:'http://localhost:3000/api/Materias', data: nuevaMateria }).then(response => { this.props.history.push('/'); }).catch(err => console.log(err)); } render(){ return( <div className="row justify-content-center"> <div className="jumbotron col-12 top-spacing-20 text-center"> <h1>Añadir Materia</h1> </div> <form className="col-8" onSubmit={this.onSubmit.bind(this)}> <div className="form-group"> <label htmlFor="nombre" className="col-form-label">Nombre Materia</label> <input id="nombre" type="text" name="nombre" className="form-control" ref="nombre" /> </div> <div className="form-group"> <label htmlFor="desc" className="col-form-label">Descripción</label> <textarea id="desc" name="desc" className="form-control" ref="desc" /> </div> <div className="btn-group btn-block"> <button type="submit" className="btn btn-outline-success btn-block">Crear</button> <Link to="/" className="btn btn-outline-danger" alt="Cancelar"><i className="fa fa-ban fa-lg" /></Link> </div> </form> </div> ) } }
Con este formulario ya podemos crear materias cuando la creemos veremos algo así
Vamos a crear el archivo ViewMateria.js, como siempre importamos la librerías que necesitamos en este fichero. En el constructor definimos la array state con la asociación materiaID con la propiedad que enviamos en el enlace, tres asociaciones de details, created_at y updated_at vacias para rellenarlas más tarde y, opcionalmente, una coleccion de strings con los meses del año, más adelante veremos porqué.
Con componentWillMount() cuando cargue el archivo ejecutará la funcion getMateriaDetail(), con la que traeremos de la BDD los detalles de la Materia consultada. Hacemos la llamada a la api con axios, al recibir la respuesta creamos un Date() nuevo con cada una de las fechas y hacemos un setState para guardar las respuestas en this.state. Otra vez opcionalmente, podemos convertir las fechas a un formato mas legible.
Como en el archivo anterior en render() podemos hacer el código que prefiramos para mostrar los datos. Prestando especial atención a la linea 50 en la que hay una etiqueta <p> para mostrar el código html renderizado correctamente en caso de que el campo descripcion venga con código html.
import React, { Component } from 'react'; import axios from 'axios'; import { Link } from 'react-router-dom'; export default class ViewMateria extends Component{ constructor(props){ super(props); this.state = { materiaID: this.props.match.params.mId, details: '', created_at: '', updated_at: '', meses: [ 'Enero','Febrero','Marzo', 'Abril','Mayo','Junio', 'Julio','Agosto','Septiembre', 'Octubre','Noviembre','Diciembra' ] } } componentWillMount(){ this.getMateriaDetail(); } getMateriaDetail(){ axios.get(`http://localhost:3000/api/Materias/${this.state.materiaID}`) .then(response => { var gcd = new Date(response.data.created_at); var gud = new Date(response.data.updated_at); this.setState({ details: response.data, created_at: gcd.getDate() + " de " + this.state.meses[gcd.getMonth()] + " " + gcd.getFullYear() + " || " + gcd.getHours() +":"+ (gcd.getMinutes()<10?'0':'') + gcd.getMinutes(), updated_at: gud.getDate() + " de " + this.state.meses[gud.getMonth()] + " " + gud.getFullYear() + " || " + gud.getHours() +":"+ (gud.getMinutes()<10?'0':'') + gud.getMinutes() }) }) .catch(err => console.log(err)); } render(){ return( <div className="row justify-content-center"> <div className="jumbotron col-12 top-spacing-20 text-center"> <h1>Detalles Materia</h1> </div> <div className="row col-12"> <div className="col-7"> <h3>{this.state.details.nombre}</h3> <label className="col-label-form">Descripción</label> <p dangerouslySetInnerHTML={{ __html: this.state.details.descripcion }}></p> </div> <div className="col-4 ml-md-auto card"> <div className="card-header row justify-content-end"> <h3>Información</h3> </div> <div className="card-body"> <p className="card-text"> <strong>Creado:</strong><br/>{this.state.created_at} </p> <hr /> <p> <strong>Actualizado:</strong><br/>{this.state.updated_at} </p> </div> <div className="card-footer text-muted row justify-content-center"> <Link to ={`/materia/edit/${this.state.materiaID}`} className="btn btn-outline-primary btn-block">Editar</Link> </div> </div> </div> <div className="row col-4 justify-content-center top-spacing-50"> <Link to={`/apuntes/list/${this.state.materiaID}`} className="btn btn-info btn-block"> Ver Apuntes </Link> </div> </div> ) } }
Creamos a continuación el archivo EditMateria.js con la importación de librerías que usaremos en el código. En el constructor() definimos la array state y añadimos la asociación materiaID con la propiedad que enviamos en el enlace ademas de una asociación vacía por cada una de las columnas de la tabla Materias, para la fecha de creación creamos dos asociaciones, tambien crearemos la asociación clases con una colección vacía. Y, opcionalmente, la colección de strings con los meses del año.
Con componentWillMount() cuando cargue el archivo ejecutará la funcion getMateriaDetail(), que hará exactamente lo mismo que en el archivo anterior. Solo que adicionalmente guardaremos la fecha de creación en el formado de la BDD. Además se ejecutará una función que traerá un lista con las clases asociadas a la Materia.
La función onSubmit() recogerá los datos que estén como valores en los campos del formulario y en this.state, y los enviará a editMateria() que se encargará, mediante la librería axios, de comunicarse con la api de loopback y modificar en la BDD la información.
Con delMateriaClases() primero llamaremos a la función delClases(), con la que eliminaremos las clases asociadas a la Materia que estamos editando. Después llamaremos a la función delMateria(), con la que eliminaremos la Materia.
Como punto nuevo en este código creamos una función que nos permitirá modificar los input/textarea que por defecto en reactJS ‘no permite la modificación’ al ser un valor que carga desde this.state. Es la función handleChangeInput(e).
El render() lo customizais como os apetezca, como ejemplo yo he hecho un diseño sencillo con una zona donde aparece el detalle del curso, un card con información de creación y modificación y los botones de editar dentro de la tarjeta y otro que enlazará a los Apuntes de la Materia
import React, { Component } from 'react'; import axios from 'axios'; import { Link } from 'react-router-dom'; export default class addMateria extends Component{ constructor(props){ super(props); this.state = { materiaID: this.props.match.params.mId, nombre: '', descripcion: '', created_at: '', fecha_creacion: '', update_at: '', apuntes: [], meses: [ 'Enero','Febrero','Marzo', 'Abril','Mayo','Junio', 'Julio','Agosto','Septiembre', 'Octubre','Noviembre','Diciembra' ] } } componentWillMount(){ this.getMateriaDetail(); this.getListaApuntes(); } getMateriaDetail(){ axios.get(`http://localhost:3000/api/Materias/${this.state.materiaID}`) .then(response => { var gcd = new Date(response.data.created_at); var gud = new Date(response.data.updated_at); this.setState({ nombre: response.data.nombre, descripcion: response.data.descripcion, fecha_creacion: response.data.created_at, created_at: gcd.getDate() + " de " + this.state.meses[gcd.getMonth()] + " " + gcd.getFullYear() + " || " + gcd.getHours() +":"+ (gcd.getMinutes()<10?'0':'') + gcd.getMinutes(), updated_at: gud.getDate() + " de " + this.state.meses[gud.getMonth()] + " " + gud.getFullYear() + " || " + gud.getHours() +":"+ (gud.getMinutes()<10?'0':'') + gud.getMinutes() }) }) .catch(err => console.log(err)); } getListaApuntes(){ axios.get(`http://localhost:3000/api/Materias/${this.state.materiaID}/apuntes`) .then(response => { this.setState({apuntes: response.data}) }) .catch(err => console.log(err)); } onSubmit(e){ e.preventDefault(); var date = new Date(); date.setHours(date.getHours()); const editarMateria = { id: this.state.materiaID, nombre: this.refs.nombre.value, descripcion: this.refs.descripcion.value, created_at: this.state.fecha_creacion, updated_at: date } this.editMateria(editarMateria); } editMateria(editarMateria){ axios.request({ method: 'put', url: `http://localhost:3000/api/Materias/${this.state.materiaID}/`, data: editarMateria }).then(response => { this.props.history.push(`/materia/details/${this.state.materiaID}`); }).catch(err => console.log(err)); } delMateriaApuntes(e){ e.preventDefault(); this.delApuntes(); } delApuntes(){ this.state.apuntes.map((apunte, i) => { axios.delete(`http://localhost:3000/api/Apuntes/${apunte.id}`) .then(response => { this.delMateria(); }).catch(err => console.log(err)); return 0; }) } delMateria(){ axios.delete(`http://localhost:3000/api/Materias/${this.state.materiaID}`) .then(response => { this.props.history.push('/'); }).catch(err => console.log(err)); } handleChangeInput(e){ const target = e.target; const value = target.value; const name = target.name; this.setState({ [name]: value }); } render(){ return( <div className="row justify-content-center"> <div className="jumbotron col-12 top-spacing-20 text-center"> <h1>Editar Materia</h1> </div> <form className="row col-12" onSubmit={this.onSubmit.bind(this)}> <div className="col-7"> <div className="form-group"> <label htmlFor="nombre" className="col-form-label">Nombre</label> <input type="text" className="form-control" id="nombre" name="nombre" ref="nombre" onChange={ this.handleChangeInput.bind(this) } value={this.state.nombre} /> </div> <div className="form-group"> <label htmlFor="descripcion" className="col-form-label">Descripción</label> <textarea id="descripcion" name="descripcion" className="form-control" ref="descripcion" onChange={ this.handleChangeInput.bind(this) } value={this.state.descripcion} /> </div> </div> <div className="col-3 ml-md-auto card"> <div className="card-header row justify-content-end"> <Link className="mr-md-auto" to={`/materia/details/${this.state.materiaID}`}> <i className="fa fa-left-arrow" /> </Link> <h3>Información</h3> </div> <div className="card-body"> <p className="card-text"> <strong>Creado:</strong><br/>{this.state.created_at} </p> <hr /> <p> <strong>Actualizado:</strong><br/>{this.state.updated_at} </p> </div> <div className="card-footer text-muted row justify-content-center btn-group"> <button type="submit" className="btn btn-outline-success">Modificar</button> <button type="button" className="btn btn-danger" onClick={ this.delMateriaApuntes.bind(this) }> <i className="fa fa-trash" /> </button> </div> </div> </form> </div> ) } }
Con esto ya hemos acabado las funciones CRUD del modelo Materias.
El archivo Routes.js lo actualizaremos así
import React from 'react'; import { Switch, Route } from 'react-router-dom'; import Main from './Main'; import AddMateria from '../Materias/AddMateria'; import ViewMateria from '../Materias/ViewMateria'; import EditMateria from '../Materias/EditMateria'; const Routes = () => ( <main> <Switch> <Route exact path='/' component={Main} /> <Route exact path='/materia/add' component={AddMateria} /> <Route exact path='/materia/details/:mId' component={ViewMateria} /> <Route exact path='/materia/edit/:mId' component={EditMateria} /> </Switch> </main> ) export default Routes;
Hasta aquí la tercera parte. Si todo funciona correctamente, hay que empezar con la cuarta parte.
Si os habéis topado con algún fallo, o queréis aportar alguna idea o ayuda podéis dejar un comentario.
1 respuesta
[…] Hasta aquí la segunda parte. Si todo funciona correctamente, hay que empezar con la tercera parte. […]