4D REST API + ReactJS

von Mourad Aouinat, Software Ingenieur bei 4D Marokko

In einem früheren Blogbeitrag haben wir gesehen, wie einfach es ist, eine REST API mit 4D einzurichten. In diesem Blog-Beitrag werden wir die leistungsstarke 4D REST API in Kombination mit React nutzen, um eine To-Do App zu erstellen, die Funktionen zum Öffnen von To-Dos, zum Erstellen neuer To-Dos, zum Ändern bestehender To-Dos sowie Funktionen für Massenänderungen und Massenlöschungen enthält.

Vollständige Anwendung: TODO-Anwendung mit ReactJS und 4D REST API

Einrichten der ReactJS-Anwendung

In diesem Tutorial werden wir eine einfache, schöne Todo-App erstellen.

Dieses Tutorial ist die Fortsetzung des 4D REST Server Tutorials. Es setzt voraus, dass Sie Erfahrung mit ReactJS haben und Node.js 12.13.0 oder höher auf Ihrem Computer installiert haben.

Fügen Sie zunächst diesen Befehl in Ihr Terminal ein, um die Projektdateien zu klonen:

git clone https://github.com/mouradaouinat/todo-fd.git

Nachdem Sie das Projekt geklont haben, wechseln Sie die Verzeichnisse zum Stammverzeichnis des Projekts:

cd todoapp

Um den Schritten dieses Tutorials zu folgen, wechseln Sie zu den Startdateien:

git checkout starter

Wenn Sie direkt zur fertigen Anwendung übergehen möchten, wechseln Sie zum vollständigen Zweig:

git checkout complete

Installieren Sie die Abhängigkeiten:

npm install

Und starten Sie dann das Projekt durch Ausführen:

npm start

Das werden Sie nun sehen:

Konfigurieren des Proxy-Servers

Um eine Verbindung zu Ihrem 4D Rest Server herzustellen, konfigurieren Sie den Dev Server der App auf die gleiche Rest API URL wie die, die Sie in 4D konfiguriert haben:

"/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")],
    },
  },
};

Verbinden der App mit der 4D REST API

SCHRITT 1: Todos abrufen

Zuerst stellen wir sicher, dass der 4D Server läuft, dann holen wir die ersten Todo-Daten mit der eingebauten JavaScript Abrufmethode.

Um die Todos im Array-Format anstelle des JSON-Objekts zu erhalten, müssen Sie nur $asArray zu Ihrer REST-Anfrage hinzufügen (z. B. $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;

SCHRITT 2: Hinzufügen eines Todos

Um ein neues Todo hinzuzufügen, senden wir eine POST-Anfrage an /rest/Tasks/?$method=update. Mit dem Teil $method=update können Sie eine oder mehrere Entitäten in einer einzigen POST-Anfrage aktualisieren und/oder erstellen.

Die Antwort ist das neu erstellte Todo mit zusätzlichen Eigenschaften, __KEY und __STAMP, die wir benötigen, um jedes Todo zu aktualisieren:

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

SCHRITT 3: Aktualisieren eines Todo

Um eine Entität zu aktualisieren, müssen Sie die Parameter __KEY und __STAMP zusammen mit allen geänderten Attributen an das Objekt übergeben. Wenn diese beiden Parameter fehlen, wird eine Entität mit den Werten des Objekts hinzugefügt, das Sie im Textkörper Ihrer POST senden.

In diesem Beispiel werden die Parameter __Key und __STAMP implizit mit dem JavaScript-Spread-Operator übergeben:

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

SCHRITT 4: Löschen eines ToDo

In unserer Anwendung ist ein Todo eine bestimmte Entität in der Datenklasse Tasks. Um ein Todo zu löschen, senden wir eine POST-Anfrage mit dem Query-Parameter ?$method=delete und übergeben die Todo-ID wie folgt: 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;

SCHRITT 5: Massenaktualisierung

Die Aktualisierung mehrerer Entitäten erfolgt auf die gleiche Weise wie die Aktualisierung einer einzelnen Entität, nur dass wir anstelle einer einzelnen Todo ein Array von Todos an den Körper der POST-Anfrage übergeben:

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

SCHRITT 6: Massenlöschung

Wir können auch mehrere todos mit einer einzigen POST-Anfrage löschen, indem wir den Abfrageparameter $filter verwenden. Die folgenden Zeilen löschen alle erledigten Aufgaben:

/rest/Aufgaben/?$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;

Zum Mitnehmen

Wie wir in diesem Artikel gesehen haben, ist der 4D REST Server von Haus aus vollgepackt mit Funktionen, mit denen Sie bei der Entwicklung von Anwendungen schnell loslegen können. Wenn Sie Fragen haben, können Sie diese gerne im 4D Forum stellen!

Mourad Aouinat
Mourad Aouinat ist seit Juni 2020 bei 4D als Full-Stack-Entwickler tätig. Er ist verantwortlich für die Erstellung von Layouts und Benutzeroberflächen für Webanwendungen sowie für die Erfassung und Verfeinerung von Spezifikationen und Anforderungen auf der Grundlage technischer Anforderungen. Mourad ist ein autodidaktischer Entwickler mit einem Hintergrund in Wirtschaft und Finanzen, der sich für Open-Source-Software und User Experience begeistert.