@farm.js/react 0.1.0-beta.1 → 0.1.0-beta.2

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
@@ -188,11 +188,68 @@ and unproven calls use the original React fallback.
188
188
  `toSorted` and `toReversed` are emitted as standard runtime calls rather than polyfilled. Configure
189
189
  the TypeScript `lib` with ES2023 and target a runtime that supports them when using those methods.
190
190
 
191
- React remains the fallback and compatibility boundary. A map beside static children, a row with a
192
- non-inline or async handler, a controlled interactive form field, a fragment, a ref, a custom
193
- component, or a conditional mixed directly beside other children in the same host slot uses the
194
- existing React-owned keyed boundary. The outer compiled component can still be skipped, but React
195
- reconciles that list's rows and owns their events, lifecycle, and state.
191
+ React remains the fallback and compatibility boundary. An interactive map beside static children,
192
+ a non-host sibling between ranges, a row with a non-inline or async handler, a file input or dynamic
193
+ form control shape, a fragment, a ref, a custom component, or a conditional mixed directly beside
194
+ other children in the same host slot uses the existing React-owned keyed boundary. The outer
195
+ compiled component can still be skipped, but React reconciles that list's rows and owns their
196
+ events, lifecycle, and state.
197
+
198
+ Non-interactive host maps can share a nested or component-root container with static host siblings:
199
+
200
+ ```tsx
201
+ <ul>
202
+ <li>Primary</li>
203
+ {primary.map((item) => (
204
+ <li key={item.id}>{item.label}</li>
205
+ ))}
206
+ <li>Secondary</li>
207
+ {secondary.map((item) => (
208
+ <li key={item.id}>{item.label}</li>
209
+ ))}
210
+ <li>{primary.length + secondary.length} total</li>
211
+ </ul>
212
+ ```
213
+
214
+ The compiler emits one range block for that host container. React renders and hydrates the original
215
+ markup. After mount, Farm records the static element segments and reconciles each keyed range with
216
+ its own row table and LIS pass. It does not add wrappers or hydration markers. When the container is
217
+ the component root, the block forwards that exact host element to the outer binding runtime, so
218
+ root attributes and styles keep updating normally. A root with one eligible map or `List` also
219
+ works without static siblings. Direct children must be host elements or eligible ranges; events,
220
+ controlled rows, row conditionals, components, fragments, and nested dynamic structures keep the
221
+ React boundary. Duplicate keys, an adoption shape mismatch, or a parent-driven static markup change
222
+ remounts the complete container through React.
223
+
224
+ Controlled host fields can use the hybrid keyed-row path:
225
+
226
+ ```tsx
227
+ <ul>
228
+ {items.map((item) => (
229
+ <li key={item.id}>
230
+ <input
231
+ value={item.label}
232
+ onChange={(event) =>
233
+ setItems((current) =>
234
+ current.map((row) =>
235
+ row.id === item.id ? { ...row, label: event.currentTarget.value } : row,
236
+ ),
237
+ )
238
+ }
239
+ />
240
+ <input type="checkbox" checked={item.done} onChange={/* keyed update */} />
241
+ </li>
242
+ ))}
243
+ </ul>
244
+ ```
245
+
246
+ React owns the controls, events, hydration, and every structural commit. Farm patches `value`,
247
+ `checked`, selected options, and dependent row bindings by stable key. It preserves focused text
248
+ selection and leaves composition events on React. When `event.currentTarget` is referenced inside
249
+ a queued functional compiler-state updater, generated code snapshots the referenced property while
250
+ the handler is active. Static input types and static option attributes are required. File inputs,
251
+ dynamic types or options, `contentEditable`, refs, custom controls, textarea children beside
252
+ `value`, and async or non-inline handlers keep the React fallback.
196
253
 
197
254
  For custom rows or an explicit key selector, use the public component:
198
255
 
@@ -276,8 +333,9 @@ assertion; it is not presented as a cross-machine timing benchmark.
276
333
  Application and prototype calls, dynamic style objects, handlers outside JSX events, nested,
277
334
  computed, and rest props patterns, async handlers, unkeyed or index-keyed lists, chained maps,
278
335
  unsupported conditional roots, effects, and more advanced hook support intentionally stay on React
