Hero

Creación de una nueva página y diseño con Next.js

Mayo 30, 2022

jperez
NextJS

Next.js es un framework de React que permite crear aplicaciones que se ejecutan tanto en el cliente como en el servidor, también conocidas como aplicaciones end-to-end de JavaScript. Este framework ayuda a crear aplicaciones end-to-end en menos tiempo, optimizando las funciones básicas, como el enrutamiento del lado del cliente y el diseño de la página. A la vez que simplifica las funciones avanzadas, como el renderizado del lado del servidor y la división del código.

Tener en cuenta que con Next.js, cada página está representada por un archivo JavaScript en el subdirectorio pages. Cada archivo, en lugar de tener plantillas HTML, exporta un componente React que utiliza Next.js para representar la página, siendo la ruta raíz por defecto.

Next.js también admite páginas con rutas dinámicas. Por ejemplo, si se crea un archivo llamado pages/posts/[id].js, entonces será accesible en posts/1, etc. Esto será visto en otro artículo con ejemplos de códigos.

Por defecto, Next.js pre-renderiza cada página. Esto significa que Next.js genera el HTML de cada página por adelantado, en lugar de que todo lo haga el JavaScript del lado del cliente. El pre-renderizado puede resultar en un mejor rendimiento y SEO.

Cada HTML generado se asocia con el código JavaScript mínimo necesario para esa página. Cuando el navegador carga una página, su código JavaScript se ejecuta y hace que la página sea totalmente interactiva.

Por otro lado, el modelo React nos permite deconstruir una página en una serie de componentes. Muchos de estos componentes suelen reutilizarse entre páginas. Por ejemplo, puedes tener la misma barra de navegación y pie de página en todas las páginas. Este componente es un layout y envolverá nuestra aplicación con el diseno básico que definamos

A continuación, se describe paso a paso de como crear una página y un layout en NextJs:

