Mochi

What is Mochi?

Mochi brings modern web development to Ruby by leveraging the excellent work from Opal and tightly coupling it with the open Web Components standard. Mochi provides also native Sorbet support for all that type-checking goodness.

Type-Safe Ruby to the Web

All in all Mochi brings type-safe Ruby to the modern web using standards-compliant Web Components with batteries included.

Write interactive web components in Ruby with reactive state management, event handling, and component composition - all with the familiar Ruby syntax you already know and love.

Installation

Getting started with Mochi requires setting up the compiler and its dependencies:

Prerequisites

  • Crystal Icon Crystal - The compiler's language
  • Opal Icon Opal - Ruby-to-JavaScript transpiler
  • Task Icon Task - Build automation
  • Git for cloning the repository

Build the Compiler

git clone http://github.com/yampug/mochi cd mochi task build

This builds the Mochi compiler that processes your Ruby code and generates fully native Web Components.

Create a New Project

mochi --initialize="my_fancy_project"

This creates a preconfigured project directory with boilerplate files and a proper structure to get started.

First Steps

Once you have Mochi installed, let's create your first component and understand the basic concepts.

Component Structure

Mochi components follow a specific pattern with required methods:

class HelloWorld
  @tag_name = "hello-world"  # Web Component tag name

  def initialize
    # Set up initial state
  end

  def reactables
    []  # Array of reactive property names
  end

  def html
    %Q{
      <div class="greeting">
        <h1>Hello, Mochi!</h1>
        <p>Welcome to modern Ruby web development</p>
      </div>
    }
  end

  def css
    %Q{
      .greeting {
        background: #f0f0f0;
        padding: 1rem;
        border-radius: 8px;
      }
    }
  end
end

Reactive Counter Component

A more complex example with reactive state and event handling:

class Counter
  @tag_name = "my-counter"

  def initialize
    @count = 0
  end

  def reactables
    ["count"]  # Makes @count reactive
  end

  def html
    %Q{
      <div class="counter">
        <h2>Count: {count}</h2>
        <button on:click={increment}>Increment</button>
        <button on:click={decrement}>Decrement</button>
      </div>
    }
  end

  def css
    %Q{
      .counter {
        background: #fff;
        border: 2px solid #ff0068;
        padding: 1rem;
        text-align: center;
      }

      button {
        margin: 0.5rem;
        padding: 0.5rem 1rem;
      }
    }
  end

  def increment
    @count += 1
  end

  def decrement
    @count -= 1
  end

  def mounted
    puts "Counter component mounted!"
  end

  def unmounted
    puts "Counter component unmounted!"
  end
end

Compilation

Compile your Ruby components to JavaScript:

mochi -i "./lib" -o "./dist" -m -tc

Options:

  • -i: Input directory containing Ruby files
  • -o: Output directory for generated JavaScript
  • -m: Minimize the generated JS code (optional)
  • -tc: Run Sorbet typechecks (optional)

Components

Components are the building blocks of Mochi applications. They are Ruby classes that get compiled into Web Components with reactive state management.

At the end of the day components remain just Ruby classes by themselves, so you can import any other Ruby module in your code base like a caculation utility directly from within your components.

Component Lifecycle

Mochi components follow a predictable lifecycle with these hooks:

  • initialize - Set up initial state and configuration
  • mounted(shadow_root) - Component is added to the DOM with access to shadow DOM
  • unmounted - Component is removed from the DOM for cleanup

Reactive Properties

Properties listed in reactables become reactive and automatically trigger re-renders:

class UserProfile
  @tag_name = "user-profile"

  def initialize
    @user_name = "Guest"
    @avatar_url = "/default-avatar.png"
    @online = false
  end

  def reactables
    ["user_name", "avatar_url", "online"]
  end

  def html
    %Q{
      <div class="profile">
        <img src="{avatar_url}" alt="Avatar">
        <h3>{user_name}</h3>
        <span class="status {online}">
          {online} ? "Online" : "Offline"
        </span>
        <button on:click={toggle_status}>Toggle Status</button>
      </div>
    }
  end

  def css
    %Q{
      .profile {
        display: flex;
        align-items: center;
        gap: 1rem;
      }

      .status.true { color: green; }
      .status.false { color: gray; }
    }
  end

  def toggle_status
    @online = !@online
  end
end

Template Interpolation & Event Binding

Automatic Reactivity

The compiler automatically generates getter/setter methods for reactive properties and handles template interpolation with {property} syntax.

Event Binding

Bind DOM events to Ruby methods:

  • on:click={method_name} - Handle click events
  • on:change={method_name} - Handle input changes (receives event and value)
  • bind:attribute="{value}" - Bi-directional data binding with child components

Router

Mochi includes provides a clean route management component to make conditional rendering and also single-page applications very straight forward.

Basic Setup

Inside your html your can make use of the <mochi-router> component like so:

<!-- Only renders when the "/" page is visited-->
<mochi-route match="/">
    <div>
        <h3>Home Page</h3>
        <p>This is the home page.</p>
    </div>
</mochi-route>

