web_ruby_ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +130 -0
- data/bin/web_ruby_ui +7 -0
- data/lib/web_ruby_ui/cli.rb +93 -0
- data/lib/web_ruby_ui/component.rb +94 -0
- data/lib/web_ruby_ui/dom.rb +81 -0
- data/lib/web_ruby_ui/router.rb +30 -0
- data/lib/web_ruby_ui/version.rb +5 -0
- data/lib/web_ruby_ui/vnode.rb +50 -0
- data/lib/web_ruby_ui.rb +12 -0
- metadata +141 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 390ac580582694ff163f766967f0b90aab3d3df6c70a8306a192f58e0889d0d1
|
|
4
|
+
data.tar.gz: 781d6b5303602c16d22564abe89b7c82c666fff7656e550195a1b27b5e1dfd6c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 5b17c0c0917b850ad1265cee471e88f94e52d510488387f5075226d1a27b2217d04874149bd72ed95b76dd61e41419b9bb52ca26ca65e8e10be4d086e5f879e8
|
|
7
|
+
data.tar.gz: 302755ff2c233d61f2238169a73766cfd285a14be5a0330de84a08c8b286394a523f030e3632f29fff3f311c3db80540d308944cb7ac820a7ecc83e109220675
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aditya
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction: including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# web_ruby_ui ππ
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/rb/web_ruby_ui)
|
|
4
|
+
[](https://rubygems.org/gems/web_ruby_ui)
|
|
5
|
+
[](https://github.com/aditya-8108/web_ruby_ui/actions/workflows/ci.yml)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
**The 100% Pure Ruby Frontend Web Framework in WebAssembly.**
|
|
9
|
+
|
|
10
|
+
Build interactive Single-Page Applications (SPAs) entirely in Ruby running inside the browser via `ruby.wasm` β **Zero JavaScript, Zero React, Zero Node, and Zero NPM dependencies.**
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Key Features π
|
|
15
|
+
|
|
16
|
+
- β‘ **100% Pure Ruby Frontend**: Write interactive web components, event listeners, and state management in native Ruby.
|
|
17
|
+
- π **Reactive State Management**: Define `state :count, default: 0`. Updating state (`self.count += 1`) automatically re-renders component views.
|
|
18
|
+
- π³ **Virtual DOM Diffing**: In-memory `VNode` tree generation and diffing engine for optimal browser DOM rendering.
|
|
19
|
+
- πΊοΈ **Client-Side SPA Routing**: Single-page application route matching (`Router`) for multi-view Ruby web apps.
|
|
20
|
+
- π οΈ **Dev Server & CLI Tooling**: `web_ruby_ui dev` launches a local WebAssembly dev server with instant browser hot-loading.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Architecture Overview π
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
28
|
+
BROWSER (WebAssembly Environment)
|
|
29
|
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
30
|
+
JS::Object (Browser DOM APIs)
|
|
31
|
+
β²
|
|
32
|
+
β DOM Events (onclick, oninput) & Render Patches
|
|
33
|
+
βΌ
|
|
34
|
+
WebRubyUI::DOM (Event Bridge & JS Interop)
|
|
35
|
+
β²
|
|
36
|
+
β In-Memory VNode Tree & Patch Diffing
|
|
37
|
+
βΌ
|
|
38
|
+
WebRubyUI::Component (Reactive State & HTML DSL)
|
|
39
|
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
40
|
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
Add this line to your application's Gemfile:
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
gem 'web_ruby_ui'
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
And then execute:
|
|
54
|
+
```bash
|
|
55
|
+
bundle install
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Quickstart (Todo App in Pure Ruby)
|
|
61
|
+
|
|
62
|
+
Create a reactive Single-Page Application without touching JavaScript:
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
require 'web_ruby_ui'
|
|
66
|
+
|
|
67
|
+
class TodoApp < WebRubyUI::Component
|
|
68
|
+
state :items, default: [
|
|
69
|
+
{ id: 1, title: 'Learn Ruby WebAssembly', completed: true },
|
|
70
|
+
{ id: 2, title: 'Build WebRubyUI frontend framework', completed: true }
|
|
71
|
+
]
|
|
72
|
+
state :new_title, default: ''
|
|
73
|
+
|
|
74
|
+
def render
|
|
75
|
+
div(class: 'max-w-md mx-auto my-10 p-6 bg-white rounded-xl shadow-lg') do
|
|
76
|
+
h1(class: 'text-2xl font-bold text-indigo-600 mb-4') { 'RubyWasm Todo App π' }
|
|
77
|
+
|
|
78
|
+
# Add Todo Form
|
|
79
|
+
div(class: 'flex mb-4 gap-2') do
|
|
80
|
+
input(
|
|
81
|
+
type: 'text',
|
|
82
|
+
class: 'flex-1 px-4 py-2 border rounded-lg',
|
|
83
|
+
placeholder: 'What needs to be done?',
|
|
84
|
+
value: new_title,
|
|
85
|
+
oninput: ->(e) { self.new_title = e[:target][:value] }
|
|
86
|
+
)
|
|
87
|
+
button(
|
|
88
|
+
class: 'px-4 py-2 bg-indigo-600 text-white rounded-lg font-semibold',
|
|
89
|
+
onclick: -> { add_todo }
|
|
90
|
+
) { 'Add' }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Todo List
|
|
94
|
+
ul(class: 'divide-y divide-gray-200') do
|
|
95
|
+
items.each do |item|
|
|
96
|
+
li(class: 'py-3 flex items-center justify-between') do
|
|
97
|
+
span(class: item[:completed] ? 'line-through text-gray-400' : 'text-gray-800') do
|
|
98
|
+
item[:title]
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def add_todo
|
|
109
|
+
return if new_title.nil? || new_title.strip.empty?
|
|
110
|
+
self.items = items + [{ id: Time.now.to_i, title: new_title.strip, completed: false }]
|
|
111
|
+
self.new_title = ''
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## CLI Commands
|
|
119
|
+
|
|
120
|
+
| Command | Description |
|
|
121
|
+
| --- | --- |
|
|
122
|
+
| `web_ruby_ui new my_app` | Generates a new WebRubyUI project structure |
|
|
123
|
+
| `web_ruby_ui dev` | Launches a local WebAssembly web server at `http://localhost:8000` |
|
|
124
|
+
| `web_ruby_ui build` | Packages standalone `index.html` for production deployment |
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
MIT License.
|
data/bin/web_ruby_ui
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'thor'
|
|
4
|
+
require 'webrick'
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
|
|
7
|
+
module WebRubyUI
|
|
8
|
+
class CLI < Thor
|
|
9
|
+
map 'new' => :new_project
|
|
10
|
+
|
|
11
|
+
desc 'dev [APP_DIR]', 'Start a local WebAssembly dev server for a WebRubyUI app'
|
|
12
|
+
method_option :port, type: :numeric, default: 8000, aliases: '-p', desc: 'Port to run dev server on'
|
|
13
|
+
def dev(app_dir = '.')
|
|
14
|
+
port = options[:port]
|
|
15
|
+
say "π Starting WebRubyUI Wasm dev server on http://localhost:#{port}...", :green
|
|
16
|
+
|
|
17
|
+
target_dir = File.expand_path(app_dir)
|
|
18
|
+
index_path = File.join(target_dir, 'index.html')
|
|
19
|
+
|
|
20
|
+
ensure_index_html(index_path)
|
|
21
|
+
|
|
22
|
+
server = WEBrick::HTTPServer.new(
|
|
23
|
+
Port: port,
|
|
24
|
+
DocumentRoot: target_dir,
|
|
25
|
+
AccessLog: [],
|
|
26
|
+
Logger: WEBrick::Log.new(File::NULL)
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
trap('INT') { server.shutdown }
|
|
30
|
+
server.start
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
desc 'build [APP_DIR]', 'Build standalone HTML/Wasm package for production deployment'
|
|
34
|
+
def build(app_dir = '.')
|
|
35
|
+
target_dir = File.expand_path(app_dir)
|
|
36
|
+
index_path = File.join(target_dir, 'index.html')
|
|
37
|
+
|
|
38
|
+
ensure_index_html(index_path)
|
|
39
|
+
say "β¨ Successfully built WebRubyUI Wasm package in #{target_dir}/index.html", :green
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
desc 'new APP_NAME', 'Generate a new WebRubyUI Single-Page App project'
|
|
43
|
+
def new_project(app_name)
|
|
44
|
+
target_dir = File.expand_path(app_name)
|
|
45
|
+
FileUtils.mkdir_p(File.join(target_dir, 'app'))
|
|
46
|
+
|
|
47
|
+
app_rb = <<~RUBY
|
|
48
|
+
# frozen_string_literal: true
|
|
49
|
+
|
|
50
|
+
require 'web_ruby_ui'
|
|
51
|
+
|
|
52
|
+
class App < WebRubyUI::Component
|
|
53
|
+
state :count, default: 0
|
|
54
|
+
|
|
55
|
+
def render
|
|
56
|
+
div(class: 'card') do
|
|
57
|
+
h1 { "Welcome to WebRubyUI!" }
|
|
58
|
+
p { "Count: \#{count}" }
|
|
59
|
+
button(onclick: -> { self.count += 1 }) { "Increment" }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
RUBY
|
|
64
|
+
|
|
65
|
+
File.write(File.join(target_dir, 'app', 'app.rb'), app_rb)
|
|
66
|
+
ensure_index_html(File.join(target_dir, 'index.html'))
|
|
67
|
+
say "π Created new WebRubyUI project in #{target_dir}!", :green
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def ensure_index_html(path)
|
|
73
|
+
return if File.exist?(path)
|
|
74
|
+
|
|
75
|
+
html = <<~HTML
|
|
76
|
+
<!DOCTYPE html>
|
|
77
|
+
<html>
|
|
78
|
+
<head>
|
|
79
|
+
<meta charset="utf-8">
|
|
80
|
+
<title>WebRubyUI Application</title>
|
|
81
|
+
<script src="https://cdn.jsdelivr.net/npm/@ruby/3.3-wasm-wasi@2.5.0/dist/browser.script.iife.js"></script>
|
|
82
|
+
</head>
|
|
83
|
+
<body>
|
|
84
|
+
<div id="app">Loading WebRubyUI Wasm...</div>
|
|
85
|
+
<script type="ruby" src="./app/app.rb"></script>
|
|
86
|
+
</body>
|
|
87
|
+
</html>
|
|
88
|
+
HTML
|
|
89
|
+
|
|
90
|
+
File.write(path, html)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'vnode'
|
|
4
|
+
require_relative 'dom'
|
|
5
|
+
|
|
6
|
+
module WebRubyUI
|
|
7
|
+
# Base Component class providing reactive state tracking and HTML builder DSL.
|
|
8
|
+
class Component
|
|
9
|
+
class << self
|
|
10
|
+
def state(name, default: nil)
|
|
11
|
+
state_definitions[name] = default
|
|
12
|
+
|
|
13
|
+
define_method(name) do
|
|
14
|
+
@state_values[name]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
define_method("#{name}=") do |new_val|
|
|
18
|
+
old_val = @state_values[name]
|
|
19
|
+
return unless old_val != new_val
|
|
20
|
+
|
|
21
|
+
@state_values[name] = new_val
|
|
22
|
+
re_render!
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def state_definitions
|
|
27
|
+
@state_definitions ||= {}
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
attr_reader :mount_element_id
|
|
32
|
+
|
|
33
|
+
def initialize(props = {})
|
|
34
|
+
@props = props
|
|
35
|
+
@state_values = {}
|
|
36
|
+
self.class.state_definitions.each do |name, default|
|
|
37
|
+
@state_values[name] = default
|
|
38
|
+
end
|
|
39
|
+
@current_vnode = nil
|
|
40
|
+
@children_stack = []
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def mount(element_id)
|
|
44
|
+
@mount_element_id = element_id
|
|
45
|
+
re_render!
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def render
|
|
49
|
+
raise NotImplementedError, "#{self.class.name}#render must be implemented"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def re_render!
|
|
53
|
+
new_vnode = build_vnode_tree
|
|
54
|
+
@current_vnode = new_vnode
|
|
55
|
+
|
|
56
|
+
DOM.render_to_element(@mount_element_id, new_vnode) if @mount_element_id
|
|
57
|
+
new_vnode
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# HTML Builder DSL methods
|
|
61
|
+
%i[div h1 h2 h3 p span button input ul li a section header footer form label].each do |tag_name|
|
|
62
|
+
define_method(tag_name) do |attrs = {}, &block|
|
|
63
|
+
element(tag_name, attrs, &block)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def build_vnode_tree
|
|
70
|
+
@children_stack = []
|
|
71
|
+
vnode = render
|
|
72
|
+
vnode.is_a?(VNode) ? vnode : VNode.new(tag: :div, text_content: vnode.to_s)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def element(tag, attrs = {}, &block)
|
|
76
|
+
children = []
|
|
77
|
+
text_content = nil
|
|
78
|
+
|
|
79
|
+
if block
|
|
80
|
+
current_children = []
|
|
81
|
+
@children_stack.push(current_children)
|
|
82
|
+
|
|
83
|
+
result = instance_eval(&block)
|
|
84
|
+
text_content = result.to_s if current_children.empty? && result && !result.is_a?(VNode)
|
|
85
|
+
|
|
86
|
+
children = @children_stack.pop
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
node = VNode.new(tag: tag, attributes: attrs, children: children, text_content: text_content)
|
|
90
|
+
@children_stack.last << node if @children_stack.any?
|
|
91
|
+
node
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cgi'
|
|
4
|
+
|
|
5
|
+
module WebRubyUI
|
|
6
|
+
# Bridge to browser DOM via WebAssembly JS interop (JS::Object) or in-memory simulation.
|
|
7
|
+
class DOM
|
|
8
|
+
class << self
|
|
9
|
+
def event_registry
|
|
10
|
+
@event_registry ||= {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def register_event(element_id, event_type, handler)
|
|
14
|
+
key = "#{element_id}_#{event_type}"
|
|
15
|
+
event_registry[key] = handler
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def trigger_event(element_id, event_type, event_data = {})
|
|
19
|
+
key = "#{element_id}_#{event_type}"
|
|
20
|
+
handler = event_registry[key]
|
|
21
|
+
handler&.call(event_data)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def render_to_element(element_id, vnode)
|
|
25
|
+
if Object.const_defined?(:JS)
|
|
26
|
+
js_document = JS.global[:document]
|
|
27
|
+
js_target = js_document.getElementById(element_id.to_s)
|
|
28
|
+
if js_target
|
|
29
|
+
js_target[:innerHTML] = render_html_string(vnode)
|
|
30
|
+
bind_js_events(js_document, vnode)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
render_html_string(vnode)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def render_html_string(vnode)
|
|
38
|
+
return CGI.escapeHTML(vnode.text_content.to_s) unless vnode.is_a?(VNode)
|
|
39
|
+
|
|
40
|
+
attrs = vnode.attributes.map do |k, v|
|
|
41
|
+
next if k.to_s.start_with?('on') # Skip event handlers in HTML attributes
|
|
42
|
+
|
|
43
|
+
"#{k}=\"#{v}\""
|
|
44
|
+
end.compact.join(' ')
|
|
45
|
+
|
|
46
|
+
attr_str = attrs.empty? ? '' : " #{attrs}"
|
|
47
|
+
|
|
48
|
+
if vnode.children.empty? && vnode.text_content
|
|
49
|
+
escaped_text = CGI.escapeHTML(vnode.text_content.to_s)
|
|
50
|
+
"<#{vnode.tag}#{attr_str}>#{escaped_text}</#{vnode.tag}>"
|
|
51
|
+
else
|
|
52
|
+
children_html = vnode.children.map { |c| render_html_string(c) }.join
|
|
53
|
+
"<#{vnode.tag}#{attr_str}>#{children_html}</#{vnode.tag}>"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def bind_js_events(document, vnode)
|
|
60
|
+
return unless vnode.is_a?(VNode)
|
|
61
|
+
|
|
62
|
+
vnode.attributes.each do |attr_name, handler|
|
|
63
|
+
next unless attr_name.to_s.start_with?('on') && handler.is_a?(Proc)
|
|
64
|
+
|
|
65
|
+
event_type = attr_name.to_s.sub(/\Aon/, '')
|
|
66
|
+
elem_id = vnode.attributes[:id] || "wrui_#{vnode.object_id}"
|
|
67
|
+
register_event(elem_id, event_type, handler)
|
|
68
|
+
|
|
69
|
+
js_elem = document.getElementById(elem_id.to_s)
|
|
70
|
+
next unless js_elem
|
|
71
|
+
|
|
72
|
+
js_elem.addEventListener(event_type) do |e|
|
|
73
|
+
handler.call(e)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
vnode.children.each { |c| bind_js_events(document, c) }
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WebRubyUI
|
|
4
|
+
# Client-side router matching URL paths to WebRubyUI Components.
|
|
5
|
+
class Router
|
|
6
|
+
attr_reader :routes
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@routes = {}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def route(path, component_class)
|
|
13
|
+
@routes[path.to_s] = component_class
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def match(path)
|
|
17
|
+
clean_path = path.to_s.empty? ? '/' : path.to_s
|
|
18
|
+
@routes[clean_path] || @routes['/']
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def navigate(path, target_element_id = 'app')
|
|
22
|
+
component_class = match(path)
|
|
23
|
+
return nil unless component_class
|
|
24
|
+
|
|
25
|
+
component = component_class.new(path: path)
|
|
26
|
+
component.mount(target_element_id)
|
|
27
|
+
component
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WebRubyUI
|
|
4
|
+
# Represents a virtual DOM node in pure Ruby memory.
|
|
5
|
+
class VNode
|
|
6
|
+
attr_reader :tag, :attributes, :children, :text_content
|
|
7
|
+
|
|
8
|
+
def initialize(tag:, attributes: {}, children: [], text_content: nil)
|
|
9
|
+
@tag = tag.to_s.downcase
|
|
10
|
+
@attributes = attributes || {}
|
|
11
|
+
@children = children || []
|
|
12
|
+
@text_content = text_content
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def to_h
|
|
16
|
+
{
|
|
17
|
+
tag: @tag,
|
|
18
|
+
attributes: @attributes,
|
|
19
|
+
children: @children.map { |c| c.is_a?(VNode) ? c.to_h : c },
|
|
20
|
+
text_content: @text_content
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def diff(other_vnode)
|
|
25
|
+
patches = []
|
|
26
|
+
|
|
27
|
+
return [{ type: :remove }] if other_vnode.nil?
|
|
28
|
+
|
|
29
|
+
return [{ type: :replace, new_node: other_vnode }] if tag != other_vnode.tag
|
|
30
|
+
|
|
31
|
+
patches << { type: :update_text, new_text: other_vnode.text_content } if text_content != other_vnode.text_content
|
|
32
|
+
|
|
33
|
+
# Attr diffing
|
|
34
|
+
attr_diff = diff_attributes(attributes, other_vnode.attributes)
|
|
35
|
+
patches << { type: :update_attrs, attrs: attr_diff } if attr_diff.any?
|
|
36
|
+
|
|
37
|
+
patches
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def diff_attributes(old_attrs, new_attrs)
|
|
43
|
+
diffs = {}
|
|
44
|
+
(old_attrs.keys | new_attrs.keys).each do |k|
|
|
45
|
+
diffs[k] = new_attrs[k] if old_attrs[k] != new_attrs[k]
|
|
46
|
+
end
|
|
47
|
+
diffs
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/lib/web_ruby_ui.rb
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'web_ruby_ui/version'
|
|
4
|
+
require_relative 'web_ruby_ui/vnode'
|
|
5
|
+
require_relative 'web_ruby_ui/dom'
|
|
6
|
+
require_relative 'web_ruby_ui/component'
|
|
7
|
+
require_relative 'web_ruby_ui/router'
|
|
8
|
+
require_relative 'web_ruby_ui/cli'
|
|
9
|
+
|
|
10
|
+
module WebRubyUI
|
|
11
|
+
class Error < StandardError; end
|
|
12
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: web_ruby_ui
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Aditya
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-08-06 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: thor
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.3'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.3'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: webrick
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '1.8'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '1.8'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: bundler
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '2.0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '2.0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rake
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '13.0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '13.0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: rspec
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '3.0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '3.0'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: rubocop
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '1.50'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '1.50'
|
|
97
|
+
description: Build reactive, single-page web applications with components, state management,
|
|
98
|
+
and virtual DOM diffing in pure Ruby running on WebAssembly.
|
|
99
|
+
email:
|
|
100
|
+
executables:
|
|
101
|
+
- web_ruby_ui
|
|
102
|
+
extensions: []
|
|
103
|
+
extra_rdoc_files: []
|
|
104
|
+
files:
|
|
105
|
+
- LICENSE
|
|
106
|
+
- README.md
|
|
107
|
+
- bin/web_ruby_ui
|
|
108
|
+
- lib/web_ruby_ui.rb
|
|
109
|
+
- lib/web_ruby_ui/cli.rb
|
|
110
|
+
- lib/web_ruby_ui/component.rb
|
|
111
|
+
- lib/web_ruby_ui/dom.rb
|
|
112
|
+
- lib/web_ruby_ui/router.rb
|
|
113
|
+
- lib/web_ruby_ui/version.rb
|
|
114
|
+
- lib/web_ruby_ui/vnode.rb
|
|
115
|
+
homepage: https://github.com/aditya-8108/web_ruby_ui
|
|
116
|
+
licenses:
|
|
117
|
+
- MIT
|
|
118
|
+
metadata:
|
|
119
|
+
homepage_uri: https://github.com/aditya-8108/web_ruby_ui
|
|
120
|
+
source_code_uri: https://github.com/aditya-8108/web_ruby_ui
|
|
121
|
+
rubygems_mfa_required: 'true'
|
|
122
|
+
post_install_message:
|
|
123
|
+
rdoc_options: []
|
|
124
|
+
require_paths:
|
|
125
|
+
- lib
|
|
126
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
- - ">="
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: 3.0.0
|
|
131
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
132
|
+
requirements:
|
|
133
|
+
- - ">="
|
|
134
|
+
- !ruby/object:Gem::Version
|
|
135
|
+
version: '0'
|
|
136
|
+
requirements: []
|
|
137
|
+
rubygems_version: 3.3.5
|
|
138
|
+
signing_key:
|
|
139
|
+
specification_version: 4
|
|
140
|
+
summary: 100% Pure Ruby Frontend Web Framework in WebAssembly
|
|
141
|
+
test_files: []
|