Skip to content

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 ​

Released under the MIT License.