ripple 0.2.206 → 0.2.208

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.206",
6
+ "version": "0.2.208",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -76,7 +76,7 @@
76
76
  },
77
77
  "dependencies": {
78
78
  "@jridgewell/sourcemap-codec": "^1.5.5",
79
- "@sveltejs/acorn-typescript": "^1.0.6",
79
+ "@sveltejs/acorn-typescript": "^1.0.9",
80
80
  "acorn": "^8.15.0",
81
81
  "clsx": "^2.1.1",
82
82
  "devalue": "^5.3.2",
@@ -97,6 +97,6 @@
97
97
  "vscode-languageserver-types": "^3.17.5"
98
98
  },
99
99
  "peerDependencies": {
100
- "ripple": "0.2.206"
100
+ "ripple": "0.2.208"
101
101
  }
102
102
  }
@@ -104,30 +104,28 @@ function visit_function(node, context) {
104
104
  }
105
105
  }
106
106
 
107
- let body = context.visit(node.body, {
108
- ...state,
109
- // we are new context so tracking no longer applies
110
- metadata: { ...state.metadata, tracking: false },
111
- });
112
-
113
- if (metadata?.tracked === true) {
114
- const new_body = [];
115
-
116
- if (!is_inside_component(context, true) && is_component_level_function(context)) {
117
- new_body.push(b.var('__block', b.call('_$_.scope')));
118
- }
119
- if (body.type === 'BlockStatement') {
120
- new_body.push(...body.body);
121
- }
107
+ let body = /** @type {AST.BlockStatement | AST.Expression} */ (
108
+ context.visit(node.body, {
109
+ ...state,
110
+ // we are new context so tracking no longer applies
111
+ metadata: { ...state.metadata, tracking: false },
112
+ })
113
+ );
122
114
 
123
- return {
124
- ...node,
125
- params: node.params.map((param) => context.visit(param, state)),
126
- body: body.type === 'BlockStatement' ? { ...body, body: new_body } : body,
127
- };
115
+ if (
116
+ metadata?.tracked === true &&
117
+ !is_inside_component(context, true) &&
118
+ is_component_level_function(context) &&
119
+ body.type === 'BlockStatement'
120
+ ) {
121
+ body = { ...body, body: [b.var('__block', b.call('_$_.scope')), ...body.body] };
128
122
  }
129
123
 
130
- return context.next(state);
124
+ return {
125
+ ...node,
126
+ params: node.params.map((param) => context.visit(param, state)),
127
+ body,
128
+ };
131
129
  }
132
130
 
133
131
  /**
@@ -2896,12 +2894,20 @@ function transform_children(children, context) {
2896
2894
  : node.type == 'Text'
2897
2895
  ? state.scope.generate('text')
2898
2896
  : state.scope.generate('node'),
2897
+ /** @type {AST.NodeWithLocation} */ (node.type === 'Element' ? node.openingElement : node),
2899
2898
  );
2900
2899
  };
2901
2900
 
2902
2901
  /** @param {AST.Node} node */
2903
2902
  const create_initial = (node) => {
2904
- const id = is_fragment ? b.id(state.scope.generate('fragment')) : get_id(node);
2903
+ const id = is_fragment
2904
+ ? b.id(
2905
+ state.scope.generate('fragment'),
2906
+ /** @type {AST.NodeWithLocation} */ (
2907
+ node.type === 'Element' ? node.openingElement : node
2908
+ ),
2909
+ )
2910
+ : get_id(node);
2905
2911
  initial = id;
2906
2912
  template_id = state.scope.generate('root');
2907
2913
  state.init?.push(b.var(id, b.call(template_id)));
@@ -248,15 +248,35 @@ export function bindValue(maybe_tracked, set_func = undefined) {
248
248
  });
