form0-core 0.1.1 → 0.1.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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 paqu.io
3
+ Copyright (c) 2025-present paqu.io
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -35,3 +35,17 @@ npm install form0-core
35
35
  ## Security
36
36
 
37
37
  See `SECURITY.md` for security modes and configuration.
38
+
39
+ ## Requirements
40
+
41
+ - Node.js 18+
42
+
43
+ ## Related repositories
44
+
45
+ - [form0-cli](https://github.com/paqu-io/form0-cli) - Command-line interface
46
+ - [form0-react](https://github.com/paqu-io/form0-react) - React components
47
+ - [form0-react-native](https://github.com/paqu-io/form0-react-native) - React Native components
48
+
49
+ ## Contributing
50
+
51
+ Contributions are welcome! Please feel free to submit [issues](https://github.com/paqu-io/form0-core/issues) and [pull requests](https://github.com/paqu-io/form0-core/pulls).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "form0-core",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/"
6
6
  },
@@ -199,6 +199,12 @@ export function createStructuredRecord(state, fields = null, options = {}, id =
199
199
  sectionFields,
200
200
  } = buildRepeatableMetadata(elementsSource);
201
201
 
202
+ const hasStructuredRepeatableState =
203
+ state &&
204
+ Object.prototype.hasOwnProperty.call(state, 'repeatable') &&
205
+ state.repeatable &&
206
+ typeof state.repeatable === 'object';
207
+
202
208
  // Transform the values object using the preferred keys
203
209
  for (const [dataName, value] of Object.entries(state.values)) {
204
210
  // Skip flattened choice fields (created by form renderer for allow_other fields)
@@ -292,7 +298,9 @@ export function createStructuredRecord(state, fields = null, options = {}, id =
292
298
  const childIds = getNestedChildIds([...currentPath, pathKey]);
293
299
 
294
300
  // Structured RepeatableSection state supplied (new format)
295
- if (Array.isArray(structuredInstances) && structuredInstances.length > 0) {
301
+ // Empty arrays are treated as explicit "no instances" and must not fall back
302
+ // to legacy flattened behavior, which can emit phantom records.
303
+ if (Array.isArray(structuredInstances)) {
296
304
  const recordCount = Math.max(structuredInstances.length, childIds.length);
297
305
  const childRecords = [];
298
306
 
@@ -360,16 +368,20 @@ export function createStructuredRecord(state, fields = null, options = {}, id =
360
368
  }
361
369
 
362
370
  for (const [, childRepInfo] of repInfo.children) {
363
- const nestedInstances = instanceRepeatable[childRepInfo.preferredKey] || [];
371
+ const rawNestedInstances = Object.prototype.hasOwnProperty.call(
372
+ instanceRepeatable,
373
+ childRepInfo.preferredKey
374
+ )
375
+ ? instanceRepeatable[childRepInfo.preferredKey]
376
+ : [];
377
+ const nestedInstances = Array.isArray(rawNestedInstances) ? rawNestedInstances : [];
364
378
  const childRepeatableArray = processRepeatableSection(
365
379
  childRepInfo,
366
380
  [...currentPath, pathKey],
367
381
  nestedInstances
368
382
  );
369
- if (childRepeatableArray.length > 0) {
370
- const childOutputKey = resolveRepeatableOutputKey(childRepInfo);
371
- childRecord.form_values[childOutputKey] = childRepeatableArray;
372
- }
383
+ const childOutputKey = resolveRepeatableOutputKey(childRepInfo);
384
+ childRecord.form_values[childOutputKey] = childRepeatableArray;
373
385
  }
374
386
 
375
387
  if (hasMeaningfulRecord(childRecord)) {
@@ -501,14 +513,23 @@ export function createStructuredRecord(state, fields = null, options = {}, id =
501
513
  for (const [, repInfo] of repeatableSectionTree) {
502
514
  if (repInfo.parentPath.length === 0) {
503
515
  // This is a top-level RepeatableSection
504
- const structuredInstances =
505
- state.repeatable && state.repeatable[repInfo.preferredKey]
516
+ const rawStructuredInstances = hasStructuredRepeatableState
517
+ ? Object.prototype.hasOwnProperty.call(state.repeatable, repInfo.preferredKey)
506
518
  ? state.repeatable[repInfo.preferredKey]
507
- : null;
519
+ : []
520
+ : null;
521
+ const structuredInstances = hasStructuredRepeatableState
522
+ ? Array.isArray(rawStructuredInstances)
523
+ ? rawStructuredInstances
524
+ : []
525
+ : null;
508
526
 
509
527
  const childRecords = processRepeatableSection(repInfo, [], structuredInstances);
510
- if (childRecords.length > 0) {
511
- const repeatableOutputKey = resolveRepeatableOutputKey(repInfo);
528
+ const repeatableOutputKey = resolveRepeatableOutputKey(repInfo);
529
+ if (hasStructuredRepeatableState) {
530
+ // In structured mode keep explicit repeatable keys, even when empty.
531
+ form_values[repeatableOutputKey] = childRecords;
532
+ } else if (childRecords.length > 0) {
512
533
  form_values[repeatableOutputKey] = childRecords;
513
534
  }
514
535
  }