ripple 0.2.4 → 0.2.6

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.
@@ -8,7 +8,7 @@ import {
8
8
  PAUSED,
9
9
  RENDER_BLOCK,
10
10
  ROOT_BLOCK,
11
- TRY_BLOCK
11
+ TRY_BLOCK,
12
12
  } from './constants';
13
13
  import { next_sibling } from './operations';
14
14
  import { apply_element_spread } from './render';
@@ -18,13 +18,15 @@ import {
18
18
  active_reaction,
19
19
  run_block,
20
20
  run_teardown,
21
- schedule_update
21
+ schedule_update,
22
22
  } from './runtime';
23
23
  import { suspend } from './try';
24
24
 
25
25
  export function user_effect(fn) {
26
26
  if (active_block === null) {
27
- throw new Error('effect() must be called within an active context, such as a component or effect');
27
+ throw new Error(
28
+ 'effect() must be called within an active context, such as a component or effect'
29
+ );
28
30
  }
29
31
 
30
32
  var component = active_component;
@@ -33,7 +35,7 @@ export function user_effect(fn) {
33
35
  e.push({
34
36
  b: active_block,
35
37
  fn,
36
- r: active_reaction
38
+ r: active_reaction,
37
39
  });
38
40
 
39
41
  return;
@@ -66,6 +68,26 @@ export function async(fn) {
66
68
  });
67
69
  }
68
70
 
71
+ export function use(element, get_fn) {
72
+ var fn = undefined;
73
+ var e;
74
+
75
+ return block(RENDER_BLOCK, () => {
76
+ if (fn !== (fn = get_fn())) {
77
+ if (e) {
78
+ destroy_block(e);
79
+ e = null;
80
+ }
81
+
82
+ if (fn) {
83
+ e = branch(() => {
84
+ effect(() => fn(element));
85
+ });
86
+ }
87
+ }
88
+ });
89
+ }
90
+
69
91
  export function root(fn) {
70
92
  return block(ROOT_BLOCK, fn);
71
93
  }
@@ -97,7 +119,7 @@ export function block(flags, fn, state = null) {
97
119
  p: active_block,
98
120
  prev: null,
99
121
  s: state,
100
- t: null
122
+ t: null,
101
123
  };
102
124
 
