Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ Once you have a `dist` folder being created, you can either:
+ import { createHeadlessForm } from '../../path/to/repo/json-schema-form/dist'
```

- 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/):
- 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/):

```bash
# in json-schema-form repo:
Expand Down
23 changes: 20 additions & 3 deletions src/field/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,14 +277,17 @@ function getArrayFields(schema: NonBooleanJsfSchema, originalSchema: NonBooleanJ

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

for (const key in objectSchema.properties) {
const isFieldRequired = objectSchema.required?.includes(key) || false
const field = buildFieldSchema({
schema: objectSchema.properties[key],
name: key,
required: isFieldRequired,
originalSchema,
// Use the inner field's original schema, not the GROUP_ARRAY schema
// This prevents the GROUP_ARRAY's default from being copied to inner fields
originalSchema: originalItemsSchema?.properties?.[key] || objectSchema.properties[key],
strictInputType,
})
if (field) {
Expand All @@ -298,7 +301,8 @@ function getArrayFields(schema: NonBooleanJsfSchema, originalSchema: NonBooleanJ
schema: schema.items,
name: 'item',
required: false,
originalSchema,
// Use the items schema from originalSchema, not the GROUP_ARRAY schema itself
originalSchema: originalSchema.items || schema.items,
strictInputType,
})
if (field) {
Expand Down Expand Up @@ -376,7 +380,7 @@ export function buildFieldSchema({
const inputType = getInputType(type, name, originalSchema, strictInputType)
const inputHasInnerFields = ['fieldset', 'group-array'].includes(inputType)

return {
const hiddenField: Field = {
type: inputType,
name,
inputType,
Expand All @@ -385,6 +389,13 @@ export function buildFieldSchema({
isVisible: false,
...(inputHasInnerFields && { fields: [] }),
}

// Preserve default from originalSchema for hidden fields (important for GROUP_ARRAY fields)
if (originalSchema.default !== undefined) {
hiddenField.default = originalSchema.default
}

return hiddenField
}

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

// Preserve default from originalSchema if it's missing in the processed schema
// This is important for GROUP_ARRAY fields where defaults can be lost during conditional merging
if (field.default === undefined && originalSchema.default !== undefined) {
field.default = originalSchema.default
}

if (inputType === 'checkbox') {
addCheckboxAttributes(inputType, field, schema)
}
Expand Down
243 changes: 241 additions & 2 deletions test/fields/array.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,8 +217,6 @@ describe('buildFieldArray', () => {
isVisible: true,
nameKey: 'title',
required: false,
foo: 'bar',
bar: 'baz',
},
],
items: expect.any(Object),
Expand Down Expand Up @@ -1093,4 +1091,245 @@ describe('buildFieldArray', () => {
// as both will be rendered from the same fields in the `fields` property.
})
})

describe('default values', () => {
it('should preserve default values for GROUP_ARRAY fields', () => {
const schema: JsfSchema = {
type: 'array',
default: [
{ title: 'Book 1', pages: 100 },
{ title: 'Book 2', pages: 200 },
],
items: {
type: 'object',
properties: {
title: { type: 'string' },
pages: { type: 'number' },
},
},
}

const field = buildFieldSchema(schema, 'books', false)

expect(field?.default).toEqual([
{ title: 'Book 1', pages: 100 },
{ title: 'Book 2', pages: 200 },
])
})

it('should preserve default values from originalSchema when processed schema loses it', () => {
const originalSchema: JsfSchema = {
type: 'array',
default: [{ title: 'Test Book', pages: 300 }],
items: {
type: 'object',
properties: {
title: { type: 'string' },
pages: { type: 'number' },
},
},
}

// Simulate a processed schema where default was lost (e.g., during conditional merging)
const processedSchema: JsfSchema = {
type: 'array',
// default is missing here
items: {
type: 'object',
properties: {
title: { type: 'string' },
pages: { type: 'number' },
},
},
}

const field = buildField({
schema: processedSchema,
name: 'books',
required: false,
originalSchema,
strictInputType: false,
})

expect(field?.default).toEqual([{ title: 'Test Book', pages: 300 }])
})

it('should use processed schema default if both exist', () => {
const originalSchema: JsfSchema = {
type: 'array',
default: [{ title: 'Original Book', pages: 150 }],
items: {
type: 'object',
properties: {
title: { type: 'string' },
pages: { type: 'number' },
},
},
}

const processedSchema: JsfSchema = {
type: 'array',
default: [{ title: 'Updated Book', pages: 250 }],
items: {
type: 'object',
properties: {
title: { type: 'string' },
pages: { type: 'number' },
},
},
}

const field = buildField({
schema: processedSchema,
name: 'books',
required: false,
originalSchema,
strictInputType: false,
})

// Should use processed schema's default, not originalSchema's
expect(field?.default).toEqual([{ title: 'Updated Book', pages: 250 }])
})

it('should preserve default values for GROUP_ARRAY fields that become visible conditionally', () => {
const schema: JsfObjectSchema = {
type: 'object',
properties: {
has_books: {
type: 'string',
default: 'no',
oneOf: [
{ const: 'yes', title: 'Yes' },
{ const: 'no', title: 'No' },
],
},
books: {
type: 'array',
default: [
{ title: 'Book 1', pages: 100 },
{ title: 'Book 2', pages: 200 },
],
items: {
type: 'object',
properties: {
title: { type: 'string' },
pages: { type: 'number' },
},
},
},
},
allOf: [
{
if: {
properties: {
has_books: { const: 'yes' },
},
},
then: {
required: ['books'],
},
else: {
properties: {
books: false,
},
},
},
],
}

// Initially, field should be hidden but still have default
const formInitial = createHeadlessForm(schema, { initialValues: { has_books: 'no' } })
const fieldInitial = formInitial.fields.find(f => f.name === 'books')
expect(fieldInitial?.isVisible).toBe(false)
// Default should be preserved even when hidden
expect(fieldInitial?.default).toEqual([
{ title: 'Book 1', pages: 100 },
{ title: 'Book 2', pages: 200 },
])

// When field becomes visible, default should still be available
formInitial.handleValidation({ has_books: 'yes' })
const fieldVisible = formInitial.fields.find(f => f.name === 'books')
expect(fieldVisible?.isVisible).toBe(true)
expect(fieldVisible?.default).toEqual([
{ title: 'Book 1', pages: 100 },
{ title: 'Book 2', pages: 200 },
])
})
})

describe('inner field defaults', () => {
it('should NOT copy GROUP_ARRAY default to inner fields', () => {
const schema: JsfSchema = {
type: 'array',
default: [
{ title: 'Book 1', pages: 100 },
{ title: 'Book 2', pages: 200 },
],
items: {
type: 'object',
properties: {
title: {
type: 'string',
title: 'Title',
},
pages: {
type: 'number',
},
},
},
}

const field = buildFieldSchema(schema, 'books', false)

// Parent GROUP_ARRAY field should have the default array
expect(field?.default).toEqual([
{ title: 'Book 1', pages: 100 },
{ title: 'Book 2', pages: 200 },
])

// Inner fields should NOT have the parent's default array
const titleField = field?.fields?.find(f => f.name === 'title')
const pagesField = field?.fields?.find(f => f.name === 'pages')

expect(titleField?.default).toBeUndefined()
expect(pagesField?.default).toBeUndefined()
})

it('should preserve inner field defaults when specified', () => {
const schema: JsfSchema = {
type: 'array',
default: [
{ title: 'Book 1', pages: 300 },
],
items: {
type: 'object',
properties: {
title: {
type: 'string',
default: 'Untitled',
},
pages: {
type: 'number',
default: 0,
},
},
},
}

const field = buildFieldSchema(schema, 'books', false)

// Parent GROUP_ARRAY field should have the default array
expect(field?.default).toEqual([
{ title: 'Book 1', pages: 300 },
])

// Inner fields should have their own defaults, not the parent's array
const titleField = field?.fields?.find(f => f.name === 'title')
const pagesField = field?.fields?.find(f => f.name === 'pages')

expect(titleField?.default).toBe('Untitled')
expect(pagesField?.default).toBe(0)
})
})
})