Comment configurer un projet Ruby on Rails v7 avec un frontend React sur Ubuntu 20.04

L’auteur a choisi l’Electronic Frontier Foundation pour recevoir une donation dans le cadre du programme Write for DOnations.

Introduction

Ruby on Rails est un framework populaire pour les applications web côté serveur. Il alimente de nombreuses applications populaires présentes sur le web aujourd’hui, telles que GitHub, Basecamp, SoundCloud, Airbnb et Twitch. Avec son accent sur l’expérience des programmeurs et la communauté passionnée qui l’entoure, Ruby on Rails vous fournira les outils nécessaires pour construire et maintenir votre application web moderne.

React est une bibliothèque JavaScript utilisée pour créer des interfaces utilisateur frontales. Soutenu par Facebook, c’est l’une des bibliothèques frontales les plus populaires utilisées sur le web aujourd’hui. React offre des fonctionnalités telles qu’un modèle d’objet de document virtuel (DOM), une architecture de composants et une gestion d’état, ce qui rend le processus de développement front-end plus organisé et efficace.

Avec le passage du frontend web vers des frameworks séparés du code côté serveur, combiner l’élégance de Rails avec l’efficacité de React vous permettra de construire des applications puissantes et modernes informées par les tendances actuelles. En utilisant React pour rendre les composants à partir d’une vue Rails (au lieu du moteur de template Rails), votre application bénéficiera des dernières avancées en matière de JavaScript et de développement frontend tout en tirant parti de l’expressivité de Ruby on Rails.

Dans ce tutoriel, vous créerez une application Ruby on Rails qui stocke vos recettes préférées, puis les affiche avec un frontend React. Une fois terminé, vous pourrez créer, afficher et supprimer des recettes à l’aide d’une interface React stylisée avec Bootstrap:

Prérequis

Pour suivre ce tutoriel, vous avez besoin de :

Remarque : La version 7 de Rails n’est pas rétrocompatible. Si vous utilisez la version 5 de Rails, veuillez consulter le tutoriel sur Comment configurer un projet Ruby on Rails v5 avec une interface React sur Ubuntu 18.04.

Étape 1 — Création d’une nouvelle application Rails

Vous allez construire votre application de recettes sur le framework d’application Rails dans cette étape. Tout d’abord, vous allez créer une nouvelle application Rails, qui sera configurée pour fonctionner avec React.

Rails fournit plusieurs scripts appelés générateurs qui créent tout ce qui est nécessaire pour construire une application web moderne. Pour consulter une liste complète de ces commandes et ce qu’elles font, exécutez la commande suivante dans votre terminal :

  1. rails -h

Cette commande produira une liste exhaustive d’options, vous permettant de définir les paramètres de votre application. L’une des commandes répertoriées est la commande new, qui crée une nouvelle application Rails.

Maintenant, vous allez créer une nouvelle application Rails en utilisant le générateur new. Exécutez la commande suivante dans votre terminal:

  1. rails new rails_react_recipe -d postgresql -j esbuild -c bootstrap -T

La commande précédente crée une nouvelle application Rails dans un répertoire nommé rails_react_recipe, installe les dépendances Ruby et JavaScript requises, et configure Webpack. Les indicateurs associés à cette commande de générateur new incluent les suivants:

  • Le drapeau -d spécifie le moteur de base de données préféré, qui dans ce cas est PostgreSQL.
  • Le drapeau -j spécifie l’approche JavaScript de l’application. Rails propose plusieurs façons différentes de gérer le code JavaScript dans les applications Rails. L’option esbuild passée au drapeau -j indique à Rails de préconfigurer esbuild comme le bundler JavaScript préféré.
  • Le drapeau -c spécifie le processeur CSS de l’application. Bootstrap est l’option préférée dans ce cas.
  • Le drapeau -T indique à Rails de sauter la génération de fichiers de test puisque vous n’écrirez pas de tests pour ce tutoriel. Cette commande est également suggérée si vous souhaitez utiliser un outil de test Ruby différent de celui fourni par Rails.

Une fois que la commande est terminée, passez au répertoire rails_react_recipe, qui est le répertoire racine de votre application:

  1. cd rails_react_recipe

Ensuite, répertoriez le contenu du répertoire :

  1. ls

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

Ce répertoire racine contient plusieurs fichiers et dossiers générés automatiquement qui composent la structure d’une application Rails, y compris un fichier package.json contenant les dépendances pour une application React.

Maintenant que vous avez créé avec succès une nouvelle application Rails, vous allez la connecter à une base de données à l’étape suivante.

Étape 2 — Configuration de la base de données

Avant d’exécuter votre nouvelle application Rails, vous devez d’abord la connecter à une base de données. À cette étape, vous allez connecter la nouvelle application Rails créée à une base de données PostgreSQL afin que les données des recettes puissent être stockées et récupérées au besoin.

Le fichier database.yml trouvé dans config/database.yml contient les détails de la base de données comme les noms de base de données pour différents environnements de développement. Rails spécifie un nom de base de données pour les différents environnements de développement en ajoutant un trait de soulignement (_) suivi du nom de l’environnement. Dans ce tutoriel, vous utiliserez les valeurs de configuration de base de données par défaut, mais vous pouvez changer vos valeurs de configuration si nécessaire.

