Skip to content

API Reference

Documentação completa de todas as funções, componentes e tipos exportados pelo Slash.

Terminal window
bun add @_bashell/slash
# ou
npm install @_bashell/slash

Cria elementos DOM usando hyperscript.

function h(
tag: string | Function,
props?: Props | null,
...children: Child[]
): Node

Parâmetros:

  • tag: Nome da tag HTML ou componente funcional
  • props: Propriedades do elemento (opcional)
  • children: Filhos do elemento (strings, números, Nodes, arrays, reativos)

Retorna: Node - o elemento DOM real (o Slash não tem VNodes)

Exemplo:

import { h } from '@_bashell/slash'
const element = h('div', { class: 'container' },
h('h1', null, 'Hello'),
h('p', null, 'World')
)

Template tag (HTM) que converte template strings em nós DOM. Seu tipo de retorno é Child; na prática, um Node (ou array de nós).

const html: HTMTemplate

Uso:

import { html } from '@_bashell/slash'
const element = html`
<div class="container">
<h1>Hello</h1>
<p>World</p>
</div>
`

Interpolação:

const name = 'John'
const items = ['a', 'b', 'c']
const element = html`
<div>
<h1>Hello ${name}</h1>
<ul>
${items.map(item => html`<li>${item}</li>`)}
</ul>
</div>
`

Monta uma view no DOM. Se o container já tiver HTML do servidor e a página tiver o script __SLASH_STATE__, hidrata (veja Hydration).

function render(
view: Child | (() => Child),
container: Element | string | null | undefined
): Node | Node[]

Parâmetros:

  • view: o que renderizar (um Child, ou uma função que o retorna)
  • container: elemento DOM ou seletor CSS ('#app'). Lança erro se o seletor não encontrar elemento

Retorna: o Node (ou Node[]) montado.

Exemplo:

import { html, render } from '@_bashell/slash'
const App = html`<h1>My App</h1>`
const root = document.getElementById('app')
if (root) {
render(App, root)
}

Remove um nó do DOM e limpa recursos.

function destroyNode(element: HTMLElement): void

Parâmetros:

  • element: Elemento DOM para destruir

Uso:

import { destroyNode } from '@_bashell/slash'
const element = document.getElementById('my-component')
if (element) {
destroyNode(element)
}

Cria um container de estado observável: watchers são notificados quando o valor muda e componentes que leem o estado com get() re-renderizam automaticamente (monte-os como <${Comp} />). Passar o próprio State como child ou prop (${count}) não é reativo: use ${count.get()} dentro de um componente.

function createState<T>(
initialValue: T,
options?: StateOptions
): State<T>

Parâmetros:

  • initialValue: Valor inicial do estado
  • options: Opções de configuração (opcional)

Opções:

interface StateOptions {
enableHistory?: boolean // Habilita histórico (time-travel debug)
historyMaxSize?: number // Tamanho máximo do histórico (padrão: 100)
}

Retorna: Objeto State<T> com métodos:

Retorna o valor atual do estado.

get(): T

Exemplo:

const count = createState(0)
console.log(count.get()) // 0

Atualiza o valor do estado e notifica observadores.

set(newValue: T): void

Exemplo:

const count = createState(0)
count.set(5)
console.log(count.get()) // 5

Registra um observador que é chamado quando o estado muda.

watch(callback: (newValue: T) => void): () => void

Parâmetros:

  • callback: Função chamada ao mudar estado

Retorna: Função de cleanup para remover o observador

Exemplo:

const count = createState(0)
const unwatch = count.watch((newVal) => {
console.log(`Changed to ${newVal}`)
})
count.set(5) // Logs: "Changed to 5"
// Remover observador
unwatch()

Ordem e reentrância: watchers são notificados na ordem de registro. Se um watcher chamar set() no mesmo estado durante a notificação, vale o último valor: a notificação aninhada entrega o valor atual a todos os watchers e a notificação antiga é interrompida, então nenhum watcher recebe um valor velho depois do novo. Watchers anteriores ao que fez o set veem o valor antigo e depois o novo. set segue síncrono, e um erro lançado na notificação aninhada chega a quem chamou set.

Exemplo Completo:

import { createState, html, render } from '@_bashell/slash'
const count = createState(0, { enableHistory: true })
// Componente que usa estado
const Counter = () => html`
<div>
<p>Count: ${count.get()}</p>
<button onClick=${() => count.set(count.get() + 1)}>
Increment
</button>
</div>
`
// Monte como <${Counter} />: chamado diretamente (Counter()), o componente não é reativo
render(html`<${Counter} />`, '#app')

