Skip to content

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.

  1. Manual: Controle total usando createState() e event handlers (campos não controlados)
  2. Two-way binding: Helpers para sincronização automática (ex: textFieldControl)
  3. Form data: Extrair dados do form com formToObject() e onSubmit()

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 foco
render(NameField(), '#app')

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 recriados
function 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')

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 recriados
render(TextForm(), '#app')

textFieldControl aceita três modos:

ModoQuando atualizaUse quando
"input"A cada tecla digitadaFeedback instantâneo (default)
"change"Ao sair do campo (blur)Validação menos frequente
"both"Nos dois eventosMáxima compatibilidade
// Input mode (default)
const ctrl1 = textFieldControl(state, 'input')
// Change mode
const ctrl2 = textFieldControl(state, 'change')
// Ambos
const ctrl3 = textFieldControl(state, 'both')
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')
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')
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')

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" }

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>
`
}

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:

import { html, createState, render } from '@_bashell/slash'
const values = { email: '', password: '' } // texto em edição: não é lido no render
const 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 validar
const 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')
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 state
const 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')

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)
}
// Checkbox
function handleCheck(e: CheckboxEvent) {
console.log(e.target.checked)
}
// Radio
function handleRadio(e: RadioEvent) {
console.log(e.target.value, e.target.checked)
}
// Select
function handleSelect(e: SelectEvent) {
console.log(e.target.value)
}
// Form submit
function handleSubmit(e: FormSubmitEvent) {
e.preventDefault()
console.log(e.currentTarget) // HTMLFormElement
}

Funções utilitárias para extrair valores de eventos:

import { getText, getChecked, getSelectValue } from '@_bashell/slash'
// Text field
const handleInput = (e: TextFieldEvent) => {
const value = getText(e) // string
console.log(value)
}
// Checkbox/Radio
const handleCheck = (e: CheckboxEvent) => {
const checked = getChecked(e) // boolean
console.log(checked)
}
// Select
const handleSelect = (e: SelectEvent) => {
const value = getSelectValue(e) // string
console.log(value)
}