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á!