Come configurare un progetto Ruby on Rails v7 con un frontend React su Ubuntu 20.04

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:

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.

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:

  1. 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:

  1. 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’opzione esbuild 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:

  1. cd rails_react_recipe

Successivamente, elenca i contenuti della directory:

  1. ls

Il contenuto verrà stampato in modo simile a questo:

Output
Gemfile 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:

  1. rails db:create

Questo comando crea un database development e test, restituendo il seguente output:

Output
Created 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:

  1. 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:

Output
started 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:

  1. 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:

  1. nano package.json

I pacchetti installati verranno elencati sotto la chiave dependencies:

~/rails_react_recipe/package.json
{
  "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:

  1. 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:

  1. 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 the index action you specified in the command.
  • A homepage_helper.rb file for adding helper methods related to the Homepage 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:

  1. nano config/routes.rb

In questo file, sostituisci get 'homepage/index' con root 'homepage#index' in modo che il file corrisponda al seguente:

~/rails_react_recipe/config/routes.rb
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:

  1. 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:

  1. 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:

  1. 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:

  1. nano ~/rails_react_recipe/app/javascript/application.js

Aggiungi la linea di codice evidenziata al file:

~/rails_react_recipe/app/javascript/application.js
// 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:

  1. nano ~/rails_react_recipe/app/javascript/components/Home.jsx

Aggiungi il seguente codice al file:

~/rails_react_recipe/app/javascript/components/Home.jsx
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:

  1. 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:

  1. nano ~/rails_react_recipe/app/javascript/routes/index.jsx

Aggiungi il seguente codice:

~/rails_react_recipe/app/javascript/routes/index.jsx
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:

  1. nano ~/rails_react_recipe/app/javascript/components/App.jsx

Aggiungi il seguente codice nel file App.jsx:

~/rails_react_recipe/app/javascript/components/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:

  1. nano ~/rails_react_recipe/app/javascript/components/index.jsx

Aggiungi il seguente codice nel file index.js:

~/rails_react_recipe/app/javascript/components/index.jsx
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:

  1. nano ~/rails_react_recipe/app/assets/stylesheets/application.bootstrap.scss

Successivamente, sostituisci il contenuto del file application.bootstrap.scss con il seguente codice:

~/rails_react_recipe/app/assets/stylesheets/application.bootstrap.scss
@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:

  1. 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:

  1. 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:

  1. nano ~/rails_react_recipe/app/models/recipe.rb

Aggiungi le seguenti linee di codice al file:

~/rails_react_recipe/app/models/recipe.rb
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:

  1. nano ~/rails_react_recipe/db/migrate/20221017220817_create_recipes.rb

Aggiungi i materiali evidenziati in modo che il tuo file corrisponda al seguente:

db/migrate/20221017220817_create_recipes.rb
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:

  1. 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:

  1. 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:

  1. nano ~/rails_react_recipe/config/routes.rb

Aggiorna questo file per assomigliare al codice seguente, modificando o aggiungendo le linee evidenziate:

~/rails_react_recipe/config/routes.rb
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:

  1. 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:

  1. nano ~/rails_react_recipe/app/controllers/api/v1/recipes_controller.rb

Aggiungi le linee evidenziate al controller delle ricette:

~/rails_react_recipe/app/controllers/api/v1/recipes_controller.rb
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:

~/rails_react_recipe/app/controllers/api/v1/recipes_controller.rb
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:

~/rails_react_recipe/app/controllers/api/v1/recipes_controller.rb
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:

  1. nano ~/rails_react_recipe/db/seeds.rb

Sostituisci i contenuti iniziali del file di semina con il seguente codice:

~/rails_react_recipe/db/seeds.rb
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:

  1. 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:

  1. 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:

~/rails_react_recipe/app/javascript/components/Recipes.jsx
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:

~/rails_react_recipe/app/javascript/components/Recipes.jsx
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:

~/rails_react_recipe/app/javascript/components/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("/"));
  }, []);
};

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:

~/rails_react_recipe/app/javascript/components/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:

  1. nano app/javascript/routes/index.jsx

Aggiungi le righe evidenziate al file:

~/rails_react_recipe/app/javascript/routes/index.jsx
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:

  1. 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:

  1. 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:

~/rails_react_recipe/app/javascript/components/Recipe.jsx
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:

~/rails_react_recipe/app/javascript/components/Recipe.jsx
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:

~/rails_react_recipe/app/javascript/components/Recipe.jsx
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:

~/rails_react_recipe/app/javascript/components/Recipe.jsx
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(/&lt;/g, "<").replace(/&gt;/g, ">");
  };
};

export default Recipe;

Infine, restituisci il markup per visualizzare la ricetta nello stato del componente sulla pagina, aggiungendo le linee evidenziate:

~/rails_react_recipe/app/javascript/components/Recipe.jsx
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(/&lt;/g, "<").replace(/&gt;/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:

  1. nano app/javascript/routes/index.jsx

Aggiungi le seguenti righe evidenziate al file:

~/rails_react_recipe/app/javascript/routes/index.jsx
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:

  1. nano app/javascript/components/NewRecipe.jsx

Nel nuovo file, importa i moduli React, useState, Link e useNavigate che hai utilizzato in altri componenti:

~/rails_react_recipe/app/javascript/components/NewRecipe.jsx
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";

Successivamente, crea ed esporta un componente funzionale NewRecipe aggiungendo le linee evidenziate:

~/rails_react_recipe/app/javascript/components/NewRecipe.jsx
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 &lt;), rispettivamente. Per fare ciò, aggiungi le linee evidenziate al componente NewRecipe:

~/rails_react_recipe/app/javascript/components/NewRecipe.jsx
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, "&lt;")
      .replace(/>/g, "&gt;");
  };
};

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:

~/rails_react_recipe/app/javascript/components/NewRecipe.jsx
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, "&lt;")
      .replace(/>/g, "&gt;");
  };

  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:

~/rails_react_recipe/app/javascript/components/NewRecipe.jsx
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, "&lt;")
      .replace(/>/g, "&gt;");
  };

  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:

  1. nano app/javascript/routes/index.jsx

Aggiorna il file del percorso per includere queste righe evidenziate:

~/rails_react_recipe/app/javascript/routes/index.jsx
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:

  1. nano app/javascript/components/Recipe.jsx

Nel componente Recipe, aggiungi una funzione deleteRecipe con le linee evidenziate:

~/rails_react_recipe/app/javascript/components/Recipe.jsx
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(/&lt;/g, "<").replace(/&gt;/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:

~/rails_react_recipe/app/javascript/components/Recipe.jsx
...
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:

~/rails_react_recipe/app/javascript/components/Recipe.jsx
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(/&lt;/g, "<").replace(/&gt;/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.

Source:
https://www.digitalocean.com/community/tutorials/how-to-set-up-a-ruby-on-rails-v7-project-with-a-react-frontend-on-ubuntu-20-04