103
125
  if (active_reaction !== null && (active_reaction.f & COMPUTED) !== 0) {
@@ -22,3 +22,4 @@ export var UNINITIALIZED = Symbol();
22
22
  export var TRACKED_OBJECT = Symbol();
23
23
  export var SPREAD_OBJECT = Symbol();
24
24
  export var COMPUTED_PROPERTY = Symbol();
25
+ export var USE_PROP = '@use';
@@ -9,7 +9,7 @@ export {
9
9
  set_selected,
10
10
  } from './render.js';
11
11
 
12
- export { render, render_spread, async } from './blocks.js';
12
+ export { render, render_spread, async, use } from './blocks.js';
13
13
 
14
14
  export { event, delegate } from './events.js';
15
15
 
@@ -39,7 +39,8 @@ export {
39
39
  structured_clone,
40
40
  push_component,
41
41
  pop_component,
42
- untrack
42
+ untrack,
43
+ use_prop,
43
44
  } from './runtime.js';
44
45
 
45
46
  export { for_block as for } from './for.js';
@@ -49,5 +50,3 @@ export { if_block as if } from './if.js';
49
50
  export { try_block as try, resume_context, aborted } from './try.js';
50
51
 
51
52
  export { template, append } from './template.js';
52
-
53
-
@@ -1,5 +1,6 @@
1
- import { effect } from './blocks';
2
- import { get_descriptors, get_prototype_of } from './utils';
1
+ import { destroy_block, use } from './blocks';
2
+ import { USE_PROP } from './constants';
3
+ import { get_descriptors, get_own_property_symbols, get_prototype_of } from './utils';
3
4
 
4
5
  export function set_text(text, value) {
5
6
  // For objects, we apply string coercion (which might make things like $state array references in the template reactive) before diffing
@@ -62,6 +63,8 @@ export function set_attribute(element, attribute, value) {
62
63
 
63
64
  export function set_attributes(element, attributes) {
64
65
  for (const key in attributes) {
66
+ if (key === '$children') continue;
67
+
65
68
  let value = attributes[key];
66
69
 
67
70
  if (key === 'class') {
@@ -158,7 +161,33 @@ export function set_selected(element, selected) {
158
161
  }
159
162
 
160
163
  export function apply_element_spread(element, fn) {
164
+ var prev;
165
+ var effects = {};
166
+
161
167
  return () => {
162
- set_attributes(element, fn());
168
+ var next = fn();
169
+
170
+ for (let symbol of get_own_property_symbols(effects)) {
171
+ if (!next[symbol]) {
172
+ destroy_block(effects[symbol]);
173
+ }
174
+ }
175
+
176
+ for (const symbol of get_own_property_symbols(next)) {
177
+ var use_fn = next[symbol];
178
+
179
+ if (symbol.description === USE_PROP && (!prev || use_fn !== prev[symbol])) {
180
+ if (effects[symbol]) {
181
+ destroy_block(effects[symbol]);
182
+ }
183
+ effects[symbol] = use(element, use_fn);
184
+ }
185
+
186
+ next[symbol] = use_fn;
187
+ }
188
+
189
+ set_attributes(element, next);
190
+
191
+ prev = next;
163
192
  };
164
193
  }
@@ -22,7 +22,8 @@ import {
22
22
  TRACKED,
23
23
  TRACKED_OBJECT,
24
24
  TRY_BLOCK,
25
- UNINITIALIZED
25
+ UNINITIALIZED,
26
+ USE_PROP
26
27
  } from './constants';
27
28
  import { capture, suspend } from './try.js';
28
29
  import { define_property, is_array } from './utils';
@@ -923,3 +924,7 @@ export function pop_component() {
923
924
  }
924
925
  active_component = component.p;
925
926
  }
927
+
928
+ export function use_prop() {
929
+ return Symbol(USE_PROP);
930
+ }
@@ -7,6 +7,7 @@ export var get_prototype_of = Object.getPrototypeOf;
7
7
  export var object_values = Object.values;
8
8
  export var object_entries = Object.entries;
9
9
  export var object_keys = Object.keys;
10
+ export var get_own_property_symbols = Object.getOwnPropertySymbols;
10
11
  export var structured_clone = structuredClone;
11
12
 
12
13
  export function create_anchor() {
@@ -716,6 +716,17 @@ export function jsx_id(name) {
716
716
  };
717
717
  }
718
718
 
719
+ /**
720
+ * @param {ESTree.Expression} argument
721
+ * @returns {ESTree.JSXSpreadAttribute}
722
+ */
723
+ export function jsx_spread_attribute(argument) {
724
+ return {
725
+ type: 'JSXSpreadAttribute',
726
+ argument
727
+ };
728
+ }
729
+
719
730
  export {
720
731
  await_builder as await,
721
732
  let_builder as let,
@@ -0,0 +1,66 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`basic > render dynamic text 1`] = `
4
+ <div>
5
+ <button>
6
+ Change Text
7
+ </button>
8
+ <div>
9
+ Hello World
10
+ </div>
11
+
12
+ </div>
13
+ `;
14
+
15
+ exports[`basic > render semi-dynamic text 1`] = `
16
+ <div>
17
+ <div>
18
+ Hello World
19
+ </div>
20
+
21
+ </div>
22
+ `;
23
+
24
+ exports[`basic > render static attributes 1`] = `
25
+ <div>
26
+ <div
27
+ class="foo"
28
+ id="bar"
29
+ style="color: red;"
30
+ >
31
+ Hello World
32
+ </div>
33
+
34
+ </div>
35
+ `;
36
+
37
+ exports[`basic > render static elements 1`] = `
38
+ <div>
39
+ <div
40
+ class="foo"
41
+ id="bar"
42
+ style="color: red;"
43
+ >
44
+ Hello World
45
+ </div>
46
+
47
+ </div>
48
+ `;
49
+
50
+ exports[`basic > render static text 1`] = `
51
+ <div>
52
+ <div>
53
+ Hello World
54
+ </div>
55
+
56
+ </div>
57
+ `;
58
+
59
+ exports[`basic > renders correctly 1`] = `
60
+ <div>
61
+ <div>
62
+ Hello World
63
+ </div>
64
+
65
+ </div>
66
+ `;
@@ -0,0 +1,273 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mount, flushSync } from 'ripple';
3
+
4
+ describe('basic', () => {
5
+ let container;
6
+
7
+ function render(component) {
8
+ mount(component, { target: container });
9
+ }
10
+
11
+ beforeEach(() => {
12
+ container = document.createElement('div');
13
+ document.body.appendChild(container);
14
+ });
15
+
16
+ afterEach(() => {
17
+ document.body.removeChild(container);
18
+ container = null;
19
+ });
20
+
21
+ it('render static text', () => {
22
+ component Basic() {
23
+ <div>{"Hello World"}</div>
24
+ }
25
+
26
+ render(Basic);
27
+
28
+ expect(container).toMatchSnapshot();
29
+ });
30
+
31
+ it('render static attributes', () => {
32
+ component Basic() {
33
+ <div class="foo" id="bar" style="color: red;">{"Hello World"}</div>
34
+ }
35
+
36
+ render(Basic);
37
+
38
+ expect(container).toMatchSnapshot();
39
+ });
40
+
41
+ it('render semi-dynamic text', () => {
42
+ component Basic() {
43
+ let text = 'Hello World';
44
+ <div>{text}</div>
45
+ }
46
+
47
+ render(Basic);
48
+
49
+ expect(container).toMatchSnapshot();
50
+ });
51
+
52
+ it('render dynamic text', () => {
53
+ component Basic() {
54
+ let $text = 'Hello World';
55
+
56
+ <button onClick={() => $text = 'Hello Ripple'}>{"Change Text"}</button>
57
+
58
+ <div>{$text}</div>
59
+ }
60
+
61
+ render(Basic);
62
+
63
+ const button = container.querySelector('button');
64
+
65
+ button.click();
66
+ flushSync();
67
+
68
+ expect(container.querySelector('div').textContent).toEqual('Hello Ripple');
69
+ });
70
+
71
+ it('render dynamic class attribute', () => {
72
+ component Basic() {
73
+ let $active = false;
74
+
75
+ <button onClick={() => $active = !$active}>{"Toggle"}</button>
76
+
77
+ <div $class={$active ? 'active' : 'inactive'}>{"Dynamic Class"}</div>
78
+ }
79
+
80
+ render(Basic);
81
+
82
+ const button = container.querySelector('button');
83
+ const div = container.querySelector('div');
84
+
85
+ expect(div.className).toBe('inactive');
86
+
87
+ button.click();
88
+ flushSync();
89
+
90
+ expect(div.className).toBe('active');
91
+
92
+ button.click();
93
+ flushSync();
94
+
95
+ expect(div.className).toBe('inactive');
96
+ });
97
+
98
+ it('render dynamic id attribute', () => {
99
+ component Basic() {
100
+ let $count = 0;
101
+
102
+ <button onClick={() => $count++}>{"Increment"}</button>
103
+
104
+ <div $id={`item-${$count}`}>{"Dynamic ID"}</div>
105
+ }
106
+
107
+ render(Basic);
108
+
109
+ const button = container.querySelector('button');
110
+ const div = container.querySelector('div');
111
+
112
+ expect(div.id).toBe('item-0');
113
+
114
+ button.click();
115
+ flushSync();
116
+
117
+ expect(div.id).toBe('item-1');
118
+
119
+ button.click();
120
+ flushSync();
121
+
122
+ expect(div.id).toBe('item-2');
123
+ });
124
+
125
+ it('render dynamic style attribute', () => {
126
+ component Basic() {
127
+ let $color = 'red';
128
+
129
+ <button onClick={() => $color = $color === 'red' ? 'blue' : 'red'}>{"Change Color"}</button>
130
+
131
+ <div $style={`color: ${$color}; font-weight: bold;`}>{"Dynamic Style"}</div>
132
+ }
133
+
134
+ render(Basic);
135
+
136
+ const button = container.querySelector('button');
137
+ const div = container.querySelector('div');
138
+
139
+ expect(div.style.color).toBe('red');
140
+ expect(div.style.fontWeight).toBe('bold');
141
+
142
+ button.click();
143
+ flushSync();
144
+
145
+ expect(div.style.color).toBe('blue');
146
+ expect(div.style.fontWeight).toBe('bold');
147
+ });
148
+
149
+ it('render dynamic boolean attributes', () => {
150
+ component Basic() {
151
+ let $disabled = false;
152
+ let $checked = false;
153
+
154
+ <button onClick={() => { $disabled = !$disabled; $checked = !$checked; }}>{"Toggle"}</button>
155
+
156
+ <input type="checkbox" $disabled={$disabled} $checked={$checked} />
157
+ }
158
+
159
+ render(Basic);
160
+
161
+ const button = container.querySelector('button');
162
+ const input = container.querySelector('input');
163
+
164
+ expect(input.disabled).toBe(false);
165
+ expect(input.checked).toBe(false);
166
+
167
+ button.click();
168
+ flushSync();
169
+
170
+ expect(input.disabled).toBe(true);
171
+ expect(input.checked).toBe(true);
172
+ });
173
+
174
+ it('render multiple dynamic attributes', () => {
175
+ component Basic() {
176
+ let $theme = 'light';
177
+ let $size = 'medium';
178
+
179
+ <button onClick={() => {
180
+ $theme = $theme === 'light' ? 'dark' : 'light';
181
+ $size = $size === 'medium' ? 'large' : 'medium';
182
+ }}>{"Toggle Theme & Size"}</button>
183
+
184
+ <div
185
+ $class={`theme-${$theme} size-${$size}`}
186
+ $data-theme={$theme}
187
+ $data-size={$size}
188
+ >
189
+ {"Multiple Dynamic Attributes"}
190
+ </div>
191
+ }
192
+
193
+ render(Basic);
194
+
195
+ const button = container.querySelector('button');
196
+ const div = container.querySelector('div');
197
+
198
+ expect(div.className).toBe('theme-light size-medium');
199
+ expect(div.getAttribute('data-theme')).toBe('light');
200
+ expect(div.getAttribute('data-size')).toBe('medium');
201
+
202
+ button.click();
203
+ flushSync();
204
+
205
+ expect(div.className).toBe('theme-dark size-large');
206
+ expect(div.getAttribute('data-theme')).toBe('dark');
207
+ expect(div.getAttribute('data-size')).toBe('large');
208
+ });
209
+
210
+ it('render conditional attributes', () => {
211
+ component Basic() {
212
+ let $showTitle = false;
213
+ let $showAria = false;
214
+
215
+ <button onClick={() => { $showTitle = !$showTitle; $showAria = !$showAria; }}>{"Toggle Attributes"}</button>
216
+
217
+ <div
218
+ $title={$showTitle ? 'This is a title' : null}
219
+ $aria-label={$showAria ? 'Accessible label' : null}
220
+ >
221
+ {"Conditional Attributes"}
222
+ </div>
223
+ }
224
+
225
+ render(Basic);
226
+
227
+ const button = container.querySelector('button');
228
+ const div = container.querySelector('div');
229
+
230
+ expect(div.hasAttribute('title')).toBe(false);
231
+ expect(div.hasAttribute('aria-label')).toBe(false);
232
+
233
+ button.click();
234
+ flushSync();
235
+
236
+ expect(div.getAttribute('title')).toBe('This is a title');
237
+ expect(div.getAttribute('aria-label')).toBe('Accessible label');
238
+
239
+ button.click();
240
+ flushSync();
241
+
242
+ expect(div.hasAttribute('title')).toBe(false);
243
+ expect(div.hasAttribute('aria-label')).toBe(false);
244
+ });
245
+
246
+ it('render spread attributes', () => {
247
+ component Basic() {
248
+ let $attrs = { class: 'initial', id: 'test-1' };
249
+
250
+ <button onClick={() => {
251
+ $attrs = { class: 'updated', id: 'test-2', 'data-extra': 'value' };
252
+ }}>{"Update Attributes"}</button>
253
+
254
+ <div {...$attrs}>{"Spread Attributes"}</div>
255
+ }
256
+
257
+ render(Basic);
258
+
259
+ const button = container.querySelector('button');
260
+ const div = container.querySelector('div');
261
+
262
+ expect(div.className).toBe('initial');
263
+ expect(div.id).toBe('test-1');
264
+ expect(div.hasAttribute('data-extra')).toBe(false);
265
+
266
+ button.click();
267
+ flushSync();
268
+
269
+ expect(div.className).toBe('updated');
270
+ expect(div.id).toBe('test-2');
271
+ expect(div.getAttribute('data-extra')).toBe('value');
272
+ });
273
+ });
@@ -0,0 +1,32 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mount, flushSync } from 'ripple';
3
+
4
+ describe('@use element decorators', () => {
5
+ let container;
6
+
7
+ function render(component) {
8
+ mount(component, { target: container });
9
+ }
10
+
11
+ beforeEach(() => {
12
+ container = document.createElement('div');
13
+ document.body.appendChild(container);
14
+ });
15
+
16
+ afterEach(() => {
17
+ document.body.removeChild(container);
18
+ container = null;
19
+ });
20
+
21
+ it('capture a <div>', () => {
22
+ let div;
23
+
24
+ component Component() {
25
+ <div {@use (node) => { div = node; }}>{"Hello World"}</div>
26
+ }
27
+
28
+ render(Component);
29
+
30
+ expect(div.textContent).toBe('Hello World');
31
+ });
32
+ });