Remarque : À ce stade, vous pouvez modifier config/database.yml pour définir le rôle PostgreSQL que vous souhaitez que Rails utilise pour créer votre base de données. Lors des prérequis, vous avez créé un rôle sécurisé par un mot de passe dans le tutoriel Comment utiliser PostgreSQL avec votre application Ruby on Rails. Si vous n’avez pas encore défini l’utilisateur, vous pouvez maintenant suivre les instructions pour Étape 4 — Configuration et création de votre base de données dans le même tutoriel préalable.

Rails propose de nombreuses commandes qui facilitent le développement d’applications web, y compris des commandes pour travailler avec des bases de données telles que create, drop et reset. Pour créer une base de données pour votre application, exécutez la commande suivante dans votre terminal :

  1. rails db:create

Cette commande crée une base de données development et test, produisant la sortie suivante :

Output
Created database 'rails_react_recipe_development' Created database 'rails_react_recipe_test'

Maintenant que l’application est connectée à une base de données, démarrez l’application en exécutant la commande suivante :

  1. bin/dev

Rails fournit un script alternatif bin/dev qui lance une application Rails en exécutant les commandes du fichier Procfile.dev dans le répertoire racine de l’application en utilisant le gemme Foreman.

Une fois que vous avez exécuté cette commande, votre invite de commande disparaîtra, et la sortie suivante s’affichera à sa place :

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.

Pour accéder à votre application, ouvrez une fenêtre de navigateur et rendez-vous sur http://localhost:3000. La page d’accueil par défaut de Rails se chargera, ce qui signifie que vous avez correctement configuré votre application Rails:

Pour arrêter le serveur web, appuyez sur CTRL+C dans le terminal où le serveur s’exécute. Vous recevrez un message d’adieu de Puma:

Output
^C SIGINT received, starting shutdown - Gracefully stopping, waiting for requests to finish === puma shutdown: 2019-07-31 14:21:24 -0400 === - Goodbye! Exiting sending SIGTERM to all processes terminated by SIGINT terminated by SIGINT exited with code 0

Votre invite de terminal réapparaîtra ensuite.

Vous avez configuré avec succès une base de données pour votre application de recettes alimentaires. À l’étape suivante, vous installerez les dépendances JavaScript dont vous avez besoin pour assembler votre frontend React.

Étape 3 — Installation des Dépendances Frontend

Dans cette étape, vous installerez les dépendances JavaScript nécessaires sur le frontend de votre application de recettes alimentaires. Elles comprennent:

  • React pour la construction des interfaces utilisateur.
  • React DOM pour permettre à React d’interagir avec le DOM du navigateur.
  • React Router pour gérer la navigation dans une application React.

Exécutez la commande suivante pour installer ces packages avec le gestionnaire de paquets Yarn:

  1. yarn add react react-dom react-router-dom

Cette commande utilise Yarn pour installer les packages spécifiés et les ajoute au fichier package.json. Pour vérifier cela, ouvrez le fichier package.json situé à la racine du projet :

  1. nano package.json

Les packages installés seront répertoriés sous la clé 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"
  }
}

Fermez le fichier en appuyant sur CTRL+X.

Vous avez installé quelques dépendances front-end pour votre application. Ensuite, vous configurerez une page d’accueil pour votre application de recettes culinaires.

Étape 4 — Configuration de la page d’accueil

Avec les dépendances requises installées, vous allez maintenant créer une page d’accueil pour l’application afin qu’elle serve de page d’atterrissage lorsque les utilisateurs visitent l’application pour la première fois.

Rails suit le modèle architectural Modèle-Vue-Contrôleur pour les applications. Dans le modèle MVC, le rôle d’un contrôleur est de recevoir des demandes spécifiques et de les transmettre au modèle ou à la vue appropriés. Actuellement, l’application affiche la page de bienvenue de Rails lorsque l’URL racine est chargée dans le navigateur. Pour changer cela, vous allez créer un contrôleur et une vue pour la page d’accueil, puis les associer à une route.

Rails fournit un générateur de controller pour créer un contrôleur. Le générateur de controller reçoit un nom de contrôleur et une action correspondante. Pour en savoir plus à ce sujet, vous pouvez consulter la documentation Rails.

Ce tutoriel appellera le contrôleur Homepage. Exécutez la commande suivante pour créer un contrôleur Homepage avec une action index:

  1. rails g controller Homepage index

Note :
Sous Linux, l’erreur FATAL: Listen error: unable to monitor directories for changes. peut résulter d’une limite système sur le nombre de fichiers que votre machine peut surveiller pour les modifications. Exécutez la commande suivante pour la corriger :

  1. echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p

Cette commande augmentera définitivement le nombre de répertoires que vous pouvez surveiller avec Listen à 524288. Vous pouvez modifier cela à nouveau en exécutant la même commande et en remplaçant 524288 par le nombre souhaité.

L’exécution de la commande controller génère les fichiers suivants :

  • 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 fichier index.html.erb en tant que page de vue pour afficher tout ce qui concerne la page d’accueil.

Outre ces nouvelles pages créées en exécutant la commande Rails, Rails met également à jour votre fichier de routes situé dans config/routes.rb, en ajoutant une route get pour votre page d’accueil, que vous modifierez en tant que 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

Dans ce fichier, remplacez get 'homepage/index' par root 'homepage#index' afin que le fichier corresponde à ce qui suit :

