El autor seleccionó la Electronic Frontier Foundation para recibir una donación como parte del programa Write for DOnations.
Introducción
Ruby on Rails es un popular framework de aplicación web del lado del servidor. Impulsa muchas aplicaciones populares que existen en la web hoy en día, como GitHub, Basecamp, SoundCloud, Airbnb y Twitch. Con su énfasis en la experiencia del programador y la apasionada comunidad construida en torno a él, Ruby on Rails le proporcionará las herramientas que necesita para construir y mantener su aplicación web moderna.
React es una biblioteca de JavaScript utilizada para crear interfaces de usuario de front-end. Respaldada por Facebook, es una de las bibliotecas de front-end más populares utilizadas en la web hoy en día. React ofrece características como un Modelo de Objeto de Documento (DOM) virtual, arquitectura de componentes y gestión de estado, que hacen que el proceso de desarrollo de front-end sea más organizado y eficiente.
Con el frontend web moviéndose hacia frameworks separados del código del lado del servidor, combinar la elegancia de Rails con la eficiencia de React te permitirá construir aplicaciones potentes y modernas informadas por las tendencias actuales. Al utilizar React para renderizar componentes desde dentro de una vista de Rails (en lugar del motor de plantillas de Rails), tu aplicación se beneficiará de los últimos avances en JavaScript y desarrollo frontend, aprovechando la expresividad de Ruby on Rails.
En este tutorial, crearás una aplicación de Ruby on Rails que almacena tus recetas favoritas y luego las muestra con un frontend de React. Cuando hayas terminado, podrás crear, ver y eliminar recetas utilizando una interfaz de React estilizada con Bootstrap:
Requisitos previos
Para seguir este tutorial, necesitas:
-
Node.js y npm instalados en tu máquina de desarrollo. Este tutorial utiliza Node.js versión 16.14.0 y npm versión 8.3.1. Node.js es un entorno de ejecución de JavaScript que te permite ejecutar tu código fuera del navegador. Viene con un Administrador de Paquetes preinstalado llamado npm, que te permite instalar y actualizar paquetes. Para instalarlos en Ubuntu 20.04 o macOS, sigue la sección “Instalación mediante un PPA” de Cómo Instalar Node.js en Ubuntu 20.04 o los pasos en Cómo Instalar Node.js y Crear un Entorno de Desarrollo Local en macOS.
-
El Administrador de Paquetes Yarn instalado en tu máquina de desarrollo, lo que te permitirá descargar el framework React. Este tutorial fue probado en la versión 1.22.10; para instalar esta dependencia, sigue la guía oficial de instalación de Yarn.
-
Ruby on Rails instalado. Para obtenerlo, sigue nuestra guía sobre Cómo Instalar Ruby on Rails con rbenv en Ubuntu 20.04. Si deseas desarrollar esta aplicación en macOS, puedes usar Cómo Instalar Ruby on Rails con rbenv en macOS. Este tutorial fue probado en la versión 3.1.2 de Ruby y la versión 7.0.4 de Rails, así que asegúrate de especificar estas versiones durante el proceso de instalación.
Nota: La versión 7 de Rails no es compatible con versiones anteriores. Si estás utilizando la versión 5 de Rails, por favor visita el tutorial para Cómo Configurar un Proyecto de Ruby on Rails v5 con un Frontend de React en Ubuntu 18.04.
- PostgreSQL está instalado, como se describe en los Pasos 1 y 2 Cómo utilizar PostgreSQL con tu aplicación Ruby on Rails en Ubuntu 20.04 o Cómo utilizar PostgreSQL con tu aplicación Ruby on Rails en macOS. Para seguir este tutorial, puedes utilizar la versión 12 o superior de PostgreSQL. Si deseas desarrollar esta aplicación en una distribución diferente de Linux u otro sistema operativo, consulta la página oficial de descargas de PostgreSQL. Para obtener más información sobre cómo utilizar PostgreSQL, consulta Cómo instalar y utilizar PostgreSQL.
Paso 1 — Crear una nueva aplicación Rails
Construirás tu aplicación de recetas en el marco de aplicación Rails en este paso. Primero, crearás una nueva aplicación Rails, que se configurará para funcionar con React.
Rails proporciona varios scripts llamados generadores que crean todo lo necesario para construir una aplicación web moderna. Para revisar una lista completa de estos comandos y lo que hacen, ejecuta el siguiente comando en tu terminal:
- rails -h
Este comando generará una lista exhaustiva de opciones, permitiéndote establecer los parámetros de tu aplicación. Uno de los comandos listados es el comando new
, que crea una nueva aplicación de Rails.
Ahora, crearás una nueva aplicación de Rails utilizando el generador new
. Ejecuta el siguiente comando en tu terminal:
- rails new rails_react_recipe -d postgresql -j esbuild -c bootstrap -T
El comando anterior crea una nueva aplicación de Rails en un directorio llamado rails_react_recipe
, instala las dependencias requeridas de Ruby y JavaScript, y configura Webpack. Las banderas asociadas con este comando generador new
incluyen lo siguiente:
- La bandera
-d
especifica el motor de base de datos preferido, que en este caso es PostgreSQL. - La bandera
-j
especifica el enfoque de JavaScript de la aplicación. Rails ofrece algunas formas diferentes de manejar el código JavaScript en las aplicaciones de Rails. La opciónesbuild
pasada a la bandera-j
instruye a Rails a preconfigurar esbuild como el empaquetador de JavaScript preferido. - La bandera
-c
especifica el procesador de CSS de la aplicación. Bootstrap es la opción preferida en este caso. - La bandera
-T
instruye a Rails a omitir la generación de archivos de prueba ya que no escribirás pruebas para este tutorial. Este comando también se sugiere si deseas usar una herramienta de prueba de Ruby diferente de la que proporciona Rails.
Una vez que el comando haya terminado, dirígete al directorio rails_react_recipe
, que es el directorio raíz de tu aplicación:
- cd rails_react_recipe
A continuación, enumera el contenido del directorio:
- ls
OutputGemfile README.md bin db node_modules storage yarn.lock
Gemfile.lock Rakefile config lib package.json tmp
Procfile.dev app config.ru log public vendor
Este directorio raíz tiene varios archivos y carpetas generados automáticamente que conforman la estructura de una aplicación Rails, incluido un archivo package.json
que contiene las dependencias de una aplicación React.
Ahora que ha creado correctamente una nueva aplicación Rails, la conectará a una base de datos en el siguiente paso.
Paso 2: Configuración de la base de datos
Antes de ejecutar su nueva aplicación Rails, primero debe conectarla a una base de datos. En este paso, conectará la aplicación Rails recién creada a una base de datos PostgreSQL para que los datos de las recetas se puedan almacenar y recuperar según sea necesario.
El archivo database.yml
encontrado en config/database.yml
contiene detalles de la base de datos como nombres de base de datos para diferentes entornos de desarrollo. Rails especifica un nombre de base de datos para los diversos entornos de desarrollo agregando un guion bajo (_
) seguido del nombre del entorno. En este tutorial, usará los valores de configuración de base de datos predeterminados, pero puede cambiar sus valores de configuración si es necesario.
Nota: En este punto, puedes alterar config/database.yml
para establecer qué rol de PostgreSQL te gustaría que Rails use para crear tu base de datos. Durante los requisitos previos, creaste un rol que está asegurado por una contraseña en el tutorial Cómo Usar PostgreSQL con tu Aplicación Ruby on Rails. Si aún no has configurado el usuario, ahora puedes seguir las instrucciones para Paso 4 — Configurar y Crear tu Base de Datos en el mismo tutorial previo.
Rails ofrece muchos comandos que facilitan el desarrollo de aplicaciones web, incluidos comandos para trabajar con bases de datos como create
, drop
y reset
. Para crear una base de datos para tu aplicación, ejecuta el siguiente comando en tu terminal:
- rails db:create
Este comando crea una base de datos development
y test
, dando como resultado la siguiente salida:
OutputCreated database 'rails_react_recipe_development'
Created database 'rails_react_recipe_test'
Ahora que la aplicación está conectada a una base de datos, inicia la aplicación ejecutando el siguiente comando:
- bin/dev
Rails proporciona un script alternativo bin/dev
que inicia una aplicación Rails ejecutando los comandos en el archivo Procfile.dev
en el directorio raíz de la aplicación utilizando la gema Foreman.
Una vez que ejecutes este comando, tu indicador de comando desaparecerá y en su lugar se imprimirá la siguiente salida:
Outputstarted with pid 70099
started with pid 70100
started with pid 70101
yarn run v1.22.10
yarn run v1.22.10
$ esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=assets --watch
$ sass ./app/assets/stylesheets/application.bootstrap.scss:./app/assets/builds/application.css --no-source-map --load-path=node_modules --watch
=> Booting Puma
=> Rails 7.0.4 application starting in development
=> Run `bin/rails server --help` for more startup options
[watch] build finished, watching for changes...
Puma starting in single mode...
* Puma version: 5.6.5 (ruby 3.1.2-p20) ("Birdie's Version")
* Min threads: 5
* Max threads: 5
* Environment: development
* PID: 70099
* Listening on http://127.0.0.1:3000
* Listening on http://[::1]:3000
Use Ctrl-C to stop
Sass is watching for changes. Press Ctrl-C to stop.
Para acceder a tu aplicación, abre una ventana del navegador y navega a http://localhost:3000
. Se cargará la página de bienvenida predeterminada de Rails, lo que significa que has configurado correctamente tu aplicación de Rails:
Para detener el servidor web, presiona CTRL+C
en la terminal donde se está ejecutando el servidor. Obtendrás un mensaje de despedida de Puma:
Output^C SIGINT received, starting shutdown
- Gracefully stopping, waiting for requests to finish
=== puma shutdown: 2019-07-31 14:21:24 -0400 ===
- Goodbye!
Exiting
sending SIGTERM to all processes
terminated by SIGINT
terminated by SIGINT
exited with code 0
Luego, volverá a aparecer el prompt de tu terminal.
Has configurado correctamente una base de datos para tu aplicación de recetas de comida. En el siguiente paso, instalarás las dependencias de JavaScript que necesitas para armar tu frontend de React.
Paso 3 — Instalación de Dependencias del Frontend
En este paso, instalarás las dependencias de JavaScript necesarias en el frontend de tu aplicación de recetas de comida. Incluyen:
- React para construir interfaces de usuario.
- React DOM para permitir que React interactúe con el DOM del navegador.
- React Router para manejar la navegación en una aplicación de React.
Ejecuta el siguiente comando para instalar estos paquetes con el gestor de paquetes Yarn:
- yarn add react react-dom react-router-dom
Este comando utiliza Yarn para instalar los paquetes especificados y los añade al archivo package.json
. Para verificar esto, abre el archivo package.json
ubicado en el directorio raíz del proyecto:
- nano package.json
Los paquetes instalados se listarán bajo la clave dependencies
:
{
"name": "app",
"private": "true",
"dependencies": {
"@hotwired/stimulus": "^3.1.0",
"@hotwired/turbo-rails": "^7.1.3",
"@popperjs/core": "^2.11.6",
"bootstrap": "^5.2.1",
"bootstrap-icons": "^1.9.1",
"esbuild": "^0.15.7",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.3.0",
"sass": "^1.54.9"
},
"scripts": {
"build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=assets",
"build:css": "sass ./app/assets/stylesheets/application.bootstrap.scss:./app/assets/builds/application.css --no-source-map --load-path=node_modules"
}
}
Cierra el archivo presionando CTRL+X
.
Has instalado algunas dependencias front-end para tu aplicación. A continuación, configurarás una página de inicio para tu aplicación de recetas de comida.
Paso 4 — Configuración de la Página de Inicio
Con las dependencias requeridas instaladas, ahora crearás una página de inicio para la aplicación para que sirva como la página de inicio cuando los usuarios visiten por primera vez la aplicación.
Rails sigue el patrón arquitectónico Modelo-Vista-Controlador para aplicaciones. En el patrón MVC, el propósito de un controlador es recibir solicitudes específicas y pasarlas al modelo o vista apropiados. Actualmente, la aplicación muestra la página de bienvenida de Rails cuando se carga la URL raíz en el navegador. Para cambiar esto, crearás un controlador y una vista para la página de inicio y luego lo emparejarás con una ruta.
Rails proporciona un generador de controller
para crear un controlador. El generador de controller
recibe un nombre de controlador y una acción correspondiente. Para obtener más información al respecto, puedes revisar la documentación de Rails.
Este tutorial llamará al controlador Homepage
. Ejecuta el siguiente comando para crear un controlador Homepage
con una acción index
:
- rails g controller Homepage index
Nota:
En Linux, el error FATAL: Listen error: unable to monitor directories for changes.
puede deberse a un límite del sistema en la cantidad de archivos que tu máquina puede monitorear para realizar cambios. Ejecuta el siguiente comando para solucionarlo:
- echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
Este comando aumentará permanentemente la cantidad de directorios que puedes monitorear con Listen
a 524288
. Puedes cambiar esto nuevamente ejecutando el mismo comando y reemplazando 524288
con tu número deseado.
Al ejecutar el comando controller
, se generan los siguientes archivos:
- A
homepage_controller.rb
file for receiving all homepage-related requests. This file contains theindex
action you specified in the command. - A
homepage_helper.rb
file for adding helper methods related to theHomepage
controller. - Un archivo
index.html.erb
como la página de vista para representar cualquier cosa relacionada con la página de inicio.
Además de estas nuevas páginas creadas al ejecutar el comando de Rails, Rails también actualiza tu archivo de rutas ubicado en config/routes.rb
, agregando una ruta get
para tu página de inicio, que modificarás como tu ruta principal.
A root route in Rails specifies what will show up when users visit the root URL of your application. In this case, you want your users to see your homepage. Open the routes file located at config/routes.rb
in your favorite editor:
- nano config/routes.rb
En este archivo, reemplaza get 'homepage/index'
con root 'homepage#index'
para que el archivo coincida con lo siguiente:
Rails.application.routes.draw do
root 'homepage#index'
# Para obtener detalles sobre el DSL disponible dentro de este archivo, consulta http://guides.rubyonrails.org/routing.html
end
Esta modificación instruye a Rails para que mapee las solicitudes a la raíz de la aplicación a la acción index
del controlador Homepage
, que a su vez renderiza en el navegador lo que sea que esté en el archivo index.html.erb
ubicado en app/views/homepage/index.html.erb
.
Guarda y cierra el archivo.
Para verificar que esto funcione, inicia tu aplicación:
- bin/dev
Cuando abras o actualices la aplicación en el navegador, se cargará una nueva página de inicio para tu aplicación:
Una vez que hayas verificado que tu aplicación funciona, presiona CTRL+C
para detener el servidor.
A continuación, abre el archivo ~/rails_react_recipe/app/views/homepage/index.html.erb
:
- nano ~/rails_react_recipe/app/views/homepage/index.html.erb
Elimina el código dentro del archivo, luego guarda el archivo como vacío. Al hacer esto, te aseguras de que el contenido de index.html.erb
no interfiera con la representación de React en tu frontend.
Ahora que has configurado tu página de inicio para tu aplicación, puedes pasar a la siguiente sección, donde configurarás el frontend de tu aplicación para usar React.
Paso 5 — Configurar React como tu frontend de Rails
En este paso, configurarás Rails para usar React en el frontend de la aplicación, en lugar de su motor de plantillas. Esta nueva configuración te permitirá crear una página de inicio más atractiva visualmente con React.
Con la ayuda de la opción esbuild
especificada al generar la aplicación de Rails, la mayoría de la configuración requerida para permitir que JavaScript funcione sin problemas con Rails ya está en su lugar. Todo lo que queda es cargar el punto de entrada de la aplicación React en el punto de entrada de esbuild
para los archivos JavaScript. Para hacer esto, comienza creando un directorio de componentes en el directorio app/javascript
:
- mkdir ~/rails_react_recipe/app/javascript/components
El directorio components
albergará el componente para la página de inicio, junto con otros componentes de React en la aplicación, incluido el archivo de entrada en la aplicación React.
A continuación, abre el archivo application.js
ubicado en app/javascript/application.js
:
- nano ~/rails_react_recipe/app/javascript/application.js
Agrega la línea de código resaltada al archivo:
// Punto de entrada para el script de compilación en tu package.json
import "@hotwired/turbo-rails"
import "./controllers"
import * as bootstrap from "bootstrap"
import "./components"
La línea de código agregada al archivo application.js
importará el código en el archivo de entrada index.jsx
, haciéndolo disponible para esbuild
para el empaquetado. Con el directorio /components
importado en el punto de entrada JavaScript de la aplicación Rails, puedes crear un componente React para tu página de inicio. La página de inicio contendrá algunos textos y un botón de llamada a la acción para ver todas las recetas.
Guarda y cierra el archivo.
Luego, crea un archivo Home.jsx
en el directorio components
:
- nano ~/rails_react_recipe/app/javascript/components/Home.jsx
Agrega el siguiente código al archivo:
import React from "react";
import { Link } from "react-router-dom";
export default () => (
<div className="vw-100 vh-100 primary-color d-flex align-items-center justify-content-center">
<div className="jumbotron jumbotron-fluid bg-transparent">
<div className="container secondary-color">
<h1 className="display-4">Food Recipes</h1>
<p className="lead">
A curated list of recipes for the best homemade meal and delicacies.
</p>
<hr className="my-4" />
<Link
to="/recipes"
className="btn btn-lg custom-button"
role="button"
>
View Recipes
</Link>
</div>
</div>
</div>
);
En este código, importas React y el componente Link
de React Router. El componente Link
crea un hipervínculo para navegar de una página a otra. Luego, creas y exportas un componente funcional que contiene algo de lenguaje de marcado para tu página de inicio, estilizado con clases de Bootstrap.
Guarda y cierra el archivo.
Con tu componente Home
configurado, ahora configurarás el enrutamiento usando React Router. Crea un directorio routes
en el directorio app/javascript
:
- mkdir ~/rails_react_recipe/app/javascript/routes
El directorio routes
contendrá algunas rutas con sus componentes correspondientes. Cada vez que se cargue una ruta especificada, se representará su componente correspondiente en el navegador.
En el directorio routes
, crea un archivo index.jsx
:
- nano ~/rails_react_recipe/app/javascript/routes/index.jsx
Agrega el siguiente código a él:
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Home from "../components/Home";
export default (
<Router>
<Routes>
<Route path="/" element={<Home />} />
</Routes>
</Router>
);
En este archivo de ruta index.jsx
, importas los siguientes módulos: el módulo React
que te permite usar React, así como los módulos BrowserRouter
, Routes
y Route
de React Router, que juntos te ayudan a navegar de una ruta a otra. Por último, importas tu componente Home
, que se renderizará cada vez que una solicitud coincida con la ruta raíz (/
). Cuando desees agregar más páginas a tu aplicación, puedes declarar una ruta en este archivo y emparejarla con el componente que deseas renderizar para esa página.
Guarda y cierra el archivo.
Ahora has configurado el enrutamiento utilizando React Router. Para que React sea consciente de las rutas disponibles y las utilice, estas deben estar disponibles en el punto de entrada de la aplicación. Para lograr esto, renderizarás tus rutas en un componente que React representará en tu archivo de entrada.
Crea un archivo App.jsx
en el directorio app/javascript/components
:
- nano ~/rails_react_recipe/app/javascript/components/App.jsx
Agrega el siguiente código al archivo App.jsx
:
import React from "react";
import Routes from "../routes";
export default props => <>{Routes}</>;
En el archivo App.jsx
, importas React y los archivos de ruta que acabas de crear. Luego exportas un componente para renderizar las rutas dentro de fragmentos. Este componente se representará en el punto de entrada de la aplicación, haciendo que las rutas estén disponibles cada vez que se carga la aplicación.
Guarda y cierra el archivo.
Ahora que has configurado tu App.jsx
, puedes renderizarlo en tu archivo de entrada. Crea un archivo index.jsx
en el directorio components
:
- nano ~/rails_react_recipe/app/javascript/components/index.jsx
Agrega el siguiente código al archivo index.js
:
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
document.addEventListener("turbo:load", () => {
const root = createRoot(
document.body.appendChild(document.createElement("div"))
);
root.render(<App />);
});
En las líneas de import
, importas la biblioteca React, la función createRoot
de ReactDOM y tu componente App
. Utilizando la función createRoot
de ReactDOM, creas un elemento raíz como un elemento div
añadido a la página y renderizas tu componente App
en él. Cuando se carga la aplicación, React representará el contenido del componente App
dentro del elemento div
en la página.
Guarda y cierra el archivo.
Finalmente, agregarás algunos estilos CSS a tu página de inicio.
Abre el archivo application.bootstrap.scss
en el directorio ~/rails_react_recipe/app/assets/stylesheets/application.bootstrap.scss
:
- nano ~/rails_react_recipe/app/assets/stylesheets/application.bootstrap.scss
A continuación, reemplaza el contenido del archivo application.bootstrap.scss
con el siguiente código:
@import 'bootstrap/scss/bootstrap';
@import 'bootstrap-icons/font/bootstrap-icons';
.bg_primary-color {
background-color: #FFFFFF;
}
.primary-color {
background-color: #FFFFFF;
}
.bg_secondary-color {
background-color: #293241;
}
.secondary-color {
color: #293241;
}
.custom-button.btn {
background-color: #293241;
color: #FFF;
border: none;
}
.hero {
width: 100vw;
height: 50vh;
}
.hero img {
object-fit: cover;
object-position: top;
height: 100%;
width: 100%;
}
.overlay {
height: 100%;
width: 100%;
opacity: 0.4;
}
Configura algunos colores personalizados para la página. La sección .hero
creará el marco para una imagen heroica, o un gran banner web en la página principal de tu sitio web, que agregarás más tarde. Además, el estilo custom-button.btn
dará estilo al botón que el usuario utilizará para ingresar a la aplicación.
Con tus estilos CSS en su lugar, guarda y cierra el archivo.
A continuación, reinicia el servidor web de tu aplicación:
- bin/dev
Luego, recarga la aplicación en tu navegador. Se cargará una nueva página de inicio:
Detén el servidor web con CTRL+C
.
Configuraste tu aplicación para utilizar React como frontend en este paso. En el siguiente paso, crearás modelos y controladores que te permitirán crear, leer, actualizar y eliminar recetas.
Paso 6: Crear el Controlador y Modelo de Recetas
Ahora que has configurado un frontend de React para tu aplicación, crearás un modelo y controlador de Receta. El modelo de receta representará la tabla de la base de datos que contiene información sobre las recetas del usuario, mientras que el controlador recibirá y manejará las solicitudes para crear, leer, actualizar o eliminar recetas. Cuando un usuario solicita una receta, el controlador de recetas recibe esta solicitud y la pasa al modelo de recetas, que recupera los datos solicitados de la base de datos. Luego, el modelo devuelve los datos de la receta como respuesta al controlador. Finalmente, esta información se muestra en el navegador.
Comienza creando un modelo Receta
usando el subcomando generate model
proporcionado por Rails y especificando el nombre del modelo junto con sus columnas y tipos de datos. Ejecuta el siguiente comando:
- rails generate model Recipe name:string ingredients:text instruction:text image:string
El comando anterior instruye a Rails a crear un modelo Receta
junto con una columna nombre
de tipo string
, una columna ingredientes
y instrucción
de tipo text
, y una columna imagen
de tipo string
. Este tutorial ha nombrado el modelo Receta
, porque los modelos en Rails usan un nombre en singular mientras que sus tablas de base de datos correspondientes usan un nombre en plural.
Al ejecutar el comando generate model
se crean dos archivos y se imprime la siguiente salida:
Output invoke active_record
create db/migrate/20221017220817_create_recipes.rb
create app/models/recipe.rb
Los dos archivos creados son:
- A
recipe.rb
file that holds all the model-related logic. - A
20221017220817_create_recipes.rb
file (the number at the beginning of the file may differ depending on the date when you run the command). This migration file contains the instruction for creating the database structure.
A continuación, editarás el archivo del modelo de receta para asegurar que solo se guarden datos válidos en la base de datos. Puedes lograr esto agregando validaciones de base de datos a tu modelo.
Abre tu modelo de receta ubicado en app/models/recipe.rb
:
- nano ~/rails_react_recipe/app/models/recipe.rb
Agrega las siguientes líneas de código resaltadas al archivo:
class Recipe < ApplicationRecord
validates :name, presence: true
validates :ingredients, presence: true
validates :instruction, presence: true
end
En este código, agregas validación del modelo, que verifica la presencia de los campos name
, ingredients
e instruction
. Sin estos tres campos, una receta es inválida y no se guardará en la base de datos.
Guarda y cierra el archivo.
Para que Rails cree la tabla recipes
en tu base de datos, debes ejecutar una migración, que es una forma de realizar cambios en tu base de datos de manera programática. Para asegurarte de que la migración funcione con la base de datos que configuraste, debes realizar cambios en el archivo 20221017220817_create_recipes.rb
.
Abre este archivo en tu editor:
- nano ~/rails_react_recipe/db/migrate/20221017220817_create_recipes.rb
Agrega los materiales resaltados para que tu archivo coincida con lo siguiente:
class CreateRecipes < ActiveRecord::Migration[5.2]
def change
create_table :recipes do |t|
t.string :name, null: false
t.text :ingredients, null: false
t.text :instruction, null: false
t.string :image, default: 'https://raw.githubusercontent.com/do-community/react_rails_recipe/master/app/assets/images/Sammy_Meal.jpg'
t.timestamps
end
end
end
Este archivo de migración contiene una clase Ruby con un método change
y un comando para crear una tabla llamada recipes
junto con las columnas y sus tipos de datos. También actualizas 20221017220817_create_recipes.rb
con una restricción NOT NULL
en las columnas name
, ingredients
e instruction
agregando null: false
, asegurando que estas columnas tengan un valor antes de cambiar la base de datos. Finalmente, agregas una URL de imagen predeterminada para tu columna de imagen; esta podría ser otra URL si deseas usar una imagen diferente.
Con estos cambios, guarda y sale del archivo. Ahora estás listo para ejecutar tu migración y crear tu tabla. En tu terminal, ejecuta el siguiente comando:
- rails db:migrate
Utiliza el comando de migración de base de datos para ejecutar las instrucciones en tu archivo de migración. Una vez que el comando se ejecute correctamente, recibirás una salida similar a la siguiente:
Output== 20190407161357 CreateRecipes: migrating ====================================
-- create_table(:recipes)
-> 0.0140s
== 20190407161357 CreateRecipes: migrated (0.0141s) ===========================
Con tu modelo de receta en su lugar, a continuación crearás tu controlador de recetas para agregar la lógica de creación, lectura y eliminación de recetas. Ejecuta el siguiente comando:
- rails generate controller api/v1/Recipes index create show destroy --skip-template-engine --no-helper
En este comando, crearás un controlador de Recetas
en un directorio api/v1
con acciones de index
, create
, show
y destroy
. La acción index
manejará la obtención de todas tus recetas; la acción create
será responsable de crear nuevas recetas; la acción show
obtendrá una sola receta, y la acción destroy
contendrá la lógica para eliminar una receta.
También pasas algunas banderas para hacer que el controlador sea más ligero, incluyendo:
--skip-template-engine
, que indica a Rails que omita la generación de archivos de vista de Rails ya que React maneja tus necesidades de frontend.--no-helper
, que indica a Rails que omita la generación de un archivo de ayuda para tu controlador.
Al ejecutar el comando también se actualiza tu archivo de rutas con una ruta para cada acción en el controlador de Recetas
.
Cuando se ejecute el comando, imprimirá una salida como esta:
Output create app/controllers/api/v1/recipes_controller.rb
route namespace :api do
namespace :v1 do
get 'recipes/index'
get 'recipes/create'
get 'recipes/show'
get 'recipes/destroy'
end
end
Para usar estas rutas, realizarás cambios en tu archivo config/routes.rb
. Abre el archivo routes.rb
en tu editor de texto:
- nano ~/rails_react_recipe/config/routes.rb
Actualiza este archivo para que se vea como el siguiente código, alterando o agregando las líneas resaltadas:
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
get 'recipes/index'
post 'recipes/create'
get '/show/:id', to: 'recipes#show'
delete '/destroy/:id', to: 'recipes#destroy'
end
end
root 'homepage#index'
get '/*path' => 'homepage#index'
# Define tus rutas de aplicación según el DSL en https://guides.rubyonrails.org/routing.html
# Define la ruta de la raíz ("/")
# root "articles#index"
end
En este archivo de rutas, modifica el verbo HTTP de las rutas create
y destroy
para que pueda post
y delete
datos. También modifica las rutas para las acciones show
y destroy
agregando un parámetro :id
a la ruta. :id
contendrá el número de identificación de la receta que deseas leer o eliminar.
Agrega una ruta de captura de todo con get '/*path'
que dirigirá cualquier otra solicitud que no coincida con las rutas existentes a la acción index
del controlador homepage
. El enrutamiento del front-end manejará las solicitudes no relacionadas con la creación, lectura o eliminación de recetas.
Guarda y sale del archivo.
Para evaluar una lista de rutas disponibles en tu aplicación, ejecuta el siguiente comando:
- rails routes
Al ejecutar este comando, se muestra una lista extensa de patrones de URI, verbos y controladores o acciones coincidentes para tu proyecto.
A continuación, agregarás la lógica para obtener todas las recetas a la vez. Rails utiliza la biblioteca ActiveRecord para manejar tareas relacionadas con la base de datos como esta. ActiveRecord conecta clases con tablas de base de datos relacionales y proporciona una API rica para trabajar con ellas.
Para obtener todas las recetas, utilizarás ActiveRecord para consultar la tabla de recetas y obtener todas las recetas en la base de datos.
Abre el archivo recipes_controller.rb
con el siguiente comando:
- nano ~/rails_react_recipe/app/controllers/api/v1/recipes_controller.rb
Agrega las líneas resaltadas al controlador de recetas:
class Api::V1::RecipesController < ApplicationController
def index
recipe = Recipe.all.order(created_at: :desc)
render json: recipe
end
def create
end
def show
end
def destroy
end
end
En tu acción index
, utilizas el método all
de ActiveRecord para obtener todas las recetas en tu base de datos. Utilizando el método order
, las ordenas en orden descendente según su fecha de creación, lo que colocará las recetas más nuevas primero. Por último, envías tu lista de recetas como respuesta JSON con render
.
A continuación, agregarás la lógica para crear nuevas recetas. Al igual que al obtener todas las recetas, confiarás en ActiveRecord para validar y guardar los detalles de la receta proporcionados. Actualiza tu controlador de recetas con las siguientes líneas de código resaltadas:
class Api::V1::RecipesController < ApplicationController
def index
recipe = Recipe.all.order(created_at: :desc)
render json: recipe
end
def create
recipe = Recipe.create!(recipe_params)
if recipe
render json: recipe
else
render json: recipe.errors
end
end
def show
end
def destroy
end
private
def recipe_params
params.permit(:name, :image, :ingredients, :instruction)
end
end
En la acción create
, utilizas el método create
de ActiveRecord para crear una nueva receta. El método create
puede asignar todos los parámetros del controlador proporcionados al modelo de una vez. Este método facilita la creación de registros pero abre la posibilidad de un uso malintencionado. El uso malintencionado puede prevenirse utilizando la característica de parámetros fuertes proporcionada por Rails. De esta manera, los parámetros no pueden asignarse a menos que hayan sido permitidos. Pasas un parámetro recipe_params
al método create
en tu código. El recipe_params
es un método private
donde permites que los parámetros del controlador eviten que contenido incorrecto o malintencionado llegue a tu base de datos. En este caso, permites un parámetro name
, image
, ingredients
e instruction
para el uso válido del método create
.
Tu controlador de recetas ahora puede leer y crear recetas. Todo lo que queda es la lógica para leer y eliminar una única receta. Actualiza tu controlador de recetas con el código resaltado:
class Api::V1::RecipesController < ApplicationController
before_action :set_recipe, only: %i[show destroy]
def index
recipe = Recipe.all.order(created_at: :desc)
render json: recipe
end
def create
recipe = Recipe.create!(recipe_params)
if recipe
render json: recipe
else
render json: recipe.errors
end
end
def show
render json: @recipe
end
def destroy
@recipe&.destroy
render json: { message: 'Recipe deleted!' }
end
private
def recipe_params
params.permit(:name, :image, :ingredients, :instruction)
end
def set_recipe
@recipe = Recipe.find(params[:id])
end
end
En las nuevas líneas de código, creas un método privado set_recipe
llamado por un before_action
solo cuando las acciones show
y delete
coinciden con una solicitud. El método set_recipe
utiliza el método find
de ActiveRecord para encontrar una receta cuyo id
coincida con el id
proporcionado en los params
y lo asigna a una variable de instancia @recipe
. En la acción show
, devuelves el objeto @recipe
establecido por el método set_recipe
como una respuesta JSON.
En la acción destroy
, hiciste algo similar usando el operador de navegación segura de Ruby &.
, que evita errores de nil
al llamar a un método. Esta adición te permite eliminar una receta solo si existe, luego enviar un mensaje como respuesta.
Después de realizar estos cambios en recipes_controller.rb
, guarda y cierra el archivo.
En este paso, creaste un modelo y un controlador para tus recetas. Has escrito toda la lógica necesaria para trabajar con recetas en el backend. En la próxima sección, crearás componentes para ver tus recetas.
Paso 7 — Visualización de Recetas
En esta sección, crearás componentes para visualizar recetas. Crearás dos páginas: una para ver todas las recetas existentes y otra para ver recetas individuales.
Comenzarás creando una página para ver todas las recetas. Antes de crear la página, necesitas recetas con las que trabajar, ya que tu base de datos está actualmente vacía. Rails proporciona una manera de crear datos de semilla para tu aplicación.
Abre el archivo de semilla llamado seeds.rb
para editarlo:
- nano ~/rails_react_recipe/db/seeds.rb
Reemplaza el contenido inicial del archivo de semilla con el siguiente código:
9.times do |i|
Recipe.create(
name: "Recipe #{i + 1}",
ingredients: '227g tub clotted cream, 25g butter, 1 tsp cornflour,100g parmesan, grated nutmeg, 250g fresh fettuccine or tagliatelle, snipped chives or chopped parsley to serve (optional)',
instruction: 'In a medium saucepan, stir the clotted cream, butter, and cornflour over a low-ish heat and bring to a low simmer. Turn off the heat and keep warm.'
)
end
En este código, utilizas un bucle que instruye a Rails a crear nueve recetas con secciones para name
, ingredients
, y instruction
. Guarda y sale del archivo.
Para poblar la base de datos con estos datos, ejecuta el siguiente comando en tu terminal:
- rails db:seed
Al ejecutar este comando, se añaden nueve recetas a tu base de datos. Ahora puedes recuperarlas y mostrarlas en el frontend.
El componente para ver todas las recetas hará una solicitud HTTP a la acción index
en el controlador RecipesController
para obtener una lista de todas las recetas. Estas recetas luego se mostrarán en tarjetas en la página.
Crea un archivo Recipes.jsx
en el directorio app/javascript/components
:
- nano ~/rails_react_recipe/app/javascript/components/Recipes.jsx
Una vez que el archivo esté abierto, importa los módulos React
, useState
, useEffect
, Link
, y useNavigate
agregando las siguientes líneas:
import React, { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
A continuación, agrega las líneas resaltadas para crear y exportar un componente funcional de React llamado Recipes
:
import React, { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
const Recipes = () => {
const navigate = useNavigate();
const [recipes, setRecipes] = useState([]);
};
export default Recipes;
Dentro del componente Recipe
, la API de navegación de React Router llamará al gancho useNavigate. El gancho useState de React inicializará el estado recipes
, que es un array vacío ([]
), y una función setRecipes
para actualizar el estado recipes
.
A continuación, en un gancho useEffect
, realizarás una solicitud HTTP para obtener todas tus recetas. Para hacer esto, agrega las líneas resaltadas:
import React, { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
const Recipes = () => {
const navigate = useNavigate();
const [recipes, setRecipes] = useState([]);
useEffect(() => {
const url = "/api/v1/recipes/index";
fetch(url)
.then((res) => {
if (res.ok) {
return res.json();
}
throw new Error("Network response was not ok.");
})
.then((res) => setRecipes(res))
.catch(() => navigate("/"));
}, []);
};
export default Recipes;
En tu gancho useEffect
, realizas una llamada HTTP para obtener todas las recetas utilizando la API Fetch. Si la respuesta es exitosa, la aplicación guarda el array de recetas en el estado recipes
. Si ocurre un error, redirigirá al usuario a la página de inicio.
Por último, devuelve el marcado para los elementos que se evaluarán y mostrarán en la página del navegador cuando se renderice el componente. En este caso, el componente renderizará una tarjeta de recetas del estado recipes
. Agrega las líneas resaltadas a Recipes.jsx
:
import React, { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
const Recipes = () => {
const navigate = useNavigate();
const [recipes, setRecipes] = useState([]);
useEffect(() => {
const url = "/api/v1/recipes/index";
fetch(url)
.then((res) => {
if (res.ok) {
return res.json();
}
throw new Error("Network response was not ok.");
})
.then((res) => setRecipes(res))
.catch(() => navigate("/"));
}, []);
const allRecipes = recipes.map((recipe, index) => (
<div key={index} className="col-md-6 col-lg-4">
<div className="card mb-4">
<img
src={recipe.image}
className="card-img-top"
alt={`${recipe.name} image`}
/>
<div className="card-body">
<h5 className="card-title">{recipe.name}</h5>
<Link to={`/recipe/${recipe.id}`} className="btn custom-button">
View Recipe
</Link>
</div>
</div>
</div>
));
const noRecipe = (
<div className="vw-100 vh-50 d-flex align-items-center justify-content-center">
<h4>
No recipes yet. Why not <Link to="/new_recipe">create one</Link>
</h4>
</div>
);
return (
<>
<section className="jumbotron jumbotron-fluid text-center">
<div className="container py-5">
<h1 className="display-4">Recipes for every occasion</h1>
<p className="lead text-muted">
We’ve pulled together our most popular recipes, our latest
additions, and our editor’s picks, so there’s sure to be something
tempting for you to try.
</p>
</div>
</section>
<div className="py-5">
<main className="container">
<div className="text-end mb-3">
<Link to="/recipe" className="btn custom-button">
Create New Recipe
</Link>
</div>
<div className="row">
{recipes.length > 0 ? allRecipes : noRecipe}
</div>
<Link to="/" className="btn btn-link">
Home
</Link>
</main>
</div>
</>
);
};
export default Recipes;
Guarda y sal de Recipes.jsx
.
Ahora que has creado un componente para mostrar todas las recetas, crearás una ruta para ello. Abre el archivo de ruta del frontend app/javascript/routes/index.jsx
:
- nano app/javascript/routes/index.jsx
Agrega las líneas resaltadas al archivo:
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Home from "../components/Home";
import Recipes from "../components/Recipes";
export default (
<Router>
<Routes>
<Route path="/" exact component={Home} />
<Route path="/recipes" element={<Recipes />} />
</Routes>
</Router>
);
Guarda y sale del archivo.
En este punto, es una buena idea verificar que tu código esté funcionando como se espera. Como hiciste antes, utiliza el siguiente comando para iniciar tu servidor:
- bin/dev
Luego abre la aplicación en tu navegador. Presiona el botón Ver Receta en la página de inicio para acceder a una página de visualización con tus recetas iniciales:
Utiliza CTRL+C
en tu terminal para detener el servidor y volver al prompt.
Ahora que puedes ver todas las recetas en tu aplicación, es hora de crear un segundo componente para ver recetas individuales. Crea un archivo Recipe.jsx
en el directorio app/javascript/components
:
- nano app/javascript/components/Recipe.jsx
Al igual que con el componente Recipes
, importa los módulos React
, useState
, useEffect
, Link
, useNavigate
y useParam
agregando las siguientes líneas:
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
A continuación, agrega las líneas resaltadas para crear y exportar un componente funcional de React llamado Recipe
:
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
const Recipe = () => {
const params = useParams();
const navigate = useNavigate();
const [recipe, setRecipe] = useState({ ingredients: "" });
};
export default Recipe;
Como el componente Recipes
, inicializas la navegación de React Router con el gancho useNavigate
. Un estado recipe
y una función setRecipe
actualizarán el estado con el gancho useState
. Además, llamas al gancho useParams
, que devuelve un objeto cuyos pares clave/valor son de parámetros de URL.
Para encontrar una receta específica, tu aplicación necesita conocer el id
de la receta, lo que significa que tu componente Recipe
espera un param
id
en la URL. Puedes acceder a esto a través del objeto params
que contiene el valor de retorno del gancho useParams
.
Siguiente, declare un gancho useEffect
donde accederá al id
param
del objeto params
. Una vez que obtenga el id
param de la receta, realizará una solicitud HTTP para recuperarla. Agregue las líneas resaltadas a su archivo:
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
const Recipe = () => {
const params = useParams();
const navigate = useNavigate();
const [recipe, setRecipe] = useState({ ingredients: "" });
useEffect(() => {
const url = `/api/v1/show/${params.id}`;
fetch(url)
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then((response) => setRecipe(response))
.catch(() => navigate("/recipes"));
}, [params.id]);
};
export default Recipe;
En el gancho useEffect
, utiliza el valor params.id
para hacer una solicitud HTTP GET y recuperar la receta que posee el id
y luego guardarla en el estado del componente usando la función setRecipe
. La aplicación redirige al usuario a la página de recetas si la receta no existe.
A continuación, agregue una función addHtmlEntities
, que se utilizará para reemplazar las entidades de caracteres con entidades HTML en el componente. La función addHtmlEntities
tomará una cadena y reemplazará todos los corchetes escapados de apertura y cierre con sus entidades HTML. Esta función le ayudará a convertir cualquier carácter escapado que se haya guardado en las instrucciones de su receta. Agregue las líneas resaltadas:
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
const Recipe = () => {
const params = useParams();
const navigate = useNavigate();
const [recipe, setRecipe] = useState({ ingredients: "" });
useEffect(() => {
const url = `/api/v1/show/${params.id}`;
fetch(url)
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then((response) => setRecipe(response))
.catch(() => navigate("/recipes"));
}, [params.id]);
const addHtmlEntities = (str) => {
return String(str).replace(/</g, "<").replace(/>/g, ">");
};
};
export default Recipe;
Finalmente, devuelva el marcado para representar la receta en el estado del componente en la página, agregando las líneas resaltadas:
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
const Recipe = () => {
const params = useParams();
const navigate = useNavigate();
const [recipe, setRecipe] = useState({ ingredients: "" });
useEffect(() => {
const url = `/api/v1/show/${params.id}`;
fetch(url)
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then((response) => setRecipe(response))
.catch(() => navigate("/recipes"));
}, [params.id]);
const addHtmlEntities = (str) => {
return String(str).replace(/</g, "<").replace(/>/g, ">");
};
const ingredientList = () => {
let ingredientList = "No ingredients available";
if (recipe.ingredients.length > 0) {
ingredientList = recipe.ingredients
.split(",")
.map((ingredient, index) => (
<li key={index} className="list-group-item">
{ingredient}
</li>
));
}
return ingredientList;
};
const recipeInstruction = addHtmlEntities(recipe.instruction);
return (
<div className="">
<div className="hero position-relative d-flex align-items-center justify-content-center">
<img
src={recipe.image}
alt={`${recipe.name} image`}
className="img-fluid position-absolute"
/>
<div className="overlay bg-dark position-absolute" />
<h1 className="display-4 position-relative text-white">
{recipe.name}
</h1>
</div>
<div className="container py-5">
<div className="row">
<div className="col-sm-12 col-lg-3">
<ul className="list-group">
<h5 className="mb-2">Ingredients</h5>
{ingredientList()}
</ul>
</div>
<div className="col-sm-12 col-lg-7">
<h5 className="mb-2">Preparation Instructions</h5>
<div
dangerouslySetInnerHTML={{
__html: `${recipeInstruction}`,
}}
/>
</div>
<div className="col-sm-12 col-lg-2">
<button
type="button"
className="btn btn-danger"
>
Delete Recipe
</button>
</div>
</div>
<Link to="/recipes" className="btn btn-link">
Back to recipes
</Link>
</div>
</div>
);
};
export default Recipe;
Con una función ingredientList
, divides tus ingredientes de receta separados por comas en un array y los mapeas para crear una lista de ingredientes. Si no hay ingredientes, la aplicación muestra un mensaje que dice No hay ingredientes disponibles. También reemplazas todos los corchetes de apertura y cierre en las instrucciones de la receta al pasarlas por la función addHtmlEntities
. Por último, el código muestra la imagen de la receta como una imagen destacada, añade un botón Eliminar Receta junto a las instrucciones de la receta, y agrega un botón que enlaza de vuelta a la página de recetas.
Nota: El uso del atributo dangerouslySetInnerHTML
de React es arriesgado ya que expone tu aplicación a ataques de cross-site scripting. Este riesgo se reduce asegurando que los caracteres especiales ingresados al crear recetas sean reemplazados usando la función stripHtmlEntities
declarada en el componente NewRecipe
.
Guarda y cierra el archivo.
Para ver el componente Recipe
en una página, lo añadirás a tu archivo de rutas. Abre tu archivo de rutas para editarlo:
- nano app/javascript/routes/index.jsx
Añade las siguientes líneas resaltadas al archivo:
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Home from "../components/Home";
import Recipes from "../components/Recipes";
import Recipe from "../components/Recipe";
export default (
<Router>
<Routes>
<Route path="/" exact component={Home} />
<Route path="/recipes" exact component={Recipes} />
<Route path="/recipe/:id" element={<Recipe />} />
</Routes>
</Router>
);
Importas tu componente Recipe
en este archivo de ruta y añades una ruta. Su ruta tiene un param
:id
que será reemplazado por el id
de la receta que deseas ver.
Guarda y cierra el archivo.
Utiliza el script bin/dev
para iniciar tu servidor nuevamente, luego visita http://localhost:3000
en tu navegador. Haz clic en el botón Ver Recetas para navegar a la página de recetas. En la página de recetas, accede a cualquier receta haciendo clic en su botón Ver Receta. Serás recibido con una página poblada con los datos de tu base de datos:
Puedes detener el servidor con CTRL+C
.
En este paso, agregaste nueve recetas a tu base de datos y creaste componentes para ver estas recetas, tanto individualmente como en conjunto. En el próximo paso, agregarás un componente para crear recetas.
Paso 8 — Creando Recetas
El siguiente paso para tener una aplicación de recetas de comida utilizable es la capacidad de crear nuevas recetas. En este paso, crearás un componente para esta función. El componente contendrá un formulario para recopilar los detalles de la receta requeridos por el usuario y luego realizará una solicitud a la acción create
en el controlador Recipe
para guardar los datos de la receta.
Crea un archivo NewRecipe.jsx
en el directorio app/javascript/components
:
- nano app/javascript/components/NewRecipe.jsx
En el nuevo archivo, importa los módulos React
, useState
, Link
y useNavigate
que utilizaste en otros componentes:
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
A continuación, crea y exporta un componente funcional NewRecipe
añadiendo las líneas resaltadas:
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
const NewRecipe = () => {
const navigate = useNavigate();
const [name, setName] = useState("");
const [ingredients, setIngredients] = useState("");
const [instruction, setInstruction] = useState("");
};
export default NewRecipe;
Como con los componentes anteriores, inicializas la navegación del enrutador React con el gancho useNavigate
y luego utilizas el gancho useState
para inicializar un estado de name
, ingredients
e instruction
, cada uno con sus respectivas funciones de actualización. Estos son los campos que necesitarás para crear una receta válida.
A continuación, crea una función stripHtmlEntities
que convertirá los caracteres especiales (como <
) en sus valores escapados/codificados (como <
), respectivamente. Para hacer esto, agrega las líneas resaltadas al componente NewRecipe
:
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
const NewRecipe = () => {
const navigate = useNavigate();
const [name, setName] = useState("");
const [ingredients, setIngredients] = useState("");
const [instruction, setInstruction] = useState("");
const stripHtmlEntities = (str) => {
return String(str)
.replace(/\n/g, "<br> <br>")
.replace(/</g, "<")
.replace(/>/g, ">");
};
};
export default NewRecipe;
En la función stripHtmlEntities
, reemplaza los caracteres <
y >
con sus valores escapados. De esta manera, no almacenarás HTML sin procesar en tu base de datos.
A continuación, agrega las líneas resaltadas para añadir las funciones onChange
y onSubmit
al componente NewRecipe
para manejar la edición y envío del formulario:
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
const NewRecipe = () => {
const navigate = useNavigate();
const [name, setName] = useState("");
const [ingredients, setIngredients] = useState("");
const [instruction, setInstruction] = useState("");
const stripHtmlEntities = (str) => {
return String(str)
.replace(/\n/g, "<br> <br>")
.replace(/</g, "<")
.replace(/>/g, ">");
};
const onChange = (event, setFunction) => {
setFunction(event.target.value);
};
const onSubmit = (event) => {
event.preventDefault();
const url = "/api/v1/recipes/create";
if (name.length == 0 || ingredients.length == 0 || instruction.length == 0)
return;
const body = {
name,
ingredients,
instruction: stripHtmlEntities(instruction),
};
const token = document.querySelector('meta[name="csrf-token"]').content;
fetch(url, {
method: "POST",
headers: {
"X-CSRF-Token": token,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
})
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then((response) => navigate(`/recipe/${response.id}`))
.catch((error) => console.log(error.message));
};
};
export default NewRecipe;
La función onChange
acepta la entrada del usuario evento
y la función de establecimiento de estado, luego actualiza posteriormente el estado con el valor de entrada del usuario. En la función onSubmit
, verificas que ninguno de los campos requeridos esté vacío. Luego construyes un objeto que contiene los parámetros necesarios para crear una nueva receta. Utilizando la función stripHtmlEntities
, reemplazas los caracteres <
y >
en las instrucciones de la receta con su valor escapado y reemplazas cada salto de línea con una etiqueta de salto de línea, conservando así el formato de texto ingresado por el usuario. Por último, realizas una solicitud HTTP POST para crear la nueva receta y redirigir a su página en caso de una respuesta exitosa.
Para protegerse contra los ataques de Falsificación de Petición en Sitio Cruzado (CSRF), Rails adjunta un token de seguridad CSRF al documento HTML. Este token es necesario cada vez que se realiza una solicitud que no sea GET
. Con la constante token
en el código anterior, tu aplicación verifica el token en el servidor y genera una excepción si el token de seguridad no coincide con lo esperado. En la función onSubmit
, la aplicación recupera el token CSRF incrustado en tu documento HTML por Rails y luego realiza una solicitud HTTP con una cadena JSON. Si la receta se crea correctamente, la aplicación redirige al usuario a la página de la receta donde pueden ver su receta recién creada.
Por último, devuelve el marcado que renderiza un formulario para que el usuario ingrese los detalles de la receta que desea crear. Agrega las líneas destacadas:
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
const NewRecipe = () => {
const navigate = useNavigate();
const [name, setName] = useState("");
const [ingredients, setIngredients] = useState("");
const [instruction, setInstruction] = useState("");
const stripHtmlEntities = (str) => {
return String(str)
.replace(/\n/g, "<br> <br>")
.replace(/</g, "<")
.replace(/>/g, ">");
};
const onChange = (event, setFunction) => {
setFunction(event.target.value);
};
const onSubmit = (event) => {
event.preventDefault();
const url = "/api/v1/recipes/create";
if (name.length == 0 || ingredients.length == 0 || instruction.length == 0)
return;
const body = {
name,
ingredients,
instruction: stripHtmlEntities(instruction),
};
const token = document.querySelector('meta[name="csrf-token"]').content;
fetch(url, {
method: "POST",
headers: {
"X-CSRF-Token": token,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
})
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then((response) => navigate(`/recipe/${response.id}`))
.catch((error) => console.log(error.message));
};
return (
<div className="container mt-5">
<div className="row">
<div className="col-sm-12 col-lg-6 offset-lg-3">
<h1 className="font-weight-normal mb-5">
Add a new recipe to our awesome recipe collection.
</h1>
<form onSubmit={onSubmit}>
<div className="form-group">
<label htmlFor="recipeName">Recipe name</label>
<input
type="text"
name="name"
id="recipeName"
className="form-control"
required
onChange={(event) => onChange(event, setName)}
/>
</div>
<div className="form-group">
<label htmlFor="recipeIngredients">Ingredients</label>
<input
type="text"
name="ingredients"
id="recipeIngredients"
className="form-control"
required
onChange={(event) => onChange(event, setIngredients)}
/>
<small id="ingredientsHelp" className="form-text text-muted">
Separate each ingredient with a comma.
</small>
</div>
<label htmlFor="instruction">Preparation Instructions</label>
<textarea
className="form-control"
id="instruction"
name="instruction"
rows="5"
required
onChange={(event) => onChange(event, setInstruction)}
/>
<button type="submit" className="btn custom-button mt-3">
Create Recipe
</button>
<Link to="/recipes" className="btn btn-link mt-3">
Back to recipes
</Link>
</form>
</div>
</div>
</div>
);
};
export default NewRecipe;
El marcado devuelto incluye un formulario que contiene tres campos de entrada; uno para el nombre de la receta, otro para los ingredientes de la receta y otro para las instrucciones. Cada campo de entrada tiene un controlador de eventos `onChange` que llama a la función `onChange`. También se adjunta un controlador de eventos `onSubmit` al botón de enviar que llama a la función `onSubmit` que envía los datos del formulario.
Guarda y sale del archivo.
Para acceder a este componente en el navegador, actualiza tu archivo de ruta con su ruta:
- nano app/javascript/routes/index.jsx
Actualiza tu archivo de ruta para incluir estas líneas destacadas:
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Home from "../components/Home";
import Recipes from "../components/Recipes";
import Recipe from "../components/Recipe";
import NewRecipe from "../components/NewRecipe";
export default (
<Router>
<Routes>
<Route path="/" exact component={Home} />
<Route path="/recipes" exact component={Recipes} />
<Route path="/recipe/:id" exact component={Recipe} />
<Route path="/recipe" element={<NewRecipe />} />
</Routes>
</Router>
);
Con la ruta en su lugar, guarda y sale del archivo.
Reinicia tu servidor de desarrollo y visita http://localhost:3000
en tu navegador. Navega hasta la página de recetas y haz clic en el botón Crear Nueva Receta. Encontrarás una página con un formulario para agregar recetas a tu base de datos:
Ingresa los detalles de la receta requeridos y haz clic en el botón Crear Receta. La receta recién creada aparecerá en la página. Cuando estés listo, cierra el servidor.
En este paso, agregaste la capacidad de crear recetas a tu aplicación de recetas de comida. En el siguiente paso, agregarás la funcionalidad para eliminar recetas.
Paso 9 — Eliminación de Recetas
En esta sección, modificarás tu componente de Receta para incluir una opción para eliminar recetas. Cuando hagas clic en el botón de eliminar en la página de la receta, la aplicación enviará una solicitud para eliminar una receta de la base de datos.
Primero, abre tu archivo Recipe.jsx
para editarlo:
- nano app/javascript/components/Recipe.jsx
En el componente Recipe
, agrega una función deleteRecipe
con las líneas resaltadas:
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
const Recipe = () => {
const params = useParams();
const navigate = useNavigate();
const [recipe, setRecipe] = useState({ ingredients: "" });
useEffect(() => {
const url = `/api/v1/show/${params.id}`;
fetch(url)
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then((response) => setRecipe(response))
.catch(() => navigate("/recipes"));
}, [params.id]);
const addHtmlEntities = (str) => {
return String(str).replace(/</g, "<").replace(/>/g, ">");
};
const deleteRecipe = () => {
const url = `/api/v1/destroy/${params.id}`;
const token = document.querySelector('meta[name="csrf-token"]').content;
fetch(url, {
method: "DELETE",
headers: {
"X-CSRF-Token": token,
"Content-Type": "application/json",
},
})
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then(() => navigate("/recipes"))
.catch((error) => console.log(error.message));
};
const ingredientList = () => {
let ingredientList = "No ingredients available";
if (recipe.ingredients.length > 0) {
ingredientList = recipe.ingredients
.split(",")
.map((ingredient, index) => (
<li key={index} className="list-group-item">
{ingredient}
</li>
));
}
return ingredientList;
};
const recipeInstruction = addHtmlEntities(recipe.instruction);
return (
<div className="">
...
En la función deleteRecipe
, obtienes el id
de la receta a eliminar, luego construyes tu URL y obtienes el token CSRF. Después, realizas una solicitud DELETE
al controlador Recipes
para eliminar la receta. La aplicación redirige al usuario a la página de recetas si la receta se elimina correctamente.
Para ejecutar el código en la función deleteRecipe
cada vez que se hace clic en el botón de eliminar, pásalo como el manejador de eventos de clic al botón. Agrega un evento onClick
al elemento del botón de eliminar en el componente:
...
return (
<div className="">
<div className="hero position-relative d-flex align-items-center justify-content-center">
<img
src={recipe.image}
alt={`${recipe.name} image`}
className="img-fluid position-absolute"
/>
<div className="overlay bg-dark position-absolute" />
<h1 className="display-4 position-relative text-white">
{recipe.name}
</h1>
</div>
<div className="container py-5">
<div className="row">
<div className="col-sm-12 col-lg-3">
<ul className="list-group">
<h5 className="mb-2">Ingredients</h5>
{ingredientList()}
</ul>
</div>
<div className="col-sm-12 col-lg-7">
<h5 className="mb-2">Preparation Instructions</h5>
<div
dangerouslySetInnerHTML={{
__html: `${recipeInstruction}`,
}}
/>
</div>
<div className="col-sm-12 col-lg-2">
<button
type="button"
className="btn btn-danger"
onClick={deleteRecipe}
>
Delete Recipe
</button>
</div>
</div>
<Link to="/recipes" className="btn btn-link">
Back to recipes
</Link>
</div>
</div>
);
...
En este punto del tutorial, tu archivo Recipe.jsx
completo debería coincidir con este archivo:
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
const Recipe = () => {
const params = useParams();
const navigate = useNavigate();
const [recipe, setRecipe] = useState({ ingredients: "" });
useEffect(() => {
const url = `/api/v1/show/${params.id}`;
fetch(url)
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then((response) => setRecipe(response))
.catch(() => navigate("/recipes"));
}, [params.id]);
const addHtmlEntities = (str) => {
return String(str).replace(/</g, "<").replace(/>/g, ">");
};
const deleteRecipe = () => {
const url = `/api/v1/destroy/${params.id}`;
const token = document.querySelector('meta[name="csrf-token"]').content;
fetch(url, {
method: "DELETE",
headers: {
"X-CSRF-Token": token,
"Content-Type": "application/json",
},
})
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then(() => navigate("/recipes"))
.catch((error) => console.log(error.message));
};
const ingredientList = () => {
let ingredientList = "No ingredients available";
if (recipe.ingredients.length > 0) {
ingredientList = recipe.ingredients
.split(",")
.map((ingredient, index) => (
<li key={index} className="list-group-item">
{ingredient}
</li>
));
}
return ingredientList;
};
const recipeInstruction = addHtmlEntities(recipe.instruction);
return (
<div className="">
<div className="hero position-relative d-flex align-items-center justify-content-center">
<img
src={recipe.image}
alt={`${recipe.name} image`}
className="img-fluid position-absolute"
/>
<div className="overlay bg-dark position-absolute" />
<h1 className="display-4 position-relative text-white">
{recipe.name}
</h1>
</div>
<div className="container py-5">
<div className="row">
<div className="col-sm-12 col-lg-3">
<ul className="list-group">
<h5 className="mb-2">Ingredients</h5>
{ingredientList()}
</ul>
</div>
<div className="col-sm-12 col-lg-7">
<h5 className="mb-2">Preparation Instructions</h5>
<div
dangerouslySetInnerHTML={{
__html: `${recipeInstruction}`,
}}
/>
</div>
<div className="col-sm-12 col-lg-2">
<button
type="button"
className="btn btn-danger"
onClick={deleteRecipe}
>
Delete Recipe
</button>
</div>
</div>
<Link to="/recipes" className="btn btn-link">
Back to recipes
</Link>
</div>
</div>
);
};
export default Recipe;
Guarda y cierra el archivo.
Reinicia el servidor de la aplicación y navega hasta la página de inicio. Haz clic en el botón Ver Recetas para acceder a todas las recetas existentes, luego abre cualquier receta en particular y haz clic en el botón Eliminar Receta en la página para eliminar el artículo. Serás redirigido a la página de recetas y la receta eliminada ya no existirá.
¡Con el botón de eliminar funcionando, ahora tienes una aplicación de recetas completamente funcional!
Conclusión
En este tutorial, creaste una aplicación de recetas de comida con Ruby on Rails y un frontend de React, utilizando PostgreSQL como tu base de datos y Bootstrap para el estilo. Si deseas seguir construyendo con Ruby on Rails, considera seguir nuestro tutorial Cómo asegurar comunicaciones en una aplicación Rails de tres capas utilizando túneles SSH o visita nuestra serie Cómo programar en Ruby para refrescar tus habilidades en Ruby. Para profundizar en React, prueba Cómo mostrar datos de la API de DigitalOcean con React.