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.
- checksums.yaml +7 -0
- data/.cursor/rules/ruby_comments.mdc +29 -0
- data/.github/workflows/playwright.yml +74 -0
- data/.github/workflows/rspec.yml +33 -0
- data/.node-version +1 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +218 -0
- data/Rakefile +4 -0
- data/docs/conditional-rendering.md +119 -0
- data/docs/lifecycle-hooks.md +75 -0
- data/docs/list-rendering.md +51 -0
- data/examples/Gemfile +5 -0
- data/examples/Gemfile.lock +41 -0
- data/examples/Makefile +15 -0
- data/examples/npm-packages/runtime/counter/index.html +28 -0
- data/examples/npm-packages/runtime/counter/index.rb +62 -0
- data/examples/npm-packages/runtime/counter/index.spec.js +42 -0
- data/examples/npm-packages/runtime/hello/index.html +28 -0
- data/examples/npm-packages/runtime/hello/index.rb +29 -0
- data/examples/npm-packages/runtime/hello/index.spec.js +53 -0
- data/examples/npm-packages/runtime/input/index.html +28 -0
- data/examples/npm-packages/runtime/input/index.rb +46 -0
- data/examples/npm-packages/runtime/input/index.spec.js +58 -0
- data/examples/npm-packages/runtime/list/index.html +27 -0
- data/examples/npm-packages/runtime/list/index.rb +33 -0
- data/examples/npm-packages/runtime/list/index.spec.js +46 -0
- data/examples/npm-packages/runtime/on_mounted_demo/index.html +40 -0
- data/examples/npm-packages/runtime/on_mounted_demo/index.rb +59 -0
- data/examples/npm-packages/runtime/on_mounted_demo/index.spec.js +50 -0
- data/examples/npm-packages/runtime/r_if_attribute_demo/index.html +34 -0
- data/examples/npm-packages/runtime/r_if_attribute_demo/index.rb +113 -0
- data/examples/npm-packages/runtime/r_if_attribute_demo/index.spec.js +140 -0
- data/examples/npm-packages/runtime/random_cocktail/index.html +27 -0
- data/examples/npm-packages/runtime/random_cocktail/index.rb +69 -0
- data/examples/npm-packages/runtime/random_cocktail/index.spec.js +101 -0
- data/examples/npm-packages/runtime/search_field/index.html +27 -0
- data/examples/npm-packages/runtime/search_field/index.rb +39 -0
- data/examples/npm-packages/runtime/search_field/index.spec.js +59 -0
- data/examples/npm-packages/runtime/todos/index.html +28 -0
- data/examples/npm-packages/runtime/todos/index.rb +239 -0
- data/examples/npm-packages/runtime/todos/index.spec.js +161 -0
- data/examples/npm-packages/runtime/todos/todos_repository.rb +23 -0
- data/examples/package.json +12 -0
- data/examples/src/counter/index.html +23 -0
- data/examples/src/counter/index.rb +60 -0
- data/lib/ruby_wasm_ui +1 -0
- data/lib/ruby_wasm_ui.rb +1 -0
- data/package-lock.json +100 -0
- data/package.json +32 -0
- data/packages/npm-packages/runtime/Gemfile +3 -0
- data/packages/npm-packages/runtime/Gemfile.lock +26 -0
- data/packages/npm-packages/runtime/README.md +5 -0
- data/packages/npm-packages/runtime/eslint.config.mjs +16 -0
- data/packages/npm-packages/runtime/package-lock.json +6668 -0
- data/packages/npm-packages/runtime/package.json +38 -0
- data/packages/npm-packages/runtime/rollup.config.mjs +89 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/component_spec.rb +416 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/dom/scheduler_spec.rb +98 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/nodes_equal_spec.rb +190 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/template/build_conditional_group_spec.rb +505 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/template/build_for_group_spec.rb +377 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/template/build_vdom_spec.rb +573 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/template/parser_spec.rb +627 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/utils/arrays_spec.rb +228 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/utils/objects_spec.rb +127 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/utils/props_spec.rb +205 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/utils/strings_spec.rb +107 -0
- data/packages/npm-packages/runtime/spec/spec_helper.rb +16 -0
- data/packages/npm-packages/runtime/src/__tests__/sample.test.js +5 -0
- data/packages/npm-packages/runtime/src/index.js +37 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/app.rb +53 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/component.rb +215 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/dispatcher.rb +46 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/attributes.rb +105 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/destroy_dom.rb +63 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/events.rb +40 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/mount_dom.rb +108 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/patch_dom.rb +237 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/scheduler.rb +51 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom.rb +13 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/nodes_equal.rb +45 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/template/build_conditional_group.rb +150 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/template/build_for_group.rb +125 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/template/build_vdom.rb +220 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/template/parser.rb +134 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/template.rb +11 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/utils/arrays.rb +185 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/utils/objects.rb +37 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/utils/props.rb +25 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/utils/strings.rb +19 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/utils.rb +11 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/vdom.rb +84 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/version.rb +5 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui.rb +14 -0
- data/packages/npm-packages/runtime/vitest.config.js +8 -0
- data/playwright.config.js +78 -0
- data/sig/ruby_wasm_ui.rbs +4 -0
- 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,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)
|