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.
@@ -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 { value, checked } from 'ripple/bindings';
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
- console.log('text changed', @text);
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 value(text)} />
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
- console.log('checked changed', @value);
78
+ logs.push('checked changed', @value);
50
79
  });
51
80
 
52
- <input type="checkbox" {ref checked(value)} />
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('input'));
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
- export type Tracked<V> = { '#v': V };
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
- export declare function track<V>(value?: V | (() => V), get?: (v: V) => V, set?: (next: V, prev: V) => V): Tracked<V>;
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;
@@ -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;
@@ -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
- }