Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
75d4a07
fix: small fixes for backward compatibility
antoniocapeloremote May 28, 2025
d9583a2
chore: maybe this refactor makes sense
antoniocapeloremote May 28, 2025
422217b
wip
antoniocapeloremote May 28, 2025
e47acce
chore: wip 2
antoniocapeloremote May 28, 2025
789774e
fix: fix wrong variable in the mutateSchema fn and fix 1 test
antoniocapeloremote May 29, 2025
55b551f
chore: refactoring code and cleaning tests
antoniocapeloremote May 29, 2025
e22d96b
chore: fix wrong fixture on json-logic-v0 test
antoniocapeloremote May 29, 2025
786641e
chore: wip mutating arrays
antoniocapeloremote May 29, 2025
ad59f0a
chore: revert array mutation
antoniocapeloremote May 29, 2025
961e309
fix: fix deepMerge for arrays
antoniocapeloremote May 29, 2025
011a363
chore: comment
antoniocapeloremote May 30, 2025
8ce3c45
fix: remove unused import
antoniocapeloremote May 30, 2025
c54ac6e
chore: improved array handling
antoniocapeloremote May 30, 2025
500e8bd
chore: further array refinements
antoniocapeloremote May 30, 2025
70b9a3b
chore: handling required array case
antoniocapeloremote May 30, 2025
bdfdc1c
fix: fix property cleaning on updated fields
antoniocapeloremote May 30, 2025
74ba3d1
chore: comments
antoniocapeloremote May 30, 2025
6d9f2fc
chore: refactor
antoniocapeloremote May 30, 2025
2b02720
chore: make fields visible by default
antoniocapeloremote May 30, 2025
a0a643e
chore: improve error messages by calculating complete schema for field
antoniocapeloremote May 30, 2025
5ab562b
fix: fix fixture
antoniocapeloremote May 30, 2025
81e2182
fix: consider allof/anyof/oneof when merging schemas
antoniocapeloremote Jun 2, 2025
b7821ec
fix: fix schemaDynamicValidationConst
antoniocapeloremote Jun 2, 2025
e9735cd
fix: add fallback values object on applySchemaRules fn
antoniocapeloremote Jun 2, 2025
5ae491b
chore: add support for value as a const substitute
antoniocapeloremote Jun 3, 2025
0d465f0
fix: consider original schema for calculating correct input type for …
antoniocapeloremote Jun 4, 2025
da7f9db
fix: fix missing multiple attribute in array
antoniocapeloremote Jun 4, 2025
b7095a9
chore: reorder jsdoc params
antoniocapeloremote Jun 4, 2025
d72c807
chore: refactor const fallback
antoniocapeloremote Jun 4, 2025
e1f36f8
chore: refactor buildFieldSchema to accept object
antoniocapeloremote Jun 4, 2025
cd57b6c
chore: jsdoc
antoniocapeloremote Jun 4, 2025
b6aca91
chore: jsdoc
antoniocapeloremote Jun 4, 2025
1b1dda1
chore: refactor, remove unnecessary fn
antoniocapeloremote Jun 4, 2025
8a5e5dd
chore: fix lint
antoniocapeloremote Jun 4, 2025
151ffee
chore: remove old argument
antoniocapeloremote Jun 4, 2025
e26e693
chore: rever need of value/const replacement
antoniocapeloremote Jun 4, 2025
c922796
chore: cleanup type
antoniocapeloremote Jun 4, 2025
d6afa5b
chore: rename
antoniocapeloremote Jun 4, 2025
55e3475
chore: cleanup
antoniocapeloremote Jun 4, 2025
1b6cc82
chore: address PR comments
antoniocapeloremote Jun 4, 2025
9c0eeda
chore: small simplification
antoniocapeloremote Jun 4, 2025
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
20 changes: 18 additions & 2 deletions next/src/errors/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ import { randexp } from 'randexp'
import { convertKBToMB } from '../utils'
import { DATE_FORMAT } from '../validation/custom/date'

/**
* Check if the schema is a checkbox
* @param schema - The schema to check
* @returns True if the schema is a checkbox, false otherwise
*/
function isCheckbox(schema: NonBooleanJsfSchema): boolean {
return schema['x-jsf-presentation']?.inputType === 'checkbox'
}

// Both required and const error messages are the same for checkboxes
const CHECKBOX_ACK_ERROR_MESSAGE = 'Please acknowledge this field'

