ripple 0.2.107 → 0.2.109

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.
@@ -4,15 +4,15 @@ import { branch, create_try_block, destroy_block, is_destroyed, resume_block } f
4
4
  import { TRY_BLOCK } from './constants.js';
5
5
  import { next_sibling } from './operations.js';
6
6
  import {
7
- active_block,
8
- active_component,
9
- active_reaction,
10
- queue_microtask,
11
- set_active_block,
12
- set_active_component,
13
- set_active_reaction,
14
- set_tracking,
15
- tracking,
7
+ active_block,
8
+ active_component,
9
+ active_reaction,
10
+ queue_microtask,
11
+ set_active_block,
12
+ set_active_component,
13
+ set_active_reaction,
14
+ set_tracking,
15
+ tracking,
16
16
  } from './runtime.js';
17
17
 
18
18
  /**
@@ -23,142 +23,144 @@ import {
23
23
  * @returns {void}
24
24
  */
25
25
  export function try_block(node, fn, catch_fn, pending_fn = null) {
26
- var anchor = node;
27
- /** @type {Block | null} */
28
- var b = null;
29
- /** @type {Block | null} */
30
- var suspended = null;
31
- var pending_count = 0;
32
- /** @type {DocumentFragment | null} */
33
- var offscreen_fragment = null;
34
-
35
- /**
36
- * @param {Block} block
37
- * @param {DocumentFragment} fragment
38
- * @returns {void}
39
- */
40
- function move_block(block, fragment) {
41
- var state = block.s;
42
- var node = state.start;
43
- var end = state.end;
44
-
45
- while (node !== null) {
46
- var next = node === end ? null : next_sibling(node);
47
-
48
- fragment.append(node);
49
- node = next;
50
- }
51
- }
52
-
53
- function handle_await() {
54
- if (pending_count++ === 0) {
55
- queue_microtask(() => {
56
- if (b !== null) {
57
- suspended = b;
58
- offscreen_fragment = document.createDocumentFragment();
59
- move_block(b, offscreen_fragment);
60
-
61
- b = branch(() => {
62
- /** @type {(anchor: Node) => void} */ (pending_fn)(anchor);
63
- });
64
- }
65
- });
66
- }
67
-
68
- return () => {
69
- if (--pending_count === 0) {
70
- if (b !== null) {
71
- destroy_block(b);
72
- }
73
- /** @type {ChildNode} */ (anchor).before(/** @type {DocumentFragment} */ (offscreen_fragment));
74
- offscreen_fragment = null;
75
- resume_block(/** @type {Block} */ (suspended));
76
- b = suspended;
77
- suspended = null;
78
- }
79
- };
80
- }
81
-
82
- /**
83
- * @param {any} error
84
- * @returns {void}
85
- */
86
- function handle_error(error) {
87
- if (b !== null) {
88
- destroy_block(b);
89
- }
90
-
91
- b = branch(() => {
92
- /** @type {(anchor: Node, error: any) => void} */ (catch_fn)(anchor, error);
93
- });
94
- }
95
-
96
- var state = {
97
- a: pending_fn !== null ? handle_await : null,
98
- c: catch_fn !== null ? handle_error : null,
99
- };
100
-
101
- create_try_block(() => {
102
- b = branch(() => {
103
- fn(anchor);
104
- });
105
- }, state);
26
+ var anchor = node;
27
+ /** @type {Block | null} */
28
+ var b = null;
29
+ /** @type {Block | null} */
30
+ var suspended = null;
31
+ var pending_count = 0;
32
+ /** @type {DocumentFragment | null} */
33
+ var offscreen_fragment = null;
34
+
35
+ /**
36
+ * @param {Block} block
37
+ * @param {DocumentFragment} fragment
38
+ * @returns {void}
39
+ */
40
+ function move_block(block, fragment) {
41
+ var state = block.s;
42
+ var node = state.start;
43
+ var end = state.end;
44
+
45
+ while (node !== null) {
46
+ var next = node === end ? null : next_sibling(node);
47
+
48
+ fragment.append(node);
49
+ node = next;
50
+ }
51
+ }
52
+
53
+ function handle_await() {
54
+ if (pending_count++ === 0) {
55
+ queue_microtask(() => {
56
+ if (b !== null) {
57
+ suspended = b;
58
+ offscreen_fragment = document.createDocumentFragment();
59
+ move_block(b, offscreen_fragment);
60
+
61
+ b = branch(() => {
62
+ /** @type {(anchor: Node) => void} */ (pending_fn)(anchor);
63
+ });
64
+ }
65
+ });
66
+ }
67
+
68
+ return () => {
69
+ if (--pending_count === 0) {
70
+ if (b !== null) {
71
+ destroy_block(b);
72
+ }
73
+ /** @type {ChildNode} */ (anchor).before(
74
+ /** @type {DocumentFragment} */ (offscreen_fragment),
75
+ );
76
+ offscreen_fragment = null;
77
+ resume_block(/** @type {Block} */ (suspended));
78
+ b = suspended;
79
+ suspended = null;
80
+ }
81
+ };
82
+ }
83
+
84
+ /**
85
+ * @param {any} error
86
+ * @returns {void}
87
+ */
88
+ function handle_error(error) {
89
+ if (b !== null) {
90
+ destroy_block(b);
91
+ }
92
+
93
+ b = branch(() => {
94
+ /** @type {(anchor: Node, error: any) => void} */ (catch_fn)(anchor, error);
95
+ });
96
+ }
97
+
98
+ var state = {
99
+ a: pending_fn !== null ? handle_await : null,
100
+ c: catch_fn !== null ? handle_error : null,
101
+ };
102
+
103
+ create_try_block(() => {
104
+ b = branch(() => {
105
+ fn(anchor);
106
+ });
107
+ }, state);
106
108
  }