~/rails_react_recipe/config/routes.rb
Rails.application.routes.draw do
  root 'homepage#index'
  # Pour plus de détails sur le DSL disponible dans ce fichier, consultez http://guides.rubyonrails.org/routing.html
end

Cette modification instruit Rails de mapper les requêtes à la racine de l’application vers l’action index du contrôleur Homepage, qui à son tour rend dans le navigateur tout ce qui se trouve dans le fichier index.html.erb situé à app/views/homepage/index.html.erb.

Enregistrez et fermez le fichier.

Pour vérifier que cela fonctionne, démarrez votre application:

  1. bin/dev

Lorsque vous ouvrez ou actualisez l’application dans le navigateur, une nouvelle page d’accueil pour votre application se chargera:

Une fois que vous avez vérifié que votre application fonctionne, appuyez sur CTRL+C pour arrêter le serveur.

Ensuite, ouvrez le fichier ~/rails_react_recipe/app/views/homepage/index.html.erb:

  1. nano ~/rails_react_recipe/app/views/homepage/index.html.erb

Supprimez le code à l’intérieur du fichier, puis enregistrez le fichier comme vide. En faisant cela, vous vous assurez que le contenu de index.html.erb n’interfère pas avec le rendu React de votre frontend.

Maintenant que vous avez configuré votre page d’accueil pour votre application, vous pouvez passer à la section suivante, où vous configurerez le frontend de votre application pour utiliser React.

Étape 5 — Configuration de React comme frontend de votre Rails

Dans cette étape, vous allez configurer Rails pour utiliser React sur le frontend de l’application, au lieu de son moteur de modèle. Cette nouvelle configuration vous permettra de créer une page d’accueil plus attrayante visuellement avec React.

Avec l’aide de l’option esbuild spécifiée lors de la génération de l’application Rails, la plupart de la configuration nécessaire pour permettre à JavaScript de fonctionner parfaitement avec Rails est déjà en place. Tout ce qu’il reste à faire est de charger le point d’entrée de l’application React dans le point d’entrée esbuild pour les fichiers JavaScript. Pour ce faire, commencez par créer un répertoire de composants dans le répertoire app/javascript:

  1. mkdir ~/rails_react_recipe/app/javascript/components

Le répertoire components abritera le composant pour la page d’accueil, ainsi que d’autres composants React de l’application, y compris le fichier d’entrée dans l’application React.

Ensuite, ouvrez le fichier application.js situé dans app/javascript/application.js:

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

Ajoutez la ligne de code mise en évidence dans le fichier:

~/rails_react_recipe/app/javascript/application.js
// Point d'entrée pour le script de construction dans votre package.json
import "@hotwired/turbo-rails"
import "./controllers"
import * as bootstrap from "bootstrap"
import "./components"

La ligne de code ajoutée au fichier application.js importera le code du fichier d’entrée index.jsx, le rendant disponible pour esbuild pour le regroupement. Avec le répertoire /components importé dans le point d’entrée JavaScript de l’application Rails, vous pouvez créer un composant React pour votre page d’accueil. La page d’accueil contiendra du texte et un bouton d’appel à l’action pour voir toutes les recettes.

Enregistrez et fermez le fichier.

Ensuite, créez un fichier Home.jsx dans le répertoire components:

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

Ajoutez le code suivant au fichier :

~/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>
);

Dans ce code, vous importez React et le composant Link de React Router. Le composant Link crée un hyperlien pour naviguer d’une page à une autre. Ensuite, vous créez et exportez un composant fonctionnel contenant un langage de balisage pour votre page d’accueil, stylisé avec des classes Bootstrap.

Enregistrez et fermez le fichier.

Avec votre composant Home configuré, vous allez maintenant configurer le routage à l’aide de React Router. Créez un répertoire routes dans le répertoire app/javascript:

  1. mkdir ~/rails_react_recipe/app/javascript/routes

Le répertoire routes contiendra quelques routes avec leurs composants correspondants. Chaque fois qu’une route spécifiée est chargée, elle rendra son composant correspondant dans le navigateur.

Dans le répertoire routes, créez un fichier index.jsx:

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

Ajoutez le code suivant à ce fichier:

~/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>
);

Dans ce fichier de route index.jsx, vous importez les modules suivants : le module React qui vous permet d’utiliser React, ainsi que les modules BrowserRouter, Routes et Route de React Router, qui vous aident ensemble à naviguer d’une route à une autre. Enfin, vous importez votre composant Home, qui sera rendu chaque fois qu’une demande correspond à la route racine (/). Lorsque vous souhaitez ajouter plus de pages à votre application, vous pouvez déclarer une route dans ce fichier et la faire correspondre au composant que vous souhaitez rendre pour cette page.

Enregistrez et quittez le fichier.

Vous avez maintenant configuré le routage à l’aide de React Router. Pour que React puisse prendre connaissance des routes disponibles et les utiliser, les routes doivent être accessibles au point d’entrée de l’application. Pour ce faire, vous rendrez vos routes dans un composant que React rendra dans votre fichier d’entrée.

Créez un fichier App.jsx dans le répertoire app/javascript/components:

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

Ajoutez le code suivant dans le fichier App.jsx:

~/rails_react_recipe/app/javascript/components/App.jsx
import React from "react";
import Routes from "../routes";

export default props => <>{Routes}</>;

