ngx-json-render 0.5.0 → 0.5.1

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/README.md CHANGED
@@ -594,6 +594,18 @@ const bound = injectBoundProp<string>(() => ctx.props().value, () => ctx.binding
594
594
 
595
595
  Share one store across renderers (or drive it from your own state management) by passing a core `StateStore` — `createStateStore()`, or `createStoreAdapter()` over Redux/NgRx/etc. — via the `store` input. `createStoreSetState(store)` adapts a whole-state updater to fine-grained path writes.
596
596
 
597
+ ### What a state write re-renders
598
+
599
+ A write reaches only the components whose resolved props actually changed. Writing `/user/name` re-runs the template of the text that shows it, not the rest of the tree. So `ctx.props()` and `ctx.element()` are the signals to read: a template that reads anything else, such as a mutable object or `Date.now()`, is no longer refreshed by unrelated writes.
600
+
601
+ "Changed" is decided per prop value:
602
+
603
+ - Primitives compare by value.
604
+ - Objects and arrays compare by reference with the built-in store, which copies every path it writes.
605
+ - With an external `store`, objects and arrays always count as changed, because such a store may write into its snapshot in place.
606
+
607
+ Elements with a `$bindState` or `$bindItem` prop still re-run on every write. Their DOM can hold the user's input before state does, and the note below depends on them getting the chance to put it right.
608
+
597
609
  ### A note on inputs
598
610
 
599
611
  If a catalog component renders `<input [value]="ctx.props().value">`, remember that one-way bindings do not re-assert the DOM when the bound value returns to its previously applied value while the user typed in between (e.g. `pushState` + `clearStatePath`). Sync imperatively instead — see `InputComponent` in the demo app for the pattern.
@@ -1091,6 +1091,32 @@ function repeatsItself(path) {
1091
1091
  }
1092
1092
  return false;
1093
1093
  }
1094
+ /**
1095
+ * Whether two objects hold the same values under the same keys.
1096
+ *
1097
+ * `objectsByIdentity` decides what the same object or array reference means.
1098
+ * The internal store copies every path it writes and never touches a snapshot
1099
+ * it has handed out, so there the same reference is the same content. An
1100
+ * external store may write into its snapshot in place, so there only
1101
+ * primitives can be trusted to compare equal, and any object value counts as
1102
+ * changed.
1103
+ */
1104
+ function sameValues(a, b, objectsByIdentity) {
1105
+ const keys = Object.keys(a);
1106
+ if (keys.length !== Object.keys(b).length)
1107
+ return false;
1108
+ for (const key of keys) {
1109
+ if (!Object.hasOwn(b, key))
1110
+ return false;
1111
+ const value = a[key];
1112
+ if (!Object.is(value, b[key]))
1113
+ return false;
1114
+ if (!objectsByIdentity && typeof value === 'object' && value !== null) {
1115
+ return false;
1116
+ }
1117
+ }
1118
+ return true;
1119
+ }
1094
1120
  /** The chain from the root down to `path`, as a line a human can follow. */
1095
1121
  function describePath(path) {
1096
1122
  const keys = [];
@@ -1138,7 +1164,22 @@ class JrElement {
1138
1164
  return true;
1139
1165
  return evaluateVisibility(el.visible, this.resolutionCtx());
1140
1166
  }, ...(ngDevMode ? [{ debugName: "visible" }] : /* istanbul ignore next */ []));
1141
- /** The element with all prop expressions resolved. */
1167
+ /**
1168
+ * The element with all prop expressions resolved.
1169
+ *
1170
+ * Resolving builds a new props object on every state write, whatever the
1171
+ * write touched. Handing that on as a change would re-run every catalog
1172
+ * template in the tree for one keystroke, so a result that holds the same
1173
+ * values as the last one counts as unchanged and keeps the old object.
1174
+ *
1175
+ * Two-way bound elements are the exception, and keep treating every
1176
+ * resolution as a change. Their component writes the user's input into the
1177
+ * DOM before it reaches state, so the DOM can differ from a prop whose
1178
+ * value never did: typing and a `clearStatePath` in the same turn take
1179
+ * the prop from '' through 'milk' back to '' with nobody reading 'milk'.
1180
+ * The README tells catalog authors to sync such inputs with an effect on
1181
+ * props, and that effect only gets to put the DOM right if it runs.
1182
+ */
1142
1183
  resolvedElement = computed(() => {
1143
1184
  const el = this.rawElement();
1144
1185
  if (!el)
@@ -1147,14 +1188,24 @@ class JrElement {
1147
1188
  ...el,
1148
1189
  props: resolveElementProps(el.props ?? {}, this.resolutionCtx()),
1149
1190
  };
1150
- }, ...(ngDevMode ? [{ debugName: "resolvedElement" }] : /* istanbul ignore next */ []));
1191
+ }, { ...(ngDevMode ? { debugName: "resolvedElement" } : /* istanbul ignore next */ {}), equal: (a, b) => {
1192
+ if (a === b)
1193
+ return true;
1194
+ if (!a || !b || untracked(this.bindings))
1195
+ return false;
1196
+ const { props: aProps, ...aRest } = a;
1197
+ const { props: bProps, ...bRest } = b;
1198
+ // Everything but props is the spec element itself, never state.
1199
+ return (sameValues(aRest, bRest, true) &&
1200
+ sameValues(aProps, bProps, !untracked(this.root.store)));
1201
+ } });
1151
1202
  /** Two-way binding paths ($bindState / $bindItem) by prop name. */
1152
1203
  bindings = computed(() => {
1153
1204
  const el = this.rawElement();
1154
1205
  return el
1155
1206
  ? resolveBindings(el.props ?? {}, this.resolutionCtx())
1156
1207
  : undefined;
1157
- }, ...(ngDevMode ? [{ debugName: "bindings" }] : /* istanbul ignore next */ []));
1208
+ }, { ...(ngDevMode ? { debugName: "bindings" } : /* istanbul ignore next */ {}), equal: (a, b) => a === b || (!!a && !!b && sameValues(a, b, true)) });
1158
1209
  entry = computed(() => {
1159
1210
  const el = this.rawElement();
1160
1211
  return el ? this.root.resolveEntry(el.type) : undefined;