Skip to main content

📘 Cadastro de Pessoas com Ant Design

No capítulo anterior, conhecemos o Ant Design (AntD) e utilizamos alguns de seus componentes para construir uma interface de Login.

Agora avançaremos para uma aplicação mais completa: um cadastro de pessoas.

A aplicação permitirá cadastrar dois tipos de pessoa:

Pessoa
│
├── Pessoa Física (PF)
│
└── Pessoa Jurídica (PJ)

Além dos dados principais, o cadastro será organizado em diferentes partes:

Cadastro de Pessoa
│
├── Dados principais
│
├── Endereço
│
└── Telefones

Neste capítulo utilizaremos recursos importantes do Ant Design, como Form, Form.Item, Row, Col, Input, DatePicker, Select, Button, Space, List e message.

O principal avanço será a utilização do sistema de formulários e validação do Ant Design.

Objetivos do capítulo

Ao final deste capítulo, você deverá ser capaz de:

  • construir formulários utilizando Form e Form.Item;
  • criar regras de validação declarativas;
  • organizar formulários em componentes menores;
  • utilizar campos diferentes para Pessoa Física e Pessoa Jurídica;
  • construir layouts utilizando Row e Col;
  • utilizar campos aninhados em formulários;
  • adicionar elementos dinamicamente a uma lista;
  • apresentar feedback utilizando message;
  • complementar o Ant Design utilizando CSS.

Estrutura da aplicação

Para evitar a criação de um componente muito grande, dividiremos o cadastro em diferentes componentes.

A estrutura principal será:

src/
│
├── App.jsx
│
└── components/
└── cadastrapessoa/
├── PessoaForm.jsx
├── EnderecoForm.jsx
├── TelefoneList.jsx
└── pessoaform.css

Cada arquivo possuirá uma responsabilidade:

PessoaForm.jsx
│
├── formulário principal
├── dados da pessoa
├── seleção PF/PJ
└── integração dos demais componentes

EnderecoForm.jsx
│
└── dados de endereço

TelefoneList.jsx
│
└── gerenciamento dos telefones

pessoaform.css
│
└── ajustes visuais complementares

Essa organização aplica um conceito que já estudamos anteriormente: componentização.

Em vez de concentrarmos toda a aplicação em um único arquivo, distribuímos responsabilidades entre componentes menores.


Configurando o App.jsx

O componente App será responsável por carregar o formulário principal.

App.jsx​

import PessoaForm from "./components/cadastrapessoa/PessoaForm.jsx";

function App() {

return (
<>
<PessoaForm />
</>
);
}

export default App;

O fluxo inicial da aplicação será:

main.jsx
│
▼
App.jsx
│
▼
PessoaForm.jsx

PessoaForm será o componente principal do cadastro e concentrará a integração dos demais componentes.


Formulários com Ant Design

No capítulo anterior utilizamos campos isolados do Ant Design.

Agora utilizaremos:

<Form>

O componente Form permite organizar e controlar diferentes campos de um formulário.

Dentro dele utilizaremos:

<Form.Item>

Podemos visualizar:

Form
│
├── Form.Item
│ └── Input
│
├── Form.Item
│ └── DatePicker
│
├── Form.Item
│ └── Select
│
└── Form.Item
└── Button
Form.Item

Form.Item funciona como um container para um campo do formulário.

Ele pode definir informações como:

label
name
rules

Isso permite associar o campo a um nome e definir regras de validação.


Criando uma instância do formulário

Dentro de PessoaForm, utilizaremos:

const [form] = Form.useForm();

Essa instrução cria uma instância do formulário.

Podemos pensar da seguinte forma:

Form.useForm()
│
▼
form
│
▼
controle do formulário

Depois associamos essa instância ao componente:

<Form form={form}>

Isso permite acessar e manipular os dados do formulário programaticamente.


Pessoa Física e Pessoa Jurídica

Nosso cadastro trabalhará com dois tipos de pessoa.

Para controlar a opção selecionada utilizaremos:

const [tipoPessoa, setTipoPessoa] = useState("PF");

Inicialmente:

tipoPessoa = "PF"

Quando o usuário selecionar outra opção:

PF
│
│ usuário seleciona PJ
▼
onChangeTipo()
│
▼
setTipoPessoa("PJ")
│
▼
tipoPessoa = "PJ"

