par Mourad Aouinat, ingénieur logiciel chez 4D Maroc
Dans un précédent billet de blog, nous avons vu comment il est facile de mettre en place une API REST en utilisant 4D. Dans ce billet de blog, nous allons exploiter la puissante API REST de 4D en combinaison avec React pour construire une application To-Do qui comprend des fonctionnalités pour ouvrir les todos, en créer de nouveaux, modifier les existants, et des fonctionnalités pour la modification et la suppression en masse.
Application complète : Application TODO utilisant ReactJS et 4D REST API
Configuration de l’application ReactJS
Dans ce tutoriel, nous allons construire une application Todo simple et belle.
Ce tutoriel est la suite du tutoriel sur le serveur 4D REST. Il suppose que vous avez de l’expérience avec ReactJS, et que Node.js 12.13.0 ou plus est installé sur votre ordinateur.
Tout d’abord, collez cette commande dans votre terminal pour cloner les fichiers du projet :
git clone https://github.com/mouradaouinat/todo-fd.git
Après avoir cloné le projet, changez de répertoire pour aller à la racine du projet :
cd todoapp
Pour suivre les étapes de ce tutoriel, changez de branche vers les fichiers de démarrage :
git checkout starter
Si vous voulez passer à l’étape suivante et aller directement à l’application finie, passez à la branche complète:
git checkout complete
Installez les dépendances :
npm install
Et ensuite, démarrez le projet en exécutant :
npm start
Voici ce que vous allez voir :
Configuration du serveur proxy
Pour vous connecter à votre serveur de repos 4D, assurez-vous de configurer le serveur de développement de l’application avec la même URL d’API de repos que celle que vous avez configurée sur 4D:
"/rest":{
target: ADDRESS:PORT,
secure: false,
},
// craco.config.js module.exports = { devServer: { proxy: { "/rest": { target: "http://127.0.0.1:4000", secure: false, }, }, }, style: { postcss: { plugins: [require("tailwindcss"), require("autoprefixer")], }, }, };
Connexion de l’application à l’API REST 4D
ÉTAPE 1 : Obtention de Todos
Tout d’abord, nous nous assurons que le serveur 4D est opérationnel, puis nous récupérons les données initiales des todos à l’aide de la méthode JavaScript intégrée.
Pour obtenir les todos au format Array au lieu de l’objet JSON, il suffit d’ajouter $asArray à votre requête REST (par exemple, $asArray=true).
// src/components/Todos.tsx import { useEffect } from "react"; import { useTodos } from "../Provider"; import TodoItem from "./TodoItem"; const Todos: React.FC = () => { const { todos, setTodos } = useTodos(); useEffect(() => { fetch("/rest/Tasks/?$asArray=true") .then((resp) => resp.json()) .then((todos) => setTodos(todos)) .catch(console.error); }, [setTodos]); return ( <ul className="section-list"> {todos.map((todo) => ( <TodoItem todo={todo} key={todo.id} /> ))} </ul> ); }; export default Todos;
ÉTAPE 2 : Ajout d’un Todo
Pour ajouter un nouveau Todo, nous envoyons une requête POST à /rest/Tasks/?$method=update. La partie $method=update vous permet de mettre à jour et/ou de créer une ou plusieurs entités en une seule requête POST.
La réponse est le todo nouvellement créé avec des propriétés supplémentaires, __KEY, et __STAMP dont nous allons avoir besoin pour mettre à jour chaque todo :
// src/components/AddTodo.tsx import React, { useState } from "react"; import { HiOutlineChevronDown } from "react-icons/hi"; import { useTodos } from "../Provider"; const AddTodo: React.FC = () => { const { todos, setTodos } = useTodos(); const [input, setInput] = useState(""); const allChecked = todos.every((todoItem) => todoItem.completed); function handleAddTodo(e: React.KeyboardEvent) { if (e.key === "Enter" && input) { const newTodo = { title: input, completed: false, }; fetch("/rest/Tasks/?$method=update", { method: "POST", body: JSON.stringify(newTodo), }) .then((res) => res.json()) .then((newTodo) => { setTodos((state) => [newTodo, ...state]); setInput(""); }) .catch(console.error); } } function handleChange({ target: { value }, }: React.ChangeEvent) { setInput(value); } return ( <div className="todo-prompt"> <div className="todo-prompt__container"> {todos.length ? ( < button className="todo-prompt__toggle" onClick={() => { setTodos((state) => { if (allChecked) { return state.map((item) => ({ ...item, completed: false, })); } else { return state.map((item) => ({ ...item, completed: true, })); } }); }} > <HiOutlineChevronDown className="todo-prompt__toggle-icon" /> </button> ) : null} </div> <input type="text" placeholder="What needs to be done?" className="todo-prompt__input" value={input} onChange={handleChange} onKeyDown={handleAddTodo} /> </div> ); }; export default AddTodo;
ÉTAPE 3 : mise à jour d’un todo
Pour mettre à jour une entité, vous devez passer les paramètres __KEY et __STAMP dans l’objet ainsi que les attributs modifiés. Si ces deux paramètres sont manquants, une entité sera ajoutée avec les valeurs de l’objet que vous envoyez dans le corps de votre POST.
Dans cet exemple, nous passons implicitement les paramètres __Key et __STAMP en utilisant l’opérateur JavaScript spread :
// src/components/TodoItem.tsx import { useState } from "react"; import DeleteBtn from "./DeleteBtn"; import { useTodos } from "../Provider"; import { ITodo } from "../interfaces"; const TodoItem: React.FC<{ todo: ITodo }> = ({ todo }) => { const { setTodos } = useTodos(); const [isEditing, setIsEditing] = useState(false); const [inputValue, setInputValue] = useState(todo.title); function handleCompleted() { fetch("/rest/Tasks/?$method=update", { method: "POST", body: JSON.stringify({ ...todo, completed: !todo.completed, }), }) .then((res) => res.json()) .then(({ __STATUS, uri, ...rest }) => { if (__STATUS.success) { setTodos((todos) => { return todos.map((item) => { if (todo.id === item.id) { return rest; } return item; }); }); } }) .catch(console.error); } function updateTodoTitle() { fetch("/rest/Tasks/?$method=update", { method: "POST", body: JSON.stringify({ ...todo, title: inputValue, }), }) .then((res) => res.json()) .then(({ __STATUS, uri, ...rest }) => { if (__STATUS.success) { setTodos((todos) => { return todos.map((item) => { if (todo.id === item.id) { return rest; } return item; }); }); } }) .catch(console.error); setIsEditing(false); } function handleKeyDown(e: React.KeyboardEvent) { if (e.key === "Escape") { setInputValue(todo.title); setIsEditing(false); } if (e.key === "Enter") { updateTodoTitle(); } } return ( <li className="todo-item" onDoubleClick={() => { if (!todo.completed) { setIsEditing(true); } }} > {isEditing ? ( <input className="todo-item__input" autoFocus value={inputValue} onChange={({ target: { value } }) => { setInputValue(value); }} onBlur={updateTodoTitle} onKeyDown={handleKeyDown} /> ) : ( <div className="todo-item__container"> <div className="todo-item__devider"> <input type="checkbox" className="todo-item__checkbox" checked={todo.completed} onChange={handleCompleted} /> </div> <label className={`todo-item__title ${ todo.completed ? "todo-item__title--completed" : "todo-item__title--active" }`} > {todo.title} </label> <div className="todo-item__delete-btn group-hover:flex"> <DeleteBtn id={todo.id} /> </div> </div> )} </li> ); }; export default TodoItem;
ÉTAPE 4 : Suppression d’un todo
Dans notre application, un todo est une entité spécifique de la classe de données Tasks. Pour supprimer un todo, nous allons envoyer une requête POST avec le paramètre de requête ?$method=delete et passer l’id du todo comme suit : id /rest/DataClass(id)/?$method=delete
// src/components/DeleteBtn.tsx import React from "react"; import { ImCross } from "react-icons/im"; import { useTodos } from "../Provider"; const DeleteBtn: React.FC<{ id: number }> = ({ id }) => { const { setTodos } = useTodos(); return ( <button onClick={() => { fetch(`/rest/Tasks(${id})/?$method=delete`, { method: "POST", }) .then((res) => { if (res.ok) { setTodos((todos) => todos.filter((todo) => todo.id !== id)); } }) .catch(console.error); }} > <ImCross className="todo-item__delete-icon" /> </button> ); }; export default DeleteBtn;
ÉTAPE 5 : Mise à jour en masse
La mise à jour de plusieurs entités est identique à la mise à jour d’une seule entité, mais au lieu de transmettre un todo, nous allons transmettre un tableau de todos dans le corps de la requête POST :
// src/components/AddTodo.tsx import React, { useState } from "react"; import { HiOutlineChevronDown } from "react-icons/hi"; import { useTodos } from "../Provider"; const AddTodo: React.FC = () => { const { todos, setTodos } = useTodos(); const [input, setInput] = useState(""); const allChecked = todos.every((todoItem) => todoItem.completed); function handleAddTodo(e: React.KeyboardEvent) { if (e.key === "Enter" && input) { const newTodo = { title: input, completed: false, }; fetch("/rest/Tasks/?$method=update", { method: "POST", body: JSON.stringify(newTodo), }) .then((res) => res.json()) .then((newTodo) => { setTodos((state) => [newTodo, ...state]); setInput(""); }) .catch(console.error); } } function handleChange({ target: { value }, }: React.ChangeEvent) { setInput(value); } function toggleAllTodosCompleted() { fetch("/rest/Tasks/?$method=update", { method: "POST", body: JSON.stringify( todos.map((todo) => ({ ...todo, completed: allChecked ? false : true })) ), }) .then((res) => { if (res.ok) { fetch("/rest/Tasks/?$asArray=true") .then((resp) => resp.json()) .then((todos) => setTodos(todos)) .catch(console.error); } }) .catch(console.error); } return ( <div className="todo-prompt"> <div className="todo-prompt__container"> {todos.length ? ( <button className="todo-prompt__toggle" onClick={toggleAllTodosCompleted} > <HiOutlineChevronDown className="todo-prompt__toggle-icon" /> </button> ) : null} </div> <input type="text" placeholder="What needs to be done?" className="todo-prompt__input" value={input} onChange={handleChange} onKeyDown={handleAddTodo} /> </div> ); }; export default AddTodo;
ÉTAPE 6 : Suppression en masse
Nous pouvons également supprimer plusieurs todos avec une seule requête POST en utilisant le paramètre de requête $filter. Les lignes ci-dessous suppriment tous les todos terminés :
/rest/Tasks/?$filter= »completed=true »&$method=delete
// src/components/Footer.tsx import { useTodos } from "../Provider"; const Footer: React.FC = () => { const { todos, setTodos } = useTodos(); function handleDeleteCompleted() { fetch('/rest/Tasks/?$filter="completed=true"&$method=delete', { method: "POST", }) .then((res) => { if (res.ok) { setTodos((state) => { return state.filter((item) => !item.completed); }); } }) .catch(console.error); } return todos.length ? ( <div className="app-footer"> <span>{todos.filter((item) => !item.completed).length} items left</span> <div className="app-footer__action-box"> {todos.some((item) => item.completed) ? ( <button className="app-footer__clear-btn" onClick={handleDeleteCompleted} > Clear Completed </button> ) : null} </div> </div> ) : null; }; export default Footer;
À emporter
Comme nous l’avons vu dans cet article, le serveur REST de 4D est livré prêt à l’emploi, avec de nombreuses fonctionnalités permettant d’être rapidement opérationnel lors du développement d’applications. N’hésitez pas à poser toutes vos questions sur le forum 4D !