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 - The compiler's languageOpal - Ruby-to-JavaScript transpiler
Task - Build automation
- Git for cloning the repository
Build the Compiler
This builds the Mochi compiler that processes your Ruby code and generates fully native Web Components.
Create a New 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:
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 eventson: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:
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:
This generates two files:
opal-runtime.js- The Opal Ruby runtimebundle.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>