Para comenzar se debe tomar como código inicial el proyecto creado anteriormente (https://github.com/jjosequevedo/product-list). Es importante aclarar que este repositorio es un listado de productos y lo que se hará en este artículo es añadirle las categorías para agruparlos.

Entonces se comienza creando un directorio con el nombre de categories:

nextjs page layout

Dentro del directorio, se agrega el archivo index.js con el siguiente código:

import React from 'react';

class Categories extends React.Component {

  render() {
    return (
      <h1>My first page!</h1>
    );
  }
}

export default Categories;

Por lo que ahora tenemos una nueva página con la ruta /categories.

Luego, se crean 2 nuevos componentes que serán usados en la nueva página categoryform.js y categorylist.js.

Para el primer componente se debe agregar el siguiente código con el cual se podrá crear un formulario para adicionar una nueva categoría o para cargar los datos de una ya creada para editarla:

categoryform.js

import React from 'react';

class CategoryForm extends React.Component {

  constructor(props) {
    super(props);
    this.state = {
      title: 'Add Category',
      _id: '',
      category_name: '',
    };
  }

  // Clear values.
  clearValues = () => {
    this.setState({
      _id: '',
      category_name: ''
    });
  }

  // Update state.
  onChanged = e => {
    this.setState({
      category_name: e.target.value
    });
  };

  // Add a category.
  onAddCategoryAction = e => {
    e.preventDefault();
    if (typeof this.props.onAddFormAction == 'function' && this.isValid()) {
      this.props.onAddFormAction({
        category_name: this.state.category_name
      });
      this.clearValues();
    }
  };

  // Edit a category.
  onEditCategoryAction = e => {
    e.preventDefault();
    if (typeof this.props.onEditFormAction == 'function' && this.isValid()) {
      this.props.onEditFormAction(this.state);
      this.clearValues();
    }
  };

  // Load the values after selecting a category from the list.
  loadValues = category => {
    this.setState({
      _id: category._id,
      category_name: category.category_name
    });
  };

  // Display buttons according to the operation.
  showButtons = () => {
    if (this.state._id != '') {
      return (
        <>
          <button type="submit" className="btn btn-primary" onClick={this.onEditCategoryAction}>Edit</button>
          <a className="btn btn-danger" onClick={this.clearValues}>Cancel</a>
        </>
      );
    }
    return (
      <button type="submit" className="btn btn-primary" onClick={this.onAddCategoryAction}>Add</button>
    );
  };

  // Check if the state is valid.
  isValid = () => {
    const isValid = Object.keys(this.state).every(key => {
      if (key != '_id') {
        return this.state[key] != '';
      }
      return true;
    });
    if (!isValid) {
      alert("There are some fields empty!");
    }
    return isValid;
  };

  render() {
    return (
      <div className='card mb-2 mt-2'>
        <div className='card-body'>
          <h5 className="card-title">{this.state.title}</h5>
          <hr className='divider' />
          <form method='post' name='form-category'>
            <div className='row mb-2'>
              <div className="col">
                <input type="text"
                  className="form-control"
                  id="category_name"
                  name="category_name"
                  placeholder='Category name'
                  value={this.state.category_name}
                  required={true}
                  onChange={this.onChanged} />
              </div>
              <div className='col'>
                <div className="d-grid gap-2 d-md-flex">
                  {this.showButtons()}
                </div>
              </div>
            </div>
          </form>
        </div>
      </div>
    );
  }
}

export default CategoryForm;

Para el segundo componente se debe agregar el siguiente código, el objetivo es listar todas las categorías que se han creado hasta el momento y se mostrará por cada fila 2 botones que permitirán editar o eliminar una categoría.

import React from 'react';
/**
 * This is the category list component.
 */
class CategoryList extends React.Component {

  // Action to edit a category.
  onEdit = (e, category) => {
    e.preventDefault();
    if (typeof this.props.onEdit == 'function') {
      this.props.onEdit(category);
    }
  };

  // Action to delete a category.
  onDelete = (e, category) => {
    e.preventDefault();
    if (typeof this.props.onEdit == 'function') {
      this.props.onDelete(category);
    }
  };

  render = () => {
    return (
      <>
        <ul className="nav nav-tabs" role="tablist">
          <li className="nav-item" role="presentation">
            <button className="nav-link active" id="category-list-tab" databstoggle="tab" databstarget="#category-list-tab" type="button" role="tab" aria-controls="category-list-tab" aria-selected="true">Category list</button>
          </li>
        </ul>
        <div className="tab-content">
          <div className="tab-pane fade show active" id="home" role="tabpanel" aria-labelledby="category-list-tab">
            <table className="table">
              <thead className="thead-light">
                <tr>
                  <th scope="col">#</th>
                  <th scope="col">Category name</th>
                  <th scope="col">Action</th>
                </tr>
              </thead>
              <tbody>
                {/* List categorys */}
                {
                  this.props.categories.map((p, i) => {
                    return (
                      <tr key={p._id}>
                        <th scope="row">{i + 1}</th>
                        <td>{p.category_name}</td>
                        <td>
                          <div className="d-grid gap-2 d-md-flex">
                            <button className='btn btn-light' onClick={(e) => this.onEdit(e, p)}>Edit</button>
                            <button className='btn btn-danger' onClick={e => this.onDelete(e, p)}>Delete</button>
                          </div>
                        </td>
                      </tr>
                    );
                  })
                }
              </tbody>
            </table>
          </div>
        </div>
      </>
    );
  };
}

export default CategoryList;

Ahora debemos modificar la página que acabamos de crear para agregar los 2 nuevos componentes:

import React from 'react';
import CategoryForm from '../../components/categoryform';
import CategoryList from '../../components/categorylist';

class Categories extends React.Component {

  constructor(props) {
    super(props);
    // Create a category form reference.
    this.categoryForm = React.createRef();
    this.state = {
      categories: []
    };
  }

  // After mounting the component, the list is loaded.
  componentDidMount = () => {
    this.loadList();
  };

  // Load a list of categories.
  loadList = () => {
    fetch('/api/listcategories').then(response => {
      return response.text();
    }).then(value => {
      this.setState({ categories: JSON.parse(value) });
    });
  }

  // Add a new category.
  onAddFormAction = data => {
    const request = {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    };
    fetch('/api/addcategory', request)
      .then(response => {
        if (response.status == 200) {
          this.loadList();
        }
      });
  };

  // Edit a selected category.
  onEditFormAction = data => {
    const request = {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    };
    fetch('/api/editcategory', request)
      .then(response => {
        if (response.status == 200) {
          this.loadList();
        }
      });
  };

  // Load a selected category into the form.
  onEdit = category => {
    if (typeof this.categoryForm.current.loadValues == 'function') {
      this.categoryForm.current.loadValues(category);
    }
  };

  // Remove a selected category.
  onDelete = category => {
    const request = {
      method: 'DELETE',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(category)
    };
    fetch('/api/deletecategory', request)
      .then(response => {
        if (response.status == 200) {
          this.loadList();
        }
      });
  };

  render() {
    return (
      <>
        <CategoryForm
          ref={this.categoryForm}
          onAddFormAction={this.onAddFormAction}
          onEditFormAction={this.onEditFormAction} />
        <CategoryList
          categories={this.state.categories}
          onEdit={this.onEdit}
          onDelete={this.onDelete} />
      </>
    );
  }
}

export default Categories;

Para que el usuario pueda navegar dentro del sitio, agregaremos un layout que nos permita tener un menú lateral para movernos entre las páginas. Entonces, creamos el directorio layouts y agregamos el archivo AdminLayout.js con el código:

AdminLayout.js

import React from 'react';
import Sidebar from '../components/sidebar';

class AdminLayout extends React.Component {

  render() {
    return (
      <div className='container-fluid'>
        <div className='row'>
          <nav className='col-md-3 col-lg-2 d-md-block bg-light sidebar collapse'>
            <Sidebar />
          </nav>
          <main className='col-md-9 ms-sm-auto col-lg-10 px-md-4'>
            <div className='container'>
              <div className='row'>
                <div className='col'>
                  <div className="card mt-2">
                    <div className="card-header">
                      Product details
                    </div>
                    <div className="card-body">
                      {this.props.children}
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </main>
        </div>
      </div>
    );
  }
}

export default AdminLayout;

Para que podamos ver el layout en el sitio modificaremos el archivo _app.js y agregaremos el siguiente código:

_app.js

import AdminLayout from '../layouts/AdminLayout';

function MyApp({ Component, pageProps }) {
  return (
    <AdminLayout>
      <Component {...pageProps} />
    </AdminLayout>
  );
}

export default MyApp;

Finalmente ejecutamos el comando yarn dev y abrimos un navegador e ingresamos la dirección http://localhost:3000. Debemos ser capaces de poder verlo así:

nextjs page layout 1

Puedes descargar el código desde aquí: https://github.com/jjosequevedo/product-categories

Recibe consejos y oportunidades de trabajo 100% remotas y en dólares de weKnow Inc.