L’autore ha selezionato l’Electronic Frontier Foundation per ricevere una donazione come parte del programma Write for DOnations.
Introduzione
Ruby on Rails è un popolare framework per applicazioni web lato server. Alimenta molte applicazioni famose presenti oggi sul web, come GitHub, Basecamp, SoundCloud, Airbnb e Twitch. Con il suo focus sull’esperienza del programmatore e sulla comunità appassionata che lo circonda, Ruby on Rails ti fornirà gli strumenti necessari per costruire e mantenere la tua moderna applicazione web.
React è una libreria JavaScript utilizzata per creare interfacce utente front-end. Supportata da Facebook, è una delle librerie front-end più popolari utilizzate oggi sul web. React offre funzionalità come un Modello Oggetto Documento virtuale (DOM), architettura a componenti e gestione dello stato, che rendono il processo di sviluppo front-end più organizzato ed efficiente.
Con il frontend del web che si sta spostando verso framework separati dal codice lato server, combinare l’eleganza di Rails con l’efficienza di React ti permetterà di costruire applicazioni potenti e moderne informate dalle tendenze attuali. Utilizzando React per rendere i componenti all’interno di una vista di Rails (anziché il motore di template di Rails), la tua applicazione beneficerà degli ultimi sviluppi in JavaScript e nello sviluppo frontend, sfruttando al contempo l’espressività di Ruby on Rails.
In questo tutorial, creerai un’applicazione Ruby on Rails che memorizza le tue ricette preferite e le visualizza con un frontend React. Quando avrai finito, sarai in grado di creare, visualizzare ed eliminare ricette utilizzando un’interfaccia React stilizzata con Bootstrap:
Prerequisiti
Per seguire questo tutorial, hai bisogno di:
-
Node.js e npm installati sul tuo computer di sviluppo. Questo tutorial utilizza Node.js versione 16.14.0 e npm versione 8.3.1. Node.js è un ambiente di esecuzione JavaScript che ti consente di eseguire il tuo codice al di fuori del browser. Viene fornito con un Gestore di Pacchetti pre-installato chiamato npm, che ti consente di installare e aggiornare pacchetti. Per installarli su Ubuntu 20.04 o macOS, segui la sezione “Installazione tramite un PPA” di Come installare Node.js su Ubuntu 20.04 o i passaggi in Come installare Node.js e creare un ambiente di sviluppo locale su macOS.
-
Il package manager Yarn installato sulla tua macchina di sviluppo, che ti permetterà di scaricare il framework React. Questo tutorial è stato testato sulla versione 1.22.10; per installare questa dipendenza, segui la guida ufficiale di installazione di Yarn.
-
Ruby on Rails installato. Per ottenere questo, segui la nostra guida su Come installare Ruby on Rails con rbenv su Ubuntu 20.04. Se desideri sviluppare questa applicazione su macOS, puoi utilizzare Come installare Ruby on Rails con rbenv su macOS. Questo tutorial è stato testato sulla versione 3.1.2 di Ruby e sulla versione 7.0.4 di Rails, quindi assicurati di specificare queste versioni durante il processo di installazione.
Nota: La versione 7 di Rails non è retrocompatibile. Se stai utilizzando la versione 5 di Rails, visita il tutorial su Come configurare un progetto Ruby on Rails v5 con un frontend React su Ubuntu 18.04.
- PostgreSQL installato, come descritto nei passaggi 1 e 2 Come utilizzare PostgreSQL con la tua applicazione Ruby on Rails su Ubuntu 20.04 o Come utilizzare PostgreSQL con la tua applicazione Ruby on Rails su macOS. Per seguire questo tutorial, puoi utilizzare PostgreSQL versione 12 o superiore. Se desideri sviluppare questa applicazione su una distribuzione diversa di Linux o un altro sistema operativo, consulta la pagina dei download ufficiali di PostgreSQL. Per ulteriori informazioni su come utilizzare PostgreSQL, consulta Come installare e utilizzare PostgreSQL.
Passaggio 1 — Creazione di una nuova applicazione Rails
Costruirai la tua applicazione di ricette sul framework dell’applicazione Rails in questo passaggio. Innanzitutto, creerai una nuova applicazione Rails, che sarà configurata per funzionare con React.
Rails fornisce diversi script chiamati generatori che creano tutto il necessario per costruire un’applicazione web moderna. Per visualizzare un elenco completo di questi comandi e cosa fanno, esegui il seguente comando nel tuo terminale:
- rails -h
Questo comando restituirà un elenco completo di opzioni, consentendoti di impostare i parametri dell’applicazione. Uno dei comandi elencati è il comando new
, che crea una nuova applicazione Rails.
Ora, creerai una nuova applicazione Rails utilizzando il generatore new
. Esegui il seguente comando nel tuo terminale:
- rails new rails_react_recipe -d postgresql -j esbuild -c bootstrap -T
Il comando precedente crea una nuova applicazione Rails in una directory chiamata rails_react_recipe
, installa le dipendenze Ruby e JavaScript necessarie e configura Webpack. Le bandiere associate a questo comando del generatore new
includono le seguenti:
- La bandiera
-d
specifica il motore di database preferito, che in questo caso è PostgreSQL. - La bandiera
-j
specifica l’approccio JavaScript dell’applicazione. Rails offre alcuni modi diversi per gestire il codice JavaScript nelle applicazioni Rails. L’opzioneesbuild
passata alla bandiera-j
istruisce Rails a preconfigurare esbuild come il bundler JavaScript preferito. - La bandiera
-c
specifica il processore CSS dell’applicazione. Bootstrap è l’opzione preferita in questo caso. - La bandiera
-T
istruisce Rails a saltare la generazione dei file di test poiché non scriverai test per questo tutorial. Questo comando è anche suggerito se desideri utilizzare uno strumento di test Ruby diverso da quello fornito da Rails.
Una volta completato il comando, passa alla directory rails_react_recipe
, che è la directory principale della tua app:
- cd rails_react_recipe
Successivamente, elenca i contenuti della directory:
- ls
Il contenuto verrà stampato in modo simile a questo:
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
Questa directory principale contiene diversi file e cartelle generate automaticamente che costituiscono la struttura di un’applicazione Rails, inclusi un file package.json
contenente le dipendenze per un’applicazione React.
Ora che hai creato con successo una nuova applicazione Rails, la collegherai a un database nel prossimo passaggio.
Passaggio 2 — Configurazione del Database
Prima di eseguire la tua nuova applicazione Rails, devi prima collegarla a un database. In questo passaggio, collegherai la nuova applicazione Rails appena creata a un database PostgreSQL in modo che i dati della ricetta possano essere memorizzati e recuperati come necessario.
Il file database.yml
trovato in config/database.yml
contiene dettagli del database come i nomi del database per diversi ambienti di sviluppo. Rails specifica un nome del database per i vari ambienti di sviluppo aggiungendo un trattino basso (_
) seguito dal nome dell’ambiente. In questo tutorial, utilizzerai i valori di configurazione predefiniti del database, ma puoi cambiare i valori di configurazione se necessario.
Nota: A questo punto, puoi modificare config/database.yml
per impostare quale ruolo PostgreSQL desideri che Rails utilizzi per creare il tuo database. Durante i prerequisiti, hai creato un ruolo protetto da una password nel tutorial Come utilizzare PostgreSQL con la tua applicazione Ruby on Rails. Se non hai ancora impostato l’utente, puoi seguire le istruzioni per Passo 4 – Configurare e Creare il Tuo Database nello stesso tutorial prerequisito.
Rails offre molti comandi che semplificano lo sviluppo di applicazioni web, tra cui comandi per lavorare con database come create
, drop
e reset
. Per creare un database per la tua applicazione, esegui il seguente comando nel terminale:
- rails db:create
Questo comando crea un database development
e test
, restituendo il seguente output:
OutputCreated database 'rails_react_recipe_development'
Created database 'rails_react_recipe_test'
Ora che l’applicazione è connessa a un database, avvia l’applicazione eseguendo il seguente comando:
- bin/dev
Rails fornisce uno script alternativo bin/dev
che avvia un’applicazione Rails eseguendo i comandi nel file Procfile.dev
nella directory principale dell’app utilizzando la gemma Foreman.
Una volta eseguito questo comando, il prompt dei comandi scomparirà e al suo posto verrà stampato il seguente output:
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.
Per accedere alla tua applicazione, apri una finestra del browser e vai a http://localhost:3000
. La pagina di benvenuto predefinita di Rails verrà caricata, il che significa che hai configurato correttamente la tua applicazione Rails:
Per arrestare il server web, premi CTRL+C
nel terminale dove il server è in esecuzione. Riceverai un messaggio di addio da 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
Il prompt del tuo terminale riapparirà quindi.
Hai configurato con successo un database per la tua applicazione di ricette alimentari. Nel prossimo passaggio, installerai le dipendenze JavaScript necessarie per mettere insieme il tuo frontend React.
Passaggio 3 — Installazione delle dipendenze del frontend
In questo passaggio, installerai le dipendenze JavaScript necessarie sul frontend della tua applicazione di ricette alimentari. Queste includono:
- React per la creazione di interfacce utente.
- React DOM per consentire a React di interagire con il DOM del browser.
- React Router per gestire la navigazione in un’applicazione React.
Esegui il seguente comando per installare questi pacchetti con il gestore di pacchetti Yarn:
- yarn add react react-dom react-router-dom
Questo comando utilizza Yarn per installare i pacchetti specificati e li aggiunge al file package.json
. Per verificarlo, apri il file package.json
situato nella directory principale del progetto:
- nano package.json
I pacchetti installati verranno elencati sotto la chiave 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"
}
}
Chiudi il file premendo CTRL+X
.
Hai installato alcune dipendenze front-end per la tua applicazione. Successivamente, configurerai una homepage per la tua applicazione di ricette alimentari.
Passaggio 4 – Configurazione della homepage
Con le dipendenze richieste installate, creerai ora una homepage per l’applicazione che servirà come pagina iniziale quando gli utenti visitano l’applicazione per la prima volta.
Rails segue il pattern architetturale Model-View-Controller per le applicazioni. Nel pattern MVC, lo scopo di un controller è quello di ricevere richieste specifiche e inoltrarle al modello o alla vista appropriati. Attualmente, l’applicazione mostra la pagina di benvenuto di Rails quando viene caricato l’URL radice nel browser. Per cambiarlo, creerai un controller e una vista per la homepage e quindi lo abbinerai a un percorso.
Rails fornisce un generatore di controller
per creare un controller. Il generatore di controller
riceve un nome del controller e un’azione corrispondente. Per ulteriori informazioni, puoi consultare la documentazione di Rails.
Questo tutorial chiamerà il controller Homepage
. Esegui il seguente comando per creare un controller Homepage
con un’azione index
:
- rails g controller Homepage index
Nota: Su Linux, l’errore FATAL: Listen error: unable to monitor directories for changes.
potrebbe essere causato da un limite di sistema sul numero di file che la tua macchina può monitorare per le modifiche. Esegui il seguente comando per risolverlo:
- echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
Questo comando aumenterà permanentemente il numero di directory che puoi monitorare con Listen
a 524288
. Puoi cambiarlo nuovamente eseguendo lo stesso comando e sostituendo 524288
con il numero desiderato.
Eseguendo il comando controller
vengono generati i seguenti file:
- 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 file
index.html.erb
come pagina di visualizzazione per renderizzare tutto ciò che è relativo alla homepage.
Oltre a queste nuove pagine create eseguendo il comando Rails, Rails aggiorna anche il file delle route situato in config/routes.rb
, aggiungendo una route get
per la tua homepage, che modificherai come route principale.
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
In questo file, sostituisci get 'homepage/index'
con root 'homepage#index'
in modo che il file corrisponda al seguente:
Rails.application.routes.draw do
root 'homepage#index'
# Per i dettagli sul DSL disponibile in questo file, consulta http://guides.rubyonrails.org/routing.html
end
Questa modifica istruisce Rails a mappare le richieste alla radice dell’applicazione all’azione index
del controller Homepage
, che a sua volta rende nel browser ciò che si trova nel file index.html.erb
situato in app/views/homepage/index.html.erb
.
Salva e chiudi il file.
Per verificare che tutto funzioni, avvia l’applicazione:
- bin/dev
Quando apri o aggiorni l’applicazione nel browser, si caricherà una nuova pagina iniziale per la tua applicazione:
Una volta verificato che l’applicazione funziona, premi CTRL+C
per fermare il server.
Successivamente, apri il file ~/rails_react_recipe/app/views/homepage/index.html.erb
:
- nano ~/rails_react_recipe/app/views/homepage/index.html.erb
Rimuovi il codice all’interno del file e salva il file come vuoto. In questo modo, garantisci che il contenuto di index.html.erb
non interferisca con il rendering di React per il tuo frontend.
Ora che hai configurato la tua homepage per l’applicazione, puoi passare alla sezione successiva, dove configurerai il frontend della tua applicazione per utilizzare React.
Passaggio 5 — Configurare React come Frontend di Rails
In questo passaggio, configurerai Rails per utilizzare React sulla frontend dell’applicazione, invece del suo motore di template. Questa nuova configurazione ti permetterà di creare una homepage più accattivante visualmente con React.
Con l’aiuto dell’opzione esbuild
specificata durante la generazione dell’applicazione Rails, gran parte della configurazione necessaria per consentire a JavaScript di funzionare in modo fluido con Rails è già in atto. Tutto ciò che resta è caricare il punto di ingresso dell’app React nel punto di ingresso di esbuild
per i file JavaScript. Per fare ciò, inizia creando una directory dei componenti nella directory app/javascript
:
- mkdir ~/rails_react_recipe/app/javascript/components
La directory components
conterrà il componente per la homepage, insieme ad altri componenti React nell’applicazione, inclusi il file di ingresso nell’applicazione React.
Successivamente, apri il file application.js
situato in app/javascript/application.js
:
- nano ~/rails_react_recipe/app/javascript/application.js
Aggiungi la linea di codice evidenziata al file:
// Punto di ingresso per lo script di compilazione nel tuo package.json
import "@hotwired/turbo-rails"
import "./controllers"
import * as bootstrap from "bootstrap"
import "./components"
La linea di codice aggiunta al file application.js
importerà il codice nel file di ingresso index.jsx
, rendendolo disponibile per esbuild
per l’incapsulamento. Con la directory /components
importata nel punto di ingresso JavaScript dell’app Rails, puoi creare un componente React per la tua homepage. La homepage conterrà alcuni testi e un pulsante di call to action per visualizzare tutte le ricette.
Salva e chiudi il file.
Successivamente, crea un file Home.jsx
nella directory components
:
- nano ~/rails_react_recipe/app/javascript/components/Home.jsx
Aggiungi il seguente codice al file:
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>
);
In questo codice, importi React e il componente Link
da React Router. Il componente Link
crea un collegamento ipertestuale per navigare da una pagina all’altra. Successivamente, crei ed esporti un componente funzionale che contiene del markup per la tua homepage, stilizzato con classi Bootstrap.
Salva e chiudi il file.
Con il tuo componente Home
impostato, configurerai ora il routing utilizzando React Router. Crea una directory routes
nella directory app/javascript
:
- mkdir ~/rails_react_recipe/app/javascript/routes
La directory routes
conterrà alcune route con i rispettivi componenti. Ogni volta che viene caricata una route specificata, renderà il suo componente corrispondente nel browser.
Nella directory routes
, crea un file index.jsx
:
- nano ~/rails_react_recipe/app/javascript/routes/index.jsx
Aggiungi il seguente codice:
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>
);
In questo file di route index.jsx
, importi i seguenti moduli: il modulo React
che ti consente di utilizzare React, nonché i moduli BrowserRouter
, Routes
e Route
da React Router, che insieme ti aiutano a navigare da una route all’altra. Infine, importi il tuo componente Home
, che verrà renderizzato ogni volta che una richiesta corrisponde alla route radice (/
). Quando desideri aggiungere più pagine alla tua applicazione, puoi dichiarare una route in questo file e associarla al componente che desideri renderizzare per quella pagina.
Salva ed esci dal file.
Hai ora configurato il routing utilizzando React Router. Perché React sia consapevole delle route disponibili e le utilizzi, le route devono essere disponibili al punto di ingresso dell’applicazione. Per ottenere questo, renderizzerai le tue route in un componente che React renderizzerà nel tuo file di ingresso.
Crea un file App.jsx
nella directory app/javascript/components
:
- nano ~/rails_react_recipe/app/javascript/components/App.jsx
Aggiungi il seguente codice nel file App.jsx
:
import React from "react";
import Routes from "../routes";
export default props => <>{Routes}</>;
Nel file App.jsx
, importi React e i file di route appena creati. Quindi esporti un componente per renderizzare le route all’interno di fragment. Questo componente verrà renderizzato al punto di ingresso dell’applicazione, rendendo le route disponibili ogni volta che l’applicazione viene caricata.
Salva e chiudi il file.
Ora che hai configurato il tuo App.jsx
, puoi renderizzarlo nel tuo file di ingresso. Crea un file index.jsx
nella directory components
:
- nano ~/rails_react_recipe/app/javascript/components/index.jsx
Aggiungi il seguente codice nel file 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 />);
});
Nelle righe di import
, importi la libreria React, la funzione createRoot
da ReactDOM e il tuo componente App
. Utilizzando la funzione createRoot
di ReactDOM, crei un elemento radice come elemento div
aggiunto alla pagina, e renderizzi il tuo componente App
al suo interno. Quando l’applicazione viene caricata, React renderizzerà il contenuto del componente App
all’interno dell’elemento div
sulla pagina.
Salva ed esci dal file.
Infine, aggiungerai alcuni stili CSS alla tua homepage.
Apri il file application.bootstrap.scss
nella directory ~/rails_react_recipe/app/assets/stylesheets/application.bootstrap.scss
:
- nano ~/rails_react_recipe/app/assets/stylesheets/application.bootstrap.scss
Successivamente, sostituisci il contenuto del file application.bootstrap.scss
con il seguente codice:
@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;
}
Hai impostato alcuni colori personalizzati per la pagina. La sezione .hero
creerà la struttura per un’immagine hero, o un grande banner web sulla prima pagina del tuo sito web, che aggiungerai successivamente. Inoltre, lo stile custom-button.btn
definisce lo stile del pulsante che l’utente utilizzerà per accedere all’applicazione.
Con i tuoi stili CSS in posizione, salva ed esci dal file.
Successivamente, riavvia il server web per la tua applicazione:
- bin/dev
Quindi ricarica l’applicazione nel tuo browser. Una nuova homepage verrà caricata:
Interrompi il web server con CTRL+C
.
Hai configurato la tua applicazione per utilizzare React come frontend in questo passaggio. Nel prossimo passo, creerai modelli e controller che ti consentiranno di creare, leggere, aggiornare ed eliminare ricette.
Passo 6 — Creazione del Controller e Modello della Ricetta
Ora che hai configurato un frontend React per la tua applicazione, creerai un modello e un controller di ricetta. Il modello della ricetta rappresenterà la tabella del database contenente informazioni sulle ricette dell’utente, mentre il controller riceverà e gestirà le richieste per creare, leggere, aggiornare o eliminare ricette. Quando un utente richiede una ricetta, il controller della ricetta riceve questa richiesta e la passa al modello della ricetta, che recupera i dati richiesti dal database. Il modello restituisce quindi i dati della ricetta come risposta al controller. Infine, queste informazioni vengono visualizzate nel browser.
Inizia creando un modello Recipe
utilizzando il sottocomando generate model
fornito da Rails e specificando il nome del modello insieme alle sue colonne e tipi di dati. Esegui il seguente comando:
- rails generate model Recipe name:string ingredients:text instruction:text image:string
Il comando precedente istruisce Rails a creare un modello Recipe
insieme a una colonna name
di tipo string
, una colonna ingredients
e instruction
di tipo text
, e una colonna image
di tipo string
. Questo tutorial ha chiamato il modello Recipe
, perché i modelli in Rails usano un nome singolare mentre le relative tabelle del database usano un nome plurale.
Eseguendo il comando generate model
vengono creati due file e viene stampato il seguente output:
Output invoke active_record
create db/migrate/20221017220817_create_recipes.rb
create app/models/recipe.rb
I due file creati sono:
- 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.
Successivamente, modificherai il file del modello della ricetta per garantire che solo dati validi vengano salvati nel database. Puoi ottenere questo aggiungendo alcune validazioni del database al tuo modello.
Apri il tuo modello della ricetta situato in app/models/recipe.rb
:
- nano ~/rails_react_recipe/app/models/recipe.rb
Aggiungi le seguenti linee di codice al file:
class Recipe < ApplicationRecord
validates :name, presence: true
validates :ingredients, presence: true
validates :instruction, presence: true
end
Nel codice, aggiungi la validazione del modello, che controlla la presenza dei campi name
, ingredients
e instruction
. Senza questi tre campi, una ricetta non è valida e non verrà salvata nel database.
Salva e chiudi il file.
Per fare in modo che Rails crei la tabella recipes
nel tuo database, devi eseguire una migrazione, che è un modo per apportare modifiche al tuo database in modo programmato. Per assicurarti che la migrazione funzioni con il database che hai impostato, devi apportare modifiche al file 20221017220817_create_recipes.rb
.
Apri questo file nel tuo editor:
- nano ~/rails_react_recipe/db/migrate/20221017220817_create_recipes.rb
Aggiungi i materiali evidenziati in modo che il tuo file corrisponda al seguente:
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
Questo file di migrazione contiene una classe Ruby con un metodo change
e un comando per creare una tabella chiamata recipes
insieme alle colonne e ai loro tipi di dati. Aggiorni anche 20221017220817_create_recipes.rb
con un vincolo NOT NULL
sulle colonne name
, ingredients
e instruction
aggiungendo null: false
, garantendo che queste colonne abbiano un valore prima di modificare il database. Infine, aggiungi un URL dell’immagine predefinito per la tua colonna di immagine; questo potrebbe essere un altro URL se desideri utilizzare un’immagine diversa.
Con queste modifiche, salva ed esci dal file. Ora sei pronto per eseguire la migrazione e creare la tua tabella. Nel tuo terminale, esegui il seguente comando:
- rails db:migrate
Puoi utilizzare il comando di migrazione del database per eseguire le istruzioni nel tuo file di migrazione. Una volta che il comando viene eseguito con successo, riceverai un output simile al seguente:
Output== 20190407161357 CreateRecipes: migrating ====================================
-- create_table(:recipes)
-> 0.0140s
== 20190407161357 CreateRecipes: migrated (0.0141s) ===========================
Con il tuo modello di ricetta in posizione, creerai successivamente il controller delle ricette per aggiungere la logica per la creazione, la lettura e l’eliminazione delle ricette. Esegui il seguente comando:
- rails generate controller api/v1/Recipes index create show destroy --skip-template-engine --no-helper
Con questo comando, crei un controller Recipes
in una directory api/v1
con azioni index
, create
, show
, e destroy
. L’azione index
gestirà il recupero di tutte le tue ricette; l’azione create
sarà responsabile della creazione di nuove ricette; l’azione show
recupererà una singola ricetta, e l’azione destroy
conterrà la logica per eliminare una ricetta.
Passi anche alcuni flag per rendere il controller più leggero, tra cui:
--skip-template-engine
, che istruisce Rails a saltare la generazione dei file di visualizzazione di Rails poiché React gestisce le tue esigenze lato front-end.--no-helper
, che istruisce Rails a saltare la generazione di un file di helper per il tuo controller.
L’esecuzione del comando aggiorna anche il file delle tue route con una route per ciascuna azione nel controller Recipes
.
Quando il comando viene eseguito, stamperà un output come questo:
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
Per utilizzare queste route, apporta modifiche al tuo file config/routes.rb
. Apri il file routes.rb
nel tuo editor di testo:
- nano ~/rails_react_recipe/config/routes.rb
Aggiorna questo file per assomigliare al codice seguente, modificando o aggiungendo le linee evidenziate:
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'
# Definisci i percorsi delle tue applicazioni secondo il DSL in https://guides.rubyonrails.org/routing.html
# Definisce il percorso radice ("/")
# root "articles#index"
end
In questo file di routing, modifichi il verbo HTTP delle route create
e destroy
in modo che possano post
e delete
i dati. Modifichi anche le route per le azioni show
e destroy
aggiungendo un parametro :id
alla route. :id
conterrà il numero di identificazione della ricetta che desideri leggere o eliminare.
Aggiungi una route catch-all con get '/*path'
che indirizzerà qualsiasi altra richiesta che non corrisponde alle route esistenti all’azione index
del controller homepage
. Il routing lato client si occuperà delle richieste non correlate alla creazione, lettura o eliminazione delle ricette.
Salva ed esci dal file.
Per valutare un elenco di percorsi disponibili nella tua applicazione, esegui il seguente comando:
- rails routes
Eseguendo questo comando verrà visualizzato un lungo elenco di schemi URI, verbi e controller o azioni corrispondenti per il tuo progetto.
Successivamente, aggiungerai la logica per ottenere tutte le ricette in una volta sola. Rails utilizza la libreria ActiveRecord per gestire compiti correlati al database come questo. ActiveRecord collega le classi alle tabelle del database relazionale e fornisce una ricca API per lavorare con esse.
Per ottenere tutte le ricette, utilizzerai ActiveRecord per interrogare la tabella delle ricette e recuperare tutte le ricette nel database.
Apri il file recipes_controller.rb
con il seguente comando:
- nano ~/rails_react_recipe/app/controllers/api/v1/recipes_controller.rb
Aggiungi le linee evidenziate al controller delle ricette:
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
Nella tua azione index
, utilizzi il metodo all
di ActiveRecord per ottenere tutte le ricette nel tuo database. Utilizzando il metodo order
, le ordini in ordine decrescente in base alla data di creazione, posizionando le ricette più recenti per prime. Infine, invii la lista delle ricette come risposta JSON con render
.
Successivamente, aggiungerai la logica per la creazione di nuove ricette. Come per il recupero di tutte le ricette, ti affiderai ad ActiveRecord per convalidare e salvare i dettagli della ricetta forniti. Aggiorna il controller delle ricette con le seguenti linee di codice evidenziate:
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
Nell’azione create
, si utilizza il metodo create
di ActiveRecord per creare una nuova ricetta. Il metodo create
può assegnare tutti i parametri del controller nel modello in una volta sola. Questo metodo semplifica la creazione di record, ma apre la possibilità di un uso malintenzionato. L’uso malintenzionato può essere prevenuto utilizzando la funzionalità strong parameters fornita da Rails. In questo modo, i parametri non possono essere assegnati a meno che non siano stati consentiti. Si passa un parametro recipe_params
al metodo create
nel codice. Il metodo recipe_params
è un metodo private
in cui si permettono i parametri del controller per evitare l’inserimento di contenuti errati o malintenzionati nel database. In questo caso, si permette l’uso valido del metodo create
per i parametri name
, image
, ingredients
, e instruction
.
Il tuo controller delle ricette può ora leggere e creare ricette. Tutto ciò che resta è la logica per la lettura ed eliminazione di una singola ricetta. Aggiorna il tuo controller delle ricette con il codice evidenziato:
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
Nelle nuove righe di codice, viene creato un metodo privato set_recipe
chiamato da un before_action
solo quando le azioni show
e delete
corrispondono a una richiesta. Il metodo set_recipe
utilizza il metodo find
di ActiveRecord per trovare una ricetta il cui id
corrisponde all’id
fornito nei params
e lo assegna a una variabile di istanza @recipe
. Nell’azione show
, restituisci l’oggetto @recipe
impostato dal metodo set_recipe
come risposta JSON.
Nell’azione destroy
, hai fatto qualcosa di simile utilizzando l’operatore di navigazione sicura di Ruby &.
, che evita errori nil
durante la chiamata di un metodo. Questa aggiunta ti permette di eliminare una ricetta solo se esiste, quindi inviare un messaggio come risposta.
Dopo aver apportato queste modifiche a recipes_controller.rb
, salva e chiudi il file.
In questo passaggio, hai creato un modello e un controller per le tue ricette. Hai scritto tutta la logica necessaria per lavorare con le ricette sul lato server. Nella prossima sezione, creerai componenti per visualizzare le tue ricette.
Passo 7 — Visualizzazione delle Ricette
In questa sezione, creerai componenti per la visualizzazione delle ricette. Creerai due pagine: una per visualizzare tutte le ricette esistenti e un’altra per visualizzare singole ricette.
Inizierai creando una pagina per visualizzare tutte le ricette. Prima di creare la pagina, hai bisogno di ricette con cui lavorare, poiché il tuo database è attualmente vuoto. Rails fornisce un modo per creare dati di semina per la tua applicazione.
Apri il file di semina chiamato seeds.rb
per modificarlo:
- nano ~/rails_react_recipe/db/seeds.rb
Sostituisci i contenuti iniziali del file di semina con il seguente codice:
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
In questo codice, utilizzi un ciclo che istruisce Rails a creare nove ricette con sezioni per nome
, ingredienti
e istruzioni
. Salva ed esci dal file.
Per seminare il database con questi dati, esegui il seguente comando nel tuo terminale:
- rails db:seed
Eseguendo questo comando aggiungi nove ricette al tuo database. Ora puoi recuperarle e renderle sul frontend.
Il componente per visualizzare tutte le ricette farà una richiesta HTTP all’azione index
nel RecipesController
per ottenere un elenco di tutte le ricette. Queste ricette verranno quindi visualizzate in schede sulla pagina.
Crea un file Recipes.jsx
nella directory app/javascript/components
:
- nano ~/rails_react_recipe/app/javascript/components/Recipes.jsx
Una volta aperto il file, importa i moduli React
, useState
, useEffect
, Link
e useNavigate
aggiungendo le seguenti righe:
import React, { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
Successivamente, aggiungi le righe evidenziate per creare ed esportare un componente funzionale React chiamato 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;
All’interno del componente Recipe
, l’API di navigazione di React Router chiamerà il hook useNavigate. Il hook useState di React inizializzerà lo stato recipes
, che è un array vuoto ([]
), e una funzione setRecipes
per aggiornare lo stato recipes
.
Successivamente, in un hook useEffect
, verrà effettuata una richiesta HTTP per recuperare tutte le ricette. Per fare ciò, aggiungi le righe evidenziate:
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;
Nel tuo hook useEffect
, fai una chiamata HTTP per recuperare tutte le ricette utilizzando l’API Fetch. Se la risposta è positiva, l’applicazione salva l’array di ricette nello stato recipes
. Se si verifica un errore, reindirizzerà l’utente alla homepage.
Infine, restituisci il markup degli elementi che verranno valutati e visualizzati sulla pagina del browser quando il componente viene renderizzato. In questo caso, il componente renderizzerà una scheda di ricette dallo stato recipes
. Aggiungi le righe evidenziate 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;
Salva ed esci da Recipes.jsx
.
Ora che hai creato un componente per visualizzare tutte le ricette, creerai una route per esso. Apri il file di route del frontend app/javascript/routes/index.jsx
:
- nano app/javascript/routes/index.jsx
Aggiungi le righe evidenziate al file:
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>
);
Salva ed esci dal file.
A questo punto, è una buona idea verificare che il tuo codice funzioni come previsto. Come hai fatto prima, utilizza il seguente comando per avviare il server:
- bin/dev
Quindi apri l’app nel tuo browser. Premi il pulsante Visualizza Ricetta sulla homepage per accedere a una pagina di visualizzazione con le tue ricette di base:
Utilizza CTRL+C
nel tuo terminale per interrompere il server e tornare al prompt.
Ora che puoi visualizzare tutte le ricette nella tua applicazione, è il momento di creare un secondo componente per visualizzare le singole ricette. Crea un file Recipe.jsx
nella directory app/javascript/components
:
- nano app/javascript/components/Recipe.jsx
Come con il componente Recipes
, importa i moduli React
, useState
, useEffect
, Link
, useNavigate
e useParam
aggiungendo le seguenti righe:
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
Successivamente, aggiungi le righe evidenziate per creare ed esportare un componente React funzionale chiamato 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;
Come il componente Recipes
, inizializzi la navigazione di React Router con il hook useNavigate
. Uno stato recipe
e una funzione setRecipe
aggiorneranno lo stato con il hook useState
. Inoltre, chiami il hook useParams
, che restituisce un oggetto i cui coppie chiave/valore sono i parametri URL.
Per trovare una ricetta specifica, la tua applicazione deve conoscere l’id
della ricetta, il che significa che il tuo componente Recipe
si aspetta un param
id
nell’URL. Puoi accedere a questo tramite l’oggetto params
che contiene il valore restituito del hook useParams
.
Next, dichiara un hook useEffect
in cui accederai all’id
parametro dall’oggetto params
. Una volta ottenuto il parametro id
della ricetta, effettuerai una richiesta HTTP per recuperare la ricetta. Aggiungi le linee evidenziate al tuo file:
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;
Nell’hook useEffect
, utilizzi il valore params.id
per effettuare una richiesta HTTP di tipo GET per recuperare la ricetta associata all’id
e successivamente salvarla nello stato del componente usando la funzione setRecipe
. L’app reindirizza l’utente alla pagina delle ricette se la ricetta non esiste.
Successivamente, aggiungi una funzione addHtmlEntities
, che verrà utilizzata per sostituire le entità dei caratteri con entità HTML nel componente. La funzione addHtmlEntities
prenderà una stringa e sostituirà tutte le parentesi graffe di apertura e chiusura con le rispettive entità HTML. Questa funzione ti aiuterà a convertire qualsiasi carattere di escape salvato nelle istruzioni della tua ricetta. Aggiungi le linee evidenziate:
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;
Infine, restituisci il markup per visualizzare la ricetta nello stato del componente sulla pagina, aggiungendo le linee evidenziate:
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 funzione ingredientList
, dividi gli ingredienti della tua ricetta separati da virgole in un array e mappi su di essi per creare un elenco degli ingredienti. Se non ci sono ingredienti, l’app visualizza un messaggio che dice Nessun ingrediente disponibile. Sostituisci anche tutte le parentesi aperte e chiuse nelle istruzioni della ricetta passandole attraverso la funzione addHtmlEntities
. Infine, il codice visualizza l’immagine della ricetta come un’immagine principale, aggiunge un pulsante Elimina Ricetta accanto alle istruzioni della ricetta e aggiunge un pulsante che rimanda alla pagina delle ricette.
Nota: Utilizzare l’attributo dangerouslySetInnerHTML
di React è rischioso poiché espone la tua app agli attacchi cross-site scripting. Questo rischio viene ridotto assicurandosi che i caratteri speciali inseriti durante la creazione delle ricette vengano sostituiti utilizzando la funzione stripHtmlEntities
dichiarata nel componente NewRecipe
.
Salva e chiudi il file.
Per visualizzare il componente Ricetta
in una pagina, dovrai aggiungerlo al tuo file di route. Apri il file delle route per modificarlo:
- nano app/javascript/routes/index.jsx
Aggiungi le seguenti righe evidenziate al file:
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>
);
Importi il tuo componente Ricetta
in questo file di route e aggiungi una route. La sua route ha un parametro
:id
che verrà sostituito dall’id
della ricetta che desideri visualizzare.
Salva e chiudi il file.
Usa lo script bin/dev
per avviare nuovamente il server, quindi visita http://localhost:3000
nel tuo browser. Clicca sul pulsante Visualizza Ricette per navigare alla pagina delle ricette. Sulla pagina delle ricette, accedi a qualsiasi ricetta cliccando sul pulsante Visualizza Ricetta. Ti verrà mostrata una pagina popolata con i dati dal tuo database:
Puoi fermare il server con CTRL+C
.
In questo passaggio, hai aggiunto nove ricette al tuo database e creato componenti per visualizzare queste ricette, sia individualmente che come collezione. Nel prossimo passaggio, aggiungerai un componente per creare ricette.
Passaggio 8 – Creazione di Ricette
Il prossimo passaggio per avere un’applicazione utilizzabile di ricette alimentari è la capacità di creare nuove ricette. In questo passaggio, creerai un componente per questa funzionalità. Il componente conterrà un modulo per raccogliere i dettagli richiesti della ricetta dall’utente e quindi effettuare una richiesta all’azione create
nel controller Recipe
per salvare i dati della ricetta.
Crea un file NewRecipe.jsx
nella directory app/javascript/components
:
- nano app/javascript/components/NewRecipe.jsx
Nel nuovo file, importa i moduli React
, useState
, Link
e useNavigate
che hai utilizzato in altri componenti:
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
Successivamente, crea ed esporta un componente funzionale NewRecipe
aggiungendo le linee evidenziate:
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;
Come con i componenti precedenti, inizializzi la navigazione di React Router con il hook useNavigate
e poi utilizzi il hook useState
per inizializzare gli stati name
, ingredients
e instruction
, ognuno con le rispettive funzioni di aggiornamento. Questi sono i campi necessari per creare una ricetta valida.
Successivamente, crea una funzione stripHtmlEntities
che convertirà i caratteri speciali (come <
) nei loro valori escape/encodificati (come <
), rispettivamente. Per fare ciò, aggiungi le linee evidenziate 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;
Nella funzione stripHtmlEntities
, sostituisci i caratteri <
e >
con i loro valori escape. In questo modo, non memorizzerai HTML grezzo nel tuo database.
Successivamente, aggiungi le linee evidenziate per aggiungere le funzioni onChange
e onSubmit
al componente NewRecipe
per gestire la modifica e l’invio del modulo:
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 funzione onChange
accetta l’input dell’utente event
e la funzione setter dello stato, quindi aggiorna successivamente lo stato con il valore dell’input dell’utente. Nella funzione onSubmit
, si controlla che nessuno dei campi richiesti sia vuoto. Successivamente si costruisce un oggetto contenente i parametri necessari per creare una nuova ricetta. Utilizzando la funzione stripHtmlEntities
, si sostituiscono i caratteri <
e >
nelle istruzioni della ricetta con il loro valore di escape e si sostituisce ogni carattere di nuova riga con un tag di interruzione, mantenendo così il formato del testo inserito dall’utente. Infine, si effettua una richiesta HTTP POST per creare la nuova ricetta e reindirizzare alla sua pagina in caso di risposta positiva.
Per proteggersi dagli attacchi di Cross-Site Request Forgery (CSRF), Rails allega un token di sicurezza CSRF al documento HTML. Questo token è richiesto ogni volta che viene effettuata una richiesta diversa da GET
. Con la costante token
nel codice precedente, la tua applicazione verifica il token sul server e genera un’eccezione se il token di sicurezza non corrisponde a quello atteso. Nella funzione onSubmit
, l’applicazione recupera il token CSRF incorporato nel tuo documento HTML da Rails e quindi effettua una richiesta HTTP con una stringa JSON. Se la ricetta viene creata con successo, l’applicazione reindirizza l’utente alla pagina della ricetta dove può visualizzare la ricetta appena creata.
Infine, restituisci il markup che renderizza un modulo per consentire all’utente di inserire i dettagli della ricetta che desidera creare. Aggiungi le linee evidenziate:
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;
Il markup restituito include un modulo che contiene tre campi di input; uno per ciascuno dei recipeName
, recipeIngredients
e instruction
. Ogni campo di input ha un gestore eventi onChange
che chiama la funzione onChange
. Un gestore eventi onSubmit
è anche associato al pulsante di invio e chiama la funzione onSubmit
che invia i dati del modulo.
Salva ed esci dal file.
Per accedere a questo componente nel browser, aggiorna il file del percorso con il suo percorso:
- nano app/javascript/routes/index.jsx
Aggiorna il file del percorso per includere queste righe evidenziate:
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 il percorso impostato, salva ed esci dal tuo file.
Riavvia il tuo server di sviluppo e visita http://localhost:3000
nel tuo browser. Vai alla pagina delle ricette e clicca il pulsante Crea Nuova Ricetta. Troverai una pagina con un modulo per aggiungere ricette al tuo database:
Inserisci i dettagli della ricetta richiesti e clicca il pulsante Crea Ricetta. La ricetta appena creata apparirà quindi sulla pagina. Quando sei pronto, chiudi il server.
In questo passaggio, hai aggiunto la possibilità di creare ricette alla tua applicazione di ricette alimentari. Nel prossimo passaggio, aggiungerai la funzionalità per eliminare le ricette.
Passaggio 9 — Eliminazione Ricette
In questa sezione, modificherai il tuo componente Ricetta per includere un’opzione per eliminare ricette. Quando clicchi sul pulsante di eliminazione nella pagina della ricetta, l’applicazione invierà una richiesta per eliminare una ricetta dal database.
Per prima cosa, apri il tuo file Recipe.jsx
per modificarlo:
- nano app/javascript/components/Recipe.jsx
Nel componente Recipe
, aggiungi una funzione deleteRecipe
con le linee evidenziate:
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="">
...
Nella funzione deleteRecipe
, ottieni l’id
della ricetta da eliminare, quindi costruisci l’URL e ottieni il token CSRF. Successivamente, effettui una richiesta DELETE
al controller Recipes
per eliminare la ricetta. L’applicazione reindirizza l’utente alla pagina delle ricette se la ricetta viene eliminata con successo.
Per eseguire il codice nella funzione deleteRecipe
ogni volta che viene cliccato il pulsante di eliminazione, passalo come gestore dell’evento di clic al pulsante. Aggiungi un evento onClick
all’elemento del pulsante di eliminazione nel 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>
);
...
A questo punto nel tutorial, il tuo file Recipe.jsx
completo dovrebbe corrispondere a questo file:
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;
Salva ed esci dal file.
Riavvia il server dell’applicazione e vai alla homepage. Clicca il pulsante Visualizza Ricette per accedere a tutte le ricette esistenti, quindi apri una particolare ricetta e clicca il pulsante Elimina Ricetta sulla pagina per eliminare l’articolo. Sarai reindirizzato alla pagina delle ricette e la ricetta eliminata non esisterà più.
Con il pulsante di eliminazione funzionante, hai ora un’applicazione di ricette completamente funzionale!
Conclusione
In questo tutorial, hai creato un’applicazione per ricette alimentari con Ruby on Rails e un frontend React, utilizzando PostgreSQL come database e Bootstrap per lo stile. Se desideri continuare a sviluppare con Ruby on Rails, considera di seguire il nostro tutorial Sicurezza delle comunicazioni in un’applicazione Rails a tre livelli utilizzando tunnel SSH o visita la nostra serie Come programmare in Ruby per rinfrescare le tue competenze Ruby. Per approfondire React, prova Come visualizzare i dati dall’API di DigitalOcean con React.