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.
- package/package.json +1 -1
- package/src/compiler/phases/1-parse/index.js +33 -3
- package/src/compiler/phases/2-analyze/index.js +14 -1
- package/src/compiler/phases/3-transform/client/index.js +6 -3
- package/src/compiler/phases/3-transform/server/index.js +34 -34
- package/src/compiler/types/index.d.ts +2 -2
- package/src/compiler/utils.js +28 -0
- package/src/runtime/internal/client/blocks.js +7 -7
- package/src/runtime/internal/client/composite.js +41 -9
- package/src/runtime/internal/client/context.js +51 -51
- package/src/runtime/internal/client/css.js +2 -2
- package/src/runtime/internal/client/html.js +7 -4
- package/src/runtime/internal/client/index.js +1 -1
- package/src/runtime/internal/client/render.js +185 -158
- package/src/runtime/internal/client/runtime.js +7 -5
- package/src/runtime/internal/client/template.js +69 -63
- package/src/runtime/internal/client/try.js +127 -125
- package/src/runtime/internal/client/utils.js +5 -5
- package/tests/client/dynamic-elements.test.ripple +207 -0
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
114
|
+
var current = active_block;
|
|
113
115
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
+
var restore = capture();
|
|
173
|
+
var value = await promise;
|
|
172
174
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
});
|