ruwi 0.10.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.
Files changed (117) hide show
  1. checksums.yaml +7 -0
  2. data/.cursor/rules/ruby_comments.mdc +29 -0
  3. data/.github/workflows/playwright.yml +74 -0
  4. data/.github/workflows/rspec.yml +31 -0
  5. data/.node-version +1 -0
  6. data/CODE_OF_CONDUCT.md +132 -0
  7. data/LICENSE.txt +21 -0
  8. data/Makefile +56 -0
  9. data/README.md +237 -0
  10. data/Rakefile +4 -0
  11. data/docs/conditional-rendering.md +119 -0
  12. data/docs/lifecycle-hooks.md +75 -0
  13. data/docs/list-rendering.md +51 -0
  14. data/examples/.gitignore +4 -0
  15. data/examples/Gemfile +5 -0
  16. data/examples/Gemfile.lock +39 -0
  17. data/examples/Makefile +15 -0
  18. data/examples/npm-packages/runtime/counter/index.html +28 -0
  19. data/examples/npm-packages/runtime/counter/index.rb +62 -0
  20. data/examples/npm-packages/runtime/counter/index.spec.js +42 -0
  21. data/examples/npm-packages/runtime/hello/index.html +28 -0
  22. data/examples/npm-packages/runtime/hello/index.rb +29 -0
  23. data/examples/npm-packages/runtime/hello/index.spec.js +53 -0
  24. data/examples/npm-packages/runtime/input/index.html +28 -0
  25. data/examples/npm-packages/runtime/input/index.rb +46 -0
  26. data/examples/npm-packages/runtime/input/index.spec.js +58 -0
  27. data/examples/npm-packages/runtime/list/index.html +27 -0
  28. data/examples/npm-packages/runtime/list/index.rb +33 -0
  29. data/examples/npm-packages/runtime/list/index.spec.js +46 -0
  30. data/examples/npm-packages/runtime/on_mounted_demo/index.html +40 -0
  31. data/examples/npm-packages/runtime/on_mounted_demo/index.rb +59 -0
  32. data/examples/npm-packages/runtime/on_mounted_demo/index.spec.js +50 -0
  33. data/examples/npm-packages/runtime/r_if_attribute_demo/index.html +34 -0
  34. data/examples/npm-packages/runtime/r_if_attribute_demo/index.rb +113 -0
  35. data/examples/npm-packages/runtime/r_if_attribute_demo/index.spec.js +140 -0
  36. data/examples/npm-packages/runtime/random_cocktail/index.html +27 -0
  37. data/examples/npm-packages/runtime/random_cocktail/index.rb +69 -0
  38. data/examples/npm-packages/runtime/random_cocktail/index.spec.js +101 -0
  39. data/examples/npm-packages/runtime/search_field/index.html +27 -0
  40. data/examples/npm-packages/runtime/search_field/index.rb +39 -0
  41. data/examples/npm-packages/runtime/search_field/index.spec.js +59 -0
  42. data/examples/npm-packages/runtime/todos/index.html +28 -0
  43. data/examples/npm-packages/runtime/todos/index.rb +239 -0
  44. data/examples/npm-packages/runtime/todos/index.spec.js +161 -0
  45. data/examples/npm-packages/runtime/todos/todos_repository.rb +23 -0
  46. data/examples/package.json +12 -0
  47. data/examples/src/counter/index.html +23 -0
  48. data/examples/src/counter/index.rb +60 -0
  49. data/examples/src/index.html +21 -0
  50. data/examples/src/index.rb +26 -0
  51. data/examples/src/todos/index.html +23 -0
  52. data/examples/src/todos/index.rb +237 -0
  53. data/examples/src/todos/todos_repository.rb +23 -0
  54. data/exe/ruwi +6 -0
  55. data/lib/ruwi/cli/command/base.rb +192 -0
  56. data/lib/ruwi/cli/command/dev.rb +207 -0
  57. data/lib/ruwi/cli/command/pack.rb +36 -0
  58. data/lib/ruwi/cli/command/rebuild.rb +38 -0
  59. data/lib/ruwi/cli/command/setup.rb +159 -0
  60. data/lib/ruwi/cli/command.rb +48 -0
  61. data/lib/ruwi/runtime/app.rb +53 -0
  62. data/lib/ruwi/runtime/component.rb +215 -0
  63. data/lib/ruwi/runtime/dispatcher.rb +46 -0
  64. data/lib/ruwi/runtime/dom/attributes.rb +105 -0
  65. data/lib/ruwi/runtime/dom/destroy_dom.rb +63 -0
  66. data/lib/ruwi/runtime/dom/events.rb +40 -0
  67. data/lib/ruwi/runtime/dom/mount_dom.rb +108 -0
  68. data/lib/ruwi/runtime/dom/patch_dom.rb +237 -0
  69. data/lib/ruwi/runtime/dom/scheduler.rb +51 -0
  70. data/lib/ruwi/runtime/dom.rb +13 -0
  71. data/lib/ruwi/runtime/nodes_equal.rb +45 -0
  72. data/lib/ruwi/runtime/template/build_conditional_group.rb +150 -0
  73. data/lib/ruwi/runtime/template/build_for_group.rb +125 -0
  74. data/lib/ruwi/runtime/template/build_vdom.rb +220 -0
  75. data/lib/ruwi/runtime/template/parser.rb +134 -0
  76. data/lib/ruwi/runtime/template.rb +11 -0
  77. data/lib/ruwi/runtime/utils/arrays.rb +185 -0
  78. data/lib/ruwi/runtime/utils/objects.rb +37 -0
  79. data/lib/ruwi/runtime/utils/props.rb +25 -0
  80. data/lib/ruwi/runtime/utils/strings.rb +19 -0
  81. data/lib/ruwi/runtime/utils.rb +11 -0
  82. data/lib/ruwi/runtime/vdom.rb +84 -0
  83. data/lib/ruwi/version.rb +5 -0
  84. data/lib/ruwi.rb +14 -0
  85. data/package-lock.json +73 -0
  86. data/package.json +32 -0
  87. data/packages/npm-packages/runtime/README.md +5 -0
  88. data/packages/npm-packages/runtime/eslint.config.mjs +16 -0
  89. data/packages/npm-packages/runtime/package-lock.json +6668 -0
  90. data/packages/npm-packages/runtime/package.json +38 -0
  91. data/packages/npm-packages/runtime/rollup.config.mjs +147 -0
  92. data/packages/npm-packages/runtime/src/__tests__/sample.test.js +5 -0
  93. data/packages/npm-packages/runtime/src/index.js +37 -0
  94. data/packages/npm-packages/runtime/src/ruwi +1 -0
  95. data/packages/npm-packages/runtime/src/ruwi.rb +1 -0
  96. data/packages/npm-packages/runtime/vitest.config.js +8 -0
  97. data/playwright.config.js +78 -0
  98. data/sig/ruwi.rbs +4 -0
  99. data/spec/ruwi/cli/command/base_spec.rb +503 -0
  100. data/spec/ruwi/cli/command/dev_spec.rb +442 -0
  101. data/spec/ruwi/cli/command/pack_spec.rb +131 -0
  102. data/spec/ruwi/cli/command/rebuild_spec.rb +95 -0
  103. data/spec/ruwi/cli/command/setup_spec.rb +251 -0
  104. data/spec/ruwi/cli/command_spec.rb +118 -0
  105. data/spec/ruwi/runtime/component_spec.rb +416 -0
  106. data/spec/ruwi/runtime/dom/scheduler_spec.rb +98 -0
  107. data/spec/ruwi/runtime/nodes_equal_spec.rb +190 -0
  108. data/spec/ruwi/runtime/template/build_conditional_group_spec.rb +505 -0
  109. data/spec/ruwi/runtime/template/build_for_group_spec.rb +377 -0
  110. data/spec/ruwi/runtime/template/build_vdom_spec.rb +573 -0
  111. data/spec/ruwi/runtime/template/parser_spec.rb +627 -0
  112. data/spec/ruwi/runtime/utils/arrays_spec.rb +228 -0
  113. data/spec/ruwi/runtime/utils/objects_spec.rb +127 -0
  114. data/spec/ruwi/runtime/utils/props_spec.rb +205 -0
  115. data/spec/ruwi/runtime/utils/strings_spec.rb +107 -0
  116. data/spec/spec_helper.rb +16 -0
  117. metadata +229 -0
