@remoteoss/json-schema-form 0.11.14-dev.20250514100552 → 0.12.0-beta.0

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/CHANGELOG.md CHANGED
@@ -1,3 +1,21 @@
1
+ #### 0.12.0-beta.0 (2025-06-25)
2
+
3
+ ##### New Features
4
+
5
+ * allow select fields to be made from anyOf ([#197](https://github.com/remoteoss/json-schema-form/pull/197)) ([d5da6ae8](https://github.com/remoteoss/json-schema-form/commit/d5da6ae8816a28c8b0011fc699bf351078d6f437))
6
+
7
+ #### 0.11.15-beta.0 (2025-06-06)
8
+
9
+ ##### Bug Fixes
10
+
11
+ * **v0:** Handle computed presentational attributes correctly ([#195](https://github.com/remoteoss/json-schema-form/pull/195)) ([19c7ff5b](https://github.com/remoteoss/json-schema-form/commit/19c7ff5b4c5cc90bd1025a3ad3032076e4e25b65))
12
+
13
+ #### 0.11.14-beta.0 (2025-05-14)
14
+
15
+ ##### Chores
16
+
17
+ * Reminder to publish GitHub release ([#183](https://github.com/remoteoss/json-schema-form/pull/183)) ([adfa8c14](https://github.com/remoteoss/json-schema-form/commit/adfa8c14ca118b1012642f39c8388cf2ba1614c9))
18
+
1
19
  #### 0.11.13-beta.0 (2025-05-05)
2
20
 
3
21
  ##### Bug Fixes
package/README.md CHANGED
@@ -5,14 +5,16 @@
5
5
  <p align="center">
6
6
  <code>json-schema-form</code> is a headless UI form library powered by <a href="https://json-schema.org/">JSON Schemas</a>.
7
7
  <br/>
8
- It transforms JSON schemas into Javascript to be consumed by your UI libraries.
8
+ It transforms JSON schemas into Javascript `fields` to be more easily consumed by your UI libraries.
9
9
  </p>
10
10
 
11
11
  ---
12
12
 
13
13
  ### Why JSON Schemas for forms?
14
14
 
15
- JSON Schemas are the SSoT (Single Source of Truth) that allows you to share form's _structure_ and _validations_ between frontend and backend, regardless of the language used.
15
+ JSON Schemas are the SSoT (Single Source of Truth) that allows you to share form's data _structure_ and _validations_ between the server (backend) and the client (frontend), regardless of the language used.
16
+
17
+ You can use it beyond UI Forms, like lists, tables, and any other UI that needs structured JSON data.
16
18
 
17
19
  ## Installation
18
20
 
@@ -25,12 +27,18 @@ npm install @remoteoss/json-schema-form
25
27
  yarn install @remoteoss/json-schema-form
26
28
  ```
27
29
 
28
- ## Documentation
30
+ ## Getting Started
31
+
32
+ Check the 📚 **[JSF website](https://json-schema-form.vercel.app/)** for documentation.
29
33
 
30
- Check the 📚 **[JSF website](https://json-schema-form.vercel.app/)** for documentation and demos.
34
+ ### Playground
35
+
36
+ Check the đŸ•šī¸ **[JSF Playground](https://json-schema-form.vercel.app/?path=/docs/playground--docs)** for demos.
31
37
 
32
38
  ## Contributing
33
39
 
34
40
  Read [CONTRIBUTING](CONTRIBUTING.md) to get started.
35
41
 
42
+ We are working on the [next version v1.0](/next). A rewrite in TypeScript with major bugfixes and missing features. It aims to support the latest JSON Schema dialect 2020-12.
43
+
36
44
  _Backed by [Remote.com](https://remote.com/)_
package/dist/index.cjs CHANGED
@@ -1,8 +1,8 @@
1
1
 
2
2
  /*!
3
3
  Copyright (c) 2025 Remote Technology, Inc.
4
- NPM Package: @remoteoss/json-schema-form@0.11.14-dev.20250514100552
5
- Generated: Wed, 14 May 2025 10:06:23 GMT
4
+ NPM Package: @remoteoss/json-schema-form@0.12.0-beta.0
5
+ Generated: Wed, 25 Jun 2025 12:45:39 GMT
6
6
 
7
7
  MIT License
8
8
 
@@ -97,6 +97,75 @@ function hasProperty(object2, propertyName) {
97
97
  return Object.prototype.hasOwnProperty.call(object2, propertyName);
98
98
  }
99
99
 
100
+ // src/internals/checkIfConditionMatches.js
101
+ function checkIfConditionMatchesProperties(node, formValues, formFields, logic) {
102
+ if (typeof node.if === "boolean") {
103
+ return node.if;
104
+ }
105
+ if (node.if.anyOf) {
106
+ return node.if.anyOf.some(
107
+ (property) => checkIfConditionMatchesProperties({ if: property }, formValues, formFields, logic)
108
+ );
109
+ }
110
+ return Object.keys(node.if.properties ?? {}).every((name) => {
111
+ const currentProperty = node.if.properties[name];
112
+ const value = formValues[name];
113
+ const hasEmptyValue = typeof value === "undefined" || // NOTE: This is a "Remote API" dependency, as empty fields are sent as "null".
114
+ value === null;
115
+ const hasIfExplicit = node.if.required?.includes(name);
116
+ if (hasEmptyValue && !hasIfExplicit) {
117
+ return true;
118
+ }
119
+ if (hasProperty(currentProperty, "const")) {
120
+ return compareFormValueWithSchemaValue(value, currentProperty.const);
121
+ }
122
+ if (currentProperty.contains?.pattern) {
123
+ const formValue = value || [];
124
+ if (Array.isArray(formValue)) {
125
+ const pattern = new RegExp(currentProperty.contains.pattern);
126
+ return (value || []).some((item) => pattern.test(item));
127
+ }
128
+ }
129
+ if (currentProperty.enum) {
130
+ return currentProperty.enum.includes(value);
131
+ }
132
+ if (currentProperty.properties) {
133
+ return checkIfConditionMatchesProperties(
134
+ { if: currentProperty },
135
+ formValues[name],
136
+ getField(name, formFields).fields,
137
+ logic
138
+ );
139
+ }
140
+ const field = getField(name, formFields);
141
+ return validateFieldSchema(
142
+ {
143
+ ...field,
144
+ ...currentProperty,
145
+ required: true
146
+ },
147
+ value
148
+ );
149
+ });
150
+ }
151
+ function checkIfMatchesValidationsAndComputedValues(node, formValues, logic, parentID) {
152
+ const validationsMatch = Object.entries(node.if.validations ?? {}).every(([name, property]) => {
153
+ const currentValue = logic.getScope(parentID).applyValidationRuleInCondition(name, formValues);
154
+ if (Object.hasOwn(property, "const") && currentValue === property.const)
155
+ return true;
156
+ return false;
157
+ });
158
+ const computedValuesMatch = Object.entries(node.if.computedValues ?? {}).every(
159
+ ([name, property]) => {
160
+ const currentValue = logic.getScope(parentID).applyComputedValueRuleInCondition(name, formValues);
161
+ if (Object.hasOwn(property, "const") && currentValue === property.const)
162
+ return true;
163
+ return false;
164
+ }
165
+ );
166
+ return computedValuesMatch && validationsMatch;
167
+ }
168
+
100
169
  // src/internals/helpers.js
101
170
  var import_merge = __toESM(require("lodash/fp/merge"));
102
171
  var import_get = __toESM(require("lodash/get"));
@@ -356,76 +425,6 @@ function _composeFieldCustomClosure(defaultComposeFn) {
356
425
  };
357
426
  }
358
427
 
359
- // src/internals/checkIfConditionMatches.js
360
- function checkIfConditionMatchesProperties(node, formValues, formFields, logic) {
361
- if (typeof node.if === "boolean") {
362
- return node.if;
363
- }
364
- if (node.if.anyOf) {
365
- return node.if.anyOf.some(
366
- (property) => checkIfConditionMatchesProperties({ if: property }, formValues, formFields, logic)
367
- );
368
- }
369
- return Object.keys(node.if.properties ?? {}).every((name) => {
370
- const currentProperty = node.if.properties[name];
371
- const field = getField(name, formFields ?? []);
372
- const isFieldsetField = field?.inputType === supportedTypes.FIELDSET;
373
- const value = formValues?.[name] ?? (isFieldsetField ? {} : void 0);
374
- const hasEmptyValue = typeof value === "undefined" || // NOTE: This is a "Remote API" dependency, as empty fields are sent as "null".
375
- value === null;
376
- const hasIfExplicit = node.if.required?.includes(name);
377
- if (hasEmptyValue && !hasIfExplicit) {
378
- return true;
379
- }
380
- if (hasProperty(currentProperty, "const")) {
381
- return compareFormValueWithSchemaValue(value, currentProperty.const);
382
- }
383
- if (currentProperty.contains?.pattern) {
384
- const formValue = value || [];
385
- if (Array.isArray(formValue)) {
386
- const pattern = new RegExp(currentProperty.contains.pattern);
387
- return (value || []).some((item) => pattern.test(item));
388
- }
389
- }
390
- if (currentProperty.enum) {
391
- return currentProperty.enum.includes(value);
392
- }
393
- if (currentProperty.properties) {
394
- return checkIfConditionMatchesProperties(
395
- { if: currentProperty },
396
- formValues[name],
397
- getField(name, formFields).fields,
398
- logic
399
- );
400
- }
401
- return validateFieldSchema(
402
- {
403
- ...field,
404
- ...currentProperty,
405
- required: true
406
- },
407
- value
408
- );
409
- });
410
- }
411
- function checkIfMatchesValidationsAndComputedValues(node, formValues, logic, parentID) {
412
- const validationsMatch = Object.entries(node.if.validations ?? {}).every(([name, property]) => {
413
- const currentValue = logic.getScope(parentID).applyValidationRuleInCondition(name, formValues);
414
- if (Object.hasOwn(property, "const") && currentValue === property.const)
415
- return true;
416
- return false;
417
- });
418
- const computedValuesMatch = Object.entries(node.if.computedValues ?? {}).every(
419
- ([name, property]) => {
420
- const currentValue = logic.getScope(parentID).applyComputedValueRuleInCondition(name, formValues);
421
- if (Object.hasOwn(property, "const") && currentValue === property.const)
422
- return true;
423
- return false;
424
- }
425
- );
426
- return computedValuesMatch && validationsMatch;
427
- }
428
-
429
428
  // src/jsonLogic.js
430
429
  var import_json_logic_js = __toESM(require("json-logic-js"));
431
430
 
@@ -968,9 +967,16 @@ function replaceHandlebarsTemplates({
968
967
  function calculateComputedAttributes(fieldParams, { parentID = "root" } = {}) {
969
968
  return ({ logic, isRequired, config, formValues }) => {
970
969
  const { name, computedAttributes } = fieldParams;
971
- const attributes = Object.fromEntries(
970
+ let attributes = Object.fromEntries(
972
971
  Object.entries(computedAttributes).map(handleComputedAttribute(logic, formValues, parentID, name)).filter(([, value]) => value !== null)
973
972
  );
973
+ const { "x-jsf-presentation": presentation, ...rest } = attributes;
974
+ if (presentation) {
975
+ attributes = {
976
+ ...rest,
977
+ ...presentation
978
+ };
979
+ }
974
980
  return {
975
981
  ...attributes,
976
982
  schema: buildYupSchema(
@@ -994,16 +1000,21 @@ function handleComputedAttribute(logic, formValues, parentID, name) {
994
1000
  handleNestedObjectForComputedValues(value, formValues, parentID, logic, name)
995
1001
  ];
996
1002
  case "x-jsf-presentation": {
997
- if (value.statement) {
998
- return [
999
- "statement",
1000
- handleNestedObjectForComputedValues(value.statement, formValues, parentID, logic, name)
1001
- ];
1002
- }
1003
- return [
1004
- key,
1005
- handleNestedObjectForComputedValues(value.statement, formValues, parentID, logic, name)
1006
- ];
1003
+ const values = {};
1004
+ Object.entries(value).forEach(([presentationKey, presentationValue]) => {
1005
+ if (typeof presentationValue === "object") {
1006
+ values[presentationKey] = handleNestedObjectForComputedValues(
1007
+ presentationValue,
1008
+ formValues,
1009
+ parentID,
1010
+ logic,
1011
+ name
1012
+ );
1013
+ } else {
1014
+ values[presentationKey] = logic.getScope(parentID).applyComputedValueInField(presentationValue, formValues, name);
1015
+ }
1016
+ });
1017
+ return [key, values];
1007
1018
  }
1008
1019
  case "const":
1009
1020
  default: {
@@ -1445,8 +1456,8 @@ function getFieldOptions(node, presentation) {
1445
1456
  if (presentation.options) {
1446
1457
  return presentation.options;
1447
1458
  }
1448
- if (node.oneOf || presentation.inputType === "radio") {
1449
- return convertToOptions(node.oneOf || []);
1459
+ if (node.oneOf || node.anyOf || presentation.inputType === "radio") {
1460
+ return convertToOptions(node.oneOf || node.anyOf || []);
1450
1461
  }
1451
1462
  if (node.items?.anyOf) {
1452
1463
  return convertToOptions(node.items.anyOf);
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
 
2
2
  /*!
3
3
  Copyright (c) 2025 Remote Technology, Inc.
4
- NPM Package: @remoteoss/json-schema-form@0.11.14-dev.20250514100552
5
- Generated: Wed, 14 May 2025 10:06:23 GMT
4
+ NPM Package: @remoteoss/json-schema-form@0.12.0-beta.0
5
+ Generated: Wed, 25 Jun 2025 12:45:39 GMT
6
6
 
7
7
  MIT License
8
8
 
@@ -60,6 +60,75 @@ function hasProperty(object2, propertyName) {
60
60
  return Object.prototype.hasOwnProperty.call(object2, propertyName);
61
61
  }
62
62
 
63
+ // src/internals/checkIfConditionMatches.js
64
+ function checkIfConditionMatchesProperties(node, formValues, formFields, logic) {
65
+ if (typeof node.if === "boolean") {
66
+ return node.if;
67
+ }
68
+ if (node.if.anyOf) {
69
+ return node.if.anyOf.some(
70
+ (property) => checkIfConditionMatchesProperties({ if: property }, formValues, formFields, logic)
71
+ );
72
+ }
73
+ return Object.keys(node.if.properties ?? {}).every((name) => {
74
+ const currentProperty = node.if.properties[name];
75
+ const value = formValues[name];
76
+ const hasEmptyValue = typeof value === "undefined" || // NOTE: This is a "Remote API" dependency, as empty fields are sent as "null".
77
+ value === null;
78
+ const hasIfExplicit = node.if.required?.includes(name);
79
+ if (hasEmptyValue && !hasIfExplicit) {
80
+ return true;
81
+ }
82
+ if (hasProperty(currentProperty, "const")) {
83
+ return compareFormValueWithSchemaValue(value, currentProperty.const);
84
+ }
85
+ if (currentProperty.contains?.pattern) {
86
+ const formValue = value || [];
87
+ if (Array.isArray(formValue)) {
88
+ const pattern = new RegExp(currentProperty.contains.pattern);
89
+ return (value || []).some((item) => pattern.test(item));
90
+ }
91
+ }
92
+ if (currentProperty.enum) {
93
+ return currentProperty.enum.includes(value);
94
+ }
95
+ if (currentProperty.properties) {
96
+ return checkIfConditionMatchesProperties(
97
+ { if: currentProperty },
98
+ formValues[name],
99
+ getField(name, formFields).fields,
100
+ logic
101
+ );
102
+ }
103
+ const field = getField(name, formFields);
104
+ return validateFieldSchema(
105
+ {
106
+ ...field,
107
+ ...currentProperty,
108
+ required: true
109
+ },
110
+ value
111
+ );
112
+ });
113
+ }
114
+ function checkIfMatchesValidationsAndComputedValues(node, formValues, logic, parentID) {
115
+ const validationsMatch = Object.entries(node.if.validations ?? {}).every(([name, property]) => {
116
+ const currentValue = logic.getScope(parentID).applyValidationRuleInCondition(name, formValues);
117
+ if (Object.hasOwn(property, "const") && currentValue === property.const)
118
+ return true;
119
+ return false;
120
+ });
121
+ const computedValuesMatch = Object.entries(node.if.computedValues ?? {}).every(
122
+ ([name, property]) => {
123
+ const currentValue = logic.getScope(parentID).applyComputedValueRuleInCondition(name, formValues);
124
+ if (Object.hasOwn(property, "const") && currentValue === property.const)
125
+ return true;
126
+ return false;
127
+ }
128
+ );
129
+ return computedValuesMatch && validationsMatch;
130
+ }
131
+
63
132
  // src/internals/helpers.js
64
133
  import merge from "lodash/fp/merge";
65
134
  import get from "lodash/get";
@@ -319,76 +388,6 @@ function _composeFieldCustomClosure(defaultComposeFn) {
319
388
  };
320
389
  }
321
390
 
322
- // src/internals/checkIfConditionMatches.js
323
- function checkIfConditionMatchesProperties(node, formValues, formFields, logic) {
324
- if (typeof node.if === "boolean") {
325
- return node.if;
326
- }
327
- if (node.if.anyOf) {
328
- return node.if.anyOf.some(
329
- (property) => checkIfConditionMatchesProperties({ if: property }, formValues, formFields, logic)
330
- );
331
- }
332
- return Object.keys(node.if.properties ?? {}).every((name) => {
333
- const currentProperty = node.if.properties[name];
334
- const field = getField(name, formFields ?? []);
335
- const isFieldsetField = field?.inputType === supportedTypes.FIELDSET;
336
- const value = formValues?.[name] ?? (isFieldsetField ? {} : void 0);
337
- const hasEmptyValue = typeof value === "undefined" || // NOTE: This is a "Remote API" dependency, as empty fields are sent as "null".
338
- value === null;
339
- const hasIfExplicit = node.if.required?.includes(name);
340
- if (hasEmptyValue && !hasIfExplicit) {
341
- return true;
342
- }
343
- if (hasProperty(currentProperty, "const")) {
344
- return compareFormValueWithSchemaValue(value, currentProperty.const);
345
- }
346
- if (currentProperty.contains?.pattern) {
347
- const formValue = value || [];
348
- if (Array.isArray(formValue)) {
349
- const pattern = new RegExp(currentProperty.contains.pattern);
350
- return (value || []).some((item) => pattern.test(item));
351
- }
352
- }
353
- if (currentProperty.enum) {
354
- return currentProperty.enum.includes(value);
355
- }
356
- if (currentProperty.properties) {
357
- return checkIfConditionMatchesProperties(
358
- { if: currentProperty },
359
- formValues[name],
360
- getField(name, formFields).fields,
361
- logic
362
- );
363
- }
364
- return validateFieldSchema(
365
- {
366
- ...field,
367
- ...currentProperty,
368
- required: true
369
- },
370
- value
371
- );
372
- });
373
- }
374
- function checkIfMatchesValidationsAndComputedValues(node, formValues, logic, parentID) {
375
- const validationsMatch = Object.entries(node.if.validations ?? {}).every(([name, property]) => {
376
- const currentValue = logic.getScope(parentID).applyValidationRuleInCondition(name, formValues);
377
- if (Object.hasOwn(property, "const") && currentValue === property.const)
378
- return true;
379
- return false;
380
- });
381
- const computedValuesMatch = Object.entries(node.if.computedValues ?? {}).every(
382
- ([name, property]) => {
383
- const currentValue = logic.getScope(parentID).applyComputedValueRuleInCondition(name, formValues);
384
- if (Object.hasOwn(property, "const") && currentValue === property.const)
385
- return true;
386
- return false;
387
- }
388
- );
389
- return computedValuesMatch && validationsMatch;
390
- }
391
-
392
391
  // src/jsonLogic.js
393
392
  import jsonLogic from "json-logic-js";
394
393
 
@@ -931,9 +930,16 @@ function replaceHandlebarsTemplates({
931
930
  function calculateComputedAttributes(fieldParams, { parentID = "root" } = {}) {
932
931
  return ({ logic, isRequired, config, formValues }) => {
933
932
  const { name, computedAttributes } = fieldParams;
934
- const attributes = Object.fromEntries(
933
+ let attributes = Object.fromEntries(
935
934
  Object.entries(computedAttributes).map(handleComputedAttribute(logic, formValues, parentID, name)).filter(([, value]) => value !== null)
936
935
  );
936
+ const { "x-jsf-presentation": presentation, ...rest } = attributes;
937
+ if (presentation) {
938
+ attributes = {
939
+ ...rest,
940
+ ...presentation
941
+ };
942
+ }
937
943
  return {
938
944
  ...attributes,
939
945
  schema: buildYupSchema(
@@ -957,16 +963,21 @@ function handleComputedAttribute(logic, formValues, parentID, name) {
957
963
  handleNestedObjectForComputedValues(value, formValues, parentID, logic, name)
958
964
  ];
959
965
  case "x-jsf-presentation": {
960
- if (value.statement) {
961
- return [
962
- "statement",
963
- handleNestedObjectForComputedValues(value.statement, formValues, parentID, logic, name)
964
- ];
965
- }
966
- return [
967
- key,
968
- handleNestedObjectForComputedValues(value.statement, formValues, parentID, logic, name)
969
- ];
966
+ const values = {};
967
+ Object.entries(value).forEach(([presentationKey, presentationValue]) => {
968
+ if (typeof presentationValue === "object") {
969
+ values[presentationKey] = handleNestedObjectForComputedValues(
970
+ presentationValue,
971
+ formValues,
972
+ parentID,
973
+ logic,
974
+ name
975
+ );
976
+ } else {
977
+ values[presentationKey] = logic.getScope(parentID).applyComputedValueInField(presentationValue, formValues, name);
978
+ }
979
+ });
980
+ return [key, values];
970
981
  }
971
982
  case "const":
972
983
  default: {
@@ -1408,8 +1419,8 @@ function getFieldOptions(node, presentation) {
1408
1419
  if (presentation.options) {
1409
1420
  return presentation.options;
1410
1421
  }
1411
- if (node.oneOf || presentation.inputType === "radio") {
1412
- return convertToOptions(node.oneOf || []);
1422
+ if (node.oneOf || node.anyOf || presentation.inputType === "radio") {
1423
+ return convertToOptions(node.oneOf || node.anyOf || []);
1413
1424
  }
1414
1425
  if (node.items?.anyOf) {
1415
1426
  return convertToOptions(node.items.anyOf);
@@ -1,8 +1,8 @@
1
1
 
2
2
  /*!
3
3
  Copyright (c) 2025 Remote Technology, Inc.
4
- NPM Package: @remoteoss/json-schema-form@0.11.14-dev.20250514100552
5
- Generated: Wed, 14 May 2025 10:06:23 GMT
4
+ NPM Package: @remoteoss/json-schema-form@0.12.0-beta.0
5
+ Generated: Wed, 25 Jun 2025 12:45:39 GMT
6
6
 
7
7
  MIT License
8
8
 
@@ -11577,6 +11577,75 @@ function hasProperty(object2, propertyName) {
11577
11577
  return Object.prototype.hasOwnProperty.call(object2, propertyName);
11578
11578
  }
11579
11579
 
11580
+ // src/internals/checkIfConditionMatches.js
11581
+ function checkIfConditionMatchesProperties(node, formValues, formFields, logic) {
11582
+ if (typeof node.if === "boolean") {
11583
+ return node.if;
11584
+ }
11585
+ if (node.if.anyOf) {
11586
+ return node.if.anyOf.some(
11587
+ (property2) => checkIfConditionMatchesProperties({ if: property2 }, formValues, formFields, logic)
11588
+ );
11589
+ }
11590
+ return Object.keys(node.if.properties ?? {}).every((name) => {
11591
+ const currentProperty = node.if.properties[name];
11592
+ const value = formValues[name];
11593
+ const hasEmptyValue = typeof value === "undefined" || // NOTE: This is a "Remote API" dependency, as empty fields are sent as "null".
11594
+ value === null;
11595
+ const hasIfExplicit = node.if.required?.includes(name);
11596
+ if (hasEmptyValue && !hasIfExplicit) {
11597
+ return true;
11598
+ }
11599
+ if (hasProperty(currentProperty, "const")) {
11600
+ return compareFormValueWithSchemaValue(value, currentProperty.const);
11601
+ }
11602
+ if (currentProperty.contains?.pattern) {
11603
+ const formValue = value || [];
11604
+ if (Array.isArray(formValue)) {
11605
+ const pattern = new RegExp(currentProperty.contains.pattern);
11606
+ return (value || []).some((item) => pattern.test(item));
11607
+ }
11608
+ }
11609
+ if (currentProperty.enum) {
11610
+ return currentProperty.enum.includes(value);
11611
+ }
11612
+ if (currentProperty.properties) {
11613
+ return checkIfConditionMatchesProperties(
11614
+ { if: currentProperty },
11615
+ formValues[name],
11616
+ getField(name, formFields).fields,
11617
+ logic
11618
+ );
11619
+ }
11620
+ const field = getField(name, formFields);
11621
+ return validateFieldSchema(
11622
+ {
11623
+ ...field,
11624
+ ...currentProperty,
11625
+ required: true
11626
+ },
11627
+ value
11628
+ );
11629
+ });
11630
+ }
11631
+ function checkIfMatchesValidationsAndComputedValues(node, formValues, logic, parentID) {
11632
+ const validationsMatch = Object.entries(node.if.validations ?? {}).every(([name, property2]) => {
11633
+ const currentValue = logic.getScope(parentID).applyValidationRuleInCondition(name, formValues);
11634
+ if (Object.hasOwn(property2, "const") && currentValue === property2.const)
11635
+ return true;
11636
+ return false;
11637
+ });
11638
+ const computedValuesMatch = Object.entries(node.if.computedValues ?? {}).every(
11639
+ ([name, property2]) => {
11640
+ const currentValue = logic.getScope(parentID).applyComputedValueRuleInCondition(name, formValues);
11641
+ if (Object.hasOwn(property2, "const") && currentValue === property2.const)
11642
+ return true;
11643
+ return false;
11644
+ }
11645
+ );
11646
+ return computedValuesMatch && validationsMatch;
11647
+ }
11648
+
11580
11649
  // src/internals/helpers.js
11581
11650
  var import_merge = __toESM(require_merge2());
11582
11651
  var import_get2 = __toESM(require_get());
@@ -11836,76 +11905,6 @@ function _composeFieldCustomClosure(defaultComposeFn) {
11836
11905
  };
11837
11906
  }
11838
11907
 
11839
- // src/internals/checkIfConditionMatches.js
11840
- function checkIfConditionMatchesProperties(node, formValues, formFields, logic) {
11841
- if (typeof node.if === "boolean") {
11842
- return node.if;
11843
- }
11844
- if (node.if.anyOf) {
11845
- return node.if.anyOf.some(
11846
- (property2) => checkIfConditionMatchesProperties({ if: property2 }, formValues, formFields, logic)
11847
- );
11848
- }
11849
- return Object.keys(node.if.properties ?? {}).every((name) => {
11850
- const currentProperty = node.if.properties[name];
11851
- const field = getField(name, formFields ?? []);
11852
- const isFieldsetField = field?.inputType === supportedTypes.FIELDSET;
11853
- const value = formValues?.[name] ?? (isFieldsetField ? {} : void 0);
11854
- const hasEmptyValue = typeof value === "undefined" || // NOTE: This is a "Remote API" dependency, as empty fields are sent as "null".
11855
- value === null;
11856
- const hasIfExplicit = node.if.required?.includes(name);
11857
- if (hasEmptyValue && !hasIfExplicit) {
11858
- return true;
11859
- }
11860
- if (hasProperty(currentProperty, "const")) {
11861
- return compareFormValueWithSchemaValue(value, currentProperty.const);
11862
- }
11863
- if (currentProperty.contains?.pattern) {
11864
- const formValue = value || [];
11865
- if (Array.isArray(formValue)) {
11866
- const pattern = new RegExp(currentProperty.contains.pattern);
11867
- return (value || []).some((item) => pattern.test(item));
11868
- }
11869
- }
11870
- if (currentProperty.enum) {
11871
- return currentProperty.enum.includes(value);
11872
- }
11873
- if (currentProperty.properties) {
11874
- return checkIfConditionMatchesProperties(
11875
- { if: currentProperty },
11876
- formValues[name],
11877
- getField(name, formFields).fields,
11878
- logic
11879
- );
11880
- }
11881
- return validateFieldSchema(
11882
- {
11883
- ...field,
11884
- ...currentProperty,
11885
- required: true
11886
- },
11887
- value
11888
- );
11889
- });
11890
- }
11891
- function checkIfMatchesValidationsAndComputedValues(node, formValues, logic, parentID) {
11892
- const validationsMatch = Object.entries(node.if.validations ?? {}).every(([name, property2]) => {
11893
- const currentValue = logic.getScope(parentID).applyValidationRuleInCondition(name, formValues);
11894
- if (Object.hasOwn(property2, "const") && currentValue === property2.const)
11895
- return true;
11896
- return false;
11897
- });
11898
- const computedValuesMatch = Object.entries(node.if.computedValues ?? {}).every(
11899
- ([name, property2]) => {
11900
- const currentValue = logic.getScope(parentID).applyComputedValueRuleInCondition(name, formValues);
11901
- if (Object.hasOwn(property2, "const") && currentValue === property2.const)
11902
- return true;
11903
- return false;
11904
- }
11905
- );
11906
- return computedValuesMatch && validationsMatch;
11907
- }
11908
-
11909
11908
  // src/jsonLogic.js
11910
11909
  var import_json_logic_js = __toESM(require_logic());
11911
11910
 
@@ -12447,9 +12446,16 @@ function replaceHandlebarsTemplates({
12447
12446
  function calculateComputedAttributes(fieldParams, { parentID = "root" } = {}) {
12448
12447
  return ({ logic, isRequired, config, formValues }) => {
12449
12448
  const { name, computedAttributes } = fieldParams;
12450
- const attributes = Object.fromEntries(
12449
+ let attributes = Object.fromEntries(
12451
12450
  Object.entries(computedAttributes).map(handleComputedAttribute(logic, formValues, parentID, name)).filter(([, value]) => value !== null)
12452
12451
  );
12452
+ const { "x-jsf-presentation": presentation, ...rest } = attributes;
12453
+ if (presentation) {
12454
+ attributes = {
12455
+ ...rest,
12456
+ ...presentation
12457
+ };
12458
+ }
12453
12459
  return {
12454
12460
  ...attributes,
12455
12461
  schema: buildYupSchema(
@@ -12473,16 +12479,21 @@ function handleComputedAttribute(logic, formValues, parentID, name) {
12473
12479
  handleNestedObjectForComputedValues(value, formValues, parentID, logic, name)
12474
12480
  ];
12475
12481
  case "x-jsf-presentation": {
12476
- if (value.statement) {
12477
- return [
12478
- "statement",
12479
- handleNestedObjectForComputedValues(value.statement, formValues, parentID, logic, name)
12480
- ];
12481
- }
12482
- return [
12483
- key,
12484
- handleNestedObjectForComputedValues(value.statement, formValues, parentID, logic, name)
12485
- ];
12482
+ const values2 = {};
12483
+ Object.entries(value).forEach(([presentationKey, presentationValue]) => {
12484
+ if (typeof presentationValue === "object") {
12485
+ values2[presentationKey] = handleNestedObjectForComputedValues(
12486
+ presentationValue,
12487
+ formValues,
12488
+ parentID,
12489
+ logic,
12490
+ name
12491
+ );
12492
+ } else {
12493
+ values2[presentationKey] = logic.getScope(parentID).applyComputedValueInField(presentationValue, formValues, name);
12494
+ }
12495
+ });
12496
+ return [key, values2];
12486
12497
  }
12487
12498
  case "const":
12488
12499
  default: {
@@ -12924,8 +12935,8 @@ function getFieldOptions(node, presentation) {
12924
12935
  if (presentation.options) {
12925
12936
  return presentation.options;
12926
12937
  }
12927
- if (node.oneOf || presentation.inputType === "radio") {
12928
- return convertToOptions(node.oneOf || []);
12938
+ if (node.oneOf || node.anyOf || presentation.inputType === "radio") {
12939
+ return convertToOptions(node.oneOf || node.anyOf || []);
12929
12940
  }
12930
12941
  if (node.items?.anyOf) {
12931
12942
  return convertToOptions(node.items.anyOf);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remoteoss/json-schema-form",
3
- "version": "0.11.14-dev.20250514100552",
3
+ "version": "0.12.0-beta.0",
4
4
  "description": "Headless UI form powered by JSON Schemas",
5
5
  "author": "Remote.com <engineering@remote.com> (https://remote.com/)",
6
6
  "license": "MIT",
@@ -596,74 +596,6 @@ describe('Conditional with anyOf', () => {
596
596
  });
597
597
  });
598
598
 
599
- describe('Conditional with fieldset', () => {
600
- const schema = {
601
- additionalProperties: false,
602
- type: 'object',
603
- properties: {
604
- field_a: {
605
- type: 'object',
606
- properties: {
607
- min: { type: 'number' },
608
- max: { type: 'number' },
609
- },
610
- },
611
- field_b: { type: 'string' },
612
- },
613
- allOf: [
614
- {
615
- if: {
616
- properties: {
617
- field_a: {
618
- properties: {
619
- min: {
620
- minimum: 10,
621
- },
622
- },
623
- required: ['min'],
624
- },
625
- },
626
- required: ['field_a'],
627
- },
628
- then: {
629
- required: ['field_b'],
630
- },
631
- else: {
632
- properties: {
633
- field_b: false,
634
- },
635
- },
636
- },
637
- ],
638
- };
639
-
640
- it('handles true case', () => {
641
- const { fields, handleValidation } = createHeadlessForm(schema, { strictInputType: false });
642
-
643
- expect(fields[1].isVisible).toBe(false);
644
- expect(handleValidation({ field_a: { min: 100 } }).formErrors).toEqual({
645
- field_b: 'Required field',
646
- });
647
- expect(fields[1].isVisible).toBe(true);
648
- });
649
-
650
- it('handles false case', () => {
651
- const { fields, handleValidation } = createHeadlessForm(schema, { strictInputType: false });
652
-
653
- expect(fields[1].isVisible).toBe(false);
654
- expect(handleValidation({ field_a: { min: 1 } }).formErrors).toBeUndefined();
655
- expect(fields[1].isVisible).toBe(false);
656
- });
657
-
658
- it('handles undefined fieldset case', () => {
659
- const { fields, handleValidation } = createHeadlessForm(schema, { strictInputType: false });
660
-
661
- expect(fields[1].isVisible).toBe(false);
662
- expect(handleValidation({}).formErrors).toBeUndefined();
663
- expect(fields[1].isVisible).toBe(false);
664
- });
665
- });
666
-
667
599
  describe('Conditionals - bugs and code-smells', () => {
668
600
  // Why do we have these bugs?
669
601
  // To be honest we never realized it much later later.
@@ -571,6 +571,44 @@ describe('createHeadlessForm', () => {
571
571
  });
572
572
  });
573
573
 
574
+ it('support "select" field type from anyOf', () => {
575
+ const schema = {
576
+ type: 'object',
577
+ properties: {
578
+ browser: {
579
+ type: 'string',
580
+ anyOf: [
581
+ { title: 'Chrome', const: 'chr' },
582
+ { title: 'Firefox', const: 'ff' },
583
+ { title: 'Add new option', pattern: '.*' },
584
+ ],
585
+ 'x-jsf-presentation': {
586
+ inputType: 'select',
587
+ },
588
+ },
589
+ },
590
+ };
591
+
592
+ const { fields } = createHeadlessForm(schema);
593
+ const fieldSelect = fields[0];
594
+ expect(fieldSelect).toMatchObject({
595
+ options: [
596
+ {
597
+ value: 'chr',
598
+ label: 'Chrome',
599
+ },
600
+ {
601
+ value: 'ff',
602
+ label: 'Firefox',
603
+ },
604
+ {
605
+ label: 'Add new option',
606
+ pattern: '.*',
607
+ },
608
+ ],
609
+ });
610
+ });
611
+
574
612
  it('supports "select" field type with multiple options @deprecated', () => {
575
613
  const result = createHeadlessForm(schemaInputTypeSelectMultipleDeprecated);
576
614
  expect(result).toMatchObject({
@@ -205,7 +205,7 @@ export const schemaInputTypeHidden = {
205
205
  a_hidden_select_multiple: {
206
206
  ...schemaInputTypeCountriesMultiple.properties.nationality,
207
207
  title: 'Select multi hidden',
208
- default: ['Albania, Algeria'],
208
+ default: ['Albania', 'Algeria'],
209
209
  'x-jsf-presentation': { inputType: 'hidden' },
210
210
  type: 'array',
211
211
  },
@@ -448,20 +448,19 @@ export const mockGroupArrayInput = {
448
448
  sex: {
449
449
  description:
450
450
  'We know sex is non-binary but for insurance and payroll purposes, we need to collect this information.',
451
- enum: ['female', 'male'],
452
451
  'x-jsf-presentation': {
453
452
  inputType: 'radio',
454
- options: [
455
- {
456
- label: 'Male',
457
- value: 'male',
458
- },
459
- {
460
- label: 'Female',
461
- value: 'female',
462
- },
463
- ],
464
453
  },
454
+ oneOf: [
455
+ {
456
+ const: 'male',
457
+ title: 'Male',
458
+ },
459
+ {
460
+ const: 'female',
461
+ title: 'Female',
462
+ },
463
+ ],
465
464
  title: 'Child Sex',
466
465
  type: 'string',
467
466
  },
@@ -1469,7 +1468,6 @@ export const schemaDynamicValidationConst = {
1469
1468
  required: ['a_fieldset', 'validate_tabs', 'mandatory_group_array'],
1470
1469
  'x-jsf-order': ['validate_tabs', 'a_fieldset', 'mandatory_group_array', 'a_group_array'],
1471
1470
  };
1472
-
1473
1471
  export const schemaDynamicValidationMinimumMaximum = JSONSchemaBuilder()
1474
1472
  .addInput({
1475
1473
  a_number: mockNumberInput,
@@ -1543,6 +1541,7 @@ export const schemaDynamicValidationContains = JSONSchemaBuilder()
1543
1541
  },
1544
1542
  'x-jsf-presentation': {
1545
1543
  inputType: 'select',
1544
+ multiple: true,
1546
1545
  options: [
1547
1546
  {
1548
1547
  label: 'All',
@@ -364,6 +364,55 @@ export const schemaWithComputedAttributesAndErrorMessages = {
364
364
  },
365
365
  };
366
366
 
367
+ export const schemaWithComputedPresentationAttributes = {
368
+ properties: {
369
+ amount: {
370
+ description: 'Total amount to be reimbursed, including taxes.',
371
+ minimum: 0,
372
+ title: 'Total amount',
373
+ type: 'number',
374
+ 'x-jsf-logic-computedAttrs': {
375
+ 'x-jsf-presentation': {
376
+ currency: 'currency_selected',
377
+ },
378
+ },
379
+ 'x-jsf-presentation': {
380
+ currency: '---',
381
+ inputType: 'money',
382
+ },
383
+ },
384
+ currency: {
385
+ default: 'USD',
386
+ description: 'Currency in which the expense was paid.',
387
+ oneOf: [
388
+ {
389
+ const: 'UGX',
390
+ title: 'UGX - Ugandan Shilling',
391
+ },
392
+ {
393
+ const: 'USD',
394
+ title: 'USD - United States Dollar',
395
+ },
396
+ ],
397
+ title: 'Currency',
398
+ type: 'string',
399
+ 'x-jsf-presentation': {
400
+ inputType: 'select',
401
+ },
402
+ },
403
+ },
404
+ type: 'object',
405
+ 'x-jsf-logic': {
406
+ computedValues: {
407
+ currency_selected: {
408
+ rule: {
409
+ var: 'currency',
410
+ },
411
+ },
412
+ },
413
+ },
414
+ };
415
+
367
416
  export const schemaWithDeepVarThatDoesNotExist = {
368
417
  properties: {
369
418
  field_a: {
@@ -35,6 +35,7 @@ import {
35
35
  schemaWithValidationThatDoesNotExistOnProperty,
36
36
  badSchemaThatWillNotSetAForcedValue,
37
37
  schemaWithReduceAccumulator,
38
+ schemaWithComputedPresentationAttributes,
38
39
  } from './jsonLogic.fixtures';
39
40
  import { mockConsole, restoreConsoleAndEnsureItWasNotCalled } from './testUtils';
40
41
  import { createHeadlessForm } from '@/createHeadlessForm';
@@ -385,6 +386,17 @@ describe('jsonLogic: cross-values validations', () => {
385
386
  expect(fieldB.statement).toEqual({ description: 'Must be bigger than 4 and smaller than 8' });
386
387
  });
387
388
 
389
+ it('presentation attributes work', () => {
390
+ const { fields, handleValidation } = createHeadlessForm(
391
+ schemaWithComputedPresentationAttributes,
392
+ { strictInputType: false }
393
+ );
394
+ const fieldB = fields.find((i) => i.name === 'amount');
395
+ expect(handleValidation({ currency: 'UGX' }).formErrors).toEqual(undefined);
396
+ expect(fieldB.inputType).toEqual('money');
397
+ expect(fieldB.currency).toEqual('UGX');
398
+ });
399
+
388
400
  it('Use a inline-rule in a schema for a title attribute', () => {
389
401
  const { fields, handleValidation } = createHeadlessForm(
390
402
  schemaWithInlineRuleForComputedAttributeWithCopy,