ruwi 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. checksums.yaml +7 -0
  2. data/.cursor/rules/ruby_comments.mdc +29 -0
  3. data/.github/workflows/playwright.yml +74 -0
  4. data/.github/workflows/rspec.yml +31 -0
  5. data/.node-version +1 -0
  6. data/CODE_OF_CONDUCT.md +132 -0
  7. data/LICENSE.txt +21 -0
  8. data/Makefile +56 -0
  9. data/README.md +237 -0
  10. data/Rakefile +4 -0
  11. data/docs/conditional-rendering.md +119 -0
  12. data/docs/lifecycle-hooks.md +75 -0
  13. data/docs/list-rendering.md +51 -0
  14. data/examples/.gitignore +4 -0
  15. data/examples/Gemfile +5 -0
  16. data/examples/Gemfile.lock +39 -0
  17. data/examples/Makefile +15 -0
  18. data/examples/npm-packages/runtime/counter/index.html +28 -0
  19. data/examples/npm-packages/runtime/counter/index.rb +62 -0
  20. data/examples/npm-packages/runtime/counter/index.spec.js +42 -0
  21. data/examples/npm-packages/runtime/hello/index.html +28 -0
  22. data/examples/npm-packages/runtime/hello/index.rb +29 -0
  23. data/examples/npm-packages/runtime/hello/index.spec.js +53 -0
  24. data/examples/npm-packages/runtime/input/index.html +28 -0
  25. data/examples/npm-packages/runtime/input/index.rb +46 -0
  26. data/examples/npm-packages/runtime/input/index.spec.js +58 -0
  27. data/examples/npm-packages/runtime/list/index.html +27 -0
  28. data/examples/npm-packages/runtime/list/index.rb +33 -0
  29. data/examples/npm-packages/runtime/list/index.spec.js +46 -0
  30. data/examples/npm-packages/runtime/on_mounted_demo/index.html +40 -0
  31. data/examples/npm-packages/runtime/on_mounted_demo/index.rb +59 -0
  32. data/examples/npm-packages/runtime/on_mounted_demo/index.spec.js +50 -0
  33. data/examples/npm-packages/runtime/r_if_attribute_demo/index.html +34 -0
  34. data/examples/npm-packages/runtime/r_if_attribute_demo/index.rb +113 -0
  35. data/examples/npm-packages/runtime/r_if_attribute_demo/index.spec.js +140 -0
  36. data/examples/npm-packages/runtime/random_cocktail/index.html +27 -0
  37. data/examples/npm-packages/runtime/random_cocktail/index.rb +69 -0
  38. data/examples/npm-packages/runtime/random_cocktail/index.spec.js +101 -0
  39. data/examples/npm-packages/runtime/search_field/index.html +27 -0
  40. data/examples/npm-packages/runtime/search_field/index.rb +39 -0
  41. data/examples/npm-packages/runtime/search_field/index.spec.js +59 -0
  42. data/examples/npm-packages/runtime/todos/index.html +28 -0
  43. data/examples/npm-packages/runtime/todos/index.rb +239 -0
  44. data/examples/npm-packages/runtime/todos/index.spec.js +161 -0
  45. data/examples/npm-packages/runtime/todos/todos_repository.rb +23 -0
  46. data/examples/package.json +12 -0
  47. data/examples/src/counter/index.html +23 -0
  48. data/examples/src/counter/index.rb +60 -0
  49. data/examples/src/index.html +21 -0
  50. data/examples/src/index.rb +26 -0
  51. data/examples/src/todos/index.html +23 -0
  52. data/examples/src/todos/index.rb +237 -0
  53. data/examples/src/todos/todos_repository.rb +23 -0
  54. data/exe/ruwi +6 -0
  55. data/lib/ruwi/cli/command/base.rb +192 -0
  56. data/lib/ruwi/cli/command/dev.rb +207 -0
  57. data/lib/ruwi/cli/command/pack.rb +36 -0
  58. data/lib/ruwi/cli/command/rebuild.rb +38 -0
  59. data/lib/ruwi/cli/command/setup.rb +159 -0
  60. data/lib/ruwi/cli/command.rb +48 -0
  61. data/lib/ruwi/runtime/app.rb +53 -0
  62. data/lib/ruwi/runtime/component.rb +215 -0
  63. data/lib/ruwi/runtime/dispatcher.rb +46 -0
  64. data/lib/ruwi/runtime/dom/attributes.rb +105 -0
  65. data/lib/ruwi/runtime/dom/destroy_dom.rb +63 -0
  66. data/lib/ruwi/runtime/dom/events.rb +40 -0
  67. data/lib/ruwi/runtime/dom/mount_dom.rb +108 -0
  68. data/lib/ruwi/runtime/dom/patch_dom.rb +237 -0
  69. data/lib/ruwi/runtime/dom/scheduler.rb +51 -0
  70. data/lib/ruwi/runtime/dom.rb +13 -0
  71. data/lib/ruwi/runtime/nodes_equal.rb +45 -0
  72. data/lib/ruwi/runtime/template/build_conditional_group.rb +150 -0
  73. data/lib/ruwi/runtime/template/build_for_group.rb +125 -0
  74. data/lib/ruwi/runtime/template/build_vdom.rb +220 -0
  75. data/lib/ruwi/runtime/template/parser.rb +134 -0
  76. data/lib/ruwi/runtime/template.rb +11 -0
  77. data/lib/ruwi/runtime/utils/arrays.rb +185 -0
  78. data/lib/ruwi/runtime/utils/objects.rb +37 -0
  79. data/lib/ruwi/runtime/utils/props.rb +25 -0
  80. data/lib/ruwi/runtime/utils/strings.rb +19 -0
  81. data/lib/ruwi/runtime/utils.rb +11 -0
  82. data/lib/ruwi/runtime/vdom.rb +84 -0
  83. data/lib/ruwi/version.rb +5 -0
  84. data/lib/ruwi.rb +14 -0
  85. data/package-lock.json +73 -0
  86. data/package.json +32 -0
  87. data/packages/npm-packages/runtime/README.md +5 -0
  88. data/packages/npm-packages/runtime/eslint.config.mjs +16 -0
  89. data/packages/npm-packages/runtime/package-lock.json +6668 -0
  90. data/packages/npm-packages/runtime/package.json +38 -0
  91. data/packages/npm-packages/runtime/rollup.config.mjs +147 -0
  92. data/packages/npm-packages/runtime/src/__tests__/sample.test.js +5 -0
  93. data/packages/npm-packages/runtime/src/index.js +37 -0
  94. data/packages/npm-packages/runtime/src/ruwi +1 -0
  95. data/packages/npm-packages/runtime/src/ruwi.rb +1 -0
  96. data/packages/npm-packages/runtime/vitest.config.js +8 -0
  97. data/playwright.config.js +78 -0
  98. data/sig/ruwi.rbs +4 -0
  99. data/spec/ruwi/cli/command/base_spec.rb +503 -0
  100. data/spec/ruwi/cli/command/dev_spec.rb +442 -0
  101. data/spec/ruwi/cli/command/pack_spec.rb +131 -0
  102. data/spec/ruwi/cli/command/rebuild_spec.rb +95 -0
  103. data/spec/ruwi/cli/command/setup_spec.rb +251 -0
  104. data/spec/ruwi/cli/command_spec.rb +118 -0
  105. data/spec/ruwi/runtime/component_spec.rb +416 -0
  106. data/spec/ruwi/runtime/dom/scheduler_spec.rb +98 -0
  107. data/spec/ruwi/runtime/nodes_equal_spec.rb +190 -0
  108. data/spec/ruwi/runtime/template/build_conditional_group_spec.rb +505 -0
  109. data/spec/ruwi/runtime/template/build_for_group_spec.rb +377 -0
  110. data/spec/ruwi/runtime/template/build_vdom_spec.rb +573 -0
  111. data/spec/ruwi/runtime/template/parser_spec.rb +627 -0
  112. data/spec/ruwi/runtime/utils/arrays_spec.rb +228 -0
  113. data/spec/ruwi/runtime/utils/objects_spec.rb +127 -0
  114. data/spec/ruwi/runtime/utils/props_spec.rb +205 -0
  115. data/spec/ruwi/runtime/utils/strings_spec.rb +107 -0
  116. data/spec/spec_helper.rb +16 -0
  117. metadata +229 -0
@@ -0,0 +1,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>