Formulários
Trabalhando com formulários
Section titled “Trabalhando com formulários”O Slash fornece helpers tipados para trabalhar com formulários de forma simples e eficiente. Não há “magia” - você usa HTML nativo e adiciona reatividade com createState() e os helpers de form.
Abordagens disponíveis
Section titled “Abordagens disponíveis”- Manual: Controle total usando
createState()e event handlers (campos não controlados) - Two-way binding: Helpers para sincronização automática (ex:
textFieldControl) - Form data: Extrair dados do form com
formToObject()eonSubmit()
Limitação importante: foco e re-render
Section titled “Limitação importante: foco e re-render”Binding reativo (value=${reactive})
Section titled “Binding reativo (value=${reactive})”Um objeto com get() e subscribe(fn) (o contrato Reactive) como valor de atributo mantém o elemento em sincronia com o state, sem re-render:
import { html, createState, render } from '@_bashell/slash'
const name = createState({ value: '' })
// Reactive derivado do state: { get, subscribe }const nameValue = { get: () => name.get().value, subscribe: (fn: (v: string) => void) => name.watch((s) => fn(s.value)),}
function NameField() { return html` <input value=${nameValue} onInput=${(e: Event) => name.set({ value: (e.target as HTMLInputElement).value })} /> `}
// Chamada direta: o <input> é atualizado no lugar e mantém o focorender(NameField(), '#app')Formulário básico (manual)
Section titled “Formulário básico (manual)”A forma mais simples é usar createState() e event handlers. Os campos são não controlados e o state é lido só no envio:
import { html, createState, render } from '@_bashell/slash'
const form = createState({ email: '', password: '' })
const handleSubmit = (e: Event) => { e.preventDefault() console.log('Login:', form.get())}
// Não lê states no render: os inputs nunca são recriadosfunction LoginForm() { return html` <form onSubmit=${handleSubmit}> <div> <label>Email:</label> <input type="email" onInput=${(e: Event) => { form.set({ ...form.get(), email: (e.target as HTMLInputElement).value }) }} /> </div>
<div> <label>Senha:</label> <input type="password" onInput=${(e: Event) => { form.set({ ...form.get(), password: (e.target as HTMLInputElement).value }) }} /> </div>
<button type="submit">Entrar</button> </form> `}
render(html`<${LoginForm} />`, '#app')Two-way binding com helpers
Section titled “Two-way binding com helpers”Os controles (textFieldControl, checkboxControl, radioControl, SelectControl) devolvem props prontas para espalhar no elemento (...${controle}). O valor é um Reactive: o elemento acompanha o state sem re-renderizar o componente, e o evento atualiza o state.
textFieldControl - Text inputs e textareas
Section titled “textFieldControl - Text inputs e textareas”import { html, createState, render } from '@_bashell/slash'import { textFieldControl } from '@_bashell/slash'
const nameState = createState({ value: '' })const bioState = createState({ value: '' })
// Control para <input> e <textarea>const nameControl = textFieldControl(nameState, 'input')const bioControl = textFieldControl(bioState, 'input')
// Mostra os valores: componente à parte, montado com <${Resumo} />const Resumo = () => html` <div> <p>Nome: ${nameState.get().value}</p> <p>Bio: ${bioState.get().value}</p> </div>`
function TextForm() { return html` <form> <div> <label>Nome:</label> <input type="text" ...${nameControl} /> </div>
<div> <label>Bio:</label> <textarea ...${bioControl}></textarea> </div>
<${Resumo} /> </form> `}
// Chamada direta: os campos são atualizados no lugar, sem serem recriadosrender(TextForm(), '#app')Modos de controle
Section titled “Modos de controle”textFieldControl aceita três modos:
| Modo | Quando atualiza | Use quando |
|---|---|---|
"input" | A cada tecla digitada | Feedback instantâneo (default) |
"change" | Ao sair do campo (blur) | Validação menos frequente |
"both" | Nos dois eventos | Máxima compatibilidade |
// Input mode (default)const ctrl1 = textFieldControl(state, 'input')
// Change modeconst ctrl2 = textFieldControl(state, 'change')
// Ambosconst ctrl3 = textFieldControl(state, 'both')checkboxControl - Checkboxes
Section titled “checkboxControl - Checkboxes”import { html, createState, render } from '@_bashell/slash'import { checkboxControl } from '@_bashell/slash'
const termsState = createState({ value: false })const newsState = createState({ value: true })
const termsCtrl = checkboxControl(termsState)const newsCtrl = checkboxControl(newsState)
const Resumo = () => html` <div> <p>Termos aceitos: ${termsState.get().value ? 'Sim' : 'Não'}</p> <p>Newsletter: ${newsState.get().value ? 'Sim' : 'Não'}</p> </div>`
function TermsForm() { return html` <form> <label> <input type="checkbox" ...${termsCtrl} /> Aceito os termos </label>
<label> <input type="checkbox" ...${newsCtrl} /> Receber newsletter </label>
<${Resumo} /> </form> `}
render(TermsForm(), '#app')radioControl - Radio buttons
Section titled “radioControl - Radio buttons”import { html, createState, render } from '@_bashell/slash'import { radioControl } from '@_bashell/slash'
const themeState = createState({ value: 'light' })
const lightCtrl = radioControl(themeState, 'light')const darkCtrl = radioControl(themeState, 'dark')const autoCtrl = radioControl(themeState, 'auto')
const Selecionado = () => html`<p>Tema selecionado: ${themeState.get().value}</p>`
function PreferenceForm() { return html` <form> <p>Escolha o tema:</p>
<label> <input type="radio" name="theme" ...${lightCtrl} /> Claro </label>
<label> <input type="radio" name="theme" ...${darkCtrl} /> Escuro </label>
<label> <input type="radio" name="theme" ...${autoCtrl} /> Automático </label>
<${Selecionado} /> </form> `}
render(PreferenceForm(), '#app')SelectControl - Select dropdowns
Section titled “SelectControl - Select dropdowns”import { html, createState, render } from '@_bashell/slash'import { SelectControl } from '@_bashell/slash'
const countryState = createState({ value: 'br' })const countryCtrl = SelectControl(countryState)
const Selecionado = () => html`<p>País selecionado: ${countryState.get().value}</p>`
function CountryForm() { return html` <form> <label> País: <select ...${countryCtrl}> <option value="br">Brasil</option> <option value="us">Estados Unidos</option> <option value="pt">Portugal</option> <option value="es">Espanha</option> </select> </label>
<${Selecionado} /> </form> `}
render(CountryForm(), '#app')Form data helpers
Section titled “Form data helpers”formToObject - Extrair dados do form
Section titled “formToObject - Extrair dados do form”Converte um <form> em um objeto JavaScript:
import { formToObject } from '@_bashell/slash'
const form = document.querySelector('form')!const data = formToObject(form)
console.log(data)// { email: "user@example.com", password: "secret", terms: "on" }onSubmit - Handler de submit simplificado
Section titled “onSubmit - Handler de submit simplificado”Previne o default e extrai dados automaticamente:
import { html } from '@_bashell/slash'import { onSubmit } from '@_bashell/slash'
function ContactForm() { const handleSubmit = onSubmit((data, e) => { console.log('Dados:', data) // data = { name: "...", email: "...", message: "..." }
// Fazer POST para API fetch('/api/contact', { method: 'POST', body: JSON.stringify(data) }) })
return html` <form onSubmit=${handleSubmit}> <input type="text" name="name" placeholder="Nome" /> <input type="email" name="email" placeholder="Email" /> <textarea name="message" placeholder="Mensagem"></textarea> <button type="submit">Enviar</button> </form> `}Validação
Section titled “Validação”O Slash não possui um sistema de validação built-in, mas você pode criar facilmente com createState(). Os campos ficam não controlados e só a área de erros é reativa:
Validação básica
Section titled “Validação básica”import { html, createState, render } from '@_bashell/slash'
const values = { email: '', password: '' } // texto em edição: não é lido no renderconst errors = createState({ email: '', password: '' })
const validate = () => { const next = { email: '', password: '' }
if (!values.email) next.email = 'Email é obrigatório' else if (!values.email.includes('@')) next.email = 'Email inválido'
if (!values.password) next.password = 'Senha é obrigatória' else if (values.password.length < 6) next.password = 'Senha deve ter no mínimo 6 caracteres'
errors.set(next) return !next.email && !next.password}
// Só este componente lê `errors`: os inputs não são recriados ao validarconst FieldError = ({ field }: { field: 'email' | 'password' }) => { const message = errors.get()[field] return html`${message && html`<span class="error">${message}</span>`}`}
function SignupForm() { const handleSubmit = (e: Event) => { e.preventDefault() if (validate()) console.log('Formulário válido!', values) }
return html` <form onSubmit=${handleSubmit}> <div> <input type="email" placeholder="Email" onInput=${(e: Event) => { values.email = (e.target as HTMLInputElement).value }} /> <${FieldError} field="email" /> </div>
<div> <input type="password" placeholder="Senha" onInput=${(e: Event) => { values.password = (e.target as HTMLInputElement).value }} /> <${FieldError} field="password" /> </div>
<button type="submit">Cadastrar</button> </form> `}
render(html`<${SignupForm} />`, '#app')Validação assíncrona
Section titled “Validação assíncrona”import { html, createState, render } from '@_bashell/slash'
const status = createState({ checking: false, error: '' })let username = ''
const checkUsername = async (name: string) => { status.set({ checking: true, error: '' })
try { const res = await fetch(`/api/check-username?name=${encodeURIComponent(name)}`) const { available } = await res.json()
status.set({ checking: false, error: available ? '' : 'Username já existe' }) } catch (err) { status.set({ checking: false, error: 'Erro ao verificar' }) }}
// Só a mensagem lê o stateconst UsernameStatus = () => { const { checking, error } = status.get() return html` <span> ${checking && html`<span>Verificando...</span>`} ${error && html`<span class="error">${error}</span>`} </span> `}
function UsernameForm() { return html` <form> <input type="text" placeholder="Username" onInput=${(e: Event) => { username = (e.target as HTMLInputElement).value }} onBlur=${() => { if (username) checkUsername(username) }} /> <${UsernameStatus} /> </form> `}
render(html`<${UsernameForm} />`, '#app')Tipos de eventos de formulário
Section titled “Tipos de eventos de formulário”O Slash exporta tipos completos para todos os eventos de formulário:
import type { TextFieldEvent, CheckboxEvent, RadioEvent, SelectEvent, FormSubmitEvent, FormResetEvent, ButtonEvent} from '@_bashell/slash'
// Text field (input/textarea)function handleInput(e: TextFieldEvent) { console.log(e.target.value)}
// Checkboxfunction handleCheck(e: CheckboxEvent) { console.log(e.target.checked)}
// Radiofunction handleRadio(e: RadioEvent) { console.log(e.target.value, e.target.checked)}
// Selectfunction handleSelect(e: SelectEvent) { console.log(e.target.value)}
// Form submitfunction handleSubmit(e: FormSubmitEvent) { e.preventDefault() console.log(e.currentTarget) // HTMLFormElement}Helpers de extração de valor
Section titled “Helpers de extração de valor”Funções utilitárias para extrair valores de eventos:
import { getText, getChecked, getSelectValue } from '@_bashell/slash'
// Text fieldconst handleInput = (e: TextFieldEvent) => { const value = getText(e) // string console.log(value)}
// Checkbox/Radioconst handleCheck = (e: CheckboxEvent) => { const checked = getChecked(e) // boolean console.log(checked)}
// Selectconst handleSelect = (e: SelectEvent) => { const value = getSelectValue(e) // string console.log(value)}Próximos passos
Section titled “Próximos passos”- Error Handling - Tratamento de erros
- Componentes - Criando form components reutilizáveis
- Estado - Gerenciamento de estado avançado