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 :
-
Node.js et npm installés sur votre machine de développement. Ce tutoriel utilise la version 16.14.0 de Node.js et la version 8.3.1 de npm. Node.js est un environnement d’exécution JavaScript qui vous permet d’exécuter votre code en dehors du navigateur. Il est livré avec un gestionnaire de paquets pré-installé appelé npm, qui vous permet d’installer et de mettre à jour des packages. Pour les installer sur Ubuntu 20.04 ou macOS, suivez la section « Installation à l’aide d’un PPA » de Comment installer Node.js sur Ubuntu 20.04 ou les étapes de Comment installer Node.js et créer un environnement de développement local sur macOS.
-
Le gestionnaire de packages Yarn installé sur votre machine de développement, qui vous permettra de télécharger le framework React. Ce tutoriel a été testé avec la version 1.22.10 ; pour installer cette dépendance, suivez le guide d’installation officiel de Yarn.
-
Ruby on Rails est installé. Pour cela, suivez notre guide sur Comment installer Ruby on Rails avec rbenv sur Ubuntu 20.04. Si vous souhaitez développer cette application sur macOS, vous pouvez utiliser Comment installer Ruby on Rails avec rbenv sur macOS. Ce tutoriel a été testé avec la version 3.1.2 de Ruby et la version 7.0.4 de Rails, assurez-vous de spécifier ces versions lors du processus d’installation.
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.
- PostgreSQL installé, comme décrit dans les Étapes 1 et 2 Comment utiliser PostgreSQL avec votre application Ruby on Rails sur Ubuntu 20.04 ou Comment utiliser PostgreSQL avec votre application Ruby on Rails sur macOS. Pour suivre ce tutoriel, vous pouvez utiliser PostgreSQL version 12 ou supérieure. Si vous souhaitez développer cette application sur une distribution Linux différente ou un autre système d’exploitation, consultez la page de téléchargements officielle de PostgreSQL. Pour plus d’informations sur l’utilisation de PostgreSQL, consultez Comment installer et utiliser PostgreSQL.
É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 :
- 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:
- 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’optionesbuild
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:
- cd rails_react_recipe
Ensuite, répertoriez le contenu du répertoire :
- ls
OutputGemfile README.md bin db node_modules storage yarn.lock
Gemfile.lock Rakefile config lib package.json tmp
Procfile.dev app config.ru log public vendor
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 :
- rails db:create
Cette commande crée une base de données development
et test
, produisant la sortie suivante :
OutputCreated 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 :
- 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 :
Outputstarted with pid 70099
started with pid 70100
started with pid 70101
yarn run v1.22.10
yarn run v1.22.10
$ esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=assets --watch
$ sass ./app/assets/stylesheets/application.bootstrap.scss:./app/assets/builds/application.css --no-source-map --load-path=node_modules --watch
=> Booting Puma
=> Rails 7.0.4 application starting in development
=> Run `bin/rails server --help` for more startup options
[watch] build finished, watching for changes...
Puma starting in single mode...
* Puma version: 5.6.5 (ruby 3.1.2-p20) ("Birdie's Version")
* Min threads: 5
* Max threads: 5
* Environment: development
* PID: 70099
* Listening on http://127.0.0.1:3000
* Listening on http://[::1]:3000
Use Ctrl-C to stop
Sass is watching for changes. Press Ctrl-C to stop.
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:
- 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 :
- nano package.json
Les packages installés seront répertoriés sous la clé dependencies
:
{
"name": "app",
"private": "true",
"dependencies": {
"@hotwired/stimulus": "^3.1.0",
"@hotwired/turbo-rails": "^7.1.3",
"@popperjs/core": "^2.11.6",
"bootstrap": "^5.2.1",
"bootstrap-icons": "^1.9.1",
"esbuild": "^0.15.7",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.3.0",
"sass": "^1.54.9"
},
"scripts": {
"build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=assets",
"build:css": "sass ./app/assets/stylesheets/application.bootstrap.scss:./app/assets/builds/application.css --no-source-map --load-path=node_modules"
}
}
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
:
- 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 :
- 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 theindex
action you specified in the command. - A
homepage_helper.rb
file for adding helper methods related to theHomepage
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:
- nano config/routes.rb
Dans ce fichier, remplacez get 'homepage/index'
par root 'homepage#index'
afin que le fichier corresponde à ce qui suit :
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:
- 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
:
- 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
:
- 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
:
- nano ~/rails_react_recipe/app/javascript/application.js
Ajoutez la ligne de code mise en évidence dans le fichier:
// 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
:
- nano ~/rails_react_recipe/app/javascript/components/Home.jsx
Ajoutez le code suivant au fichier :
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
:
- 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
:
- nano ~/rails_react_recipe/app/javascript/routes/index.jsx
Ajoutez le code suivant à ce fichier:
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
:
- nano ~/rails_react_recipe/app/javascript/components/App.jsx
Ajoutez le code suivant dans le fichier 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
:
- nano ~/rails_react_recipe/app/javascript/components/index.jsx
Ajoutez le code suivant dans le fichier index.js
:
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
document.addEventListener("turbo:load", () => {
const root = createRoot(
document.body.appendChild(document.createElement("div"))
);
root.render(<App />);
});
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` :
- nano ~/rails_react_recipe/app/assets/stylesheets/application.bootstrap.scss
Ensuite, remplacez le contenu du fichier `application.bootstrap.scss` par le code suivant :
@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 :
- 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 :
- 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
:
- nano ~/rails_react_recipe/app/models/recipe.rb
Ajoutez les lignes de code suivantes au fichier :
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 :
- nano ~/rails_react_recipe/db/migrate/20221017220817_create_recipes.rb
Ajoutez les éléments en surbrillance pour que votre fichier corresponde au suivant :
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 :
- 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 :
- 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 :
- 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.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:
- 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 :
- nano ~/rails_react_recipe/app/controllers/api/v1/recipes_controller.rb
Ajoutez les lignes surlignées au contrôleur des recettes :
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.
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é :
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 :
- nano ~/rails_react_recipe/db/seeds.rb
Remplacez le contenu initial du fichier de données de départ par le code suivant :
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 :
- 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
:
- 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 :
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
:
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:
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
:
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
:
- nano app/javascript/routes/index.jsx
Ajoutez les lignes en surbrillance au fichier:
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 :
- 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
:
- 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 :
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
:
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 :
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 :
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
const Recipe = () => {
const params = useParams();
const navigate = useNavigate();
const [recipe, setRecipe] = useState({ ingredients: "" });
useEffect(() => {
const url = `/api/v1/show/${params.id}`;
fetch(url)
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then((response) => setRecipe(response))
.catch(() => navigate("/recipes"));
}, [params.id]);
const addHtmlEntities = (str) => {
return String(str).replace(/</g, "<").replace(/>/g, ">");
};
};
export default Recipe;
Enfin, renvoyez le balisage pour afficher la recette dans l’état du composant sur la page en ajoutant les lignes en surbrillance :
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
const Recipe = () => {
const params = useParams();
const navigate = useNavigate();
const [recipe, setRecipe] = useState({ ingredients: "" });
useEffect(() => {
const url = `/api/v1/show/${params.id}`;
fetch(url)
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then((response) => setRecipe(response))
.catch(() => navigate("/recipes"));
}, [params.id]);
const addHtmlEntities = (str) => {
return String(str).replace(/</g, "<").replace(/>/g, ">");
};
const ingredientList = () => {
let ingredientList = "No ingredients available";
if (recipe.ingredients.length > 0) {
ingredientList = recipe.ingredients
.split(",")
.map((ingredient, index) => (
<li key={index} className="list-group-item">
{ingredient}
</li>
));
}
return ingredientList;
};
const recipeInstruction = addHtmlEntities(recipe.instruction);
return (
<div className="">
<div className="hero position-relative d-flex align-items-center justify-content-center">
<img
src={recipe.image}
alt={`${recipe.name} image`}
className="img-fluid position-absolute"
/>
<div className="overlay bg-dark position-absolute" />
<h1 className="display-4 position-relative text-white">
{recipe.name}
</h1>
</div>
<div className="container py-5">
<div className="row">
<div className="col-sm-12 col-lg-3">
<ul className="list-group">
<h5 className="mb-2">Ingredients</h5>
{ingredientList()}
</ul>
</div>
<div className="col-sm-12 col-lg-7">
<h5 className="mb-2">Preparation Instructions</h5>
<div
dangerouslySetInnerHTML={{
__html: `${recipeInstruction}`,
}}
/>
</div>
<div className="col-sm-12 col-lg-2">
<button
type="button"
className="btn btn-danger"
>
Delete Recipe
</button>
</div>
</div>
<Link to="/recipes" className="btn btn-link">
Back to recipes
</Link>
</div>
</div>
);
};
export default Recipe;
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 :
- nano app/javascript/routes/index.jsx
Ajoutez les lignes suivantes surlignées dans le fichier :
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
:
- 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 :
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 :
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 <
), respectivement. Pour ce faire, ajoutez les lignes surlignées au composant NewRecipe
:
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
const NewRecipe = () => {
const navigate = useNavigate();
const [name, setName] = useState("");
const [ingredients, setIngredients] = useState("");
const [instruction, setInstruction] = useState("");
const stripHtmlEntities = (str) => {
return String(str)
.replace(/\n/g, "<br> <br>")
.replace(/</g, "<")
.replace(/>/g, ">");
};
};
export default NewRecipe;
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:
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
const NewRecipe = () => {
const navigate = useNavigate();
const [name, setName] = useState("");
const [ingredients, setIngredients] = useState("");
const [instruction, setInstruction] = useState("");
const stripHtmlEntities = (str) => {
return String(str)
.replace(/\n/g, "<br> <br>")
.replace(/</g, "<")
.replace(/>/g, ">");
};
const onChange = (event, setFunction) => {
setFunction(event.target.value);
};
const onSubmit = (event) => {
event.preventDefault();
const url = "/api/v1/recipes/create";
if (name.length == 0 || ingredients.length == 0 || instruction.length == 0)
return;
const body = {
name,
ingredients,
instruction: stripHtmlEntities(instruction),
};
const token = document.querySelector('meta[name="csrf-token"]').content;
fetch(url, {
method: "POST",
headers: {
"X-CSRF-Token": token,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
})
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then((response) => navigate(`/recipe/${response.id}`))
.catch((error) => console.log(error.message));
};
};
export default NewRecipe;
La 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 :
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
const NewRecipe = () => {
const navigate = useNavigate();
const [name, setName] = useState("");
const [ingredients, setIngredients] = useState("");
const [instruction, setInstruction] = useState("");
const stripHtmlEntities = (str) => {
return String(str)
.replace(/\n/g, "<br> <br>")
.replace(/</g, "<")
.replace(/>/g, ">");
};
const onChange = (event, setFunction) => {
setFunction(event.target.value);
};
const onSubmit = (event) => {
event.preventDefault();
const url = "/api/v1/recipes/create";
if (name.length == 0 || ingredients.length == 0 || instruction.length == 0)
return;
const body = {
name,
ingredients,
instruction: stripHtmlEntities(instruction),
};
const token = document.querySelector('meta[name="csrf-token"]').content;
fetch(url, {
method: "POST",
headers: {
"X-CSRF-Token": token,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
})
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then((response) => navigate(`/recipe/${response.id}`))
.catch((error) => console.log(error.message));
};
return (
<div className="container mt-5">
<div className="row">
<div className="col-sm-12 col-lg-6 offset-lg-3">
<h1 className="font-weight-normal mb-5">
Add a new recipe to our awesome recipe collection.
</h1>
<form onSubmit={onSubmit}>
<div className="form-group">
<label htmlFor="recipeName">Recipe name</label>
<input
type="text"
name="name"
id="recipeName"
className="form-control"
required
onChange={(event) => onChange(event, setName)}
/>
</div>
<div className="form-group">
<label htmlFor="recipeIngredients">Ingredients</label>
<input
type="text"
name="ingredients"
id="recipeIngredients"
className="form-control"
required
onChange={(event) => onChange(event, setIngredients)}
/>
<small id="ingredientsHelp" className="form-text text-muted">
Separate each ingredient with a comma.
</small>
</div>
<label htmlFor="instruction">Preparation Instructions</label>
<textarea
className="form-control"
id="instruction"
name="instruction"
rows="5"
required
onChange={(event) => onChange(event, setInstruction)}
/>
<button type="submit" className="btn custom-button mt-3">
Create Recipe
</button>
<Link to="/recipes" className="btn btn-link mt-3">
Back to recipes
</Link>
</form>
</div>
</div>
</div>
);
};
export default NewRecipe;
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 :
- nano app/javascript/routes/index.jsx
Mettez à jour votre fichier de route pour inclure ces lignes en surbrillance :
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:
- nano app/javascript/components/Recipe.jsx
Dans le composant Recipe
, ajoutez une fonction deleteRecipe
avec les lignes surlignées:
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
const Recipe = () => {
const params = useParams();
const navigate = useNavigate();
const [recipe, setRecipe] = useState({ ingredients: "" });
useEffect(() => {
const url = `/api/v1/show/${params.id}`;
fetch(url)
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then((response) => setRecipe(response))
.catch(() => navigate("/recipes"));
}, [params.id]);
const addHtmlEntities = (str) => {
return String(str).replace(/</g, "<").replace(/>/g, ">");
};
const deleteRecipe = () => {
const url = `/api/v1/destroy/${params.id}`;
const token = document.querySelector('meta[name="csrf-token"]').content;
fetch(url, {
method: "DELETE",
headers: {
"X-CSRF-Token": token,
"Content-Type": "application/json",
},
})
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then(() => navigate("/recipes"))
.catch((error) => console.log(error.message));
};
const ingredientList = () => {
let ingredientList = "No ingredients available";
if (recipe.ingredients.length > 0) {
ingredientList = recipe.ingredients
.split(",")
.map((ingredient, index) => (
<li key={index} className="list-group-item">
{ingredient}
</li>
));
}
return ingredientList;
};
const recipeInstruction = addHtmlEntities(recipe.instruction);
return (
<div className="">
...
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:
...
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:
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
const Recipe = () => {
const params = useParams();
const navigate = useNavigate();
const [recipe, setRecipe] = useState({ ingredients: "" });
useEffect(() => {
const url = `/api/v1/show/${params.id}`;
fetch(url)
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then((response) => setRecipe(response))
.catch(() => navigate("/recipes"));
}, [params.id]);
const addHtmlEntities = (str) => {
return String(str).replace(/</g, "<").replace(/>/g, ">");
};
const deleteRecipe = () => {
const url = `/api/v1/destroy/${params.id}`;
const token = document.querySelector('meta[name="csrf-token"]').content;
fetch(url, {
method: "DELETE",
headers: {
"X-CSRF-Token": token,
"Content-Type": "application/json",
},
})
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then(() => navigate("/recipes"))
.catch((error) => console.log(error.message));
};
const ingredientList = () => {
let ingredientList = "No ingredients available";
if (recipe.ingredients.length > 0) {
ingredientList = recipe.ingredients
.split(",")
.map((ingredient, index) => (
<li key={index} className="list-group-item">
{ingredient}
</li>
));
}
return ingredientList;
};
const recipeInstruction = addHtmlEntities(recipe.instruction);
return (
<div className="">
<div className="hero position-relative d-flex align-items-center justify-content-center">
<img
src={recipe.image}
alt={`${recipe.name} image`}
className="img-fluid position-absolute"
/>
<div className="overlay bg-dark position-absolute" />
<h1 className="display-4 position-relative text-white">
{recipe.name}
</h1>
</div>
<div className="container py-5">
<div className="row">
<div className="col-sm-12 col-lg-3">
<ul className="list-group">
<h5 className="mb-2">Ingredients</h5>
{ingredientList()}
</ul>
</div>
<div className="col-sm-12 col-lg-7">
<h5 className="mb-2">Preparation Instructions</h5>
<div
dangerouslySetInnerHTML={{
__html: `${recipeInstruction}`,
}}
/>
</div>
<div className="col-sm-12 col-lg-2">
<button
type="button"
className="btn btn-danger"
onClick={deleteRecipe}
>
Delete Recipe
</button>
</div>
</div>
<Link to="/recipes" className="btn btn-link">
Back to recipes
</Link>
</div>
</div>
);
};
export default Recipe;
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.