Skip to content

Instantly share code, notes, and snippets.

@rg3915
Created November 8, 2024 22:18
Show Gist options
  • Save rg3915/23929bf6d46cfca04722009e47f877c8 to your computer and use it in GitHub Desktop.
Save rg3915/23929bf6d46cfca04722009e47f877c8 to your computer and use it in GitHub Desktop.
Form Wizard + AlpineJS + State Machine
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="https://picocss.com/favicon.svg" type="image/svg+xml">
<title>Form Wizard - Máquina de Estado</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css" />
<link rel="stylesheet" href="assets/css/style.css">
<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>
</head>
<body>
<div
class="container"
x-data="formWizard()"
>
<!-- Toast container -->
<div class="toast-container">
<div
class="toast"
:class="{
'visible': toast.visible,
'removing': toast.removing,
'success': toast.type === 'success',
'error': toast.type === 'error'
}"
@click="hideToast"
x-show="toast.visible"
>
<span x-text="toast.message"></span>
<span class="toast-close">×</span>
</div>
</div>
<header>
<h1>Cadastro de Pessoa</h1>
</header>
<!-- Seleção do tipo de cadastro -->
<section
class="tipo-cadastro"
x-show="currentState === 'inicio'"
>
<button
@click="selectTipo('fisica')"
:class="{ 'outline': tipo !== 'fisica' }"
>
Pessoa Física
</button>
<button
@click="selectTipo('juridica')"
:class="{ 'outline': tipo !== 'juridica' }"
>
Pessoa Jurídica
</button>
</section>
<!-- Navegação -->
<nav x-show="currentState !== 'inicio'">
<ul>
<li :class="{
'active': currentState === 'identificacao',
'completed': hasPassedState('identificacao')
}">Identificação</li>
<li :class="{
'active': currentState === 'documentos',
'completed': hasPassedState('documentos')
}">Documentos</li>
<li :class="{
'active': currentState === 'endereco',
'completed': hasPassedState('endereco')
}">Endereço</li>
<li :class="{
'active': currentState === 'confirmacao',
'completed': hasPassedState('confirmacao')
}">Confirmação</li>
</ul>
</nav>
<form @submit.prevent="handleSubmit">
<!-- Estado: Identificação -->
<section x-show="currentState === 'identificacao'">
<fieldset>
<h2>Identificação</h2>
<label class="required">Nome:</label>
<input
type="text"
x-model="form.nome"
required
>
<label class="required">E-mail:</label>
<input
type="email"
x-model="form.email"
required
>
<div class="buttons">
<button
type="button"
@click="setState('inicio')"
>Anterior</button>
<button
type="button"
@click="nextState"
>Próximo</button>
</div>
</fieldset>
</section>
<!-- Estado: Documentos -->
<section x-show="currentState === 'documentos'">
<h2>Documentos</h2>
<template x-if="tipo === 'fisica'">
<fieldset>
<label>CPF:
<input
type="text"
x-model="form.cpf"
>
</label>
<label>
RG:
<input
type="text"
x-model="form.rg"
>
</label>
</fieldset>
</template>
<template x-if="tipo === 'juridica'">
<fieldset>
<label>
CNPJ:
<input
type="text"
x-model="form.cnpj"
>
</label>
<label>
Razão Social:
<input
type="text"
x-model="form.razaoSocial"
>
</label>
</fieldset>
</template>
<div class="buttons">
<button
type="button"
@click="prevState"
>Anterior</button>
<button
type="button"
@click="nextState"
>Próximo</button>
</div>
</section>
<!-- Estado: Endereço -->
<section x-show="currentState === 'endereco'">
<fieldset>
<h2>Endereço</h2>
<label>
CEP:
<input
type="text"
x-model="form.cep"
@change="getAddressByCep(form.cep)"
>
</label>
<label>
Logradouro:
<input
type="text"
x-model="form.logradouro"
>
</label>
<label>
Complemento:
<input
type="text"
x-model="form.complemento"
>
</label>
<label>
Bairro:
<input
type="text"
x-model="form.bairro"
>
</label>
<label>
Cidade:
<input
type="text"
x-model="form.cidade"
>
</label>
<label>
UF:
<input
type="text"
x-model="form.uf"
>
</label>
<div class="buttons">
<button
type="button"
@click="prevState"
>Anterior</button>
<button
type="button"
@click="nextState"
>Próximo</button>
</div>
</fieldset>
</section>
<!-- Estado: Confirmação -->
<section x-show="currentState === 'confirmacao'">
<h2>Confirmação dos Dados</h2>
<div>
<h3>Dados Pessoais</h3>
<p><strong>Nome:</strong> <span x-text="form.nome"></span></p>
<p><strong>Email:</strong> <span x-text="form.email"></span></p>
<template x-if="tipo === 'fisica'">
<div>
<p><strong>CPF:</strong> <span x-text="form.cpf"></span></p>
<p><strong>RG:</strong> <span x-text="form.rg"></span></p>
</div>
</template>
<template x-if="tipo === 'juridica'">
<div>
<p><strong>CNPJ:</strong> <span x-text="form.cnpj"></span></p>
<p><strong>Razão Social:</strong> <span x-text="form.razaoSocial"></span></p>
</div>
</template>
<h3>Endereço</h3>
<p><strong>CEP:</strong> <span x-text="form.cep"></span></p>
<p><strong>Logradouro:</strong> <span x-text="form.logradouro"></span></p>
<p><strong>Complemento:</strong> <span x-text="form.complemento || '---'"></span></p>
<p><strong>Bairro:</strong> <span x-text="form.bairro"></span></p>
<p><strong>Cidade:</strong> <span x-text="form.cidade"></span></p>
<p><strong>UF:</strong> <span x-text="form.uf"></span></p>
</div>
<div class="buttons">
<button
type="button"
@click="prevState"
>Anterior</button>
<button type="submit">Finalizar Cadastro</button>
</div>
</section>
</form>
</div>
<script src="assets/js/main.js"></script>
</body>
</html>
const formWizard = () => ({
// Estados possíveis do formulário
states: ['inicio', 'identificacao', 'documentos', 'endereco', 'confirmacao'],
currentState: 'inicio',
tipo: null,
// Adiciona estado para o toast
toast: {
message: '',
visible: false,
removing: false,
type: 'success',
timeoutId: null
},
form: {
nome: '',
email: '',
cpf: '',
rg: '',
cnpj: '',
razaoSocial: '',
cep: '',
logradouro: '',
complemento: '',
bairro: '',
cidade: '',
uf: ''
},
// Função para mostrar toast
showToast(message, type = 'success') {
// Limpa timeout anterior se existir
if (this.toast.timeoutId) {
clearTimeout(this.toast.timeoutId)
}
// Reset do estado do toast
this.toast.removing = false
this.toast.message = message
this.toast.type = type
this.toast.visible = true
// Auto-hide após 3 segundos
this.toast.timeoutId = setTimeout(() => {
this.hideToast()
}, 3000)
},
// Função para esconder toast com animação
hideToast() {
this.toast.removing = true
// Aguarda a animação terminar antes de esconder
setTimeout(() => {
this.toast.visible = false
this.toast.removing = false
}, 300)
// Limpa o timeout se existir
if (this.toast.timeoutId) {
clearTimeout(this.toast.timeoutId)
}
},
resetForm() {
this.form = {
nome: '',
email: '',
cpf: '',
rg: '',
cnpj: '',
razaoSocial: '',
cep: '',
logradouro: '',
complemento: '',
cidade: '',
uf: ''
}
this.tipo = null
},
// Define o tipo de cadastro e inicia o fluxo
selectTipo(tipo) {
this.tipo = tipo
this.setState('identificacao')
},
// Verifica se já passou por determinado estado
hasPassedState(state) {
const currentIndex = this.states.indexOf(this.currentState)
const stateIndex = this.states.indexOf(state)
return stateIndex < currentIndex
},
// Define um estado específico
setState(state) {
if (this.states.includes(state)) {
this.currentState = state
}
},
// Avança para o próximo estado
nextState() {
const currentIndex = this.states.indexOf(this.currentState)
if (currentIndex < this.states.length - 1) {
this.currentState = this.states[currentIndex + 1]
}
},
// Retorna ao estado anterior
prevState() {
const currentIndex = this.states.indexOf(this.currentState)
if (currentIndex > 0) {
this.currentState = this.states[currentIndex - 1]
}
},
// Manipula o envio do formulário
handleSubmit() {
console.log('Formulário enviado:', {
tipo: this.tipo,
dados: this.form
})
this.showToast('Cadastro realizado com sucesso!')
// Reset e retorno ao início
this.resetForm()
this.setState('inicio')
},
getAddressByCep(cep) {
if (cep.length !== 8 && cep.length !== 9) {
return Promise.reject(new Error('Invalid CEP length'))
}
return fetch(`https://viacep.com.br/ws/${cep}/json/`)
.then(response => response.json())
.then(data => {
// Update input fields
this.form.logradouro = data.logradouro
this.form.bairro = data.bairro
this.form.cidade = data.localidade
this.form.uf = data.uf
console.log('Address updated:', data)
})
.catch(error => {
console.error(error.message)
})
}
})
.required::after {
content: '*';
color: red;
margin-left: 4px;
}
.error-message {
color: #ff4444;
font-size: 0.875rem;
margin-top: 0.25rem;
}
nav {
padding: 2rem 0;
}
nav ul {
display: flex;
list-style: none;
padding: 0;
margin: 0;
position: relative;
justify-content: space-between;
align-items: center;
max-width: 800px;
margin: 0 auto;
}
nav ul::before {
position: absolute;
top: 50%;
left: 0;
width: 100%;
height: 2px;
background: #e0e0e0;
z-index: 1;
}
nav ul li {
position: relative;
z-index: 2;
padding: 1rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
nav ul li::before {
content: '';
width: 40px;
height: 40px;
border-radius: 50%;
border: 2px solid #e0e0e0;
background: white;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
nav ul li.active::before {
border-color: #1095c1;
background: #1095c1;
box-shadow: 0 0 0 3px rgba(16, 149, 193, 0.2);
}
nav ul li.completed::before {
border-color: #1095c1;
background: #1095c1;
}
nav ul li.active {
color: #1095c1;
font-weight: bold;
}
.buttons {
display: flex;
gap: 1rem;
justify-content: flex-end;
margin-top: 2rem;
}
.tipo-cadastro {
display: flex;
gap: 2rem;
justify-content: center;
margin-bottom: 2rem;
}
.toast-container {
position: fixed;
top: 1rem;
right: 1rem;
z-index: 9999;
}
.toast {
padding: 1rem 1.5rem;
margin-bottom: 0.5rem;
border-radius: 4px;
font-weight: 500;
cursor: pointer;
opacity: 0;
transform: translateX(100%);
transition: all 0.3s ease-in-out;
display: flex;
align-items: center;
gap: 0.5rem;
}
/* Transição de entrada */
.toast.visible {
opacity: 1;
transform: translateX(0);
}
/* Transição de saída */
.toast.removing {
opacity: 0;
transform: translateX(100%);
}
.toast.success {
background-color: #48bb78;
color: white;
}
.toast.error {
background-color: #f56565;
color: white;
}
/* Ícone de fechar */
.toast-close {
margin-left: auto;
opacity: 0.7;
transition: opacity 0.2s;
}
.toast-close:hover {
opacity: 1;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment