Skip to content

Instantly share code, notes, and snippets.

@hara-y-u
Created June 13, 2025 04:19
Show Gist options
  • Save hara-y-u/ba81966e1d0ef3e62b438d12c4202393 to your computer and use it in GitHub Desktop.
Save hara-y-u/ba81966e1d0ef3e62b438d12c4202393 to your computer and use it in GitHub Desktop.
ruby.wasm Reactive HTML
<html>
<script src="https://cdn.jsdelivr.net/npm/@ruby/[email protected]/dist/browser.script.iife.js"></script>
<script type="text/ruby">
require "js"
puts RUBY_VERSION # (Printed to the Web browser console)
JS.global[:document].write "Hello, world!"
module Observable
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def observed(*attributes)
attributes.each do |attribute|
define_method(attribute) do
instance_variable_get("@#{attribute}")
end
define_method("#{attribute}=") do |value|
old_value = instance_variable_get("@#{attribute}")
instance_variable_set("@#{attribute}", value)
observers.each do |observer|
observer.after_changed(self, attribute, value, old_value)
end
end
end
end
end
def observers
@observers ||= []
end
end
module Observer
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def observe(attribute)
@observable_names ||= []
@observable_names << attribute
end
end
def initialize(_)
super
start_observing
end
def after_changed(observable, attribute_name, new_value, old_value); end
private
def start_observing
self.class.instance_variable_get(:@observable_names)&.each do |name|
observable = send(name)
observable.observers << self
end
end
end
module View
include Observer
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
include Observer::ClassMethods
alias state observe
end
attr_accessor :element
def body = "<div>#{self.class.name} View</div>"
def after_changed(observable, attribute_name, new_value, old_value)
render
end
def render
return unless element
new_html = body
if element[:innerHTML] != new_html
element[:innerHTML] = new_html
setup_actions
setup_child_views
end
end
private
def setup_actions
triggers = element.querySelectorAll("[data-action]").to_a
triggers.each do |trigger|
action = trigger[:dataset][:action].to_s
(event, statement) = action.split("->")
trigger.addEventListener(event) do |event|
instance_eval(statement)
end
end
end
def setup_child_views
child_view_elements = element.querySelectorAll("[data-view]").to_a
child_view_elements.each do |child_view_element|
child_view = instance_eval(child_view_element[:dataset][:view].to_s)
child_view.element = child_view_element
child_view.render
end
end
end
# Counter Examples
class Counter
include Observable
observed :count
def initialize
@count = 0
end
def increment
self.count += 1
end
def decrement
self.count -= 1
end
def value
self.count
end
end
CounterView = Struct.new(:counter, keyword_init: true) do
include View
state :counter
def body
<<~HTML
<div>
<h1>Counter: #{counter.value}</h1>
<button data-action="click->counter.increment">Increment</button>
<button data-action="click->counter.decrement">Decrement</button>
<h2>Child Counter View 1 (shares counter object)</h2>
<div data-view="ChildCounterView.new(counter:)"></div>
<h2>Child Counter View 2 (does not share counter object)</h2>
<div data-view="ChildCounterView.new(counter: Counter.new)"></div>
</div>
HTML
end
end
ChildCounterView = Struct.new(:counter, keyword_init: true) do
include View
state :counter
def body
<<~HTML
<div>
<h1>Counter: #{counter.value}</h1>
<button data-action="click->counter.increment">Increment</button>
<button data-action="click->counter.decrement">Decrement</button>
</div>
HTML
end
end
counter = Counter.new
counter_view = CounterView.new(counter:)
counter_view.element = JS.global[:document][:body]
counter_view.render
</script>
<body></body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment