Skip to content

Composition

Utilities for merging form configurations and composing validation schemas from multiple sources.

Import

ts
import { mergeFormConfigs, composeSchemas } from '@ereo/forms'

mergeFormConfigs

Deep-merges two FormConfig objects into one. Useful for building forms from reusable config fragments.

Signature

ts
function mergeFormConfigs<
  A extends Record<string, any>,
  B extends Record<string, any>,
>(configA: FormConfig<A>, configB: FormConfig<B>): FormConfig<A & B>

Merge Rules

PropertyStrategy
defaultValuesDeep-merged (B overrides A for conflicting keys)
validatorsConcatenated -- same-path validators from both configs are combined into arrays
onSubmitB wins (B ?? A)
schemaB wins (B ?? A)
validateOnB wins (B ?? A)
validateOnMountB wins (B ?? A)
resetOnSubmitB wins (B ?? A)

Example

ts
import { mergeFormConfigs, useForm, required, email, minLength } from '@ereo/forms'

const accountConfig = {
  defaultValues: { email: '', password: '' },
  validators: {
    email: [required(), email()],
    password: [required(), minLength(8)],
  },
}

const profileConfig = {
  defaultValues: { name: '', bio: '' },
  validators: {
    name: required(),
  },
}

// Combine into one form
const merged = mergeFormConfigs(accountConfig, profileConfig)
const form = useForm(merged)
// form.values has { email, password, name, bio }

Validator Concatenation

If both configs have validators for the same path, they are combined:

ts
const a = {
  defaultValues: { email: '' },
  validators: { email: required() },
}
const b = {
  defaultValues: { email: '' },
  validators: { email: email() },
}

const merged = mergeFormConfigs(a, b)
// merged.validators.email === [required(), email()]

composeSchemas

Combines two validation schemas under different prefixes into one schema.

Signature

ts
function composeSchemas<A, B>(
  prefix1: string,
  schema1: ValidationSchema<unknown, A>,
  prefix2: string,
  schema2: ValidationSchema<unknown, B>
): ValidationSchema<unknown, Record<string, unknown>>

Parameters

NameTypeDescription
prefix1stringKey for the first schema's data (e.g. 'account')
schema1ValidationSchemaFirst schema
prefix2stringKey for the second schema's data (e.g. 'profile')
schema2ValidationSchemaSecond schema

Example

ts
import { composeSchemas, zodAdapter } from '@ereo/forms'
import { z } from 'zod'

const addressSchema = zodAdapter(z.object({
  street: z.string().min(1),
  city: z.string().min(1),
}))

const paymentSchema = zodAdapter(z.object({
  cardNumber: z.string().min(16),
  expiry: z.string(),
}))

const combinedSchema = composeSchemas(
  'shipping', addressSchema,
  'payment', paymentSchema
)

// Validates: { shipping: { street, city }, payment: { cardNumber, expiry } }
// Errors are prefixed: 'shipping.street', 'payment.cardNumber', etc.

How It Works

  • parse() calls schema1.parse(data[prefix1]) and schema2.parse(data[prefix2])
  • safeParse() collects all issues from both schemas, prefixing paths with the respective prefix
  • If both schemas pass, returns the combined result
  • If either fails, returns all issues from both

Released under the MIT License.