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,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/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>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 = Ruwi.define_component(
|
|
4
|
+
template: ->() {
|
|
5
|
+
Ruwi::Template::Parser.parse_and_eval(<<~HTML, binding)
|
|
6
|
+
<li>{props[:todo]}</li>
|
|
7
|
+
HTML
|
|
8
|
+
}
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
List = Ruwi.define_component(
|
|
12
|
+
template: ->() {
|
|
13
|
+
|
|
14
|
+
Ruwi::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 = Ruwi::App.create(List, { todos: ['foo', 'bar', 'baz'] })
|
|
32
|
+
app_element = JS.global[:document].getElementById("app")
|
|
33
|
+
app.mount(app_element)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { test, expect } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
test.describe("List Example", () => {
|
|
4
|
+
test("should display list with initial todos", async ({ page }) => {
|
|
5
|
+
await page.goto("/examples/npm-packages/runtime/list/index.html?env=DEV");
|
|
6
|
+
await page.waitForTimeout(3000);
|
|
7
|
+
|
|
8
|
+
// Check the page title
|
|
9
|
+
await expect(page).toHaveTitle("List");
|
|
10
|
+
|
|
11
|
+
// Check that the list is rendered
|
|
12
|
+
await expect(page.locator("ul")).toBeVisible();
|
|
13
|
+
|
|
14
|
+
// Check that all initial todos are displayed (both component and element versions)
|
|
15
|
+
const listItems = page.locator("li");
|
|
16
|
+
await expect(listItems).toHaveCount(6);
|
|
17
|
+
|
|
18
|
+
// Check the content of each list item (component version first, then element version)
|
|
19
|
+
await expect(listItems.nth(0)).toHaveText("foo");
|
|
20
|
+
await expect(listItems.nth(1)).toHaveText("bar");
|
|
21
|
+
await expect(listItems.nth(2)).toHaveText("baz");
|
|
22
|
+
await expect(listItems.nth(3)).toHaveText("foo");
|
|
23
|
+
await expect(listItems.nth(4)).toHaveText("bar");
|
|
24
|
+
await expect(listItems.nth(5)).toHaveText("baz");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("should render r-for with components correctly", async ({ page }) => {
|
|
28
|
+
await page.goto("/examples/npm-packages/runtime/list/index.html?env=DEV");
|
|
29
|
+
await page.waitForTimeout(3000);
|
|
30
|
+
|
|
31
|
+
// Check that ListItem components are rendered (first 3 items)
|
|
32
|
+
const componentItems = page.locator("li").nth(0);
|
|
33
|
+
await expect(componentItems).toBeVisible();
|
|
34
|
+
await expect(componentItems).toHaveText("foo");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("should render r-for with elements correctly", async ({ page }) => {
|
|
38
|
+
await page.goto("/examples/npm-packages/runtime/list/index.html?env=DEV");
|
|
39
|
+
await page.waitForTimeout(3000);
|
|
40
|
+
|
|
41
|
+
// Check that direct li elements are rendered (last 3 items)
|
|
42
|
+
const elementItems = page.locator("li").nth(3);
|
|
43
|
+
await expect(elementItems).toBeVisible();
|
|
44
|
+
await expect(elementItems).toHaveText("foo");
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="ja">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>on_mounted Demo</title>
|
|
7
|
+
<style>
|
|
8
|
+
body {
|
|
9
|
+
font-family: Arial, sans-serif;
|
|
10
|
+
max-width: 800px;
|
|
11
|
+
margin: 0 auto;
|
|
12
|
+
padding: 20px;
|
|
13
|
+
}
|
|
14
|
+
div {
|
|
15
|
+
margin: 20px 0;
|
|
16
|
+
padding: 15px;
|
|
17
|
+
border: 1px solid #ddd;
|
|
18
|
+
border-radius: 5px;
|
|
19
|
+
}
|
|
20
|
+
h1 {
|
|
21
|
+
color: #333;
|
|
22
|
+
text-align: center;
|
|
23
|
+
}
|
|
24
|
+
h2 {
|
|
25
|
+
color: #666;
|
|
26
|
+
}
|
|
27
|
+
p {
|
|
28
|
+
color: #777;
|
|
29
|
+
}
|
|
30
|
+
</style>
|
|
31
|
+
</head>
|
|
32
|
+
<body>
|
|
33
|
+
<div id="app"></div>
|
|
34
|
+
<script
|
|
35
|
+
type="module"
|
|
36
|
+
src="../../../../packages/npm-packages/runtime/dist/ruwi.js"
|
|
37
|
+
></script>
|
|
38
|
+
<script type="text/ruby" src="./index.rb"></script>
|
|
39
|
+
</body>
|
|
40
|
+
</html>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
require "js"
|
|
2
|
+
|
|
3
|
+
# Component with argument-less on_mounted that can call component methods directly
|
|
4
|
+
SimpleComponent = Ruwi.define_component(
|
|
5
|
+
state: -> { { message: "Not mounted yet" } },
|
|
6
|
+
|
|
7
|
+
# on_mounted without arguments - can call update_state directly!
|
|
8
|
+
on_mounted: -> {
|
|
9
|
+
puts "SimpleComponent mounted without arguments!"
|
|
10
|
+
update_state(message: "Mounted and state updated without component argument!")
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
template: ->() {
|
|
14
|
+
Ruwi::Template::Parser.parse_and_eval(<<~HTML, binding)
|
|
15
|
+
<div>
|
|
16
|
+
<h2>Simple Component (no args in on_mounted)</h2>
|
|
17
|
+
<p>{state[:message]}</p>
|
|
18
|
+
</div>
|
|
19
|
+
HTML
|
|
20
|
+
}
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Component with on_mounted that takes component argument (existing behavior)
|
|
24
|
+
AdvancedComponent = Ruwi.define_component(
|
|
25
|
+
state: -> { { message: "Not mounted yet" } },
|
|
26
|
+
|
|
27
|
+
# on_mounted with component argument - existing behavior still works
|
|
28
|
+
on_mounted: ->() {
|
|
29
|
+
puts "AdvancedComponent mounted with component argument!"
|
|
30
|
+
update_state(message: "Mounted and state updated!")
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
template: ->() {
|
|
34
|
+
Ruwi::Template::Parser.parse_and_eval(<<~HTML, binding)
|
|
35
|
+
<div>
|
|
36
|
+
<h2>Advanced Component (with args in on_mounted)</h2>
|
|
37
|
+
<p>{state[:message]}</p>
|
|
38
|
+
</div>
|
|
39
|
+
HTML
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Main App component
|
|
44
|
+
AppComponent = Ruwi.define_component(
|
|
45
|
+
template: -> {
|
|
46
|
+
Ruwi::Template::Parser.parse_and_eval(<<~HTML, binding)
|
|
47
|
+
<div>
|
|
48
|
+
<h1>on_mounted Demo</h1>
|
|
49
|
+
<SimpleComponent />
|
|
50
|
+
<AdvancedComponent />
|
|
51
|
+
</div>
|
|
52
|
+
HTML
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Mount the app
|
|
57
|
+
app = Ruwi::App.create(AppComponent)
|
|
58
|
+
app_element = JS.global[:document].getElementById("app")
|
|
59
|
+
app.mount(app_element)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { test, expect } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
test.describe("On Mounted Demo Example", () => {
|
|
4
|
+
test("should display components and verify on_mounted hooks execute correctly", async ({
|
|
5
|
+
page,
|
|
6
|
+
}) => {
|
|
7
|
+
await page.goto("/examples/npm-packages/runtime/on_mounted_demo/index.html?env=DEV");
|
|
8
|
+
await page.waitForTimeout(3000);
|
|
9
|
+
|
|
10
|
+
// Check the page title
|
|
11
|
+
await expect(page).toHaveTitle("on_mounted Demo");
|
|
12
|
+
|
|
13
|
+
// Check main heading
|
|
14
|
+
await expect(page.locator("h1")).toHaveText("on_mounted Demo");
|
|
15
|
+
|
|
16
|
+
// Check both component headings are present
|
|
17
|
+
await expect(page.locator("h2").first()).toHaveText(
|
|
18
|
+
"Simple Component (no args in on_mounted)"
|
|
19
|
+
);
|
|
20
|
+
await expect(page.locator("h2").last()).toHaveText(
|
|
21
|
+
"Advanced Component (with args in on_mounted)"
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
// Verify on_mounted hooks have executed and updated the messages
|
|
25
|
+
const simpleComponentMessage = page
|
|
26
|
+
.locator("h2")
|
|
27
|
+
.first()
|
|
28
|
+
.locator("..")
|
|
29
|
+
.locator("p");
|
|
30
|
+
await expect(simpleComponentMessage).toHaveText(
|
|
31
|
+
"Mounted and state updated without component argument!"
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const advancedComponentMessage = page
|
|
35
|
+
.locator("h2")
|
|
36
|
+
.last()
|
|
37
|
+
.locator("..")
|
|
38
|
+
.locator("p");
|
|
39
|
+
await expect(advancedComponentMessage).toHaveText(
|
|
40
|
+
"Mounted and state updated!"
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Neither component should show the initial "Not mounted yet" message
|
|
44
|
+
const paragraphs = page.locator("p");
|
|
45
|
+
for (let i = 0; i < (await paragraphs.count()); i++) {
|
|
46
|
+
const text = await paragraphs.nth(i).textContent();
|
|
47
|
+
expect(text).not.toBe("Not mounted yet");
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="ja">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>r-if Attribute Demo</title>
|
|
7
|
+
<script>
|
|
8
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
9
|
+
const isDev = urlParams.get("env") === "DEV";
|
|
10
|
+
const script = document.createElement("script");
|
|
11
|
+
script.defer = true;
|
|
12
|
+
script.src = isDev
|
|
13
|
+
? "../../../../packages/npm-packages/runtime/dist/ruwi.js"
|
|
14
|
+
: "https://unpkg.com/ruwi@latest";
|
|
15
|
+
document.head.appendChild(script);
|
|
16
|
+
</script>
|
|
17
|
+
<script defer type="text/ruby" src="index.rb"></script>
|
|
18
|
+
<style>
|
|
19
|
+
body {
|
|
20
|
+
font-family: Arial, sans-serif;
|
|
21
|
+
max-width: 600px;
|
|
22
|
+
margin: 20px auto;
|
|
23
|
+
padding: 20px;
|
|
24
|
+
background-color: #f5f5f5;
|
|
25
|
+
}
|
|
26
|
+
</style>
|
|
27
|
+
</head>
|
|
28
|
+
<body>
|
|
29
|
+
<h1>r-if Attribute Demo</h1>
|
|
30
|
+
<div id="app">
|
|
31
|
+
<!-- Ruby WASM UI app will mount here -->
|
|
32
|
+
</div>
|
|
33
|
+
</body>
|
|
34
|
+
</html>
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
require "js"
|
|
2
|
+
|
|
3
|
+
# R-If Attribute Demo component to demonstrate conditional rendering using r-if attribute
|
|
4
|
+
RIfAttributeDemo = Ruwi.define_component(
|
|
5
|
+
# Initialize component state
|
|
6
|
+
state: ->() {
|
|
7
|
+
{
|
|
8
|
+
show_message: false,
|
|
9
|
+
counter: 0
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
# Render the component with r-if attribute examples
|
|
14
|
+
template: ->() {
|
|
15
|
+
Ruwi::Template::Parser.parse_and_eval(<<~HTML, binding)
|
|
16
|
+
<div>
|
|
17
|
+
<p>Using r-if as an attribute (like Vue.js r-if)</p>
|
|
18
|
+
|
|
19
|
+
<!-- Example 1: Simple boolean toggle -->
|
|
20
|
+
<div style="border: 1px solid #ccc; margin: 10px 0; padding: 10px;">
|
|
21
|
+
<h2>Toggle Message</h2>
|
|
22
|
+
<button
|
|
23
|
+
style="background: #007bff; color: white; padding: 8px 16px; border: none; cursor: pointer;"
|
|
24
|
+
on="{click: ->() { toggle_message }}"
|
|
25
|
+
>
|
|
26
|
+
{state[:show_message] ? "Hide" : "Show"} Message
|
|
27
|
+
</button>
|
|
28
|
+
|
|
29
|
+
<div
|
|
30
|
+
style="background: #d4edda; border: 1px solid #c3e6cb; color: #155724; padding: 10px; margin-top: 10px;"
|
|
31
|
+
r-if="{state[:show_message]}"
|
|
32
|
+
>
|
|
33
|
+
<p>This message is conditionally rendered using r-if attribute!</p>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<!-- Example 2: Counter-based condition -->
|
|
38
|
+
<div style="border: 1px solid #ccc; margin: 10px 0; padding: 10px;">
|
|
39
|
+
<h2>Counter Conditions</h2>
|
|
40
|
+
<button
|
|
41
|
+
style="background: #28a745; color: white; padding: 8px 16px; border: none; cursor: pointer; margin-right: 5px;"
|
|
42
|
+
on="{click: ->() { increment_counter }}"
|
|
43
|
+
>
|
|
44
|
+
+1
|
|
45
|
+
</button>
|
|
46
|
+
<button
|
|
47
|
+
style="background: #dc3545; color: white; padding: 8px 16px; border: none; cursor: pointer; margin-right: 5px;"
|
|
48
|
+
on="{click: ->() { decrement_counter }}"
|
|
49
|
+
>
|
|
50
|
+
-1
|
|
51
|
+
</button>
|
|
52
|
+
<button
|
|
53
|
+
style="background: #6c757d; color: white; padding: 8px 16px; border: none; cursor: pointer;"
|
|
54
|
+
on="{click: ->() { reset_counter }}"
|
|
55
|
+
>
|
|
56
|
+
Reset
|
|
57
|
+
</button>
|
|
58
|
+
|
|
59
|
+
<p>Counter: <strong>{state[:counter]}</strong></p>
|
|
60
|
+
|
|
61
|
+
<div
|
|
62
|
+
style="background: #d4edda; border: 1px solid #c3e6cb; color: #155724; padding: 10px; margin: 5px 0;"
|
|
63
|
+
r-if="{state[:counter] > 0}"
|
|
64
|
+
>
|
|
65
|
+
<p>Counter is positive! ({state[:counter]})</p>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<div
|
|
69
|
+
style="background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; padding: 10px; margin: 5px 0;"
|
|
70
|
+
r-if="{state[:counter] < 0}"
|
|
71
|
+
>
|
|
72
|
+
<p>Counter is negative! ({state[:counter]})</p>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div
|
|
76
|
+
style="background: #e2e3e5; border: 1px solid #d6d8db; color: #383d41; padding: 10px; margin: 5px 0;"
|
|
77
|
+
r-if="{state[:counter] == 0}"
|
|
78
|
+
>
|
|
79
|
+
<p>Counter is zero.</p>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
HTML
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
# Component methods
|
|
87
|
+
methods: {
|
|
88
|
+
# Toggle the message visibility
|
|
89
|
+
toggle_message: ->() {
|
|
90
|
+
update_state(show_message: !state[:show_message])
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
# Increment the counter
|
|
94
|
+
increment_counter: ->() {
|
|
95
|
+
update_state(counter: state[:counter] + 1)
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
# Decrement the counter
|
|
99
|
+
decrement_counter: ->() {
|
|
100
|
+
update_state(counter: state[:counter] - 1)
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
# Reset the counter to zero
|
|
104
|
+
reset_counter: ->() {
|
|
105
|
+
update_state(counter: 0)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Create and mount the app
|
|
111
|
+
app = Ruwi::App.create(RIfAttributeDemo)
|
|
112
|
+
app_element = JS.global[:document].getElementById("app")
|
|
113
|
+
app.mount(app_element)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { test, expect } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
test.describe("R-If Attribute Demo Example", () => {
|
|
4
|
+
test("should display page layout and toggle message functionality", async ({
|
|
5
|
+
page,
|
|
6
|
+
}) => {
|
|
7
|
+
await page.goto("/examples/npm-packages/runtime/r_if_attribute_demo/index.html?env=DEV");
|
|
8
|
+
await page.waitForTimeout(3000);
|
|
9
|
+
|
|
10
|
+
// Check the page title
|
|
11
|
+
await expect(page).toHaveTitle("r-if Attribute Demo");
|
|
12
|
+
|
|
13
|
+
// Check main heading
|
|
14
|
+
await expect(page.locator("h1")).toHaveText("r-if Attribute Demo");
|
|
15
|
+
|
|
16
|
+
// Check description
|
|
17
|
+
await expect(page.locator("p").first()).toHaveText(
|
|
18
|
+
"Using r-if as an attribute (like Vue.js r-if)"
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
// Check section headings
|
|
22
|
+
await expect(page.locator("h2").first()).toHaveText("Toggle Message");
|
|
23
|
+
await expect(page.locator("h2").last()).toHaveText("Counter Conditions");
|
|
24
|
+
|
|
25
|
+
// Test message toggle functionality
|
|
26
|
+
const toggleButton = page.locator("button").first();
|
|
27
|
+
const messageDiv = page
|
|
28
|
+
.getByText("This message is conditionally rendered using r-if attribute!")
|
|
29
|
+
.locator("..");
|
|
30
|
+
|
|
31
|
+
// Initially message should be hidden (show_message: false)
|
|
32
|
+
await expect(toggleButton).toHaveText("Show Message");
|
|
33
|
+
await expect(messageDiv).not.toBeVisible();
|
|
34
|
+
|
|
35
|
+
// Click to show message
|
|
36
|
+
await toggleButton.click();
|
|
37
|
+
await page.waitForTimeout(100);
|
|
38
|
+
|
|
39
|
+
await expect(toggleButton).toHaveText("Hide Message");
|
|
40
|
+
await expect(messageDiv).toBeVisible();
|
|
41
|
+
await expect(messageDiv.locator("p")).toHaveText(
|
|
42
|
+
"This message is conditionally rendered using r-if attribute!"
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// Click to hide message again
|
|
46
|
+
await toggleButton.click();
|
|
47
|
+
await page.waitForTimeout(100);
|
|
48
|
+
|
|
49
|
+
await expect(toggleButton).toHaveText("Show Message");
|
|
50
|
+
await expect(messageDiv).not.toBeVisible();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("should handle counter operations and conditional message display", async ({
|
|
54
|
+
page,
|
|
55
|
+
}) => {
|
|
56
|
+
await page.goto("/examples/npm-packages/runtime/r_if_attribute_demo/index.html?env=DEV");
|
|
57
|
+
await page.waitForTimeout(3000);
|
|
58
|
+
|
|
59
|
+
// Check counter buttons
|
|
60
|
+
const incrementButton = page.locator("button").nth(1); // +1 button
|
|
61
|
+
const decrementButton = page.locator("button").nth(2); // -1 button
|
|
62
|
+
const resetButton = page.locator("button").nth(3); // Reset button
|
|
63
|
+
const counterDisplay = page.locator("p").filter({ hasText: "Counter:" });
|
|
64
|
+
|
|
65
|
+
await expect(incrementButton).toHaveText("+1");
|
|
66
|
+
await expect(decrementButton).toHaveText("-1");
|
|
67
|
+
await expect(resetButton).toHaveText("Reset");
|
|
68
|
+
|
|
69
|
+
// Check initial counter display and zero message
|
|
70
|
+
await expect(counterDisplay).toContainText("Counter:0");
|
|
71
|
+
|
|
72
|
+
const positiveMessage = page
|
|
73
|
+
.getByText("Counter is positive!")
|
|
74
|
+
.locator("..");
|
|
75
|
+
const negativeMessage = page
|
|
76
|
+
.getByText("Counter is negative!")
|
|
77
|
+
.locator("..");
|
|
78
|
+
const zeroMessage = page.getByText("Counter is zero.").locator("..");
|
|
79
|
+
|
|
80
|
+
await expect(zeroMessage).toBeVisible();
|
|
81
|
+
await expect(positiveMessage).not.toBeVisible();
|
|
82
|
+
await expect(negativeMessage).not.toBeVisible();
|
|
83
|
+
|
|
84
|
+
// Test increment operations and positive message
|
|
85
|
+
await incrementButton.click();
|
|
86
|
+
await page.waitForTimeout(100);
|
|
87
|
+
await expect(counterDisplay).toContainText("Counter:1");
|
|
88
|
+
await expect(positiveMessage).toBeVisible();
|
|
89
|
+
await expect(positiveMessage.locator("p")).toHaveText(
|
|
90
|
+
"Counter is positive! (1)"
|
|
91
|
+
);
|
|
92
|
+
await expect(zeroMessage).not.toBeVisible();
|
|
93
|
+
await expect(negativeMessage).not.toBeVisible();
|
|
94
|
+
|
|
95
|
+
// Test more increments
|
|
96
|
+
await incrementButton.click();
|
|
97
|
+
await incrementButton.click();
|
|
98
|
+
await page.waitForTimeout(100);
|
|
99
|
+
await expect(counterDisplay).toContainText("Counter:3");
|
|
100
|
+
await expect(positiveMessage.locator("p")).toHaveText(
|
|
101
|
+
"Counter is positive! (3)"
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// Test reset
|
|
105
|
+
await resetButton.click();
|
|
106
|
+
await page.waitForTimeout(100);
|
|
107
|
+
await expect(counterDisplay).toContainText("Counter:0");
|
|
108
|
+
await expect(zeroMessage).toBeVisible();
|
|
109
|
+
await expect(positiveMessage).not.toBeVisible();
|
|
110
|
+
await expect(negativeMessage).not.toBeVisible();
|
|
111
|
+
|
|
112
|
+
// Test decrement operations and negative message
|
|
113
|
+
await decrementButton.click();
|
|
114
|
+
await page.waitForTimeout(100);
|
|
115
|
+
await expect(counterDisplay).toContainText("Counter:-1");
|
|
116
|
+
await expect(negativeMessage).toBeVisible();
|
|
117
|
+
await expect(negativeMessage.locator("p")).toHaveText(
|
|
118
|
+
"Counter is negative! (-1)"
|
|
119
|
+
);
|
|
120
|
+
await expect(zeroMessage).not.toBeVisible();
|
|
121
|
+
await expect(positiveMessage).not.toBeVisible();
|
|
122
|
+
|
|
123
|
+
// Test more decrements
|
|
124
|
+
await decrementButton.click();
|
|
125
|
+
await decrementButton.click();
|
|
126
|
+
await page.waitForTimeout(100);
|
|
127
|
+
await expect(counterDisplay).toContainText("Counter:-3");
|
|
128
|
+
await expect(negativeMessage.locator("p")).toHaveText(
|
|
129
|
+
"Counter is negative! (-3)"
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// Verify styling for conditional messages
|
|
133
|
+
await expect(negativeMessage).toHaveAttribute("style");
|
|
134
|
+
|
|
135
|
+
// Reset and verify zero message styling
|
|
136
|
+
await resetButton.click();
|
|
137
|
+
await page.waitForTimeout(100);
|
|
138
|
+
await expect(zeroMessage).toHaveAttribute("style");
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -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/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>Random Cocktail</title>
|
|
21
|
+
</head>
|
|
22
|
+
|
|
23
|
+
<body>
|
|
24
|
+
<h1>Random Cocktail</h1>
|
|
25
|
+
<div id="app"></div>
|
|
26
|
+
</body>
|
|
27
|
+
</html>
|