Created
June 13, 2025 04:19
-
-
Save hara-y-u/ba81966e1d0ef3e62b438d12c4202393 to your computer and use it in GitHub Desktop.
ruby.wasm Reactive HTML
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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