@@ -0,0 +1,119 @@
1
+ # Conditional Rendering with r-if
2
+
3
+ ruwi provides the `r-if` directive for conditional rendering of elements based on state or computed values:
4
+
5
+ ```ruby
6
+ # Component demonstrating r-if conditional rendering
7
+ ConditionalComponent = Ruwi.define_component(
8
+ state: ->() {
9
+ {
10
+ show_message: false,
11
+ counter: 0
12
+ }
13
+ },
14
+
15
+ template: ->() {
16
+ Ruwi::Template::Parser.parse_and_eval(<<~HTML, binding)
17
+ <div>
18
+ <!-- Simple boolean condition -->
19
+ <div r-if="{state[:show_message]}">
20
+ <p>This message is conditionally rendered!</p>
21
+ </div>
22
+
23
+ <!-- Using r-if, r-elsif, r-else for multiple conditions -->
24
+ <div r-if="{state[:counter] > 0}">
25
+ <p>Counter is positive: {state[:counter]}</p>
26
+ </div>
27
+
28
+ <div r-elsif="{state[:counter] < 0}">
29
+ <p>Counter is negative: {state[:counter]}</p>
30
+ </div>
31
+
32
+ <div r-else>
33
+ <p>Counter is zero</p>
34
+ </div>
35
+
36
+ <!-- Toggle button -->
37
+ <button on="{click: ->() { toggle_message }}">
38
+ {state[:show_message] ? "Hide" : "Show"} Message
39
+ </button>
40
+ </div>
41
+ HTML
42
+ },
43
+
44
+ methods: {
45
+ toggle_message: ->() {
46
+ update_state(show_message: !state[:show_message])
47
+ }
48
+ }
49
+ )
50
+ ```
51
+
52
+ ## r-if, r-elsif, r-else Syntax
53
+
54
+ The conditional directives work together to create if-elsif-else chains:
55
+
56
+ ### Basic Usage
57
+
58
+ - **`r-if="{condition}"`**: Renders the element when condition is truthy
59
+ - **`r-elsif="{condition}"`**: Renders when previous conditions are falsy and this condition is truthy
60
+ - **`r-else`**: Renders when all previous conditions are falsy (no condition needed)
61
+
62
+ ### Expression Evaluation
63
+
64
+ All conditional expressions are evaluated as Ruby code within curly braces `{}`:
65
+
66
+ - Use any valid Ruby expression that returns a truthy or falsy value
67
+ - Access component state with `state[:key]`
68
+ - Access props with `props[:key]`
69
+ - Support for comparison operators (`>`, `<`, `==`, `!=`, etc.)
70
+ - Support for logical operators (`&&`, `||`, `!`)
71
+ - Support for method calls and complex expressions
72
+
73
+ ### Conditional Chain Rules
74
+
75
+ 1. **Sequential Processing**: Conditions are evaluated in order (r-if → r-elsif → r-else)
76
+ 2. **Mutual Exclusivity**: Only one element in a conditional chain will render
77
+ 3. **Grouping**: Consecutive conditional elements form a single conditional group
78
+ 4. **Breaking**: A new `r-if` breaks the current conditional chain and starts a new one
79
+
80
+ ### Advanced Example
81
+
82
+ ```ruby
83
+ # Real-world example with loading states and data
84
+ LoadingComponent = Ruwi.define_component(
85
+ state: ->() {
86
+ {
87
+ is_loading: false,
88
+ data: nil,
89
+ error: nil
90
+ }
91
+ },
92
+
93
+ template: ->() {
94
+ Ruwi::Template::Parser.parse_and_eval(<<~HTML, binding)
95
+ <div>
96
+ <!-- Loading state -->
97
+ <p r-if="{state[:is_loading]}">Loading...</p>
98
+
99
+ <!-- Error state -->
100
+ <div r-elsif="{state[:error]}" style="color: red;">
101
+ <p>Error: {state[:error]}</p>
102
+ <button on="{click: ->() { retry_load }}">Retry</button>
103
+ </div>
104
+
105
+ <!-- Success state with data -->
106
+ <div r-elsif="{state[:data]}">
107
+ <h2>{state[:data][:title]}</h2>
108
+ <p>{state[:data][:description]}</p>
109
+ </div>
110
+
111
+ <!-- Initial state (no loading, no error, no data) -->
112
+ <button r-else on="{click: ->() { load_data }}">
113
+ Load Data
114
+ </button>
115
+ </div>
116
+ HTML
117
+ }
118
+ )
119
+ ```
@@ -0,0 +1,75 @@
1
+ # Lifecycle Hooks
2
+
3
+ Components support lifecycle hooks to execute code at specific points in a component's lifecycle:
4
+
5
+ ```ruby
6
+ RandomCocktailComponent = Ruwi.define_component(
7
+ state: ->() {
8
+ {
9
+ is_loading: false,
10
+ cocktail: nil
11
+ }
12
+ },
13
+
14
+ methods: {
15
+ fetch_cocktail: -> {
16
+ # Set loading state
17
+ update_state(is_loading: true, cocktail: nil)
18
+
19
+ # Use Fiber for asynchronous API call
20
+ Fiber.new do
21
+ response = JS.global.fetch("https://www.thecocktaildb.com/api/json/v1/1/random.php").await
22
+ response.call(:json).then(->(data) {
23
+ update_state(is_loading: false, cocktail: data[:drinks][0])
24
+ }).catch(->(error) {
25
+ update_state(is_loading: false, cocktail: nil)
26
+ })
27
+ end.transfer
28
+ }
29
+ },
30
+
31
+ # Called after the component is mounted to the DOM
32
+ on_mounted: ->() {
33
+ fetch_cocktail
34
+ },
35
+
36
+ template: ->() {
37
+ is_loading = state[:is_loading] # Used in template
38
+ cocktail = state[:cocktail] # Used in template
39
+
40
+ template = <<~HTML
41
+ <div>
42
+ <p r-if="{is_loading}">Loading...</p>
43
+ <button r-elsif="{cocktail.nil?}" on="{click: ->() { fetch_cocktail }}">
44
+ Get a cocktail
45
+ </button>
46
+ <template r-else>
47
+ <h2>{cocktail['strDrink']}</h2>
48
+ <p>{cocktail['strInstructions']}</p>
49
+ <img src="{cocktail['strDrinkThumb']}" alt="{cocktail['strDrink']}" style="width: 300px; height: 300px" />
50
+ <button on="{click: ->() { fetch_cocktail }}">
51
+ Get another cocktail
52
+ </button>
53
+ </template>
54
+ </div>
55
+ HTML
56
+
57
+ Ruwi::Template::Parser.parse_and_eval(template, binding)
58
+ }
59
+ )
60
+ ```
61
+
62
+ ## Available Lifecycle Hooks
63
+
64
+ ### on_mounted
65
+
66
+ The `on_mounted` hook is called after the component is mounted to the DOM. This is useful for:
67
+
68
+ - Making initial API calls
69
+ - Setting up event listeners
70
+ - Initializing third-party libraries
71
+ - Performing DOM manipulations that require the element to be in the document
72
+
73
+ ## Asynchronous Operations
74
+
75
+ Note: Unlike JavaScript frameworks, asynchronous operations in ruwi are designed to use Ruby's Fiber system rather than Promises.
@@ -0,0 +1,51 @@
1
+ # List Rendering with r-for
2
+
3
+ ruwi provides the `r-for` directive for rendering lists of items. You can use `r-for` with both components and regular HTML elements:
4
+
5
+ ```ruby
6
+ # Define a reusable list item component
7
+ ListItem = Ruwi.define_component(
8
+ template: ->() {
9
+ Ruwi::Template::Parser.parse_and_eval(<<~HTML, binding)
10
+ <li>{props[:todo]}</li>
11
+ HTML
12
+ }
13
+ )
14
+
15
+ # Main list component demonstrating r-for usage
16
+ List = Ruwi.define_component(
17
+ template: ->() {
18
+ Ruwi::Template::Parser.parse_and_eval(<<~HTML, binding)
19
+ <ul>
20
+ <!-- Using r-for with a component -->
21
+ <ListItem
22
+ r-for="{todo in props[:todos]}"
23
+ todo="{todo}"
24
+ />
25
+
26
+ <!-- Using r-for with regular HTML elements -->
27
+ <li r-for="todo in props[:todos]">
28
+ { todo }
29
+ </li>
30
+ </ul>
31
+ HTML
32
+ }
33
+ )
34
+
35
+ # Create and mount the app with initial data
36
+ app = Ruwi::App.create(List, { todos: ['foo', 'bar', 'baz'] })
37
+ app_element = JS.global[:document].getElementById("app")
38
+ app.mount(app_element)
39
+ ```
40
+
41
+ ## r-for Syntax
42
+
43
+ The `r-for` directive uses the syntax `"item in collection"` where:
44
+
45
+ - `item` is the variable name for each iteration
46
+ - `collection` is the array or enumerable to iterate over
47
+
48
+ You can use `r-for` in two ways:
49
+
50
+ 1. **With components**: Pass the current item as props to child components
51
+ 2. **With HTML elements**: Directly render HTML elements for each item
@@ -0,0 +1,4 @@
1
+ ruby.wasm
2
+ /rubies
3
+ /build
4
+ /dist
data/examples/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "ruwi", path: "../../"
@@ -0,0 +1,39 @@
1
+ PATH
2
+ remote: ../..
3
+ specs:
4
+ ruwi (0.9.1)
5
+ js (~> 2.7)
6
+ listen (~> 3.8)
7
+ puma (~> 6.0)
8
+ rack (~> 3.0)
9
+ ruby_wasm (~> 2.7)
10
+
11
+ GEM
12
+ remote: https://rubygems.org/
13
+ specs:
14
+ ffi (1.17.2-arm64-darwin)
15
+ js (2.7.2)
16
+ listen (3.9.0)
17
+ rb-fsevent (~> 0.10, >= 0.10.3)
18
+ rb-inotify (~> 0.9, >= 0.9.10)
19
+ logger (1.7.0)
20
+ nio4r (2.7.5)
21
+ puma (6.6.1)
22
+ nio4r (~> 2.0)
23
+ rack (3.2.4)
24
+ rb-fsevent (0.11.2)
25
+ rb-inotify (0.11.1)
26
+ ffi (~> 1.0)
27
+ ruby_wasm (2.7.2)
28
+ logger
29
+ ruby_wasm (2.7.2-arm64-darwin)
30
+ logger
31
+
32
+ PLATFORMS
33
+ arm64-darwin
34
+
35
+ DEPENDENCIES
36
+ ruwi!
37
+
38
+ BUNDLED WITH
39
+ 2.7.2
data/examples/Makefile ADDED
@@ -0,0 +1,15 @@
1
+ .PHONY: setup
2
+ setup: ## Setup app
3
+ bundle install
4
+
5
+ .PHONY: build
6
+ build: ## Build app
7
+ bundle exec rbwasm build --ruby-version 3.4 -o ruby.wasm
8
+
9
+ .PHONY: pack
10
+ pack: ## Pack app
11
+ bundle exec rbwasm pack ruby.wasm --dir ./src::./src -o src.wasm
12
+
13
+ .PHONY: serve
14
+ serve: ## Serve app
15
+ npx http-server . -o './src' --cors -P 'http://localhost:8080?' -c-1
@@ -0,0 +1,28 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+
8
+ <script>
9
+ const urlParams = new URLSearchParams(window.location.search);
10
+ const isDev = urlParams.get("env") === "DEV";
11
+ const script = document.createElement("script");
12
+ script.defer = true;
13
+ script.src = isDev
14
+ ? "../../../../packages/npm-packages/runtime/dist/ruwi.js"
15
+ : "https://unpkg.com/ruwi@latest";
16
+ document.head.appendChild(script);
17
+ </script>
18
+ <script defer type="text/ruby" src="index.rb"></script>
19
+
20
+ <title>Counter</title>
21
+ </head>
22
+
23
+ <body>
24
+ <h1>Counter</h1>
25
+ <div id="app-a"></div>
26
+ <div id="app-b"></div>
27
+ </body>
28
+ </html>
@@ -0,0 +1,62 @@
1
+ require "js"
2
+
3
+ # Counter component using the latest component-based API with TemplateParser
4
+ CounterComponent = Ruwi.define_component(
5
+ # Initialize component state
6
+ state: ->(props) {
7
+ { count: props[:count] || 0 }
8
+ },
9
+
10
+ # Render the counter component
11
+ template: ->() {
12
+ Ruwi::Template::Parser.parse_and_eval(<<~HTML, binding)
13
+ <div>
14
+ <div>{state[:count]}</div>
15
+ <!-- Both ButtonComponent and button-component are valid -->
16
+ <ButtonComponent
17
+ label="Increment"
18
+ on="{ click_button: -> { increment } }">
19
+ </ButtonComponent>
20
+ <button-component
21
+ label="Decrement"
22
+ on="{ click_button: -> { decrement } }"
23
+ />
24
+ </div>
25
+ HTML
26
+ },
27
+
28
+ # Component methods
29
+ methods: {
30
+ # Increment the counter
31
+ increment: ->() {
32
+ update_state(count: state[:count] + 1)
33
+ },
34
+
35
+ # Decrement the counter
36
+ decrement: ->() {
37
+ update_state(count: state[:count] - 1)
38
+ }
39
+ }
40
+ )
41
+
42
+ # Button component - reusable button with click handler
43
+ ButtonComponent = Ruwi.define_component(
44
+ template: ->() {
45
+ Ruwi::Template::Parser.parse_and_eval(<<~HTML, binding)
46
+ <button on="{ click: ->() { emit('click_button') } }">
47
+ {props[:label]}
48
+ </button>
49
+ HTML
50
+ }
51
+ )
52
+
53
+ # app_a to be destroyed
54
+ app_a = Ruwi::App.create(CounterComponent, count: 0)
55
+ app_element_a = JS.global[:document].getElementById("app-a")
56
+ app_a.mount(app_element_a)
57
+ app_a.unmount
58
+
59
+ # app_b to be mounted
60
+ app_b = Ruwi::App.create(CounterComponent, count: 10)
61
+ app_element_b = JS.global[:document].getElementById("app-b")
62
+ app_b.mount(app_element_b)
@@ -0,0 +1,42 @@
1
+ import { test, expect } from "@playwright/test";
2
+
3
+ test.describe("Counter Example", () => {
4
+ test("should display counter and handle increment/decrement operations", async ({
5
+ page,
6
+ }) => {
7
+ await page.goto("/examples/npm-packages/runtime/counter/index.html?env=DEV");
8
+ await page.waitForTimeout(3000);
9
+
10
+ // Check the page title
11
+ await expect(page).toHaveTitle("Counter");
12
+
13
+ // Check the main heading
14
+ await expect(page.locator("h1")).toHaveText("Counter");
15
+
16
+ // Check that app-b is mounted with initial count of 10
17
+ const counterDisplay = page.locator("#app-b > div > div:first-child");
18
+ await expect(counterDisplay).toHaveText("10");
19
+
20
+ // Verify increment and decrement buttons are present
21
+ const incrementBtn = page.locator("#app-b button").first();
22
+ const decrementBtn = page.locator("#app-b button").last();
23
+ await expect(incrementBtn).toHaveText("Increment");
24
+ await expect(decrementBtn).toHaveText("Decrement");
25
+
26
+ // Test increment operations
27
+ await incrementBtn.click();
28
+ await incrementBtn.click();
29
+ await incrementBtn.click();
30
+ await page.waitForTimeout(100);
31
+ await expect(counterDisplay).toHaveText("13");
32
+
33
+ // Test decrement operations
34
+ await decrementBtn.click();
35
+ await decrementBtn.click();
36
+ await decrementBtn.click();
37
+ await decrementBtn.click();
38
+ await decrementBtn.click();
39
+ await page.waitForTimeout(100);
40
+ await expect(counterDisplay).toHaveText("8");
41
+ });
42
+ });
@@ -0,0 +1,28 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+
8
+ <script>
9
+ const urlParams = new URLSearchParams(window.location.search);
10
+ const isDev = urlParams.get("env") === "DEV";
11
+ const script = document.createElement("script");
12
+ script.defer = true;
13
+ script.src = isDev
14
+ ? "../../../../packages/npm-packages/runtime/dist/ruwi.js"
15
+ : "https://unpkg.com/ruwi@latest";
16
+ document.head.appendChild(script);
17
+ </script>
18
+ <script defer type="text/ruby" src="index.rb"></script>
19
+
20
+ <title>Hello World</title>
21
+ </head>
22
+
23
+ <body>
24
+ <h1>Hello World</h1>
25
+ <div id="h1-a"></div>
26
+ <div id="h1-b"></div>
27
+ </body>
28
+ </html>
@@ -0,0 +1,29 @@
1
+ require "js"
2
+
3
+ # h_a to be destroyed
4
+ h_a = Ruwi::Vdom.h("div", {}, [Ruwi::Vdom.h("h1", {}, ["Hello, world!"])])
5
+ h1_a_element = JS.global[:document].getElementById("h1-a")
6
+ Ruwi::Dom::Events.add_event_listener("click", ->(e) { puts "clicked" }, h1_a_element)
7
+ Ruwi::Dom::Attributes.set_attributes(h1_a_element, {
8
+ class: "bg-red-500"
9
+ })
10
+ Ruwi::Dom::Attributes.set_attribute(h1_a_element, "data-test", "test")
11
+ Ruwi::Dom::Attributes.set_class(h1_a_element, "bg-blue-500")
12
+ Ruwi::Dom::Attributes.set_style(h1_a_element, "background-color", "red")
13
+ Ruwi::Dom::Attributes.remove_attribute(h1_a_element, "data-test")
14
+ Ruwi::Dom::Attributes.remove_style(h1_a_element, "background-color")
15
+
16
+ Ruwi::Dom::MountDom.execute(h_a, h1_a_element)
17
+ Ruwi::Dom::DestroyDom.execute(h_a)
18
+
19
+ # h_b to be mounted
20
+ h_b = Ruwi::Vdom.h("div", {}, [Ruwi::Vdom.h("h1", {}, ["Hello, world!"])])
21
+ h1_b_element = JS.global[:document].getElementById("h1-b")
22
+ Ruwi::Dom::Events.add_event_listener("click", ->(e) { puts "clicked" }, h1_b_element)
23
+ Ruwi::Dom::Attributes.set_attributes(h1_b_element, {
24
+ class: "bg-red-500"
25
+ })
26
+ Ruwi::Dom::Attributes.set_attribute(h1_b_element, "data-test", "test")
27
+ Ruwi::Dom::Attributes.set_class(h1_b_element, "bg-red-500")
28
+ Ruwi::Dom::Attributes.set_style(h1_b_element, "background-color", "blue")
29
+ Ruwi::Dom::MountDom.execute(h_b, h1_b_element)
@@ -0,0 +1,53 @@
1
+ import { test, expect } from "@playwright/test";
2
+
3
+ test.describe("Hello Example", () => {
4
+ test("should display hello world content and handle click events", async ({
5
+ page,
6
+ }) => {
7
+ // Navigate to the hello example
8
+ await page.goto("/examples/npm-packages/runtime/hello/index.html?env=DEV");
9
+
10
+ // Wait for Ruby WASM to load and initialize
11
+ await page.waitForTimeout(3000);
12
+
13
+ // Check the page title
14
+ await expect(page).toHaveTitle("Hello World");
15
+
16
+ // Check the main heading (first h1 in body)
17
+ await expect(page.locator("body > h1")).toHaveText("Hello World");
18
+
19
+ // Check that the h1-b div contains the "Hello, world!" text
20
+ await expect(page.locator("#h1-b h1")).toHaveText("Hello, world!");
21
+
22
+ // Verify that h1-b element has the expected classes and styles
23
+ const h1BElement = page.locator("#h1-b");
24
+ await expect(h1BElement).toHaveClass(/bg-red-500/);
25
+ await expect(h1BElement).toHaveAttribute("data-test", "test");
26
+
27
+ // Check the inline style for background color
28
+ const computedStyle = await h1BElement.evaluate(
29
+ (el) => getComputedStyle(el).backgroundColor
30
+ );
31
+ expect(computedStyle).toBe("rgb(0, 0, 255)"); // blue color
32
+
33
+ // Test click event handling
34
+ const consoleMessages = [];
35
+ page.on("console", (msg) => {
36
+ if (msg.type() === "log") {
37
+ consoleMessages.push(msg.text());
38
+ }
39
+ });
40
+
41
+ // Click on the h1-b element
42
+ await h1BElement.click();
43
+
44
+ // Wait a bit for the console log to appear
45
+ await page.waitForTimeout(500);
46
+
47
+ // Verify that the click event was logged (may have newline)
48
+ const hasClickedMessage = consoleMessages.some((msg) =>
49
+ msg.includes("clicked")
50
+ );
51
+ expect(hasClickedMessage).toBe(true);
52
+ });
53
+ });
@@ -0,0 +1,28 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+
8
+ <script>
9
+ const urlParams = new URLSearchParams(window.location.search);
10
+ const isDev = urlParams.get("env") === "DEV";
11
+ const script = document.createElement("script");
12
+ script.defer = true;
13
+ script.src = isDev
14
+ ? "../../../../packages/npm-packages/runtime/dist/ruwi.js"
15
+ : "https://unpkg.com/ruwi@latest";
16
+ document.head.appendChild(script);
17
+ </script>
18
+ <script defer type="text/ruby" src="index.rb"></script>
19
+ <script defer src="https://cdn.tailwindcss.com"></script>
20
+
21
+ <title>Input</title>
22
+ </head>
23
+
24
+ <body>
25
+ <h1>Input</h1>
26
+ <div id="app"></div>
27
+ </body>
28
+ </html>
@@ -0,0 +1,46 @@
1
+ require "js"
2
+
3
+ # input-form component using the latest component-based API
4
+ InputForm = Ruwi.define_component(
5
+ # Initialize component state
6
+ state: ->() {
7
+ {
8
+ url_name: '',
9
+ is_valid: false
10
+ }
11
+ },
12
+
13
+ # Render the input component
14
+ template: ->() {
15
+ Ruwi::Template::Parser.parse_and_eval(<<~HTML, binding)
16
+ <form class="w-full max-w-sm">
17
+ <label class="block mb-2 text-sm font-medium text-700">User Name</label>
18
+ <input
19
+ type="text"
20
+ value="{state[:url_name]}"
21
+ 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'}"
22
+ on="{input: ->(e) { update_url_name(e[:target][:value].to_s) }}" />
23
+ <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'}">
24
+ {state[:is_valid] ? "Valid" : "User name must be at least 4 characters"}
25
+ </p>
26
+ <p>*User name must be at least 4 characters</p>
27
+ </form>
28
+ HTML
29
+ },
30
+
31
+ # Component methods
32
+ methods: {
33
+ # Update the URL name and validation status
34
+ # @param value [String] The new URL name value
35
+ update_url_name: ->(value) {
36
+ update_state(
37
+ url_name: value,
38
+ is_valid: value.length >= 4
39
+ )
40
+ }
41
+ }
42
+ )
43
+
44
+ app = Ruwi::App.create(InputForm)
45
+ app_element = JS.global[:document].getElementById("app")
46
+ app.mount(app_element)