ripple 0.2.113 → 0.2.115
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 -6
- package/src/compiler/phases/1-parse/index.js +1 -0
- package/src/compiler/phases/2-analyze/index.js +17 -0
- package/src/compiler/phases/3-transform/client/index.js +48 -6
- package/src/compiler/phases/3-transform/segments.js +164 -10
- package/src/runtime/index-client.js +185 -5
- package/src/runtime/internal/client/runtime.js +4 -4
- package/src/utils/builders.js +274 -262
- package/tests/client/input-value.test.ripple +66 -6
- package/types/index.d.ts +33 -2
- package/src/bindings/index.d.ts +0 -13
- package/src/bindings/index.js +0 -79
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
2
|
import { mount, flushSync, track, effect } from 'ripple';
|
|
3
|
-
import {
|
|
3
|
+
import { bindValue, bindChecked } from 'ripple';
|
|
4
4
|
|
|
5
5
|
describe('use value()', () => {
|
|
6
6
|
let container;
|
|
@@ -22,42 +22,102 @@ describe('use value()', () => {
|
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
it('should update value on input', () => {
|
|
25
|
+
const logs = [];
|
|
26
|
+
|
|
25
27
|
component App() {
|
|
26
28
|
const text = track('');
|
|
27
29
|
|
|
28
30
|
effect(() => {
|
|
29
|
-
|
|
31
|
+
logs.push('text changed', @text);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
<input type="text" {ref bindValue(text)} />
|
|
35
|
+
}
|
|
36
|
+
render(App);
|
|
37
|
+
flushSync();
|
|
38
|
+
|
|
39
|
+
const input = container.querySelector('input');
|
|
40
|
+
input.value = 'Hello';
|
|
41
|
+
input.dispatchEvent(new Event('input'));
|
|
42
|
+
flushSync();
|
|
43
|
+
expect(input.value).toBe('Hello');
|
|
44
|
+
expect(logs).toEqual(['text changed', '', 'text changed', 'Hello']);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should update value on input with a predefined value', () => {
|
|
48
|
+
const logs = [];
|
|
49
|
+
|
|
50
|
+
component App() {
|
|
51
|
+
const text = track('foo');
|
|
52
|
+
|
|
53
|
+
effect(() => {
|
|
54
|
+
logs.push('text changed', @text);
|
|
30
55
|
});
|
|
31
56
|
|
|
32
|
-
<input type="text" {ref
|
|
57
|
+
<input type="text" {ref bindValue(text)} />
|
|
33
58
|
}
|
|
34
59
|
render(App);
|
|
35
60
|
flushSync();
|
|
36
61
|
|
|
62
|
+
expect(container.querySelector('input').value).toBe('foo');
|
|
37
63
|
const input = container.querySelector('input');
|
|
38
64
|
input.value = 'Hello';
|
|
39
65
|
input.dispatchEvent(new Event('input'));
|
|
40
66
|
flushSync();
|
|
41
67
|
expect(input.value).toBe('Hello');
|
|
68
|
+
expect(logs).toEqual(['text changed', 'foo', 'text changed', 'Hello']);
|
|
42
69
|
});
|
|
43
70
|
|
|
44
71
|
it('should update checked on input', () => {
|
|
72
|
+
const logs = [];
|
|
73
|
+
|
|
45
74
|
component App() {
|
|
46
75
|
const value = track(false);
|
|
47
76
|
|
|
48
77
|
effect(() => {
|
|
49
|
-
|
|
78
|
+
logs.push('checked changed', @value);
|
|
50
79
|
});
|
|
51
80
|
|
|
52
|
-
<input type="checkbox" {ref
|
|
81
|
+
<input type="checkbox" {ref bindChecked(value)} />
|
|
53
82
|
}
|
|
54
83
|
render(App);
|
|
55
84
|
flushSync();
|
|
56
85
|
|
|
57
86
|
const input = container.querySelector('input');
|
|
58
87
|
input.checked = true;
|
|
59
|
-
input.dispatchEvent(new Event('
|
|
88
|
+
input.dispatchEvent(new Event('change'));
|
|
60
89
|
flushSync();
|
|
90
|
+
|
|
61
91
|
expect(input.checked).toBe(true);
|
|
92
|
+
expect(logs).toEqual(['checked changed', false, 'checked changed', true]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should update select value on change', () => {
|
|
96
|
+
const logs = [];
|
|
97
|
+
|
|
98
|
+
component App() {
|
|
99
|
+
const select = track('2');
|
|
100
|
+
|
|
101
|
+
effect(() => {
|
|
102
|
+
logs.push('select changed', @select);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
<select {ref bindValue(select)}>
|
|
106
|
+
<option value="1">{"One"}</option>
|
|
107
|
+
<option value="2">{"Two"}</option>
|
|
108
|
+
<option value="3">{"Three"}</option>
|
|
109
|
+
</select>
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
render(App);
|
|
113
|
+
flushSync();
|
|
114
|
+
|
|
115
|
+
const select = container.querySelector('select');
|
|
116
|
+
select.value = '3';
|
|
117
|
+
select.dispatchEvent(new Event('change'));
|
|
118
|
+
flushSync();
|
|
119
|
+
|
|
120
|
+
expect(select.value).toBe('3');
|
|
121
|
+
expect(logs).toEqual(['select changed', '2', 'select changed', '3']);
|
|
62
122
|
});
|
|
63
123
|
});
|
package/types/index.d.ts
CHANGED
|
@@ -73,7 +73,23 @@ declare global {
|
|
|
73
73
|
|
|
74
74
|
export declare function createRefKey(): symbol;
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
// Base Tracked interface - all tracked values have a '#v' property containing the actual value
|
|
77
|
+
export interface Tracked<V> { '#v': V; }
|
|
78
|
+
|
|
79
|
+
// Augment Tracked to be callable when V is a Component
|
|
80
|
+
// This allows <@Something /> to work in JSX when Something is Tracked<Component>
|
|
81
|
+
export interface Tracked<V> {
|
|
82
|
+
(props: V extends Component<infer P> ? P : never): V extends Component ? void : never;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Helper type to infer component type from a function that returns a component
|
|
86
|
+
// If T is a function returning a Component, extract the Component type itself, not the return type (void)
|
|
87
|
+
export type InferComponent<T> =
|
|
88
|
+
T extends () => infer R
|
|
89
|
+
? R extends Component<any>
|
|
90
|
+
? R
|
|
91
|
+
: T
|
|
92
|
+
: T;
|
|
77
93
|
|
|
78
94
|
export type Props<K extends PropertyKey = any, V = unknown> = Record<K, V>;
|
|
79
95
|
export type PropsWithExtras<T extends object> = Props & T & Record<string, unknown>;
|
|
@@ -90,7 +106,10 @@ type RestKeys<T, K extends readonly (keyof T)[]> = Expand<Omit<T, K[number]>>;
|
|
|
90
106
|
type SplitResult<T extends Props, K extends readonly (keyof T)[]> =
|
|
91
107
|
[...PickKeys<T, K>, Tracked<RestKeys<T, K>>];
|
|
92
108
|
|
|
93
|
-
|
|
109
|
+
// Overload for function values - infers the return type of the function
|
|
110
|
+
export declare function track<V>(value: () => V, get?: (v: InferComponent<V>) => InferComponent<V>, set?: (next: InferComponent<V>, prev: InferComponent<V>) => InferComponent<V>): Tracked<InferComponent<V>>;
|
|
111
|
+
// Overload for non-function values
|
|
112
|
+
export declare function track<V>(value?: V, get?: (v: V) => V, set?: (next: V, prev: V) => V): Tracked<V>;
|
|
94
113
|
|
|
95
114
|
export declare function trackSplit<V extends Props, const K extends readonly (keyof V)[]>(
|
|
96
115
|
value: V,
|
|
@@ -200,3 +219,15 @@ export declare const MediaQuery: {
|
|
|
200
219
|
};
|
|
201
220
|
|
|
202
221
|
export function Portal<V = HTMLElement>({ target, children: Component }: { target: V, children?: Component }): void;
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @param {Tracked<V>} tracked
|
|
225
|
+
* @returns {(node: HTMLInputElement | HTMLSelectElement) => void}
|
|
226
|
+
*/
|
|
227
|
+
export declare function bindValue<V>(tracked: Tracked<V>): (node: HTMLInputElement | HTMLSelectElement) => void;
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* @param {Tracked<V>} tracked
|
|
231
|
+
* @returns {(node: HTMLInputElement) => void}
|
|
232
|
+
*/
|
|
233
|
+
export declare function bindChecked<V>(tracked: Tracked<V>): (node: HTMLInputElement) => void;
|
package/src/bindings/index.d.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import type { Tracked } from "ripple";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @param {Tracked<V>} tracked
|
|
5
|
-
* @returns {(node: HTMLInputElement) => void}
|
|
6
|
-
*/
|
|
7
|
-
export declare function value<V>(tracked: Tracked<V>): (node: HTMLInputElement) => void;
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* @param {Tracked<V>} tracked
|
|
11
|
-
* @returns {(node: HTMLInputElement) => void}
|
|
12
|
-
*/
|
|
13
|
-
export declare function checked<V>(tracked: Tracked<V>): (node: HTMLInputElement) => void;
|
package/src/bindings/index.js
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
/** @import {Block, Tracked} from '#client' */
|
|
2
|
-
|
|
3
|
-
import { active_block, get, set, tick } from '../runtime/internal/client';
|
|
4
|
-
import { on } from '../runtime/internal/client/events';
|
|
5
|
-
import { is_tracked_object } from '../runtime/internal/client/utils';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* @param {string} value
|
|
9
|
-
*/
|
|
10
|
-
function to_number(value) {
|
|
11
|
-
return value === '' ? null : +value;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* @param {HTMLInputElement} input
|
|
16
|
-
*/
|
|
17
|
-
function is_numberlike_input(input) {
|
|
18
|
-
var type = input.type;
|
|
19
|
-
return type === 'number' || type === 'range';
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* @param {unknown} maybe_tracked
|
|
24
|
-
* @returns {(node: HTMLInputElement) => void}
|
|
25
|
-
*/
|
|
26
|
-
export function value(maybe_tracked) {
|
|
27
|
-
if (!is_tracked_object(maybe_tracked)) {
|
|
28
|
-
throw new TypeError('value() argument is not a tracked object');
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const block = /** @type {Block} */ (active_block);
|
|
32
|
-
const tracked = /** @type {Tracked} */ (maybe_tracked);
|
|
33
|
-
|
|
34
|
-
return (input) => {
|
|
35
|
-
const clear_event = on(input, 'input', async () => {
|
|
36
|
-
/** @type {any} */
|
|
37
|
-
var value = input.value;
|
|
38
|
-
value = is_numberlike_input(input) ? to_number(value) : value;
|
|
39
|
-
set(tracked, value, block);
|
|
40
|
-
|
|
41
|
-
await tick();
|
|
42
|
-
|
|
43
|
-
if (value !== (value = get(tracked))) {
|
|
44
|
-
var start = input.selectionStart;
|
|
45
|
-
var end = input.selectionEnd;
|
|
46
|
-
input.value = value ?? '';
|
|
47
|
-
|
|
48
|
-
// Restore selection
|
|
49
|
-
if (end !== null) {
|
|
50
|
-
input.selectionStart = start;
|
|
51
|
-
input.selectionEnd = Math.min(end, input.value.length);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
return clear_event;
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* @param {unknown} maybe_tracked
|
|
62
|
-
* @returns {(node: HTMLInputElement) => void}
|
|
63
|
-
*/
|
|
64
|
-
export function checked(maybe_tracked) {
|
|
65
|
-
if (!is_tracked_object(maybe_tracked)) {
|
|
66
|
-
throw new TypeError('checked() argument is not a tracked object');
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const block = /** @type {any} */ (active_block);
|
|
70
|
-
const tracked = /** @type {Tracked<any>} */ (maybe_tracked);
|
|
71
|
-
|
|
72
|
-
return (input) => {
|
|
73
|
-
const clear_event = on(input, 'change', () => {
|
|
74
|
-
set(tracked, input.checked, block);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
return clear_event;
|
|
78
|
-
};
|
|
79
|
-
}
|