ruby_wasm_ui 0.8.1

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 (99) 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 +33 -0
  5. data/.node-version +1 -0
  6. data/CODE_OF_CONDUCT.md +132 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +218 -0
  9. data/Rakefile +4 -0
  10. data/docs/conditional-rendering.md +119 -0
  11. data/docs/lifecycle-hooks.md +75 -0
  12. data/docs/list-rendering.md +51 -0
  13. data/examples/Gemfile +5 -0
  14. data/examples/Gemfile.lock +41 -0
  15. data/examples/Makefile +15 -0
  16. data/examples/npm-packages/runtime/counter/index.html +28 -0
  17. data/examples/npm-packages/runtime/counter/index.rb +62 -0
  18. data/examples/npm-packages/runtime/counter/index.spec.js +42 -0
  19. data/examples/npm-packages/runtime/hello/index.html +28 -0
  20. data/examples/npm-packages/runtime/hello/index.rb +29 -0
  21. data/examples/npm-packages/runtime/hello/index.spec.js +53 -0
  22. data/examples/npm-packages/runtime/input/index.html +28 -0
  23. data/examples/npm-packages/runtime/input/index.rb +46 -0
  24. data/examples/npm-packages/runtime/input/index.spec.js +58 -0
  25. data/examples/npm-packages/runtime/list/index.html +27 -0
  26. data/examples/npm-packages/runtime/list/index.rb +33 -0
  27. data/examples/npm-packages/runtime/list/index.spec.js +46 -0
  28. data/examples/npm-packages/runtime/on_mounted_demo/index.html +40 -0
  29. data/examples/npm-packages/runtime/on_mounted_demo/index.rb +59 -0
  30. data/examples/npm-packages/runtime/on_mounted_demo/index.spec.js +50 -0
  31. data/examples/npm-packages/runtime/r_if_attribute_demo/index.html +34 -0
  32. data/examples/npm-packages/runtime/r_if_attribute_demo/index.rb +113 -0
  33. data/examples/npm-packages/runtime/r_if_attribute_demo/index.spec.js +140 -0
  34. data/examples/npm-packages/runtime/random_cocktail/index.html +27 -0
  35. data/examples/npm-packages/runtime/random_cocktail/index.rb +69 -0
  36. data/examples/npm-packages/runtime/random_cocktail/index.spec.js +101 -0
  37. data/examples/npm-packages/runtime/search_field/index.html +27 -0
  38. data/examples/npm-packages/runtime/search_field/index.rb +39 -0
  39. data/examples/npm-packages/runtime/search_field/index.spec.js +59 -0
  40. data/examples/npm-packages/runtime/todos/index.html +28 -0
  41. data/examples/npm-packages/runtime/todos/index.rb +239 -0
  42. data/examples/npm-packages/runtime/todos/index.spec.js +161 -0
  43. data/examples/npm-packages/runtime/todos/todos_repository.rb +23 -0
  44. data/examples/package.json +12 -0
  45. data/examples/src/counter/index.html +23 -0
  46. data/examples/src/counter/index.rb +60 -0
  47. data/lib/ruby_wasm_ui +1 -0
  48. data/lib/ruby_wasm_ui.rb +1 -0
  49. data/package-lock.json +100 -0
  50. data/package.json +32 -0
  51. data/packages/npm-packages/runtime/Gemfile +3 -0
  52. data/packages/npm-packages/runtime/Gemfile.lock +26 -0
  53. data/packages/npm-packages/runtime/README.md +5 -0
  54. data/packages/npm-packages/runtime/eslint.config.mjs +16 -0
  55. data/packages/npm-packages/runtime/package-lock.json +6668 -0
  56. data/packages/npm-packages/runtime/package.json +38 -0
  57. data/packages/npm-packages/runtime/rollup.config.mjs +89 -0
  58. data/packages/npm-packages/runtime/spec/ruby_wasm_ui/component_spec.rb +416 -0
  59. data/packages/npm-packages/runtime/spec/ruby_wasm_ui/dom/scheduler_spec.rb +98 -0
  60. data/packages/npm-packages/runtime/spec/ruby_wasm_ui/nodes_equal_spec.rb +190 -0
  61. data/packages/npm-packages/runtime/spec/ruby_wasm_ui/template/build_conditional_group_spec.rb +505 -0
  62. data/packages/npm-packages/runtime/spec/ruby_wasm_ui/template/build_for_group_spec.rb +377 -0
  63. data/packages/npm-packages/runtime/spec/ruby_wasm_ui/template/build_vdom_spec.rb +573 -0
  64. data/packages/npm-packages/runtime/spec/ruby_wasm_ui/template/parser_spec.rb +627 -0
  65. data/packages/npm-packages/runtime/spec/ruby_wasm_ui/utils/arrays_spec.rb +228 -0
  66. data/packages/npm-packages/runtime/spec/ruby_wasm_ui/utils/objects_spec.rb +127 -0
  67. data/packages/npm-packages/runtime/spec/ruby_wasm_ui/utils/props_spec.rb +205 -0
  68. data/packages/npm-packages/runtime/spec/ruby_wasm_ui/utils/strings_spec.rb +107 -0
  69. data/packages/npm-packages/runtime/spec/spec_helper.rb +16 -0
  70. data/packages/npm-packages/runtime/src/__tests__/sample.test.js +5 -0
  71. data/packages/npm-packages/runtime/src/index.js +37 -0
  72. data/packages/npm-packages/runtime/src/ruby_wasm_ui/app.rb +53 -0
  73. data/packages/npm-packages/runtime/src/ruby_wasm_ui/component.rb +215 -0
  74. data/packages/npm-packages/runtime/src/ruby_wasm_ui/dispatcher.rb +46 -0
  75. data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/attributes.rb +105 -0
  76. data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/destroy_dom.rb +63 -0
  77. data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/events.rb +40 -0
  78. data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/mount_dom.rb +108 -0
  79. data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/patch_dom.rb +237 -0
  80. data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/scheduler.rb +51 -0
  81. data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom.rb +13 -0
  82. data/packages/npm-packages/runtime/src/ruby_wasm_ui/nodes_equal.rb +45 -0
  83. data/packages/npm-packages/runtime/src/ruby_wasm_ui/template/build_conditional_group.rb +150 -0
  84. data/packages/npm-packages/runtime/src/ruby_wasm_ui/template/build_for_group.rb +125 -0
  85. data/packages/npm-packages/runtime/src/ruby_wasm_ui/template/build_vdom.rb +220 -0
  86. data/packages/npm-packages/runtime/src/ruby_wasm_ui/template/parser.rb +134 -0
  87. data/packages/npm-packages/runtime/src/ruby_wasm_ui/template.rb +11 -0
  88. data/packages/npm-packages/runtime/src/ruby_wasm_ui/utils/arrays.rb +185 -0
  89. data/packages/npm-packages/runtime/src/ruby_wasm_ui/utils/objects.rb +37 -0
  90. data/packages/npm-packages/runtime/src/ruby_wasm_ui/utils/props.rb +25 -0
  91. data/packages/npm-packages/runtime/src/ruby_wasm_ui/utils/strings.rb +19 -0
  92. data/packages/npm-packages/runtime/src/ruby_wasm_ui/utils.rb +11 -0
  93. data/packages/npm-packages/runtime/src/ruby_wasm_ui/vdom.rb +84 -0
  94. data/packages/npm-packages/runtime/src/ruby_wasm_ui/version.rb +5 -0
  95. data/packages/npm-packages/runtime/src/ruby_wasm_ui.rb +14 -0
  96. data/packages/npm-packages/runtime/vitest.config.js +8 -0
  97. data/playwright.config.js +78 -0
  98. data/sig/ruby_wasm_ui.rbs +4 -0
  99. metadata +168 -0
