Skip to content

Commit d304469

Browse files
authored
feat(group-array): support default values (#245)
* feat(group-array): make defaults work on group arrays * fix: typo
1 parent 726a8b6 commit d304469

3 files changed

Lines changed: 262 additions & 6 deletions

File tree

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ Once you have a `dist` folder being created, you can either:
104104
+ import { createHeadlessForm } from '../../path/to/repo/json-schema-form/dist'
105105
```
106106

107-
- Optpion B: Use [npm link](https://docs.npmjs.com/cli/v9/commands/npm-link) or [yarn link](https://classic.yarnpkg.com/lang/en/docs/cli/link/):
107+
- Option B: Use [npm link](https://docs.npmjs.com/cli/v9/commands/npm-link) or [yarn link](https://classic.yarnpkg.com/lang/en/docs/cli/link/):
108108

109109
```bash
110110
# in json-schema-form repo:

src/field/schema.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -277,14 +277,17 @@ function getArrayFields(schema: NonBooleanJsfSchema, originalSchema: NonBooleanJ
277277

278278
if (schema.items?.type === 'object') {
279279
const objectSchema = schema.items as JsfObjectSchema
280+
const originalItemsSchema = originalSchema.items as JsfObjectSchema | undefined
280281

281282
for (const key in objectSchema.properties) {
282283
const isFieldRequired = objectSchema.required?.includes(key) || false
283284
const field = buildFieldSchema({
284285
schema: objectSchema.properties[key],
285286
name: key,
286287
required: isFieldRequired,
287-
originalSchema,
288+
// Use the inner field's original schema, not the GROUP_ARRAY schema
289+
// This prevents the GROUP_ARRAY's default from being copied to inner fields
290+
originalSchema: originalItemsSchema?.properties?.[key] || objectSchema.properties[key],
288291
strictInputType,
289292
})
290293
if (field) {
@@ -298,7 +301,8 @@ function getArrayFields(schema: NonBooleanJsfSchema, originalSchema: NonBooleanJ
298301
schema: schema.items,
299302
name: 'item',
300303
required: false,
301-
originalSchema,
304+
// Use the items schema from originalSchema, not the GROUP_ARRAY schema itself
305+
originalSchema: originalSchema.items || schema.items,
302306
strictInputType,
303307
})
304308
if (field) {
@@ -376,7 +380,7 @@ export function buildFieldSchema({
376380
const inputType = getInputType(type, name, originalSchema, strictInputType)
377381
const inputHasInnerFields = ['fieldset', 'group-array'].includes(inputType)
378382

379-
return {
383+
const hiddenField: Field = {
380384
type: inputType,
381385
name,
382386
inputType,
@@ -385,6 +389,13 @@ export function buildFieldSchema({
385389
isVisible: false,
386390
...(inputHasInnerFields && { fields: [] }),
387391
}
392+
393+
// Preserve default from originalSchema for hidden fields (important for GROUP_ARRAY fields)
394+
if (originalSchema.default !== undefined) {
395+
hiddenField.default = originalSchema.default
396+
}
397+
398+
return hiddenField
388399
}
389400

390401
// If schema is any other boolean (true), just return null
@@ -418,6 +429,12 @@ export function buildFieldSchema({
418429
...(errorMessage && { errorMessage }),
419430
}
420431

432+
// Preserve default from originalSchema if it's missing in the processed schema
433+
// This is important for GROUP_ARRAY fields where defaults can be lost during conditional merging
434+
if (field.default === undefined && originalSchema.default !== undefined) {
435+
field.default = originalSchema.default
436+
}
437+
421438
if (inputType === 'checkbox') {
422439
addCheckboxAttributes(inputType, field, schema)
423440
}

test/fields/array.test.ts

Lines changed: 241 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,8 +217,6 @@ describe('buildFieldArray', () => {
217217
isVisible: true,
218218
nameKey: 'title',
219219
required: false,
220-
foo: 'bar',
221-
bar: 'baz',
222220
},
223221
],
224222
items: expect.any(Object),
@@ -1093,4 +1091,245 @@ describe('buildFieldArray', () => {
10931091
// as both will be rendered from the same fields in the `fields` property.
10941092
})
10951093
})
1094+
1095+
describe('default values', () => {
1096+
it('should preserve default values for GROUP_ARRAY fields', () => {
1097+
const schema: JsfSchema = {
1098+
type: 'array',
1099+
default: [
1100+
{ title: 'Book 1', pages: 100 },
1101+
{ title: 'Book 2', pages: 200 },
1102+
],
1103+
items: {
1104+
type: 'object',
1105+
properties: {
1106+
title: { type: 'string' },
1107+
pages: { type: 'number' },
1108+
},
1109+
},
1110+
}
1111+
1112+
const field = buildFieldSchema(schema, 'books', false)
1113+
1114+
expect(field?.default).toEqual([
1115+
{ title: 'Book 1', pages: 100 },
1116+
{ title: 'Book 2', pages: 200 },
1117+
])
1118+
})
1119+
1120+
it('should preserve default values from originalSchema when processed schema loses it', () => {
1121+
const originalSchema: JsfSchema = {
1122+
type: 'array',
1123+
default: [{ title: 'Test Book', pages: 300 }],
1124+
items: {
1125+
type: 'object',
1126+
properties: {
1127+
title: { type: 'string' },
1128+
pages: { type: 'number' },
1129+
},
1130+
},
1131+
}
1132+
1133+
// Simulate a processed schema where default was lost (e.g., during conditional merging)
1134+
const processedSchema: JsfSchema = {
1135+
type: 'array',
1136+
// default is missing here
1137+
items: {
1138+
type: 'object',
1139+
properties: {
1140+
title: { type: 'string' },
1141+
pages: { type: 'number' },
1142+
},
1143+
},
1144+
}
1145+
1146+
const field = buildField({
1147+
schema: processedSchema,
1148+
name: 'books',
1149+
required: false,
1150+
originalSchema,
1151+
strictInputType: false,
1152+
})
1153+
1154+
expect(field?.default).toEqual([{ title: 'Test Book', pages: 300 }])
1155+
})
1156+
1157+
it('should use processed schema default if both exist', () => {
1158+
const originalSchema: JsfSchema = {
1159+
type: 'array',
1160+
default: [{ title: 'Original Book', pages: 150 }],
1161+
items: {
1162+
type: 'object',
1163+
properties: {
1164+
title: { type: 'string' },
1165+
pages: { type: 'number' },
1166+
},
1167+
},
1168+
}
1169+
1170+
const processedSchema: JsfSchema = {
1171+
type: 'array',
1172+
default: [{ title: 'Updated Book', pages: 250 }],
1173+
items: {
1174+
type: 'object',
1175+
properties: {
1176+
title: { type: 'string' },
1177+
pages: { type: 'number' },
1178+
},
1179+
},
1180+
}
1181+
1182+
const field = buildField({
1183+
schema: processedSchema,
1184+
name: 'books',
1185+
required: false,
1186+
originalSchema,
1187+
strictInputType: false,
1188+
})
1189+
1190+
// Should use processed schema's default, not originalSchema's
1191+
expect(field?.default).toEqual([{ title: 'Updated Book', pages: 250 }])
1192+
})
1193+
1194+
it('should preserve default values for GROUP_ARRAY fields that become visible conditionally', () => {
1195+
const schema: JsfObjectSchema = {
1196+
type: 'object',
1197+
properties: {
1198+
has_books: {
1199+
type: 'string',
1200+
default: 'no',
1201+
oneOf: [
1202+
{ const: 'yes', title: 'Yes' },
1203+
{ const: 'no', title: 'No' },
1204+
],
1205+
},
1206+
books: {
1207+
type: 'array',
1208+
default: [
1209+
{ title: 'Book 1', pages: 100 },
1210+
{ title: 'Book 2', pages: 200 },
1211+
],
1212+
items: {
1213+
type: 'object',
1214+
properties: {
1215+
title: { type: 'string' },
1216+
pages: { type: 'number' },
1217+
},
1218+
},
1219+
},
1220+
},
1221+
allOf: [
1222+
{
1223+
if: {
1224+
properties: {
1225+
has_books: { const: 'yes' },
1226+
},
1227+
},
1228+
then: {
1229+
required: ['books'],
1230+
},
1231+
else: {
1232+
properties: {
1233+
books: false,
1234+
},
1235+
},
1236+
},
1237+
],
1238+
}
1239+
1240+
// Initially, field should be hidden but still have default
1241+
const formInitial = createHeadlessForm(schema, { initialValues: { has_books: 'no' } })
1242+
const fieldInitial = formInitial.fields.find(f => f.name === 'books')
1243+
expect(fieldInitial?.isVisible).toBe(false)
1244+
// Default should be preserved even when hidden
1245+
expect(fieldInitial?.default).toEqual([
1246+
{ title: 'Book 1', pages: 100 },
1247+
{ title: 'Book 2', pages: 200 },
1248+
])
1249+
1250+
// When field becomes visible, default should still be available
1251+
formInitial.handleValidation({ has_books: 'yes' })
1252+
const fieldVisible = formInitial.fields.find(f => f.name === 'books')
1253+
expect(fieldVisible?.isVisible).toBe(true)
1254+
expect(fieldVisible?.default).toEqual([
1255+
{ title: 'Book 1', pages: 100 },
1256+
{ title: 'Book 2', pages: 200 },
1257+
])
1258+
})
1259+
})
1260+
1261+
describe('inner field defaults', () => {
1262+
it('should NOT copy GROUP_ARRAY default to inner fields', () => {
1263+
const schema: JsfSchema = {
1264+
type: 'array',
1265+
default: [
1266+
{ title: 'Book 1', pages: 100 },
1267+
{ title: 'Book 2', pages: 200 },
1268+
],
1269+
items: {
1270+
type: 'object',
1271+
properties: {
1272+
title: {
1273+
type: 'string',
1274+
title: 'Title',
1275+
},
1276+
pages: {
1277+
type: 'number',
1278+
},
1279+
},
1280+
},
1281+
}
1282+
1283+
const field = buildFieldSchema(schema, 'books', false)
1284+
1285+
// Parent GROUP_ARRAY field should have the default array
1286+
expect(field?.default).toEqual([
1287+
{ title: 'Book 1', pages: 100 },
1288+
{ title: 'Book 2', pages: 200 },
1289+
])
1290+
1291+
// Inner fields should NOT have the parent's default array
1292+
const titleField = field?.fields?.find(f => f.name === 'title')
1293+
const pagesField = field?.fields?.find(f => f.name === 'pages')
1294+
1295+
expect(titleField?.default).toBeUndefined()
1296+
expect(pagesField?.default).toBeUndefined()
1297+
})
1298+
1299+
it('should preserve inner field defaults when specified', () => {
1300+
const schema: JsfSchema = {
1301+
type: 'array',
1302+
default: [
1303+
{ title: 'Book 1', pages: 300 },
1304+
],
1305+
items: {
1306+
type: 'object',
1307+
properties: {
1308+
title: {
1309+
type: 'string',
1310+
default: 'Untitled',
1311+
},
1312+
pages: {
1313+
type: 'number',
1314+
default: 0,
1315+
},
1316+
},
1317+
},
1318+
}
1319+
1320+
const field = buildFieldSchema(schema, 'books', false)
1321+
1322+
// Parent GROUP_ARRAY field should have the default array
1323+
expect(field?.default).toEqual([
1324+
{ title: 'Book 1', pages: 300 },
1325+
])
1326+
1327+
// Inner fields should have their own defaults, not the parent's array
1328+
const titleField = field?.fields?.find(f => f.name === 'title')
1329+
const pagesField = field?.fields?.find(f => f.name === 'pages')
1330+
1331+
expect(titleField?.default).toBe('Untitled')
1332+
expect(pagesField?.default).toBe(0)
1333+
})
1334+
})
10961335
})

0 commit comments

Comments
 (0)