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.
- checksums.yaml +7 -0
- data/.cursor/rules/ruby_comments.mdc +29 -0
- data/.github/workflows/playwright.yml +74 -0
- data/.github/workflows/rspec.yml +31 -0
- data/.node-version +1 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/Makefile +56 -0
- data/README.md +237 -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/.gitignore +4 -0
- data/examples/Gemfile +5 -0
- data/examples/Gemfile.lock +39 -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/examples/src/index.html +21 -0
- data/examples/src/index.rb +26 -0
- data/examples/src/todos/index.html +23 -0
- data/examples/src/todos/index.rb +237 -0
- data/examples/src/todos/todos_repository.rb +23 -0
- data/exe/ruwi +6 -0
- data/lib/ruwi/cli/command/base.rb +192 -0
- data/lib/ruwi/cli/command/dev.rb +207 -0
- data/lib/ruwi/cli/command/pack.rb +36 -0
- data/lib/ruwi/cli/command/rebuild.rb +38 -0
- data/lib/ruwi/cli/command/setup.rb +159 -0
- data/lib/ruwi/cli/command.rb +48 -0
- data/lib/ruwi/runtime/app.rb +53 -0
- data/lib/ruwi/runtime/component.rb +215 -0
- data/lib/ruwi/runtime/dispatcher.rb +46 -0
- data/lib/ruwi/runtime/dom/attributes.rb +105 -0
- data/lib/ruwi/runtime/dom/destroy_dom.rb +63 -0
- data/lib/ruwi/runtime/dom/events.rb +40 -0
- data/lib/ruwi/runtime/dom/mount_dom.rb +108 -0
- data/lib/ruwi/runtime/dom/patch_dom.rb +237 -0
- data/lib/ruwi/runtime/dom/scheduler.rb +51 -0
- data/lib/ruwi/runtime/dom.rb +13 -0
- data/lib/ruwi/runtime/nodes_equal.rb +45 -0
- data/lib/ruwi/runtime/template/build_conditional_group.rb +150 -0
- data/lib/ruwi/runtime/template/build_for_group.rb +125 -0
- data/lib/ruwi/runtime/template/build_vdom.rb +220 -0
- data/lib/ruwi/runtime/template/parser.rb +134 -0
- data/lib/ruwi/runtime/template.rb +11 -0
- data/lib/ruwi/runtime/utils/arrays.rb +185 -0
- data/lib/ruwi/runtime/utils/objects.rb +37 -0
- data/lib/ruwi/runtime/utils/props.rb +25 -0
- data/lib/ruwi/runtime/utils/strings.rb +19 -0
- data/lib/ruwi/runtime/utils.rb +11 -0
- data/lib/ruwi/runtime/vdom.rb +84 -0
- data/lib/ruwi/version.rb +5 -0
- data/lib/ruwi.rb +14 -0
- data/package-lock.json +73 -0
- data/package.json +32 -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 +147 -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/ruwi +1 -0
- data/packages/npm-packages/runtime/src/ruwi.rb +1 -0
- data/packages/npm-packages/runtime/vitest.config.js +8 -0
- data/playwright.config.js +78 -0
- data/sig/ruwi.rbs +4 -0
- data/spec/ruwi/cli/command/base_spec.rb +503 -0
- data/spec/ruwi/cli/command/dev_spec.rb +442 -0
- data/spec/ruwi/cli/command/pack_spec.rb +131 -0
- data/spec/ruwi/cli/command/rebuild_spec.rb +95 -0
- data/spec/ruwi/cli/command/setup_spec.rb +251 -0
- data/spec/ruwi/cli/command_spec.rb +118 -0
- data/spec/ruwi/runtime/component_spec.rb +416 -0
- data/spec/ruwi/runtime/dom/scheduler_spec.rb +98 -0
- data/spec/ruwi/runtime/nodes_equal_spec.rb +190 -0
- data/spec/ruwi/runtime/template/build_conditional_group_spec.rb +505 -0
- data/spec/ruwi/runtime/template/build_for_group_spec.rb +377 -0
- data/spec/ruwi/runtime/template/build_vdom_spec.rb +573 -0
- data/spec/ruwi/runtime/template/parser_spec.rb +627 -0
- data/spec/ruwi/runtime/utils/arrays_spec.rb +228 -0
- data/spec/ruwi/runtime/utils/objects_spec.rb +127 -0
- data/spec/ruwi/runtime/utils/props_spec.rb +205 -0
- data/spec/ruwi/runtime/utils/strings_spec.rb +107 -0
- data/spec/spec_helper.rb +16 -0
- 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
|
data/examples/.gitignore
ADDED
data/examples/Gemfile
ADDED
|
@@ -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)
|