Skip to content

Roteamento

O Router do Slash é um sistema de roteamento para criar Single Page Applications (SPAs). Ele permite navegar entre diferentes “páginas” sem recarregar o browser, mantendo a URL sincronizada com o conteúdo exibido.

  • ✅ Modo History API ou Hash mode
  • ✅ Parâmetros dinâmicos (/users/:id)
  • ✅ Query strings (?search=foo&page=2)
  • ✅ Navigation guards (proteção de rotas)
  • ✅ Rotas aninhadas (nested routes)
  • ✅ Metadados de rota (custom data)
  • ✅ SSR-ready (Server-Side Rendering)
  • ✅ Programmatic navigation (push, replace, back, forward)

Use createRouter() para criar uma instância do router:

import { createRouter } from '@_bashell/slash'
import type { RouterConfig } from '@_bashell/slash'
const router = createRouter({
mode: 'history', // 'history' ou 'hash'
routes: [
{
path: '/',
component: () => html`<h1>Home</h1>`
},
{
path: '/about',
component: () => html`<h1>Sobre</h1>`
}
]
})
interface RouterConfig {
/** Array de definições de rotas */
routes: RouteConfig[]
/** Modo do router (padrão: "history") */
mode?: 'history' | 'hash'
/** Path de fallback para 404 (padrão: null) */
fallback?: string
/** Guards globais de navegação */
guards?: NavigationGuard[]
/** Path inicial (para SSR) */
initialPath?: string
}

Cada rota é definida por um objeto RouteConfig:

interface RouteConfig {
/** Padrão do path (ex: "/users/:id") */
path: string
/** Componente a ser renderizado */
component: (state: RouterState) => Child
/** Nome da rota (opcional) */
name?: string
/** Metadados da rota */
meta?: RouteMeta
/** Guards de navegação específicos */
guards?: NavigationGuard[]
/** Rotas filhas/aninhadas */
children?: RouteConfig[]
}
import { html, createRouter } from '@_bashell/slash'
const router = createRouter({
mode: 'history',
routes: [
{
path: '/',
name: 'home',
component: () => html`
<div class="home">
<h1>Bem-vindo!</h1>
<p>Esta é a página inicial</p>
</div>
`
},
{
path: '/about',
name: 'about',
meta: { requiresAuth: false },
component: () => html`
<div class="about">
<h1>Sobre nós</h1>
<p>Conheça nossa história</p>
</div>
`
}
]
})

O componente Router renderiza o componente da rota atual e se atualiza a cada navegação. Use-o como componente, em qualquer posição do template:

import { html, render } from '@_bashell/slash'
import { Router } from '@_bashell/slash/router'
const App = () => html`
<div id="app">
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
<main>
<${Router} router=${router} />
</main>
</div>
`
render(html`<${App} />`, document.getElementById('root')!)

A forma direta ${Router({ router })} também continua válida.

O componente Link cria links de navegação que usam o router:

import { html } from '@_bashell/slash'
import { Link } from '@_bashell/slash'
function Navigation({ router }) {
return html`
<nav>
<${Link} to="/" router=${router}>Home</${Link}>
<${Link} to="/about" router=${router}>About</${Link}>
<${Link} to="/users/123" router=${router}>Perfil</${Link}>
</nav>
`
}

Link aceita somente caminhos do app: /x, ?q ou #h. Qualquer outra coisa (./x, ../x, about, //host, javascript:, https://...) nunca navega: o href vira about:blank#blocked e o dev mode avisa. Para um link externo de verdade, use a prop external:

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

external só libera http(s), mailto:, tel: e sms: e adiciona rel="noopener noreferrer". ?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. Barras invertidas em router.push() viram o caminho da mesma origem ('/\\evil' vai para /evil), e um push recusado pelo navegador rejeita com Navigation failed: ... sem dessincronizar o roteador. Veja Segurança.

Rotas podem ter segmentos dinâmicos que extraem valores da URL:

const router = createRouter({
routes: [
{
path: '/users/:id',
component: (state) => {
const userId = state.params.id
return html`
<div>
<h1>Perfil do Usuário</h1>
<p>ID: ${userId}</p>
</div>
`
}
},
{
path: '/posts/:postId/comments/:commentId',
component: (state) => {
const { postId, commentId } = state.params
return html`
<div>
<h2>Post ${postId}</h2>
<p>Comentário ${commentId}</p>
</div>
`
}
}
]
})

Os parâmetros estão disponíveis em state.params:

type RouteParams = Record<string, string>
// Exemplo: /users/123
// state.params = { id: "123" }
// Exemplo: /posts/456/comments/789
// state.params = { postId: "456", commentId: "789" }

Query strings são automaticamente parseadas e disponíveis em state.query:

