API REST 4D + ReactJS

Traduit automatiquement de Deepl

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 !

Mourad Aouinat
Mourad Aouinat a rejoint 4D en tant que développeur full stack en juin 2020. Il est en charge de la création de la mise en page des applications web/interfaces utilisateur et de la collecte et de l'affinage des spécifications et des exigences en fonction des besoins techniques. Mourad est un développeur autodidacte avec une formation en économie et finance, passionné par les logiciels open-source et l'expérience utilisateur.