107
109
 
108
110
  /**
109
111
  * @returns {() => void}
110
112
  */
111
113
  export function suspend() {
112
- var current = active_block;
114
+ var current = active_block;
113
115
 
114
- while (current !== null) {
115
- var state = current.s;
116
- if ((current.f & TRY_BLOCK) !== 0 && state.a !== null) {
117
- return state.a();
118
- }
119
- current = current.p;
120
- }
116
+ while (current !== null) {
117
+ var state = current.s;
118
+ if ((current.f & TRY_BLOCK) !== 0 && state.a !== null) {
119
+ return state.a();
120
+ }
121
+ current = current.p;
122
+ }
121
123
 
122
- throw new Error('Missing parent `try { ... } pending { ... }` statement');
124
+ throw new Error('Missing parent `try { ... } pending { ... }` statement');
123
125
  }
124
126
 
125
127
  /**
126
128
  * @returns {void}
127
129
  */
128
130
  function exit() {
129
- set_tracking(false);
130
- set_active_reaction(null);
131
- set_active_block(null);
132
- set_active_component(null);
131
+ set_tracking(false);
132
+ set_active_reaction(null);
133
+ set_active_block(null);
134
+ set_active_component(null);
133
135
  }
134
136
 
135
137
  /**
136
138
  * @returns {() => void}
137
139
  */
138
140
  export function capture() {
139
- var previous_tracking = tracking;
140
- var previous_block = active_block;
141
- var previous_reaction = active_reaction;
142
- var previous_component = active_component;
143
-
144
- return () => {
145
- set_tracking(previous_tracking);
146
- set_active_block(previous_block);
147
- set_active_reaction(previous_reaction);
148
- set_active_component(previous_component);
149
-
150
- queue_microtask(exit);
151
- };
141
+ var previous_tracking = tracking;
142
+ var previous_block = active_block;
143
+ var previous_reaction = active_reaction;
144
+ var previous_component = active_component;
145
+
146
+ return () => {
147
+ set_tracking(previous_tracking);
148
+ set_active_block(previous_block);
149
+ set_active_reaction(previous_reaction);
150
+ set_active_component(previous_component);
151
+
152
+ queue_microtask(exit);
153
+ };
152
154
  }
153
155
 
154
156
  /**
155
157
  * @returns {boolean}
156
158
  */
157
159
  export function aborted() {
158
- if (active_block === null) {
159
- return true;
160
- }
161
- return is_destroyed(active_block);
160
+ if (active_block === null) {
161
+ return true;
162
+ }
163
+ return is_destroyed(active_block);
162
164
  }
163
165
 
164
166
  /**
@@ -167,11 +169,11 @@ export function aborted() {
167
169
  * @returns {Promise<() => T>}
168
170
  */
169
171
  export async function resume_context(promise) {
170
- var restore = capture();
171
- var value = await promise;
172
+ var restore = capture();
173
+ var value = await promise;
172
174
 
173
- return () => {
174
- restore();
175
- return value;
176
- };
175
+ return () => {
176
+ restore();
177
+ return value;
178
+ };
177
179
  }