249
249
  } else {
250
250
  var input = /** @type {HTMLInputElement} */ (node);
251
- var selection_restore_needed = false;
252
251
 
253
252
  clear_event = on(input, 'input', () => {
254
- selection_restore_needed = true;
255
253
  /** @type {any} */
256
254
  var value = input.value;
257
255
  value = is_numberlike_input(input) ? to_number(value) : value;
258
- // the setter will schedule a microtask and the render block below will run
259
256
  setter(value);
257
+ const getter_value = getter();
258
+
259
+ // Check the getter to see if it's different from the input.value
260
+ // The setter may have decided not to update its track value or update it to something else
261
+ // We treat the getter as the source of truth since we cannot verify the change otherwise
262
+ // If getter() !== input.value, we set the input value right away
263
+ // the `render` block may be scheduled only if the tracked value has changed
264
+ // but it will not do anything if getter() === input.value
265
+ // The result is: the `render` block will ALWAYS exit early if the microtask
266
+ // came from this event handler
267
+ if (value !== getter_value) {
268
+ var start = input.selectionStart;
269
+ var end = input.selectionEnd;
270
+
271
+ input.value = getter_value ?? '';
272
+
273
+ if (end !== null && start !== null) {
274
+ end = Math.min(end, input.value.length);
275
+ start = Math.min(start, end);
276
+ input.selectionStart = start;
277
+ input.selectionEnd = end;
278
+ }
279
+ }
260
280
  });
261
281
 
262
282
  render(() => {
@@ -271,23 +291,9 @@ export function bindValue(maybe_tracked, set_func = undefined) {
271
291
  }
272
292
 
273
293
  if (value !== input.value) {
274
- if (selection_restore_needed) {
275
- var start = input.selectionStart;
276
- var end = input.selectionEnd;
277
-
278
- input.value = value ?? '';
279
-
280
- // Restore selection
281
- if (end !== null && start !== null) {
282
- end = Math.min(end, input.value.length);
283
- start = Math.min(start, end);
284
- input.selectionStart = start;
285
- input.selectionEnd = end;
286
- }
287
- selection_restore_needed = false;
288
- } else {
289
- input.value = value ?? '';
290
- }
294
+ // this can only get here if the tracked value was changed directly,
295
+ // and not via the input event
296
+ input.value = value ?? '';
291
297
  }
292
298
  });
293
299
 
@@ -31,7 +31,11 @@ function remove() {
31
31
  */
32
32
  function remove_when_css_loaded(callback) {
33
33
  /** @type {HTMLLinkElement[]} */
34
- const links = Array.from(document.querySelectorAll('link[rel="stylesheet"]'));
34
+ const links = Array.from(
35
+ /** @type {NodeListOf<HTMLLinkElement>} */ (
36
+ document.querySelectorAll('link[rel="stylesheet"]')
37
+ ),
38
+ );
35
39
  let remaining = links.length;
36
40
 
37
41
  if (remaining === 0) {
@@ -320,10 +320,14 @@ export function get(name, body) {
320
320
 
321
321
  /**
322
322
  * @param {string} name
323
+ * @param {AST.NodeWithLocation} [loc_info]
323
324
  * @returns {AST.Identifier}
324
325
  */
325
- export function id(name) {
326
- return { type: 'Identifier', name, metadata: { path: [] } };
326
+ export function id(name, loc_info) {
327
+ /** @type {AST.Identifier} */
328
+ const node = { type: 'Identifier', name, metadata: { path: [] } };
329
+
330
+ return set_location(node, loc_info);
327
331
  }
328
332
 
329
333
  /**
@@ -231,4 +231,47 @@ describe('basic client > rendering & text', () => {
231
231
  render(App);
232
232
  expect(container).toMatchSnapshot();
233
233
  });
234
+
235
+ it('should handle consecutive text nodes without duplication', () => {
236
+ component App() {
237
+ const Something = conditional('a');
238
+
239
+ <Something />
240
+
241
+ function conditional(item: 'a') {
242
+ let hello = 'Hello';
243
+ const obj = {
244
+ a: component() {
245
+ <div>
246
+ {'a'}
247
+ {' '}
248
+ {hello}
249
+ </div>
250
+ },
251
+ };
252
+
253
+ return obj[item];
254
+ }
255
+ }
256
+
257
+ render(App);
258
+ const div = container.querySelector('div');
259
+ expect(div.textContent).toEqual('a Hello');
260
+ });
261
+
262
+ it('should handle multiple consecutive text expressions', () => {
263
+ component App() {
264
+ let name = 'World';
265
+ <div>
266
+ {'Hello'}
267
+ {' '}
268
+ {name}
269
+ {'!'}
270
+ </div>
271
+ }
272
+
273
+ render(App);
274
+ const div = container.querySelector('div');
275
+ expect(div.textContent).toEqual('Hello World!');
276
+ });
234
277
  });