Skip to content

Renderização Básica

Slash oferece duas formas de criar elementos DOM: a função h() (hyperscript) e o template tag html (HTM).

O template tag html permite escrever markup HTML-like diretamente no JavaScript/TypeScript sem necessidade de transpilação:

import { html } from '@ezbug/slash'
const element = html`
<div class="container">
<h1>Hello, World!</h1>
<p>This is a paragraph</p>
</div>
`

Vantagens:

  • Sintaxe familiar (similar a JSX)
  • Sem build step necessário
  • Type safety completo
  • Syntax highlighting em editores modernos

Para uma abordagem mais programática, use a função h():

import { h } from '@ezbug/slash'
const element = h('div', { class: 'container' },
h('h1', null, 'Hello, World!'),
h('p', null, 'This is a paragraph')
)

Assinatura:

function h(
tag: string | Component,
props: Props | null,
...children: Child[]
): Node
// HTM
const greeting = html`
<div class="greeting">
<h1>Hello, ${name}!</h1>
</div>
`
// Hyperscript equivalente
const greeting = h('div', { class: 'greeting' },
h('h1', null, `Hello, ${name}!`)
)
import { html } from '@ezbug/slash'
// Div simples
const div = html`<div>Content</div>`
// Com classes
const styled = html`<div class="container flex">Styled</div>`
// Com IDs
const unique = html`<div id="app">App Root</div>`
// Elementos vazios
const input = html`<input type="text" />`
const br = html`<br />`

Slash detecta automaticamente elementos SVG e aplica o namespace correto:

import { html } from '@ezbug/slash'
const icon = html`
<svg width="24" height="24" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" fill="blue" />
<path d="M12 6v6l4 2" stroke="white" stroke-width="2" />
</svg>
`

Tags SVG reconhecidas automaticamente: src/utils/constants.ts

  • svg, circle, path, rect, line, polyline, polygon
  • ellipse, text, g, defs, use, symbol, clipPath
  • linearGradient, radialGradient, stop, mask, pattern

Props são passadas como objeto no segundo argumento de h() ou como atributos no html:

// Com html
const button = html`
<button
class="btn btn-primary"
disabled=${false}
data-id="123"
>
Click Me
</button>
`
// Com h()
const button = h('button', {
class: 'btn btn-primary',
disabled: false,
'data-id': '123'
}, 'Click Me')
const input = html`
<input
type="text"
placeholder="Enter name"
required=${true}
maxlength="50"
/>
`

Propriedades especiais são definidas via assignment direto:

const input = html`
<input
type="checkbox"
checked=${true}
value="option1"
/>
`

Properties especiais:

  • value (inputs, textarea, select)
  • checked (checkbox, radio)
  • selected (option)
  • disabled
  • className (alternativa a class)

Classes podem ser strings, arrays ou objetos:

// String simples
const el1 = html`<div class="container"></div>`
// Array (classes condicionais)
const classes = ['btn', isActive && 'active', 'large']
const el2 = html`<button class=${classes}>Click</button>`
// Objeto (toggle classes)
const classMap = { active: true, disabled: false, large: true }
const el3 = html`<button class=${classMap}>Click</button>`

Implementação: src/rendering/props-core.ts

// String CSS
const el1 = html`<div style="color: red; font-size: 16px"></div>`
// Objeto CSS
const styles = { color: 'red', fontSize: '16px' }
const el2 = html`<div style=${styles}></div>`

Children podem ser:

  • Strings e números
  • Elementos DOM (Node)
  • Arrays (aninhados)
  • Reactive/State (atualizados automaticamente)
  • null, undefined, false (ignorados)
import { html, createState } from '@ezbug/slash'
const count = createState(0)
const name = "Alice"
const element = html`
<div>
<h1>Hello, ${name}!</h1>
<p>Count: ${count}</p>
${count.get() > 5 && html`<p>Count is high!</p>`}
<ul>
${[1, 2, 3].map(n => html`<li>Item ${n}</li>`)}
</ul>
</div>
`

Implementação: src/rendering/children.ts

A função render() monta elementos no DOM:

import { render, html } from '@ezbug/slash'
const App = () => html`
<div>
<h1>My App</h1>
</div>
`
// Renderizar em elemento existente
const root = document.getElementById('app')
render(App(), root)
// Ou usando seletor CSS
render(App(), '#app')
function render(
view: Child | (() => Child),
container: Element | string | null | undefined
): Node | Node[]

Parâmetros:

  • view: Elemento ou função que retorna elemento
  • container: Elemento DOM ou seletor CSS

Retorno: Node único ou array de Nodes inseridos

Implementação: src/rendering/render.ts

render() limpa children anteriores do container automaticamente:

const root = document.getElementById('app')
// Primeira renderização
render(html`<div>First</div>`, root)
// Segunda renderização - remove 'First' antes
render(html`<div>Second</div>`, root)

Se o container já tem conteúdo renderizado pelo servidor E existe um <script id="__SLASH_STATE__">, render() hidrata em vez de substituir:

// Server-side
const html = renderToString(App())
const output = `
<div id="app">${html}</div>
<script id="__SLASH_STATE__" type="application/json">
${JSON.stringify(stateData)}
</script>
`
// Client-side
render(App(), '#app') // Hidrata DOM existente

render() valida o container e lança erros claros em dev mode:

// Container não encontrado
render(App(), '#non-existent')
// Error: [slash] render(): selector "#non-existent" not found
// Container null
render(App(), null)
// Error: [slash] render(): container Element is required (received null/undefined)

Event handlers são passados como props prefixadas com on:

import { html, createState } from '@ezbug/slash'
const Counter = () => {
const count = createState(0)
const increment = () => count.set(count.get() + 1)
const decrement = () => count.set(count.get() - 1)
return html`
<div>
<p>Count: ${count}</p>
<button onclick=${increment}>+</button>
<button onclick=${decrement}>-</button>
</div>
`
}

Todos os eventos DOM padrão são suportados:

const element = html`
<input
type="text"
oninput=${(e) => console.log(e.target.value)}
onchange=${handleChange}
onfocus=${handleFocus}
onblur=${handleBlur}
onkeydown=${handleKeyDown}
onkeyup=${handleKeyUp}
/>
`

Event handlers recebem o evento nativo do browser:

const handleClick = (event: MouseEvent) => {
console.log('Clicked at', event.clientX, event.clientY)
event.preventDefault()
event.stopPropagation()
}
const button = html`
<button onclick=${handleClick}>Click Me</button>
`

Para opções avançadas, use array tuple [handler, options]:

const handleScroll = (e: Event) => {
console.log('Scrolled')
}
const container = html`
<div onscroll=${[handleScroll, { passive: true, capture: false }]}>
Content
</div>
`

Event Options:

  • capture: boolean - Captura na fase de capturing
  • passive: boolean - Listener não chama preventDefault()
  • once: boolean - Listener executado apenas uma vez

Implementação: src/rendering/events.ts

import { html } from '@ezbug/slash'
import type { TextFieldEvent } from '@ezbug/slash'
const handleInput = (e: TextFieldEvent<'input'>) => {
const value = e.target.value // Type-safe access
console.log('Input value:', value)
}
const form = html`
<form>
<input type="text" oninput=${handleInput} />
</form>
`

Form event types: src/forms/form.types.ts

Event listeners são automaticamente removidos quando um nó é destruído:

import { destroyNode } from '@ezbug/slash'
const button = html`<button onclick=${handler}>Click</button>`
// Quando não mais necessário
destroyNode(button as Node) // Remove listener automaticamente

Implementação: src/lifecycle/cleanup.ts

import { html, createState, render } from '@ezbug/slash'
const ToggleButton = () => {
const isActive = createState(false)
const toggle = () => isActive.set(!isActive.get())
return html`
<button
class=${isActive.get() ? 'active' : ''}
onclick=${toggle}
>
${isActive.get() ? 'Active' : 'Inactive'}
</button>
`
}
render(ToggleButton(), '#app')
import { html, createState, render } from '@ezbug/slash'
const TodoList = () => {
const todos = createState<string[]>(['Buy milk', 'Walk dog'])
const input = createState('')
const addTodo = () => {
const value = input.get().trim()
if (value) {
todos.set([...todos.get(), value])
input.set('')
}
}
return html`
<div>
<h1>Todos</h1>
<ul>
${todos.get().map(todo => html`<li>${todo}</li>`)}
</ul>
<input
type="text"
value=${input}
oninput=${(e: Event) => input.set((e.target as HTMLInputElement).value)}
/>
<button onclick=${addTodo}>Add</button>
</div>
`
}
render(TodoList(), '#app')
import { html, createState, render } from '@ezbug/slash'
const LoginForm = () => {
const email = createState('')
const password = createState('')
const error = createState('')
const handleSubmit = (e: Event) => {
e.preventDefault()
if (!email.get().includes('@')) {
error.set('Invalid email')
return
}
if (password.get().length < 6) {
error.set('Password must be at least 6 characters')
return
}
error.set('')
console.log('Login:', { email: email.get(), password: password.get() })
}
return html`
<form onsubmit=${handleSubmit}>
<h1>Login</h1>
${error.get() && html`<p class="error">${error}</p>`}
<input
type="email"
placeholder="Email"
value=${email}
oninput=${(e: Event) => email.set((e.target as HTMLInputElement).value)}
/>
<input
type="password"
placeholder="Password"
value=${password}
oninput=${(e: Event) => password.set((e.target as HTMLInputElement).value)}
/>
<button type="submit">Login</button>
</form>
`
}
render(LoginForm(), '#app')

Agora que você domina renderização básica, explore:

  1. Sistema de Estado - State management reativo avançado
  2. Componentes - Criar componentes reutilizáveis
  3. Batch Updates - Otimizar múltiplas atualizações