SPA com Roteamento
SPA com Roteamento
Section titled “SPA com Roteamento”Este exemplo demonstra como criar uma Single Page Application (SPA) completa usando o sistema de roteamento do Slash. Você aprenderá sobre navegação client-side, route params, guards, lazy loading e transições de página.
Funcionalidades
Section titled “Funcionalidades”- ✅ Roteamento client-side (SPA)
- ✅ Navegação com
<Link>e programática - ✅ Route params e query strings
- ✅ Navigation guards (auth, validação)
- ✅ Lazy loading de componentes
- ✅ Transições de página
- ✅ 404 e tratamento de erros
- ✅ Hash mode e History mode
- ✅ Nested routes
Estrutura do Projeto
Section titled “Estrutura do Projeto”src/├── index.ts # Entry point├── router.ts # Configuração do router├── pages/│ ├── Home.ts # Página inicial│ ├── About.ts # Sobre│ ├── Products.ts # Lista de produtos│ ├── ProductDetail.ts # Detalhe do produto│ ├── Dashboard.ts # Dashboard (protegido)│ ├── Login.ts # Login│ └── NotFound.ts # 404├── components/│ ├── Layout.ts # Layout base│ ├── Header.ts # Header com navegação│ └── PrivateRoute.ts # HOC para rotas protegidas├── services/│ └── auth.ts # Serviço de autenticação└── types.ts # TiposTipos TypeScript
Section titled “Tipos TypeScript”export interface User { id: string name: string email: string role: 'user' | 'admin'}
export interface Product { id: string name: string description: string price: number category: string image: string}
export interface RouteParams { id?: string [key: string]: string | undefined}Serviço de Autenticação
Section titled “Serviço de Autenticação”import { createState } from '@ezbug/slash'import type { User } from '../types'
// Estado de autenticaçãoexport const authState = createState<{ user: User | null isAuthenticated: boolean isLoading: boolean}>({ user: null, isAuthenticated: false, isLoading: true})
// Carrega usuário do localStorage ao iniciarexport const initAuth = (): void => { const savedUser = localStorage.getItem('user') if (savedUser) { try { const user = JSON.parse(savedUser) authState.set({ user, isAuthenticated: true, isLoading: false }) } catch (e) { authState.set({ user: null, isAuthenticated: false, isLoading: false }) } } else { authState.set({ user: null, isAuthenticated: false, isLoading: false }) }}
// Loginexport const login = async (email: string, password: string): Promise<void> => { // Simula API call await new Promise(resolve => setTimeout(resolve, 1000))
// Mock user const user: User = { id: '1', name: 'John Doe', email, role: 'user' }
localStorage.setItem('user', JSON.stringify(user)) authState.set({ user, isAuthenticated: true, isLoading: false })}
// Logoutexport const logout = (): void => { localStorage.removeItem('user') authState.set({ user: null, isAuthenticated: false, isLoading: false })}
// Check if user has roleexport const hasRole = (role: string): boolean => { const state = authState.get() return state.isAuthenticated && state.user?.role === role}Configuração do Router
Section titled “Configuração do Router”import { createRouter, type RouteConfig } from '@ezbug/slash'import { authState } from './services/auth'
// Lazy load de páginasconst Home = () => import('./pages/Home')const About = () => import('./pages/About')const Products = () => import('./pages/Products')const ProductDetail = () => import('./pages/ProductDetail')const Dashboard = () => import('./pages/Dashboard')const Login = () => import('./pages/Login')const NotFound = () => import('./pages/NotFound')
// Guard de autenticaçãoconst authGuard = async (to: RouteConfig) => { const state = authState.get()
if (!state.isAuthenticated) { // Redireciona para login return { redirect: '/login', query: { redirect: to.path } } }
return true}
// Guard de adminconst adminGuard = async (to: RouteConfig) => { const state = authState.get()
if (!state.isAuthenticated) { return { redirect: '/login', query: { redirect: to.path } } }
if (state.user?.role !== 'admin') { return { redirect: '/', error: 'Access denied' } }
return true}
// Configuração de rotasconst routes: RouteConfig[] = [ { path: '/', component: Home, meta: { title: 'Home' } }, { path: '/about', component: About, meta: { title: 'About Us' } }, { path: '/products', component: Products, meta: { title: 'Products' } }, { path: '/products/:id', component: ProductDetail, meta: { title: 'Product Details' } }, { path: '/dashboard', component: Dashboard, meta: { title: 'Dashboard', requiresAuth: true }, beforeEnter: authGuard }, { path: '/admin', component: () => import('./pages/Admin'), meta: { title: 'Admin Panel', requiresAuth: true }, beforeEnter: adminGuard }, { path: '/login', component: Login, meta: { title: 'Login' } }, { path: '*', component: NotFound, meta: { title: '404 Not Found' } }]
// Cria routerexport const router = createRouter({ mode: 'history', // ou 'hash' routes, scrollBehavior: (to, from) => { // Scroll to top ao navegar return { top: 0, left: 0 } }})
// Global navigation guardrouter.beforeEach((to, from) => { // Atualiza título da página if (to.meta?.title) { document.title = `${to.meta.title} - My SPA` }
// Log de navegação (analytics) console.log(`Navigating from ${from.path} to ${to.path}`)
return true})
// After navigation hookrouter.afterEach((to, from) => { // Pode usar para fechar modals, resetar estados, etc console.log(`Navigated to ${to.path}`)})Componente Header
Section titled “Componente Header”Header com navegação ativa:
import { html, type VNode } from '@ezbug/slash'import { Link, router } from '@ezbug/slash'import { authState, logout } from '../services/auth'
export const Header = (): VNode => { const auth = authState.get() const currentPath = router.getCurrentRoute()?.path || '/'
const isActive = (path: string): boolean => { return currentPath === path }
const handleLogout = () => { logout() router.push('/') }
return html` <header class="header"> <div class="container"> <nav class="nav"> <div class="nav-brand"> ${Link({ to: '/', children: 'My SPA' })} </div>
<ul class="nav-links"> <li class=${isActive('/') ? 'active' : ''}> ${Link({ to: '/', children: 'Home' })} </li> <li class=${isActive('/products') ? 'active' : ''}> ${Link({ to: '/products', children: 'Products' })} </li> <li class=${isActive('/about') ? 'active' : ''}> ${Link({ to: '/about', children: 'About' })} </li>
${auth.isAuthenticated ? html` <li class=${isActive('/dashboard') ? 'active' : ''}> ${Link({ to: '/dashboard', children: 'Dashboard' })} </li> <li> <button class="btn-link" onclick=${handleLogout}> Logout </button> </li> <li class="user-info"> ${auth.user?.name} </li> ` : html` <li class=${isActive('/login') ? 'active' : ''}> ${Link({ to: '/login', children: 'Login' })} </li> `} </ul> </nav> </div> </header> `}Página Home
Section titled “Página Home”import { html, type VNode } from '@ezbug/slash'import { Link } from '@ezbug/slash'import { authState } from '../services/auth'
export const Home = (): VNode => { const auth = authState.get()
return html` <div class="home"> <section class="hero"> <h1>Welcome to My SPA</h1> <p>A single page application built with Slash</p>
${auth.isAuthenticated ? html` <p class="welcome"> Hello, ${auth.user?.name}! </p> ${Link({ to: '/dashboard', class: 'btn btn-primary', children: 'Go to Dashboard' })} ` : html` ${Link({ to: '/login', class: 'btn btn-primary', children: 'Get Started' })} `} </section>
<section class="features"> <h2>Features</h2> <div class="feature-grid"> <div class="feature-card"> <h3>🚀 Fast Navigation</h3> <p>Client-side routing for instant page transitions</p> </div> <div class="feature-card"> <h3>🔒 Protected Routes</h3> <p>Authentication guards for secure areas</p> </div> <div class="feature-card"> <h3>📦 Lazy Loading</h3> <p>Code splitting for optimal performance</p> </div> <div class="feature-card"> <h3>🎨 Modern UI</h3> <p>Clean and responsive design</p> </div> </div> </section>
<section class="cta"> <h2>Browse Our Products</h2> ${Link({ to: '/products', class: 'btn btn-secondary', children: 'View Products' })} </section> </div> `}Página Products
Section titled “Página Products”Lista de produtos com filtros:
import { html, type VNode, createState } from '@ezbug/slash'import { Link, router } from '@ezbug/slash'import type { Product } from '../types'
// Mock de produtosconst mockProducts: Product[] = [ { id: '1', name: 'Laptop Pro', description: 'High-performance laptop', price: 1299, category: 'electronics', image: '/images/laptop.jpg' }, { id: '2', name: 'Wireless Mouse', description: 'Ergonomic wireless mouse', price: 29, category: 'accessories', image: '/images/mouse.jpg' }, { id: '3', name: 'Mechanical Keyboard', description: 'RGB mechanical keyboard', price: 149, category: 'accessories', image: '/images/keyboard.jpg' }]
export const Products = (): VNode => { const query = router.getCurrentRoute()?.query || {} const categoryFilter = query.category || 'all'
const filterState = createState(categoryFilter)
const categories = ['all', 'electronics', 'accessories']
const filteredProducts = mockProducts.filter(p => filterState.get() === 'all' || p.category === filterState.get() )
const handleFilterChange = (category: string) => { filterState.set(category) router.push({ path: '/products', query: category === 'all' ? {} : { category } }) }
return html` <div class="products-page"> <h1>Our Products</h1>
<div class="filters"> <label>Filter by category:</label> <div class="filter-buttons"> ${categories.map(cat => html` <button class=${`filter-btn ${filterState.get() === cat ? 'active' : ''}`} onclick=${() => handleFilterChange(cat)} > ${cat.charAt(0).toUpperCase() + cat.slice(1)} </button> `)} </div> </div>
<div class="products-grid"> ${filteredProducts.map(product => html` <div class="product-card"> <img src=${product.image} alt=${product.name} /> <h3>${product.name}</h3> <p>${product.description}</p> <div class="product-footer"> <span class="price">$${product.price}</span> ${Link({ to: `/products/${product.id}`, class: 'btn btn-sm', children: 'View Details' })} </div> </div> `)} </div>
${filteredProducts.length === 0 ? html` <div class="no-products"> <p>No products found in this category.</p> </div> ` : null} </div> `}Página ProductDetail
Section titled “Página ProductDetail”Detalhe de produto com route params:
import { html, type VNode } from '@ezbug/slash'import { router, Link } from '@ezbug/slash'import type { Product } from '../types'
// Mock (em produção viria de API)const mockProducts: Record<string, Product> = { '1': { id: '1', name: 'Laptop Pro', description: 'High-performance laptop with latest specs', price: 1299, category: 'electronics', image: '/images/laptop.jpg' }, '2': { id: '2', name: 'Wireless Mouse', description: 'Ergonomic wireless mouse with precision tracking', price: 29, category: 'accessories', image: '/images/mouse.jpg' }}
export const ProductDetail = (): VNode => { const route = router.getCurrentRoute() const productId = route?.params?.id
if (!productId) { return html` <div class="error"> <h2>Product not found</h2> ${Link({ to: '/products', children: '← Back to products' })} </div> ` }
const product = mockProducts[productId]
if (!product) { return html` <div class="error"> <h2>Product not found</h2> ${Link({ to: '/products', children: '← Back to products' })} </div> ` }
const handleAddToCart = () => { alert(`Added ${product.name} to cart!`) }
return html` <div class="product-detail"> ${Link({ to: '/products', class: 'back-link', children: '← Back to products' })}
<div class="product-content"> <div class="product-image"> <img src=${product.image} alt=${product.name} /> </div>
<div class="product-info"> <h1>${product.name}</h1>
<div class="product-category"> Category: ${product.category} </div>
<p class="product-description"> ${product.description} </p>
<div class="product-price"> $${product.price} </div>
<button class="btn btn-primary btn-lg" onclick=${handleAddToCart} > Add to Cart </button> </div> </div>
<div class="related-products"> <h2>Related Products</h2> <p>More products coming soon...</p> </div> </div> `}Página Login
Section titled “Página Login”import { html, type VNode, createState, batch } from '@ezbug/slash'import { router } from '@ezbug/slash'import { login } from '../services/auth'
export const Login = (): VNode => { const formState = createState({ email: '', password: '', isLoading: false, error: '' })
const handleSubmit = async (e: Event) => { e.preventDefault() const state = formState.get()
batch(() => { formState.set({ ...state, isLoading: true, error: '' }) })
try { await login(state.email, state.password)
// Redireciona para onde o usuário queria ir const redirect = router.getCurrentRoute()?.query?.redirect || '/dashboard' router.push(redirect as string) } catch (error: any) { formState.set({ ...formState.get(), isLoading: false, error: error.message || 'Login failed' }) } }
const state = formState.get()
return html` <div class="login-page"> <div class="login-card"> <h1>Login</h1>
${state.error ? html` <div class="alert alert-error"> ${state.error} </div> ` : null}
<form onsubmit=${handleSubmit}> <div class="form-group"> <label for="email">Email</label> <input id="email" type="email" required value=${state.email} oninput=${(e: Event) => { const target = e.target as HTMLInputElement formState.set({ ...state, email: target.value }) }} disabled=${state.isLoading} /> </div>
<div class="form-group"> <label for="password">Password</label> <input id="password" type="password" required value=${state.password} oninput=${(e: Event) => { const target = e.target as HTMLInputElement formState.set({ ...state, password: target.value }) }} disabled=${state.isLoading} /> </div>
<button type="submit" class="btn btn-primary btn-block" disabled=${state.isLoading} > ${state.isLoading ? 'Logging in...' : 'Login'} </button> </form>
<p class="hint"> Use any email and password to login </p> </div> </div> `}Página Dashboard (Protegida)
Section titled “Página Dashboard (Protegida)”import { html, type VNode } from '@ezbug/slash'import { authState } from '../services/auth'
export const Dashboard = (): VNode => { const auth = authState.get()
return html` <div class="dashboard"> <h1>Dashboard</h1>
<div class="welcome"> <h2>Welcome back, ${auth.user?.name}!</h2> <p>Email: ${auth.user?.email}</p> <p>Role: ${auth.user?.role}</p> </div>
<div class="dashboard-grid"> <div class="dashboard-card"> <h3>📊 Analytics</h3> <p>View your analytics and insights</p> </div>
<div class="dashboard-card"> <h3>📝 Recent Activity</h3> <p>Track your recent actions</p> </div>
<div class="dashboard-card"> <h3>⚙️ Settings</h3> <p>Manage your account settings</p> </div>
<div class="dashboard-card"> <h3>💬 Messages</h3> <p>Check your messages</p> </div> </div> </div> `}Layout Principal
Section titled “Layout Principal”import { html, type VNode } from '@ezbug/slash'import { Router } from '@ezbug/slash'import { Header } from './Header'import { router } from '../router'
export const Layout = (): VNode => { return html` <div class="app"> ${Header()}
<main class="main"> <div class="container"> ${Router({ router })} </div> </main>
<footer class="footer"> <div class="container"> <p>© 2026 My SPA. Built with Slash.</p> </div> </footer> </div> `}Entry Point
Section titled “Entry Point”import { render } from '@ezbug/slash'import { Layout } from './components/Layout'import { router } from './router'import { initAuth } from './services/auth'import './styles.css'
// Inicializa autenticaçãoinitAuth()
// Inicializa routerrouter.init()
// Render appconst root = document.getElementById('app')if (root) { render(Layout(), root)}Navegação Programática
Section titled “Navegação Programática”Exemplos de navegação via código:
import { router } from './router'
// Navegação simplesrouter.push('/products')
// Com paramsrouter.push('/products/123')
// Com query stringsrouter.push({ path: '/products', query: { category: 'electronics', page: '1' }})
// Voltarrouter.back()
// Avançarrouter.forward()
// Ir para índice específico no históricorouter.go(-2) // volta 2 páginas
// Replace (não adiciona ao histórico)router.replace('/login')Executando o Projeto
Section titled “Executando o Projeto”# Desenvolvimentobun run dev
# Buildbun run buildTransições de Página
Section titled “Transições de Página”Adicione transições suaves:
/* Fade transition */.page-enter { opacity: 0;}
.page-enter-active { opacity: 1; transition: opacity 0.3s;}
.page-leave { opacity: 1;}
.page-leave-active { opacity: 0; transition: opacity 0.3s;}Melhorias Possíveis
Section titled “Melhorias Possíveis”- Breadcrumbs: Navegação hierárquica
- Loading States: Skeleton screens durante navegação
- Error Boundaries: Tratamento de erros por rota
- Route Transitions: Animações entre páginas
- Nested Routes: Subrotas com layouts específicos
- Route Meta: Permissões, títulos, analytics
- Scroll Restoration: Lembrar posição do scroll
- Prefetching: Pré-carregar rotas ao hover
Pontos-Chave de Aprendizado
Section titled “Pontos-Chave de Aprendizado”- SPA Performance: Navegação instantânea sem reload
- Route Guards: Controle de acesso às rotas
- Lazy Loading: Code splitting automático
- Navigation Hooks: beforeEach, afterEach para side effects
- Query Params: Estado na URL para compartilhamento
- History API: Controle total do histórico do navegador
Próximos Passos
Section titled “Próximos Passos”- Form Validation - Validação avançada de formulários
- Router API - Documentação completa do Router
- Navigation Guards - Guards avançados