@sigx/lynx-runtime 0.4.5 → 0.4.7

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/dist/nodeOps.d.ts CHANGED
@@ -10,5 +10,13 @@ import type { RendererOptions } from '@sigx/runtime-core/internals';
10
10
  import { ShadowElement } from './shadow-element.js';
11
11
  export type LynxNode = ShadowElement;
12
12
  export type LynxElement = ShadowElement;
13
+ /**
14
+ * Test-only: clear the module-level per-element maps above. They are keyed by
15
+ * element id, so a test suite that recycles ids via `resetShadowState()`
16
+ * without this reset would resolve stale slots from earlier tests (and skip
17
+ * pushing SET_EVENT for "already registered" events). Production code never
18
+ * recycles ids, so this is never called outside tests.
19
+ */
20
+ export declare function resetNodeOpsState(): void;
13
21
  export declare function resolveClass(el: ShadowElement): string;
14
22
  export declare const nodeOps: RendererOptions<ShadowElement, ShadowElement>;
package/dist/nodeOps.js CHANGED
@@ -55,6 +55,18 @@ const elementEventSigns = new Map();
55
55
  // to bindEvent:tap), we keep one sign in __AddEvent and dispatch to all handlers from
56
56
  // a multi-handler wrapper in the BG registry.
57
57
  const nativeEventSlots = new Map();
58
+ /**
59
+ * Test-only: clear the module-level per-element maps above. They are keyed by
60
+ * element id, so a test suite that recycles ids via `resetShadowState()`
61
+ * without this reset would resolve stale slots from earlier tests (and skip
62
+ * pushing SET_EVENT for "already registered" events). Production code never
63
+ * recycles ids, so this is never called outside tests.
64
+ */
65
+ export function resetNodeOpsState() {
66
+ sentWorklets.clear();
67
+ elementEventSigns.clear();
68
+ nativeEventSlots.clear();
69
+ }
58
70
  // ---------------------------------------------------------------------------
59
71
  // Style normalisation — numeric values → 'Npx' (Lynx requires units)
60
72
  // ---------------------------------------------------------------------------
@@ -229,8 +241,19 @@ export const nodeOps = {
229
241
  }
230
242
  let slot = elSlots.get(nativeKey);
231
243
  if (!slot) {
244
+ // Record what the user typed so the `value` patch branch below can
245
+ // tell a model echo apart from a programmatic write (#143).
246
+ const trackInputValue = event.name === 'input'
247
+ && (el.type === 'input' || el.type === 'textarea');
232
248
  // First handler for this native event — register with Lynx.
233
249
  const sign = register((data) => {
250
+ if (trackInputValue) {
251
+ const v = data?.detail?.value;
252
+ // Normalize to a string ('' for nullish) — the `value` patch
253
+ // branch compares and stores the same representation, and
254
+ // setValue only ever pushes strings.
255
+ el._lastInputValue = v == null ? '' : String(v);
256
+ }
234
257
  // Dispatch to all handlers registered for this slot.
235
258
  const s = elSlots.get(nativeKey);
236
259
  if (s) {
@@ -293,6 +316,33 @@ export const nodeOps = {
293
316
  else if (key === 'id') {
294
317
  pushOp(OP.SET_ID, el.id, nextValue);
295
318
  }
319
+ else if (key === 'value' && (el.type === 'input' || el.type === 'textarea')) {
320
+ pushOp(OP.SET_PROP, el.id, key, nextValue);
321
+ // The native field treats the `value` attribute as initial-only once
322
+ // the user has edited it — programmatic writes (clear-on-send, editor
323
+ // toolbar inserts) must additionally go through the element's
324
+ // `setValue` UI method or the visible text never changes (#143).
325
+ // Skip during initial mount (`el.parent == null`: props are patched
326
+ // before insertion, and the attribute covers the initial value) — NOT
327
+ // by `_prevValue == null`, which would also skip legitimate post-mount
328
+ // transitions like value={null} → value={'text'}. Also skip the model
329
+ // echo (the re-render caused by the user's own typing, where the new
330
+ // value is exactly what the input event just reported) so cursor/IME
331
+ // composition isn't disturbed while typing.
332
+ // Normalize to a string ('' for nullish) on BOTH sides of the
333
+ // comparison — `value` is typed string on input/textarea but user code
334
+ // can write a number/boolean by mistake, and setValue is a native
335
+ // text-field method that expects a string. Any other representation
336
+ // would desync `_lastInputValue` from the native field and re-invoke
337
+ // redundantly on later renders.
338
+ const next = nextValue == null ? '' : String(nextValue);
339
+ if (el.parent != null && next !== el._lastInputValue) {
340
+ pushOp(OP.INVOKE_UI_METHOD, el.id, 'setValue', { value: next });
341
+ // The programmatic write replaces whatever the user had typed; track
342
+ // it so the next echo comparison stays correct.
343
+ el._lastInputValue = next;
344
+ }
345
+ }
296
346
  else {
297
347
  pushOp(OP.SET_PROP, el.id, key, nextValue);
298
348
  }
@@ -19,6 +19,7 @@ export declare class ShadowElement {
19
19
  _vShowHidden: boolean;
20
20
  _baseClass: string;
21
21
  _transitionClasses: Set<string>;
22
+ _lastInputValue: string | undefined;
22
23
  constructor(type: string, forceId?: number);
23
24
  insertBefore(child: ShadowElement, anchor: ShadowElement | null): void;
24
25
  removeChild(child: ShadowElement): void;
@@ -23,6 +23,16 @@ export class ShadowElement {
23
23
  // Class management for Transition support.
24
24
  _baseClass = '';
25
25
  _transitionClasses = new Set();
26
+ // Last text known to be in the native <input>/<textarea>: recorded from the
27
+ // native input event by nodeOps, and updated by post-mount programmatic
28
+ // writes that go through the setValue UI method. Used to tell a model echo
29
+ // (signal update caused by typing) apart from a programmatic write
30
+ // (clear-on-send, toolbar insert) — only the latter must be pushed back to
31
+ // the native field. Always stored as a string ('' for nullish, matching
32
+ // what setValue pushes); `undefined` until the first input event or
33
+ // setValue-pushing write (the first-render `value` attribute does NOT
34
+ // initialize it).
35
+ _lastInputValue = undefined;
26
36
  constructor(type, forceId) {
27
37
  this.id = forceId !== undefined ? forceId : ShadowElement.nextId++;
28
38
  this.type = type;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sigx/lynx-runtime",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "description": "Lynx renderer for SignalX (background thread)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -41,7 +41,7 @@
41
41
  "dependencies": {
42
42
  "@sigx/runtime-core": "^0.4.8",
43
43
  "@sigx/reactivity": "^0.4.8",
44
- "@sigx/lynx-runtime-internal": "^0.4.5"
44
+ "@sigx/lynx-runtime-internal": "^0.4.7"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "@lynx-js/types": "*"