Testers & Registry API
The component registry and tester system controls which component renders each field.
Overview
QuickForms uses a priority-based tester system borrowed from JSONForms:
- For each field, all registered testers are evaluated
- Each tester returns a priority number (higher = better match)
- The component with the highest priority renders the field
This allows you to:
- Override built-in components
- Add components for custom formats
- Create specialized components for specific use cases
Component Registry
createDefaultRegistry
Creates a registry with all standard Vue components registered.
function createDefaultRegistry(): ComponentRegistry<Component>Example:
import { createDefaultRegistry } from '@quickflo/quickforms-vue'
const registry = createDefaultRegistry()createQuasarRegistry
Creates a registry with all Quasar components registered.
function createQuasarRegistry(): ComponentRegistry<Component>Example:
import { createQuasarRegistry } from '@quickflo/quickforms-quasar'
const registry = createQuasarRegistry()ComponentRegistry Methods
register
Register a new component with its tester.
registry.register(
name: string,
component: Component,
tester: TesterFunction
): voidParameters:
name- Unique identifier for the componentcomponent- Vue componenttester- Function that returns priority number
Example:
import CustomInput from './CustomInput.vue'
import { rankWith, isStringType } from '@quickflo/quickforms-vue'
registry.register(
'custom-string',
CustomInput,
rankWith(10, isStringType)
)getComponent
Get the best-matching component for a schema.
registry.getComponent(schema: JSONSchema): Component | undefinedExample:
const schema = { type: 'string', format: 'email' }
const component = registry.getComponent(schema)Tester Functions
Testers evaluate schemas and return priority numbers.
TesterFunction
type TesterFunction = (schema: JSONSchema) => numberReturns:
0- Does not match> 0- Matches (higher = better match)
rankWith
Combines a priority rank with a predicate function.
function rankWith(
rank: number,
predicate: (schema: JSONSchema) => boolean
): TesterFunctionExample:
import { rankWith, isStringType } from '@quickflo/quickforms-vue'
// Priority 10 if schema is string type
const tester = rankWith(10, isStringType)
console.log(tester({ type: 'string' })) // 10
console.log(tester({ type: 'number' })) // 0Type Testers
Check the JSON Schema type property.
isStringType
function isStringType(schema: JSONSchema): booleanReturns true if schema.type === 'string'.
isNumberType
function isNumberType(schema: JSONSchema): booleanReturns true if schema.type === 'number'.
isIntegerType
function isIntegerType(schema: JSONSchema): booleanReturns true if schema.type === 'integer'.
isBooleanType
function isBooleanType(schema: JSONSchema): booleanReturns true if schema.type === 'boolean'.
isObjectType
function isObjectType(schema: JSONSchema): booleanReturns true if schema.type === 'object'.
isArrayType
function isArrayType(schema: JSONSchema): booleanReturns true if schema.type === 'array'.
isNullType
function isNullType(schema: JSONSchema): booleanReturns true if schema.type === 'null'.
isEnumType
function isEnumType(schema: JSONSchema): booleanReturns true if schema has enum property.
Format Testers
Check the JSON Schema format property.
hasFormat
function hasFormat(format?: string): (schema: JSONSchema) => booleanReturns predicate that checks if schema has a specific format.
Examples:
import { hasFormat } from '@quickflo/quickforms-vue'
// Check for specific format
const isEmail = hasFormat('email')
console.log(isEmail({ type: 'string', format: 'email' })) // true
// Check for any format
const hasAnyFormat = hasFormat()
console.log(hasAnyFormat({ type: 'string', format: 'email' })) // true
console.log(hasAnyFormat({ type: 'string' })) // falseisEmailFormat
function isEmailFormat(schema: JSONSchema): booleanReturns true if schema.format === 'email'.
isDateFormat
function isDateFormat(schema: JSONSchema): booleanReturns true if schema.format === 'date'.
isTimeFormat
function isTimeFormat(schema: JSONSchema): booleanReturns true if schema.format === 'time'.
isDateTimeFormat
function isDateTimeFormat(schema: JSONSchema): booleanReturns true if schema.format === 'date-time'.
isUrlFormat
function isUrlFormat(schema: JSONSchema): booleanReturns true if schema.format === 'url' or 'uri'.
Composition Testers
Check for schema composition keywords.
hasOneOf
function hasOneOf(schema: JSONSchema): booleanReturns true if schema has oneOf property.
hasAnyOf
function hasAnyOf(schema: JSONSchema): booleanReturns true if schema has anyOf property.
hasAllOf
function hasAllOf(schema: JSONSchema): booleanReturns true if schema has allOf property.
hasConst
function hasConst(schema: JSONSchema): booleanReturns true if schema has const property.
hasConditional
function hasConditional(schema: JSONSchema): booleanReturns true if schema has if/then/else conditional.
isDiscriminatedUnion
function isDiscriminatedUnion(schema: JSONSchema): booleanReturns true if schema is a discriminated union (oneOf with discriminator).
Extension Testers
Check for custom schema extensions.
hasExtension
function hasExtension(
key: string,
value?: any
): (schema: JSONSchema) => booleanReturns predicate that checks for custom x-* properties.
Examples:
import { hasExtension } from '@quickflo/quickforms-vue'
// Check if extension exists
const hasWidget = hasExtension('x-widget')
console.log(hasWidget({ type: 'string', 'x-widget': 'color' })) // true
// Check if extension has specific value
const isColorWidget = hasExtension('x-widget', 'color')
console.log(isColorWidget({ type: 'string', 'x-widget': 'color' })) // true
console.log(isColorWidget({ type: 'string', 'x-widget': 'slider' })) // falseLogical Combinators
Combine multiple testers with logical operators.
and
function and(
...predicates: Array<(schema: JSONSchema) => boolean>
): (schema: JSONSchema) => booleanReturns true if ALL predicates match.
Example:
import { and, isStringType, hasFormat } from '@quickflo/quickforms-vue'
// Must be string AND have email format
const isEmailString = and(
isStringType,
hasFormat('email')
)
console.log(isEmailString({ type: 'string', format: 'email' })) // true
console.log(isEmailString({ type: 'string' })) // falseor
function or(
...predicates: Array<(schema: JSONSchema) => boolean>
): (schema: JSONSchema) => booleanReturns true if ANY predicate matches.
Example:
import { or, hasFormat } from '@quickflo/quickforms-vue'
// Email OR URL format
const isEmailOrUrl = or(
hasFormat('email'),
hasFormat('url')
)
console.log(isEmailOrUrl({ type: 'string', format: 'email' })) // true
console.log(isEmailOrUrl({ type: 'string', format: 'url' })) // true
console.log(isEmailOrUrl({ type: 'string' })) // falsenot
function not(
predicate: (schema: JSONSchema) => boolean
): (schema: JSONSchema) => booleanInverts a predicate.
Example:
import { not, hasFormat } from '@quickflo/quickforms-vue'
// String without a format
const isPlainString = not(hasFormat())
console.log(isPlainString({ type: 'string' })) // true
console.log(isPlainString({ type: 'string', format: 'email' })) // falseComplete Examples
Example 1: Custom Phone Input
import { rankWith, and, isStringType, hasFormat } from '@quickflo/quickforms-vue'
import PhoneInput from './PhoneInput.vue'
// Priority 10 for strings with phone format
const isPhoneField = rankWith(10, and(
isStringType,
hasFormat('phone')
))
registry.register('phone', PhoneInput, isPhoneField)
// Use in schema
const schema = {
phone: {
type: 'string',
format: 'phone', // Triggers PhoneInput
title: 'Phone Number'
}
}Example 2: Custom Widget Selector
import { rankWith, hasExtension } from '@quickflo/quickforms-vue'
import ColorPicker from './ColorPicker.vue'
import SliderInput from './SliderInput.vue'
// High priority for x-widget
const isColorWidget = rankWith(15, hasExtension('x-widget', 'color'))
const isSliderWidget = rankWith(15, hasExtension('x-widget', 'slider'))
registry.register('color-picker', ColorPicker, isColorWidget)
registry.register('slider', SliderInput, isSliderWidget)
// Use in schema
const schema = {
color: {
type: 'string',
'x-widget': 'color', // Triggers ColorPicker
title: 'Theme Color'
},
volume: {
type: 'number',
'x-widget': 'slider', // Triggers SliderInput
title: 'Volume'
}
}Example 3: Override Built-in Component
import { rankWith, isStringType } from '@quickflo/quickforms-vue'
import CustomStringField from './CustomStringField.vue'
// Built-in StringField has priority 2
// Use priority 20 to override
const isCustomString = rankWith(20, isStringType)
registry.register('custom-string', CustomStringField, isCustomString)
// Now ALL string fields use CustomStringFieldExample 4: Conditional Component
import { rankWith, and, isNumberType } from '@quickflo/quickforms-vue'
import RangeSlider from './RangeSlider.vue'
// Use slider for numbers with min/max
const isRangeNumber = rankWith(12, (schema) =>
isNumberType(schema) &&
schema.minimum !== undefined &&
schema.maximum !== undefined
)
registry.register('range-slider', RangeSlider, isRangeNumber)
// Triggers RangeSlider
const schema = {
age: {
type: 'number',
minimum: 0,
maximum: 120
}
}Built-in Component Priorities
Reference for built-in component priorities:
| Component | Tester | Priority |
|---|---|---|
| HiddenField | x-hidden: true | 100 |
| OneOfField | hasOneOf | 10 |
| AllOfField | hasAllOf | 10 |
| ArrayField | isArrayType | 5 |
| ObjectField | isObjectType | 5 |
| EnumField | isEnumType | 4 |
| DateField | isDateFormat | isTimeFormat | isDateTimeFormat | 3 |
| BooleanField | isBooleanType | 2 |
| NumberField | isNumberType | isIntegerType | 2 |
| StringField | isStringType | 2 |
Strategy:
- Use priority 1-5 for generic fallbacks
- Use priority 10-15 for specific overrides
- Use priority 20+ to override built-ins completely
- Use priority 100+ for critical components (hidden fields)
Best Practices
- Start with high priority - Use 10+ to ensure your component is selected
- Be specific - Combine multiple predicates for precise targeting
- Test thoroughly - Verify your tester matches only intended schemas
- Document behavior - Comment why you're using specific priorities
- Avoid conflicts - Two components at same priority = undefined behavior
- Use extensions - Custom
x-*properties are cleaner than complex predicates
Debugging
Check which component will render:
const schema = { type: 'string', format: 'email' }
const component = registry.getComponent(schema)
console.log(component?.__name) // Component nameList all registered components:
// Access internal registry (not officially exposed)
console.log(registry._components)Next Steps
- Custom Components - Build and register custom components
- Schema Extensions - Use
x-*properties with testers - Components API - All built-in components