Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
247 changes: 195 additions & 52 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
# ruby-wasm-ui

A modern web frontend framework for Ruby using [ruby.wasm](https://github.com/ruby/ruby.wasm). Write reactive web applications using familiar Ruby syntax and patterns.
A modern web frontend framework for Ruby using [ruby.wasm](https://github.com/ruby/ruby.wasm). Write reactive web applications using familiar Ruby syntax and component-based architecture.

**Currently under active development.**
**⚠️ 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
- **Component-Based Architecture**: Build reusable UI components with state and lifecycle methods
- **Template Parser**: Write HTML templates with embedded Ruby expressions and component support
- **Virtual DOM**: Efficient DOM updates using a virtual DOM implementation
- **Template Parser**: Write HTML templates with embedded Ruby expressions
- **Event Handling**: Intuitive event system with Ruby lambdas
- **Component Architecture**: Build reusable components with clean separation of concerns
- **Reactive State Management**: Simple state updates with automatic re-rendering
- **Event Handling**: Intuitive event system using Ruby lambdas
- **Ruby Syntax**: Write frontend applications using Ruby instead of JavaScript

## Quick Start
Expand All @@ -35,77 +35,220 @@ Create `app.rb`:
```ruby
require "js"

# Define actions to handle state changes
actions = {
increment: ->(state, _payload) {
{ count: state[:count] + 1 }
# Define a counter component
CounterComponent = RubyWasmUi.define_component(
# Initialize component state
state: ->(props) {
{ count: 0 }
},
decrement: ->(state, _payload) {
{ count: state[:count] - 1 }
}
}

# Define the view function using HTML templates
view = ->(state, emit) {
template = <<~HTML
<div>
<h1>Count: {state[:count]}</h1>
<button onclick="{->(e) { emit.call(:increment) }}">+</button>
<button onclick="{->(e) { emit.call(:decrement) }}">-</button>
</div>
HTML
eval RubyWasmUi::Template::Parser.parse(template)
}
# Render the component using Template::Parser
render: ->(component) {
template = <<~HTML
<div>
<h1>Count: {component.state[:count]}</h1>
<button on="{click: ->(_e) { component.increment }}">+</button>
<button on="{click: ->(_e) { component.decrement }}">-</button>
</div>
HTML

vdom_code = RubyWasmUi::Template::Parser.parse(template)
eval(vdom_code)
},

# Create and mount the app
app = RubyWasmUi::App.create(
state: { count: 0 },
view: view,
actions: actions
# Define component methods
methods: {
increment: ->() {
state = self.state
self.update_state(count: state[:count] + 1)
},

decrement: ->() {
state = self.state
self.update_state(count: state[:count] - 1)
}
}
)

# Create and mount the application
app = RubyWasmUi::App.create(CounterComponent)
app_element = JS.global[:document].getElementById("app")
app.mount(app_element)
```

## Component Architecture

### Defining Components

Components are defined using `RubyWasmUi.define_component`:

```ruby
MyComponent = RubyWasmUi.define_component(
# Initial state (optional)
state: ->(props) {
{ message: "Hello", count: 0 }
},

# Render method (required)
render: ->(component) {
# Return VDOM using Template::Parser or direct VDOM construction
},

# Component methods (optional)
methods: {
handle_click: ->() {
# Update state
self.update_state(count: self.state[:count] + 1)
}
}
)
```

### State Management

Components manage their own state using `update_state`:

```ruby
# Update single value
self.update_state(count: 10)

# Update multiple values
self.update_state(
count: self.state[:count] + 1,
message: "Updated!"
)
```

## Template Syntax

Ruby expressions can be embedded in HTML templates using `{}`:
### Ruby Expressions

Embed Ruby expressions in HTML using `{}`:

```ruby
template = <<~HTML
<div>
<h1>{component.state[:title]}</h1>
<p class="{component.state[:is_valid] ? 'text-green' : 'text-red'}">
{component.state[:is_valid] ? 'Valid!' : 'Invalid'}
</p>
</div>
HTML
```

### Event Handling

Use the `on` attribute for event handlers:

```ruby
# Display state values
<div>{state[:message]}</div>

# Conditional rendering
<p class="{state[:is_valid] ? 'text-green-500' : 'text-red-500'}">
{state[:is_valid] ? 'Valid!' : 'Invalid input'}
</p>

# Event handlers
<button onclick="{->(e) { emit.call('handle_click', e[:target][:value]) }}">
Click me
</button>

# Input binding
<input
type="text"
value="{state[:input_value]}"
oninput="{->(e) { emit.call('update_input', e[:target][:value]) }}"
/>
template = <<~HTML
<button on="{click: ->(_e) { component.handle_click }}">
Click me
</button>

<input
type="text"
value="{component.state[:input_value]}"
on="{input: ->(e) { component.update_input(e[:target][:value].to_s) }}" />
HTML
```

### Component Composition

Use other components within templates:

```ruby
template = <<~HTML
<div>
<h1>My App</h1>
<MyButton text="Click me" on="{click: ->(data) { component.handle_button_click }}" />
<UserCard user="{component.state[:current_user]}" />
</div>
HTML
```

## Examples

The repository includes several examples demonstrating different features:

### Counter (`examples/counter`)
Basic increment/decrement counter showing state management and event handling.

### Input Validation (`examples/input`)
Form input with real-time validation and conditional styling.

### Search Field (`examples/search_field`)
Component composition with parent-child communication using events.

### TODO Application (`examples/todos`)
Complex application demonstrating:
- Multiple components
- State management
- Inline editing
- Dynamic lists
- Event handling

### Random Cocktail (`examples/random_cocktail`)
API integration example showing:
- Async operations with Promises
- Loading states
- Error handling
- Image display

## Advanced Usage

### Direct VDOM Construction

For complex scenarios, you can construct VDOM directly:

```ruby
render: ->(component) {
RubyWasmUi::Vdom.h('div', {}, [
RubyWasmUi::Vdom.h('h1', {}, ['My App']),
RubyWasmUi::Vdom.h('button', {
on: { click: ->(_e) { component.handle_click } }
}, ['Click me'])
])
}
```

### Component Communication

Components can emit events to communicate with parents:

```ruby
# Child component
methods: {
handle_click: ->() {
self.emit('button_clicked', { data: 'some data' })
}
}

# Parent component template
template = <<~HTML
<ChildComponent on="{button_clicked: ->(data) { component.handle_child_event(data) }}" />
HTML
```

## Development

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

```bash
# Install dependencies
npm install

# Run production examples
npm run serve:examples

# Run development examples
# Run development examples (with local runtime)
npm run serve:examples:dev
```

## Contributing

This project is currently under active development. Contributions are welcome!

## License

MIT
19 changes: 9 additions & 10 deletions examples/counter/index.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,16 @@

# Render the counter component
render: ->(component) {
state = component.state
template = <<~HTML
<div>
<div>{component.state[:count]}</div>
<button on="{click: ->(_e) { component.increment }}">Increment</button>
<button on="{click: ->(_e) { component.decrement }}">Decrement</button>
</div>
HTML

RubyWasmUi::Vdom.h("div", {}, [
RubyWasmUi::Vdom.h("div", {}, [state[:count].to_s]),
RubyWasmUi::Vdom.h("button", {
on: { click: ->(_e) { component.increment } }
}, ["Increment"]),
RubyWasmUi::Vdom.h("button", {
on: { click: ->(_e) { component.decrement } }
}, ["Decrement"])
])
vdom_code = RubyWasmUi::Template::Parser.parse(template)
eval(vdom_code)
},

# Component methods
Expand Down
41 changes: 20 additions & 21 deletions examples/input/index.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,27 @@
# Render the input component
render: ->(component) {
state = component.state

valid_class = "bg-green-50 border border-green-500 text-green-900 placeholder-green-700 text-sm rounded-lg focus:ring-green-500 focus:border-green-500 block w-full p-2.5 dark:bg-green-100 dark:border-green-400"
invalid_class = "bg-red-50 border border-red-500 text-red-900 placeholder-red-700 text-sm rounded-lg focus:ring-red-500 focus:border-red-500 block w-full p-2.5 dark:bg-red-100 dark:border-red-400"

template = <<~HTML
<form class="w-full max-w-sm">
<label class="block mb-2 text-sm font-medium text-700">ユーザー名</label>
<input
type="text"
value="{state[:url_name]}"
class="{state[:is_valid] ? valid_class : invalid_class}"
on="{input: ->(e) { component.update_url_name(e[:target][:value].to_s) }}" />
<p class="{state[:is_valid] ? 'mt-2 text-sm text-green-600 dark:text-green-500' : 'mt-2 text-sm text-red-600 dark:text-red-500'}">
{state[:is_valid] ? "有効です" : "ユーザー名は4文字以上にしてください"}
</p>
<p>*ユーザー名は4文字以上です</p>
</form>
HTML

RubyWasmUi::Vdom.h("form", { class: "w-full max-w-sm" }, [
RubyWasmUi::Vdom.h("label", {
class: "block mb-2 text-sm font-medium text-700"
}, ["ユーザー名"]),
RubyWasmUi::Vdom.h("input", {
type: "text",
value: state[:url_name],
class: state[:is_valid] ?
"bg-green-50 border border-green-500 text-green-900 placeholder-green-700 text-sm rounded-lg focus:ring-green-500 focus:border-green-500 block w-full p-2.5 dark:bg-green-100 dark:border-green-400" :
"bg-red-50 border border-red-500 text-red-900 placeholder-red-700 text-sm rounded-lg focus:ring-red-500 focus:border-red-500 block w-full p-2.5 dark:bg-red-100 dark:border-red-400",
on: {
input: ->(e) { component.update_url_name(e[:target][:value].to_s) }
}
}),
RubyWasmUi::Vdom.h("p", {
class: state[:is_valid] ?
"mt-2 text-sm text-green-600 dark:text-green-500" :
"mt-2 text-sm text-red-600 dark:text-red-500"
}, [state[:is_valid] ? "有効です" : "ユーザー名は4文字以上にしてください"]),
RubyWasmUi::Vdom.h("p", {}, ["*ユーザー名は4文字以上です"])
])
vdom_code = RubyWasmUi::Template::Parser.parse(template)
eval(vdom_code)
},

# Component methods
Expand Down
Loading