4D REST API + ReactJS

Automaticky přeloženo z Deepl

Mourad Aouinat, softwarový inženýr ve společnosti 4D Morocco

V předchozím příspěvku na blogu jsme si ukázali, jak snadno nastavit rozhraní REST API pomocí 4D. V tomto blogovém příspěvku využijeme výkonné rozhraní 4D REST API v kombinaci s Reactem k vytvoření aplikace To-Do, která obsahuje funkce pro otevírání todos, vytváření nových todos, úpravu stávajících todos a funkce pro hromadné úpravy a hromadné mazání.

Celá aplikace: Aplikace TODO s využitím ReactJS a 4D REST API

Nastavení aplikace ReactJS

V tomto tutoriálu vytvoříme jednoduchou a krásnou aplikaci Todo.

Tento tutoriál navazuje na tutoriál o serveru 4D REST. Předpokládá, že máte zkušenosti s ReactJS a v počítači máte nainstalovaný Node.js 12.13.0 nebo vyšší.

Nejprve vložte do terminálu tento příkaz, abyste naklonovali soubory projektu:

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

Po naklonování projektu změňte adresáře na kořenový adresář projektu:

cd todoapp

Chcete-li postupovat podle kroků tohoto návodu, přepněte větev na startovací soubory:

git checkout starter

Pokud chcete přeskočit a přejít rovnou k hotové aplikaci, přepněte se na kompletní větev:

git checkout complete

Nainstalujte závislosti:

npm install

A poté spusťte projekt spuštěním:

npm start

Tohle uvidíte:

Konfigurace proxy serveru

Abyste se mohli připojit k rest serveru 4D, nezapomeňte nakonfigurovat dev server aplikace na stejnou adresu URL rest API, jakou jste nakonfigurovali na 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")],
    },
  },
};

Připojení aplikace k rozhraní 4D REST API

KROK 1: Získání Todos

Nejprve se ujistíme, že je server 4D spuštěn, a poté načteme počáteční data todo pomocí vestavěné metody JavaScript fetch.

Chcete-li získat todos ve formátu pole místo objektu JSON, stačí do požadavku REST přidat $asArray (např. $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;

KROK 2: Přidání položky Todo

Chceme-li přidat nové Todo, odešleme požadavek POST na adresu /rest/Tasks/?$method=update. Část $method=update umožňuje aktualizovat a/nebo vytvořit jednu nebo více entit v jediném požadavku POST.

Odpovědí je nově vytvořené todo s dalšími vlastnostmi __KEY a __STAMP, které budeme potřebovat k aktualizaci jednotlivých 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;

KROK 3: Aktualizace todo

Chcete-li aktualizovat entitu, musíte v objektu předat parametry __KEY a __STAMP spolu se všemi upravenými atributy. Pokud oba tyto parametry chybí, bude přidána entita s hodnotami v objektu, který odešlete v těle POST.

V tomto příkladu implicitně předáváme parametry __Key a __STAMP pomocí operátoru 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;

KROK 4: Odstranění položky Todo

V naší aplikaci je todo specifická entita v datové třídě Tasks. Pro odstranění todo odešleme požadavek POST s parametrem dotazu ?$method=delete a předáme id todo následujícím způsobem: 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;

KROK 5: Hromadná aktualizace

Aktualizace více entit je stejná jako aktualizace jedné entity, ale místo jednoho todo předáme do těla požadavku POST pole 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);
  }

  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;

KROK 6: Hromadné mazání

Pomocí parametru dotazu $filter můžeme také odstranit více todos pomocí jediného požadavku POST. Níže uvedené řádky odstraní všechny dokončené todos:

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

Odnést

Jak jsme viděli v tomto článku, 4D REST Server je po vybalení z krabice vybaven funkcemi, které umožňují rychlé zprovoznění při vývoji aplikací. Neváhejte se na fóru 4D zeptat na cokoli, co vás zajímá!

Mourad Aouinat
Mourad Aouinat nastoupil do společnosti 4D jako full stack vývojář v červnu 2020 a má na starosti vytváření vzhledu webových aplikací/uživatelských rozhraní a shromažďování a upřesňování specifikací a požadavků na základě technických potřeb. Mourad je vývojář samouk s ekonomickým a finančním vzděláním, který je nadšencem do open-source softwaru a uživatelského zážitku.