Assim, a interface poderá apresentar campos diferentes de acordo com o tipo selecionado.


Selecionando o tipo de pessoa

Utilizaremos:

<Form.Item
label="Tipo de Pessoa"
name="tipo"
initialValue="PF"
style={{ marginBottom: 10 }}
>

<Radio.Group onChange={onChangeTipo}>

<Radio value="PF">
Pessoa Física
</Radio>

<Radio value="PJ">
Pessoa Jurídica
</Radio>

</Radio.Group>

</Form.Item>

Temos dois componentes importantes:

Radio.Group
│
├── Radio PF
└── Radio PJ

A propriedade:

value="PF"

representa Pessoa Física.

Enquanto:

value="PJ"

representa Pessoa Jurídica.


Evento onChangeTipo

Podemos utilizar:

const onChangeTipo = (e) => {

const novoTipo = e.target.value;

setTipoPessoa(novoTipo);

form.setFieldValue(
"tipo",
novoTipo
);
};

O evento recebido contém a opção selecionada:

e.target.value
│
├── "PF"
│
└── "PJ"

O estado é atualizado por:

setTipoPessoa(novoTipo);

E o formulário também recebe o valor:

form.setFieldValue(
"tipo",
novoTipo
);

Criando campos com Form.Item

Vamos observar um campo simples:

<Form.Item
label="Nome"
name="nome"
rules={[
{
required: true,
message: "Informe o nome!"
}
]}
>

<Input
placeholder="Nome completo ou razão social"
/>

</Form.Item>

Temos:

Form.Item
│
├── label
├── name
├── rules
│
└── Input

label

A propriedade:

label="Nome"

define o rótulo apresentado para o campo.

Quando utilizamos:

<Form layout="vertical">

o rótulo aparece acima do campo.

Visualmente:

Nome
┌────────────────────────────┐
│ Nome completo... │
└────────────────────────────┘

name

A propriedade:

name="nome"

identifica o campo dentro do formulário.

Assim, quando o formulário for enviado, poderemos obter uma estrutura semelhante a:

values
│
├── tipo
├── nome
├── email
└── ...

Por exemplo:

values.nome

permite acessar o valor associado ao campo nome.


Validação com rules

Uma das principais vantagens de Form.Item é a possibilidade de declarar regras de validação.

Observe:

rules={[
{
required: true,
message: "Informe o nome!"
}
]}

A propriedade:

required: true

indica que o campo é obrigatório.

Enquanto:

message

define a mensagem apresentada caso a regra não seja atendida.

O fluxo será:

Usuário envia formulário
│
▼
AntD verifica rules
│
├── campo válido
│ │
│ ▼
│ continua
│
└── campo inválido
│
▼
exibe mensagem
Validação declarativa

Observe que não precisamos criar manualmente uma condição como:

if (nome === "") {
...
}

A regra é declarada diretamente no Form.Item.

Essa abordagem é chamada de validação declarativa.


Validando o e-mail

Podemos combinar diferentes regras:

<Form.Item
label="Email"
name="email"
rules={[
{
required: true,
message: "Informe o e-mail!"
},
{
type: "email",
message: "Formato de e-mail inválido!"
}
]}
>

<Input
placeholder="exemplo@email.com"
/>

</Form.Item>

Nesse exemplo temos duas validações:

Regra 1
│
└── campo obrigatório

Regra 2
│
└── formato de e-mail

Assim:

email vazio
│
▼
"Informe o e-mail!"

email com formato inválido
│
▼
"Formato de e-mail inválido!"

Layout vertical

O formulário utilizará:

<Form
layout="vertical"
form={form}
onFinish={onFinish}
>

A propriedade:

layout="vertical"

organiza os campos desta forma:

Nome
[________________________]

Email
[________________________]

Data
[________________________]

Em vez de posicionar o rótulo ao lado do campo.


Enviando o formulário com onFinish

O Ant Design utiliza:

onFinish={onFinish}

Quando as validações forem atendidas, a função será executada.

Podemos criar:

const onFinish = (values) => {

console.log(
"Dados cadastrados:",
values
);

message.success(
"Cadastro realizado com sucesso!"
);
};

A variável:

values

conterá os dados preenchidos no formulário.

