Ya conseguimos construir nuestra API simple, ahora, en este tutorial vamos a unir su poder para construir la aplicación del Front. Ésta estará basada en React y NextJS . Durante este tutorial, te guiaré paso a paso para que lleguemos a construir el CRUD básico del que es base la API del capítulo anterior .
Recuerda que si no leiste al artículo anterior del CRUD no tienes la API.
Antes de empezar: Cambios mínimos en la API
Para poder usar nuestra API en otra aplicación, como es la que vamos a desarrollar hoy, debemos de habilitar CORS en todas las peticiones. Para ello nos vamos al proyecto de la api e instalamos el módulo de cors
:
$ yarn add cors
Para hacer que funcione en nuestro proyecto solo debemos de incluir este módulo en nuestro index.js
y pasárselo al servidor de Express del API. Las primeras líneas quedarían así:
const express = require('express')
const apiRoutes = require('./routes/api')
const mongoose = require('mongoose')
const bodyParser = require('body-parser')
const cors = require('cors')
const app = express()
const port = process.env.PORT || '3000'
const mongoUri = process.env.MONGO_URI || 'mongodb://localhost:27017/openwebinars'
mongoose.connect(mongoUri)
app.use(bodyParser.json())
app.use(cors())
[...]
`
Paso 1: Construyendo la vista para desplegar todos los posts
En primer lugar, procedemos a instalar todas y cada una de las dependencias que necesitaremos durante el proceso:
$ yarn add next react react-dom express moment
Quiero destacar que usaremos el paquete de express
para poder gestionar las rutas con parámetro (como la de un elemento) y el de moment
para trabajar con fechas de una manera más amigable.
Una vez tengamos todas las dependencias creamos la carpeta pages/
para dejar allí las páginas que yo vaya mencionando y la carpeta components/
para dejar los componentes(y así reutilizarlos).
Creamos la página principal(recuerda, carpeta pages
) en la que enseñaremos todos los Posts(esto será un componente llamado Posts
):
import React from 'react'
import Head from 'next/head'
import Posts from '../components/Posts'
function FirstPage() {
return(
<div className="App">
<Head>
<link rel="stylesheet" href="/static/app.css"/>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"/>
</Head>
<Posts />
</div>
)
}
export default FirstPage
Lo más destacable de este componente es que usamos el componente Head
de Next que nos permite incrustar elementos en el <head>
de el HTML que se genere. Hemos añadido una hoja de estilos para mejorar el sitio así como la librería de FontAwesome que usaremos para íconos.
Con respecto al componente de Posts
este es su código:
import React from 'react'
import moment from 'moment'
import Header from './Header'
class Posts extends React.Component {
constructor(props) {
super(props)
this.state = {
posts: []
}
}
componentDidMount() {
fetch('https://owcrud-api.now.sh/api/posts')
.catch(err => console.error(err))
.then(res => res.json())
.then(posts => this.setState({ posts }))
}
render() {
if(this.state.posts.length > 0) {
return(
<div className="App">
<Header />
<div className="Posts">
{
this.state.posts.map((post) => (
<div className="Posts-Item">
<div className="PhotoSegment">
<img src={post.image} alt={post.title}/>
</div>
<div className="DetailsSegment">
<h2 className="Post-Title">{post.title}</h2>
<h3 className="Post-Date">{moment(post.releaseDate).fromNow()}</h3>
</div>
</div>
))
}
</div>
</div>
)
} else {
return(
<h3>Cargando...</h3>
)
}
}
}
export default Posts
Si te fijas, es muy sencillo, nos limitamos a hacer una petición a la ruta que trae todos los Posts usando la API de Fetch y los mostramos uno a uno . También añadimos el componente de Header
, el cual no es más que un elemento decorativo que tienes disponible en el repositorio del proyecto .
Con este código tan sencillo, y aplicando unos cuantos estilos a las clases que hemos definido, obtendremos una vista así:
Recuerda que para colocar clases a los elementos en React usamos className
en vez de class
ya que este último se confunde con el código de JavaScript. Todo ésto lo vimos en ReactJS: Diferencias en atributos de JSX y HTML .
Paso 2: Construyendo la vista de post individual
¿Pensabas que todo sería más dificil? ¡Te equivocabas! Ahora crearemos una vista para cada post individual . Esta se accederá de la siguiente manera /post/:id
, como ya vimos cada página que creamos en Next crea una URL pero sin parámetro. De momento lo que haremos es crear la página Post
y sus componentes y al final la asignaremos a la ruta /post/:id
.
La página de Post
es muy sencilla a la anterior:
import React from 'react'
import Head from 'next/head'
import Post from '../components/Post'
class SinglePage extends React.Component {
static async getInitialProps({ req }) {
return { id: req.params.id }
}
render() {
return(
<div className="App">
<Head>
<link rel="stylesheet" href="/static/app.css"/>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"/>
</Head>
<Post id={this.props.id}/>
</div>
)
}
}
export default SinglePage
Si te fijas, en esta página destacamos el método getInitialProps
que nos servirá para obtener el token que esté en la URL(cuando la asociemos /post/:id
) y así poder pedirle a la API ese Post. Se lo pasamos al componente Post
que cargará un solo Post y pedirá este a la API:
import React from 'react'
import moment from 'moment'
import Header from './Header'
class Post extends React.Component {
constructor(props) {
super(props)
this.state = {
item: {}
}
}
componentDidMount() {
fetch(`https://owcrud-api.now.sh/api/posts/${this.props.id}`)
.catch(err => console.error(err))
.then(res => res.json())
.then(item => this.setState({ item }))
}
render() {
if(this.state.item._id) {
return(
<div className="App">
<Header />
<div className="Post">
<div className="Item">
<div className="Item-Detail">
<div className="Item-Line">
<h1 className="Post-Title">{this.state.item.title}</h1>
<span
className="Post-Icon fa fa-pencil"
onClick={this.updatePost}
data-id={this.state.item._id}
/>
</div>
<h3 className="Post-Date">{moment(this.state.item.releaseDate).fromNow()}</h3>
</div>
<div className="Item-Categories">
<p>En este curso aprenderás:
{
this.state.item.contents.map(content => (
<p className="Content-Item">{content}</p>
))
}
</p>
</div>
<div className="Item-Photo">
<img src={this.state.item.image} alt={this.state.item.title}/>
</div>
</div>
</div>
</div>
)
} else {
return('Cargando...')
}
}
}
export default Post
Aquí viene la novedad Ya que NextJS no soporta las rutas con parámetro vamos a crear un archivo llamado server.js
que usando métodos tanto de Express como de Next nos va a ayudar a redirigir todas las rutas al lugar correcto:
const express = require('express')
const { parse } = require('url') //Módulo nativo de Node. No hay que instalarlo.
const next = require('next')
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler() // Este método gestiona las rutas tal y como Next lo hace por defecto
app.prepare().then(() => {
const server = express()
server.get('/post/:id', (req, res) => {
const mergedQuery = Object.assign({}, req.query, req.params) //Unimos en un objeto los parámetros y los querys, en el caso de que los necesitemos
console.log(req.params)
return app.render(req, res, '/post', mergedQuery) // Mandamos al usuario que pida esta ruta a la registrada como Post por Next.
})
server.get('*', (req, res) => {
return handle(req, res) // Next gestiona el resto de rutas
})
const port = process.env.PORT || 3000
server.listen(port, (err) => {
if (err) throw err
console.log(`> Ready on port ${port}...`)
})
})
¿Sencillo, verdad? Si pruebas esta vista(con estilos) deberás de ver algo así:
Paso 3: Añadiendo funcionalidad para eliminar un Post
Como habrás notado, en la vista de todos los Posts tenemos un pequeño ícono que usaremos para borrar el elemento cuando hagamos click en éste. Para ésto crearemos un método en el componente Posts
:
deletePost(ev) {
let el = ev.target
let id = el.dataset.id
let index = el.dataset.index
fetch(`https://owcrud-api.now.sh/api/posts/${id}`, {
method: 'DELETE'
})
.catch(err => console.error(err))
.then(() => {
let posts = this.state.posts
posts.splice(index, 1)
this.setState({ posts })
})
}
Si te fijas, el botón que usamos tiene dos elementos data-*
correspondientes al id de la base de datos y al índice del array de todos los Posts, éstos nos ayudan a borrar el Post que básicamente es una petición al API y, en caso de que todo vaya bien, eliminarlo del Array de Posts del estado del componente.
Recuerda, que debes de hacer bind del this
en el constructor tal que así:
constructor(props) {
super(props)
this.state = {
posts: []
}
this.deletePost = this.deletePost.bind(this)
}
Si te fijas, al hacer click en el elemento desaparecerá de la lista de elementos en la vista y del Array.
Paso 4: Construyendo el formulario para añadir un nuevo post
En este paso, habilitaremos un formulario muy básico para crear un nuevo Post en nuestra API, que a su vez se verá reflejada en el Front que estamos llevando a cabo.
Para ello crearemos una nueva página, muy similar a las que ya estamos acostumbrados a crear:
import React from 'react'
import Head from 'next/head'
import Form from '../components/Form'
class AddPost extends React.Component {
render() {
return(
<div className="App">
<Head>
<link rel="stylesheet" href="/static/app.css"/>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"/>
</Head>
<Form />
</div>
)
}
}
export default AddPost
Y ahora, construimos el componente del formulario, su lógica es simplemente la de un formulario común con un botón con la diferencia que los campos se van agregando al state conforme escribimos, siendo más limpio nuestro código cuando querramos obtener lo que el usuario ha escrito(ya que solo llamamamos al state).
También tenemos un método para poder enviar el contenido del formulario a la API, y así, crear el nuevo Post:
import React from 'react'
import moment from 'moment'
class PostForm extends React.Component {
constructor(props) {
super(props)
this.state = {
form: {}
}
this.sendForm = this.sendForm.bind(this)
}
sendForm(ev) {
ev.preventDefault()
fetch('https://owcrud-api.now.sh/api/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(this.state.form)
})
.catch(err => console.log(err))
.then(res => res.json())
.then(thing => console.log(thing))
}
render() {
return(
<form className="PostForm">
<div className="FormInput">
<label htmlFor="Title" className="FormInput-Label">Título</label>
<input
type="text"
className="FormInput-Input"
name="Title"
onChange={(ev) => { this.setState({ form: { ...this.state.form, title: ev.target.value } }) }}
/>
</div>
<div className="FormInput">
<label htmlFor="Categories" className="FormInput-Label">Categorías</label>
<input
type="text"
className="FormInput-Input"
name="Categories"
onChange={(ev) => { this.setState({ form: { ...this.state.form, contents: ev.target.value.split(',') } }) }}
/>
</div>
<div className="FormInput">
<label htmlFor="ReleaseDate" className="FormInput-Label">Fecha de lanzamiento</label>
<input
type="date"
className="FormInput-Input"
name="ReleaseDate"
onChange={(ev) => { this.setState({ form: { ...this.state.form, releaseDate: moment(ev.target.value).unix() } }) }}
/>
</div>
<div className="FormInput">
<label htmlFor="ImageURL" className="FormInput-Label">URL de la Imagen</label>
<input
type="text"
className="FormInput-Input"
name="ImageURL"
onChange={(ev) => { this.setState({ form: { ...this.state.form, image: ev.target.value } }) }}
/>
</div>
<div className="FormInput">
<label htmlFor="Special" className="FormInput-Label">Curso Especial</label>
<input
type="checkbox"
className="FormInput-Input"
name="Special"
onChange={(ev) => { let result = (ev.target.value == 'on') ? true : false
this.setState({ form: { ...this.state.form, special: result } }) }}
/>
</div>
<button onClick={this.sendForm} className="Form-Btn">Publicar curso</button>
</form>
)
}
}
export default PostForm
Como ves, al crear un usuario en la API de manera satisfactoria, hacemos console.log()
de la petición. También podríamos hacer algo como redireccionar a la raíz, donde están todos los posts así el usuario ya puede ver el nuevo post.
Paso 5: Adaptando el componente de formulario para hacer que actualice Posts
Finalmente, vamos a reutilizar el útltimo componente que hemos creado, siguiendo la filosofía de React. Para ello vamos a hacerle algunos cambios en el método de envío y añadiremos usando el método del ciclo de vida componentDidMount()
el id del Post que vayamos a editar a nuestro estado. De esta manera se enviará a la API junto a los cambios y está tocará la base de datos:
sendForm(ev) {
ev.preventDefault()
// Añadimos un if con la respectivs petición para editar un Post.
if(this.props.type == 'update') {
fetch('https://owcrud-api.now.sh/api/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(this.state.form)
})
.catch(err => console.error(err))
.then(res => res.json())
.then(item => this.props.updateItem(item))
} else {
fetch('https://owcrud-api.now.sh/api/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(this.state.form)
})
.catch(err => console.log(err))
.then(res => res.json())
.then(thing => console.log(thing))
}
}
componentDidMount() {
// Con este prop controlamos si vamos a usar el componente para crear o para actualizar.
if(this.props.type == 'update') {
const elements = document.querySelectorAll('.FormInput-Input')
// Mostramos los datos que ya tenemos en la vista en el formulario, para ayudar al usuario en el proceso de edición.
elements[0].value = this.props.item.title
elements[1].value = this.props.item.contents.toString()
elements[2].value = this.props.item.releaseDate.split('T')[0]
elements[3].value = this.props.item.image
elements[4].checked = this.props.item.special
// Añadimos el id del Post que estamos cambiando
this.setState({ form: { ...this.state.form, _id: this.props.item._id } })
}
}
Para mostrar este componente, en el componente que correponde a la vista de Post simple lo llamamos(usando el prop type="update"
) y creamos el método de updateItem()
que hemos colocado en el método que envía los datos para gestionar las acciones después de cambiar el Post en la API:
updateItem(item) {
this.setState({ item })
this.toggleUpdate()
}
[...]
let showForm = (this.state.showUpdate) ?
(<div className="Update-Form">
<Form type="update" item={this.state.item} updateItem={this.updateItem} /> </div>) : null
Como ves, instancio el componente en una variable para poder hacer que sólo se vea si el usuario le da a el botón que también tenemos en esta vista.
Conclusiones
Hemos visto todas las facetas del lenguaje de Javascript, que a diferencia de otros son bastantes. Aquí acaba la construcción de el CRUD aunque aquí abajo tienes algunos enlaces interesantes para poder complementarte a lo aprendido en esta lectura. No dudes en exponer tus cuestiones en los comentarios.
Enlaces Relacionados
Populating <Head>
- NextJS Docs