@@ -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 = RubyWasmUi.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
+ RubyWasmUi::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 ruby-wasm-ui are designed to use Ruby's Fiber system rather than Promises.
@@ -0,0 +1,51 @@
1
+ # List Rendering with r-for
2
+
3
+ ruby-wasm-ui 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 = RubyWasmUi.define_component(
8
+ template: ->() {
9
+ RubyWasmUi::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 = RubyWasmUi.define_component(
17
+ template: ->() {
18
+ RubyWasmUi::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 = RubyWasmUi::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
data/examples/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "ruby_wasm_ui", path: "../../"
@@ -0,0 +1,41 @@
1
+ PATH
2
+ remote: ../..
3
+ specs:
4
+ ruby_wasm_ui (0.8.1)
5
+ js (~> 2.7)
6
+ ruby_wasm (~> 2.7)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ js (2.7.2)
12
+ logger (1.7.0)
13
+ ruby_wasm (2.7.2)
14
+ logger
15
+ ruby_wasm (2.7.2-aarch64-linux)
16
+ logger
17
+ ruby_wasm (2.7.2-aarch64-linux-musl)
18
+ logger
19
+ ruby_wasm (2.7.2-arm64-darwin)
20
+ logger
21
+ ruby_wasm (2.7.2-x86_64-darwin)
22
+ logger
23
+ ruby_wasm (2.7.2-x86_64-linux)
24
+ logger
25
+ ruby_wasm (2.7.2-x86_64-linux-musl)
26
+ logger
27
+
28
+ PLATFORMS
29
+ aarch64-linux
30
+ aarch64-linux-musl
31
+ arm64-darwin
32
+ ruby
33
+ x86_64-darwin
34
+ x86_64-linux
35
+ x86_64-linux-musl
36
+
37
+ DEPENDENCIES
38
+ ruby_wasm_ui!
39
+
40
+ BUNDLED WITH
41
+ 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/ruby-wasm-ui.js"
15
+ : "https://unpkg.com/ruby-wasm-ui@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 = RubyWasmUi.define_component(
5
+ # Initialize component state
6
+ state: ->(props) {
7
+ { count: props[:count] || 0 }
8
+ },
9
+
10
+ # Render the counter component
11
+ template: ->() {
12
+ RubyWasmUi::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 = RubyWasmUi.define_component(
44
+ template: ->() {
45
+ RubyWasmUi::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 = RubyWasmUi::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 = RubyWasmUi::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/ruby-wasm-ui.js"
15
+ : "https://unpkg.com/ruby-wasm-ui@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 = RubyWasmUi::Vdom.h("div", {}, [RubyWasmUi::Vdom.h("h1", {}, ["Hello, world!"])])
5
+ h1_a_element = JS.global[:document].getElementById("h1-a")
6
+ RubyWasmUi::Dom::Events.add_event_listener("click", ->(e) { puts "clicked" }, h1_a_element)
7
+ RubyWasmUi::Dom::Attributes.set_attributes(h1_a_element, {
8
+ class: "bg-red-500"
9
+ })
10
+ RubyWasmUi::Dom::Attributes.set_attribute(h1_a_element, "data-test", "test")
11
+ RubyWasmUi::Dom::Attributes.set_class(h1_a_element, "bg-blue-500")
12
+ RubyWasmUi::Dom::Attributes.set_style(h1_a_element, "background-color", "red")
13
+ RubyWasmUi::Dom::Attributes.remove_attribute(h1_a_element, "data-test")
14
+ RubyWasmUi::Dom::Attributes.remove_style(h1_a_element, "background-color")
15
+
16
+ RubyWasmUi::Dom::MountDom.execute(h_a, h1_a_element)
17
+ RubyWasmUi::Dom::DestroyDom.execute(h_a)
18
+
19
+ # h_b to be mounted
20
+ h_b = RubyWasmUi::Vdom.h("div", {}, [RubyWasmUi::Vdom.h("h1", {}, ["Hello, world!"])])
21
+ h1_b_element = JS.global[:document].getElementById("h1-b")
22
+ RubyWasmUi::Dom::Events.add_event_listener("click", ->(e) { puts "clicked" }, h1_b_element)
23
+ RubyWasmUi::Dom::Attributes.set_attributes(h1_b_element, {
24
+ class: "bg-red-500"
25
+ })
26
+ RubyWasmUi::Dom::Attributes.set_attribute(h1_b_element, "data-test", "test")
27
+ RubyWasmUi::Dom::Attributes.set_class(h1_b_element, "bg-red-500")
28
+ RubyWasmUi::Dom::Attributes.set_style(h1_b_element, "background-color", "blue")
29
+ RubyWasmUi::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/ruby-wasm-ui.js"
15
+ : "https://unpkg.com/ruby-wasm-ui@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 = RubyWasmUi.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
+ RubyWasmUi::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 = RubyWasmUi::App.create(InputForm)
45
+ app_element = JS.global[:document].getElementById("app")
46
+ app.mount(app_element)
@@ -0,0 +1,58 @@
1
+ import { test, expect } from "@playwright/test";
2
+
3
+ test.describe("Input Example", () => {
4
+ test("should display input form and validate input length", async ({
5
+ page,
6
+ }) => {
7
+ await page.goto("/examples/npm-packages/runtime/input/index.html?env=DEV");
8
+ await page.waitForTimeout(3000);
9
+
10
+ // Check the page title
11
+ await expect(page).toHaveTitle("Input");
12
+
13
+ // Check form elements are present
14
+ await expect(page.locator("label")).toHaveText("User Name");
15
+ const input = page.locator('input[type="text"]');
16
+ await expect(input).toBeVisible();
17
+
18
+ // Check help text
19
+ await expect(page.locator("p").last()).toHaveText(
20
+ "*User name must be at least 4 characters"
21
+ );
22
+
23
+ // Check initial validation message (invalid state)
24
+ const validationMessage = page.locator("p").first();
25
+ await expect(validationMessage).toHaveText(
26
+ "User name must be at least 4 characters"
27
+ );
28
+ await expect(validationMessage).toHaveClass(/text-red-600/);
29
+ await expect(input).toHaveClass(/border-red-500/);
30
+
31
+ // Type less than 4 characters - should still be invalid
32
+ await input.fill("abc");
33
+ await page.waitForTimeout(100);
34
+ await expect(validationMessage).toHaveText(
35
+ "User name must be at least 4 characters"
36
+ );
37
+ await expect(validationMessage).toHaveClass(/text-red-600/);
38
+ await expect(input).toHaveClass(/border-red-500/);
39
+
40
+ // Type 4 or more characters - should become valid
41
+ await input.fill("abcd");
42
+ await page.waitForTimeout(100);
43
+ await expect(validationMessage).toHaveText("Valid");
44
+ await expect(validationMessage).toHaveClass(/text-green-600/);
45
+ await expect(input).toHaveClass(/border-green-500/);
46
+
47
+ // Clear input to make it invalid again
48
+ await input.fill("ab");
49
+ await page.waitForTimeout(100);
50
+
51
+ // Should be invalid again
52
+ await expect(input).toHaveValue("ab");
53
+ await expect(validationMessage).toHaveText(
54
+ "User name must be at least 4 characters"
55
+ );
56
+ await expect(validationMessage).toHaveClass(/text-red-600/);
57
+ });
58
+ });
@@ -0,0 +1,27 @@
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/ruby-wasm-ui.js"
15
+ : "https://unpkg.com/ruby-wasm-ui@latest";
16
+ document.head.appendChild(script);
17
+ </script>
18
+ <script defer type="text/ruby" src="index.rb"></script>
19
+
20
+ <title>List</title>
21
+ </head>
22
+
23
+ <body>
24
+ <h1>List</h1>
25
+ <div id="app"></div>
26
+ </body>
27
+ </html>
@@ -0,0 +1,33 @@
1
+ require "js"
2
+
3
+ ListItem = RubyWasmUi.define_component(
4
+ template: ->() {
5
+ RubyWasmUi::Template::Parser.parse_and_eval(<<~HTML, binding)
6
+ <li>{props[:todo]}</li>
7
+ HTML
8
+ }
9
+ )
10
+
11
+ List = RubyWasmUi.define_component(
12
+ template: ->() {
13
+
14
+ RubyWasmUi::Template::Parser.parse_and_eval(<<~HTML, binding)
15
+ <ul>
16
+ <!-- component -->
17
+ <ListItem
18
+ r-for="{todo in props[:todos]}"
19
+ todo="{todo}"
20
+ />
21
+ <!-- element -->
22
+ <li r-for="todo in props[:todos]">
23
+ { todo }
24
+ </li>
25
+ </ul>
26
+ HTML
27
+ }
28
+ )
29
+
30
+ # Create and mount the app
31
+ app = RubyWasmUi::App.create(List, { todos: ['foo', 'bar', 'baz'] })
32
+ app_element = JS.global[:document].getElementById("app")
33
+ app.mount(app_element)