ripple 0.2.108 → 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
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Ripple is an elegant TypeScript UI framework",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Dominic Gannaway",
|
|
6
|
-
"version": "0.2.
|
|
6
|
+
"version": "0.2.109",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"module": "src/runtime/index-client.js",
|
|
9
9
|
"main": "src/runtime/index-client.js",
|
|
@@ -178,10 +178,9 @@ const visitors = {
|
|
|
178
178
|
add_ripple_internal_import(context);
|
|
179
179
|
return b.call('_$_.get', build_getter(node, context));
|
|
180
180
|
}
|
|
181
|
-
|
|
182
|
-
add_ripple_internal_import(context);
|
|
183
|
-
return build_getter(node, context);
|
|
184
181
|
}
|
|
182
|
+
add_ripple_internal_import(context);
|
|
183
|
+
return build_getter(node, context);
|
|
185
184
|
}
|
|
186
185
|
}
|
|
187
186
|
},
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
import { branch, destroy_block, render } from './blocks.js';
|
|
4
4
|
import { COMPOSITE_BLOCK } from './constants.js';
|
|
5
|
+
import { apply_element_spread } from './render';
|
|
5
6
|
import { active_block } from './runtime.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
|
-
* @
|
|
9
|
+
* @typedef {((anchor: Node, props: Record<string, any>, block: Block | null) => void)} ComponentFunction
|
|
10
|
+
* @param {() => ComponentFunction | keyof HTMLElementTagNameMap} get_component
|
|
9
11
|
* @param {Node} node
|
|
10
12
|
* @param {Record<string, any>} props
|
|
11
13
|
* @returns {void}
|
|
@@ -23,9 +25,39 @@ export function composite(get_component, node, props) {
|
|
|
23
25
|
b = null;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
if (typeof component === 'function') {
|
|
29
|
+
// Handle as regular component
|
|
30
|
+
b = branch(() => {
|
|
31
|
+
var block = active_block;
|
|
32
|
+
/** @type {ComponentFunction} */ (component)(anchor, props, block);
|
|
33
|
+
});
|
|
34
|
+
} else {
|
|
35
|
+
// Custom element
|
|
36
|
+
b = branch(() => {
|
|
37
|
+
var block = /** @type {Block} */ (active_block);
|
|
38
|
+
|
|
39
|
+
var element = document.createElement(
|
|
40
|
+
/** @type {keyof HTMLElementTagNameMap} */ (component),
|
|
41
|
+
);
|
|
42
|
+
/** @type {ChildNode} */ (anchor).before(element);
|
|
43
|
+
|
|
44
|
+
if (block.s === null) {
|
|
45
|
+
block.s = {
|
|
46
|
+
start: element,
|
|
47
|
+
end: element,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const spread_fn = apply_element_spread(element, () => props || {});
|
|
52
|
+
spread_fn();
|
|
53
|
+
|
|
54
|
+
if (typeof props?.children === 'function') {
|
|
55
|
+
var child_anchor = document.createComment('');
|
|
56
|
+
element.appendChild(child_anchor);
|
|
57
|
+
|
|
58
|
+
props?.children?.(child_anchor, {}, block);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
30
62
|
}, COMPOSITE_BLOCK);
|
|
31
63
|
}
|
|
@@ -128,39 +128,66 @@ export function apply_styles(element, newStyles) {
|
|
|
128
128
|
* @returns {void}
|
|
129
129
|
*/
|
|
130
130
|
export function set_attributes(element, attributes) {
|
|
131
|
+
let found_enumerable_keys = false;
|
|
132
|
+
|
|
131
133
|
for (const key in attributes) {
|
|
132
134
|
if (key === 'children') continue;
|
|
135
|
+
found_enumerable_keys = true;
|
|
133
136
|
|
|
134
137
|
let value = attributes[key];
|
|
135
|
-
|
|
136
138
|
if (is_tracked_object(value)) {
|
|
137
139
|
value = get(value);
|
|
138
140
|
}
|
|
141
|
+
set_attribute_helper(element, key, value);
|
|
142
|
+
}
|
|
139
143
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
// Use delegation for delegated events
|
|
152
|
-
/** @type {any} */ (element)['__' + event_name] = value;
|
|
153
|
-
delegate([event_name]);
|
|
154
|
-
} else {
|
|
155
|
-
// Use addEventListener for non-delegated events
|
|
156
|
-
event(event_name, element, value);
|
|
144
|
+
// Only if no enumerable keys but attributes object exists
|
|
145
|
+
// This handles spread_props Proxy objects from dynamic elements with {...spread}
|
|
146
|
+
if (!found_enumerable_keys && attributes) {
|
|
147
|
+
const allKeys = Reflect.ownKeys(attributes);
|
|
148
|
+
for (const key of allKeys) {
|
|
149
|
+
if (key === 'children') continue;
|
|
150
|
+
if (typeof key === 'symbol') continue; // Skip symbols - handled by apply_element_spread
|
|
151
|
+
|
|
152
|
+
let value = attributes[key];
|
|
153
|
+
if (is_tracked_object(value)) {
|
|
154
|
+
value = get(value);
|
|
157
155
|
}
|
|
158
|
-
|
|
159
|
-
set_attribute(element, key, value);
|
|
156
|
+
set_attribute_helper(element, key, value);
|
|
160
157
|
}
|
|
161
158
|
}
|
|
162
159
|
}
|
|
163
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Helper function to set a single attribute
|
|
163
|
+
* @param {Element} element
|
|
164
|
+
* @param {string} key
|
|
165
|
+
* @param {any} value
|
|
166
|
+
*/
|
|
167
|
+
function set_attribute_helper(element, key, value) {
|
|
168
|
+
if (key === 'class') {
|
|
169
|
+
const is_html = element.namespaceURI === 'http://www.w3.org/1999/xhtml';
|
|
170
|
+
set_class(/** @type {HTMLElement} */ (element), value, undefined, is_html);
|
|
171
|
+
} else if (key === '#class') {
|
|
172
|
+
// Special case for static class when spreading props
|
|
173
|
+
element.classList.add(value);
|
|
174
|
+
} else if (typeof key === 'string' && is_event_attribute(key)) {
|
|
175
|
+
// Handle event handlers in spread props
|
|
176
|
+
const event_name = get_attribute_event_name(key);
|
|
177
|
+
|
|
178
|
+
if (is_delegated(event_name)) {
|
|
179
|
+
// Use delegation for delegated events
|
|
180
|
+
/** @type {any} */ (element)['__' + event_name] = value;
|
|
181
|
+
delegate([event_name]);
|
|
182
|
+
} else {
|
|
183
|
+
// Use addEventListener for non-delegated events
|
|
184
|
+
event(event_name, element, value);
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
set_attribute(element, key, value);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
164
191
|
/**
|
|
165
192
|
* @param {import('clsx').ClassValue} value
|
|
166
193
|
* @param {string} [hash]
|
|
@@ -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
|
+
});
|