O fluxo será:

Usuário
│
▼
preenche formulário
│
▼
clica em cadastrar
│
▼
AntD executa validações
│
├── erro
│ └── apresenta mensagens
│
└── válido
│
▼
onFinish(values)
│
├── console.log()
└── message.success()

Campos específicos para PF e PJ

Como o formulário possui dois tipos de pessoa, podemos utilizar renderização condicional.

Por exemplo:

{tipoPessoa === "PF" ? (
<>
{/* campos de Pessoa Física */}
</>
) : (
<>
{/* campos de Pessoa Jurídica */}
</>
)}

Temos:

tipoPessoa
│
├── PF
│ └── campos de Pessoa Física
│
└── PJ
└── campos de Pessoa Jurídica

Assim, o formulário pode se adaptar à opção selecionada pelo usuário.


Campo de data

Para dados relacionados a datas podemos utilizar:

<DatePicker />

Por exemplo:

<Form.Item
label={
tipoPessoa === "PF"
? "Data de Nascimento"
: "Data de Fundação"
}
name="data"
rules={[
{
required: true,
message: "Informe a data!"
}
]}
>

<DatePicker
style={{ width: "100%" }}
format="DD/MM/YYYY"
/>

</Form.Item>

Observe que até o label pode ser dinâmico:

PF
│
└── Data de Nascimento

PJ
│
└── Data de Fundação

Componente EnderecoForm

Um formulário de cadastro pode possuir muitos campos.

Para evitar que PessoaForm.jsx concentre toda a implementação, criaremos um componente específico para endereço.

PessoaForm
│
├── Dados principais
│
├── EnderecoForm
│
└── TelefoneList

EnderecoForm será um subformulário.


Estrutura do endereço

Os dados podem ser organizados como:

endereco
│
├── cidade
├── uf
└── regiao

Para isso utilizaremos nomes aninhados no Form.Item.

Exemplo:

name={["endereco", "cidade"]}

Isso associa o campo a:

endereco.cidade

Da mesma forma:

name={["endereco", "uf"]}

representa:

endereco.uf

Criando EnderecoForm.jsx

EnderecoForm.jsx​

import React from "react";

import {
Form,
Input,
Row,
Col,
Select
} from "antd";

const { Option } = Select;

export default function EnderecoForm() {

return (
<>
<h3>Endereço</h3>

<Row gutter={8}>

<Col span={12}>

<Form.Item
label="Cidade"
name={["endereco", "cidade"]}
rules={[
{
required: true,
message: "Informe a cidade!"
}
]}
>

<Input
placeholder="Cidade"
/>

</Form.Item>

</Col>

<Col span={6}>

<Form.Item
label="UF"
name={["endereco", "uf"]}
rules={[
{
required: true,
message: "Informe a UF!"
}
]}
>

<Input
placeholder="UF"
maxLength={2}
/>

</Form.Item>

</Col>

<Col span={6}>

<Form.Item
label="Região"
name={["endereco", "regiao"]}
rules={[
{
required: true,
message: "Selecione a região!"
}
]}
>

<Select
placeholder="Selecione"
>

<Option value="Norte">
Norte
</Option>

<Option value="Nordeste">
Nordeste
</Option>

<Option value="Centro-Oeste">
Centro-Oeste
</Option>

<Option value="Sudeste">
Sudeste
</Option>

<Option value="Sul">
Sul
</Option>

</Select>

</Form.Item>

</Col>

</Row>
</>
);
}

Layout com Row e Col

Uma novidade importante é:

<Row>

e:

<Col>

Esses componentes permitem organizar a interface em uma grade.

Temos:

Row
│
├── Col
├── Col
└── Col

No exemplo:

<Row gutter={8}>

a propriedade:

gutter

define o espaçamento entre as colunas.


Utilizando span

Observe:

<Col span={12}>

e:

<Col span={6}>

Podemos visualizar:

┌──────────────────────────────────────────────┐
│ Row │
├───────────────────────┬───────────┬──────────┤
│ Cidade │ UF │ Região │
│ span=12 │ span=6 │ span=6 │
└───────────────────────┴───────────┴──────────┘

A soma é:

12 + 6 + 6 = 24

Assim, os campos ocupam a mesma linha.

Grid do Ant Design

