Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f3fbf98
feat: support computed attributes by mutating the schema in one pass
antoniocapeloremote May 20, 2025
fb4057f
chore: refactor
antoniocapeloremote May 20, 2025
115f49e
chore: add missing import
antoniocapeloremote May 20, 2025
611ceb2
chore: lint fix
antoniocapeloremote May 20, 2025
7f72778
chore: refactor
antoniocapeloremote May 20, 2025
a4072dd
chore: remove validateJsonLogicComputedAttributes as its done via the…
antoniocapeloremote May 20, 2025
2d16dec
fix: fix missing recursion
antoniocapeloremote May 20, 2025
134ae9b
chore: add unit test for applyComputedAttrsToSchema
antoniocapeloremote May 20, 2025
f01bb59
fix: fix lint
antoniocapeloremote May 20, 2025
2e0bbb9
fix: fix lint
antoniocapeloremote May 20, 2025
03842c9
feat: support computed attributes on conditional branches
antoniocapeloremote May 23, 2025
e2919c7
chore: adding all json logic tests from v0
antoniocapeloremote May 23, 2025
cc24bcf
chore: refactor tests
antoniocapeloremote May 23, 2025
9fe429f
chore: improve recursion
antoniocapeloremote May 23, 2025
d4213b5
chore: use first error message from validation errors
antoniocapeloremote May 23, 2025
bf75b63
chore: add (applicable) v0 tests for json-logic
antoniocapeloremote May 23, 2025
a75c002
chore: add comment
antoniocapeloremote May 23, 2025
a5a59c2
chore: cleanup
antoniocapeloremote May 23, 2025
24d6458
chore: cleanup
antoniocapeloremote May 23, 2025
0b3650b
chore: rewrite comment
antoniocapeloremote May 26, 2025
d29bc6f
chore: comments
antoniocapeloremote May 26, 2025
ac3eaf0
chore: comments
antoniocapeloremote May 26, 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
14 changes: 9 additions & 5 deletions next/src/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ 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 { validateSchema } from './validation/schema'

export { ValidationOptions } from './validation/schema'
Expand Down Expand Up @@ -242,22 +243,25 @@ export function createHeadlessForm(
): FormResult {
const initialValues = options.initialValues || {}
const strictInputType = options.strictInputType || false
const fields = buildFields({ schema, strictInputType })
// 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 })

// Making sure field properties are correct for the initial values
mutateFields(fields, initialValues, schema)
mutateFields(fields, initialValues, updatedSchema, options.validationOptions)

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

const handleValidation = (value: SchemaValue) => {
const result = validate(value, schema, options.validationOptions)
const updatedSchema = applyComputedAttrsToSchema(schema, schema['x-jsf-logic']?.computedValues, value)
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, schema)
buildFieldsInPlace(fields, updatedSchema)

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

return result
}
Expand Down
36 changes: 24 additions & 12 deletions next/src/mutations.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { Field } from './field/type'
import type { JsfObjectSchema, JsfSchema, NonBooleanJsfSchema, ObjectValue, SchemaValue } from './types'
import type { JsfObjectSchema, JsfSchema, JsonLogicContext, NonBooleanJsfSchema, ObjectValue, SchemaValue } from './types'
import type { ValidationOptions } from './validation/schema'
import { buildFieldSchema } from './field/schema'
import { applyComputedAttrsToSchema, getJsonLogicContextFromSchema } from './validation/json-logic'
import { validateSchema } from './validation/schema'
import { isObjectValue } from './validation/util'

Expand All @@ -22,16 +23,20 @@ export function mutateFields(
return
}

// Apply rules to current level of fields
applySchemaRules(fields, values, schema, options)
// We should get the json-logic context from the schema in case we need to mutate fields using computed values
const jsonLogicSchema = schema['x-jsf-logic']
const jsonLogicContext = jsonLogicSchema ? getJsonLogicContextFromSchema(jsonLogicSchema, values) : undefined

// Apply schema rules to current level of fields
applySchemaRules(fields, values, schema, options, jsonLogicContext)

