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