Dans le fichier App.jsx, importez React et les fichiers de route que vous venez de créer. Ensuite, exportez un composant pour rendre les routes dans des fragments. Ce composant sera rendu au point d’entrée de l’application, rendant les routes disponibles chaque fois que l’application est chargée.

Enregistrez et fermez le fichier.

Maintenant que vous avez configuré votre fichier App.jsx, vous pouvez le rendre dans votre fichier d’entrée. Créez un fichier index.jsx dans le répertoire components:

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

Ajoutez le code suivant dans le fichier 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 />);
});

Dans les lignes d’importation, importez la bibliothèque React, la fonction createRoot de ReactDOM, et votre composant App. En utilisant la fonction createRoot de ReactDOM, créez un élément racine en tant qu’élément div ajouté à la page, et vous rendez votre composant App en lui. Lorsque l’application est chargée, React rendra le contenu du composant App à l’intérieur de l’élément div sur la page.

Enregistrez et quittez le fichier.

Enfin, vous ajouterez quelques styles CSS à votre page d’accueil.

Ouvrez le fichier `application.bootstrap.scss` dans votre répertoire `~/rails_react_recipe/app/assets/stylesheets/application.bootstrap.scss` :

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

Ensuite, remplacez le contenu du fichier `application.bootstrap.scss` par le code suivant :

~/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;
}

Vous définissez quelques couleurs personnalisées pour la page. La section `.hero` créera le cadre pour une image de héros, ou une grande bannière web sur la page d’accueil de votre site web, que vous ajouterez plus tard. De plus, le style `custom-button.btn` stylise le bouton que l’utilisateur utilisera pour accéder à l’application.

Avec vos styles CSS en place, enregistrez et quittez le fichier.

Ensuite, redémarrez le serveur web de votre application :

  1. bin/dev

Puis rechargez l’application dans votre navigateur. Une toute nouvelle page d’accueil se chargera :

Arrêtez le serveur web avec `CTRL+C`.

Vous avez configuré votre application pour utiliser React comme frontend à cette étape. À l’étape suivante, vous créerez des modèles et des contrôleurs qui vous permettront de créer, lire, mettre à jour et supprimer des recettes.

Étape 6 — Création du contrôleur et du modèle de recette

Maintenant que vous avez configuré un frontend React pour votre application, vous allez créer un modèle et un contrôleur de recette. Le modèle de recette représentera la table de base de données contenant des informations sur les recettes de l’utilisateur, tandis que le contrôleur recevra et gérera les requêtes pour créer, lire, mettre à jour ou supprimer des recettes. Lorsqu’un utilisateur demande une recette, le contrôleur de recette reçoit cette demande et la transmet au modèle de recette, qui récupère les données demandées depuis la base de données. Le modèle renvoie ensuite les données de la recette en réponse au contrôleur. Enfin, ces informations sont affichées dans le navigateur.

Commencez par créer un modèle Recipe en utilisant la sous-commande generate model fournie par Rails et en spécifiant le nom du modèle ainsi que ses colonnes et types de données. Exécutez la commande suivante :

  1. rails generate model Recipe name:string ingredients:text instruction:text image:string

La commande précédente indique à Rails de créer un modèle Recipe accompagné d’une colonne name de type string, d’une colonne ingredients et instruction de type text, et d’une colonne image de type string. Ce tutoriel a nommé le modèle Recipe, car les modèles dans Rails utilisent un nom au singulier tandis que leurs tables de base de données correspondantes utilisent un nom au pluriel.

L’exécution de la commande generate model crée deux fichiers et affiche la sortie suivante :

Output
invoke active_record create db/migrate/20221017220817_create_recipes.rb create app/models/recipe.rb

Les deux fichiers créés sont :

  • 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.

Ensuite, vous éditerez le fichier du modèle de recette pour vous assurer que seules des données valides sont enregistrées dans la base de données. Vous pouvez y parvenir en ajoutant une validation de base de données à votre modèle.

Ouvrez votre modèle de recette situé à app/models/recipe.rb:

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

Ajoutez les lignes de code suivantes au fichier :

~/rails_react_recipe/app/models/recipe.rb
class Recipe < ApplicationRecord
  validates :name, presence: true
  validates :ingredients, presence: true
  validates :instruction, presence: true
end

Dans ce code, vous ajoutez une validation de modèle, qui vérifie la présence des champs name, ingredients et instruction. Sans ces trois champs, une recette est invalide et ne sera pas enregistrée dans la base de données.

Enregistrez et fermez le fichier.

Pour que Rails crée la table recipes dans votre base de données, vous devez exécuter une migration, qui est une façon de faire des modifications à votre base de données de manière programmatique. Pour vous assurer que la migration fonctionne avec la base de données que vous avez configurée, vous devez apporter des modifications au fichier 20221017220817_create_recipes.rb.

Ouvrez ce fichier dans votre éditeur :

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

Ajoutez les éléments en surbrillance pour que votre fichier corresponde au suivant :

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

Ce fichier de migration contient une classe Ruby avec une méthode change et une commande pour créer une table appelée recipes ainsi que les colonnes et leurs types de données. Vous mettez également à jour 20221017220817_create_recipes.rb avec une contrainte NOT NULL sur les colonnes name, ingredients et instruction en ajoutant null: false, garantissant que ces colonnes ont une valeur avant de modifier la base de données. Enfin, vous ajoutez une URL d’image par défaut pour votre colonne d’image ; il pourrait s’agir d’une autre URL si vous souhaitez utiliser une image différente.

Avec ces modifications, enregistrez et quittez le fichier. Vous êtes maintenant prêt à exécuter votre migration et à créer votre table. Dans votre terminal, exécutez la commande suivante :

  1. rails db:migrate

Vous utilisez la commande de migration de base de données pour exécuter les instructions de votre fichier de migration. Une fois que la commande s’exécute avec succès, vous recevrez une sortie similaire à ce qui suit :

Output
== 20190407161357 CreateRecipes: migrating ==================================== -- create_table(:recipes) -> 0.0140s == 20190407161357 CreateRecipes: migrated (0.0141s) ===========================

Avec votre modèle de recette en place, vous allez ensuite créer votre contrôleur de recettes pour ajouter la logique de création, de lecture et de suppression des recettes. Exécutez la commande suivante :

  1. rails generate controller api/v1/Recipes index create show destroy --skip-template-engine --no-helper

Dans cette commande, vous créez un contrôleur Recipes dans un répertoire api/v1 avec les actions index, create, show et destroy. L’action index gérera la récupération de toutes vos recettes ; l’action create sera responsable de la création de nouvelles recettes ; l’action show récupérera une seule recette, et l’action destroy contiendra la logique de suppression d’une recette.

Vous passez également certains indicateurs pour rendre le contrôleur plus léger, notamment :

  • --skip-template-engine, qui indique à Rails de ne pas générer de fichiers de vue Rails puisque React gère vos besoins côté front-end.
  • --no-helper, qui indique à Rails de ne pas générer de fichier d’aide pour votre contrôleur.

En exécutant la commande, votre fichier de routes est également mis à jour avec une route pour chaque action dans le contrôleur Recipes.

Lorsque la commande s’exécute, elle affichera une sortie comme ceci :

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

Pour utiliser ces routes, vous apporterez des modifications à votre fichier config/routes.rb. Ouvrez le fichier routes.rb dans votre éditeur de texte :

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

Modifiez ce fichier pour qu’il ressemble au code suivant, en modifiant ou en ajoutant les lignes surlignées :

~/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'
  # Définissez vos itinéraires d'application selon le DSL dans https://guides.rubyonrails.org/routing.html

  # Définit l'itinéraire de la racine ("/")
  # root "articles#index"
end

Dans ce fichier d’itinéraire, modifiez le verbe HTTP des itinéraires create et destroy pour qu’il puisse post et delete les données. Vous modifiez également les itinéraires pour les actions show et destroy en ajoutant un paramètre :id à l’itinéraire. :id contiendra le numéro d’identification de la recette que vous souhaitez lire ou supprimer.

Vous ajoutez un itinéraire général avec get '/*path' qui dirigera toute autre demande ne correspondant pas aux itinéraires existants vers l’action index du contrôleur homepage. Le routage côté client gérera les demandes sans rapport avec la création, la lecture ou la suppression de recettes.

Enregistrez et quittez le fichier.

Pour évaluer une liste d’itinéraires disponibles dans votre application, exécutez la commande suivante:

  1. rails routes

L’exécution de cette commande affiche une longue liste de motifs d’URI, de verbes et de contrôleurs ou actions correspondants pour votre projet.

Ensuite, vous ajouterez la logique pour obtenir toutes les recettes en une seule fois. Rails utilise la bibliothèque ActiveRecord pour gérer les tâches liées à la base de données comme celle-ci. ActiveRecord connecte les classes aux tables de bases de données relationnelles et fournit une API riche pour travailler avec elles.

Pour obtenir toutes les recettes, vous utiliserez ActiveRecord pour interroger la table des recettes et récupérer toutes les recettes dans la base de données.

Ouvrez le fichier recipes_controller.rb avec la commande suivante :

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

Ajoutez les lignes surlignées au contrôleur des recettes :

~/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

Dans votre action index, utilisez la méthode all d’ActiveRecord pour obtenir toutes les recettes de votre base de données. En utilisant la méthode order, triez-les par ordre décroissant en fonction de leur date de création, ce qui placera les recettes les plus récentes en premier. Enfin, envoyez votre liste de recettes en tant que réponse JSON avec render.

Ensuite, vous ajouterez la logique pour créer de nouvelles recettes. Tout comme pour la récupération de toutes les recettes, vous vous appuierez sur ActiveRecord pour valider et enregistrer les détails de la recette fournie. Mettez à jour votre contrôleur de recettes avec les lignes de code surlignées suivantes.

~/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

Dans l’action create, vous utilisez la méthode create d’ActiveRecord pour créer une nouvelle recette. La méthode create peut attribuer tous les paramètres du contrôleur fournis dans le modèle en une seule fois. Cette méthode facilite la création d’enregistrements mais ouvre la possibilité d’une utilisation malveillante. L’utilisation malveillante peut être évitée en utilisant la fonction strong parameters fournie par Rails. De cette façon, les paramètres ne peuvent pas être attribués à moins qu’ils n’aient été autorisés. Vous passez un paramètre recipe_params à la méthode create dans votre code. Le recipe_params est une méthode private où vous autorisez les paramètres de votre contrôleur pour éviter que du contenu incorrect ou malveillant ne soit introduit dans votre base de données. Dans ce cas, vous autorisez un paramètre name, image, ingredients et instruction pour une utilisation valide de la méthode create.