export function getErrorMessage(
schema: NonBooleanJsfSchema,
value: SchemaValue,
Expand All @@ -16,13 +28,17 @@ export function getErrorMessage(
case 'type':
return getTypeErrorMessage(schema.type)
case 'required':
if (schema['x-jsf-presentation']?.inputType === 'checkbox') {
return 'Please acknowledge this field'
if (isCheckbox(schema)) {
return CHECKBOX_ACK_ERROR_MESSAGE
}
return 'Required field'
case 'forbidden':
return 'Not allowed'
case 'const':
// Boolean checkboxes that are required will come as a "const" validation error as the "empty" value is false
if (isCheckbox(schema) && value === false) {
return CHECKBOX_ACK_ERROR_MESSAGE
}
return `The only accepted value is ${JSON.stringify(schema.const)}.`
case 'enum':
return `The option "${valueToString(value)}" is not valid.`
Expand Down
89 changes: 66 additions & 23 deletions next/src/field/schema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { JsfObjectSchema, JsfSchema, JsfSchemaType, NonBooleanJsfSchema } from '../types'
import type { JsfObjectSchema, JsfSchema, JsfSchemaType, NonBooleanJsfSchema, SchemaValue } from '../types'
import type { Field, FieldOption, FieldType } from './type'
import { setCustomOrder } from '../custom/order'

Expand Down Expand Up @@ -42,13 +42,15 @@ function addOptions(field: Field, schema: NonBooleanJsfSchema) {
* Add fields attribute to a field
* @param field - The field to add the fields to
* @param schema - The schema of the field
* @param originalSchema - The original schema (needed for calculating the original input type on conditionally hidden fields)
* @param strictInputType - Whether to strictly enforce the input type
* @description
* This adds the fields attribute to based on the schema's items.
* Since options and fields are mutually exclusive, we only add fields if no options were provided.
*/
function addFields(field: Field, schema: NonBooleanJsfSchema, strictInputType?: boolean) {
function addFields(field: Field, schema: NonBooleanJsfSchema, originalSchema: JsfSchema, strictInputType?: boolean) {
if (field.options === undefined) {
const fields = getFields(schema, strictInputType)
const fields = getFields(schema, originalSchema, strictInputType)
if (fields) {
field.fields = fields
}
Expand Down Expand Up @@ -129,8 +131,6 @@ You can fix the json schema or skip this error by calling createHeadlessForm(sch
if (schema.properties) {
return 'select'
}

// Otherwise, assume "string" as the fallback type and get input from it
}

// Get input type from schema (fallback type is "string")
Expand Down Expand Up @@ -201,7 +201,7 @@ function getFieldOptions(schema: NonBooleanJsfSchema) {
if (schema.enum) {
const enumAsOneOf: JsfSchema['oneOf'] = schema.enum?.map(value => ({
title: typeof value === 'string' ? value : JSON.stringify(value),
const: value,
const: value as SchemaValue,
})) || []
return convertToOptions(enumAsOneOf)
}
Expand All @@ -212,15 +212,22 @@ function getFieldOptions(schema: NonBooleanJsfSchema) {
/**
* Get the fields for an object schema
* @param schema - The schema of the field
* @param originalSchema - The original schema (needed for calculating the original input type on conditionally hidden fields)
* @param strictInputType - Whether to strictly enforce the input type
* @returns The fields for the schema or an empty array if the schema does not define any properties
*/
function getObjectFields(schema: NonBooleanJsfSchema, strictInputType?: boolean): Field[] | null {
function getObjectFields(schema: NonBooleanJsfSchema, originalSchema: NonBooleanJsfSchema, strictInputType?: boolean): Field[] | null {
const fields: Field[] = []

for (const key in schema.properties) {
const isRequired = schema.required?.includes(key) || false
const field = buildFieldSchema(schema.properties[key], key, isRequired, strictInputType)
const field = buildFieldSchema({
schema: schema.properties[key],
name: key,
required: isRequired,
originalSchema: originalSchema.properties?.[key] || schema.properties[key],
strictInputType,
})
if (field) {
fields.push(field)
}
Expand All @@ -234,10 +241,11 @@ function getObjectFields(schema: NonBooleanJsfSchema, strictInputType?: boolean)
/**
* Get the fields for an array schema
* @param schema - The schema of the field
* @param originalSchema - The original schema (needed for calculating the original input type on conditionally hidden fields)
* @param strictInputType - Whether to strictly enforce the input type
* @returns The fields for the schema or an empty array if the schema does not define any items
*/
function getArrayFields(schema: NonBooleanJsfSchema, strictInputType?: boolean): Field[] {
function getArrayFields(schema: NonBooleanJsfSchema, originalSchema: NonBooleanJsfSchema, strictInputType?: boolean): Field[] {
const fields: Field[] = []

if (typeof schema.items !== 'object' || schema.items === null) {
Expand All @@ -249,15 +257,27 @@ function getArrayFields(schema: NonBooleanJsfSchema, strictInputType?: boolean):

for (const key in objectSchema.properties) {
const isFieldRequired = objectSchema.required?.includes(key) || false
const field = buildFieldSchema(objectSchema.properties[key], key, isFieldRequired, strictInputType)
const field = buildFieldSchema({
schema: objectSchema.properties[key],
name: key,
required: isFieldRequired,
originalSchema,
strictInputType,
})
if (field) {
field.nameKey = key
fields.push(field)
}
}
}
else {
const field = buildFieldSchema(schema.items, 'item', false, strictInputType)
const field = buildFieldSchema({
schema: schema.items,
name: 'item',
required: false,
originalSchema,
strictInputType,
})
if (field) {
fields.push(field)
}
Expand All @@ -271,15 +291,16 @@ function getArrayFields(schema: NonBooleanJsfSchema, strictInputType?: boolean):
/**
* Get the fields for a schema from either `items` or `properties`
* @param schema - The schema of the field
* @param originalSchema - The original schema (needed for calculating the original input type on conditionally hidden fields)
* @param strictInputType - Whether to strictly enforce the input type
* @returns The fields for the schema
*/
function getFields(schema: NonBooleanJsfSchema, strictInputType?: boolean): Field[] | null {
function getFields(schema: NonBooleanJsfSchema, originalSchema: NonBooleanJsfSchema, strictInputType?: boolean): Field[] | null {
if (typeof schema.properties === 'object' && schema.properties !== null) {
return getObjectFields(schema, strictInputType)
return getObjectFields(schema, originalSchema, strictInputType)
}
else if (typeof schema.items === 'object' && schema.items !== null) {
return getArrayFields(schema, strictInputType)
return getArrayFields(schema, originalSchema, strictInputType)
}

return null
Expand All @@ -298,26 +319,48 @@ const excludedSchemaProps = [
'properties', // Handled separately
]

interface BuildFieldSchemaParams {
schema: JsfSchema
name: string
required?: boolean
originalSchema: NonBooleanJsfSchema
strictInputType?: boolean
type?: JsfSchemaType
}

/**
* Build a field from any schema
* @param params - The parameters for building the field
* @param params.schema - The schema of the field
* @param params.name - The name of the field
* @param params.required - Whether the field is required
* @param params.originalSchema - The original schema (needed for calculating the original input type on conditionally hidden fields)
* @param params.strictInputType - Whether to strictly enforce the input type
* @param params.type - The schema type
* @returns The field
*/
export function buildFieldSchema(
schema: JsfSchema,
name: string,
required: boolean = false,
strictInputType: boolean = false,
type: JsfSchemaType = undefined,
): Field | null {
export function buildFieldSchema({
schema,
name,
required = false,
originalSchema,
strictInputType = false,
type = undefined,
}: BuildFieldSchemaParams): Field | null {
// If schema is boolean false, return a field with isVisible=false
if (schema === false) {
const inputType = getInputType(type, name, schema, strictInputType)
// If the schema is false (hidden field), we use the original schema to get the input type
const inputType = getInputType(type, name, originalSchema, strictInputType)
const inputHasInnerFields = ['fieldset', 'group-array'].includes(inputType)

return {
type: inputType,
name,
inputType,
jsonType: 'boolean',
required,
isVisible: false,
...(inputHasInnerFields && { fields: [] }),
}
}

Expand Down Expand Up @@ -369,7 +412,7 @@ export function buildFieldSchema(
}

addOptions(field, schema)
addFields(field, schema)
addFields(field, schema, originalSchema)

return field
}
3 changes: 2 additions & 1 deletion next/src/field/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface Field {
options?: unknown[]
const?: unknown
checkboxValue?: unknown
default?: unknown

// Allow additional properties from x-jsf-presentation (e.g. meta from oneOf/anyOf)
[key: string]: unknown
Expand All @@ -44,4 +45,4 @@ export interface FieldOption {
[key: string]: unknown
}

export type FieldType = 'text' | 'number' | 'select' | 'file' | 'radio' | 'group-array' | 'email' | 'date' | 'checkbox' | 'fieldset' | 'money' | 'country' | 'textarea'
export type FieldType = 'text' | 'number' | 'select' | 'file' | 'radio' | 'group-array' | 'email' | 'date' | 'checkbox' | 'fieldset' | 'money' | 'country' | 'textarea' | 'hidden'
64 changes: 24 additions & 40 deletions next/src/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import type { JsfObjectSchema, JsfSchema, SchemaValue } from './types'
import type { ValidationOptions } from './validation/schema'
import { getErrorMessage } from './errors/messages'
import { buildFieldSchema } from './field/schema'
import { mutateFields } from './mutations'
import { applyComputedAttrsToSchema } from './validation/json-logic'
import { calculateFinalSchema, updateFieldProperties } from './mutations'
import { validateSchema } from './validation/schema'

export { ValidationOptions } from './validation/schema'
Expand Down Expand Up @@ -231,9 +230,15 @@ export interface CreateHeadlessFormOptions {
strictInputType?: boolean
}

function buildFields(params: { schema: JsfObjectSchema, strictInputType?: boolean }): Field[] {
const { schema, strictInputType } = params
const fields = buildFieldSchema(schema, 'root', true, strictInputType, 'object')?.fields || []
function buildFields(params: { schema: JsfObjectSchema, originalSchema: JsfObjectSchema, strictInputType?: boolean }): Field[] {
const { schema, originalSchema, strictInputType } = params
const fields = buildFieldSchema({
schema,
name: 'root',
required: true,
originalSchema,
strictInputType,
})?.fields || []
return fields
}

Expand All @@ -243,25 +248,29 @@ export function createHeadlessForm(
): FormResult {
const initialValues = options.initialValues || {}
const strictInputType = options.strictInputType || false
// Make a (new) version with all the computed attrs computed and applied
const updatedSchema = applyComputedAttrsToSchema(schema, schema['x-jsf-logic']?.computedValues, initialValues)
const fields = buildFields({ schema: updatedSchema, strictInputType })
// Make a new version of the schema with all the computed attrs applied, as well as the final version of each property (taking into account conditional rules)
const updatedSchema = calculateFinalSchema({
schema,
values: initialValues,
options: options.validationOptions,
})

// Making sure field properties are correct for the initial values
mutateFields(fields, initialValues, updatedSchema, options.validationOptions)
const fields = buildFields({ schema: updatedSchema, originalSchema: schema, strictInputType })

// TODO: check if we need this isError variable exposed
const isError = false

const handleValidation = (value: SchemaValue) => {
const updatedSchema = applyComputedAttrsToSchema(schema, schema['x-jsf-logic']?.computedValues, value)
const updatedSchema = calculateFinalSchema({
schema,
values: value,
options: options.validationOptions,
})

const result = validate(value, updatedSchema, options.validationOptions)

// Fields properties might have changed, so we need to reset the fields by updating them in place
buildFieldsInPlace(fields, updatedSchema)

// Updating field properties based on the new form value
mutateFields(fields, value, updatedSchema, options.validationOptions)
updateFieldProperties(fields, updatedSchema, schema)

return result
}
Expand All @@ -273,28 +282,3 @@ export function createHeadlessForm(
handleValidation,
}
}

/**
* Updates fields in place based on a schema, recursively if needed
* @param fields - The fields array to mutate
* @param schema - The schema to use for updating fields
*/
function buildFieldsInPlace(fields: Field[], schema: JsfObjectSchema): void {
// Clear existing fields array
fields.length = 0

// Get new fields from schema
const newFields = buildFieldSchema(schema, 'root', true, false, 'object')?.fields || []

// Push all new fields into existing array
fields.push(...newFields)

// Recursively update any nested fields
for (const field of fields) {
// eslint-disable-next-line ts/ban-ts-comment
// @ts-expect-error
if (field.fields && schema.properties?.[field.name]?.type === 'object') {
buildFieldsInPlace(field.fields, schema.properties[field.name] as JsfObjectSchema)
}
}
}
Loading