ripple 0.3.9 → 0.3.10
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/CHANGELOG.md +12 -0
- package/package.json +2 -2
- package/src/compiler/phases/1-parse/index.js +25 -15
- package/src/compiler/phases/2-analyze/index.js +35 -88
- package/src/compiler/phases/2-analyze/prune.js +13 -5
- package/src/compiler/phases/3-transform/client/index.js +188 -56
- package/src/compiler/phases/3-transform/server/index.js +62 -40
- package/src/compiler/types/index.d.ts +9 -1
- package/src/compiler/types/parse.d.ts +2 -0
- package/src/compiler/utils.js +101 -1
- package/src/runtime/element.js +39 -0
- package/src/runtime/internal/client/composite.js +10 -6
- package/src/runtime/internal/client/expression.js +218 -0
- package/src/runtime/internal/client/index.js +4 -0
- package/src/runtime/internal/client/portal.js +12 -6
- package/src/runtime/internal/server/index.js +26 -1
- package/tests/client/basic/basic.components.test.ripple +85 -87
- package/tests/client/basic/basic.errors.test.ripple +4 -8
- package/tests/client/basic/basic.rendering.test.ripple +23 -8
- package/tests/client/capture-error.js +12 -0
- package/tests/client/compiler/compiler.basic.test.ripple +76 -6
- package/tests/client/composite/composite.props.test.ripple +1 -3
- package/tests/client/composite/composite.render.test.ripple +45 -13
- package/tests/client/css/global-additional-cases.test.ripple +3 -3
- package/tests/client/svg.test.ripple +4 -4
- package/tests/hydration/basic.test.js +23 -0
- package/tests/hydration/compiled/client/basic.js +118 -66
- package/tests/hydration/compiled/client/composite.js +90 -37
- package/tests/hydration/compiled/client/events.js +18 -18
- package/tests/hydration/compiled/client/for.js +62 -62
- package/tests/hydration/compiled/client/head.js +10 -10
- package/tests/hydration/compiled/client/hmr.js +13 -10
- package/tests/hydration/compiled/client/html.js +274 -236
- package/tests/hydration/compiled/client/if-children.js +41 -35
- package/tests/hydration/compiled/client/if.js +2 -2
- package/tests/hydration/compiled/client/mixed-control-flow.js +12 -12
- package/tests/hydration/compiled/client/nested-control-flow.js +46 -46
- package/tests/hydration/compiled/client/portal.js +8 -8
- package/tests/hydration/compiled/client/reactivity.js +14 -14
- package/tests/hydration/compiled/client/return.js +2 -2
- package/tests/hydration/compiled/client/try.js +4 -4
- package/tests/hydration/compiled/server/basic.js +64 -31
- package/tests/hydration/compiled/server/composite.js +62 -29
- package/tests/hydration/compiled/server/hmr.js +24 -37
- package/tests/hydration/compiled/server/html.js +472 -611
- package/tests/hydration/compiled/server/if-children.js +77 -103
- package/tests/hydration/compiled/server/portal.js +8 -8
- package/tests/hydration/components/basic.ripple +15 -5
- package/tests/hydration/components/composite.ripple +13 -1
- package/tests/hydration/components/hmr.ripple +1 -3
- package/tests/hydration/components/html.ripple +13 -35
- package/tests/hydration/components/if-children.ripple +4 -8
- package/tests/hydration/composite.test.js +11 -0
- package/tests/server/basic.attributes.test.ripple +50 -0
- package/tests/server/basic.components.test.ripple +22 -28
- package/tests/server/basic.test.ripple +12 -0
- package/tests/server/compiler.test.ripple +25 -8
- package/tests/server/composite.props.test.ripple +1 -3
- package/tests/server/style-identifier.test.ripple +2 -4
- package/types/index.d.ts +9 -2
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const RIPPLE_ELEMENT = Symbol.for('ripple.element');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {{
|
|
5
|
+
* render: Function;
|
|
6
|
+
* [RIPPLE_ELEMENT]: true;
|
|
7
|
+
* }} RippleElement
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {Function} render
|
|
12
|
+
* @returns {RippleElement}
|
|
13
|
+
*/
|
|
14
|
+
export function ripple_element(render) {
|
|
15
|
+
return {
|
|
16
|
+
render,
|
|
17
|
+
[RIPPLE_ELEMENT]: true,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {any} value
|
|
23
|
+
* @returns {value is RippleElement}
|
|
24
|
+
*/
|
|
25
|
+
export function is_ripple_element(value) {
|
|
26
|
+
return value != null && value[RIPPLE_ELEMENT] === true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {any} value
|
|
31
|
+
* @returns {any}
|
|
32
|
+
*/
|
|
33
|
+
export function normalize_children(value) {
|
|
34
|
+
if (value == null || is_ripple_element(value) || typeof value !== 'function') {
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return ripple_element(value);
|
|
39
|
+
}
|
|
@@ -5,6 +5,7 @@ import { COMPOSITE_BLOCK, DEFAULT_NAMESPACE, NAMESPACE_URI } from './constants.j
|
|
|
5
5
|
import { hydrate_next, hydrating } from './hydration.js';
|
|
6
6
|
import { active_block, active_namespace, get, with_ns } from './runtime.js';
|
|
7
7
|
import { top_element_to_ns } from './utils.js';
|
|
8
|
+
import { is_ripple_element } from '../../element.js';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* @typedef {((anchor: Node, props: Record<string, any>, block: Block | null) => void)} ComponentFunction
|
|
@@ -45,13 +46,14 @@ export function composite(get_component, node, props) {
|
|
|
45
46
|
});
|
|
46
47
|
} else if (component != null) {
|
|
47
48
|
// Custom element - only create if component is not null/undefined
|
|
49
|
+
const ns = top_element_to_ns(component, active_namespace);
|
|
48
50
|
var run = () => {
|
|
49
51
|
var block = /** @type {Block} */ (active_block);
|
|
50
52
|
|
|
51
53
|
var element =
|
|
52
|
-
|
|
54
|
+
ns !== DEFAULT_NAMESPACE
|
|
53
55
|
? document.createElementNS(
|
|
54
|
-
NAMESPACE_URI[
|
|
56
|
+
NAMESPACE_URI[ns],
|
|
55
57
|
/** @type {keyof HTMLElementTagNameMap} */ (component),
|
|
56
58
|
)
|
|
57
59
|
: document.createElement(/** @type {keyof HTMLElementTagNameMap} */ (component));
|
|
@@ -67,16 +69,18 @@ export function composite(get_component, node, props) {
|
|
|
67
69
|
|
|
68
70
|
render_spread(element, () => props || {});
|
|
69
71
|
|
|
70
|
-
if (
|
|
72
|
+
if (is_ripple_element(props?.children)) {
|
|
71
73
|
var child_anchor = document.createComment('');
|
|
72
74
|
element.appendChild(child_anchor);
|
|
73
75
|
|
|
74
|
-
|
|
76
|
+
if (ns !== DEFAULT_NAMESPACE) {
|
|
77
|
+
with_ns(ns, () => props.children.render(child_anchor, {}, block));
|
|
78
|
+
} else {
|
|
79
|
+
props.children.render(child_anchor, {}, block);
|
|
80
|
+
}
|
|
75
81
|
}
|
|
76
82
|
};
|
|
77
83
|
|
|
78
|
-
const ns = top_element_to_ns(component, active_namespace);
|
|
79
|
-
|
|
80
84
|
if (ns !== active_namespace) {
|
|
81
85
|
// support top-level dynamic element svg/math <@tag />
|
|
82
86
|
b = branch(() => with_ns(ns, run));
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/** @import { Block } from '#client' */
|
|
2
|
+
|
|
3
|
+
import { branch, destroy_block, render } from './blocks.js';
|
|
4
|
+
import { UNINITIALIZED } from './constants.js';
|
|
5
|
+
import { create_text, get_next_sibling } from './operations.js';
|
|
6
|
+
import { active_block } from './runtime.js';
|
|
7
|
+
import { hydrating, set_hydrate_node } from './hydration.js';
|
|
8
|
+
import { COMMENT_NODE, HYDRATION_END, HYDRATION_START, TEXT_NODE } from '../../../constants.js';
|
|
9
|
+
import { is_ripple_element } from '../../element.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {Node} node
|
|
13
|
+
* @param {() => any} get_value
|
|
14
|
+
* @returns {void}
|
|
15
|
+
*/
|
|
16
|
+
export function expression(node, get_value) {
|
|
17
|
+
var anchor = /** @type {ChildNode} */ (node);
|
|
18
|
+
/** @type {Block | null} */
|
|
19
|
+
var child_block = null;
|
|
20
|
+
/** @type {Comment | null} */
|
|
21
|
+
var end = null;
|
|
22
|
+
/** @type {Text | null} */
|
|
23
|
+
var text = null;
|
|
24
|
+
/** @type {string | import('../../element.js').RippleElement | typeof UNINITIALIZED} */
|
|
25
|
+
var value = UNINITIALIZED;
|
|
26
|
+
var is_element = false;
|
|
27
|
+
var initialized = false;
|
|
28
|
+
|
|
29
|
+
render(() => {
|
|
30
|
+
var next_value = get_value();
|
|
31
|
+
var next_is_element = is_ripple_element(next_value);
|
|
32
|
+
var is_hydration_marker = hydrating && anchor.nodeType === COMMENT_NODE;
|
|
33
|
+
|
|
34
|
+
if (is_hydration_marker) {
|
|
35
|
+
end ??= ensure_expression_end(anchor);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (next_is_element) {
|
|
39
|
+
if (initialized && is_element && value === next_value) {
|
|
40
|
+
if (end !== null) {
|
|
41
|
+
advance_hydration(end);
|
|
42
|
+
}
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (anchor.nodeType === TEXT_NODE) {
|
|
47
|
+
/** @type {Text} */ (anchor).nodeValue = '';
|
|
48
|
+
} else if (text !== null) {
|
|
49
|
+
text.remove();
|
|
50
|
+
text = null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (child_block !== null) {
|
|
54
|
+
destroy_block(child_block);
|
|
55
|
+
child_block = null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (end !== null && (initialized || !hydrating)) {
|
|
59
|
+
clear_expression_range(anchor, end);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (is_hydration_marker) {
|
|
63
|
+
set_hydrate_node(get_next_sibling(anchor) ?? end);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
child_block = branch(() => {
|
|
67
|
+
var block = active_block;
|
|
68
|
+
next_value.render(end ?? anchor, {}, block);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
value = next_value;
|
|
72
|
+
is_element = true;
|
|
73
|
+
initialized = true;
|
|
74
|
+
if (end !== null) {
|
|
75
|
+
advance_hydration(end);
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
var next_text = (next_value ?? '') + '';
|
|
81
|
+
|
|
82
|
+
if (initialized && !is_element && value === next_text) {
|
|
83
|
+
if (end !== null) {
|
|
84
|
+
advance_hydration(end);
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (child_block !== null) {
|
|
90
|
+
destroy_block(child_block);
|
|
91
|
+
child_block = null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (is_hydration_marker) {
|
|
95
|
+
text = get_hydrated_text(anchor, /** @type {Comment} */ (end));
|
|
96
|
+
|
|
97
|
+
if (next_text === '') {
|
|
98
|
+
if (text !== null) {
|
|
99
|
+
text.remove();
|
|
100
|
+
text = null;
|
|
101
|
+
}
|
|
102
|
+
} else if (text === null) {
|
|
103
|
+
text = create_text(next_text);
|
|
104
|
+
/** @type {Comment} */ (end).before(text);
|
|
105
|
+
} else if (text.nodeValue !== next_text) {
|
|
106
|
+
text.nodeValue = next_text;
|
|
107
|
+
}
|
|
108
|
+
} else if (anchor.nodeType === COMMENT_NODE) {
|
|
109
|
+
if (next_text === '') {
|
|
110
|
+
if (text !== null) {
|
|
111
|
+
text.remove();
|
|
112
|
+
text = null;
|
|
113
|
+
}
|
|
114
|
+
} else if (text === null) {
|
|
115
|
+
text = create_text(next_text);
|
|
116
|
+
(end ?? anchor).before(text);
|
|
117
|
+
} else if (text.nodeValue !== next_text) {
|
|
118
|
+
text.nodeValue = next_text;
|
|
119
|
+
}
|
|
120
|
+
} else if (anchor.nodeType === TEXT_NODE) {
|
|
121
|
+
/** @type {Text} */ (anchor).nodeValue = next_text;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
value = next_text;
|
|
125
|
+
is_element = false;
|
|
126
|
+
initialized = true;
|
|
127
|
+
if (end !== null) {
|
|
128
|
+
advance_hydration(end);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* @param {Node} anchor
|
|
135
|
+
* @returns {Comment}
|
|
136
|
+
*/
|
|
137
|
+
function ensure_expression_end(anchor) {
|
|
138
|
+
if (hydrating) {
|
|
139
|
+
/** @type {Node | null} */
|
|
140
|
+
var current = get_next_sibling(anchor);
|
|
141
|
+
var depth = 0;
|
|
142
|
+
|
|
143
|
+
while (current !== null) {
|
|
144
|
+
if (current.nodeType === COMMENT_NODE) {
|
|
145
|
+
var data = /** @type {Comment} */ (current).data;
|
|
146
|
+
|
|
147
|
+
if (data === HYDRATION_START) {
|
|
148
|
+
depth += 1;
|
|
149
|
+
} else if (data === HYDRATION_END) {
|
|
150
|
+
if (depth === 0) {
|
|
151
|
+
return /** @type {Comment} */ (current);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
depth -= 1;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
current = get_next_sibling(current);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
throw new Error('Hydration mismatch: expected end marker for expression block');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
var end = document.createComment(HYDRATION_END);
|
|
165
|
+
/** @type {ChildNode} */ (anchor).after(end);
|
|
166
|
+
return end;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* @param {Node} anchor
|
|
171
|
+
* @param {Node} end
|
|
172
|
+
* @returns {Text | null}
|
|
173
|
+
*/
|
|
174
|
+
function get_hydrated_text(anchor, end) {
|
|
175
|
+
var first = get_next_sibling(anchor);
|
|
176
|
+
|
|
177
|
+
if (first === end) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (first?.nodeType === TEXT_NODE && get_next_sibling(first) === end) {
|
|
182
|
+
return /** @type {Text} */ (first);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
clear_expression_range(anchor, end);
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* @param {Node} anchor
|
|
191
|
+
* @param {Node} end
|
|
192
|
+
* @returns {void}
|
|
193
|
+
*/
|
|
194
|
+
function clear_expression_range(anchor, end) {
|
|
195
|
+
var current = get_next_sibling(anchor);
|
|
196
|
+
|
|
197
|
+
while (current !== null && current !== end) {
|
|
198
|
+
var next = get_next_sibling(current);
|
|
199
|
+
/** @type {ChildNode} */ (current).remove();
|
|
200
|
+
current = next;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* @param {Comment} end
|
|
206
|
+
* @returns {void}
|
|
207
|
+
*/
|
|
208
|
+
function advance_hydration(end) {
|
|
209
|
+
if (!hydrating) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
var next = get_next_sibling(end);
|
|
214
|
+
|
|
215
|
+
if (next !== null) {
|
|
216
|
+
set_hydrate_node(next);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -105,6 +105,8 @@ export { script } from './script.js';
|
|
|
105
105
|
|
|
106
106
|
export { html } from './html.js';
|
|
107
107
|
|
|
108
|
+
export { expression } from './expression.js';
|
|
109
|
+
|
|
108
110
|
export { rpc } from './rpc.js';
|
|
109
111
|
|
|
110
112
|
export { tsx_compat } from './compat.js';
|
|
@@ -114,3 +116,5 @@ export { TRY_BLOCK, HMR } from './constants.js';
|
|
|
114
116
|
export { hmr } from './hmr.js';
|
|
115
117
|
|
|
116
118
|
export { pop, next } from './hydration.js';
|
|
119
|
+
|
|
120
|
+
export { ripple_element, normalize_children } from '../../element.js';
|
|
@@ -12,10 +12,11 @@ import {
|
|
|
12
12
|
set_hydrating,
|
|
13
13
|
set_hydrate_node,
|
|
14
14
|
} from './hydration.js';
|
|
15
|
+
import { is_ripple_element } from '../../element.js';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* @param {any} _
|
|
18
|
-
* @param {{ target: Element, children: (
|
|
19
|
+
* @param {{ target: Element, children: import('../../element.js').RippleElement }} props
|
|
19
20
|
* @returns {void}
|
|
20
21
|
*/
|
|
21
22
|
export function Portal(_, props) {
|
|
@@ -26,7 +27,7 @@ export function Portal(_, props) {
|
|
|
26
27
|
|
|
27
28
|
/** @type {Element | symbol} */
|
|
28
29
|
let target = UNINITIALIZED;
|
|
29
|
-
/** @type {(
|
|
30
|
+
/** @type {import('../../element.js').RippleElement | symbol} */
|
|
30
31
|
let children = UNINITIALIZED;
|
|
31
32
|
/** @type {Block | null} */
|
|
32
33
|
var b = null;
|
|
@@ -44,8 +45,13 @@ export function Portal(_, props) {
|
|
|
44
45
|
|
|
45
46
|
try {
|
|
46
47
|
render(() => {
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
const next_target = props.target;
|
|
49
|
+
const next_children = props.children;
|
|
50
|
+
|
|
51
|
+
if (target === next_target && children === next_children) return;
|
|
52
|
+
|
|
53
|
+
target = next_target;
|
|
54
|
+
children = next_children;
|
|
49
55
|
|
|
50
56
|
if (b !== null) {
|
|
51
57
|
destroy_block(b);
|
|
@@ -65,8 +71,8 @@ export function Portal(_, props) {
|
|
|
65
71
|
var block = /** @type {Block} */ (active_block);
|
|
66
72
|
|
|
67
73
|
b = branch(() => {
|
|
68
|
-
if (
|
|
69
|
-
children(/** @type {Text} */ (anchor), {}, block);
|
|
74
|
+
if (is_ripple_element(children)) {
|
|
75
|
+
children.render(/** @type {Text} */ (anchor), {}, block);
|
|
70
76
|
}
|
|
71
77
|
});
|
|
72
78
|
|
|
@@ -17,6 +17,7 @@ import { is_boolean_attribute } from '../../../compiler/utils.js';
|
|
|
17
17
|
import { clsx } from 'clsx';
|
|
18
18
|
import { normalize_css_property_name } from '../../../utils/normalize_css_property_name.js';
|
|
19
19
|
import { BLOCK_CLOSE, BLOCK_OPEN } from '../../../constants.js';
|
|
20
|
+
import { is_ripple_element, normalize_children, ripple_element } from '../../element.js';
|
|
20
21
|
import {
|
|
21
22
|
is_tag_valid_with_parent,
|
|
22
23
|
is_tag_valid_with_ancestor,
|
|
@@ -27,6 +28,30 @@ export { register_component_css as register_css } from './css-registry.js';
|
|
|
27
28
|
export { hash } from '../../../utils/hashing.js';
|
|
28
29
|
export { context } from './context.js';
|
|
29
30
|
export { array_slice };
|
|
31
|
+
export { ripple_element, normalize_children };
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {Output} output
|
|
35
|
+
* @param {any} value
|
|
36
|
+
* @returns {void}
|
|
37
|
+
*/
|
|
38
|
+
export function render_expression(output, value) {
|
|
39
|
+
output.push(BLOCK_OPEN);
|
|
40
|
+
|
|
41
|
+
if (is_ripple_element(value)) {
|
|
42
|
+
var result = value.render(output, {});
|
|
43
|
+
|
|
44
|
+
if (result && typeof result.then === 'function') {
|
|
45
|
+
return result.then(() => {
|
|
46
|
+
output.push(BLOCK_CLOSE);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
output.push(escape(value ?? ''));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
output.push(BLOCK_CLOSE);
|
|
54
|
+
}
|
|
30
55
|
|
|
31
56
|
/** @type {null | Component} */
|
|
32
57
|
export let active_component = null;
|
|
@@ -596,7 +621,7 @@ export function spread_attrs(attrs, css_hash) {
|
|
|
596
621
|
for (name in attrs) {
|
|
597
622
|
var value = attrs[name];
|
|
598
623
|
|
|
599
|
-
if (typeof value === 'function') continue;
|
|
624
|
+
if (name === 'children' || typeof value === 'function' || is_ripple_element(value)) continue;
|
|
600
625
|
|
|
601
626
|
if (is_ripple_object(value)) {
|
|
602
627
|
value = get(value);
|
|
@@ -7,21 +7,20 @@ import type {
|
|
|
7
7
|
PropsWithChildrenOptional,
|
|
8
8
|
} from 'ripple';
|
|
9
9
|
import { flushSync, track } from 'ripple';
|
|
10
|
+
import { did_error } from '../capture-error.js';
|
|
10
11
|
|
|
11
12
|
describe('basic client > components & composition', () => {
|
|
12
13
|
it('renders with component composition and children', () => {
|
|
13
14
|
component Card(props: PropsWithChildren<{}>) {
|
|
14
|
-
<div class="card">
|
|
15
|
-
<props.children />
|
|
16
|
-
</div>
|
|
15
|
+
<div class="card">{props.children}</div>
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
component Basic() {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
component children() {
|
|
20
|
+
<p>{'Card content here'}</p>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
<Card {children} />
|
|
25
24
|
}
|
|
26
25
|
|
|
27
26
|
render(Basic);
|
|
@@ -37,17 +36,17 @@ describe('basic client > components & composition', () => {
|
|
|
37
36
|
component Card(props: PropsWithChildrenOptional<{ test?: Component }>) {
|
|
38
37
|
<div class="card">
|
|
39
38
|
if (props.children) {
|
|
40
|
-
|
|
39
|
+
{props.children}
|
|
41
40
|
}
|
|
42
41
|
</div>
|
|
43
42
|
}
|
|
44
43
|
|
|
45
44
|
component Basic() {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
component test() {
|
|
46
|
+
<p>{'Card content here'}</p>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
<Card {test} />
|
|
51
50
|
}
|
|
52
51
|
|
|
53
52
|
render(Basic);
|
|
@@ -59,22 +58,23 @@ describe('basic client > components & composition', () => {
|
|
|
59
58
|
expect(paragraph).toBeFalsy();
|
|
60
59
|
});
|
|
61
60
|
|
|
62
|
-
it('allows tracked
|
|
61
|
+
it('allows tracked variables alongside explicit component props', () => {
|
|
63
62
|
component Card(props: PropsWithChildrenOptional<{ test?: Component }>) {
|
|
64
63
|
<div class="card">
|
|
65
64
|
if (props.children) {
|
|
66
|
-
|
|
65
|
+
{props.children}
|
|
67
66
|
}
|
|
68
67
|
</div>
|
|
69
68
|
}
|
|
70
69
|
|
|
71
70
|
component Basic() {
|
|
72
71
|
let &[test] = track(false);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
72
|
+
|
|
73
|
+
component TestSlot() {
|
|
74
|
+
<p>{'Card content here'}</p>
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
<Card test={TestSlot} />
|
|
78
78
|
<div>{test ? 'yes' : 'no'}</div>
|
|
79
79
|
}
|
|
80
80
|
|
|
@@ -90,9 +90,7 @@ describe('basic client > components & composition', () => {
|
|
|
90
90
|
|
|
91
91
|
it('renders a component when children is set a component prop', () => {
|
|
92
92
|
component Card(props: PropsWithChildren<{}>) {
|
|
93
|
-
<div class="card">
|
|
94
|
-
<props.children />
|
|
95
|
-
</div>
|
|
93
|
+
<div class="card">{props.children}</div>
|
|
96
94
|
}
|
|
97
95
|
|
|
98
96
|
component Basic() {
|
|
@@ -211,6 +209,28 @@ describe('basic client > components & composition', () => {
|
|
|
211
209
|
expect(countDiv.textContent).toBe('3');
|
|
212
210
|
});
|
|
213
211
|
|
|
212
|
+
it('updates explicit text children props reactively', () => {
|
|
213
|
+
component TextProp(&{ children }: PropsWithChildren<{}>) {
|
|
214
|
+
<div class="text-prop">{children}</div>
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
component Basic() {
|
|
218
|
+
let &[show] = track(false);
|
|
219
|
+
|
|
220
|
+
<TextProp children={show ? 'hello' : ''} />
|
|
221
|
+
<button class="show-text" onClick={() => (show = true)}>{'Show'}</button>
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
render(Basic);
|
|
225
|
+
|
|
226
|
+
expect(container.querySelector('.text-prop')?.textContent).toBe('');
|
|
227
|
+
|
|
228
|
+
container.querySelector('.show-text')?.click();
|
|
229
|
+
flushSync();
|
|
230
|
+
|
|
231
|
+
expect(container.querySelector('.text-prop')?.textContent).toBe('hello');
|
|
232
|
+
});
|
|
233
|
+
|
|
214
234
|
it('it retains this context with bracketed prop functions and keeps original chaining', () => {
|
|
215
235
|
component App() {
|
|
216
236
|
const SYMBOL_PROP = Symbol();
|
|
@@ -233,60 +253,40 @@ describe('basic client > components & composition', () => {
|
|
|
233
253
|
|
|
234
254
|
const obj2 = null;
|
|
235
255
|
|
|
256
|
+
function trigger_nonexistent() {
|
|
257
|
+
hasError = did_error(() => {
|
|
258
|
+
// @ts-ignore
|
|
259
|
+
obj['nonexistent']();
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function trigger_nonexistent_chaining() {
|
|
264
|
+
hasError = did_error(() => {
|
|
265
|
+
// @ts-ignore
|
|
266
|
+
obj['nonexistent']?.();
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function trigger_object_null() {
|
|
271
|
+
hasError = did_error(() => {
|
|
272
|
+
// @ts-ignore
|
|
273
|
+
obj2['nonexistent']();
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function trigger_object_null_chained() {
|
|
278
|
+
hasError = did_error(() => {
|
|
279
|
+
// @ts-ignore
|
|
280
|
+
obj2?.['nonexistent']?.();
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
236
284
|
<button onClick={() => obj['increment']()}>{'Increment'}</button>
|
|
237
285
|
<button onClick={() => obj[SYMBOL_PROP]()}>{'Increment'}</button>
|
|
238
|
-
<button
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
// @ts-ignore
|
|
243
|
-
obj['nonexistent']();
|
|
244
|
-
} catch {
|
|
245
|
-
hasError = true;
|
|
246
|
-
}
|
|
247
|
-
}}
|
|
248
|
-
>
|
|
249
|
-
{'Nonexistent'}
|
|
250
|
-
</button>
|
|
251
|
-
<button
|
|
252
|
-
onClick={() => {
|
|
253
|
-
hasError = false;
|
|
254
|
-
try {
|
|
255
|
-
// @ts-ignore
|
|
256
|
-
obj['nonexistent']?.();
|
|
257
|
-
} catch {
|
|
258
|
-
hasError = true;
|
|
259
|
-
}
|
|
260
|
-
}}
|
|
261
|
-
>
|
|
262
|
-
{'Nonexistent chaining'}
|
|
263
|
-
</button>
|
|
264
|
-
<button
|
|
265
|
-
onClick={() => {
|
|
266
|
-
hasError = false;
|
|
267
|
-
try {
|
|
268
|
-
// @ts-ignore
|
|
269
|
-
obj2['nonexistent']();
|
|
270
|
-
} catch {
|
|
271
|
-
hasError = true;
|
|
272
|
-
}
|
|
273
|
-
}}
|
|
274
|
-
>
|
|
275
|
-
{'Object null'}
|
|
276
|
-
</button>
|
|
277
|
-
<button
|
|
278
|
-
onClick={() => {
|
|
279
|
-
hasError = false;
|
|
280
|
-
try {
|
|
281
|
-
// @ts-ignore
|
|
282
|
-
obj2?.['nonexistent']?.();
|
|
283
|
-
} catch {
|
|
284
|
-
hasError = true;
|
|
285
|
-
}
|
|
286
|
-
}}
|
|
287
|
-
>
|
|
288
|
-
{'Object null chained'}
|
|
289
|
-
</button>
|
|
286
|
+
<button onClick={trigger_nonexistent}>{'Nonexistent'}</button>
|
|
287
|
+
<button onClick={trigger_nonexistent_chaining}>{'Nonexistent chaining'}</button>
|
|
288
|
+
<button onClick={trigger_object_null}>{'Object null'}</button>
|
|
289
|
+
<button onClick={trigger_object_null_chained}>{'Object null chained'}</button>
|
|
290
290
|
<button onClick={() => obj.arr[obj.arr.length - 1]()}>{'BinaryExpression prop'}</button>
|
|
291
291
|
|
|
292
292
|
<span>{obj.count.value}</span>
|
|
@@ -346,21 +346,19 @@ describe('basic client > components & composition', () => {
|
|
|
346
346
|
<span>{'Hello from Span'}</span>
|
|
347
347
|
},
|
|
348
348
|
button: component({ children }: PropsWithChildren<{}>) {
|
|
349
|
-
<button>
|
|
350
|
-
<children />
|
|
351
|
-
</button>
|
|
349
|
+
<button>{children}</button>
|
|
352
350
|
},
|
|
353
351
|
};
|
|
354
352
|
|
|
355
353
|
component App() {
|
|
354
|
+
component children() {
|
|
355
|
+
<span>{'Click me!'}</span>
|
|
356
|
+
}
|
|
357
|
+
|
|
356
358
|
<div>
|
|
357
359
|
<h1>{'Component as Property Test'}</h1>
|
|
358
360
|
<UI.span />
|
|
359
|
-
<UI.button
|
|
360
|
-
component children() {
|
|
361
|
-
<span>{'Click me!'}</span>
|
|
362
|
-
}
|
|
363
|
-
</UI.button>
|
|
361
|
+
<UI.button {children} />
|
|
364
362
|
</div>
|
|
365
363
|
}
|
|
366
364
|
|
|
@@ -378,13 +376,13 @@ describe('basic client > components & composition', () => {
|
|
|
378
376
|
|
|
379
377
|
it('handles empty string children', () => {
|
|
380
378
|
component Button({ children }: PropsWithChildren<{}>) {
|
|
381
|
-
|
|
379
|
+
{children}
|
|
382
380
|
}
|
|
383
381
|
|
|
384
382
|
component App() {
|
|
385
|
-
let
|
|
383
|
+
let content = '';
|
|
386
384
|
<Button>{''}</Button>
|
|
387
|
-
<Button>{
|
|
385
|
+
<Button>{content}</Button>
|
|
388
386
|
}
|
|
389
387
|
|
|
390
388
|
expect(() => {
|