Votre contrôleur de recettes peut maintenant lire et créer des recettes. Il ne reste plus que la logique pour lire et supprimer une seule recette. Mettez à jour votre contrôleur de recettes avec le code surligné :

~/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

Dans les nouvelles lignes de code, vous créez une méthode privée set_recipe appelée par un before_action uniquement lorsque les actions show et delete correspondent à une requête. La méthode set_recipe utilise la méthode find d’ActiveRecord pour trouver une recette dont l’id correspond à l’id fourni dans les params et l’assigne à une variable d’instance @recipe. Dans l’action show, vous retournez l’objet @recipe défini par la méthode set_recipe sous forme de réponse JSON.

Dans l’action destroy, vous avez fait quelque chose de similaire en utilisant l’opérateur de navigation sécurisée de Ruby &., qui évite les erreurs nil lors de l’appel d’une méthode. Cette addition vous permet de supprimer une recette uniquement si elle existe, puis d’envoyer un message en réponse.

Après avoir apporté ces modifications à recipes_controller.rb, enregistrez et fermez le fichier.

Dans cette étape, vous avez créé un modèle et un contrôleur pour vos recettes. Vous avez écrit toute la logique nécessaire pour travailler avec les recettes côté serveur. Dans la prochaine section, vous créerez des composants pour visualiser vos recettes.

Étape 7 — Visualisation des recettes

Dans cette section, vous créerez des composants pour visualiser les recettes. Vous créerez deux pages : une pour visualiser toutes les recettes existantes et une autre pour visualiser les recettes individuelles.

Vous commencerez par créer une page pour afficher toutes les recettes. Avant de créer la page, vous avez besoin de recettes à travailler, car votre base de données est actuellement vide. Rails propose une manière de créer des données de départ pour votre application.

Ouvrez le fichier de données de départ appelé seeds.rb pour l’éditer :

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

Remplacez le contenu initial du fichier de données de départ par le code suivant :

~/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

Dans ce code, utilisez une boucle qui indique à Rails de créer neuf recettes avec des sections pour name, ingredients et instruction. Enregistrez et quittez le fichier.

Pour alimenter la base de données avec ces données, exécutez la commande suivante dans votre terminal :

  1. rails db:seed

En lançant cette commande, neuf recettes seront ajoutées à votre base de données. Maintenant, vous pouvez les récupérer et les afficher à l’avant.

Le composant pour afficher toutes les recettes effectuera une requête HTTP vers l’action index dans le RecipesController pour obtenir une liste de toutes les recettes. Ces recettes seront ensuite affichées sur la page sous forme de cartes.

Créez un fichier Recipes.jsx dans le répertoire app/javascript/components :

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

Une fois le fichier ouvert, importez les modules React, useState, useEffect, Link et useNavigate en ajoutant les lignes suivantes :

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

Ensuite, ajoutez les lignes surlignées pour créer et exporter un composant fonctionnel React appelé 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;

À l’intérieur du composant Recette, l’API de navigation de React Router appellera le crochet useNavigate. Le crochet useState de React initialisera l’état recettes, qui est un tableau vide ([]), et une fonction setRecipes pour mettre à jour l’état recettes.

Ensuite, dans un crochet useEffect, vous ferez une requête HTTP pour récupérer toutes vos recettes. Pour ce faire, ajoutez les lignes en surbrillance:

~/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;

Dans votre crochet useEffect, vous effectuez un appel HTTP pour récupérer toutes les recettes en utilisant l’API Fetch. Si la réponse est réussie, l’application enregistre le tableau de recettes dans l’état recettes. Si une erreur survient, elle redirigera l’utilisateur vers la page d’accueil.

Enfin, retournez la balise pour les éléments qui seront évalués et affichés sur la page du navigateur lorsque le composant est rendu. Dans ce cas, le composant affichera une carte de recettes à partir de l’état recettes. Ajoutez les lignes en surbrillance à 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;

Enregistrez et quittez Recipes.jsx.

Maintenant que vous avez créé un composant pour afficher toutes les recettes, vous allez créer une route pour celui-ci. Ouvrez le fichier de route front-end app/javascript/routes/index.jsx:

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

Ajoutez les lignes en surbrillance au fichier:

~/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>
);

Enregistrez et quittez le fichier.

À ce stade, il est bon de vérifier que votre code fonctionne comme prévu. Comme vous l’avez fait précédemment, utilisez la commande suivante pour démarrer votre serveur :

  1. bin/dev

Ensuite, ouvrez l’application dans votre navigateur. Appuyez sur le bouton View Recipe sur la page d’accueil pour accéder à une page d’affichage avec vos recettes de base :

Utilisez CTRL+C dans votre terminal pour arrêter le serveur et revenir à votre invite.

Maintenant que vous pouvez voir toutes les recettes dans votre application, il est temps de créer un deuxième composant pour afficher des recettes individuelles. Créez un fichier Recipe.jsx dans le répertoire app/javascript/components :

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

Tout comme avec le composant Recipes, importez les modules React, useState, useEffect, Link, useNavigate et useParam en ajoutant les lignes suivantes :

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

Ensuite, ajoutez les lignes surlignées pour créer et exporter un composant fonctionnel React appelé 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;

