@remoteoss/json-schema-form
Version:
WIP V2 – Headless UI form powered by JSON Schemas
228 lines (221 loc) • 7.9 kB
TypeScript
import { RulesLogic } from 'json-logic-js';
import { JSONSchema } from 'json-schema-typed/draft-2020-12';
/**
* Defines the type of a `Field` in the form.
*/
type JsfSchemaType = Exclude<JSONSchema, boolean>['type'];
/**
* Defines the type of a value in the form that will be validated against the schema.
*/
type SchemaValue = string | number | ObjectValue | null | undefined | Array<SchemaValue> | boolean | File;
/**
* A nested object value.
*/
interface ObjectValue {
[key: string]: SchemaValue;
}
type JsfPresentation = {
inputType?: FieldType;
description?: string;
accept?: string;
maxFileSize?: number;
minDate?: string;
maxDate?: string;
} & {
[key: string]: unknown;
};
interface JsonLogicRules {
validations?: Record<string, {
errorMessage?: string;
rule: RulesLogic;
}>;
computedValues?: Record<string, {
rule: RulesLogic;
}>;
}
interface JsonLogicRootSchema extends Pick<NonBooleanJsfSchema, 'if' | 'then' | 'else' | 'allOf' | 'anyOf' | 'oneOf' | 'not'> {
}
interface JsonLogicSchema extends JsonLogicRules, JsonLogicRootSchema {
}
/**
* JSON Schema Form extending JSON Schema with additional JSON Schema Form properties.
*/
type JsfSchema = JSONSchema & {
'properties'?: Record<string, JsfSchema>;
'items'?: JsfSchema;
'enum'?: unknown[];
'anyOf'?: JsfSchema[];
'allOf'?: JsfSchema[];
'oneOf'?: JsfSchema[];
'not'?: JsfSchema;
'if'?: JsfSchema;
'then'?: JsfSchema;
'else'?: JsfSchema;
'value'?: SchemaValue;
'required'?: string[];
/** Defines the order of the fields in the form. */
'x-jsf-order'?: string[];
/** Defines the presentation of the field in the form. */
'x-jsf-presentation'?: JsfPresentation;
/** Defines the error message of the field in the form. */
'x-jsf-errorMessage'?: Record<string, string>;
/** Defines all JSON Logic rules for the schema (both validations and computed values). */
'x-jsf-logic'?: JsonLogicSchema;
/** Extra validations to run. References validations declared 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'?: Record<string, string | object>;
};
/**
* JSON Schema Form type without booleans.
* This type is used for convenience in places where a boolean is not allowed.
* @see `JsfSchema` for the full schema type which allows booleans and is used for sub schemas.
*/
type NonBooleanJsfSchema = Exclude<JsfSchema, boolean>;
/**
* JSON Schema Form type specifically for object schemas.
* This type ensures the schema has type 'object'.
*/
type JsfObjectSchema = NonBooleanJsfSchema & {
type: 'object';
};
/**
* WIP type for UI field output that allows for all `x-jsf-presentation` properties to be splatted
* TODO/QUESTION: what are the required fields for a field? what are the things we want to deprecate, if any?
*/
interface Field {
name: string;
label?: string;
description?: string;
fields?: Field[];
type: FieldType;
inputType: FieldType;
required: boolean;
jsonType: JsfSchemaType;
isVisible: boolean;
accept?: string;
errorMessage?: Record<string, string>;
computedAttributes?: Record<string, unknown>;
minDate?: string;
maxDate?: string;
maxLength?: number;
maxFileSize?: number;
format?: string;
anyOf?: unknown[];
options?: unknown[];
const?: unknown;
checkboxValue?: unknown;
default?: unknown;
[key: string]: unknown;
}
type FieldType = 'text' | 'number' | 'select' | 'file' | 'radio' | 'group-array' | 'email' | 'date' | 'checkbox' | 'fieldset' | 'money' | 'country' | 'textarea' | 'hidden';
interface LegacyOptions {
/**
* A null value will be treated as undefined.
* When true, providing a value to a schema that is `false`,
* the validation will succeed instead of returning a type error.
* This was a bug in v0, we fixed it in v1. If you need the same wrong behavior, set this to true.
* @default false
* @example
* ```ts
* Schema: { "properties": { "name": { "type": "string" } } }
* Value: { "name": null } // Validation succeeds, even though the type is not 'null'
* ```
*/
treatNullAsUndefined?: boolean;
/**
* A value against a schema "false" will be allowed.
* When true, providing a value to a non-required field that is not of type 'null' or ['null']
* the validation will succeed instead of returning a type error.
* This was a bug in v0, we fixed it in v1. If you need the same wrong behavior, set this to true.
* @default false
* @example
* ```ts
* Schema: { "properties": { "age": false } }
* Value: { age: 10 } // Validation succeeds, even though the value is forbidden;
* ```
*/
allowForbiddenValues?: boolean;
}
interface FormResult {
fields: Field[];
isError: boolean;
error: string | null;
handleValidation: (value: SchemaValue) => ValidationResult;
}
/**
* Recursive type for form error messages
* - String for leaf error messages
* - Nested object for nested fields
* - Arrays for group-array fields
*/
interface FormErrors {
[key: string]: string | FormErrors | Array<null | FormErrors>;
}
interface ValidationResult {
formErrors?: FormErrors;
}
interface CreateHeadlessFormOptions {
/**
* The initial values to use for the form
*/
initialValues?: SchemaValue;
/**
* Backward compatibility config with v0
*/
legacyOptions?: LegacyOptions;
/**
* When enabled, ['x-jsf-presentation'].inputType is required for all properties.
* @default false
*/
strictInputType?: boolean;
/**
* Custom user defined functions. A dictionary of name and function
*/
customJsonLogicOps?: Record<string, (...args: any[]) => any>;
}
declare function createHeadlessForm(schema: JsfObjectSchema, options?: CreateHeadlessFormOptions): FormResult;
type FieldOutput = Partial<JsfSchema>;
type FieldModification = Partial<JsfSchema> & {
/**
* @deprecated Use `x-jsf-presentation` instead
*/
presentation?: JsfSchema['x-jsf-presentation'];
/**
* @deprecated Use `x-jsf-errorMessage` instead
*/
errorMessage?: JsfSchema['x-jsf-errorMessage'];
};
interface ModifyConfig {
fields?: Record<string, FieldModification | ((attrs: JsfSchema) => FieldOutput)>;
allFields?: (name: string, attrs: JsfSchema) => FieldModification;
create?: Record<string, FieldModification>;
pick?: string[];
orderRoot?: string[] | ((originalOrder: string[]) => string[]);
muteLogging?: boolean;
}
type WarningType = 'FIELD_TO_CHANGE_NOT_FOUND' | 'ORDER_MISSING_FIELDS' | 'FIELD_TO_CREATE_EXISTS' | 'PICK_MISSED_FIELD';
interface Warning {
type: WarningType;
message: string;
meta?: Record<string, any>;
}
/**
* Modifies the schema
* Use modify() when you need to customize the generated fields. This function creates a new version of JSON schema based on a provided configuration. Then you pass the new schema to createHeadlessForm()
*
* @example
* const modifiedSchema = modify(schema, {
* fields: {
* name: { type: 'string', title: 'Name' },
* },
* })
* @param {JsfSchema} originalSchema - The original schema
* @param {ModifyConfig} config - The config
* @returns {ModifyResult} The new schema and the warnings that occurred during the modifications
*/
declare function modifySchema(originalSchema: JsfSchema, config: ModifyConfig): {
schema: JsfSchema;
warnings: (Warning | null)[];
};
export { type CreateHeadlessFormOptions, type Field, type FieldType, type FormErrors, type FormResult, type LegacyOptions, type ValidationResult, createHeadlessForm, modifySchema as modify };