por Mourad Aouinat, Engenheiro de Software na 4D Marrocos
Num post de blog anterior, vimos como é fácil configurar um REST API usando 4D. Neste post de blogue, vamos aproveitar a poderosa API REST 4D em combinação com React para construir uma aplicação To-Do que inclui características para abrir todos, criar novas, modificar as existentes, e características para modificação e eliminação em massa.
Aplicação completa: Aplicação TODO utilizando o ReactJS e o 4D REST API
Criação da aplicação ReactJS
Neste tutorial, construiremos uma simples e bela aplicação Todo.
Este tutorial é o seguimento do tutorial do servidor 4D REST. Assume que tem experiência com o ReactJS, e tem o Node.js 12.13.0 ou superior instalado no seu computador.
Em primeiro lugar, cole este comando no seu terminal para clonar os ficheiros do projecto:
git clone https://github.com/mouradaouinat/todo-fd.git
Depois de clonar o projecto, mude os directórios para a raiz do projecto:
cd todoapp
Para seguir com os passos deste tutorial, mude o ramo para os ficheiros iniciais:
git checkout starter
Se quiser saltar em frente e ir directamente para a aplicação terminada, mude para o ramo completo:
git checkout complete
Instalar as dependências:
npm install
E depois inicie o projecto, executando:
npm start
Isto é o que vai ver:
Configurar o servidor proxy
Para se ligar ao seu servidor de repouso 4D, certifique-se de configurar o servidor Dev da aplicação para o mesmo URL da API de repouso que configurou no 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")], }, }, };
Ligar a aplicação ao API REST 4D
PASSO 1: Conseguir Todos
Primeiro, certificamo-nos de que o servidor 4D está instalado e a funcionar, depois vamos buscar todos os dados iniciais usando o método fetch incorporado de JavaScript.
Para obter todos no formato Array em vez do objecto JSON, basta adicionar $asArray ao seu pedido REST (por exemplo, $asArray=verdadeiro).
// 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;
PASSO 2: Acrescentar um Todo
Para adicionar uma nova Todo, enviamos um pedido de POST para /rest/Tasks/?$method=update. A parte $method=update permite-lhe actualizar e/ou criar uma ou mais entidades num único pedido de POST.
A resposta é o novo todo com propriedades adicionais, __KEY, e __STAMP que vamos precisar para actualizar cada 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;
PASSO 3: Actualização de um Todo
Para actualizar uma entidade, é necessário passar os parâmetros __KEY e __STAMP no objecto, juntamente com quaisquer atributos modificados. Se estes dois parâmetros estiverem em falta, será acrescentada uma entidade com os valores no objecto que enviar no corpo do seu POST.
Estamos implicitamente a passar a __Key e __STAMP utilizando o operador de propagação JavaScript neste exemplo:
// 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;
PASSO 4: Eliminação de um Todo
Na nossa aplicação, um todo é uma Entidade específica no Dataclass das Tarefas. Para apagar um todo, vamos enviar um pedido POST com a consulta param ?$method=delete e passar o id todo da seguinte forma: 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;
PASSO 5: Actualização a granel
Actualizar várias entidades é o mesmo que actualizar uma única entidade, mas em vez de passarmos um todo, vamos passar um conjunto de todos para o corpo do pedido 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;
PASSO 6: Eliminação em massa
Também podemos apagar vários todos com um único pedido POST usando o parâmetro de consulta $filter. As linhas abaixo eliminam todos os todos completados:
/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;
Takeaway
Como vimos neste artigo, o Servidor 4D REST sai da caixa cheio de funcionalidades para se pôr rapidamente em funcionamento ao desenvolver aplicações. Sinta-se à vontade para fazer quaisquer perguntas que possa ter sobre o fórum 4D!