Agrupa múltiplas atualizações de estado: cada estado alterado notifica seus watchers uma única vez, com o valor final, no fim do batch mais externo.

function batch(fn: () => void): void

Parâmetros:

  • fn: Função síncrona que contém as atualizações

Exemplo:

import { createState, batch } from '@_bashell/slash'
const name = createState('')
const age = createState(0)
name.watch((v) => console.log('name:', v))
age.watch((v) => console.log('age:', v))
// Sem batch: cada set notifica na hora
name.set('John')
age.set(30)
// Com batch: uma notificação por estado alterado, no fim
batch(() => {
name.set('Jane')
name.set('Joan') // só 'Joan' é notificado
age.set(31)
})

Semântica:

  • Só notificam os estados cujo valor mudou; sem mudança, ninguém é notificado.
  • Batches aninhados notificam apenas no fim do batch mais externo.
  • Erros de watchers são isolados: todos os watchers rodam e o primeiro erro é relançado no final.
  • Se fn lançar, as notificações do que já mudou ainda acontecem e o erro chega a quem chamou batch.
  • batch não funciona com funções assíncronas.

Detalhes em Batch Updates.


Renderiza uma view para HTML (síncrono) e devolve também o estado a serializar.

function renderToString(view: Child | (() => Child)): {
html: string
state: Record<string, unknown>
}

Parâmetros:

  • view: filho ou função que retorna o filho (use htmlString nos templates)

Retorna: { html, state }. Reativos com get() + subscribe() (como o Router) são emitidos entre marcadores <!--reactive-start:id--> sem gravar o valor em state. Um State não é reativo no SSR: interpole state.get() para renderizar o valor.

Exemplo:

import { htmlString, renderToString, serializeStateForScript } from '@_bashell/slash/ssr'
const { html, state } = renderToString(() => htmlString`<h1>Hello SSR</h1>`)
const page = `<div id="app">${html}</div>
<script id="__SLASH_STATE__" type="application/json">${serializeStateForScript(state)}</script>`

html é sempre uma string. Monte o documento (<!DOCTYPE>, <head>, scripts) com um template literal comum; o que você colocar nele à mão (um título, por exemplo) precisa ser escapado por você.


Serializa o state para uso dentro de <script type="application/json">.

function serializeStateForScript(state: unknown): string

Faz JSON.stringify e escapa <, >, &, U+2028 e U+2029, para que os valores não possam fechar a tag <script>. Use sempre no lugar de JSON.stringify cru.


Renderiza uma view em chunks (async generator de strings). O último chunk é a tag <script id="__SLASH_STATE__"> com o estado já serializado.

function renderToStream(
view: Child | (() => Child)
): AsyncGenerator<string, void, unknown>

Exemplo:

import { htmlString, renderToStream } from '@_bashell/slash/ssr'
const App = () => htmlString`<h1>Hello Streaming</h1>`
response.writeHead(200, { 'Content-Type': 'text/html' })
for await (const chunk of renderToStream(App)) {
response.write(chunk)
}
response.end()

Template tag que renderiza HTML no servidor e devolve um SafeHtml. Toda string interpolada é escapada. Templates htmlString aninhados, componentes e listas de templates (items.map(...)) entram no pai sem escape duplo porque são SafeHtml.

const htmlString: (strings: TemplateStringsArray, ...values: unknown[]) => SafeHtml

Uso:

import { htmlString } from '@_bashell/slash/ssr'
const title = 'My Page <script>'
const page = htmlString`<h1>${title}</h1>`
String(page)
// <h1>My Page &lt;script&gt;</h1>

Use String(page) (ou renderToString(() => page).html) quando precisar de uma string. Valores dinâmicos dentro de <script>/<style> são descartados (com aviso em dev); para JSON use unsafeHtml(serializeStateForScript(dados)). Um < literal dentro de um <script> estático do template é lido como tag (limitação do htm): coloque esse código em unsafeHtml(...).


Marca uma string como marcação confiável, para ser emitida como HTML sem escape. Funciona em html (cliente) e em htmlString (servidor).

function unsafeHtml(html: string): SafeHtml
import { html, unsafeHtml } from '@_bashell/slash/core'
html`<button>${unsafeHtml('<svg viewBox="0 0 8 8"><circle cx="4" cy="4" r="3"/></svg>')} Salvar</button>`

function isSafeHtml(x: unknown): x is SafeHtml

