Reactivity
Tidos renders HTML on the server. For client-side interactivity, compile any JavaScript framework component as a Web Component and embed it using #[native_element].
How It Works
- Write your component in the JS framework of your choice.
- Compile it as a custom element to dist/ComponentName.js.
- Create a Rust struct with #[native_element] to create a type safe wrapper.
- Use it in page! or view! like any other Tidos component.
The Rust Wrapper
#[native_element] derives Component for the struct. It injects the script tag and renders the kebab-case custom element with all fields forwarded as HTML attributes:
1use tidos::native_element;23// GreetUser -> /dist/GreetUser.js -> <greet-user>4#[native_element]5pub struct GreetUser {6 pub name: String,7 pub is_admin: bool,8}910// Output of macro11impl tidos::Component for GreetUser {12 fn to_render(&self, page: &mut tidos::Page) {13 tidos::head!{ <script r#type="module" src="/dist/GreetUser.js"></script> };14 tidos::view!{ <greet-user name={self.name} :is-admin={self.is_admin}></greet-user> };15 }16}
Framework Example
1<!-- src/components/GreetUser.svelte -->2<svelte:options customElement="greet-user"></svelte:options>34<script>5 let { name = '', is_admin = false } = $props();6</script>78<div>9 <p>Hello, {name}!</p>10 {#if is_admin}11 <span class="badge">Admin</span>12 {/if}13</div>1415<style>16 :host { display: block; }17 .badge { background: #4fd1c5; color: #000; padding: 0.2rem 0.5rem; border-radius: 4px; }18</style>
Build Config
Compile the component to dist/ComponentName.js using Vite:
1import {build, createLogger, defineConfig} from 'vite';2import { svelte } from '@sveltejs/vite-plugin-svelte';3import { extname, join, basename, resolve } from 'path';4import { readdirSync, statSync, } from 'fs';56let logger = createLogger('info', { prefix: '[tidos]' })78function getEntries(dir, extension = '.svelte') {9 const entries = {};1011 function walk(currentDir) {12 const files = readdirSync(currentDir);1314 files.forEach((file) => {15 const fullPath = join(currentDir, file);16 const stat = statSync(fullPath);1718 if (stat.isDirectory()) {19 // Recurse into subdirectories20 walk(fullPath);21 } else if (extname(file) === extension) {22 // Collect files with the specified extension23 const name = basename(file, extension);24 entries[name] = resolve(__dirname, fullPath);25 }26 });27 }2829 walk(dir);30 return entries;31}3233const entries = getEntries('src');3435function tidosSvelteHMR() {3637 let shouldDebounce = false38 const hmrBuild = async () => {39 shouldDebounce = true40 await build({ logLevel: "silent" })41 };4243 return {44 name: 'tidos-svelte-hmr',45 enforce: "pre",46 // HMR47 handleHotUpdate: ({ file, server }) => {48 if (!shouldDebounce) {49 logger.info(`Changes detected, building new version...`, { timestamp: true })50 hmrBuild()51 .then(() => {52 shouldDebounce = false53 logger.info(`Build completed.`, { timestamp: true })54 })55 }56 return []57 }58 }59}6061export default defineConfig({62 plugins: [63 svelte({64 compilerOptions: {65 customElement: true,66 },67 }),68 tidosSvelteHMR(),69 ],70 build: {71 rollupOptions: {72 input: entries,73 output: {74 entryFileNames: '[name].js',75 chunkFileNames: '[name].js',76 dir: 'dist', // Output directory for the compiled files77 assetFileNames: '[name][extname]',78 },79 },80 },81});
Serving Built Files
Add a route in your Rust server to serve files from the dist directory:
1// Rocket — serve compiled JS from the dist folder2#[get("/dist/<file..>")]3async fn dist_files(file: std::path::PathBuf) -> Option<rocket::fs::NamedFile> {4 rocket::fs::NamedFile::open(std::path::Path::new("dist/").join(file)).await.ok()5}