// Process nested object fields that have conditional logic
for (const fieldName in schema.properties) {
const fieldSchema = schema.properties[fieldName]
const field = fields.find(field => field.name === fieldName)

if (field?.fields) {
applySchemaRules(field.fields, values[fieldName], fieldSchema as JsfObjectSchema, options)
applySchemaRules(field.fields, values[fieldName], fieldSchema as JsfObjectSchema, options, jsonLogicContext)
}
}
}
Expand Down Expand Up @@ -77,13 +82,14 @@ function evaluateConditional(
* @param values - The current form values
* @param schema - The JSON schema containing the rules
* @param options - Validation options
*
* @param jsonLogicContext - JSON Logic context
*/
function applySchemaRules(
fields: Field[],
values: SchemaValue,
schema: JsfObjectSchema,
options: ValidationOptions = {},
jsonLogicContext: JsonLogicContext | undefined,
) {
if (!isObjectValue(values)) {
return
Expand All @@ -108,11 +114,11 @@ function applySchemaRules(
for (const { rule, matches } of conditionalRules) {
// If the rule matches, process the then branch
if (matches && rule.then) {
processBranch(fields, values, rule.then, options)
processBranch(fields, values, rule.then, options, jsonLogicContext)
}
// If the rule doesn't match, process the else branch
else if (!matches && rule.else) {
processBranch(fields, values, rule.else, options)
processBranch(fields, values, rule.else, options, jsonLogicContext)
}
}
}
Expand All @@ -123,14 +129,20 @@ function applySchemaRules(
* @param values - The current form values
* @param branch - The branch (schema representing and then/else) to process
* @param options - Validation options
* @param jsonLogicContext - JSON Logic context
*/
function processBranch(fields: Field[], values: SchemaValue, branch: JsfSchema, options: ValidationOptions = {}) {
function processBranch(fields: Field[], values: SchemaValue, branch: JsfSchema, options: ValidationOptions = {}, jsonLogicContext: JsonLogicContext | undefined) {
if (branch.properties) {
// Cycle through each property in the schema and search for any property that needs
// to be updated in the fields collection.
// Note: False schemas mean the field should be hidden in the form (isVisible = false)
for (const fieldName in branch.properties) {
const fieldSchema = branch.properties[fieldName]
let fieldSchema = branch.properties[fieldName]

// If the field schema has computed attributes, we need to apply them
if (fieldSchema['x-jsf-logic-computedAttrs']) {
fieldSchema = applyComputedAttrsToSchema(fieldSchema as JsfObjectSchema, jsonLogicContext?.schema.computedValues, values)
}

const field = fields.find(e => e.name === fieldName)
if (field) {
// If the field has a false schema, it should be removed from the form (hidden)
Expand All @@ -139,7 +151,7 @@ function processBranch(fields: Field[], values: SchemaValue, branch: JsfSchema,
}
// If the field has inner fields, we need to process them
else if (field?.fields) {
processBranch(field.fields, values, fieldSchema)
processBranch(field.fields, values, fieldSchema, options, jsonLogicContext)
}
// If the field has properties being declared on this branch, we need to update the field
// with the new properties
Expand All @@ -164,5 +176,5 @@ function processBranch(fields: Field[], values: SchemaValue, branch: JsfSchema,
}

// Apply rules to the branch
applySchemaRules(fields, values, branch as JsfObjectSchema, options)
applySchemaRules(fields, values, branch as JsfObjectSchema, options, jsonLogicContext)
}
2 changes: 1 addition & 1 deletion next/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export type JsfSchema = JSONSchema & {
// Extra validations to run. References validations in the `x-jsf-logic` root property.
'x-jsf-logic-validations'?: string[]
// Extra attributes to add to the schema. References computedValues in the `x-jsf-logic` root property.
'x-jsf-logic-computedAttrs'?: Partial<Record<keyof NonBooleanJsfSchema, string | JsfSchema['x-jsf-errorMessage']>>
'x-jsf-logic-computedAttrs'?: Record<string, string | object>
}

/**
Expand Down
Loading