SafeHtml é o tipo devolvido por htmlString e unsafeHtml; toString() devolve a marcação. isSafeHtml não pode ser enganado por JSON vindo da rede (a marca é uma propriedade própria com Symbol.for). Um SafeHtml guardado em estado reativo perde a marca ao ser serializado para hidratação e volta como texto; reembrulhe com unsafeHtml no cliente.


Marca uma URL confiável cujo esquema a política de URLs bloqueia (por exemplo myapp://abrir/42).

function unsafeUrl(url: string): SafeUrl
import { html, unsafeUrl } from '@_bashell/slash/core'
html`<a href=${unsafeUrl('myapp://abrir/42')}>Abrir no app</a>`

function isSafeUrl(x: unknown): x is SafeUrl

SafeUrl é o tipo devolvido por unsafeUrl. Assim como no SafeHtml, a marca não é forjável por JSON e se perde ao serializar estado para hidratação (a URL volta a ser sanitizada).

Veja a política completa de URLs em Segurança.


function sanitizeUrl(attr: string, value: string, tag?: string): string
const BLOCKED_URL: 'about:blank#blocked'

Aplica a política de URLs do Slash a um valor e devolve o próprio valor ou BLOCKED_URL. Exportados por @_bashell/slash/core e @_bashell/slash/ssr. attr é o nome do atributo ('href', 'src'…) e tag (opcional, por exemplo 'img') importa porque blob: e data:image/svg+xml só passam em contextos de imagem e mídia. Em dev, um valor bloqueado emite o mesmo aviso dos templates.

import { sanitizeUrl, BLOCKED_URL } from '@_bashell/slash/core'
const base = new URL('https://app.example.com')
function destinoSeguro(next: string): string {
if (sanitizeUrl('href', next) === BLOCKED_URL) return '/'
try {
const url = new URL(next, base)
return url.origin === base.origin ? url.pathname + url.search + url.hash : '/'
} catch {
return '/'
}
}

sanitizeUrl valida o esquema, não o destino: //evil.com e https://evil.com passam. Para um redirect, confira também a origem, como acima.


Cria um loader isomórfico com cache no cliente. No servidor o loader sempre executa (sem cache).

function createLoader<T>(
fn: (ctx: LoaderContext) => T | Promise<T>,
options: LoaderOptions
): (ctx: LoaderContext) => Promise<T>

Contexto e opções:

type LoaderContext = {
params: Record<string, string>
request?: Request
isServer: boolean
}
type LoaderOptions = {
key: string // identifica o loader no cache (obrigatório)
ttl?: number // tempo de vida do cache em ms (padrão: 5 minutos)
revalidate?: boolean // ignora o cache e executa de novo
}

A chave de cache no cliente é `${key}:${JSON.stringify(ctx.params)}`.

Exemplo:

import { createLoader } from '@_bashell/slash'
const userLoader = createLoader(
async ({ params }) => {
const res = await fetch(`/api/users/${params.id}`)
return res.json()
},
{ key: 'user', ttl: 5 * 60 * 1000 }
)
// Uso (assíncrono)
const user = await userLoader({ params: { id: '123' }, isServer: false }) // carrega e cacheia
const same = await userLoader({ params: { id: '123' }, isServer: false }) // vem do cache

Limpa o cache de loaders no cliente.

function invalidateLoader(key?: string): void

Parâmetros:

  • key: key do loader cujas entradas serão removidas (opcional). Sem argumento, limpa todo o cache.
import { invalidateLoader } from '@_bashell/slash'
invalidateLoader('user') // todas as entradas do loader com key 'user'
invalidateLoader() // todo o cache

serializeLoaderData() e deserializeLoaderData()

Section titled “serializeLoaderData() e deserializeLoaderData()”
function serializeLoaderData(data: Record<string, unknown>): string // JSON.stringify + escape para <script>
function deserializeLoaderData(serialized: string): Record<string, unknown> // {} se inválido

Injeta no cache do cliente dados que vieram do servidor.

function hydrateLoaderCache(data: Record<string, unknown>): void

Exemplo (Cliente):

import { hydrateLoaderCache, deserializeLoaderData } from '@_bashell/slash'
const serialized = document.getElementById('__LOADER_DATA__')?.textContent || '{}'
hydrateLoaderCache(deserializeLoaderData(serialized))

Exemplo (Servidor): veja Data Loading.


Detecta se o código está rodando no servidor.

function isServer(): boolean

Retorna: true se no servidor, false no cliente

Exemplo:

import { isServer } from '@_bashell/slash'
if (isServer()) {
// Código apenas para servidor
console.log('Running on server')
} else {
// Código apenas para cliente
console.log('Running on client')
}

Cria um roteador. O retorno é um State<RouterState> com métodos extras de navegação.

function createRouter(config: RouterConfig): RouterInstance

Configuração:

type RouterConfig = {
routes: RouteConfig[]
mode?: 'history' | 'hash' // padrão: 'history'
fallback?: string // caminho usado quando nenhuma rota casa
guards?: NavigationGuard[] // guards globais
initialPath?: string // caminho inicial (SSR e hidratação)
}
type RouteConfig = {
path: string // ex.: '/users/:id'
component: (state: RouterState) => any
name?: string
meta?: Record<string, any>
guards?: NavigationGuard[] // guards desta rota
children?: RouteConfig[] // rotas aninhadas
}

Um guard recebe (to, from) e retorna void/true (permite), false (bloqueia) ou uma string (redireciona). Não há lazy loading, data loaders nem curingas (*): use fallback para o 404.

Exemplo:

import { html, render } from '@_bashell/slash'
import { createRouter, Router, Link } from '@_bashell/slash/router'
const router = createRouter({
mode: 'history',
fallback: '/404',
routes: [
{ path: '/', component: () => html`<h1>Home</h1>` },
{ path: '/users/:id', component: (state) => html`<h1>User ${state.params.id}</h1>`, meta: { requiresAuth: true } },
{ path: '/404', component: () => html`<h1>Not found</h1>` },
],
guards: [(to) => { console.log(`Navigating to ${to.path}`) }],
})
const App = () => html`
<div>
<nav><${Link} to="/" router=${router}>Home<//></nav>
<main><${Router} router=${router} /></main>
</div>
`
render(html`<${App} />`, '#app')

Renderiza o component da rota atual e se atualiza a cada navegação. É um reativo (get() + subscribe()), então funciona como componente em qualquer posição do template.

html`<${Router} router=${router} />`
// a forma direta também é válida: ${Router({ router })}

No servidor, Router funciona dentro de htmlString/renderToString: o HTML da rota é emitido entre marcadores <!--reactive-start:id--> e não é gravado em state. Passe initialPath ao createRouter para resolver a rota no servidor. Veja SSR.


Renderiza um <a href> cujo clique é interceptado e chama router.push(). Props extras (como class) são repassadas ao <a>.

html`<${Link} to="/about" router=${router} class="nav-link">About<//>`

Props: to: string, router: RouterInstance (obrigatório), external?: boolean e children.

to precisa ser um caminho do app (/x, ?q ou #h). Qualquer outra coisa (./x, ../x, about, //host, javascript:, https://...) não navega: o clique recebe preventDefault e o href vira about:blank#blocked. Para um site externo, use external: só libera http(s), mailto:, sms: e tel: e adiciona rel="noopener noreferrer".

html`<${Link} to="https://example.com/docs" external router=${router}>Docs<//>`
// <a href="https://example.com/docs" rel="noopener noreferrer">Docs</a>

?q e #h são relativos à página atual. O roteador só intercepta cliques em links http(s) da mesma origem (respeitando <base>); com ctrl, meta, shift ou alt, botão que não seja o esquerdo, target diferente de _self ou download, o navegador age normalmente. Se o navegador recusar um router.push()/replace(), a promise rejeita com Navigation failed: ... e o estado do roteador continua igual à URL.


router.get() // { currentRoute, params, query, meta, isNavigating }
router.watch((state) => { /* ... */ })
await router.push('/users/7') // navega e adiciona ao histórico
await router.replace('/about') // navega sem adicionar ao histórico
router.back()
router.forward()
router.go(-2) // volta 2 páginas
router.currentRoute() // RouteMatch | null

Componente com fallback para erros. Limitação importante: o html avalia os filhos de forma eager, antes de chamar o componente. Um erro lançado ao construir os filhos (por exemplo <${Quebrado} /> dentro do boundary) acontece antes do ErrorBoundary rodar e não é capturado por ele. Para proteger uma subárvore, use safeRender, que recebe uma função e chama o fallback se ela lançar.

function ErrorBoundary(props: ErrorBoundaryProps): Child
type ErrorBoundaryProps = {
fallback: (error: Error) => Child // obrigatório
onError?: (error: Error, errorInfo: { componentStack?: string }) => void
children: Child
}

Exemplo (proteja a subárvore com safeRender):

import { html, safeRender } from '@_bashell/slash'
function Quebrado(): never {
throw new Error('boom')
}
const view = safeRender(
() => html`<main><${Quebrado} /></main>`,
(error) => html`
<div class="error">
<h2>Something went wrong</h2>
<p>${error.message}</p>
</div>
`
)

Envolve uma função assíncrona e devolve uma tupla [safeFn, getError].

function catchAsync<T>(
fn: () => Promise<T>,
onError?: (error: Error) => void
): [() => Promise<T | null>, () => Error | null]
  • safeFn() executa fn; se lançar, guarda o erro, chama onError e resolve com null.
  • getError() devolve o último erro (ou null; é limpo no início de cada safeFn()).

Exemplo:

import { catchAsync } from '@_bashell/slash'
const [loadData, getError] = catchAsync(
async () => {
const res = await fetch('/api/data')
return res.json()
},
(error) => {
console.error('Fetch failed:', error)
}
)
const data = await loadData() // dados, ou null se falhou
if (data === null) {
console.log('Erro:', getError()?.message)
}

Executa uma função de view e, se ela lançar, registra o erro no console e devolve o fallback.

function safeRender(
view: () => Child,
fallback: (error: Error) => Child // obrigatório
): Child

Exemplo:

import { html, safeRender } from '@_bashell/slash'
const App = () => html`<h1>Hello</h1>`
const view = safeRender(
() => App(),
(error) => html`<h1>Error: ${error.message}</h1>`
)

No SSR, use htmlString (de @_bashell/slash/ssr) em vez de html na view e no fallback.


Configura um handler global de erros.

function setupGlobalErrorHandler(
onError: (error: Error, source: 'render' | 'runtime') => void
): void

Exemplo:

import { setupGlobalErrorHandler } from '@_bashell/slash'
setupGlobalErrorHandler((error) => {
console.error('Global error:', error)
// Enviar para serviço de logging
})

Importados de @_bashell/slash (ou de @_bashell/slash/forms). Não existe um tipo FormHelpers<T>: o Slash oferece funções pequenas que ligam inputs a um State.

FunçãoPara que serve
textFieldControl(state)Props value/onInput para <input> e <textarea>, ligados a State<{ value: string }>
checkboxControl(state)Props checked/onChange para checkbox, com State<{ value: boolean }>
radioControl(state, value)Props para um radio do grupo, com State<{ value: string }>
SelectControl(state)Props para <select>, com State<{ value: string }>
getText(e), getChecked(e), getSelectValue(e)Leem o valor de eventos tipados
onSubmit(cb)preventDefault() e entrega os dados do <form> como objeto
onReset(handler), onButtonClick(handler)Handlers tipados
formToObject(form)Converte um <form> em objeto sem protótipo (Object.create(null); campos repetidos viram arrays). Use Object.hasOwn(obj, 'campo') em vez de obj.hasOwnProperty(...)
delegate(root, type, selector, handler)Delegação de eventos; retorna função para remover o listener

Exemplo:

import { html, render, onSubmit } from '@_bashell/slash'
const log = (data: Record<string, unknown>) => console.log(data)
const Form = () => html`
<form onSubmit=${onSubmit(log)}>
<input name="email" type="email" />
<button type="submit">Enviar</button>
</form>
`
render(Form(), '#app')

Veja os conceitos, o comportamento de foco e os exemplos completos em Formulários e Form Validation.


Habilita/desabilita modo de desenvolvimento.

function setDevMode(enabled: boolean): void

Exemplo:

import { setDevMode } from '@_bashell/slash'
setDevMode(process.env.NODE_ENV === 'development')

Habilita/desabilita warnings no console.

function setWarningsEnabled(enabled: boolean): void

Verifica se está em modo dev.

function isDevMode(): boolean

Verifica se warnings estão habilitados.

function isWarningsEnabled(): boolean

Propriedades de um elemento (Record<string, unknown> | null). Handlers de evento usam camelCase (onClick, onInput).

type Props = Record<string, unknown> | null
type EventHandler = EventListenerOrEventListenerObject

Tipo retornado por createState().

type State<T> = {
get: () => T
set: (value: T) => void
// O callback recebe só o novo valor; retorna a função de cancelamento
watch: (callback: (value: T) => void) => () => void
// Presentes quando o state foi criado com { enableHistory: true }
getHistory?: () => Readonly<StateHistory<T>>
clearHistory?: () => void
}

Tipos válidos para children de elementos.

type Child =
| Node
| string
| SafeHtml
| number
| boolean
| null
| undefined
| Reactive<unknown> // { get(), subscribe(fn) }
| Child[]
| readonly Child[]