@@ -30,9 +30,9 @@ export var array_prototype = Array.prototype;
30
30
  * @returns {Text}
31
31
  */
32
32
  export function create_anchor() {
33
- var t = document.createTextNode('');
34
- /** @type {any} */ (t).__t = '';
35
- return t;
33
+ var t = document.createTextNode('');
34
+ /** @type {any} */ (t).__t = '';
35
+ return t;
36
36
  }
37
37
 
38
38
  /**
@@ -40,7 +40,7 @@ export function create_anchor() {
40
40
  * @returns {boolean}
41
41
  */
42
42
  export function is_positive_integer(value) {
43
- return Number.isInteger(value) && /**@type {number} */ (value) >= 0;
43
+ return Number.isInteger(value) && /**@type {number} */ (value) >= 0;
44
44
  }
45
45
 
46
46
  /**
@@ -49,5 +49,5 @@ export function is_positive_integer(value) {
49
49
  * @returns {boolean}
50
50
  */
51
51
  export function is_tracked_object(v) {
52
- return typeof v === 'object' && v !== null && typeof /** @type {any} */ (v).f === 'number';
52
+ return typeof v === 'object' && v !== null && typeof (/** @type {any} */ (v).f) === 'number';
53
53
  }
@@ -0,0 +1,207 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mount, flushSync, track, createRefKey } from 'ripple';
3
+
4
+ describe('dynamic DOM elements', () => {
5
+ let container;
6
+
7
+ function render(component) {
8
+ mount(component, {
9
+ target: container,
10
+ });
11
+ }
12
+
13
+ beforeEach(() => {
14
+ container = document.createElement('div');
15
+ document.body.appendChild(container);
16
+ });
17
+ afterEach(() => {
18
+ document.body.removeChild(container);
19
+ container = null;
20
+ });
21
+ it('renders static dynamic element', () => {
22
+ component App() {
23
+ let tag = track('div');
24
+
25
+ <@tag>{'Hello World'}</@tag>
26
+ }
27
+ render(App);
28
+
29
+ const element = container.querySelector('div');
30
+ expect(element).toBeTruthy();
31
+ expect(element.textContent).toBe('Hello World');
32
+ });
33
+ it('renders reactive dynamic element', () => {
34
+ component App() {
35
+ let tag = track('div');
36
+
37
+ <button onClick={() => {
38
+ @tag = 'span';
39
+ }}>{'Change Tag'}</button>
40
+ <@tag id="dynamic">{'Hello World'}</@tag>
41
+ }
42
+ render(App);
43
+ // Initially should be a div
44
+ let dynamicElement = container.querySelector('#dynamic');
45
+ expect(dynamicElement.tagName).toBe('DIV');
46
+ expect(dynamicElement.textContent).toBe('Hello World');
47
+ // Click button to change tag
48
+ const button = container.querySelector('button');
49
+ button.click();
50
+ flushSync();
51
+ // Should now be a span
52
+ dynamicElement = container.querySelector('#dynamic');
53
+ expect(dynamicElement.tagName).toBe('SPAN');
54
+ expect(dynamicElement.textContent).toBe('Hello World');
55
+ });
56
+ it('renders self-closing dynamic element', () => {
57
+ component App() {
58
+ let tag = track('input');
59
+
60
+ <@tag type="text" value="test" />
61
+ }
62
+ render(App);
63
+
64
+ const element = container.querySelector('input');
65
+ expect(element).toBeTruthy();
66
+ expect(element.type).toBe('text');
67
+ expect(element.value).toBe('test');
68
+ });
69
+ it('handles dynamic element with attributes', () => {
70
+ component App() {
71
+ let tag = track('div');
72
+ let className = track('test-class');
73
+
74
+ <@tag class={@className} id="test" data-testid="dynamic-element">{'Content'}</@tag>
75
+ }
76
+ render(App);
77
+
78
+ const element = container.querySelector('#test');
79
+ expect(element.tagName).toBe('DIV');
80
+ expect(element.className).toBe('test-class');
81
+ expect(element.getAttribute('data-testid')).toBe('dynamic-element');
82
+ expect(element.textContent).toBe('Content');
83
+ });
84
+ it('handles nested dynamic elements', () => {
85
+ component App() {
86
+ let outerTag = track('div');
87
+ let innerTag = track('span');
88
+
89
+ <@outerTag class="outer">
90
+ <@innerTag class="inner">{'Nested content'}</@innerTag>
91
+ </@outerTag>
92
+ }
93
+ render(App);
94
+
95
+ const outer = container.querySelector('.outer');
96
+ const inner = container.querySelector('.inner');
97
+
98
+ expect(outer.tagName).toBe('DIV');
99
+ expect(inner.tagName).toBe('SPAN');
100
+ expect(inner.textContent).toBe('Nested content');
101
+ expect(outer.contains(inner)).toBe(true);
102
+ });
103
+ it('handles dynamic element with class object', () => {
104
+ component App() {
105
+ let tag = track('div');
106
+ let active = track(true);
107
+
108
+ <@tag class={{ active: @active, 'dynamic-element': true }}>
109
+ {'Element with class object'}
110
+ </@tag>
111
+ }
112
+ render(App);
113
+
114
+ const element = container.querySelector('div');
115
+ expect(element).toBeTruthy();
116
+ expect(element.classList.contains('active')).toBe(true);
117
+ expect(element.classList.contains('dynamic-element')).toBe(true);
118
+ });
119
+ it('handles dynamic element with style object', () => {
120
+ component App() {
121
+ let tag = track('span');
122
+
123
+ <@tag style={{
124
+ color: 'red',
125
+ fontSize: '16px',
126
+ fontWeight: 'bold'
127
+ }}>
128
+ {'Styled dynamic element'}
129
+ </@tag>
130
+ }
131
+ render(App);
132
+
133
+ const element = container.querySelector('span');
134
+ expect(element).toBeTruthy();
135
+ expect(element.style.color).toBe('red');
136
+ expect(element.style.fontSize).toBe('16px');
137
+ expect(element.style.fontWeight).toBe('bold');
138
+ });
139
+ it('handles dynamic element with spread attributes', () => {
140
+ component App() {
141
+ let tag = track('section');
142
+ const attrs = {
143
+ id: 'spread-section',
144
+ 'data-testid': 'spread-test',
145
+ class: 'spread-class',
146
+ };
147
+
148
+ <@tag {...attrs} data-extra="additional">
149
+ {'Element with spread attributes'}
150
+ </@tag>
151
+ }
152
+ render(App);
153
+
154
+ const element = container.querySelector('section');
155
+ expect(element).toBeTruthy();
156
+ expect(element.id).toBe('spread-section');
157
+ expect(element.getAttribute('data-testid')).toBe('spread-test');
158
+ expect(element.className).toBe('spread-class');
159
+ expect(element.getAttribute('data-extra')).toBe('additional');
160
+ });
161
+ it('handles dynamic element with ref', () => {
162
+ let capturedElement = null;
163
+
164
+ component App() {
165
+ let tag = track('article');
166
+
167
+ <@tag {ref (node) => { capturedElement = node; }} id="ref-test">
168
+ {'Element with ref'}
169
+ </@tag>
170
+ }
171
+ render(App);
172
+ flushSync();
173
+ expect(capturedElement).toBeTruthy();
174
+ expect(capturedElement.tagName).toBe('ARTICLE');
175
+ expect(capturedElement.id).toBe('ref-test');
176
+ expect(capturedElement.textContent).toBe('Element with ref');
177
+ });
178
+ it('handles dynamic element with createRefKey in spread', () => {
179
+ component App() {
180
+ let tag = track('header');
181
+
182
+ function elementRef(node) {
183
+ // Set an attribute on the element to prove ref was called
184
+ node.setAttribute('data-spread-ref-called', 'true');
185
+ node.setAttribute('data-spread-ref-tag', node.tagName.toLowerCase());
186
+ }
187
+
188
+ const dynamicProps = {
189
+ id: 'spread-ref-test',
190
+ class: 'ref-element',
191
+ [createRefKey()]: elementRef
192
+ };
193
+
194
+ <@tag {...dynamicProps}>{'Element with spread ref'}</@tag>
195
+ }
196
+ render(App);
197
+ flushSync();
198
+
199
+ // Check that the spread ref was called by verifying attributes were set
200
+ const element = container.querySelector('header');
201
+ expect(element).toBeTruthy();
202
+ expect(element.getAttribute('data-spread-ref-called')).toBe('true');
203
+ expect(element.getAttribute('data-spread-ref-tag')).toBe('header');
204
+ expect(element.id).toBe('spread-ref-test');
205
+ expect(element.className).toBe('ref-element');
206
+ });
207
+ });