Las cookies nos permiten ofrecer nuestros servicios. Al utilizar nuestros servicios, aceptas el uso que hacemos de las cookies. Más Información. Aceptar

Creando un CRUD con JavaScript: Construyendo el Frontend usando React

Miguh Ruiz
  • Escrito por Miguh Ruiz el 23 de Febrero de 2018
  • 5 min de lectura | Frontend
Creando un CRUD con JavaScript: Construyendo el Frontend usando React

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. Quiero leer el capítulo anterior

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

cors - NPM

NextJS Docs - Github

Fetch API - MDN

HTMLElement.dataset` - MDN

Populating <Head> - NextJS Docs

Custom Server and Routing - NextJS Docs

Repositorio del proyecto

Aplicación hecha deploy en Now

Estas son algunas de las empresas que ya confían en OpenWebinars

Profesores y profesionales

Nuestros docentes son profesionales que trabajan día a día en la materia que imparten

Conviértete en profesor de OpenWebinars