<!-- Only renders when the "/contact" page is visited-->
<mochi-route match="/contact">
    <div>
        <h3>Contact Page</h3>
        <p>Get in touch with us!</p>
    </div>
</mochi-route>

The router intercepts local URL changes and instructs Mochi to only render the components inside of the respective <mochi-router> component. External URLs are not intercepted and work normally.

Styling

Mochi provides multiple ways to style your components, from traditional CSS to modern utility-first approaches.

Classic CSS

Use traditional CSS approaches with your Mochi components.

CSS Classes

Use traditional CSS classes with your components:

<div class="card primary-card">
  <h2>"Styled Component"</h2>
</div>

Inline Styles

Apply styles directly to elements:

<div style="background: '#f0f0f0'; padding: 1rem">
  <p>"Styled content"</p>
</div>

Component Styles

Define component-specific styles that are automatically scoped:

class StyledCard
                def css
    %Q{
      .card {
        background: white;
        border-radius: 8px;
        padding: 1rem;
        box-shadow: 0 2px 4px rgba(0,0,0,0.1);
      }
      .card h2 {
        color: #333;
        margin-bottom: 0.5rem;
      }
    }
  end

  def html
    %Q{
      <div class="card">
        <2>Styled Card</h2>
        <p>This card has component-scoped styles</p>
      </div>
    }
  end

end

Tailwind

Mochi preserves the ability to hammer in plain good old html, instead of a JavaScript based intermediate representation such as JSX. As a result Tailwind CSS can be used right out of the box for utility-first styling in your Mochi applications.

Setup

Install and configure Tailwind CSS in your Mochi project:

npm install -D tailwindcss npx tailwindcss init

Usage

Apply Tailwind utility classes directly to your components:


def html
    %Q{
      <div class="card">
        <h2 class="bg-white rounded-lg p-6 shadow-md-2xl font-bold text-gray-800 mb-4">Tailwind Card</h2>
        <p class="text-gray-600 leading-relaxed">Styled with Tailwind utilities</p>
      </div>
    }
  end

Responsive Design

Use Tailwind's responsive utilities for mobile-first design:


  def html
    %Q{
      <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
        Grid adapts to screen size
      </div>
    }
  end

Batteries

Mochi comes with a comprehensive set of built-in utilities and components - "batteries included" - to accelerate your development process. These pre-built solutions handle common web development tasks so you can focus on building your unique features.

Logging

Mochi includes a comprehensive logging system with enhanced console output, caller information, and performance timing capabilities.

Basic Logging

class MyComponent
  def initialize
    Log.info(self, "Component initialized")
  end

  def handle_click
    Log.warn(self, "This might be problematic")
    Log.error(self, "Something went wrong!")
    Log.trace(self, "Detailed trace information")
  end
end

Object & Pretty Printing

Log objects and JSON data with proper formatting:

# Log JavaScript objects
Log.object(self, `{"user_id": 123, "action": "click"}`)

# Pretty-print JSON with indentation
Log.pretty(self, `{"response": {"status": "ok", "data": [1,2,3]}}`)

Performance Timing

def expensive_operation
  Log.time_start(self, "data_processing")

  # Do complex work here
  result = process_data

  Log.time_end(self, "data_processing")
  result
end

Rich Context

Logs automatically include caller information, timestamps, and component context with beautiful formatting and colors.

Timeouts & Intervals

Handle time-based operations with Mochi's built-in timeout and interval utilities that work seamlessly with Ruby code.

Timeouts

Execute code after a delay:

class DelayedComponent
  def mounted
    # Wait 5 seconds then clear the interval
    Mochi.timeout(proc do
      puts "5 seconds have passed!"
      Mochi.clear_interval(@interval_id)
    end, 5000)
  end
end

Intervals

Execute code repeatedly at regular intervals:

class LiveClock
  def mounted
    # Update time every second
    @interval_id = Mochi.interval(proc do
      current_time = Time.now
      puts "Current time: #{current_time}"
    end, 1000)

    # Stop after 10 seconds
    Mochi.timeout(proc do
      Mochi.clear_interval(@interval_id)
      puts "Timer stopped"
    end, 10000)
  end

  def unmounted
    # Clean up when component is removed
    Mochi.clear_interval(@interval_id)
  end
end

Ruby Proc Support

Uses Ruby Proc objects for callbacks, maintaining Ruby's familiar block syntax and closure behavior.

Fetcher

The built-in Fetcher provides a clean, Ruby-like interface for HTTP requests with Promise support and response parsing.

Basic Usage

def load_data
  fetcher = Fetcher.create

  # Simple GET request
  http_resp = fetcher.fetch("/api/data",
    FetchConfigBuilder.new().build()
  ).__await__

  puts "Status: #{http_resp.status}"
  puts "Headers: #{http_resp.headers}"

  # Parse response body
  body = http_resp.body_as_text().__await__
  puts "Response: #{body}"
end

Advanced Configuration

config = FetchConfigBuilder.new()
  .set_method("POST")
  .set_headers({
    "Authorization" => "Bearer token123",
    "Content-Type" => "application/json"
  })
  .set_body('{"name": "value"}')
  .set_keep_alive(true)
  .build()

