Ruwi(Ruby Wasm Ui)

A modern web frontend framework for Ruby using ruby.wasm. Write reactive web applications using familiar Ruby syntax and patterns.

⚠️ Warning: This library is currently under development and subject to frequent breaking changes. Please use with caution and expect API changes in future versions.

Features

  • Reactive State Management: Simple, predictable state updates with actions
  • Virtual DOM: Efficient DOM updates using a virtual DOM implementation
  • Event Handling: Intuitive event system with Ruby lambdas
  • Component Architecture: Build reusable components with clean separation of concerns
  • Lifecycle Hooks: Manage component lifecycle with hooks like on_mounted
  • Ruby Syntax: Write frontend applications using Ruby instead of JavaScript

Quick Start

Create an HTML file:

<!DOCTYPE html>
<html>
  <head>
    <script src="https://unpkg.com/[email protected]"></script>
    <script defer type="text/ruby" src="app.rb"></script>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>

Create app.rb:

require "js"

# Define a Counter component
CounterComponent = Ruwi.define_component(
  # Initialize component state
  state: ->(props) {
    { count: props[:count] || 0 }
  },

  # Render the counter component
  template: ->() {
    Ruwi::Template::Parser.parse_and_eval("      <div>\n        <div>{state[:count]}</div>\n        <!-- Both ButtonComponent and button-component are valid -->\n        <ButtonComponent\n          label=\"Increment\"\n          on=\"{ click_button: -> { increment } }\">\n        </ButtonComponent>\n        <button-component\n          label=\"Decrement\"\n          on=\"{ click_button: -> { decrement } }\"\n        />\n      </div>\n    HTML\n  },\n\n  # Component methods\n  methods: {\n    increment: ->() {\n      update_state(count: state[:count] + 1)\n    },\n    decrement: ->() {\n      update_state(count: state[:count] - 1)\n    }\n  }\n)\n\n# Button component - reusable button with click handler\nButtonComponent = Ruwi.define_component(\n  template: ->() {\n    Ruwi::Template::Parser.parse_and_eval(<<~HTML, binding)\n      <button on=\"{ click: ->() { emit('click_button') } }\">\n        {props[:label]}\n      </button>\n    HTML\n  }\n)\n\n# Create and mount the app\napp = Ruwi::App.create(CounterComponent, count: 5)\napp_element = JS.global[:document].getElementById(\"app\")\napp.mount(app_element)\n", binding)

Using as a Gem

You can also use ruwi as a Ruby gem with rbwasm to build your application as a WASM file.

Setup

  1. Add ruwi to your Gemfile:
# frozen_string_literal: true

source "https://rubygems.org"

gem "ruwi"
  1. Install dependencies:
bundle install

Building Your Application

Set up your project (first time only):

bundle exec ruwi setup

This command will:

  • Configure excluded gems for WASM build
  • Build Ruby WASM file
  • Update .gitignore
  • Create initial src/index.html and src/index.rb files

The setup command will configure your project structure as follows:

my-app/

Develop Your Application

  • Development server: Start a development server with file watching and auto-build: bash bundle exec ruwi dev

Create an HTML file in the src directory that loads the WASM file:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>My App</title>
    <script type="module">
      import { DefaultRubyVM } from "https://cdn.jsdelivr.net/npm/@ruby/[email protected]/dist/browser/+esm";
      const response = await fetch("../src.wasm");
      const module = await WebAssembly.compileStreaming(response);
      const { vm } = await DefaultRubyVM(module);
      vm.evalAsync(`
        require "ruwi"
        require_relative './src/index.rb'
      `);
    </script>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>

Your src/index.rb file can use ruwi just like in the Quick Start example:

CounterComponent = Ruwi.define_component(
  state: ->(props) {
    { count: props[:count] || 0 }
  },
  template: ->() {
    Ruwi::Template::Parser.parse_and_eval("      <div>\n        <div>{state[:count]}</div>\n        <button on=\"{ click: -> { increment } }\">Increment</button>\n      </div>\n    HTML\n  },\n  methods: {\n    increment: ->() {\n      update_state(count: state[:count] + 1)\n    }\n  }\n)\n\napp = Ruwi::App.create(CounterComponent, count: 0)\napp_element = JS.global[:document].getElementById(\"app\")\napp.mount(app_element)\n", binding)

See the examples directory for a complete working example.

Additional Commands:

  • Rebuild Ruby WASM: Rebuild the Ruby WASM file when you add new gems: bash bundle exec ruwi rebuild

Deployment

Pack your application files for deployment:

bundle exec ruwi pack

This command packs your Ruby files from the ./src directory into the WASM file and outputs to the dist directory for deployment.

Documentation

Development

This project is currently under active development. To run the examples locally:

# Run production examples
npm run serve:examples

# Run development examples
npm run serve:examples:dev

License

MIT