O sistema de Grid do Ant Design utiliza uma divisão em 24 partes.

Portanto:

span={24} → linha inteira

span={12} → metade da linha

span={8} → aproximadamente 1/3

span={6} → 1/4 da linha

Utilizando Select

Para a região utilizamos:

<Select>

com diferentes opções:

<Option value="Norte">
Norte
</Option>

A estrutura é:

Select
│
├── Norte
├── Nordeste
├── Centro-Oeste
├── Sudeste
└── Sul

O usuário não precisa digitar a região.

Ele seleciona uma das opções disponíveis.


Componente TelefoneList

Agora criaremos uma funcionalidade diferente.

O usuário poderá adicionar vários telefones.

Isso significa que não teremos apenas um valor:

telefone

Teremos uma coleção:

telefones
│
├── telefone 1
├── telefone 2
├── telefone 3
└── ...

Para controlar essa funcionalidade utilizaremos useState.


Estados de TelefoneList

Precisaremos controlar:

DDD
Número
Lista de telefones

Assim:

const [ddd, setDdd] = useState("");
const [numero, setNumero] = useState("");
const [telefones, setTelefones] = useState([]);

Inicialmente:

ddd = ""

numero = ""

telefones = []

Adicionando um telefone

Podemos criar:

function adicionarTelefone() {

if (!ddd || !numero) {
return;
}

const novoTelefone = {
ddd,
numero
};

setTelefones([
...telefones,
novoTelefone
]);

setDdd("");
setNumero("");
}

O fluxo será:

DDD + Número
│
▼
adicionarTelefone()
│
▼
novoTelefone
│
▼
setTelefones()
│
▼
nova lista

Observe:

[
...telefones,
novoTelefone
]

O operador:

...

mantém os elementos existentes e acrescenta o novo telefone.


Criando TelefoneList.jsx

TelefoneList.jsx​

import React, { useState } from "react";

import {
Form,
Input,
Button,
Space,
List
} from "antd";

export default function TelefoneList() {

const [ddd, setDdd] = useState("");
const [numero, setNumero] = useState("");
const [telefones, setTelefones] = useState([]);

function adicionarTelefone() {

if (!ddd || !numero) {
return;
}

const novoTelefone = {
ddd,
numero
};

setTelefones([
...telefones,
novoTelefone
]);

setDdd("");
setNumero("");
}

return (
<>
<h3>Telefones</h3>

<Space align="baseline">

<Form.Item label="DDD">

<Input
value={ddd}
onChange={(e) =>
setDdd(e.target.value)
}
maxLength={2}
/>

</Form.Item>

<Form.Item label="Número">

<Input
value={numero}
onChange={(e) =>
setNumero(e.target.value)
}
maxLength={9}
/>

</Form.Item>

<Button
onClick={adicionarTelefone}
type="primary"
>
Adicionar
</Button>

</Space>

<List
size="small"
bordered
dataSource={telefones}
renderItem={(telefone, index) => (

<List.Item key={index}>

({telefone.ddd}) {telefone.numero}

</List.Item>

)}
style={{
marginTop: 10
}}
/>

</>
);
}

Componente Space

Já utilizamos Space anteriormente.

Agora ele organiza:

DDD
Número
Botão

horizontalmente.

<Space align="baseline">

Visualmente:

DDD          Número                Botão
[ 61 ] [ 999999999 ] [ Adicionar ]

A propriedade:

align="baseline"

ajuda a alinhar os elementos considerando sua linha de base.


Componente List

Para apresentar os telefones utilizamos:

<List />

Observe:

dataSource={telefones}

Essa propriedade fornece os dados que serão apresentados.

Depois:

renderItem={(telefone, index) => (

define como cada elemento será renderizado.

Podemos representar:

telefones
│
├── { ddd: "61", numero: "999999999" }
├── { ddd: "62", numero: "888888888" }
└── { ddd: "11", numero: "777777777" }
│
▼
List
│
▼
(61) 999999999
(62) 888888888
(11) 777777777

dataSource

A propriedade:

dataSource={telefones}

informa ao componente qual coleção deverá ser utilizada.

Neste caso:

state telefones
│
▼
dataSource
│
▼
List

Quando o estado é atualizado:

setTelefones(...)

o React renderiza novamente o componente e a lista é atualizada.


renderItem

A propriedade:

renderItem={(telefone, index) => (
...
)}

determina como cada telefone será apresentado.

No nosso caso:

<List.Item key={index}>

({telefone.ddd}) {telefone.numero}

</List.Item>

Assim:

ddd = 61
numero = 999999999

será apresentado como:

(61) 999999999

Integrando os componentes

Agora podemos visualizar a aplicação completa:

App
│
▼
PessoaForm
│
├── Tipo de Pessoa
│ ├── PF
│ └── PJ
│
├── Dados principais
│
├── EnderecoForm
│ ├── Cidade
│ ├── UF
│ └── Região
│
├── TelefoneList
│ ├── DDD
│ ├── Número
│ └── List
│
└── Botão Cadastrar

Essa estrutura demonstra uma característica importante do React:

Componente maior
│
▼
composto por
componentes menores

Estrutura básica de PessoaForm.jsx

Agora podemos reunir os conceitos principais.

PessoaForm.jsx​

import React, {
useEffect,
useState
} from "react";

import {
Form,
Input,
Button,
Radio,
DatePicker,
message
} from "antd";

import {
ArrowUpOutlined
} from "@ant-design/icons";

import EnderecoForm from "./EnderecoForm.jsx";
import TelefoneList from "./TelefoneList.jsx";

import "./pessoaform.css";

export default function PessoaForm() {

const [form] = Form.useForm();

const [tipoPessoa, setTipoPessoa] =
useState("PF");

const [mostrarTopo, setMostrarTopo] =
useState(false);

const onChangeTipo = (e) => {

const novoTipo = e.target.value;

setTipoPessoa(novoTipo);

form.setFieldValue(
"tipo",
novoTipo
);
};

const onFinish = (values) => {

console.log(
"Dados cadastrados:",
values
);

message.success(
"Cadastro realizado com sucesso!"
);
};

useEffect(() => {

function verificarRolagem() {

setMostrarTopo(
window.scrollY > 200
);
}

window.addEventListener(
"scroll",
verificarRolagem
);

return () => {

window.removeEventListener(
"scroll",
verificarRolagem
);
};

}, []);

function voltarAoTopo() {

window.scrollTo({
top: 0,
behavior: "smooth"
});
}

return (
<main className="main-scroll">

<div className="form-container">

<h2>Cadastro de Pessoas</h2>

<Form
layout="vertical"
form={form}
onFinish={onFinish}
>

<Form.Item
label="Tipo de Pessoa"
name="tipo"
initialValue="PF"
style={{
marginBottom: 10
}}
>

<Radio.Group
onChange={onChangeTipo}
>

<Radio value="PF">
Pessoa Física
</Radio>

<Radio value="PJ">
Pessoa Jurídica
</Radio>

</Radio.Group>

</Form.Item>

<Form.Item
label={
tipoPessoa === "PF"
? "Nome"
: "Razão Social"
}
name="nome"
rules={[
{
required: true,
message:
"Informe o nome!"
}
]}
>

<Input
placeholder={
tipoPessoa === "PF"
? "Nome completo"
: "Razão social"
}
/>

</Form.Item>

<Form.Item
label="Email"
name="email"
rules={[
{
required: true,
message:
"Informe o e-mail!"
},
{
type: "email",
message:
"Formato de e-mail inválido!"
}
]}
>

<Input
placeholder="exemplo@email.com"
/>

</Form.Item>

{tipoPessoa === "PF" ? (

<Form.Item
label="CPF"
name="cpf"
rules={[
{
required: true,
message:
"Informe o CPF!"
}
]}
>

<Input
placeholder="CPF"
maxLength={14}
/>

</Form.Item>

) : (

<Form.Item
label="CNPJ"
name="cnpj"
rules={[
{
required: true,
message:
"Informe o CNPJ!"
}
]}
>

<Input
placeholder="CNPJ"
maxLength={18}
/>

</Form.Item>

)}

<Form.Item
label={
tipoPessoa === "PF"
? "Data de Nascimento"
: "Data de Fundação"
}
name="data"
rules={[
{
required: true,
message:
"Informe a data!"
}
]}
>

<DatePicker
style={{
width: "100%"
}}
format="DD/MM/YYYY"
/>

</Form.Item>

<EnderecoForm />

<TelefoneList />

<Form.Item>

<Button
type="primary"
htmlType="submit"
block
size="large"
>
Cadastrar
</Button>

</Form.Item>

</Form>

</div>

{mostrarTopo && (

<button
className="scroll-top-button"
onClick={voltarAoTopo}
aria-label="Voltar ao topo"
>

<ArrowUpOutlined />

</button>

)}

</main>
);
}
Sobre o exemplo

PessoaForm é responsável pela estrutura principal e delega partes específicas do cadastro para:

EnderecoForm
TelefoneList

Essa divisão reduz a quantidade de responsabilidades concentradas em um único componente.


Botão de envio

Observe:

<Button
type="primary"
htmlType="submit"
block
size="large"
>
Cadastrar
</Button>

Uma propriedade especialmente importante é:

htmlType="submit"

Ela faz com que o botão envie o formulário.

Assim:

Button
│
▼
htmlType="submit"
│
▼
Form
│
▼
validação
│
▼
onFinish()

Se existirem erros de validação, onFinish não será executado.


Feedback com message

Após o cadastro utilizamos:

message.success(
"Cadastro realizado com sucesso!"
);

Isso fornece um feedback visual integrado ao Ant Design.

Podemos comparar:

alert()
│
└── recurso do navegador

message.success()
│
└── componente visual integrado ao AntD

Utilizando useEffect

O exemplo também utiliza:

useEffect()

para acompanhar a rolagem da página.

useEffect(() => {

function verificarRolagem() {

setMostrarTopo(
window.scrollY > 200
);
}

window.addEventListener(
"scroll",
verificarRolagem
);

return () => {

window.removeEventListener(
"scroll",
verificarRolagem
);
};

}, []);

Quando a página é rolada:

scroll
│
▼
verificarRolagem()
│
▼
window.scrollY
│
├── <= 200
│ └── mostrarTopo = false
│
└── > 200
└── mostrarTopo = true

Limpeza do evento

Observe que o useEffect retorna:

return () => {

window.removeEventListener(
"scroll",
verificarRolagem
);
};

Isso remove o evento quando o componente deixa de ser utilizado.

Podemos representar:

Componente montado
│
▼
addEventListener()
│
▼
evento ativo
│
▼
componente desmontado
│
▼
removeEventListener()
tip

Ao registrar eventos externos ao React, é importante remover esses eventos quando eles não forem mais necessários.


Botão para voltar ao topo

Quando:

mostrarTopo = true

renderizamos:

<button
className="scroll-top-button"
onClick={voltarAoTopo}
aria-label="Voltar ao topo"
>

<ArrowUpOutlined />

</button>

O ícone:

<ArrowUpOutlined />

é fornecido por:

@ant-design/icons

Rolagem suave

A função:

function voltarAoTopo() {

window.scrollTo({
top: 0,
behavior: "smooth"
});
}

faz a página retornar ao topo.

Temos:

top: 0
│
└── início da página

behavior: "smooth"
│
└── rolagem suave

CSS complementar

Mesmo utilizando uma biblioteca de componentes, podemos utilizar CSS para complementar a interface.

Criaremos:

pessoaform.css

pessoaform.css​

.main-scroll {
min-height: 100vh;
width: 100%;
padding: 32px 20px;
box-sizing: border-box;
background: #f5f5f5;
}

.form-container {
max-width: 900px;
margin: 0 auto;
background: #ffffff;
border-radius: 12px;
padding: 32px 40px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}

.form-container h2 {
text-align: center;
color: #1677ff;
margin-bottom: 24px;
}

.form-container h3 {
margin-top: 12px;
margin-bottom: 16px;
}

.scroll-top-button {
position: fixed;
right: 24px;
bottom: 24px;

width: 48px;
height: 48px;

border: none;
border-radius: 50%;

background-color: #1677ff;
color: white;

display: flex;
align-items: center;
justify-content: center;

cursor: pointer;

box-shadow:
0 4px 10px rgba(0, 0, 0, 0.25);

transition:
background-color 0.3s,
transform 0.3s;
}

.scroll-top-button:hover {
background-color: #0958d9;
transform: translateY(-2px);
}

@media (max-width: 768px) {

.main-scroll {
padding: 16px 10px;
}

.form-container {
padding: 24px 16px;
}
}

Ant Design e CSS

Observe que não estamos substituindo o Ant Design pelo CSS.

Temos duas responsabilidades:

Ant Design
│
├── Form
├── Input
├── Select
├── DatePicker
├── Button
├── List
└── validação


CSS
│
├── container
├── largura máxima
├── fundo
├── sombra
├── botão flutuante
└── ajustes responsivos

Portanto, as duas abordagens podem ser utilizadas em conjunto.


Container do formulário

A classe:

.form-container

define:

max-width: 900px
│
└── limita a largura

margin: 0 auto
│
└── centraliza

background: #ffffff
│
└── fundo branco

border-radius: 12px
│
└── cantos arredondados

padding
│
└── espaçamento interno

box-shadow
│
└── sombra

Visualmente:

Página
┌──────────────────────────────────────────────┐
│ │
│ ┌──────────────────────────────┐ │
│ │ │ │
│ │ Cadastro de Pessoas │ │
│ │ │ │
│ │ Tipo de Pessoa │ │
│ │ ( ) PF ( ) PJ │ │
│ │ │ │
│ │ Nome │ │
│ │ [__________________] │ │
│ │ │ │
│ │ ... │ │
│ │ │ │
│ └──────────────────────────────┘ │
│ │
└──────────────────────────────────────────────┘

Responsividade

O CSS também utiliza:

@media (max-width: 768px)

Isso permite modificar algumas características quando a tela possui uma largura menor.

Por exemplo:

.form-container {
padding: 24px 16px;
}

Assim, o formulário utiliza menos espaço interno em telas menores.

Temos:

Tela grande
│
└── padding maior

Tela pequena
│
└── padding menor

Visão geral da arquitetura

Agora podemos observar a arquitetura completa:

                         App.jsx
│
▼
PessoaForm.jsx
│
┌───────────────┼────────────────┐
│ │ │
▼ ▼ ▼
Dados Pessoa EnderecoForm TelefoneList
│ │ │
│ ┌────┼────┐ ┌────┼────┐
│ │ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼ ▼
Form.Item Cidade UF Região DDD Número List
│
▼
Validação AntD

Essa estrutura demonstra:

Componentização
+
Composição
+
Estado
+
Eventos
+
Formulários
+
Validação
+
Design System

Estrutura dos dados

Quando o formulário é enviado, os campos controlados pelo Form podem ser organizados em uma estrutura semelhante a:

values
│
├── tipo
├── nome
├── email
├── cpf ou cnpj
├── data
│
└── endereco
├── cidade
├── uf
└── regiao

Os campos:

name={["endereco", "cidade"]}
name={["endereco", "uf"]}
name={["endereco", "regiao"]}

são responsáveis por criar a estrutura aninhada de endereço.

info

Essa organização é especialmente útil quando o formulário representa uma entidade que possui outros conjuntos de informações relacionados.


Conceitos-chave utilizados

Neste projeto utilizamos vários recursos do Ant Design:

RecursoAplicação
FormEstrutura principal do formulário
Form.ItemCampos e validações
rulesRegras declarativas
InputEntrada de texto
Radio.GroupSeleção PF/PJ
DatePickerSeleção de data
SelectSeleção da região
RowLinha do Grid
ColColuna do Grid
SpaceEspaçamento entre elementos
ListApresentação da lista de telefones
ButtonAções do formulário
messageFeedback ao usuário

Também utilizamos conceitos React:

useState
useEffect
eventos
renderização condicional
componentização
composição
listas

Exercício de Fixação

O objetivo deste exercício é implementar, executar, testar e validar todos os códigos apresentados neste capítulo.

Crie a estrutura:

src/
│
├── App.jsx
│
└── components/
└── cadastrapessoa/
├── PessoaForm.jsx
├── EnderecoForm.jsx
├── TelefoneList.jsx
└── pessoaform.css

Implemente todos os arquivos apresentados.

Depois execute:

npm run dev

Testando o tipo de pessoa

Inicialmente, verifique se:

Pessoa Física

está selecionada.

Depois selecione:

Pessoa Jurídica

Observe se os campos específicos são alterados.

Valide o fluxo:

Radio.Group
│
▼
onChangeTipo()
│
▼
setTipoPessoa()
│
▼
nova renderização

Testando campos obrigatórios

Sem preencher os campos obrigatórios, clique em:

Cadastrar

Observe as mensagens de validação.

Identifique no código:

rules={[
{
required: true,
message: "..."
}
]}

Preencha os campos e observe o desaparecimento das mensagens.


Testando o e-mail

Digite um valor inválido:

aluno

Tente enviar o formulário.

Depois teste:

aluno@email.com

Compare o resultado.

Identifique a regra:

{
type: "email",
message:
"Formato de e-mail inválido!"
}

Testando o endereço

Preencha:

Cidade
UF
Região

Observe a organização dos campos na mesma linha.

Identifique:

Row
│
├── Col span={12}
├── Col span={6}
└── Col span={6}

Verifique também os nomes:

name={["endereco", "cidade"]}
name={["endereco", "uf"]}
name={["endereco", "regiao"]}

Testando a lista de telefones

Informe:

DDD: 61
Número: 999999999

Clique em:

Adicionar

Verifique se aparece:

(61) 999999999

Depois adicione outros telefones.

Observe se os registros anteriores permanecem na lista.

Valide:

telefones
│
▼
setTelefones()
│
▼
List

Testando o formulário completo

Preencha todos os campos corretamente.

Clique em:

Cadastrar

Verifique se:

Cadastro realizado com sucesso!

é apresentado.

Abra também o console do navegador e observe os dados enviados por:

console.log(
"Dados cadastrados:",
values
);

Analise a estrutura do objeto.


Testando o botão de voltar ao topo

Role a página.

Observe se o botão aparece depois de determinada quantidade de rolagem.

Identifique:

window.scrollY > 200

Clique no botão.

Verifique a rolagem suave para o início da página.

Analise:

window.scrollTo({
top: 0,
behavior: "smooth"
});

Testando o CSS

Observe:

form-container
main-scroll
scroll-top-button

Altere temporariamente:

max-width: 900px;

e observe o efeito.

Faça o mesmo com:

border-radius: 12px;

e:

box-shadow:
0 4px 12px rgba(0, 0, 0, 0.08);

Depois retorne aos valores originais.


Validação final

Ao concluir os testes, certifique-se de que consegue identificar:

Form
Form.Item
Form.useForm()
rules
required
type="email"
onFinish
Radio.Group
DatePicker
Row
Col
span
Select
Space
List
dataSource
renderItem
message.success()
useState
useEffect
renderização condicional
campos aninhados
componentização
CSS complementar

Procure também explicar o fluxo completo:

Usuário
│
▼
preenche formulário
│
▼
Form.Item
│
▼
rules
│
├── inválido
│ └── mensagem de erro
│
└── válido
│
▼
onFinish()
│
▼
values
│
▼
processamento dos dados
Objetivo do exercício

O objetivo não é apenas reproduzir visualmente o formulário.

Durante os testes, procure compreender a responsabilidade de cada parte:

React
│
├── estados
├── eventos
├── efeitos
├── renderização condicional
└── composição

Ant Design
│
├── formulário
├── campos
├── validação
├── Grid
├── lista
└── feedback

CSS
│
├── layout complementar
├── container
├── botão flutuante
└── responsividade

Resumo

Neste capítulo evoluímos de uma interface simples construída com Ant Design para um formulário de cadastro composto por diferentes componentes.

A evolução pode ser representada por:

Componentes React
│
▼
Ant Design
│
▼
Form
│
▼
Form.Item
│
▼
Validação declarativa
│
▼
Formulário dinâmico PF/PJ
│
▼
Subcomponentes
│
├── EnderecoForm
└── TelefoneList
│
▼
Grid responsivo
│
├── Row
└── Col
│
▼
Aplicação de cadastro

O PessoaForm atua como componente principal, enquanto EnderecoForm e TelefoneList separam responsabilidades específicas da aplicação.

Também avançamos no uso do Ant Design com:

Form + Form.Item
│
└── validação declarativa

Row + Col
│
└── organização em Grid

Input + Select + DatePicker
│
└── entrada de dados

Space + List
│
└── organização e apresentação

Button + message
│
└── ação e feedback

Essa estrutura permite construir formulários maiores sem abandonar os princípios estudados desde os primeiros capítulos: componentização, reutilização, estado, eventos e separação de responsabilidades.