Comme pour le composant Recipes, vous initialisez la navigation React Router avec le crochet useNavigate. Un état recipe et une fonction setRecipe mettront à jour l’état avec le crochet useState. De plus, vous appelez le crochet useParams, qui renvoie un objet dont les paires clé/valeur sont des paramètres d’URL.

Pour trouver une recette spécifique, votre application a besoin de connaître l’ID de la recette, ce qui signifie que votre composant Recipe attend un param id dans l’URL. Vous pouvez y accéder via l’objet params qui contient la valeur de retour du crochet useParams.

Ensuite, déclarez un crochet useEffect où vous accéderez au id param de l’objet params. Une fois que vous avez obtenu le paramètre id de la recette, vous effectuerez une requête HTTP pour récupérer la recette. Ajoutez les lignes en surbrillance à votre fichier :

~/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;

Dans le crochet useEffect, vous utilisez la valeur params.id pour effectuer une requête HTTP GET afin de récupérer la recette qui possède l’identifiant, puis pour la sauvegarder dans l’état du composant en utilisant la fonction setRecipe. L’application redirige l’utilisateur vers la page des recettes si la recette n’existe pas.

Ensuite, ajoutez une fonction addHtmlEntities, qui sera utilisée pour remplacer les entités de caractères par des entités HTML dans le composant. La fonction addHtmlEntities prendra une chaîne et remplacera tous les crochets ouvrants et fermants échappés par leurs entités HTML. Cette fonction vous aidera à convertir n’importe quel caractère échappé qui a été enregistré dans votre instruction de recette. Ajoutez les lignes en surbrillance :

~/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;

Enfin, renvoyez le balisage pour afficher la recette dans l’état du composant sur la page en ajoutant les lignes en surbrillance :

~/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;

Avec une fonction ingredientList, vous divisez vos ingrédients de recette séparés par des virgules en un tableau et mappez dessus pour créer une liste d’ingrédients. S’il n’y a pas d’ingrédients, l’application affiche un message qui dit Aucun ingrédient disponible. Vous remplacez également tous les crochets ouvrants et fermants dans les instructions de la recette en les faisant passer par la fonction addHtmlEntities. Enfin, le code affiche l’image de la recette comme une image principale, ajoute un bouton Supprimer la recette à côté des instructions de la recette, et ajoute un bouton qui renvoie à la page des recettes.

Note : L’utilisation de l’attribut dangerouslySetInnerHTML de React est risquée car elle expose votre application à des attaques de scripting entre sites. Ce risque est réduit en veillant à ce que les caractères spéciaux saisis lors de la création des recettes soient remplacés en utilisant la fonction stripHtmlEntities déclarée dans le composant NewRecipe.

Enregistrez et quittez le fichier.

Pour afficher le composant Recipe sur une page, vous l’ajouterez à votre fichier de routes. Ouvrez votre fichier de routes pour l’éditer :

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

Ajoutez les lignes suivantes surlignées dans le fichier :

~/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>
);

Vous importez votre composant Recipe dans ce fichier de route et ajoutez une route. Sa route a un param :id qui sera remplacé par l’id de la recette que vous souhaitez afficher.

Enregistrez et fermez le fichier.

Utilisez le script bin/dev pour redémarrer votre serveur, puis visitez http://localhost:3000 dans votre navigateur. Cliquez sur le bouton Voir les recettes pour accéder à la page des recettes. Sur la page des recettes, accédez à n’importe quelle recette en cliquant sur son bouton Voir la recette. Vous serez accueilli avec une page peuplée des données de votre base de données :

Vous pouvez arrêter le serveur avec CTRL+C.

Dans cette étape, vous avez ajouté neuf recettes à votre base de données et créé des composants pour visualiser ces recettes, à la fois individuellement et en tant que collection. À l’étape suivante, vous ajouterez un composant pour créer des recettes.

Étape 8 — Création de recettes

La prochaine étape pour avoir une application de recettes alimentaires utilisable est la capacité à créer de nouvelles recettes. À cette étape, vous allez créer un composant pour cette fonctionnalité. Le composant contiendra un formulaire pour collecter les détails de recette requis de l’utilisateur, puis fera une demande à l’action create dans le contrôleur Recipe pour enregistrer les données de la recette.

Créez un fichier NewRecipe.jsx dans le répertoire app/javascript/components :

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

Dans le nouveau fichier, importez les modules React, useState, Link et useNavigate que vous avez utilisés dans d’autres composants :

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

Ensuite, créez et exportez un composant fonctionnel NewRecipe en ajoutant les lignes en surbrillance :

~/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;

Comme pour les composants précédents, vous initialisez la navigation du routeur React avec le crochet useNavigate puis utilisez le crochet useState pour initialiser les états name, ingredients et instruction, chacun avec ses fonctions de mise à jour respectives. Ce sont les champs dont vous aurez besoin pour créer une recette valide.

Ensuite, créez une fonction stripHtmlEntities qui convertira les caractères spéciaux (comme <) en leurs valeurs échappées/encodées (comme &lt;), respectivement. Pour ce faire, ajoutez les lignes surlignées au composant 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;

Dans la fonction stripHtmlEntities, remplacez les caractères < et > par leurs valeurs échappées. Ainsi, vous n’enregistrerez pas de HTML brut dans votre base de données.