http_resp = fetcher.fetch("/api/users", config).__await__

Response Handling

# Get response as text
text = http_resp.body_as_text().__await__

# Parse JSON response
hash = http_resp.body_as_hash().__await__

# Check response properties
if http_resp.ok
  puts "Request successful: #{http_resp.status}"
  puts "Content type: #{http_resp.type}"
else
  puts "Request failed: #{http_resp.status_text}"
end

Promise-Based

Uses JavaScript Promises with __await__ syntax for handling asynchronous HTTP operations in Ruby.

Charts

Create beautiful, interactive charts powered by ECharts with a clean Ruby interface for data visualization.

Setup Environment

def mounted(shadow_root)
  # Set up ECharts library (loads from CDN)
  Charts.setup_environment

  # Initialize chart on element
  chart_el = Charts.init_on_element_by_query(shadow_root, "#chart-container")
end

Bar Chart Example

def create_chart(chart_el)
  config = ChartConfigBuilder.new()
    .set_title("Sales Performance")
    .set_legend(["revenue"])
    .set_x_axis(["Q1", "Q2", "Q3", "Q4"])
    .set_series([
      ChartSeriesBuilder.new()
        .set_name("revenue")
        .set_type("bar")
        .set_data([120, 200, 150, 300])
        .build()
    ])
    .build()

  Charts.load_config(chart_el, config)
end

Multiple Series

# Create multi-series chart
series = [
  ChartSeriesBuilder.new()
    .set_name("sales")
    .set_type("line")
    .set_data([10, 20, 30, 25])
    .build(),
  ChartSeriesBuilder.new()
    .set_name("profit")
    .set_type("bar")
    .set_data([5, 15, 20, 18])
    .build()
]

config = ChartConfigBuilder.new()
  .set_title("Business Metrics")
  .set_legend(["sales", "profit"])
  .set_series(series)
  .build()

ECharts Integration

Full access to ECharts features including responsive design, animations, and interactive tooltips through Ruby builders.

API Integration

Mochi makes it easy to integrate with backend APIs using the built-in Fetcher utility for asynchronous data loading.

Loading Data in Components

Use the Fetcher in component lifecycle methods:

class PostList
  @tag_name = "post-list"

  def initialize
    @posts = []
    @loading = true
    @error = nil
  end

  def reactables
    ["posts", "loading", "error"]
  end

  def html
    %Q{
      <div class="post-list">
        <h2>Latest Posts</h2>
        {loading} ? "<p>Loading posts...</p>" : ""
        {error} ? "<p class='error'>Error: {error}</p>" : ""
        <div class="posts">
          <!-- Posts will be rendered here -->
        </div>
      </div>
    }
  end

  def mounted
    load_posts
  end

  private

  def load_posts
    begin
      fetcher = Fetcher.create
      config = FetchConfigBuilder.new().build()

      http_resp = fetcher.fetch("/api/posts", config).__await__

      if http_resp.ok
        @posts = http_resp.body_as_hash().__await__
        @loading = false
        puts "Loaded #{@posts.length} posts"
      else
        @error = "Failed to load posts: #{http_resp.status_text}"
        @loading = false
      end
    rescue => e
      @error = "Network error: #{e.message}"
      @loading = false
    end
  end
end

POST Requests

Send data to APIs with POST requests:

def create_post(title, content)
  config = FetchConfigBuilder.new()
    .set_method("POST")
    .set_headers({
      "Content-Type" => "application/json",
      "Authorization" => "Bearer #{@auth_token}"
    })
    .set_body(JSON.generate({
      title: title,
      content: content
    }))
    .build()

  fetcher = Fetcher.create
  http_resp = fetcher.fetch("/api/posts", config).__await__

  if http_resp.ok
    new_post = http_resp.body_as_hash().__await__
    puts "Created post: #{new_post['title']}"
  else
    puts "Failed to create post: #{http_resp.status_text}"
  end
end

Promise-Based with __await__

Mochi's Fetcher uses JavaScript Promises with Ruby's __await__ syntax for clean asynchronous code handling.

Deployment

Deploy your compiled Mochi components as static JavaScript files to any web hosting platform.

Build for Production

Compile your components with optional minification:

mochi -i "./src" -o "./dist" -m

This generates two files:

  • opal-runtime.js - The Opal Ruby runtime
  • bundle.js - Your compiled components and Web Components

Include in HTML

Include the generated files in your HTML:

<!DOCTYPE html>
<html>
    <head>
      <title>My Mochi App</title>
    </head>
    <body>
      <!-- Your components can be used as HTML elements -->
      <my-counter></my-counter>
      <chart-demo></chart-demo>

      <!-- Load Opal runtime first -->
      <script src="/js/opal-runtime.js"
              onload='Opal.require("native"); Opal.require("promise"); Opal.require("browser/setup/full");'></script>

      <!-- Then load your compiled components -->
      <script src="/js/bundle.js"></script>
    </body>
</html>