Code Playground
These files are the real sources under www/snippets/ — the site renders them verbatim, so nothing here drifts from what the repository builds.
Running them
Clone the repository, yarn install && yarn build, then start an example (for instance yarn examples:web:dev). Every snippet assumes a bundler entry that gives you document.getElementById('app').
Counter — one component, two hosts
typescript
import { useReactiveRuntime } from '@rasenjs/reactive-vue'
import { div, button, p, mount } from '@rasenjs/dom'
import { ref } from '@vue/reactivity'
useReactiveRuntime()
const count = ref(0)
const Counter = () =>
div(
{ style: { textAlign: 'center', padding: '20px' } },
p({ style: { fontSize: '48px', margin: '20px' } }, count),
div(
{},
button({ onClick: () => count.value-- }, '-'),
button({ onClick: () => count.value++ }, '+')
)
)
mount(Counter(), document.getElementById('app')!)tsx
import { useReactiveRuntime } from '@rasenjs/reactive-vue'
import { View, Text, TouchableOpacity, registerApp } from '@rasenjs/react-native'
import { ref } from '@vue/reactivity'
useReactiveRuntime()
const count = ref(0)
registerApp('MyApp', () => (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text style={{ fontSize: 48, marginBottom: 20 }}>{count}</Text>
<View style={{ flexDirection: 'row', gap: 10 }}>
<TouchableOpacity onPress={() => count.value--}>
<Text>−</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => count.value++}>
<Text>+</Text>
</TouchableOpacity>
</View>
</View>
))Minimal DOM component (www/snippets/basic-dom.ts)
typescript
import { useReactiveRuntime } from '@rasenjs/reactive-vue'
import { div, button, p, mount } from '@rasenjs/dom'
import { ref, computed } from '@vue/reactivity'
// 1. Install the reactive runtime once, at startup
useReactiveRuntime()
// 2. Reactive state
const count = ref(0)
const label = computed(() => `Count: ${count.value}`)
// 3. A component returns a mountable. `children` accepts a flat array of
// strings, refs and mountables — a ref child is a reactive text node.
const Counter = () =>
div(
{ style: { textAlign: 'center', padding: '20px' } },
p({ style: { fontSize: '32px' } }, label),
button({ onClick: () => count.value++ }, 'Increment')
)
// 4. Mount it
mount(Counter(), document.getElementById('app')!)The same counter in JSX (www/snippets/jsx-counter.tsx)
typescript
// tsconfig.json: { "jsx": "react-jsx", "jsxImportSource": "@rasenjs/dom" }
import { useReactiveRuntime } from '@rasenjs/reactive-vue'
import { mount } from '@rasenjs/dom'
import { ref } from '@vue/reactivity'
useReactiveRuntime()
const Counter = () => {
const count = ref(0)
return (
<div>
{/* a bare ref interpolates directly: the compiler emits a renderText()
binding that unwraps it through the active runtime */}
<p>Count: {count}</p>
<button onClick={() => count.value++}>Increment</button>
</div>
)
}
mount(<Counter />, document.getElementById('app')!)Todo list
each for reconciliation, DOM properties for form state, a computed ref as a reactive text node.
Todo app (www/snippets/todo-dom.ts)
typescript
import { each } from '@rasenjs/core'
import { useReactiveRuntime } from '@rasenjs/reactive-vue'
import { div, input, button, ul, li, span, mount } from '@rasenjs/dom'
import { ref, computed } from '@vue/reactivity'
useReactiveRuntime()
interface Todo {
id: number
text: string
done: boolean
}
const todos = ref<Todo[]>([])
const newTodo = ref('')
let nextId = 1
const remaining = computed(() => todos.value.filter((t) => !t.done).length)
const addTodo = () => {
if (newTodo.value.trim()) {
todos.value.push({ id: nextId++, text: newTodo.value.trim(), done: false })
newTodo.value = ''
}
}
const toggleTodo = (id: number) => {
const todo = todos.value.find((t) => t.id === id)
if (todo) todo.done = !todo.done
}
const removeTodo = (id: number) => {
todos.value = todos.value.filter((t) => t.id !== id)
}
const TodoApp = () =>
div(
{ style: { maxWidth: '400px', margin: '0 auto', padding: '20px' } },
// Header — input.value is a DOM property, so a ref writes straight to it
div(
{ style: { marginBottom: '20px' } },
input({
value: newTodo,
placeholder: 'What needs to be done?',
style: { width: '70%', padding: '10px' },
onInput: (e) => (newTodo.value = (e.target as HTMLInputElement).value),
onKeyPress: (e: KeyboardEvent) => e.key === 'Enter' && addTodo()
}),
button(
{ style: { width: '25%', marginLeft: '5%', padding: '10px' }, onClick: addTodo },
'Add'
)
),
// List — each() reconciles by object identity, so rows move instead of
// being rebuilt; `of` is a getter so the list stays subscribed
ul(
{ style: { listStyle: 'none', padding: 0 } },
each({
of: () => todos.value,
children: (todo: Todo) =>
li(
{
style: {
display: 'flex',
alignItems: 'center',
padding: '10px',
borderBottom: '1px solid #eee'
}
},
input({
type: 'checkbox',
checked: todo.done,
onChange: () => toggleTodo(todo.id)
}),
span(
{
style: () => ({
flex: 1,
marginLeft: '10px',
textDecoration: todo.done ? 'line-through' : 'none',
color: todo.done ? '#999' : '#333'
})
},
todo.text
),
button(
{
style: { background: 'none', border: 'none', fontSize: '20px', cursor: 'pointer' },
onClick: () => removeTodo(todo.id)
},
'×'
)
)
})
),
// Footer — a computed ref child is a reactive text node
div(
{ style: { marginTop: '20px', color: '#666' } },
remaining,
' items remaining'
)
)
mount(TodoApp(), document.getElementById('app')!)Canvas
Canvas + tween (www/snippets/canvas-basic.ts)
typescript
import { useReactiveRuntime } from '@rasenjs/reactive-vue'
import { canvas, mount } from '@rasenjs/dom'
import { rect, text } from '@rasenjs/canvas-2d'
import { tween } from '@rasenjs/animation'
import { ref } from '@vue/reactivity'
useReactiveRuntime()
// A tween is a ref the animation writes to for you
const x = tween(50)
mount(
canvas({
width: 400,
height: 200,
children: [
// a prop can be a getter: it is re-evaluated when its dependencies change
rect({ x: () => x.value, y: 50, width: 100, height: 80, fill: '#4CAF50' }),
text({ text: 'Rasen', x: 12, y: 24, fill: '#333' })
]
}),
document.getElementById('app')!
)
// Writing the ref marks the canvas dirty — no render loop to write
x.to(320, { duration: 800, easing: 'easeOutCubic' })Signals driving shapes (www/snippets/canvas-animation.ts)
typescript
import { useReactiveRuntime } from '@rasenjs/reactive-vue'
import { canvas, mount } from '@rasenjs/dom'
import { rect, circle, text } from '@rasenjs/canvas-2d'
import { ref, computed } from '@vue/reactivity'
useReactiveRuntime()
// One frame counter drives every derived value; each shape subscribes only to
// the props it reads, and a change repaints the canvas once.
const time = ref(0)
const x = computed(() => 200 + Math.sin(time.value * 0.02) * 150)
const y = computed(() => 150 + Math.cos(time.value * 0.03) * 100)
const radius = computed(() => 30 + Math.sin(time.value * 0.05) * 10)
const tick = (now: number) => {
time.value = now
requestAnimationFrame(tick)
}
requestAnimationFrame(tick)
const Animation = () =>
canvas({
width: 400,
height: 300,
children: [
rect({ x: 0, y: 0, width: 400, height: 300, fill: '#1a1a2e' }),
circle({ x, y, radius, fill: '#e94560' }),
text({ text: () => `x: ${Math.round(x.value)}`, x: 12, y: 24, fill: '#8ecae6' })
]
})
mount(Animation(), document.getElementById('app')!)Server-side rendering
renderToString (www/snippets/ssr-html.ts)
typescript
import { renderToString, div, p, ul, li } from '@rasenjs/html'
// No DOM and no reactive runtime required: the string host renders once.
const html = renderToString(
div(
{ class: 'container' },
p({ class: 'title' }, 'Hello from SSR!'),
ul({ class: 'list' }, li('Item 1'), li('Item 2'), li('Item 3'))
)
)
console.log(html)
// <div class="container"><p class="title">Hello from SSR!</p><ul class="list"><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul></div>Next steps
- Examples overview — the full list of runnable demos
- Getting Started — project setup
- Components — the model behind these snippets