Ensuite, ajoutez les lignes surlignées pour ajouter les fonctions onChange et onSubmit au composant NewRecipe pour gérer la modification et la soumission du formulaire:

~/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 fonction onChange accepte l’entrée utilisateur event et la fonction setter d’état, puis met à jour l’état avec la valeur de l’entrée utilisateur. Dans la fonction onSubmit, vous vérifiez que aucun des champs requis n’est vide. Ensuite, vous construisez un objet contenant les paramètres nécessaires pour créer une nouvelle recette. En utilisant la fonction stripHtmlEntities, vous remplacez les caractères < et > dans les instructions de recette par leur valeur échappée et remplacez chaque caractère de nouvelle ligne par une balise de saut de ligne, conservant ainsi le format texte saisi par l’utilisateur. Enfin, vous effectuez une requête HTTP POST pour créer la nouvelle recette et redirigez vers sa page en cas de réponse réussie.

Pour se protéger contre les attaques de falsification de requête inter-site (CSRF), Rails attache un jeton de sécurité CSRF au document HTML. Ce jeton est requis chaque fois qu’une requête non-GET est effectuée. Avec la constante token dans le code précédent, votre application vérifie le jeton sur le serveur et lance une exception si le jeton de sécurité ne correspond pas à ce qui est attendu. Dans la fonction onSubmit, l’application récupère le jeton CSRF intégré dans votre document HTML par Rails, puis effectue une requête HTTP avec une chaîne JSON. Si la recette est créée avec succès, l’application redirige l’utilisateur vers la page de la recette où il peut voir sa nouvelle recette.

Enfin, renvoyez le balisage qui rend un formulaire pour que l’utilisateur saisisse les détails de la recette qu’il souhaite créer. Ajoutez les lignes en surbrillance :

~/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;

Le balisage retourné comprend un formulaire contenant trois champs de saisie ; un pour chaque recipeName, recipeIngredients et instruction. Chaque champ de saisie a un gestionnaire d’événements onChange qui appelle la fonction onChange. Un gestionnaire d’événements onSubmit est également attaché au bouton de soumission et appelle la fonction onSubmit qui soumet les données du formulaire.

Enregistrez et quittez le fichier.

Pour accéder à ce composant dans le navigateur, mettez à jour votre fichier de route avec son itinéraire :

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

Mettez à jour votre fichier de route pour inclure ces lignes en surbrillance :

~/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>
);

Avec l’itinéraire en place, enregistrez et quittez votre fichier.

Redémarrez votre serveur de développement et visitez http://localhost:3000 dans votre navigateur. Accédez à la page des recettes et cliquez sur le bouton Créer une nouvelle recette. Vous trouverez une page avec un formulaire pour ajouter des recettes à votre base de données :

Entrez les détails de la recette requis et cliquez sur le bouton Créer une recette. La recette nouvellement créée apparaîtra alors sur la page. Lorsque vous êtes prêt, fermez le serveur.

Dans cette étape, vous avez ajouté la possibilité de créer des recettes à votre application de recettes alimentaires. À l’étape suivante, vous ajouterez la fonctionnalité de suppression des recettes.

Étape 9 — Suppression des recettes

Dans cette section, vous allez modifier votre composant Recette pour inclure une option de suppression des recettes. Lorsque vous cliquez sur le bouton de suppression sur la page de la recette, l’application enverra une demande de suppression d’une recette de la base de données.

Tout d’abord, ouvrez votre fichier Recipe.jsx pour l’édition:

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

Dans le composant Recipe, ajoutez une fonction deleteRecipe avec les lignes surlignées:

~/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="">
...

Dans la fonction deleteRecipe, vous obtenez l’id de la recette à supprimer, puis construisez votre URL et récupérez le jeton CSRF. Ensuite, vous faites une demande DELETE au contrôleur Recipes pour supprimer la recette. L’application redirige l’utilisateur vers la page des recettes si la recette est supprimée avec succès.

Pour exécuter le code dans la fonction deleteRecipe chaque fois que le bouton de suppression est cliqué, passez-le en tant que gestionnaire d’événements de clic au bouton. Ajoutez un événement onClick à l’élément du bouton de suppression dans le composant:

~/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>
  );
...

À ce stade du tutoriel, votre fichier Recipe.jsx complet devrait correspondre à ce fichier:

~/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;

Enregistrez et quittez le fichier.

Redémarrez le serveur de l’application et accédez à la page d’accueil. Cliquez sur le bouton View Recipes pour accéder à toutes les recettes existantes, puis ouvrez une recette particulière et cliquez sur le bouton Delete Recipe sur la page pour supprimer l’article. Vous serez redirigé vers la page des recettes et la recette supprimée n’existera plus.

Avec le bouton de suppression fonctionnant, vous disposez maintenant d’une application de recettes entièrement fonctionnelle!

Conclusion

Dans ce tutoriel, vous avez créé une application de recettes alimentaires avec Ruby on Rails et une interface frontend React, en utilisant PostgreSQL comme base de données et Bootstrap pour le style. Si vous souhaitez continuer à développer avec Ruby on Rails, envisagez de suivre notre tutoriel Sécurisation des communications dans une application Rails à trois niveaux en utilisant des tunnels SSH ou visitez notre série Comment Coder en Ruby pour rafraîchir vos compétences en Ruby. Pour approfondir React, essayez Comment afficher des données de l’API DigitalOcean avec 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