Created
November 8, 2024 22:18
-
-
Save rg3915/23929bf6d46cfca04722009e47f877c8 to your computer and use it in GitHub Desktop.
Form Wizard + AlpineJS + State Machine
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
}) | |
} | |
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.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