Neste tutorial vamos criar um ToDo app usando React no frontend e NetCore no backend.
Este app possui algumas regras:
O botão Add só é habilitado quando algum valor é inputado
Quando o usuario clica no Checkbox a coluna Concluded at passa a exibir a data que a tarefa foi concluída
Quando o botão Edit é acionado o botão Add é escondido e o botão Save Changes é exibido para salvar as altereçãoes
Qualquer edição muda o valor da coluna Last modified
Este deverá ser o resultado ao final deste tutorial
Hands On
Backend NetCore
No Visual Studio, crie o projeto usando o template React
Na Solution, crie a classe ToDoModel.cs Esta classe é a representação do nosso modelo de dados
using System;
namespace ToDoApp
{
public class ToDoModel
{
public Guid Id { get; set; }
public string Name { get; set; }
public bool IsDone { get; set; }
public DateTime? CreatedAt { get; set; }
public DateTime? EditedAt { get; set; }
public DateTime? DateConclusion { get; set; }
}
}
Dentro da pasta Controllers, crie o arquivo ToDoController.cs Este controller irá responder os requests feitos pelo React
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace ToDoApp.Controllers
{
[ApiController]
[Route("[controller]")]
public class ToDoController : ControllerBase
{
private static List<ToDoModel> Tasks = new List<ToDoModel>
{
new ToDoModel{
Id = Guid.Parse("4CA27A99-5E15-4BFF-A6D4-C295BF443E2D"),
Name = "Task 1",
IsDone = true,
CreatedAt = new DateTime(2020,05,20),
EditedAt = new DateTime(2020,05,21),
DateConclusion = new DateTime(2020,05,22)
},
new ToDoModel{
Id = Guid.Parse("03976BE1-D1E4-4690-807D-5D058E09E235"),
Name = "Task 2",
IsDone = false,
CreatedAt = DateTime.Now,
}
};
[HttpGet]
public List<ToDoModel> Get()
{
return Tasks;
}
[HttpPost]
public IActionResult Post(ToDoModel task)
{
task.Id = Guid.NewGuid();
task.CreatedAt = DateTime.Now;
Tasks.Add(task);
return Ok();
}
[HttpPut("{id}")]
public IActionResult Put(Guid id, ToDoModel task)
{
foreach (var item in Tasks)
{
if (item.Id == id)
{
item.Name = task.Name;
item.EditedAt = DateTime.Now;
}
}
return Ok();
}
[HttpPatch("{id}")]
public IActionResult Patch(Guid id)
{
foreach (var item in Tasks)
{
if (item.Id == id)
{
item.IsDone = !item.IsDone;
item.EditedAt = DateTime.Now;
if (item.IsDone)
{
item.DateConclusion = DateTime.Now;
}
else
item.DateConclusion = null;
}
}
return Ok();
}
[HttpDelete("{id}")]
public IActionResult Delete(Guid id)
{
var elementToRemove = Tasks.FirstOrDefault(f => f.Id == id);
Tasks.Remove(elementToRemove);
return Ok();
}
}
}
Com isso o nosso backend está pronto! Vamos agora construir a interface utilizando o React
Frontend React
Dentro de ClientApp/src/components crie uma nova pasta chamada todo e crie dois arquivos dentro desta mesma pasta ToDo.js e ToDoList.js
Veja graficamente a representação dos componentes
Perceba que ToDoList.js é um componente filho de ToDo.js
Agora vamos tornar o componente acessível. Altere o código do App.js (ClientApp/src/) Este é o arquivo onde definimos qual será a rota do component. Para este exemplo eu escolhi a rota raiz '/' mas sinta-se livra para colocar uma rota de sua escolha
import React, { Component } from 'react';
import { Route } from 'react-router';
import { Layout } from './components/Layout';
import { ToDo } from './components/todo/ToDo'
import './custom.css'
export default class App extends Component {
static displayName = App.name;
render () {
return (
<Layout>
<Route path='/' component={ToDo} />
</Layout>
);
}
}
Altere o código de NavMenu.js (ClientApp/src/components). Este componente, como o próprio nome já diz, é responsável por gerenciar a barra de menu da aplicação.
import React, { Component } from 'react';
import { Collapse, Container, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap';
import { Link } from 'react-router-dom';
import './NavMenu.css';
export class NavMenu extends Component {
static displayName = NavMenu.name;
constructor(props) {
super(props);
this.toggleNavbar = this.toggleNavbar.bind(this);
this.state = {
collapsed: true
};
}
toggleNavbar() {
this.setState({
collapsed: !this.state.collapsed
});
}
render() {
return (
<header>
<Navbar className="navbar-expand-sm navbar-toggleable-sm ng-white border-bottom box-shadow mb-3" light>
<Container>
<NavbarBrand tag={Link} to="/">NinjaDevSpace - ToDoApp</NavbarBrand>
<NavbarToggler onClick={this.toggleNavbar} className="mr-2" />
<Collapse className="d-sm-inline-flex flex-sm-row-reverse" isOpen={!this.state.collapsed} navbar>
<ul className="navbar-nav flex-grow">
<NavItem>
<NavLink tag={Link} className="text-dark" to="/">Tasks</NavLink>
</NavItem>
</ul>
</Collapse>
</Container>
</Navbar>
</header>
);
}
}
Coloque o seguinte código dentro de ToDo.js
import React, { useState, useEffect } from 'react';
import ToDoList from './ToDoList'
export const ToDo = () => {
const [tasks, setTasks] = useState([]);
useEffect(() => {
handleGetTasks();
}, []);
const getTasks = async () => {
const response = await fetch('todo');
const data = await response.json();
return data;
};
const handleGetTasks = async () => {
let tasks = await getTasks();
setTasks(tasks);
};
const renderToDoList = () => {
return <ToDoList
tasks={tasks}
/>
};
return (
<div>
{renderToDoList()}
</div>
);
};
Coloque o seguinte código dentro de ToDoList.js
import React from 'react';
export default (props) => {
return (
<div className="container">
<div className="row">
<div className="col-12">
<table className="table">
<thead className="thead-dark">
<tr>
<th></th>
<th>Task</th>
<th>Created at</th>
<th>Last modified</th>
<th>Concluded at</th>
<th></th>
</tr>
</thead>
<tbody>
{props.tasks.map(task =>
<tr key={task.id}>
<td>
<div className="form-check">
<input
checked={task.isDone}
className="form-check-input"
type="checkbox"
value={task.id} />
</div>
</td>
<td>{task.name}</td>
<td>{task.createdAt || ""}</td>
<td>{task.editedAt || ""}</td>
<td>{task.dateConclusion || ""}</td>
<td>
<button
type="button"
className="btn btn-outline-info mr-2"
>Edit</button>
<button
type="button"
className="btn btn-outline-danger"
>Delete</button>
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
</div>
);
}
Execute a aplicação através do Visual Studio ou VsCode. Você verá a seguinte interface:
Através da imagem acima podemos concluir que a API NetCore já está retornando dados corretamente para o React.
O próximo passo agora será criar um componente para resolver o problema de formatação da data. Um ótimo pacote para esta tarefa é o Moment.js. Abaixo alguns exemplos de como colocar o Moment.js na aplicação
npm install moment --save # npm
yarn add moment # Yarn
Install-Package Moment.js # NuGet
Após a instalação do Moment.js, crie o arquivo FormatDate.js dentro de ClientApp/src/components
import moment from 'moment'
/**
* Returns a Formated Date Time.
* @param {*} date
* @param {*} dateOnly "optional parameter to only retrieve the date without time."
*/
export default function (date, dateOnly = false) {
if (!date) return "";
if (dateOnly) {
return moment(date).format('DD/MM/YYYY');
}
return moment(date).format('DD/MM/YYYY hh:mm:ss');
}
Importe o componente FormatDate.js para dentro de ToDoList.js e use-o para formatar as datas retornadas pela API NetCore
ToDoList.js
import React from 'react';
import FormatDate from '../FormatDate';
export default (props) => {
return (
<div className="container">
<div className="row">
<div className="col-12">
<table className="table">
<thead className="thead-dark">
<tr>
<th></th>
<th>Task</th>
<th>Created at</th>
<th>Last modified</th>
<th>Concluded at</th>
<th></th>
</tr>
</thead>
<tbody>
{props.tasks.map(task =>
<tr key={task.id}>
<td>
<div className="form-check">
<input
checked={task.isDone}
className="form-check-input"
type="checkbox"
value={task.id} />
</div>
</td>
<td>{task.name}</td>
<td>{FormatDate(task.createdAt || "")}</td>
<td>{FormatDate(task.editedAt || "")}</td>
<td>{FormatDate(task.dateConclusion || "")}</td>
<td>
<button
type="button"
className="btn btn-outline-info mr-2"
>Edit</button>
<button
type="button"
className="btn btn-outline-danger"
>Delete</button>
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
</div>
);
}
Repare que as datas agora estão sendo exibidas corretamente
Vamos criar agora o fluxo para adicionar uma nova tarefa. Dentro de ToDo.js adicione o seguinte código
const renderCreateTask = () => {
return <div className="container mb-3">
<div className="row">
<div className="col-12">
<div className="card border-dark">
<div className="card-header">
Create Task
</div>
<div className="card-body">
<form onSubmit={handleSubmit}>
<div className="form-row">
<div className="form-group col-md-6">
<label>Name</label>
<input
type="text"
onChange={handleTaskNameField}
className="form-control"
name="name"
placeholder="eg.: Study"
value={name}
required />
</div>
</div>
<button
type="button"
className={isSaveButtonDisabled() ? "btn btn-secondary" : "btn btn-success"}
onClick={handleSubmit}
disabled={isSaveButtonDisabled()}
>
Add
</button>
</form>
</div>
</div>
</div>
</div>
</div>
};
O código acima irá precisar de algumas funções para de validação do fluxo de adição de uma nova tarefa. Adicione-as também ao arquivo ToDo.js
const saveTasks = async () => {
await fetch('todo', {
method: 'post',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
Name: name,
IsDone: false
})
});
};
const handleSubmit = async (event) => {
await saveTasks();
setName("");
await handleGetTasks();
};
const isSaveButtonDisabled = () => {
if (name)
return false
return true;
};
const handleTaskNameField = (event) => {
setName(event.target.value);
isSaveButtonDisabled();
};
Adicione a seguinte variável de estado ao arquivo ToDo.js
const [name, setName] = useState("");
Altere o retorno de ToDo.js para que o mesmo exiba o campo relacionado a Adição/Edição de uma tarefa
return (
<div>
{renderCreateTask()}
{renderToDoList()}
</div>
);
Neste momento você deverá ser capaz de criar uma nova tarefa na aplicação. Execute o app e faça os testes!
Vamos criar agora os códigos responsáveis por deletar uma tarefa. Para excluir um registro nós só precisamos do Id do mesmo. Então quando o usuário clicar no botão Delete nós vamos enviar enviar esse Id para API que irá fazer a exclusão
Adicione as seguintes funções a ToDo.js
const deleteTask = async (taskId) => {
await fetch('todo/' + taskId, {
method: 'delete',
headers: { 'Content-Type': 'application/json' }
});
};
const handleTaskDelete = async (taskId) => {
await deleteTask(taskId);
await handleGetTasks();
};
Ainda em ToDo.js altere a função renderToDoList. A propriedade deleteTask recebe a referência de handleTaskDelete que será acionada pelo compenente filho ToDoList.js assim que o usuário clicar no botão Delete
const renderToDoList = () => {
return <ToDoList
tasks={tasks}
deleteTask={handleTaskDelete}
/>
};
Vamos alterar ToDoList.js para que o mesmo possa chamar a função que irá deletar um registro.
const handleDelete = async (taskId) => {
await props.deleteTask(taskId);
}
Adicione a chamada do método acima ao botão Delete
onClick={() => handleDelete(task.id)}
Neste momento você deverá ser capaz de deletar uma tarefa na aplicação. Execute o app e faça os testes!
Para finalizar nosso CRUD vamos criar as funções responsáveis por atualizar uma tarefa. Deixei o fluxo de Update pro final porque ele será um pouco diferente. Teremos um update para quando o usuário clicar no Checkbox e outro para quando o usuário clicar em Edit
Quando A atualização referente ao Checkbox usará HttpPatch enquanto quaisquer outras atualizações irão utilizar HttpPut
Adicione os seguintes trechos de código em ToDo.js
const [taskToEdit, setTaskToEdit] = useState(null);
const updateTask = async (task) => {
await fetch('todo/' + taskToEdit.id, {
method: 'put',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(taskToEdit)
});
};
const toggleTaskStatus = async (taskId) => {
await fetch('todo/' + taskId, {
method: 'patch',
headers: { 'Content-Type': 'application/json' }
});
};
const handleTaskUpdate = async () => {
await updateTask(taskToEdit);
setName("");
setTaskToEdit(null);
await handleGetTasks();
};
const handleTaskStatus = async (taskId) => {
await toggleTaskStatus(taskId);
await handleGetTasks();
};
Ainda em ToDo.js atualize a seguinte função
const handleTaskNameField = (event) => {
if (!event.target.value)
setTaskToEdit(null);
if (taskToEdit)
taskToEdit.name = event.target.value;
setName(event.target.value);
isSaveButtonDisabled();
};
Ainda em ToDo.js teremos que atualizar a função renderCreateTask
const renderCreateTask = () => {
return <div className="container mb-3">
<div className="row">
<div className="col-12">
<div className="card border-dark">
<div className="card-header">
Create Task
</div>
<div className="card-body">
<form onSubmit={handleSubmit}>
<div className="form-row">
<div className="form-group col-md-6">
<label>Name</label>
<input
type="text"
onChange={handleTaskNameField}
className="form-control"
name="name"
placeholder="eg.: Study"
value={name}
required />
</div>
</div>
<button
type="button"
className={isSaveButtonDisabled() ? "btn btn-secondary" : "btn btn-success"}
onClick={handleSubmit}
disabled={isSaveButtonDisabled()}
hidden={taskToEdit != null}
>
Add
</button>
<button
type="button"
className="btn btn-success"
onClick={handleTaskUpdate}
disabled={taskToEdit == null}
hidden={taskToEdit == null}
>
Save Changes
</button>
</form>
</div>
</div>
</div>
</div>
</div>
};
Ainda em ToDo.js teremos que atualizar a função renderToDoList
const renderToDoList = () => {
return <ToDoList
tasks={tasks}
toggleTaskStatus={handleTaskStatus}
updateTask={setEditTaskMode}
deleteTask={handleTaskDelete}
/>
};
Adicione as seguintes funções a ToDoList.js
const handleToggleStatusTask = async (event) => {
await props.toggleTaskStatus(event.target.value);
};
const handleUpdateTask = async (task) => {
await props.updateTask(JSON.parse(task));
}
Altere o Html referente ao Checkbox e ao botão Edit
Checkbox
<input
checked={task.isDone}
onChange={handleToggleStatusTask}
className="form-check-input"
type="checkbox"
value={task.id} />
Botão Edit
<button
type="button"
className="btn btn-outline-info mr-2"
onClick={() => handleUpdateTask(JSON.stringify(task))}
>Edit</button>
Neste momento nosso ToDo app está PRONTO! Execute o código e veja o resultado.
O código deste tutorial encontra-se no nosso GitHub
Muito top!