279
- in this release. Compiler-owned keyed rows are limited to a dedicated container with one host-only
280
- map or `List`. Inline synchronous events and dedicated row-local host conditional slots can use the
281
- hybrid keyed-row path, but branch events, nested dynamic blocks, conditionals mixed with siblings in
282
- one slot, non-inline or async row handlers, controlled interactive row forms, custom components,
336
+ in this release. Compiler-owned keyed rows support either one dedicated host-only map/`List` or
337
+ one or more non-interactive ranges in a nested or component-root host container. Inline synchronous
338
+ events and dedicated row-local host conditional slots can use the single-list hybrid path, but
339
+ branch events, interactive ranges beside siblings, nested dynamic blocks, conditionals mixed with
340
+ siblings in one slot, non-inline or async row handlers, unsupported form shapes, custom components,
283
341
  fragments, refs, SVG, and duplicate runtime keys keep or switch to React ownership.
@@ -906,6 +906,219 @@ function createKeyedRowsBlockComponent(owner) {
906
906
  FarmKeyedRowsBlock.displayName = "FarmCompiledKeyedRows";
907
907
  return FarmKeyedRowsBlock;
908
908
  }
909
+ function createKeyedRangesBlockComponent(owner) {
910
+ class FarmKeyedRangesBlock extends React__default.default.Component {
911
+ constructor() {
912
+ super(...arguments);
913
+ this.state = { fallback: false };
914
+ this.root = null;
915
+ this.mounted = false;
916
+ this.fallbackRequested = false;
917
+ this.fallbackVersion = 0;
918
+ this.propFallbackQueued = false;
919
+ this.currentProps = this.props;
920
+ this.rangeInstances = [];
921
+ this.staticSegments = [];
922
+ this.captureRoot = (root) => {
923
+ this.root = root;
924
+ this.currentProps.rootRef?.(root);
925
+ };
926
+ this.refresh = (afterCommit) => {
927
+ this.reconcile(afterCommit);
928
+ };
929
+ }
930
+ readRange(range) {
931
+ const source = range.items();
932
+ const items = source ? Array.from(source) : [];
933
+ const keys = items.map((item, index) => keyedRowIdentity(range.rowKey(item, index)));
934
+ if (new Set(keys).size !== keys.length) return null;
935
+ return { items, keys };
936
+ }
937
+ readRanges(props) {
938
+ const ranges = [];
939
+ for (const range of props.ranges) {
940
+ const rows = this.readRange(range);
941
+ if (!rows) return null;
942
+ ranges.push(rows);
943
+ }
944
+ return ranges;
945
+ }
946
+ adopt(props = this.currentProps) {
947
+ if (!this.root || !Number.isSafeInteger(props.trailing) || props.trailing < 0) return false;
948
+ const rowsByRange = this.readRanges(props);
949
+ if (!rowsByRange) return false;
950
+ const elements = [...this.root.children];
951
+ const staticSegments = [];
952
+ const instancesByRange = [];
953
+ let cursor = 0;
954
+ for (let rangeIndex = 0; rangeIndex < props.ranges.length; rangeIndex += 1) {
955
+ const range = props.ranges[rangeIndex];
956
+ const rows = rowsByRange[rangeIndex];
957
+ if (!Number.isSafeInteger(range.before) || range.before < 0) return false;
958
+ const staticEnd = cursor + range.before;
959
+ if (staticEnd > elements.length) return false;
960
+ staticSegments.push(elements.slice(cursor, staticEnd));
961
+ cursor = staticEnd;
962
+ const rowEnd = cursor + rows.items.length;
963
+ if (rowEnd > elements.length) return false;
964
+ const instances = /* @__PURE__ */ new Map();
965
+ for (let index = 0; index < rows.items.length; index += 1) {
966
+ const element = elements[cursor + index];
967
+ if (!matchesCompilerHostElement(element, range.create(rows.items[index], index))) {
968
+ return false;
969
+ }
970
+ instances.set(rows.keys[index], {
971
+ key: rows.keys[index],
972
+ element,
973
+ values: readKeyedRowBindingValues(range, rows.items[index], index),
974
+ item: rows.items[index],
975
+ index,
976
+ conditionalValues: /* @__PURE__ */ new Map()
977
+ });
978
+ }
979
+ instancesByRange.push(instances);
980
+ cursor = rowEnd;
981
+ }
982
+ if (cursor + props.trailing !== elements.length) return false;
983
+ staticSegments.push(elements.slice(cursor));
984
+ this.staticSegments = staticSegments;
985
+ this.rangeInstances = instancesByRange;
986
+ return true;
987
+ }
988
+ anchorAfter(rangeIndex) {
989
+ for (let next = rangeIndex + 1; next < this.rangeInstances.length; next += 1) {
990
+ const staticAnchor = this.staticSegments[next]?.[0];
991
+ if (staticAnchor) return staticAnchor;
992
+ const rowAnchor = this.rangeInstances[next].values().next().value?.element;
993
+ if (rowAnchor) return rowAnchor;
994
+ }
995
+ return this.staticSegments[this.rangeInstances.length]?.[0] || null;
996
+ }
997
+ reconcileRange(rangeIndex, rows) {
998
+ if (!this.root) return;
999
+ const range = this.currentProps.ranges[rangeIndex];
1000
+ const previous = this.rangeInstances[rangeIndex];
1001
+ const oldIndices = /* @__PURE__ */ new Map();
1002
+ [...previous.keys()].forEach((key, index) => oldIndices.set(key, index));
1003
+ const nextKeys = new Set(rows.keys);
1004
+ for (const [key, instance] of previous) {
1005
+ if (!nextKeys.has(key)) instance.element.remove();
1006
+ }
1007
+ const sequence = rows.keys.map((key) => oldIndices.get(key) ?? -1);
1008
+ const stablePositions = longestIncreasingSubsequencePositions(sequence);
1009
+ const nextInstances = [];
1010
+ for (let index = 0; index < rows.items.length; index += 1) {
1011
+ const key = rows.keys[index];
1012
+ const existing = previous.get(key);
1013
+ if (existing) {
1014
+ applyKeyedRowBindings(range, existing, rows.items[index], index);
1015
+ existing.item = rows.items[index];
1016
+ existing.index = index;
1017
+ nextInstances.push(existing);
1018
+ continue;
1019
+ }
1020
+ const element = createCompilerHostElement(
1021
+ this.root.ownerDocument,
1022
+ range.create(rows.items[index], index)
1023
+ );
1024
+ nextInstances.push({
1025
+ key,
1026
+ element,
1027
+ values: readKeyedRowBindingValues(range, rows.items[index], index),
1028
+ item: rows.items[index],
1029
+ index,
1030
+ conditionalValues: /* @__PURE__ */ new Map()
1031
+ });
1032
+ }
1033
+ let anchor = this.anchorAfter(rangeIndex);
1034
+ for (let index = nextInstances.length - 1; index >= 0; index -= 1) {
1035
+ const instance = nextInstances[index];
1036
+ if (sequence[index] < 0) {
1037
+ this.root.insertBefore(instance.element, anchor);
1038
+ } else if (!stablePositions.has(index) && instance.element.nextSibling !== anchor) {
1039
+ this.root.insertBefore(instance.element, anchor);
1040
+ }
1041
+ anchor = instance.element;
1042
+ }
1043
+ this.rangeInstances[rangeIndex] = new Map(
1044
+ nextInstances.map((instance) => [instance.key, instance])
1045
+ );
1046
+ }
1047
+ reconcile(afterCommit) {
1048
+ if (!this.mounted || !this.root) {
1049
+ afterCommit?.();
1050
+ return;
1051
+ }
1052
+ if (this.state.fallback) {
1053
+ this.fallbackVersion += 1;
1054
+ this.forceUpdate(afterCommit);
1055
+ return;
1056
+ }
1057
+ const rowsByRange = this.readRanges(this.currentProps);
1058
+ if (!rowsByRange || rowsByRange.length !== this.rangeInstances.length) {
1059
+ this.activateFallback(afterCommit);
1060
+ return;
1061
+ }
1062
+ const activeElement = this.root.ownerDocument.activeElement;
1063
+ const restoreFocus = Boolean(activeElement && this.root.contains(activeElement));
1064
+ for (let rangeIndex = rowsByRange.length - 1; rangeIndex >= 0; rangeIndex -= 1) {
1065
+ this.reconcileRange(rangeIndex, rowsByRange[rangeIndex]);
1066
+ }
1067
+ if (restoreFocus && activeElement?.isConnected && activeElement.ownerDocument.activeElement !== activeElement && "focus" in activeElement) {
1068
+ activeElement.focus({ preventScroll: true });
1069
+ }
1070
+ afterCommit?.();
1071
+ }
1072
+ activateFallback(afterCommit) {
1073
+ if (!this.mounted || this.state.fallback || this.fallbackRequested) {
1074
+ afterCommit?.();
1075
+ return;
1076
+ }
1077
+ this.fallbackRequested = true;
1078
+ this.setState({ fallback: true }, afterCommit);
1079
+ }
1080
+ schedulePropFallback() {
1081
+ if (this.propFallbackQueued) return;
1082
+ this.propFallbackQueued = true;
1083
+ queueMicrotask(() => {
1084
+ this.propFallbackQueued = false;
1085
+ if (this.mounted && !this.state.fallback) this.activateFallback();
1086
+ });
1087
+ }
1088
+ shouldComponentUpdate(nextProps, nextState) {
1089
+ this.currentProps = nextProps;
1090
+ if (nextState.fallback || this.state.fallback) return true;
1091
+ this.schedulePropFallback();
1092
+ return false;
1093
+ }
1094
+ componentDidMount() {
1095
+ this.mounted = true;
1096
+ this.unsubscribe = owner.subscribe(this.props.id, this.refresh);
1097
+ if (!this.adopt()) this.activateFallback();
1098
+ }
1099
+ componentWillUnmount() {
1100
+ this.mounted = false;
1101
+ this.unsubscribe?.();
1102
+ this.root = null;
1103
+ this.rangeInstances = [];
1104
+ this.staticSegments = [];
1105
+ }
1106
+ render() {
1107
+ const container = this.currentProps.render();
1108
+ if (!React__default.default.isValidElement(container) || typeof container.type !== "string") {
1109
+ throw new TypeError(
1110
+ `Compiled keyed ranges ${this.currentProps.id} must own one host container.`
1111
+ );
1112
+ }
1113
+ return React__default.default.cloneElement(container, {
1114
+ key: this.state.fallback ? `react-${this.fallbackVersion}` : "compiled",
1115
+ ref: this.captureRoot
1116
+ });
1117
+ }
1118
+ }
1119
+ FarmKeyedRangesBlock.displayName = "FarmCompiledKeyedRanges";
1120
+ return FarmKeyedRangesBlock;
1121
+ }
909
1122
  function createComponentBlockComponent(owner) {
910
1123
  class FarmComponentBlock extends React__default.default.Component {
911
1124
  constructor() {
@@ -1002,6 +1215,9 @@ function createCompiledComponent(definition) {
1002
1215
  KeyedRows: createKeyedRowsBlockComponent({
1003
1216
  subscribe: (id, refresh) => this.subscribeToBlock(id, refresh)
1004
1217
  }),
1218
+ KeyedRanges: createKeyedRangesBlockComponent({
1219
+ subscribe: (id, refresh) => this.subscribeToBlock(id, refresh)
1220
+ }),
1005
1221
  Component: createComponentBlockComponent({
1006
1222
  subscribe: (id, refresh) => this.subscribeToBlock(id, refresh)
1007
1223
  }),
@@ -1159,7 +1375,17 @@ function createCompiledComponent(definition) {
1159
1375
  if (this.hasBindingError) throw this.bindingError;
1160
1376
  const currentDefinition = definitionReference.current;
1161
1377
  const element = currentDefinition.render(this.props, this.cells, this.blockRuntime);
1162
- if (!React__default.default.isValidElement(element) || typeof element.type !== "string") {
1378
+ if (!React__default.default.isValidElement(element)) {
1379
+ throw new TypeError(
1380
+ `Compiled component ${currentDefinition.displayName} must return one host element.`
1381
+ );
1382
+ }
1383
+ if (element.type === this.blockRuntime.KeyedRanges) {
1384
+ return React__default.default.cloneElement(element, {
1385
+ rootRef: this.captureRoot
1386
+ });
1387
+ }
1388
+ if (typeof element.type !== "string") {
1163
1389
  throw new TypeError(
1164
1390
  `Compiled component ${currentDefinition.displayName} must return one host element.`
1165
1391
  );