ReactJS + LoopbackJS + MySQL [Parte 4]
En la parte 4 de la guía que empezamos aquí, haremos las funciones CRUD para el modelo Apuntes.
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
- subcomps [Nuevo]
- ApuntesItem.js [Nuevo]
- AddApunte.js [Nuevo]
- EditApunte.js [Nuevo]
- ListApunte.js [Nuevo]
- ViewApunte.js [Nuevo]
- subcomps [Nuevo]
- Materias
- subcomps
- MateriasCard.js
- AddMateria.js
- EditMateria.js
- ViewMateria.js
- subcomps
- App
- index.css
- index.js
- registerServiceWorker.js
- components
Empezamos creado el archivo ListApunte.js donde importaremos las librerías necesarias, en el constructor definiremos state con dos asociaciones, una con el id de la Materia que hemos puesto en la URL y otra hacia una colección vacía. Con getApuntes(), traemos la lista de Apuntes ordenados por fecha y la guardamos en la coleccíon de state.
En el render() definiremos una const que contendrá un mapeado de la colección anterior y por cada objeto devolverá lo que definiremos en ApuntesItem.js.
import React, { Component } from 'react'; import axios from 'axios'; import { Link } from 'react-router-dom'; import ApuntesItem from './subcomps/ApuntesItem'; export default class ListApunte extends Component{ constructor(props){ super(props); this.state = { materiaID: this.props.match.params.mId, apuntes: [] } } componentWillMount(){ this.getApuntes(); } getApuntes(){ axios.get(`http://localhost:3000/api/Materias/${this.state.materiaID}/apuntes?filter[order]=created_at asc`) .then(response => { this.setState({apuntes: response.data}) }) .catch(err => console.log(err)); } render(){ const apuntesItems = this.state.apuntes.map((apunte, i) => { var estilo = (i%2===0)? 'info':'secondary'; return( <ApuntesItem key={apunte.id} apunte={apunte} estilo={estilo} /> ) }) return( <div className="row justify-content-center"> <div className="mr-md-auto top-spacing-10"> <Link to={`/materia/details/${this.state.materiaID}`} className="btn btn-dark btn-block"> <i className="fa fa-arrow-left" /> </Link> </div> <div className="jumbotron justify-content-between row col-12 top-spacing-20"> <h1 className="col-4">Apuntes</h1> <Link to={`/apunte/add/${this.state.materiaID}`} className="btn btn-link btn-lg ml-md-auto"><i className="fa fa-plus" /></Link> </div> <div className="col-12 row justify-content-center"> <div className="list-group col-6"> { apuntesItems } </div> </div> </div> ) } }
En ApuntesItem.js haremos algo similar a lo que ya hicimos en MateriaCard.
import React, { Component } from 'react'; import { Link } from 'react-router-dom'; export default class ApuntesItems extends Component{ constructor(props){ super(props); this.state = { apunte : props.apunte, estilo : props.estilo } } render(){ var classes = `list-group-item list-group-item-action list-group-item-${this.state.estilo}`; return( <Link to={`/apunte/details/${this.state.apunte.id}`} className={classes}> <div className="row justify-content-between"> <div className="col"> { this.state.apunte.titulo } </div> <div className="col-1"> <i className="fa fa-arrow-right" /> </div> </div> </Link> ) } }
El siguiente archivo que crearemos será AddApunte.js, practicamente es lo mismo que el archivo AddMateria.js solo que modificaremos el nombre de las funciones y lo que tenemos que guardar en la BDD.
import React, { Component } from 'react'; import axios from 'axios'; import { Link } from 'react-router-dom'; export default class AddApunte extends Component{ constructor(props){ super(props); this.state = { materiaID: this.props.match.params.mId, uuid:'' } } componentWillMount(){ this.getGUID(); console.log(this.state.materiaID); } getGUID(){ 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()); const nuevoApunte = { id: this.state.uuid, clase_num: this.refs.clase.value, titulo: this.refs.titulo.value, contenido: this.refs.contenido.value, materiaId: this.state.materiaID, created_at: date, updated_at: date } this.addApunte(nuevoApunte); } addApunte(nuevoApunte){ axios.request({ method:'post', url:'http://localhost:3000/api/Apuntes', data: nuevoApunte }).then(response => { this.props.history.push(`/apuntes/list/${this.state.materiaID}`); }).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 Apuntes</h1> </div> <form className="col-8" onSubmit={this.onSubmit.bind(this)}> <div className="form-group row justify-content-between"> <div className="col-2"> <label htmlFor="clase" className="col-form-label">Clase</label> <input id="clase" type="number" min="0" name="clase" className="form-control" ref="clase" /> </div> <div className="col-7"> <label htmlFor="titulo" className="col-form-label">Titulo</label> <input id="titulo" type="text" name="titulo" className="form-control" ref="titulo" /> </div> </div> <div className="form-group"> <label htmlFor="contenido" className="col-form-label">Contenido</label> <textarea id="contenido" name="contenido" className="form-control" ref="contenido" /> </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> ) } }
En ViewApunte.js, como en los otros archivos, guarda similitudes con ViewMateria.js
import React, { Component } from 'react'; import axios from 'axios'; import { Link } from 'react-router-dom'; export default class ViewApunte extends Component{ constructor(props){ super(props); this.state = { apunteID: this.props.match.params.aId, details: '', created_at: '', updated_at: '', meses: [ 'Enero','Febrero','Marzo', 'Abril','Mayo','Junio', 'Julio','Agosto','Septiembre', 'Octubre','Noviembre','Diciembra' ] } } componentWillMount(){ this.getApunteDetail(); } getApunteDetail(){ axios.get(`http://localhost:3000/api/Apuntes/${this.state.apunteID}`) .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="mr-md-auto top-spacing-10"> <Link to={`/apuntes/list/${this.state.details.materiaId}`} className="btn btn-dark btn-block"> <i className="fa fa-arrow-left" /> </Link> </div> <div className="jumbotron col-12 top-spacing-20 text-center"> <h1>Detalles Apunte</h1> </div> <div className="row col-12"> <div className="col-7"> <div className="col-12 row justify-content-between"> <h3 className="col">{this.state.details.titulo}</h3> <small className="col-2">(Apunte: {this.state.details.clase_num})</small> </div> <label className="col-label-form top-spacing-10">Contenido</label> <p dangerouslySetInnerHTML={{ __html: this.state.details.contenido }}></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 ={`/apunte/edit/${this.state.apunteID}`} className="btn btn-outline-primary btn-block">Editar</Link> </div> </div> </div> </div> ) } }
Y por último en EditApunte.js
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 = { apunteID: this.props.match.params.aId, clase_num: '', titulo: '', contenido: '', materiaId: '', created_at: '', fecha_creacion: '', update_at: '', apuntes: [], meses: [ 'Enero','Febrero','Marzo', 'Abril','Mayo','Junio', 'Julio','Agosto','Septiembre', 'Octubre','Noviembre','Diciembra' ] } } componentWillMount(){ this.getApunteDetail(); } getApunteDetail(){ axios.get(`http://localhost:3000/api/Apuntes/${this.state.apunteID}`) .then(response => { var gcd = new Date(response.data.created_at); var gud = new Date(response.data.updated_at); this.setState({ clase_num: response.data.clase_num, titulo: response.data.titulo, contenido: response.data.contenido, materiaId: response.data.materiaId, 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)); } onSubmit(e){ e.preventDefault(); var date = new Date(); date.setHours(date.getHours()); const editarApunte = { id: this.state.apunteID, clase_num: this.refs.clase_num.value, titulo: this.refs.titulo.value, contenido: this.refs.contenido.value, materiaId: this.state.materiaId, created_at: this.state.fecha_creacion, updated_at: date } this.editApunte(editarApunte); } editApunte(editarApunte){ axios.request({ method: 'put', url: `http://localhost:3000/api/Apuntes/${this.state.apunteID}/`, data: editarApunte }).then(response => { this.props.history.push(`/apunte/details/${this.state.apunteID}`); }).catch(err => console.log(err)); } delApunte(e){ e.preventDefault(); axios.delete(`http://localhost:3000/api/Apuntes/${this.state.apunteID}`) .then(response => { this.props.history.push(`/apuntes/list/${this.state.materiaId}`); }).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="mr-md-auto top-spacing-10"> <Link to={`/apunte/details/${this.state.apunteID}`} className="btn btn-dark btn-block"> <i className="fa fa-arrow-left" /> </Link> </div> <div className="jumbotron col-12 top-spacing-20 text-center"> <h1>Editar Apunte</h1> </div> <form className="row col-12" onSubmit={this.onSubmit.bind(this)}> <div className="col-8"> <div className="form-group row col-12 justify-content-between"> <div className="col-2"> <label htmlFor="clase_num" className="col-form-label">Clase</label> <input id="clase_num" type="number" min="0" name="clase_num" className="form-control" ref="clase_num" onChange={ this.handleChangeInput.bind(this) } value={this.state.clase_num} /> </div> <div className="col-7"> <label htmlFor="titulo" className="col-form-label">Titulo</label> <input id="titulo" type="text" name="titulo" className="form-control" ref="titulo" onChange={ this.handleChangeInput.bind(this) } value={this.state.titulo} /> </div> </div> <div className="form-group"> <label htmlFor="contenido" className="col-form-label">Contenido</label> <textarea id="contenido" name="contenido" className="form-control" ref="contenido" onChange={ this.handleChangeInput.bind(this) } value={this.state.contenido} /> </div> </div> <div className="col-4 ml-md-auto card"> <div className="card-header row justify-content-end"> <Link className="mr-md-auto" to={`/apunte/details/${this.state.apunteID}`}> <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.delApunte.bind(this) }> <i className="fa fa-trash" /> </button> </div> </div> </form> </div> ) } }
Con esto tendremos la app funcionando. En próximos artículos explicaré como añadir un editor WYSIWYG a las partes de formularios que necesitemos un formato mas enriquecido y no tener que escribir código HTML en los textarea, y como hacer que loopback se quede corriendo en segundo plano en vuestro servidor (en caso de que lo tengáis como yo en uno casero).
Cualquier duda o problema que tengáis podéis dejar un comentario y trataré de ayudar a solucionarlo.
Un saludo