const router = createRouter({
routes: [
{
path: '/search',
component: (state) => {
const { q, page } = state.query
// URL: /search?q=slash&page=2
// q = "slash", page = "2"
return html`
<div>
<h1>Busca: ${q || 'vazia'}</h1>
<p>Página: ${page || '1'}</p>
</div>
`
}
}
]
})

O router expõe métodos para navegação programática:

// Navegar para uma nova rota (adiciona ao histórico)
await router.push('/about')
// Substituir rota atual (não adiciona ao histórico)
await router.replace('/login')
// Voltar na história
router.back()
// Avançar na história
router.forward()
// Ir para entrada específica na história
router.go(-2) // volta 2 páginas
router.go(1) // avança 1 página
import { html, createState } from '@_bashell/slash'
// O state fica fora do componente: se fosse criado dentro, seria recriado a cada render
const credentials = createState({ email: '', password: '' })
function LoginForm({ router }) {
const handleSubmit = async (e: Event) => {
e.preventDefault()
const { email, password } = credentials.get()
try {
const response = await login(email, password)
if (response.ok) {
// Redirecionar para dashboard após login
await router.push('/dashboard')
}
} catch (error) {
console.error('Erro ao fazer login:', error)
}
}
return html`
<form onSubmit=${handleSubmit}>
<input
type="email"
placeholder="Email"
onInput=${(e) => {
const state = credentials.get()
credentials.set({
...state,
email: (e.target as HTMLInputElement).value
})
}}
/>
<input
type="password"
placeholder="Senha"
onInput=${(e) => {
const state = credentials.get()
credentials.set({
...state,
password: (e.target as HTMLInputElement).value
})
}}
/>
<button type="submit">Entrar</button>
</form>
`
}

O router é um State<RouterState>, então você pode observar mudanças:

interface RouterState {
/** Rota atual */
currentRoute: RouteMatch | null
/** Parâmetros da rota */
params: RouteParams
/** Query parameters */
query: RouteQuery
/** Metadados da rota */
meta: RouteMeta
/** Flag de navegação em progresso */
isNavigating: boolean
}
import { createState } from '@_bashell/slash'
// Observar mudanças na rota
router.watch((state) => {
console.log('Navegou para:', state.currentRoute?.path)
console.log('Params:', state.params)
console.log('Query:', state.query)
})
// Criar estado derivado
const currentPath = createState('')
router.watch((state) => {
currentPath.set(state.currentRoute?.path || '/')
})
import { html, render, createRouter } from '@_bashell/slash'
import { Router, Link } from '@_bashell/slash'
// Componentes das páginas
const Home = () => html`
<div>
<h1>Blog Slash</h1>
<p>Bem-vindo ao nosso blog!</p>
</div>
`
const PostList = () => {
const posts = [
{ id: 1, title: 'Introdução ao Slash' },
{ id: 2, title: 'Roteamento Avançado' },
{ id: 3, title: 'SSR com Slash' }
]
return html`
<div>
<h1>Posts</h1>
<ul>
${posts.map(post => html`
<li>
<${Link} to=${`/posts/${post.id}`} router=${router}>
${post.title}
</${Link}>
</li>
`)}
</ul>
</div>
`
}
const PostDetail = (state) => {
const postId = state.params.id
return html`
<div>
<h1>Post #${postId}</h1>
<p>Conteúdo do post...</p>
<button onClick=${() => router.back()}>
Voltar
</button>
</div>
`
}
// Criar router
const router = createRouter({
mode: 'history',
routes: [
{
path: '/',
component: Home
},
{
path: '/posts',
component: PostList
},
{
path: '/posts/:id',
component: PostDetail
}
]
})
// Renderizar app
const app = html`
<div id="app">
<nav>
<${Link} to="/" router=${router}>Home</${Link}>
<${Link} to="/posts" router=${router}>Posts</${Link}>
</nav>
<main>
<${Router} router=${router} />
</main>
</div>
`
render(app, document.getElementById('root')!)

Passe initialPath para que o roteador resolva a rota de forma síncrona na criação (no servidor e na hidratação). No cliente, sem initialPath, ele usa window.location.

No servidor, Router funciona dentro de htmlString/renderToString: o HTML da rota é emitido de verdade, entre marcadores <!--reactive-start:id-->, e não é gravado no state serializado.

import { createRouter, Router } from '@_bashell/slash/router'
import { htmlString, renderToString } from '@_bashell/slash/ssr'
const router = createRouter({
routes: [{ path: '/', component: () => htmlString`<h1>Home</h1>` }],
initialPath: '/',
})
const { html } = renderToString(
() => htmlString`<main>${Router({ router })}</main>`,
)
// <main><!--reactive-start:s0--><h1>Home</h1><!--reactive-end:s0--></main>

Veja SSR para o fluxo completo de renderização e hidratação.