├── packages ├── react │ ├── .gitignore │ ├── eslint.config.mjs │ ├── rollup.config.mjs │ ├── tsconfig.json │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── formity.tsx │ │ └── schema.ts │ └── CHANGELOG.md └── system │ ├── .gitignore │ ├── eslint.config.mjs │ ├── src │ ├── types │ │ ├── handlers │ │ │ ├── model.ts │ │ │ └── typed.ts │ │ ├── state │ │ │ ├── point.ts │ │ │ ├── state.ts │ │ │ ├── position.ts │ │ │ └── inputs.ts │ │ ├── controls.ts │ │ ├── utils.ts │ │ ├── output │ │ │ ├── return.ts │ │ │ └── yield.ts │ │ ├── values.ts │ │ └── schema │ │ │ └── model.ts │ ├── utils │ │ ├── schema │ │ │ ├── form.ts │ │ │ ├── yield.ts │ │ │ ├── return.ts │ │ │ ├── variables.ts │ │ │ ├── flow.test.ts │ │ │ ├── flow.list.ts │ │ │ ├── flow.cond.ts │ │ │ ├── flow.list.test.ts │ │ │ ├── flow.loop.ts │ │ │ ├── flow.switch.ts │ │ │ ├── flow.ts │ │ │ ├── flow.loop.test.ts │ │ │ ├── flow.cond.test.ts │ │ │ └── flow.switch.test.ts │ │ ├── state.ts │ │ ├── inputs │ │ │ ├── flow.list.ts │ │ │ ├── flow.loop.ts │ │ │ ├── flow.cond.ts │ │ │ ├── flow.list.test.ts │ │ │ ├── flow.loop.test.ts │ │ │ ├── flow.switch.ts │ │ │ ├── form.test.ts │ │ │ ├── form.ts │ │ │ ├── flow.cond.test.ts │ │ │ ├── flow.switch.test.ts │ │ │ ├── flow.test.ts │ │ │ └── flow.ts │ │ ├── form.ts │ │ ├── state.test.ts │ │ └── form.test.ts │ └── index.ts │ ├── rollup.config.mjs │ ├── tsconfig.json │ ├── package.json │ ├── README.md │ └── CHANGELOG.md ├── .gitignore ├── apps └── react │ ├── src │ ├── vite-env.d.ts │ ├── multi-step │ │ ├── index.ts │ │ ├── multi-step-context.ts │ │ ├── multi-step-value.ts │ │ ├── use-multi-step.ts │ │ └── multi-step.tsx │ ├── utils.ts │ ├── components │ │ ├── navigation │ │ │ ├── next-button.tsx │ │ │ └── back-button.tsx │ │ ├── user-interface │ │ │ ├── row.tsx │ │ │ ├── button.tsx │ │ │ ├── input.tsx │ │ │ ├── field.tsx │ │ │ └── code.tsx │ │ ├── react-hook-form │ │ │ ├── text-field.tsx │ │ │ ├── listbox.tsx │ │ │ ├── number-field.tsx │ │ │ ├── select.tsx │ │ │ ├── multi-select.tsx │ │ │ └── yes-no.tsx │ │ ├── step.tsx │ │ ├── data.tsx │ │ ├── index.ts │ │ ├── layout.tsx │ │ └── fields │ │ │ ├── text-field.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── checkbox-group.tsx │ │ │ └── listbox.tsx │ ├── form.tsx │ ├── app.tsx │ ├── main.tsx │ ├── main.css │ └── schemas │ │ ├── flow.list.tsx │ │ ├── flow.loop.tsx │ │ ├── flow.cond.tsx │ │ └── flow.switch.tsx │ ├── tsconfig.node.tsbuildinfo │ ├── public │ └── favicon.ico │ ├── postcss.config.js │ ├── tsconfig.json │ ├── cypress │ ├── tsconfig.json │ ├── fixtures │ │ └── example.json │ └── support │ │ ├── component-index.html │ │ ├── component.ts │ │ └── commands.ts │ ├── tailwind.config.ts │ ├── next-env.d.ts │ ├── vite.config.ts │ ├── cypress.config.js │ ├── .gitignore │ ├── index.html │ ├── tsconfig.node.json │ ├── tsconfig.app.json │ ├── eslint.config.js │ ├── tsconfig.app.tsbuildinfo │ ├── package.json │ └── README.md ├── CHANGELOG.md ├── .changeset ├── config.json └── README.md ├── turbo.json ├── .github └── workflows │ ├── check.yml │ └── check-cypress-react.yml ├── LICENSE ├── README.md ├── CONTRIBUTING.md └── package.json /packages/react/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | -------------------------------------------------------------------------------- /packages/system/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .DS_Store 3 | .turbo 4 | .env 5 | -------------------------------------------------------------------------------- /apps/react/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /apps/react/tsconfig.node.tsbuildinfo: -------------------------------------------------------------------------------- 1 | {"root":["./vite.config.ts"],"version":"5.7.2"} -------------------------------------------------------------------------------- /apps/react/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martiserra99/formity/HEAD/apps/react/public/favicon.ico -------------------------------------------------------------------------------- /apps/react/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | Please check the [releases](https://github.com/martiserra99/formity/releases) page for information about each release. 4 | -------------------------------------------------------------------------------- /apps/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /apps/react/src/multi-step/index.ts: -------------------------------------------------------------------------------- 1 | export type { MultiStepValue } from "./multi-step-value"; 2 | export { MultiStep } from "./multi-step"; 3 | export { useMultiStep } from "./use-multi-step"; 4 | -------------------------------------------------------------------------------- /apps/react/cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["es5", "dom"], 5 | "types": ["cypress", "node"] 6 | }, 7 | "include": ["**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /apps/react/cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /apps/react/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | }; 9 | -------------------------------------------------------------------------------- /apps/react/src/multi-step/multi-step-context.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | import type { MultiStepValue } from "./multi-step-value"; 4 | 5 | export const MultiStepContext = createContext(null); 6 | -------------------------------------------------------------------------------- /apps/react/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /apps/react/src/utils.ts: -------------------------------------------------------------------------------- 1 | import type { ClassValue } from "clsx"; 2 | 3 | import { clsx } from "clsx"; 4 | import { twMerge } from "tailwind-merge"; 5 | 6 | export function cn(...inputs: ClassValue[]) { 7 | return twMerge(clsx(inputs)); 8 | } 9 | -------------------------------------------------------------------------------- /apps/react/src/multi-step/multi-step-value.ts: -------------------------------------------------------------------------------- 1 | import type { OnNext, OnBack, GetState, SetState } from "@formity/react"; 2 | 3 | export interface MultiStepValue { 4 | onNext: OnNext; 5 | onBack: OnBack; 6 | getState: GetState; 7 | setState: SetState; 8 | } 9 | -------------------------------------------------------------------------------- /packages/system/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslint from "@eslint/js"; 2 | import tseslint from "typescript-eslint"; 3 | 4 | export default [ 5 | { ignores: ["dist", "node_modules"] }, 6 | eslint.configs.recommended, 7 | ...tseslint.configs.recommended, 8 | ]; 9 | -------------------------------------------------------------------------------- /apps/react/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react-swc"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | resolve: { 8 | alias: { 9 | src: "/src", 10 | }, 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /packages/system/src/types/handlers/model.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Callback function invoked when the multi-step form yields values. 3 | */ 4 | export type OnYield = (values: unknown) => void; 5 | 6 | /** 7 | * Callback function invoked when the multi-step form returns values. 8 | */ 9 | export type OnReturn = (values: unknown) => void; 10 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.5/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": ["react-app"] 11 | } 12 | -------------------------------------------------------------------------------- /apps/react/cypress.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "cypress"; 2 | 3 | export default defineConfig({ 4 | video: false, 5 | screenshotOnRunFailure: false, 6 | component: { 7 | devServer: { 8 | framework: "react", 9 | bundler: "vite", 10 | }, 11 | }, 12 | viewportWidth: 1280, 13 | viewportHeight: 720, 14 | }); 15 | -------------------------------------------------------------------------------- /apps/react/src/components/navigation/next-button.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | 3 | import Button from "../user-interface/button"; 4 | 5 | interface NextButtonProps { 6 | children: ReactNode; 7 | } 8 | 9 | export default function NextButton({ children }: NextButtonProps) { 10 | return ; 11 | } 12 | -------------------------------------------------------------------------------- /apps/react/src/components/user-interface/row.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | 3 | interface RowProps { 4 | items: ReactNode[]; 5 | } 6 | 7 | export default function Row({ items }: RowProps) { 8 | return ( 9 |
10 | {items} 11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /apps/react/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /packages/system/src/utils/schema/form.ts: -------------------------------------------------------------------------------- 1 | import type { ItemSchema, FormSchema } from "../../types/schema/model"; 2 | 3 | /** 4 | * Type guard for `FormSchema` objects. 5 | * 6 | * @param schema An `ItemSchema` object. 7 | * @returns A boolean indicating whether the `schema` is a `FormSchema` object. 8 | */ 9 | export function is(schema: ItemSchema): schema is FormSchema { 10 | return "form" in schema; 11 | } 12 | -------------------------------------------------------------------------------- /apps/react/src/multi-step/use-multi-step.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | 3 | import { MultiStepValue } from "./multi-step-value"; 4 | import { MultiStepContext } from "./multi-step-context"; 5 | 6 | export function useMultiStep(): MultiStepValue { 7 | const context = useContext(MultiStepContext); 8 | if (!context) throw new Error("useMultiStep must be used within a MultiStep"); 9 | return context; 10 | } 11 | -------------------------------------------------------------------------------- /packages/system/src/utils/schema/yield.ts: -------------------------------------------------------------------------------- 1 | import type { ItemSchema, YieldSchema } from "../../types/schema/model"; 2 | 3 | /** 4 | * Type guard for `YieldSchema` objects. 5 | * 6 | * @param schema An `ItemSchema` object. 7 | * @returns A boolean indicating whether the `schema` is a `YieldSchema` object. 8 | */ 9 | export function is(schema: ItemSchema): schema is YieldSchema { 10 | return "yield" in schema; 11 | } 12 | -------------------------------------------------------------------------------- /apps/react/cypress/support/component-index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Components App 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/system/src/utils/schema/return.ts: -------------------------------------------------------------------------------- 1 | import type { ItemSchema, ReturnSchema } from "../../types/schema/model"; 2 | 3 | /** 4 | * Type guard for `ReturnSchema` objects. 5 | * 6 | * @param schema An `ItemSchema` object. 7 | * @returns A boolean indicating whether the `schema` is a `ReturnSchema` object. 8 | */ 9 | export function is(schema: ItemSchema): schema is ReturnSchema { 10 | return "return" in schema; 11 | } 12 | -------------------------------------------------------------------------------- /packages/system/src/utils/schema/variables.ts: -------------------------------------------------------------------------------- 1 | import type { ItemSchema, VariablesSchema } from "../../types/schema/model"; 2 | 3 | /** 4 | * Type guard for `VariablesSchema` objects. 5 | * 6 | * @param schema An `ItemSchema` object. 7 | * @returns A boolean indicating whether the `schema` is a `VariablesSchema` object. 8 | */ 9 | export function is(schema: ItemSchema): schema is VariablesSchema { 10 | return "variables" in schema; 11 | } 12 | -------------------------------------------------------------------------------- /apps/react/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Formity 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/system/src/types/state/point.ts: -------------------------------------------------------------------------------- 1 | import type { Position } from "./position"; 2 | 3 | /** 4 | * Represents a specific position within a multi-step form and its associated values. 5 | * 6 | * @property path An array of `Position` objects that define the position within the multi-step form. 7 | * @property values An object containing the values associated with the position. 8 | */ 9 | export type Point = { 10 | path: Position[]; 11 | values: object; 12 | }; 13 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turborepo.org/schema.json", 3 | "globalEnv": ["NODE_ENV"], 4 | "tasks": { 5 | "build": { 6 | "dependsOn": ["^build"], 7 | "outputs": ["dist/**"] 8 | }, 9 | "dev": { 10 | "outputs": ["dist/**"], 11 | "cache": false, 12 | "persistent": true 13 | }, 14 | "lint": { 15 | "outputs": [] 16 | }, 17 | "test": { 18 | "dependsOn": ["^build"], 19 | "outputs": [] 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /packages/system/src/types/state/state.ts: -------------------------------------------------------------------------------- 1 | import type { Point } from "./point"; 2 | import type { Inputs } from "./inputs"; 3 | 4 | /** 5 | * Represents the progression of steps completed in a multi-step form. 6 | * 7 | * @property points An array of `Point` objects. The last point in the array represents the current position with the associated values. 8 | * @property inputs An `Inputs` object that contains the values entered at each step of the form. 9 | */ 10 | export type State = { 11 | points: Point[]; 12 | inputs: Inputs; 13 | }; 14 | -------------------------------------------------------------------------------- /packages/system/src/types/handlers/typed.ts: -------------------------------------------------------------------------------- 1 | import type { Values } from "../values"; 2 | 3 | import type { YieldOutput } from "../output/yield"; 4 | import type { ReturnOutput } from "../output/return"; 5 | 6 | /** 7 | * Callback function invoked when the multi-step form yields values. 8 | */ 9 | export type OnYield = (values: YieldOutput) => void; 10 | 11 | /** 12 | * Callback function invoked when the multi-step form returns values. 13 | */ 14 | export type OnReturn = (values: ReturnOutput) => void; 15 | -------------------------------------------------------------------------------- /packages/react/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import eslint from "@eslint/js"; 3 | import tseslint from "typescript-eslint"; 4 | import reactlint from "eslint-plugin-react"; 5 | 6 | export default [ 7 | { 8 | ignores: ["dist", "node_modules"], 9 | }, 10 | { 11 | languageOptions: { 12 | globals: globals.browser, 13 | }, 14 | }, 15 | { 16 | settings: { 17 | react: { 18 | version: "detect", 19 | }, 20 | }, 21 | }, 22 | eslint.configs.recommended, 23 | ...tseslint.configs.recommended, 24 | reactlint.configs.flat.recommended, 25 | ]; 26 | -------------------------------------------------------------------------------- /packages/system/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import resolve from "@rollup/plugin-node-resolve"; 2 | import typescript from "@rollup/plugin-typescript"; 3 | import terser from "@rollup/plugin-terser"; 4 | 5 | export default { 6 | input: "src/index.ts", 7 | output: [ 8 | { 9 | file: "dist/index.cjs.js", 10 | format: "cjs", 11 | plugins: [terser()], 12 | sourcemap: true, 13 | }, 14 | { 15 | file: "dist/index.esm.js", 16 | format: "es", 17 | plugins: [terser()], 18 | sourcemap: true, 19 | }, 20 | ], 21 | plugins: [resolve(), typescript({ sourceMap: true })], 22 | external: [], 23 | }; 24 | -------------------------------------------------------------------------------- /packages/react/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import resolve from "@rollup/plugin-node-resolve"; 2 | import typescript from "@rollup/plugin-typescript"; 3 | import terser from "@rollup/plugin-terser"; 4 | 5 | export default { 6 | input: "src/index.ts", 7 | output: [ 8 | { 9 | file: "dist/index.cjs.js", 10 | format: "cjs", 11 | plugins: [terser()], 12 | sourcemap: true, 13 | }, 14 | { 15 | file: "dist/index.esm.js", 16 | format: "es", 17 | plugins: [terser()], 18 | sourcemap: true, 19 | }, 20 | ], 21 | plugins: [resolve(), typescript({ sourceMap: true })], 22 | external: ["react"], 23 | }; 24 | -------------------------------------------------------------------------------- /apps/react/src/form.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Formity, 3 | Schema, 4 | Values, 5 | OnYield, 6 | OnReturn, 7 | State, 8 | } from "@formity/react"; 9 | 10 | interface FormProps { 11 | schema: Schema; 12 | onYield?: OnYield; 13 | onReturn?: OnReturn; 14 | initialState?: State; 15 | } 16 | 17 | export function Form({ 18 | schema, 19 | onYield, 20 | onReturn, 21 | initialState, 22 | }: FormProps) { 23 | return ( 24 | 25 | schema={schema} 26 | onYield={onYield} 27 | onReturn={onReturn} 28 | initialState={initialState} 29 | /> 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /packages/system/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "module": "es2015", 5 | "target": "es2018", 6 | "moduleResolution": "node", 7 | "outDir": "./dist", 8 | "skipLibCheck": true, 9 | "declaration": true, 10 | "declarationMap": true, 11 | "noEmit": true, 12 | "esModuleInterop": true, 13 | "lib": ["esnext"], 14 | "strict": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "baseUrl": "./" 20 | }, 21 | "include": ["src", "tests"], 22 | "exclude": ["src/*.test.ts"] 23 | } 24 | -------------------------------------------------------------------------------- /packages/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "module": "es2015", 5 | "target": "es2018", 6 | "moduleResolution": "node", 7 | "jsx": "react", 8 | "outDir": "./dist", 9 | "skipLibCheck": true, 10 | "declaration": true, 11 | "declarationMap": true, 12 | "noEmit": true, 13 | "esModuleInterop": true, 14 | "lib": ["esnext"], 15 | "strict": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "noImplicitReturns": true, 19 | "noFallthroughCasesInSwitch": true, 20 | "baseUrl": "./" 21 | }, 22 | "include": ["src"], 23 | "exclude": ["src/*.test.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /apps/react/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": ["ES2023"], 5 | "module": "ESNext", 6 | "skipLibCheck": true, 7 | 8 | /* Bundler mode */ 9 | "moduleResolution": "bundler", 10 | "allowImportingTsExtensions": true, 11 | "isolatedModules": true, 12 | "moduleDetection": "force", 13 | "noEmit": true, 14 | 15 | /* Linting */ 16 | "strict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noFallthroughCasesInSwitch": true, 20 | 21 | "baseUrl": ".", 22 | "paths": { 23 | "src/*": ["./src/*"] 24 | } 25 | }, 26 | "include": ["vite.config.ts"] 27 | } 28 | -------------------------------------------------------------------------------- /packages/system/src/types/controls.ts: -------------------------------------------------------------------------------- 1 | import type { State } from "./state/state"; 2 | 3 | /** 4 | * Callback function used to navigate to the next step of a multi-step form. 5 | */ 6 | export type OnNext = (values: object) => void; 7 | 8 | /** 9 | * Callback function used to navigate to the previous step of a multi-step form. 10 | */ 11 | export type OnBack = (values: object) => void; 12 | 13 | /** 14 | * Callback function used to get the current state of the multi-step form. 15 | */ 16 | export type GetState = (values: object) => State; 17 | 18 | /** 19 | * Callback function used to set the current state of the multi-step form. 20 | */ 21 | export type SetState = (state: State) => void; 22 | -------------------------------------------------------------------------------- /apps/react/src/components/user-interface/button.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentPropsWithoutRef } from "react"; 2 | 3 | import { cn } from "../../utils"; 4 | 5 | export default function Button({ 6 | className, 7 | ...props 8 | }: ComponentPropsWithoutRef<"button">) { 9 | return ( 10 | 19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /apps/react/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Step } from "./step"; 2 | export { default as Layout } from "./layout"; 3 | export { default as NextButton } from "./navigation/next-button"; 4 | export { default as BackButton } from "./navigation/back-button"; 5 | export { default as Row } from "./user-interface/row"; 6 | export { default as TextField } from "./react-hook-form/text-field"; 7 | export { default as NumberField } from "./react-hook-form/number-field"; 8 | export { default as Listbox } from "./react-hook-form/listbox"; 9 | export { default as YesNo } from "./react-hook-form/yes-no"; 10 | export { default as Select } from "./react-hook-form/select"; 11 | export { default as MultiSelect } from "./react-hook-form/multi-select"; 12 | export { default as Data } from "./data"; 13 | -------------------------------------------------------------------------------- /apps/react/src/components/react-hook-form/listbox.tsx: -------------------------------------------------------------------------------- 1 | import { useFormContext, Controller } from "react-hook-form"; 2 | 3 | import BaseListbox from "../fields/listbox"; 4 | 5 | interface ListboxProps { 6 | name: string; 7 | label: string; 8 | options: { value: string; label: string }[]; 9 | } 10 | 11 | export default function Listbox({ name, label, options }: ListboxProps) { 12 | const { control, formState } = useFormContext(); 13 | const error = formState.errors[name] as { message: string } | undefined; 14 | return ( 15 | ( 19 | 20 | )} 21 | /> 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Check 2 | on: push 3 | jobs: 4 | check: 5 | strategy: 6 | matrix: 7 | operating-system: [ubuntu-latest] 8 | runs-on: ${{ matrix.operating-system }} 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v4 12 | - name: Setup Node 13 | uses: actions/setup-node@v4 14 | with: 15 | node-version: 20.x 16 | - name: Cache dependencies 17 | uses: actions/cache@v3 18 | with: 19 | path: ~/.npm 20 | key: npm-deps-${{ hashFiles('**/package-lock.json') }} 21 | - name: Install dependencies 22 | run: npm ci 23 | - name: Lint 24 | run: npm run lint 25 | - name: Test 26 | run: npm run test 27 | - name: Build 28 | run: npm run build 29 | -------------------------------------------------------------------------------- /.github/workflows/check-cypress-react.yml: -------------------------------------------------------------------------------- 1 | name: Check Cypress React 2 | on: push 3 | jobs: 4 | check-cypress-react: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Checkout 8 | uses: actions/checkout@v4 9 | - name: Setup Node 10 | uses: actions/setup-node@v4 11 | with: 12 | node-version: 20.x 13 | - name: Cache dependencies 14 | uses: actions/cache@v3 15 | with: 16 | path: ~/.npm 17 | key: npm-deps-${{ hashFiles('**/package-lock.json') }} 18 | - name: Install dependencies 19 | run: npm ci 20 | - name: Build 21 | run: npm run build 22 | - name: Cypress 23 | uses: cypress-io/github-action@v6 24 | with: 25 | working-directory: ./apps/react 26 | component: true 27 | -------------------------------------------------------------------------------- /apps/react/src/components/user-interface/input.tsx: -------------------------------------------------------------------------------- 1 | import type { ElementType, ComponentProps, ReactNode } from "react"; 2 | 3 | import { cn } from "../../utils"; 4 | 5 | type InputProps = Omit, "as"> & { 6 | as?: E; 7 | children?: ReactNode; 8 | className?: ReactNode; 9 | }; 10 | 11 | export default function Input({ 12 | as, 13 | children, 14 | className, 15 | ...props 16 | }: InputProps) { 17 | const As = as || "div"; 18 | return ( 19 | 27 | {children} 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /apps/react/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | 7 | export default tseslint.config( 8 | { ignores: ['dist'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | 'react-refresh/only-export-components': [ 23 | 'warn', 24 | { allowConstantExport: true }, 25 | ], 26 | }, 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /packages/system/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@formity/system", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "private": false, 6 | "description": "Core system that powers the Formity library.", 7 | "keywords": [ 8 | "formity", 9 | "form", 10 | "multi-step form", 11 | "dynamic form" 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/martiserra99/formity" 16 | }, 17 | "homepage": "https://formity.app/", 18 | "main": "dist/index.cjs.js", 19 | "module": "dist/index.esm.js", 20 | "source": "src/index.ts", 21 | "types": "dist/index.d.ts", 22 | "files": [ 23 | "dist" 24 | ], 25 | "scripts": { 26 | "lint": "eslint", 27 | "test": "vitest --run --passWithNoTests", 28 | "build": "rollup --config rollup.config.mjs", 29 | "dev": "rollup --config rollup.config.mjs --watch" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /apps/react/src/components/react-hook-form/number-field.tsx: -------------------------------------------------------------------------------- 1 | import { useFormContext, Controller } from "react-hook-form"; 2 | 3 | import TextField from "../fields/text-field"; 4 | 5 | interface NumberFieldProps { 6 | name: string; 7 | label: string; 8 | } 9 | 10 | export default function NumberField({ name, label }: NumberFieldProps) { 11 | const { control, formState } = useFormContext(); 12 | const error = formState.errors[name] as { message: string } | undefined; 13 | return ( 14 | ( 18 | field.onChange(value === "" ? 0 : Number(value))} 23 | error={error} 24 | /> 25 | )} 26 | /> 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /apps/react/src/components/react-hook-form/select.tsx: -------------------------------------------------------------------------------- 1 | import { useFormContext, Controller } from "react-hook-form"; 2 | 3 | import RadioGroup from "../fields/radio-group"; 4 | 5 | interface SelectProps { 6 | name: string; 7 | label: string; 8 | options: { value: string; label: string }[]; 9 | direction: "x" | "y"; 10 | } 11 | 12 | export default function Select({ name, label, options, direction }: SelectProps) { 13 | const { control, formState } = useFormContext(); 14 | const error = formState.errors[name] as { message: string } | undefined; 15 | return ( 16 | ( 20 | 28 | )} 29 | /> 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /apps/react/src/multi-step/multi-step.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | import type { OnNext, OnBack, GetState, SetState } from "@formity/react"; 3 | 4 | import { useMemo } from "react"; 5 | 6 | import { MultiStepContext } from "./multi-step-context"; 7 | 8 | interface MultiStepProps { 9 | step: string; 10 | onNext: OnNext; 11 | onBack: OnBack; 12 | getState: GetState; 13 | setState: SetState; 14 | children: ReactNode; 15 | } 16 | 17 | export function MultiStep({ 18 | step, 19 | onNext, 20 | onBack, 21 | getState, 22 | setState, 23 | children, 24 | }: MultiStepProps) { 25 | const value = useMemo( 26 | () => ({ onNext, onBack, getState, setState }), 27 | [onNext, onBack, getState, setState] 28 | ); 29 | return ( 30 |
31 | 32 | {children} 33 | 34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /apps/react/src/components/navigation/back-button.tsx: -------------------------------------------------------------------------------- 1 | import { useFormContext } from "react-hook-form"; 2 | import { ChevronLeftIcon } from "@heroicons/react/20/solid"; 3 | 4 | import { useMultiStep } from "../../multi-step"; 5 | import { cn } from "../../utils"; 6 | 7 | export default function BackButton() { 8 | const { getValues } = useFormContext(); 9 | const { onBack } = useMultiStep(); 10 | return ( 11 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /packages/system/src/types/state/position.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents a position within a flow control structure of a multi-step form. This can be a list, a conditional, a loop, or a switch. 3 | */ 4 | export type Position = 5 | | ListPosition 6 | | CondPosition 7 | | LoopPosition 8 | | SwitchPosition; 9 | 10 | /** 11 | * Represents a position within a list. 12 | */ 13 | export type ListPosition = { 14 | type: "list"; 15 | slot: number; 16 | }; 17 | 18 | /** 19 | * Represents a position within a conditional. 20 | */ 21 | export type CondPosition = { 22 | type: "cond"; 23 | path: "then" | "else"; 24 | slot: number; 25 | }; 26 | 27 | /** 28 | * Represents a position within a loop. 29 | */ 30 | export type LoopPosition = { 31 | type: "loop"; 32 | slot: number; 33 | }; 34 | 35 | /** 36 | * Represents a position within a switch. 37 | */ 38 | export type SwitchPosition = { 39 | type: "switch"; 40 | branch: number; 41 | slot: number; 42 | }; 43 | -------------------------------------------------------------------------------- /apps/react/src/components/react-hook-form/multi-select.tsx: -------------------------------------------------------------------------------- 1 | import { useFormContext, Controller } from "react-hook-form"; 2 | 3 | import CheckboxGroup from "../fields/checkbox-group"; 4 | 5 | interface MultiSelectProps { 6 | name: string; 7 | label: string; 8 | options: { value: string; label: string }[]; 9 | direction: "x" | "y"; 10 | } 11 | 12 | export default function MultiSelect({ name, label, options, direction }: MultiSelectProps) { 13 | const { control, formState } = useFormContext(); 14 | const error = formState.errors[name] as { message: string } | undefined; 15 | return ( 16 | ( 20 | 28 | )} 29 | /> 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /apps/react/src/components/react-hook-form/yes-no.tsx: -------------------------------------------------------------------------------- 1 | import { useFormContext, Controller } from "react-hook-form"; 2 | 3 | import RadioGroup from "../fields/radio-group"; 4 | 5 | const options = [ 6 | { value: "yes", label: "Yes" }, 7 | { value: "no", label: "No" }, 8 | ]; 9 | 10 | interface YesNoProps { 11 | name: string; 12 | label: string; 13 | } 14 | 15 | export default function YesNo({ name, label }: YesNoProps) { 16 | const { control, formState } = useFormContext(); 17 | const error = formState.errors[name] as { message: string } | undefined; 18 | return ( 19 | ( 23 | field.onChange(value === "yes" ? true : false)} 27 | options={options} 28 | direction="x" 29 | error={error} 30 | /> 31 | )} 32 | /> 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /apps/react/src/app.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from "react"; 2 | 3 | import { 4 | Schema, 5 | Values, 6 | OnYield, 7 | OnReturn, 8 | YieldOutput, 9 | ReturnOutput, 10 | } from "@formity/react"; 11 | 12 | import { Form } from "./form"; 13 | import { Data } from "./components"; 14 | 15 | interface AppProps { 16 | schema: Schema; 17 | } 18 | 19 | export default function App({ schema }: AppProps) { 20 | const [values, setValues] = useState | null>(null); 21 | 22 | const onYield = useCallback>( 23 | (values: YieldOutput) => { 24 | console.log(values); 25 | }, 26 | [] 27 | ); 28 | 29 | const onReturn = useCallback>( 30 | (values: ReturnOutput) => { 31 | setValues(values); 32 | }, 33 | [] 34 | ); 35 | 36 | if (values) { 37 | return setValues(null)} />; 38 | } 39 | 40 | return schema={schema} onYield={onYield} onReturn={onReturn} />; 41 | } 42 | -------------------------------------------------------------------------------- /packages/system/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | Formity Logo 7 | 8 |
9 | 10 | ## @formity/system 11 | 12 | Core system that powers the Formity library. 13 | 14 | ## Website 15 | 16 | **Visit [https://formity.app](https://formity.app) to get started with Formity.** 17 | 18 | ## About 19 | 20 | Formity is an advanced form-building package designed to help React developers create advanced multi-step forms. 21 | 22 | ## Features 23 | 24 | - Create advanced multi-step forms where each step adapts based on the user's previous responses. 25 | 26 | - Integrate your custom components seamlessly, with no restrictions or limitations. 27 | 28 | - Fully compatible with any form library of your choice. 29 | 30 | ## License 31 | 32 | This package is licensed under the [MIT license](../../LICENSE). 33 | -------------------------------------------------------------------------------- /packages/react/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | Formity Logo 7 | 8 |
9 | 10 | ## @formity/react 11 | 12 | A highly customizable React library for creating advanced multi-step forms. 13 | 14 | ## Website 15 | 16 | **Visit [https://formity.app](https://formity.app) to get started with Formity.** 17 | 18 | ## About 19 | 20 | Formity is an advanced form-building package designed to help React developers create advanced multi-step forms. 21 | 22 | ## Features 23 | 24 | - Create advanced multi-step forms where each step adapts based on the user's previous responses. 25 | 26 | - Integrate your custom components seamlessly, with no restrictions or limitations. 27 | 28 | - Fully compatible with any form library of your choice. 29 | 30 | ## License 31 | 32 | This package is licensed under the [MIT license](../../LICENSE). 33 | -------------------------------------------------------------------------------- /packages/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@formity/react", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "private": false, 6 | "description": "A highly customizable React library for creating advanced multi-step forms.", 7 | "keywords": [ 8 | "react", 9 | "formity", 10 | "form", 11 | "multi-step form", 12 | "dynamic form" 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/martiserra99/formity" 17 | }, 18 | "homepage": "https://formity.app/", 19 | "main": "dist/index.cjs.js", 20 | "module": "dist/index.esm.js", 21 | "source": "src/index.ts", 22 | "types": "dist/index.d.ts", 23 | "files": [ 24 | "dist" 25 | ], 26 | "scripts": { 27 | "lint": "eslint", 28 | "test": "vitest --run --passWithNoTests", 29 | "build": "rollup --config rollup.config.mjs", 30 | "dev": "rollup --config rollup.config.mjs --watch" 31 | }, 32 | "dependencies": { 33 | "@formity/system": "^1.0.0" 34 | }, 35 | "devDependencies": { 36 | "@types/react": "^18.3.12" 37 | }, 38 | "peerDependencies": { 39 | "react": ">=16" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /apps/react/src/components/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | 3 | interface LayoutProps { 4 | heading: string; 5 | description: string; 6 | fields: ReactNode[]; 7 | button: ReactNode; 8 | back?: ReactNode; 9 | } 10 | 11 | export default function Layout({ 12 | heading, 13 | description, 14 | fields, 15 | button, 16 | back, 17 | }: LayoutProps) { 18 | return ( 19 |
20 |
21 |

25 | {heading} 26 |

27 |

28 | {description} 29 |

30 |
31 |
{fields}
32 |
33 | {button} 34 |
35 | {back &&
{back}
} 36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /apps/react/src/components/user-interface/field.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "../../utils"; 2 | 3 | interface FieldProps { 4 | children: React.ReactNode; 5 | id: string; 6 | label: string; 7 | labelClassName?: string; 8 | error: { message: string } | undefined; 9 | } 10 | 11 | export default function Field({ 12 | children, 13 | id, 14 | label, 15 | labelClassName, 16 | error, 17 | }: FieldProps) { 18 | return ( 19 |
20 |
21 | {children} 22 | 33 |
34 | {error &&

{error.message}

} 35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2024 webkid GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | Formity Logo 7 | 8 |
9 | 10 | ## The Formity mono repo 11 | 12 | The Formity repository is the home of two packages: 13 | 14 | - `@formity/react`: [packages/react](./packages/react) 15 | 16 | - `@formity/system`: [packages/system](./packages/system) 17 | 18 | ## Website 19 | 20 | **Visit [https://formity.app](https://formity.app) to get started with Formity.** 21 | 22 | ## About 23 | 24 | Formity is an advanced form-building package designed to help React developers create advanced multi-step forms. 25 | 26 | ## Features 27 | 28 | - Create advanced multi-step forms where each step adapts based on the user's previous responses. 29 | 30 | - Integrate your custom components seamlessly, with no restrictions or limitations. 31 | 32 | ## License 33 | 34 | These packages are licensed under the [MIT license](./LICENSE). 35 | -------------------------------------------------------------------------------- /apps/react/src/components/user-interface/code.tsx: -------------------------------------------------------------------------------- 1 | import { Highlight } from "prism-react-renderer"; 2 | 3 | import { cn } from "../../utils"; 4 | 5 | interface CodeProps { 6 | code: unknown; 7 | } 8 | 9 | export default function Code({ code }: CodeProps) { 10 | return ( 11 | 16 | {({ className, style, tokens, getLineProps, getTokenProps }) => ( 17 |
24 |           
25 |             {tokens.map((line, lineIndex) => (
26 |               
27 | {line.map((token, tokenIndex) => ( 28 | 29 | ))} 30 |
31 | ))} 32 |
33 |
34 | )} 35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Formity 2 | 3 | Hi there! We're so glad you're interested in contributing to Formity 😁. Your involvement helps us grow and improve — thank you for considering joining our journey! 4 | 5 | Formity currently provides the **@formity/react** package, and we're excited to expand its functionality to other frameworks like Vue, Angular, and Svelte. If you take a look at **@formity/react**, you'll notice it's a lightweight package — most of the logic is handled by **@formity/system**, the core that powers everything. This architecture makes it easier to create similar packages for other frameworks. 6 | 7 | If you're experienced with Vue, Angular, Svelte, or another framework and would like to help us build these packages, we'd be thrilled to collaborate with you! 8 | 9 | Have ideas for new features or ways to enhance Formity? Awesome! You can reach out to us anytime at [contact@martiserra.me](mailto:contact@martiserra.me). And if you're ready to dive in, feel free to submit a pull request — we're always happy to review contributions from the community. 10 | 11 | Thank you for making Formity better with your support. We can't wait to see what we build together! 12 | -------------------------------------------------------------------------------- /packages/system/src/utils/schema/flow.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | 3 | import type { ListSchema, ReturnSchema } from "../../types/schema/model"; 4 | import type { Position } from "src/types/state/position"; 5 | 6 | import { find } from "./flow"; 7 | 8 | describe("FlowSchema", () => { 9 | describe("find", () => { 10 | it("returns the item at the given path within the given schema", () => { 11 | const item: ReturnSchema = { return: () => ({}) }; 12 | const schema: ListSchema = [ 13 | { variables: () => ({}) }, 14 | { 15 | cond: { 16 | if: () => true, 17 | then: [ 18 | { 19 | loop: { 20 | while: () => true, 21 | do: [item], 22 | }, 23 | }, 24 | ], 25 | else: [], 26 | }, 27 | }, 28 | ]; 29 | const path: Position[] = [ 30 | { type: "list", slot: 1 }, 31 | { type: "cond", path: "then", slot: 0 }, 32 | { type: "loop", slot: 0 }, 33 | ]; 34 | const result = find(schema, path); 35 | expect(result).toBe(item); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /apps/react/cypress/support/component.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/component.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import "./commands"; 18 | 19 | import { mount } from "cypress/react18"; 20 | 21 | import "../../src/main.css"; 22 | 23 | // Augment the Cypress namespace to include type definitions for 24 | // your custom command. 25 | // Alternatively, can be defined in cypress/support/component.d.ts 26 | // with a at the top of your spec. 27 | declare global { 28 | // eslint-disable-next-line @typescript-eslint/no-namespace 29 | namespace Cypress { 30 | interface Chainable { 31 | mount: typeof mount; 32 | } 33 | } 34 | } 35 | 36 | Cypress.Commands.add("mount", mount); 37 | 38 | // Example use: 39 | // cy.mount() 40 | -------------------------------------------------------------------------------- /apps/react/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import { BrowserRouter, Routes, Route } from "react-router-dom"; 4 | import "./main.css"; 5 | 6 | import App from "./app"; 7 | 8 | import { mainSchema, MainValues } from "./schemas/main"; 9 | import { listSchema, ListValues } from "./schemas/flow.list"; 10 | import { condSchema, CondValues } from "./schemas/flow.cond"; 11 | import { loopSchema, LoopValues } from "./schemas/flow.loop"; 12 | import { switchSchema, SwitchValues } from "./schemas/flow.switch"; 13 | 14 | createRoot(document.getElementById("root")!).render( 15 | 16 | 17 | 18 | schema={mainSchema} />} /> 19 | schema={listSchema} />} 22 | /> 23 | schema={condSchema} />} 26 | /> 27 | schema={loopSchema} />} 30 | /> 31 | schema={switchSchema} />} 34 | /> 35 | 36 | 37 | 38 | ); 39 | -------------------------------------------------------------------------------- /apps/react/tsconfig.app.tsbuildinfo: -------------------------------------------------------------------------------- 1 | {"root":["./src/app.tsx","./src/form.cy.tsx","./src/form.tsx","./src/main.tsx","./src/utils.ts","./src/vite-env.d.ts","./src/components/data.tsx","./src/components/index.ts","./src/components/layout.tsx","./src/components/step.tsx","./src/components/fields/checkbox-group.tsx","./src/components/fields/listbox.tsx","./src/components/fields/radio-group.tsx","./src/components/fields/text-field.tsx","./src/components/navigation/back-button.tsx","./src/components/navigation/next-button.tsx","./src/components/react-hook-form/listbox.tsx","./src/components/react-hook-form/multi-select.tsx","./src/components/react-hook-form/number-field.tsx","./src/components/react-hook-form/select.tsx","./src/components/react-hook-form/text-field.tsx","./src/components/react-hook-form/yes-no.tsx","./src/components/user-interface/button.tsx","./src/components/user-interface/code.tsx","./src/components/user-interface/field.tsx","./src/components/user-interface/input.tsx","./src/components/user-interface/row.tsx","./src/multi-step/index.ts","./src/multi-step/multi-step-context.ts","./src/multi-step/multi-step-value.ts","./src/multi-step/multi-step.tsx","./src/multi-step/use-multi-step.ts","./src/schemas/flow.cond.tsx","./src/schemas/flow.list.tsx","./src/schemas/flow.loop.tsx","./src/schemas/flow.switch.tsx","./src/schemas/main.tsx","./cypress/support/commands.ts","./cypress/support/component.ts"],"version":"5.7.2"} -------------------------------------------------------------------------------- /apps/react/cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************** 3 | // This example commands.ts shows you how to 4 | // create various custom commands and overwrite 5 | // existing commands. 6 | // 7 | // For more comprehensive examples of custom 8 | // commands please read more here: 9 | // https://on.cypress.io/custom-commands 10 | // *********************************************** 11 | // 12 | // 13 | // -- This is a parent command -- 14 | // Cypress.Commands.add('login', (email, password) => { ... }) 15 | // 16 | // 17 | // -- This is a child command -- 18 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 19 | // 20 | // 21 | // -- This is a dual command -- 22 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 23 | // 24 | // 25 | // -- This will overwrite an existing command -- 26 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 27 | // 28 | // declare global { 29 | // namespace Cypress { 30 | // interface Chainable { 31 | // login(email: string, password: string): Chainable 32 | // drag(subject: string, options?: Partial): Chainable 33 | // dismiss(subject: string, options?: Partial): Chainable 34 | // visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable 35 | // } 36 | // } 37 | // } -------------------------------------------------------------------------------- /apps/react/src/components/fields/text-field.tsx: -------------------------------------------------------------------------------- 1 | import type { ChangeEvent } from "react"; 2 | 3 | import { useId } from "react"; 4 | 5 | import { cn } from "../../utils"; 6 | 7 | import Field from "../user-interface/field"; 8 | import Input from "../user-interface/input"; 9 | 10 | interface TextFieldProps { 11 | type: string; 12 | label: string; 13 | value: string; 14 | onChange: (value: string) => void; 15 | error: { message: string } | undefined; 16 | } 17 | 18 | export default function TextField({ 19 | type, 20 | label, 21 | value, 22 | onChange, 23 | error, 24 | }: TextFieldProps) { 25 | const id = useId(); 26 | return ( 27 | 36 | ) => { 42 | onChange(e.target.value); 43 | }} 44 | placeholder={label} 45 | className={cn( 46 | "peer placeholder-transparent focus:border-neutral-500 focus:outline-none focus:ring-transparent", 47 | { "border-red-500 focus:border-red-500": error } 48 | )} 49 | /> 50 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /apps/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-app", 3 | "private": true, 4 | "version": "1.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite --port 3000", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview --port 3000", 11 | "cy:run": "cypress run --component", 12 | "cy:open": "cypress open" 13 | }, 14 | "dependencies": { 15 | "@formity/react": "^1.0.0", 16 | "@headlessui/react": "^2.1.10", 17 | "@heroicons/react": "^2.1.5", 18 | "@hookform/resolvers": "^3.9.1", 19 | "clsx": "^2.1.1", 20 | "expry": "^2.0.7", 21 | "prism-react-renderer": "^2.4.0", 22 | "prismjs": "^1.29.0", 23 | "react": "^18.3.1", 24 | "react-dom": "^18.3.1", 25 | "react-hook-form": "^7.54.2", 26 | "react-router-dom": "^7.1.1", 27 | "tailwind-merge": "^2.5.4", 28 | "zod": "^3.24.1" 29 | }, 30 | "devDependencies": { 31 | "@eslint/js": "^9.11.1", 32 | "@tailwindcss/forms": "^0.5.9", 33 | "@types/react": "^18.3.10", 34 | "@types/react-dom": "^18.3.0", 35 | "@vitejs/plugin-react-swc": "^3.5.0", 36 | "autoprefixer": "^10.4.20", 37 | "cypress": "^13.17.0", 38 | "eslint": "^9.11.1", 39 | "eslint-plugin-react-hooks": "^5.1.0-rc.0", 40 | "eslint-plugin-react-refresh": "^0.4.12", 41 | "globals": "^15.9.0", 42 | "only-allow": "^1.2.1", 43 | "postcss": "^8.4.47", 44 | "tailwindcss": "^3.4.14", 45 | "typescript": "^5.5.3", 46 | "typescript-eslint": "^8.7.0", 47 | "vite": "^5.4.8" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/system/src/types/state/inputs.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Values entered in the forms that are within a multi-step form. 3 | */ 4 | export type Inputs = ListInputs; 5 | 6 | /** 7 | * Union of `FlowInputs` and `FormInputs`. 8 | */ 9 | export type ItemInputs = FlowInputs | FormInputs; 10 | 11 | /** 12 | * Values entered in the forms that are within any flow control structure. 13 | */ 14 | export type FlowInputs = ListInputs | CondInputs | LoopInputs | SwitchInputs; 15 | 16 | /** 17 | * Values entered in the forms that are within a list. 18 | */ 19 | export type ListInputs = { 20 | type: "list"; 21 | list: { [position: number]: ItemInputs }; 22 | }; 23 | 24 | /** 25 | * Values entered in the forms that are within a condition. 26 | */ 27 | export type CondInputs = { 28 | type: "cond"; 29 | then: { [position: number]: ItemInputs }; 30 | else: { [position: number]: ItemInputs }; 31 | }; 32 | 33 | /** 34 | * Values entered in the forms that are within a loop. 35 | */ 36 | export type LoopInputs = { 37 | type: "loop"; 38 | list: { [position: number]: ItemInputs }; 39 | }; 40 | 41 | /** 42 | * Values entered in the forms that are within a switch. 43 | */ 44 | export type SwitchInputs = { 45 | type: "switch"; 46 | branches: { [position: number]: { [position: number]: ItemInputs } }; 47 | default: { [position: number]: ItemInputs }; 48 | }; 49 | 50 | /** 51 | * Values entered in a form. 52 | */ 53 | export type FormInputs = { [key: string]: NameInputs }; 54 | 55 | /** 56 | * Values entered in a single form field. 57 | */ 58 | export type NameInputs = { 59 | data: { here: true; data: unknown } | { here: false }; 60 | keys: { [key: PropertyKey]: NameInputs }; 61 | }; 62 | -------------------------------------------------------------------------------- /packages/system/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @formity/system 2 | 3 | ## 1.0.0 4 | 5 | ### Major Changes 6 | 7 | - This release is a version bump from 0.5.0 to 1.0.0. Although no changes in functionality have been made, we believe the project has reached a level of maturity and stability suitable for production use. All features and behaviors remain as in 0.5.0, and this version number now signals a stable API. 8 | 9 | ## 0.5.0 10 | 11 | ### Minor Changes 12 | 13 | - Renamed functions to getInitialState, getNextState and getPreviousState 14 | - Renamed ReturnValues and YieldValues to ReturnOutput and YieldOutput 15 | - Exported additional Formity types 16 | 17 | ## 0.4.1 18 | 19 | ### Patch Changes 20 | 21 | - Allow users to submit values that are not handled by the values function (it is needed if we want to jump to specific steps) 22 | 23 | ## 0.4.0 24 | 25 | ### Minor Changes 26 | 27 | - Updated Yield schema element to yield values when navigating to previous steps 28 | 29 | ## 0.3.0 30 | 31 | ### Minor Changes 32 | 33 | - Refractored codebase for improved readability and maintainability. 34 | - Renamed `Flow` type to `State` for improved clarity and alignment with its purpose. 35 | - Renamed properties of the `State` type: 36 | - `cursors` -> `points` 37 | - `entries` -> `inputs` 38 | - Updated function names: 39 | - `getFlow` -> `getState` 40 | - `setFlow` -> `setState` 41 | - Renamed the `initialFlow` prop in the `Formity` component to `initialState` for consistency with the updated naming conventions. 42 | 43 | ## 0.2.1 44 | 45 | ### Patch Changes 46 | 47 | - Updated README.md 48 | 49 | ## 0.2.0 50 | 51 | ### Minor Changes 52 | 53 | - The switch element has been introduced 54 | 55 | ## 0.1.0 56 | 57 | ### Minor Changes 58 | 59 | - Created @formity/system 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@formity/monorepo", 3 | "private": true, 4 | "version": "0.0.0", 5 | "license": "MIT", 6 | "type": "module", 7 | "workspaces": [ 8 | "packages/*", 9 | "apps/*" 10 | ], 11 | "description": "A highly customizable React library for creating dynamic multi-step forms.", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/martiserra99/formity" 15 | }, 16 | "homepage": "https://formity.app/", 17 | "scripts": { 18 | "preinstall": "only-allow npm", 19 | "build": "turbo run build", 20 | "dev": "turbo run dev", 21 | "lint": "turbo run lint", 22 | "test": "turbo run test", 23 | "react:cy:run": "npm run cy:run --workspace=react-app", 24 | "react:cy:open": "npm run cy:open --workspace=react-app", 25 | "changeset": "changeset", 26 | "changeset:version": "changeset version", 27 | "changeset:publish": "changeset publish", 28 | "clean": "npm -r --parallel exec rimraf dist .turbo node_modules" 29 | }, 30 | "devDependencies": { 31 | "@changesets/cli": "^2.27.11", 32 | "@eslint/js": "^9.13.0", 33 | "@rollup/plugin-node-resolve": "^16.0.0", 34 | "@rollup/plugin-terser": "^0.4.4", 35 | "@rollup/plugin-typescript": "^12.1.1", 36 | "@swc/core": "^1.10.3", 37 | "cypress": "^13.17.0", 38 | "eslint": "^9.13.0", 39 | "eslint-plugin-react": "^7.37.1", 40 | "globals": "^15.11.0", 41 | "only-allow": "^1.2.1", 42 | "rimraf": "^6.0.1", 43 | "rollup": "^4.24.0", 44 | "tslib": "^2.8.0", 45 | "turbo": "^2.3.3", 46 | "typescript": "^5.6.3", 47 | "typescript-eslint": "^8.11.0", 48 | "vitest": "^2.1.8" 49 | }, 50 | "optionalDependencies": { 51 | "@rollup/rollup-linux-x64-gnu": "^4.29.1" 52 | }, 53 | "packageManager": "npm@10.8.2" 54 | } 55 | -------------------------------------------------------------------------------- /apps/react/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | export default tseslint.config({ 18 | languageOptions: { 19 | // other options... 20 | parserOptions: { 21 | project: ['./tsconfig.node.json', './tsconfig.app.json'], 22 | tsconfigRootDir: import.meta.dirname, 23 | }, 24 | }, 25 | }) 26 | ``` 27 | 28 | - Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` 29 | - Optionally add `...tseslint.configs.stylisticTypeChecked` 30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: 31 | 32 | ```js 33 | // eslint.config.js 34 | import react from 'eslint-plugin-react' 35 | 36 | export default tseslint.config({ 37 | // Set the react version 38 | settings: { react: { version: '18.3' } }, 39 | plugins: { 40 | // Add the react plugin 41 | react, 42 | }, 43 | rules: { 44 | // other rules... 45 | // Enable its recommended rules 46 | ...react.configs.recommended.rules, 47 | ...react.configs['jsx-runtime'].rules, 48 | }, 49 | }) 50 | ``` 51 | -------------------------------------------------------------------------------- /apps/react/src/components/fields/radio-group.tsx: -------------------------------------------------------------------------------- 1 | import { useId } from "react"; 2 | import { Radio, RadioGroup as HeadlessRadioGroup } from "@headlessui/react"; 3 | import { CheckIcon } from "@heroicons/react/20/solid"; 4 | 5 | import { cn } from "../../utils"; 6 | 7 | import Field from "../user-interface/field"; 8 | import Input from "../user-interface/input"; 9 | 10 | interface RadioGroupProps { 11 | label: string; 12 | value: string; 13 | onChange: (value: string) => void; 14 | options: { value: string; label: string }[]; 15 | direction: "x" | "y"; 16 | error: { message: string } | undefined; 17 | } 18 | 19 | export default function RadioGroup({ 20 | label, 21 | value, 22 | onChange, 23 | options, 24 | direction, 25 | error, 26 | }: RadioGroupProps) { 27 | const id = useId(); 28 | return ( 29 | 30 | 38 | {options.map((option) => ( 39 | 48 | {option.label} 49 | 50 | 51 | ))} 52 | 53 | 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /apps/react/src/main.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | [inert] ::-webkit-scrollbar { 7 | display: none; 8 | } 9 | 10 | /* Chrome, Safari, Edge, Opera */ 11 | input::-webkit-outer-spin-button, 12 | input::-webkit-inner-spin-button { 13 | -webkit-appearance: none; 14 | margin: 0; 15 | } 16 | 17 | /* Firefox */ 18 | input[type="number"] { 19 | -moz-appearance: textfield; 20 | } 21 | 22 | pre[class*="language-"] { 23 | color: theme("colors.neutral.50"); 24 | } 25 | .token.tag, 26 | .token.class-name, 27 | .token.selector, 28 | .token.selector .class, 29 | .token.selector.class, 30 | .token.function { 31 | color: theme("colors.purple.400"); 32 | } 33 | 34 | .token.attr-name, 35 | .token.keyword, 36 | .token.rule, 37 | .token.pseudo-class, 38 | .token.important { 39 | color: theme("colors.neutral.300"); 40 | } 41 | 42 | .token.module { 43 | color: theme("colors.fuchsia.400"); 44 | } 45 | 46 | .token.attr-value, 47 | .token.class, 48 | .token.string, 49 | .token.property { 50 | color: theme("colors.indigo.300"); 51 | } 52 | 53 | .token.punctuation, 54 | .token.attr-equals { 55 | color: theme("colors.neutral.500"); 56 | } 57 | 58 | .token.comment, 59 | .token.operator, 60 | .token.combinator { 61 | color: theme("colors.neutral.400"); 62 | } 63 | 64 | .language-json .token.property { 65 | color: theme("colors.white"); 66 | } 67 | } 68 | 69 | @layer utilities { 70 | /* For Webkit-based browsers (Chrome, Safari and Opera) */ 71 | .scrollbar-hide::-webkit-scrollbar { 72 | display: none; 73 | } 74 | 75 | /* For IE, Edge and Firefox */ 76 | .scrollbar-hide { 77 | -ms-overflow-style: none; /* IE and Edge */ 78 | scrollbar-width: none; /* Firefox */ 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /packages/system/src/utils/state.ts: -------------------------------------------------------------------------------- 1 | import type { Values } from "../types/values"; 2 | 3 | import type { Schema as TypedSchema } from "../types/schema/typed"; 4 | import type { Schema, FormSchema } from "../types/schema/model"; 5 | 6 | import type { State } from "../types/state/state"; 7 | import type { Inputs, FlowInputs } from "../types/state/inputs"; 8 | 9 | import * as FlowSchemaUtils from "./schema/flow"; 10 | import * as FlowInputsUtils from "./inputs/flow"; 11 | 12 | /** 13 | * Returns the current state of the multi-step form after updating the values of the current form. 14 | * 15 | * @param state The current state of the multi-step form. 16 | * @param schema The `Schema` object representing the multi-step form. 17 | * @param values An object containing the values of the current form. 18 | * @returns The current state of the multi-step form after updating the values of the current form. 19 | */ 20 | export function getState< 21 | R, 22 | V extends Values, 23 | I extends object, 24 | P extends object 25 | >(state: State, schema: TypedSchema, values: object): State { 26 | const _schema = schema as Schema; 27 | return _getState(state, _schema, values); 28 | } 29 | 30 | function _getState(state: State, schema: Schema, values: object): State { 31 | const point = state.points[state.points.length - 1]; 32 | const formSchema = FlowSchemaUtils.find(schema, point.path) as FormSchema; 33 | const formValues = formSchema["form"]["values"](point.values); 34 | let inputs: FlowInputs = state.inputs; 35 | for (const [name, value] of Object.entries(values)) { 36 | if (name in formValues) { 37 | const keys = formValues[name][1]; 38 | const path = point.path; 39 | inputs = FlowInputsUtils.set(inputs, path, name, keys, value); 40 | } 41 | } 42 | return { points: state.points, inputs: inputs as Inputs }; 43 | } 44 | -------------------------------------------------------------------------------- /packages/system/src/utils/inputs/flow.list.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ItemInputs, 3 | FlowInputs, 4 | ListInputs, 5 | } from "../../types/state/inputs"; 6 | 7 | import type { Position, ListPosition } from "../../types/state/position"; 8 | 9 | /** 10 | * Creates a `ListInputs` object. 11 | * 12 | * @returns The created `ListInputs` object. 13 | */ 14 | export function create(): FlowInputs { 15 | return { type: "list", list: {} }; 16 | } 17 | 18 | /** 19 | * Clones a `ListInputs` object. 20 | * 21 | * @param flow A `ListInputs` object. 22 | * @returns The cloned `ListInputs` object. 23 | */ 24 | export function clone(flow: ListInputs): FlowInputs { 25 | return { ...flow, list: { ...flow.list } }; 26 | } 27 | 28 | /** 29 | * Returns the `ItemInputs` object at the given position within the given `ListInputs` object, or `null` if there is no item at the given position. 30 | * 31 | * @param flow The `ListInputs` object. 32 | * @param position The position within the `ListInputs` object. 33 | * @returns The `ItemInputs` object at the given position within the `ListInputs` object, or `null` if there is no item at the given position. 34 | */ 35 | export function getItem( 36 | flow: ListInputs, 37 | position: Position 38 | ): ItemInputs | null { 39 | const { slot } = position as ListPosition; 40 | if (slot in flow.list) return flow.list[slot]; 41 | return null; 42 | } 43 | 44 | /** 45 | * Sets the `ItemInputs` object at the given position within the given `ListInputs` object. 46 | * 47 | * @param flow The `ListInputs` object. 48 | * @param position The position within the `ListInputs` object. 49 | * @param item The `ItemInputs` object to set. 50 | */ 51 | export function setItem( 52 | flow: ListInputs, 53 | position: Position, 54 | item: ItemInputs 55 | ): void { 56 | const { slot } = position as ListPosition; 57 | flow.list[slot] = item; 58 | } 59 | -------------------------------------------------------------------------------- /packages/system/src/utils/inputs/flow.loop.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ItemInputs, 3 | FlowInputs, 4 | LoopInputs, 5 | } from "../../types/state/inputs"; 6 | 7 | import type { Position, LoopPosition } from "../../types/state/position"; 8 | 9 | /** 10 | * Creates a `LoopInputs` object. 11 | * 12 | * @returns The created `LoopInputs` object. 13 | */ 14 | export function create(): FlowInputs { 15 | return { type: "loop", list: {} }; 16 | } 17 | 18 | /** 19 | * Clones a `LoopInputs` object. 20 | * 21 | * @param flow A `LoopInputs` object. 22 | * @returns The cloned `LoopInputs` object. 23 | */ 24 | export function clone(flow: LoopInputs): FlowInputs { 25 | return { ...flow, list: { ...flow.list } }; 26 | } 27 | 28 | /** 29 | * Returns the `ItemInputs` object at the given position within the given `LoopInputs` object, or `null` if there is no item at the given position. 30 | * 31 | * @param flow The `LoopInputs` object. 32 | * @param position The position within the `LoopInputs` object. 33 | * @returns The `ItemInputs` object at the given position within the `LoopInputs` object, or `null` if there is no item at the given position. 34 | */ 35 | export function getItem( 36 | flow: LoopInputs, 37 | position: Position 38 | ): ItemInputs | null { 39 | const { slot } = position as LoopPosition; 40 | if (slot in flow.list) return flow.list[slot]; 41 | return null; 42 | } 43 | 44 | /** 45 | * Sets the `ItemInputs` object at the given position within the given `LoopInputs` object. 46 | * 47 | * @param flow The `LoopInputs` object. 48 | * @param position The position within the `LoopInputs` object. 49 | * @param item The `ItemInputs` object to set. 50 | */ 51 | export function setItem( 52 | flow: LoopInputs, 53 | position: Position, 54 | item: ItemInputs 55 | ): void { 56 | const { slot } = position as LoopPosition; 57 | flow.list[slot] = item; 58 | } 59 | -------------------------------------------------------------------------------- /packages/system/src/types/utils.ts: -------------------------------------------------------------------------------- 1 | import { ListValues } from "./values"; 2 | 3 | /** 4 | * Utility type that defines the structure and values of a condition element in a multi-step form. 5 | */ 6 | export type Cond = { 7 | type: "cond"; 8 | cond: { 9 | then: Values["then"]; 10 | else: Values["else"]; 11 | }; 12 | }; 13 | 14 | /** 15 | * Utility type that defines the structure and values of a loop element in a multi-step form. 16 | */ 17 | export type Loop = { 18 | type: "loop"; 19 | loop: { 20 | do: Values; 21 | }; 22 | }; 23 | 24 | /** 25 | * Utility type that defines the structure and values of a switch element in a multi-step form. 26 | */ 27 | export type Switch< 28 | Values extends { branches: ListValues[]; default: ListValues } 29 | > = { 30 | type: "switch"; 31 | switch: { 32 | branches: Values["branches"]; 33 | default: Values["default"]; 34 | }; 35 | }; 36 | 37 | /** 38 | * Utility type that defines the values of a form element in a multi-step form. 39 | */ 40 | export type Form = { 41 | type: "form"; 42 | form: Values; 43 | }; 44 | 45 | /** 46 | * Utility type that defines the values of a yield element in a multi-step form. 47 | */ 48 | export type Yield = { 49 | type: "yield"; 50 | yield: { 51 | next: Values["next"]; 52 | back: Values["back"]; 53 | }; 54 | }; 55 | 56 | /** 57 | * Utility type that defines the values of a return element in a multi-step form. 58 | */ 59 | export type Return = { 60 | type: "return"; 61 | return: Values; 62 | }; 63 | 64 | /** 65 | * Utility type that defines the values of a variables element in a multi-step form. 66 | */ 67 | export type Variables = { 68 | type: "variables"; 69 | variables: Values; 70 | }; 71 | -------------------------------------------------------------------------------- /packages/system/src/utils/inputs/flow.cond.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ItemInputs, 3 | FlowInputs, 4 | CondInputs, 5 | } from "../../types/state/inputs"; 6 | 7 | import type { Position, CondPosition } from "../../types/state/position"; 8 | 9 | /** 10 | * Creates a `CondInputs` object. 11 | * 12 | * @returns The created `CondInputs` object. 13 | */ 14 | export function create(): FlowInputs { 15 | return { type: "cond", then: {}, else: {} }; 16 | } 17 | 18 | /** 19 | * Clones a `CondInputs` object. 20 | * 21 | * @param flow A `CondInputs` object. 22 | * @returns The cloned `CondInputs` object. 23 | */ 24 | export function clone(flow: CondInputs): FlowInputs { 25 | return { ...flow, then: { ...flow.then }, else: { ...flow.else } }; 26 | } 27 | 28 | /** 29 | * Returns the `ItemInputs` object at the given position within the given `CondInputs` object, or `null` if there is no item at the given position. 30 | * 31 | * @param flow The `CondInputs` object. 32 | * @param position The position within the `CondInputs` object. 33 | * @returns The `ItemInputs` object at the given position within the `CondInputs` object, or `null` if there is no item at the given position. 34 | */ 35 | export function getItem( 36 | flow: CondInputs, 37 | position: Position 38 | ): ItemInputs | null { 39 | const { path, slot } = position as CondPosition; 40 | if (slot in flow[path]) return flow[path][slot]; 41 | return null; 42 | } 43 | 44 | /** 45 | * Sets the `ItemInputs` object at the given position within the given `CondInputs` object. 46 | * 47 | * @param flow The `CondInputs` object. 48 | * @param position The position within the `CondInputs` object. 49 | * @param item The `ItemInputs` object to set. 50 | */ 51 | export function setItem( 52 | flow: CondInputs, 53 | position: Position, 54 | item: ItemInputs 55 | ): void { 56 | const { path, slot } = position as CondPosition; 57 | flow[path][slot] = item; 58 | } 59 | -------------------------------------------------------------------------------- /packages/react/src/index.ts: -------------------------------------------------------------------------------- 1 | export type { OnYield, OnReturn } from "@formity/system"; 2 | 3 | export type { ModelOnYield, ModelOnReturn } from "@formity/system"; 4 | 5 | export type { YieldOutput } from "@formity/system"; 6 | export type { ReturnOutput } from "@formity/system"; 7 | 8 | export type { Inputs } from "@formity/system"; 9 | 10 | export type { 11 | ItemInputs, 12 | FlowInputs, 13 | ListInputs, 14 | CondInputs, 15 | LoopInputs, 16 | SwitchInputs, 17 | FormInputs, 18 | NameInputs, 19 | } from "@formity/system"; 20 | 21 | export type { Point } from "@formity/system"; 22 | 23 | export type { 24 | Position, 25 | ListPosition, 26 | CondPosition, 27 | LoopPosition, 28 | SwitchPosition, 29 | } from "@formity/system"; 30 | 31 | export type { State } from "@formity/system"; 32 | 33 | export type { OnNext, OnBack, GetState, SetState } from "@formity/system"; 34 | 35 | export type { 36 | Cond, 37 | Loop, 38 | Switch, 39 | Form, 40 | Yield, 41 | Return, 42 | Variables, 43 | } from "@formity/system"; 44 | 45 | export type { Values } from "@formity/system"; 46 | 47 | export type { 48 | ItemValues, 49 | FlowValues, 50 | ListValues, 51 | CondValues, 52 | LoopValues, 53 | SwitchValues, 54 | FormValues, 55 | YieldValues, 56 | ReturnValues, 57 | VariablesValues, 58 | } from "@formity/system"; 59 | 60 | export type { Schema } from "./schema"; 61 | 62 | export type { 63 | ItemSchema, 64 | FlowSchema, 65 | ListSchema, 66 | CondSchema, 67 | LoopSchema, 68 | SwitchSchema, 69 | FormSchema, 70 | YieldSchema, 71 | ReturnSchema, 72 | VariablesSchema, 73 | } from "./schema"; 74 | 75 | export type { ModelSchema } from "./schema"; 76 | 77 | export type { 78 | ModelItemSchema, 79 | ModelFlowSchema, 80 | ModelListSchema, 81 | ModelCondSchema, 82 | ModelLoopSchema, 83 | ModelSwitchSchema, 84 | ModelFormSchema, 85 | ModelYieldSchema, 86 | ModelReturnSchema, 87 | ModelVariablesSchema, 88 | } from "./schema"; 89 | 90 | export { Formity } from "./formity"; 91 | -------------------------------------------------------------------------------- /packages/system/src/utils/schema/flow.list.ts: -------------------------------------------------------------------------------- 1 | import type { ItemSchema, ListSchema } from "../../types/schema/model"; 2 | import type { Position, ListPosition } from "../../types/state/position"; 3 | 4 | /** 5 | * Type guard for `ListSchema` objects. 6 | * 7 | * @param schema An `ItemSchema` object. 8 | * @returns A boolean indicating whether the `schema` is a `ListSchema` object. 9 | */ 10 | export function is(schema: ItemSchema): schema is ListSchema { 11 | return Array.isArray(schema); 12 | } 13 | 14 | /** 15 | * Returns the initial position for the given `ListSchema` object if there is an initial position, otherwise it returns `null`. 16 | * 17 | * @param schema A `ListSchema` object. 18 | * @returns A `Position` object representing the initial position, or `null` if there is no initial position. 19 | */ 20 | export function into(schema: ListSchema): Position | null { 21 | if (schema.length > 0) { 22 | return { type: "list", slot: 0 }; 23 | } 24 | return null; 25 | } 26 | 27 | /** 28 | * Returns the next position for the given `ListSchema` object if there is a next position, otherwise it returns `null`. 29 | * 30 | * @param schema A `ListSchema` object. 31 | * @param position A `Position` object representing the current position. 32 | * @returns A `Position` object representing the next position, or `null` if there is no next position. 33 | */ 34 | export function next(schema: ListSchema, position: Position): Position | null { 35 | const { slot } = position as ListPosition; 36 | if (slot < schema.length - 1) { 37 | return { type: "list", slot: slot + 1 }; 38 | } 39 | return null; 40 | } 41 | 42 | /** 43 | * Returns the `ItemSchema` object at the given position within the given `ListSchema` object. 44 | * 45 | * @param schema The `ListSchema` object. 46 | * @param position The position within the `ListSchema` object. 47 | * @returns The `ItemSchema` object at the given position within the `ListSchema` object. 48 | */ 49 | export function at(schema: ListSchema, position: Position): ItemSchema { 50 | const { slot } = position as ListPosition; 51 | return schema[slot]; 52 | } 53 | -------------------------------------------------------------------------------- /apps/react/src/components/fields/checkbox-group.tsx: -------------------------------------------------------------------------------- 1 | import { useId } from "react"; 2 | import { CheckIcon } from "@heroicons/react/20/solid"; 3 | 4 | import { cn } from "../../utils"; 5 | 6 | import Field from "../user-interface/field"; 7 | import Input from "../user-interface/input"; 8 | 9 | interface CheckboxGroupProps { 10 | label: string; 11 | value: string[]; 12 | onChange: (value: string[]) => void; 13 | options: { value: string; label: string }[]; 14 | direction: "x" | "y"; 15 | error: { message: string } | undefined; 16 | } 17 | 18 | export default function CheckboxGroup({ 19 | label, 20 | value, 21 | onChange, 22 | options, 23 | direction, 24 | error, 25 | }: CheckboxGroupProps) { 26 | const id = useId(); 27 | return ( 28 | 29 |
35 | {options.map((option) => ( 36 | { 42 | if (value.includes(option.value)) { 43 | onChange(value.filter((v) => v !== option.value).sort()); 44 | } else { 45 | onChange([...value, option.value].sort()); 46 | } 47 | }} 48 | className={cn( 49 | "group flex cursor-pointer items-center gap-2 focus:outline-none", 50 | { "border-neutral-500": value.includes(option.value) }, 51 | { "border-red-500": error } 52 | )} 53 | > 54 | {option.label} 55 | 63 | 64 | ))} 65 |
66 |
67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /packages/system/src/utils/inputs/flow.list.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | 3 | import type { ListInputs, FormInputs } from "src/types/state/inputs"; 4 | import type { Position } from "src/types/state/position"; 5 | 6 | import { getItem, setItem } from "./flow.list"; 7 | 8 | describe("ListInputs", () => { 9 | describe("getItem", () => { 10 | it("returns the item at the given position within the given `ListInputs` object", () => { 11 | const item: FormInputs = { 12 | a: { 13 | data: { here: true, data: 1 }, 14 | keys: {}, 15 | }, 16 | }; 17 | const flow: ListInputs = { 18 | type: "list", 19 | list: { 20 | 1: item, 21 | }, 22 | }; 23 | const position: Position = { type: "list", slot: 1 }; 24 | const result = getItem(flow, position); 25 | expect(result).toBe(item); 26 | }); 27 | 28 | it("returns null when trying to get an item from a position that doesn't exist in the given `ListInputs` object", () => { 29 | const item: FormInputs = { 30 | a: { 31 | data: { here: true, data: 1 }, 32 | keys: {}, 33 | }, 34 | }; 35 | const flow: ListInputs = { 36 | type: "list", 37 | list: { 38 | 0: item, 39 | }, 40 | }; 41 | const position: Position = { type: "list", slot: 1 }; 42 | const result = getItem(flow, position); 43 | expect(result).toBe(null); 44 | }); 45 | }); 46 | 47 | describe("setItem", () => { 48 | it("sets the item at the given position within the given `ListInputs` object", () => { 49 | const flow: ListInputs = { 50 | type: "list", 51 | list: {}, 52 | }; 53 | const position: Position = { type: "list", slot: 1 }; 54 | const item: FormInputs = { 55 | a: { 56 | data: { here: true, data: 1 }, 57 | keys: {}, 58 | }, 59 | }; 60 | setItem(flow, position, item); 61 | const expected: ListInputs = { 62 | type: "list", 63 | list: { 64 | 1: item, 65 | }, 66 | }; 67 | expect(flow).toEqual(expected); 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /packages/system/src/utils/inputs/flow.loop.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | 3 | import type { LoopInputs, FormInputs } from "src/types/state/inputs"; 4 | import type { Position } from "src/types/state/position"; 5 | 6 | import { getItem, setItem } from "./flow.loop"; 7 | 8 | describe("LoopInputs", () => { 9 | describe("getItem", () => { 10 | it("returns the item at the given position within the given `LoopInputs` object", () => { 11 | const item: FormInputs = { 12 | a: { 13 | data: { here: true, data: 1 }, 14 | keys: {}, 15 | }, 16 | }; 17 | const flow: LoopInputs = { 18 | type: "loop", 19 | list: { 20 | 1: item, 21 | }, 22 | }; 23 | const position: Position = { type: "loop", slot: 1 }; 24 | const result = getItem(flow, position); 25 | expect(result).toBe(item); 26 | }); 27 | 28 | it("returns null when trying to get an item from a position that doesn't exist in the given `LoopInputs` object", () => { 29 | const item: FormInputs = { 30 | a: { 31 | data: { here: true, data: 1 }, 32 | keys: {}, 33 | }, 34 | }; 35 | const flow: LoopInputs = { 36 | type: "loop", 37 | list: { 38 | 0: item, 39 | }, 40 | }; 41 | const position: Position = { type: "loop", slot: 1 }; 42 | const result = getItem(flow, position); 43 | expect(result).toBe(null); 44 | }); 45 | }); 46 | 47 | describe("setItem", () => { 48 | it("sets the item at the given position within the given `LoopInputs` object", () => { 49 | const flow: LoopInputs = { 50 | type: "loop", 51 | list: {}, 52 | }; 53 | const position: Position = { type: "loop", slot: 1 }; 54 | const item: FormInputs = { 55 | a: { 56 | data: { here: true, data: 1 }, 57 | keys: {}, 58 | }, 59 | }; 60 | setItem(flow, position, item); 61 | const expected: LoopInputs = { 62 | type: "loop", 63 | list: { 64 | 1: item, 65 | }, 66 | }; 67 | expect(flow).toEqual(expected); 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /packages/system/src/types/output/return.ts: -------------------------------------------------------------------------------- 1 | import type { Values } from "../values"; 2 | 3 | import type { 4 | ItemValues, 5 | ListValues, 6 | CondValues, 7 | LoopValues, 8 | SwitchValues, 9 | ReturnValues, 10 | } from "../values"; 11 | 12 | /** 13 | * Returns the union of all possible values that can be returned by a multi-step form. 14 | */ 15 | export type ReturnOutput = ListData extends [ 16 | infer Next, 17 | unknown 18 | ] 19 | ? Next 20 | : never; 21 | 22 | type ItemData = Values extends ListValues 23 | ? ListData 24 | : Values extends CondValues 25 | ? CondData 26 | : Values extends LoopValues 27 | ? LoopData 28 | : Values extends SwitchValues 29 | ? SwitchData 30 | : Values extends ReturnValues 31 | ? [Data | Values["return"], true] 32 | : [Data, false]; 33 | 34 | type ListData = Values extends [ 35 | infer First, 36 | ...infer Other 37 | ] 38 | ? First extends ItemValues 39 | ? Other extends ListValues 40 | ? ItemData extends [infer Next, infer Return] 41 | ? Return extends true 42 | ? [Next, true] 43 | : ListData 44 | : never 45 | : never 46 | : never 47 | : [Data, false]; 48 | 49 | type CondData = RoutesData< 50 | [Values["cond"]["then"], Values["cond"]["else"]], 51 | Data 52 | >; 53 | 54 | type LoopData = ListData< 55 | Values["loop"]["do"], 56 | Data 57 | > extends [infer Next, unknown] 58 | ? [Next, false] 59 | : never; 60 | 61 | type SwitchData = RoutesData< 62 | [...Values["switch"]["branches"], Values["switch"]["default"]], 63 | Data 64 | >; 65 | 66 | type RoutesData< 67 | Values extends ListValues[], 68 | Data, 69 | RoutesReturn = true 70 | > = Values extends [infer First, ...infer Other] 71 | ? First extends ListValues 72 | ? Other extends ListValues[] 73 | ? ListData extends [infer Next, infer Return] 74 | ? RoutesReturn extends false 75 | ? RoutesData 76 | : RoutesData 77 | : never 78 | : never 79 | : never 80 | : [Data, RoutesReturn]; 81 | -------------------------------------------------------------------------------- /packages/system/src/utils/schema/flow.cond.ts: -------------------------------------------------------------------------------- 1 | import type { ItemSchema, CondSchema } from "../../types/schema/model"; 2 | import type { Position, CondPosition } from "../../types/state/position"; 3 | 4 | /** 5 | * Type guard for `CondSchema` objects. 6 | * 7 | * @param schema An `ItemSchema` object. 8 | * @returns A boolean indicating whether the `schema` is a `CondSchema` object. 9 | */ 10 | export function is(schema: ItemSchema): schema is CondSchema { 11 | return "cond" in schema; 12 | } 13 | 14 | /** 15 | * Returns the initial position for the given `CondSchema` object if there is an initial position, otherwise it returns `null`. 16 | * 17 | * @param schema A `CondSchema` object. 18 | * @param values An object containing the generated values within the multi-step form. 19 | * @returns A `Position` object representing the initial position, or `null` if there is no initial position. 20 | */ 21 | export function into(schema: CondSchema, values: object): Position | null { 22 | if (schema.cond.if(values)) { 23 | if (schema.cond.then.length > 0) { 24 | return { type: "cond", path: "then", slot: 0 }; 25 | } 26 | } else { 27 | if (schema.cond.else.length > 0) { 28 | return { type: "cond", path: "else", slot: 0 }; 29 | } 30 | } 31 | return null; 32 | } 33 | 34 | /** 35 | * Returns the next position for the given `CondSchema` object if there is a next position, otherwise it returns `null`. 36 | * 37 | * @param schema A `CondSchema` object. 38 | * @param position A `Position` object representing the current position. 39 | * @returns A `Position` object representing the next position, or `null` if there is no next position. 40 | */ 41 | export function next(schema: CondSchema, position: Position): Position | null { 42 | const { path, slot } = position as CondPosition; 43 | if (slot < schema.cond[path].length - 1) { 44 | return { type: "cond", path, slot: slot + 1 }; 45 | } 46 | return null; 47 | } 48 | 49 | /** 50 | * Returns the `ItemSchema` object at the given position within the given `CondSchema` object. 51 | * 52 | * @param schema The `CondSchema` object. 53 | * @param position The position within the `CondSchema` object. 54 | * @returns The `ItemSchema` object at the given position within the `CondSchema` object. 55 | */ 56 | export function at(schema: CondSchema, position: Position): ItemSchema { 57 | const { path, slot } = position as CondPosition; 58 | return schema.cond[path][slot]; 59 | } 60 | -------------------------------------------------------------------------------- /packages/react/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @formity/react 2 | 3 | ## 1.0.0 4 | 5 | ### Major Changes 6 | 7 | - This release is a version bump from 0.5.0 to 1.0.0. Although no changes in functionality have been made, we believe the project has reached a level of maturity and stability suitable for production use. All features and behaviors remain as in 0.5.0, and this version number now signals a stable API. 8 | 9 | ### Patch Changes 10 | 11 | - Updated dependencies 12 | - @formity/system@1.0.0 13 | 14 | ## 0.5.0 15 | 16 | ### Minor Changes 17 | 18 | - Renamed ReturnValues and YieldValues to ReturnOutput and YieldOutput 19 | - Exported additional Formity types 20 | 21 | ### Patch Changes 22 | 23 | - Updated dependencies 24 | - @formity/system@0.5.0 25 | 26 | ## 0.4.1 27 | 28 | ### Patch Changes 29 | 30 | - Allow users to submit values that are not handled by the values function (it is needed if we want to jump to specific steps) 31 | - Updated dependencies 32 | - @formity/system@0.4.1 33 | 34 | ## 0.4.0 35 | 36 | ### Minor Changes 37 | 38 | - Updated Yield schema element to yield values when navigating to previous steps 39 | 40 | ### Patch Changes 41 | 42 | - Updated dependencies 43 | - @formity/system@0.4.0 44 | 45 | ## 0.3.1 46 | 47 | ### Patch Changes 48 | 49 | - Solved bug about nextState being called twice in strict mode 50 | 51 | ## 0.3.0 52 | 53 | ### Minor Changes 54 | 55 | - Refractored codebase for improved readability and maintainability. 56 | - Renamed `Flow` type to `State` for improved clarity and alignment with its purpose. 57 | - Renamed properties of the `State` type: 58 | - `cursors` -> `points` 59 | - `entries` -> `inputs` 60 | - Updated function names: 61 | - `getFlow` -> `getState` 62 | - `setFlow` -> `setState` 63 | - Renamed the `initialFlow` prop in the `Formity` component to `initialState` for consistency with the updated naming conventions. 64 | 65 | ### Patch Changes 66 | 67 | - Updated dependencies 68 | - @formity/system@0.3.0 69 | 70 | ## 0.2.1 71 | 72 | ### Patch Changes 73 | 74 | - Updated README.md 75 | - Updated dependencies 76 | - @formity/system@0.2.1 77 | 78 | ## 0.2.0 79 | 80 | ### Minor Changes 81 | 82 | - The switch element has been introduced 83 | 84 | ### Patch Changes 85 | 86 | - Updated dependencies 87 | - @formity/system@0.2.0 88 | 89 | ## 0.1.0 90 | 91 | ### Minor Changes 92 | 93 | - Created @formity/react 94 | 95 | ### Patch Changes 96 | 97 | - Updated dependencies 98 | - @formity/system@0.1.0 99 | -------------------------------------------------------------------------------- /packages/system/src/utils/schema/flow.list.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | 3 | import type { ListSchema, ReturnSchema } from "../../types/schema/model"; 4 | import type { Position } from "src/types/state/position"; 5 | 6 | import { into, next, at } from "./flow.list"; 7 | 8 | describe("ListSchema", () => { 9 | describe("into", () => { 10 | it("navigates into the `ListSchema` object", () => { 11 | const schema: ListSchema = [ 12 | { 13 | form: { 14 | values: () => ({}), 15 | render: () => ({}), 16 | }, 17 | }, 18 | ]; 19 | const position = into(schema); 20 | expect(position).toEqual({ type: "list", slot: 0 }); 21 | }); 22 | 23 | it("doesn't navigate into the `ListSchema` object", () => { 24 | const schema: ListSchema = []; 25 | const position = into(schema); 26 | expect(position).toEqual(null); 27 | }); 28 | }); 29 | 30 | describe("next", () => { 31 | it("navigates to the next item in the `ListSchema` object", () => { 32 | const schema: ListSchema = [ 33 | { 34 | form: { 35 | values: () => ({}), 36 | render: () => ({}), 37 | }, 38 | }, 39 | { 40 | form: { 41 | values: () => ({}), 42 | render: () => ({}), 43 | }, 44 | }, 45 | ]; 46 | const current: Position = { type: "list", slot: 0 }; 47 | const position = next(schema, current); 48 | expect(position).toEqual({ type: "list", slot: 1 }); 49 | }); 50 | 51 | it("doesn't navigate to the next item in the `ListSchema` object", () => { 52 | const schema: ListSchema = [ 53 | { 54 | form: { 55 | values: () => ({}), 56 | render: () => ({}), 57 | }, 58 | }, 59 | ]; 60 | const current: Position = { type: "list", slot: 0 }; 61 | const position = next(schema, current); 62 | expect(position).toEqual(null); 63 | }); 64 | }); 65 | }); 66 | 67 | describe("at", () => { 68 | it("retrieves the item at the specified position in the `ListSchema` object", () => { 69 | const item: ReturnSchema = { return: () => ({}) }; 70 | const schema: ListSchema = [{ variables: () => ({}) }, item]; 71 | const position: Position = { type: "list", slot: 1 }; 72 | const result = at(schema, position); 73 | expect(result).toBe(item); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /apps/react/src/components/fields/listbox.tsx: -------------------------------------------------------------------------------- 1 | import { useId } from "react"; 2 | import { 3 | Listbox as HeadlessListbox, 4 | ListboxButton, 5 | ListboxOption, 6 | ListboxOptions, 7 | } from "@headlessui/react"; 8 | import { CheckIcon, ChevronDownIcon } from "@heroicons/react/20/solid"; 9 | 10 | import { cn } from "../../utils"; 11 | 12 | import Field from "../user-interface/field"; 13 | import Input from "../user-interface/input"; 14 | 15 | interface ListboxProps { 16 | label: string; 17 | value: string; 18 | onChange: (value: string) => void; 19 | options: { value: string; label: string }[]; 20 | error: { message: string } | undefined; 21 | } 22 | 23 | export default function Listbox({ 24 | label, 25 | value, 26 | onChange, 27 | options, 28 | error, 29 | }: ListboxProps) { 30 | const id = useId(); 31 | const option = options.find((option) => option.value === value)!; 32 | return ( 33 | 34 | 35 | 44 | {option.label} 45 | 69 | 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /packages/system/src/utils/inputs/flow.switch.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ItemInputs, 3 | FlowInputs, 4 | SwitchInputs, 5 | } from "../../types/state/inputs"; 6 | 7 | import type { Position, SwitchPosition } from "../../types/state/position"; 8 | 9 | /** 10 | * Creates a `SwitchInputs` object. 11 | * 12 | * @returns The created `SwitchInputs` object. 13 | */ 14 | export function create(): FlowInputs { 15 | return { type: "switch", branches: {}, default: {} }; 16 | } 17 | 18 | /** 19 | * Clones a `SwitchInputs` object. 20 | * 21 | * @param flow A `SwitchInputs` object. 22 | * @returns The cloned `SwitchInputs` object. 23 | */ 24 | export function clone(flow: SwitchInputs): FlowInputs { 25 | return { 26 | ...flow, 27 | branches: Object.fromEntries( 28 | Object.entries(flow.branches).map(([key, value]) => [key, { ...value }]) 29 | ), 30 | default: { ...flow.default }, 31 | }; 32 | } 33 | 34 | /** 35 | * Returns the `ItemInputs` object at the given position within the given `SwitchInputs` object, or `null` if there is no item at the given position. 36 | * 37 | * @param flow The `SwitchInputs` object. 38 | * @param position The position within the `SwitchInputs` object. 39 | * @returns The `ItemInputs` object at the given position within the `SwitchInputs` object, or `null` if there is no item at the given position. 40 | */ 41 | export function getItem( 42 | flow: SwitchInputs, 43 | position: Position 44 | ): ItemInputs | null { 45 | const { branch, slot } = position as SwitchPosition; 46 | if (branch >= 0) { 47 | if (branch in flow.branches) { 48 | if (slot in flow.branches[branch]) return flow.branches[branch][slot]; 49 | } 50 | } else { 51 | if (slot in flow.default) return flow.default[slot]; 52 | } 53 | return null; 54 | } 55 | 56 | /** 57 | * Sets the `ItemInputs` object at the given position within the given `SwitchInputs` object. 58 | * 59 | * @param flow The `SwitchInputs` object. 60 | * @param position The position within the `SwitchInputs` object. 61 | * @param item The `ItemInputs` object to set. 62 | */ 63 | export function setItem( 64 | flow: SwitchInputs, 65 | position: Position, 66 | item: ItemInputs 67 | ): void { 68 | const { branch, slot } = position as SwitchPosition; 69 | if (branch >= 0) { 70 | if (branch in flow.branches) { 71 | flow.branches[branch][slot] = item; 72 | } else { 73 | flow.branches[branch] = { [slot]: item }; 74 | } 75 | } else { 76 | flow.default[slot] = item; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /packages/system/src/utils/schema/flow.loop.ts: -------------------------------------------------------------------------------- 1 | import type { ItemSchema, LoopSchema } from "../../types/schema/model"; 2 | import type { Position, LoopPosition } from "../../types/state/position"; 3 | 4 | /** 5 | * Type guard for `LoopSchema` objects. 6 | * 7 | * @param schema An `ItemSchema` object. 8 | * @returns A boolean indicating whether the `schema` is a `LoopSchema` object. 9 | */ 10 | export function is(schema: ItemSchema): schema is LoopSchema { 11 | return "loop" in schema; 12 | } 13 | 14 | /** 15 | * Returns the initial position for the given `LoopSchema` object if there is an initial position, otherwise it returns `null`. 16 | * 17 | * @param schema A `LoopSchema` object. 18 | * @param values An object containing the generated values within the multi-step form. 19 | * @returns A `Position` object representing the initial position, or `null` if there is no initial position. 20 | */ 21 | export function into(schema: LoopSchema, values: object): Position | null { 22 | if (schema.loop.while(values)) { 23 | if (schema.loop.do.length > 0) { 24 | return { type: "loop", slot: 0 }; 25 | } 26 | } 27 | return null; 28 | } 29 | 30 | /** 31 | * Returns the next position for the given `LoopSchema` object if there is a next position, otherwise it returns `null`. 32 | * 33 | * @param schema A `LoopSchema` object. 34 | * @param position A `Position` object representing the current position. 35 | * @param values An object containing the generated values within the multi-step form. 36 | * @returns A `Position` object representing the next position, or `null` if there is no next position. 37 | */ 38 | export function next( 39 | schema: LoopSchema, 40 | position: Position, 41 | values: object 42 | ): Position | null { 43 | const { slot } = position as LoopPosition; 44 | if (slot < schema.loop.do.length - 1) { 45 | return { type: "loop", slot: slot + 1 }; 46 | } 47 | if (schema.loop.while(values)) { 48 | return { type: "loop", slot: 0 }; 49 | } 50 | return null; 51 | } 52 | 53 | /** 54 | * Returns the `ItemSchema` object at the given position within the given `LoopSchema` object. 55 | * 56 | * @param schema The `LoopSchema` object. 57 | * @param position The position within the `LoopSchema` object. 58 | * @returns The `ItemSchema` object at the given position within the `LoopSchema` object. 59 | */ 60 | export function at(schema: LoopSchema, position: Position): ItemSchema { 61 | const { slot } = position as LoopPosition; 62 | return schema.loop.do[slot]; 63 | } 64 | -------------------------------------------------------------------------------- /apps/react/src/schemas/flow.list.tsx: -------------------------------------------------------------------------------- 1 | import type { Schema, Form, Return, Variables } from "@formity/react"; 2 | 3 | import { zodResolver } from "@hookform/resolvers/zod"; 4 | import { z } from "zod"; 5 | 6 | import { Step, Layout, Row, TextField, NextButton } from "../components"; 7 | 8 | import { MultiStep } from "../multi-step"; 9 | 10 | export type ListValues = [ 11 | Variables<{ fullName: string }>, 12 | [Form<{ name: string; surname: string }>, [Variables<{ fullName: string }>]], 13 | Return<{ fullName: string }> 14 | ]; 15 | 16 | export const listSchema: Schema = [ 17 | { 18 | variables: () => ({ 19 | fullName: "", 20 | }), 21 | }, 22 | [ 23 | { 24 | form: { 25 | values: () => ({ 26 | name: ["", []], 27 | surname: ["", []], 28 | }), 29 | render: ({ values, ...rest }) => ( 30 | 31 | 46 | , 54 | , 59 | ]} 60 | />, 61 | ]} 62 | button={Next} 63 | /> 64 | 65 | 66 | ), 67 | }, 68 | }, 69 | [ 70 | { 71 | variables: ({ name, surname }) => ({ 72 | fullName: `${name} ${surname}`, 73 | }), 74 | }, 75 | ], 76 | ], 77 | { 78 | return: ({ fullName }) => ({ 79 | fullName, 80 | }), 81 | }, 82 | ]; 83 | -------------------------------------------------------------------------------- /packages/system/src/types/output/yield.ts: -------------------------------------------------------------------------------- 1 | import type { Values } from "../values"; 2 | 3 | import type { 4 | ItemValues, 5 | ListValues, 6 | CondValues, 7 | LoopValues, 8 | SwitchValues, 9 | YieldValues, 10 | ReturnValues, 11 | } from "../values"; 12 | 13 | /** 14 | * Returns the union of all possible values that can be yielded by a multi-step form. 15 | */ 16 | export type YieldOutput = ListData extends [ 17 | infer Next, 18 | unknown 19 | ] 20 | ? Next 21 | : never; 22 | 23 | type ItemData = Values extends ListValues 24 | ? ListData 25 | : Values extends CondValues 26 | ? CondData 27 | : Values extends LoopValues 28 | ? LoopData 29 | : Values extends SwitchValues 30 | ? SwitchData 31 | : Values extends YieldValues 32 | ? YieldData 33 | : Values extends ReturnValues 34 | ? [Data, true] 35 | : [Data, false]; 36 | 37 | type ListData = Values extends [ 38 | infer First, 39 | ...infer Other 40 | ] 41 | ? First extends ItemValues 42 | ? Other extends ListValues 43 | ? ItemData extends [infer Next, infer Return] 44 | ? Return extends true 45 | ? [Next, true] 46 | : ListData 47 | : never 48 | : never 49 | : never 50 | : [Data, false]; 51 | 52 | type CondData = RoutesData< 53 | [Values["cond"]["then"], Values["cond"]["else"]], 54 | Data 55 | >; 56 | 57 | type LoopData = ListData< 58 | Values["loop"]["do"], 59 | Data 60 | > extends [infer Next, unknown] 61 | ? [Next, false] 62 | : never; 63 | 64 | type SwitchData = RoutesData< 65 | [...Values["switch"]["branches"], Values["switch"]["default"]], 66 | Data 67 | >; 68 | 69 | type YieldData = [ 70 | Data | Values["yield"]["next"][number] | Values["yield"]["back"][number], 71 | false 72 | ]; 73 | 74 | type RoutesData< 75 | Values extends ListValues[], 76 | Data, 77 | RoutesReturn = true 78 | > = Values extends [infer First, ...infer Other] 79 | ? First extends ListValues 80 | ? Other extends ListValues[] 81 | ? ListData extends [infer Next, infer Return] 82 | ? RoutesReturn extends false 83 | ? RoutesData 84 | : RoutesData 85 | : never 86 | : never 87 | : never 88 | : [Data, RoutesReturn]; 89 | -------------------------------------------------------------------------------- /packages/system/src/index.ts: -------------------------------------------------------------------------------- 1 | export type { OnYield, OnReturn } from "./types/handlers/typed"; 2 | 3 | export type { 4 | OnYield as ModelOnYield, 5 | OnReturn as ModelOnReturn, 6 | } from "./types/handlers/model"; 7 | 8 | export type { YieldOutput } from "./types/output/yield"; 9 | export type { ReturnOutput } from "./types/output/return"; 10 | 11 | export type { ListSchema as Schema } from "./types/schema/typed"; 12 | 13 | export type { 14 | ItemSchema, 15 | FlowSchema, 16 | ListSchema, 17 | CondSchema, 18 | LoopSchema, 19 | SwitchSchema, 20 | FormSchema, 21 | YieldSchema, 22 | ReturnSchema, 23 | VariablesSchema, 24 | } from "./types/schema/typed"; 25 | 26 | export type { ListSchema as ModelSchema } from "./types/schema/model"; 27 | 28 | export type { 29 | ItemSchema as ModelItemSchema, 30 | FlowSchema as ModelFlowSchema, 31 | ListSchema as ModelListSchema, 32 | CondSchema as ModelCondSchema, 33 | LoopSchema as ModelLoopSchema, 34 | SwitchSchema as ModelSwitchSchema, 35 | FormSchema as ModelFormSchema, 36 | YieldSchema as ModelYieldSchema, 37 | ReturnSchema as ModelReturnSchema, 38 | VariablesSchema as ModelVariablesSchema, 39 | } from "./types/schema/model"; 40 | 41 | export type { ListInputs as Inputs } from "./types/state/inputs"; 42 | 43 | export type { 44 | ItemInputs, 45 | FlowInputs, 46 | ListInputs, 47 | CondInputs, 48 | LoopInputs, 49 | SwitchInputs, 50 | FormInputs, 51 | NameInputs, 52 | } from "./types/state/inputs"; 53 | 54 | export type { Point } from "./types/state/point"; 55 | 56 | export type { 57 | Position, 58 | ListPosition, 59 | CondPosition, 60 | LoopPosition, 61 | SwitchPosition, 62 | } from "./types/state/position"; 63 | 64 | export type { State } from "./types/state/state"; 65 | 66 | export type { OnNext, OnBack, GetState, SetState } from "./types/controls"; 67 | 68 | export type { 69 | Cond, 70 | Loop, 71 | Switch, 72 | Form, 73 | Yield, 74 | Return, 75 | Variables, 76 | } from "./types/utils"; 77 | 78 | export type { ListValues as Values } from "./types/values"; 79 | 80 | export type { 81 | ItemValues, 82 | FlowValues, 83 | ListValues, 84 | CondValues, 85 | LoopValues, 86 | SwitchValues, 87 | FormValues, 88 | YieldValues, 89 | ReturnValues, 90 | VariablesValues, 91 | } from "./types/values"; 92 | 93 | export { 94 | getInitialState, 95 | getNextState, 96 | getPreviousState, 97 | } from "./utils/navigate"; 98 | export { getForm } from "./utils/form"; 99 | export { getState } from "./utils/state"; 100 | -------------------------------------------------------------------------------- /packages/system/src/utils/inputs/form.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | 3 | import type { FormInputs } from "src/types/state/inputs"; 4 | 5 | import { get, set } from "./form"; 6 | 7 | describe("FormInputs", () => { 8 | describe("get", () => { 9 | it("returns the value that is in the given `FormInputs` object", () => { 10 | const form: FormInputs = { 11 | a: { 12 | data: { here: false }, 13 | keys: { 14 | x: { 15 | data: { here: false }, 16 | keys: { 17 | y: { 18 | data: { here: true, data: 1 }, 19 | keys: {}, 20 | }, 21 | }, 22 | }, 23 | }, 24 | }, 25 | }; 26 | const name: string = "a"; 27 | const keys: PropertyKey[] = ["x", "y"]; 28 | const defaultValue: unknown = 2; 29 | const result = get(form, name, keys, defaultValue); 30 | expect(result).toEqual(1); 31 | }); 32 | 33 | it("returns the default value if the keys are not encountered in the given `FormInputs` object", () => { 34 | const form: FormInputs = { 35 | a: { 36 | data: { here: false }, 37 | keys: { 38 | x: { 39 | data: { here: false }, 40 | keys: { 41 | y: { 42 | data: { here: true, data: 1 }, 43 | keys: {}, 44 | }, 45 | }, 46 | }, 47 | }, 48 | }, 49 | }; 50 | const name: string = "a"; 51 | const keys: PropertyKey[] = ["x", "z"]; 52 | const defaultValue: unknown = 2; 53 | const result = get(form, name, keys, defaultValue); 54 | expect(result).toEqual(2); 55 | }); 56 | }); 57 | 58 | describe("set", () => { 59 | it("sets the value in the given `FormInputs` object", () => { 60 | const flow: FormInputs = {}; 61 | const name: string = "a"; 62 | const keys: PropertyKey[] = ["x", "y"]; 63 | const data: unknown = 1; 64 | const result = set(flow, name, keys, data); 65 | const expected: FormInputs = { 66 | a: { 67 | data: { here: false }, 68 | keys: { 69 | x: { 70 | data: { here: false }, 71 | keys: { 72 | y: { 73 | data: { here: true, data: 1 }, 74 | keys: {}, 75 | }, 76 | }, 77 | }, 78 | }, 79 | }, 80 | }; 81 | expect(result).toEqual(expected); 82 | }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /packages/react/src/formity.tsx: -------------------------------------------------------------------------------- 1 | import type { Values, OnYield, OnReturn, State } from "@formity/system"; 2 | 3 | import { useState, useCallback } from "react"; 4 | import { 5 | getInitialState, 6 | getNextState, 7 | getPreviousState, 8 | getForm, 9 | getState, 10 | } from "@formity/system"; 11 | 12 | import type { Schema } from "./schema"; 13 | 14 | /** 15 | * The properties of the multi-step form. 16 | * 17 | * @template V The structure and values of the multi-step form. 18 | * @template I The input values of the multi-step form. 19 | * @template P The parameter values of the multi-step form. 20 | * @param schema The structure and behavior of the multi-step form. 21 | * @param inputs The input values of the multi-step form. 22 | * @param params The parameter values of the multi-step form. 23 | * @param onYield Callback function invoked when the multi-step form yields values. 24 | * @param onReturn Callback function invoked when the multi-step form returns values. 25 | * @param initialState The initial state of the multi-step form. 26 | */ 27 | interface FormityProps { 28 | schema: Schema; 29 | inputs?: I; 30 | params?: P; 31 | onYield?: OnYield; 32 | onReturn?: OnReturn; 33 | initialState?: State; 34 | } 35 | 36 | /** 37 | * Renders a multi-step form. 38 | */ 39 | export function Formity< 40 | V extends Values, 41 | I extends object = object, 42 | P extends object = object 43 | >({ 44 | schema, 45 | inputs = {} as I, 46 | params = {} as P, 47 | onYield = () => {}, 48 | onReturn = () => {}, 49 | initialState, 50 | }: FormityProps): React.ReactNode { 51 | const [state, setState] = useState(() => { 52 | if (initialState) return initialState; 53 | return getInitialState(schema, inputs, onYield); 54 | }); 55 | 56 | const onNext = useCallback( 57 | (values: object) => { 58 | const updated = getNextState(state, schema, values, onYield, onReturn); 59 | setState(updated); 60 | }, 61 | [state, schema, onYield, onReturn] 62 | ); 63 | 64 | const onBack = useCallback( 65 | (values: object) => { 66 | const updated = getPreviousState(state, schema, values, onYield); 67 | setState(updated); 68 | }, 69 | [state, schema] 70 | ); 71 | 72 | const _getState = useCallback( 73 | (values: object) => { 74 | return getState(state, schema, values); 75 | }, 76 | [state, schema] 77 | ); 78 | 79 | const _setState = useCallback((state: State) => { 80 | setState(state); 81 | }, []); 82 | 83 | return getForm(state, schema, params, onNext, onBack, _getState, _setState); 84 | } 85 | -------------------------------------------------------------------------------- /packages/system/src/utils/form.ts: -------------------------------------------------------------------------------- 1 | import type { Values } from "../types/values"; 2 | 3 | import type { Schema as TypedSchema } from "../types/schema/typed"; 4 | import type { Schema, FormSchema } from "../types/schema/model"; 5 | 6 | import type { State } from "../types/state/state"; 7 | 8 | import type { OnNext, OnBack, GetState, SetState } from "../types/controls"; 9 | 10 | import * as FlowSchemaUtils from "./schema/flow"; 11 | import * as FlowInputsUtils from "./inputs/flow"; 12 | 13 | /** 14 | * Returns the rendered form for the current step of the multi-step form. 15 | * 16 | * @param state The current state of the multi-step form. 17 | * @param schema The `Schema` object representing the multi-step form. 18 | * @param params An object containing the parameters for the form. 19 | * @param onNext A callback function used to navigate to the next step of the multi-step form. 20 | * @param onBack A callback function used to navigate to the previous step of the multi-step form. 21 | * @param getState A callback function used to get the current state of the multi-step form. 22 | * @param setState A callback function used to set the current state of the multi-step form. 23 | * @returns The rendered form for the current step of the multi-step form. 24 | */ 25 | export function getForm< 26 | R, 27 | V extends Values, 28 | I extends object, 29 | P extends object 30 | >( 31 | state: State, 32 | schema: TypedSchema, 33 | params: P, 34 | onNext: OnNext, 35 | onBack: OnBack, 36 | getState: GetState, 37 | setState: SetState 38 | ): R { 39 | const _schema = schema as Schema; 40 | const _params = params as object; 41 | return _getForm( 42 | state, 43 | _schema, 44 | _params, 45 | onNext, 46 | onBack, 47 | getState, 48 | setState 49 | ) as R; 50 | } 51 | 52 | function _getForm( 53 | state: State, 54 | schema: Schema, 55 | params: object, 56 | onNext: OnNext, 57 | onBack: OnBack, 58 | getState: GetState, 59 | setState: SetState 60 | ): unknown { 61 | const point = state.points[state.points.length - 1]; 62 | const form = FlowSchemaUtils.find(schema, point.path) as FormSchema; 63 | const inputs = point.values; 64 | const values = Object.fromEntries( 65 | Object.entries(form["form"]["values"](point.values)).map( 66 | ([name, [value, keys]]) => { 67 | return [ 68 | name, 69 | FlowInputsUtils.get(state.inputs, point.path, name, keys, value), 70 | ]; 71 | } 72 | ) 73 | ); 74 | return form["form"]["render"]({ 75 | inputs, 76 | values, 77 | params, 78 | onNext, 79 | onBack, 80 | getState, 81 | setState, 82 | }); 83 | } 84 | -------------------------------------------------------------------------------- /packages/system/src/types/values.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This type is meant to be extended and is used to define the structure and values of a multi-step form. 3 | */ 4 | export type Values = ListValues; 5 | 6 | /** 7 | * This type is meant to be extended and is used to define the structure and values of any element in a multi-step form. 8 | */ 9 | export type ItemValues = 10 | | FlowValues 11 | | FormValues 12 | | YieldValues 13 | | ReturnValues 14 | | VariablesValues; 15 | 16 | /** 17 | * This type is meant to be extended and is used to define the structure and values of any flow element in a multi-step form. 18 | */ 19 | export type FlowValues = ListValues | CondValues | LoopValues | SwitchValues; 20 | 21 | /** 22 | * This type is meant to be extended and is used to define the structure and values of a list element in a multi-step form. 23 | */ 24 | export type ListValues = ItemValues[]; 25 | 26 | /** 27 | * This type is meant to be extended and is used to define the structure and values of a condition element in a multi-step form. 28 | */ 29 | export type CondValues = { 30 | type: "cond"; 31 | cond: { 32 | then: ListValues; 33 | else: ListValues; 34 | }; 35 | }; 36 | 37 | /** 38 | * This type is meant to be extended and is used to define the structure and values of a loop element in a multi-step form. 39 | */ 40 | export type LoopValues = { 41 | type: "loop"; 42 | loop: { 43 | do: ListValues; 44 | }; 45 | }; 46 | 47 | /** 48 | * This type is meant to be extended and is used to define the structure and values of a switch element in a multi-step form. 49 | */ 50 | export type SwitchValues = { 51 | type: "switch"; 52 | switch: { 53 | branches: ListValues[]; 54 | default: ListValues; 55 | }; 56 | }; 57 | 58 | /** 59 | * This type is meant to be extended and is used to define the values of a form element in a multi-step form. 60 | */ 61 | export type FormValues = { 62 | type: "form"; 63 | form: object; 64 | }; 65 | 66 | /** 67 | * This type is meant to be extended and is used to define the values of a yield element in a multi-step form. 68 | */ 69 | export type YieldValues = { 70 | type: "yield"; 71 | yield: { 72 | next: unknown[]; 73 | back: unknown[]; 74 | }; 75 | }; 76 | 77 | /** 78 | * This type is meant to be extended and is used to define the values of a return element in a multi-step form. 79 | */ 80 | export type ReturnValues = { 81 | type: "return"; 82 | return: unknown; 83 | }; 84 | 85 | /** 86 | * This type is meant to be extended and is used to define the values of a variables element in a multi-step form. 87 | */ 88 | export type VariablesValues = { 89 | type: "variables"; 90 | variables: object; 91 | }; 92 | -------------------------------------------------------------------------------- /packages/system/src/utils/inputs/form.ts: -------------------------------------------------------------------------------- 1 | import type { FormInputs, NameInputs } from "../../types/state/inputs"; 2 | 3 | /** 4 | * Returns the value that is in the given `FormInputs` object using the following parameters: 5 | * 6 | * - `name`: The name of the value within the `FormInputs` object. 7 | * - `keys`: The list of keys that is used to access the value within the `FormInputs` object. 8 | * - `defaultValue`: The default value to return if the value is not found. 9 | * 10 | * @param form The `FormInputs` object. 11 | * @param name The name of the value within the `FormInputs` object. 12 | * @param keys The list of keys that is used to access the value within the `FormInputs` object. 13 | * @param defaultValue The default value to return if the value is not found. 14 | * @returns The value that is in the given `FormInputs` object or the default value if the value is not found. 15 | */ 16 | export function get( 17 | form: FormInputs, 18 | name: string, 19 | keys: PropertyKey[], 20 | defaultValue: unknown 21 | ): unknown { 22 | let current: NameInputs = form[name]; 23 | for (const key of keys) { 24 | if (key in current.keys) { 25 | current = current.keys[key]; 26 | } else { 27 | return defaultValue; 28 | } 29 | } 30 | if (current.data.here) { 31 | return current.data.data; 32 | } 33 | return defaultValue; 34 | } 35 | 36 | /** 37 | * Sets the value in the given `FormInputs` object using the following parameters: 38 | * 39 | * - `name`: The name of the value within the `FormInputs` object. 40 | * - `keys`: The list of keys that is used to access the value within the `FormInputs` object. 41 | * - `data`: The value to set. 42 | * 43 | * @param form The `FormInputs` object. 44 | * @param name The name of the value within the `FormInputs` object. 45 | * @param keys The list of keys that is used to access the value within the `FormInputs` object. 46 | * @param data The value to set. 47 | * @returns The updated `FormInputs` object. 48 | */ 49 | export function set( 50 | form: FormInputs, 51 | name: string, 52 | keys: PropertyKey[], 53 | data: unknown 54 | ): FormInputs { 55 | const updated: FormInputs = { ...form }; 56 | if (name in form) { 57 | updated[name] = { ...form[name], keys: { ...form[name].keys } }; 58 | } else { 59 | updated[name] = { data: { here: false }, keys: {} }; 60 | } 61 | let current: NameInputs = updated[name]; 62 | for (const key of keys) { 63 | if (key in current.keys) { 64 | const name: NameInputs = current.keys[key]; 65 | const copy: NameInputs = { ...name, keys: { ...name.keys } }; 66 | current.keys[key] = copy; 67 | current = copy; 68 | } else { 69 | const name: NameInputs = { data: { here: false }, keys: {} }; 70 | current.keys[key] = name; 71 | current = name; 72 | } 73 | } 74 | current.data = { here: true, data }; 75 | return updated; 76 | } 77 | -------------------------------------------------------------------------------- /packages/system/src/utils/schema/flow.switch.ts: -------------------------------------------------------------------------------- 1 | import type { ItemSchema, SwitchSchema } from "../../types/schema/model"; 2 | import type { Position, SwitchPosition } from "../../types/state/position"; 3 | 4 | /** 5 | * Type guard for `SwitchSchema` objects. 6 | * 7 | * @param schema An `ItemSchema` object. 8 | * @returns A boolean indicating whether the `schema` is a `SwitchSchema` object. 9 | */ 10 | export function is(schema: ItemSchema): schema is SwitchSchema { 11 | return "switch" in schema; 12 | } 13 | 14 | /** 15 | * Returns the initial position for the given `SwitchSchema` object if there is an initial position, otherwise it returns `null`. 16 | * 17 | * @param schema A `SwitchSchema` object. 18 | * @param values An object containing the generated values within the multi-step form. 19 | * @returns A `Position` object representing the initial position, or `null` if there is no initial position. 20 | */ 21 | export function into(schema: SwitchSchema, values: object): Position | null { 22 | for (let i = 0; i < schema.switch.branches.length; i++) { 23 | const branch = schema.switch.branches[i]; 24 | if (branch.case(values)) { 25 | if (branch.then.length > 0) { 26 | return { type: "switch", branch: i, slot: 0 }; 27 | } 28 | return null; 29 | } 30 | } 31 | if (schema.switch.default.length > 0) { 32 | return { type: "switch", branch: -1, slot: 0 }; 33 | } 34 | return null; 35 | } 36 | 37 | /** 38 | * Returns the next position for the given `SwitchSchema` object if there is a next position, otherwise it returns `null`. 39 | * 40 | * @param schema A `SwitchSchema` object. 41 | * @param position A `Position` object representing the current position. 42 | * @returns A `Position` object representing the next position, or `null` if there is no next position. 43 | */ 44 | export function next( 45 | schema: SwitchSchema, 46 | position: Position 47 | ): Position | null { 48 | const { branch, slot } = position as SwitchPosition; 49 | if (branch >= 0) { 50 | if (slot < schema.switch.branches[branch].then.length - 1) { 51 | return { type: "switch", branch, slot: slot + 1 }; 52 | } 53 | return null; 54 | } 55 | if (slot < schema.switch.default.length - 1) { 56 | return { type: "switch", branch: -1, slot: slot + 1 }; 57 | } 58 | return null; 59 | } 60 | 61 | /** 62 | * Returns the `ItemSchema` object at the given position within the given `SwitchSchema` object. 63 | * 64 | * @param schema The `SwitchSchema` object. 65 | * @param position The position within the `SwitchSchema` object. 66 | * @returns The `ItemSchema` object at the given position within the `SwitchSchema` object. 67 | */ 68 | export function at(schema: SwitchSchema, position: Position): ItemSchema { 69 | const { branch, slot } = position as SwitchPosition; 70 | if (branch >= 0) { 71 | return schema.switch.branches[branch].then[slot]; 72 | } 73 | return schema.switch.default[slot]; 74 | } 75 | -------------------------------------------------------------------------------- /packages/system/src/types/schema/model.ts: -------------------------------------------------------------------------------- 1 | import type { OnNext, OnBack, GetState, SetState } from "../controls"; 2 | 3 | /** 4 | * Defines the structure and behavior of a multi-step form. 5 | */ 6 | export type Schema = ListSchema; 7 | 8 | /** 9 | * Defines the structure and behavior of any element in a multi-step form. 10 | */ 11 | export type ItemSchema = 12 | | FlowSchema 13 | | FormSchema 14 | | YieldSchema 15 | | ReturnSchema 16 | | VariablesSchema; 17 | 18 | /** 19 | * Defines the structure and behavior of any flow element in a multi-step form. 20 | */ 21 | export type FlowSchema = 22 | | ListSchema 23 | | CondSchema 24 | | LoopSchema 25 | | SwitchSchema; 26 | 27 | /** 28 | * Defines the structure and behavior of a list element in a multi-step form. 29 | */ 30 | export type ListSchema = ItemSchema[]; 31 | 32 | /** 33 | * Defines the structure and behavior of a condition element in a multi-step form. 34 | */ 35 | export type CondSchema = { 36 | cond: { 37 | if: (inputs: object) => boolean; 38 | then: ListSchema; 39 | else: ListSchema; 40 | }; 41 | }; 42 | 43 | /** 44 | * Defines the structure and behavior of a loop element in a multi-step form. 45 | */ 46 | export type LoopSchema = { 47 | loop: { 48 | while: (inputs: object) => boolean; 49 | do: ListSchema; 50 | }; 51 | }; 52 | 53 | /** 54 | * Defines the structure and behavior of a switch element in a multi-step form. 55 | */ 56 | export type SwitchSchema = { 57 | switch: { 58 | branches: { 59 | case: (inputs: object) => boolean; 60 | then: ListSchema; 61 | }[]; 62 | default: ListSchema; 63 | }; 64 | }; 65 | 66 | /** 67 | * Defines the structure and behavior of a form element in a multi-step form. 68 | */ 69 | export type FormSchema = { 70 | form: { 71 | values: (inputs: object) => Record; 72 | render: (args: { 73 | inputs: object; 74 | values: object; 75 | params: object; 76 | onNext: OnNext; 77 | onBack: OnBack; 78 | getState: GetState; 79 | setState: SetState; 80 | }) => T; 81 | }; 82 | }; 83 | 84 | /** 85 | * Defines the structure and behavior of a yield element in a multi-step form. 86 | */ 87 | export type YieldSchema = { 88 | yield: { 89 | next: (inputs: object) => unknown[]; 90 | back: (inputs: object) => unknown[]; 91 | }; 92 | }; 93 | 94 | /** 95 | * Defines the structure and behavior of a return element in a multi-step form. 96 | */ 97 | export type ReturnSchema = { 98 | return: (inputs: object) => unknown; 99 | }; 100 | 101 | /** 102 | * Defines the structure and behavior of a variables element in a multi-step form. 103 | */ 104 | export type VariablesSchema = { 105 | variables: (inputs: object) => object; 106 | }; 107 | -------------------------------------------------------------------------------- /packages/system/src/utils/state.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | 3 | import type { Schema } from "src/types/schema/typed"; 4 | import type { Form, Cond, Loop, Variables } from "src/types/utils"; 5 | import type { State } from "src/types/state/state"; 6 | 7 | import { getState } from "./state"; 8 | 9 | describe("getState", () => { 10 | it("returns the current state of the multi-step form after updating the values of the current form.", () => { 11 | type Values = [ 12 | Variables, 13 | Cond<{ then: [Loop<[Form<{ a: number; b: number }>]>]; else: [] }> 14 | ]; 15 | const schema: Schema = [ 16 | { variables: () => ({}) }, 17 | { 18 | cond: { 19 | if: () => true, 20 | then: [ 21 | { 22 | loop: { 23 | while: () => true, 24 | do: [ 25 | { 26 | form: { 27 | values: () => ({ 28 | a: [0, []], 29 | b: [0, []], 30 | }), 31 | render: () => ({}), 32 | }, 33 | }, 34 | ], 35 | }, 36 | }, 37 | ], 38 | else: [], 39 | }, 40 | }, 41 | ]; 42 | const current: State = { 43 | points: [ 44 | { 45 | path: [ 46 | { type: "list", slot: 1 }, 47 | { type: "cond", path: "then", slot: 0 }, 48 | { type: "loop", slot: 0 }, 49 | ], 50 | values: {}, 51 | }, 52 | ], 53 | inputs: { type: "list", list: {} }, 54 | }; 55 | const state: State = getState( 56 | current, 57 | schema, 58 | { a: 1, b: 2 } 59 | ); 60 | const expected: State = { 61 | points: [ 62 | { 63 | path: [ 64 | { type: "list", slot: 1 }, 65 | { type: "cond", path: "then", slot: 0 }, 66 | { type: "loop", slot: 0 }, 67 | ], 68 | values: {}, 69 | }, 70 | ], 71 | inputs: { 72 | type: "list", 73 | list: { 74 | 1: { 75 | type: "cond", 76 | then: { 77 | 0: { 78 | type: "loop", 79 | list: { 80 | 0: { 81 | a: { 82 | data: { here: true, data: 1 }, 83 | keys: {}, 84 | }, 85 | b: { 86 | data: { here: true, data: 2 }, 87 | keys: {}, 88 | }, 89 | }, 90 | }, 91 | }, 92 | }, 93 | else: {}, 94 | }, 95 | }, 96 | }, 97 | }; 98 | expect(state).toEqual(expected); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /packages/system/src/utils/inputs/flow.cond.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | 3 | import type { CondInputs, FormInputs } from "src/types/state/inputs"; 4 | import type { Position } from "src/types/state/position"; 5 | 6 | import { getItem, setItem } from "./flow.cond"; 7 | 8 | describe("CondInputs", () => { 9 | describe("getItem", () => { 10 | it("returns the item at the given `then` position within the given `CondInputs` object", () => { 11 | const item: FormInputs = { 12 | a: { 13 | data: { here: true, data: 1 }, 14 | keys: {}, 15 | }, 16 | }; 17 | const flow: CondInputs = { 18 | type: "cond", 19 | then: { 20 | 1: item, 21 | }, 22 | else: {}, 23 | }; 24 | const position: Position = { type: "cond", path: "then", slot: 1 }; 25 | const result = getItem(flow, position); 26 | expect(result).toBe(item); 27 | }); 28 | 29 | it("returns the item at the given `else` position within the given `CondInputs` object", () => { 30 | const item: FormInputs = { 31 | a: { 32 | data: { here: true, data: 1 }, 33 | keys: {}, 34 | }, 35 | }; 36 | const flow: CondInputs = { 37 | type: "cond", 38 | then: {}, 39 | else: { 40 | 1: item, 41 | }, 42 | }; 43 | const position: Position = { type: "cond", path: "else", slot: 1 }; 44 | const result = getItem(flow, position); 45 | expect(result).toBe(item); 46 | }); 47 | 48 | it("returns null when trying to get an item from a position that doesn't exist in the given `CondInputs` object", () => { 49 | const item: FormInputs = { 50 | a: { 51 | data: { here: true, data: 1 }, 52 | keys: {}, 53 | }, 54 | }; 55 | const flow: CondInputs = { 56 | type: "cond", 57 | then: {}, 58 | else: { 59 | 1: item, 60 | }, 61 | }; 62 | const position: Position = { type: "cond", path: "then", slot: 1 }; 63 | const result = getItem(flow, position); 64 | expect(result).toBe(null); 65 | }); 66 | }); 67 | 68 | describe("setItem", () => { 69 | it("sets the item at the given `then` position within the given `CondInputs` object", () => { 70 | const flow: CondInputs = { 71 | type: "cond", 72 | then: {}, 73 | else: {}, 74 | }; 75 | const position: Position = { type: "cond", path: "then", slot: 1 }; 76 | const item: FormInputs = { 77 | a: { 78 | data: { here: true, data: 1 }, 79 | keys: {}, 80 | }, 81 | }; 82 | setItem(flow, position, item); 83 | const expected: CondInputs = { 84 | type: "cond", 85 | then: { 86 | 1: item, 87 | }, 88 | else: {}, 89 | }; 90 | expect(flow).toEqual(expected); 91 | }); 92 | 93 | it("sets the item at the given `else` position within the given `CondInputs` object", () => { 94 | const flow: CondInputs = { 95 | type: "cond", 96 | then: {}, 97 | else: {}, 98 | }; 99 | const position: Position = { type: "cond", path: "else", slot: 1 }; 100 | const item: FormInputs = { 101 | a: { 102 | data: { here: true, data: 1 }, 103 | keys: {}, 104 | }, 105 | }; 106 | setItem(flow, position, item); 107 | const expected: CondInputs = { 108 | type: "cond", 109 | then: {}, 110 | else: { 111 | 1: item, 112 | }, 113 | }; 114 | expect(flow).toEqual(expected); 115 | }); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /apps/react/src/schemas/flow.loop.tsx: -------------------------------------------------------------------------------- 1 | import type { Schema, Loop, Form, Return, Variables } from "@formity/react"; 2 | 3 | import { zodResolver } from "@hookform/resolvers/zod"; 4 | import { z } from "zod"; 5 | 6 | import { Step, Layout, Select, NextButton, BackButton } from "../components"; 7 | 8 | import { MultiStep } from "../multi-step"; 9 | 10 | export type LoopValues = [ 11 | Variables<{ languages: { value: string; question: string }[] }>, 12 | Variables<{ 13 | i: number; 14 | languagesRatings: { name: string; rating: string }[]; 15 | }>, 16 | Loop< 17 | [ 18 | Variables<{ language: { value: string; question: string } }>, 19 | Form<{ rating: string }>, 20 | Variables<{ 21 | i: number; 22 | languagesRatings: { name: string; rating: string }[]; 23 | }> 24 | ] 25 | >, 26 | Return<{ languagesRatings: { name: string; rating: string }[] }> 27 | ]; 28 | 29 | export const loopSchema: Schema = [ 30 | { 31 | variables: () => ({ 32 | languages: [ 33 | { 34 | value: "javascript", 35 | question: "What rating would you give to the JavaScript language?", 36 | }, 37 | { 38 | value: "python", 39 | question: "What rating would you give to the Python language?", 40 | }, 41 | { 42 | value: "go", 43 | question: "What rating would you give to the Go language?", 44 | }, 45 | ], 46 | }), 47 | }, 48 | { 49 | variables: () => ({ 50 | i: 0, 51 | languagesRatings: [], 52 | }), 53 | }, 54 | { 55 | loop: { 56 | while: ({ i, languages }) => i < languages.length, 57 | do: [ 58 | { 59 | variables: ({ i, languages }) => ({ 60 | language: languages[i], 61 | }), 62 | }, 63 | { 64 | form: { 65 | values: ({ language }) => ({ 66 | rating: ["love-it", [language.value]], 67 | }), 68 | render: ({ inputs, values, ...rest }) => ( 69 | 70 | 78 | , 93 | ]} 94 | button={Next} 95 | back={inputs.i > 0 ? : undefined} 96 | /> 97 | 98 | 99 | ), 100 | }, 101 | }, 102 | { 103 | variables: ({ i, languagesRatings, language, rating }) => ({ 104 | i: i + 1, 105 | languagesRatings: [ 106 | ...languagesRatings, 107 | { name: language.value, rating }, 108 | ], 109 | }), 110 | }, 111 | ], 112 | }, 113 | }, 114 | { 115 | return: ({ languagesRatings }) => ({ 116 | languagesRatings, 117 | }), 118 | }, 119 | ]; 120 | -------------------------------------------------------------------------------- /packages/system/src/utils/form.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | 3 | import type { Schema } from "src/types/schema/typed"; 4 | import type { Form, Cond, Loop, Variables } from "src/types/utils"; 5 | import type { State } from "src/types/state/state"; 6 | 7 | import { getForm } from "./form"; 8 | 9 | describe("getForm", () => { 10 | it("renders the form from the schema that is in the current position", () => { 11 | type Values = [ 12 | Variables, 13 | Cond<{ then: [Loop<[Form]>]; else: [] }> 14 | ]; 15 | const schema: Schema = [ 16 | { variables: () => ({}) }, 17 | { 18 | cond: { 19 | if: () => true, 20 | then: [ 21 | { 22 | loop: { 23 | while: () => true, 24 | do: [ 25 | { 26 | form: { 27 | values: () => ({}), 28 | render: () => ({ 29 | hello: "world", 30 | }), 31 | }, 32 | }, 33 | ], 34 | }, 35 | }, 36 | ], 37 | else: [], 38 | }, 39 | }, 40 | ]; 41 | const state: State = { 42 | points: [ 43 | { 44 | path: [ 45 | { type: "list", slot: 1 }, 46 | { type: "cond", path: "then", slot: 0 }, 47 | { type: "loop", slot: 0 }, 48 | ], 49 | values: {}, 50 | }, 51 | ], 52 | inputs: { type: "list", list: {} }, 53 | }; 54 | const form = getForm( 55 | state, 56 | schema, 57 | {}, 58 | () => {}, 59 | () => {}, 60 | () => state, 61 | () => {} 62 | ); 63 | expect(form).toEqual({ hello: "world" }); 64 | }); 65 | 66 | it("uses the values from the form defined in the schema", () => { 67 | type Values = [Form<{ name: string }>]; 68 | const schema: Schema = [ 69 | { 70 | form: { 71 | values: () => ({ 72 | name: ["John", []], 73 | }), 74 | render: ({ values }) => ({ 75 | defaultValues: { 76 | name: values.name, 77 | }, 78 | }), 79 | }, 80 | }, 81 | ]; 82 | const state: State = { 83 | points: [ 84 | { 85 | path: [{ type: "list", slot: 0 }], 86 | values: {}, 87 | }, 88 | ], 89 | inputs: { type: "list", list: {} }, 90 | }; 91 | const form = getForm( 92 | state, 93 | schema, 94 | {}, 95 | () => {}, 96 | () => {}, 97 | () => state, 98 | () => {} 99 | ); 100 | expect(form).toEqual({ 101 | defaultValues: { 102 | name: "John", 103 | }, 104 | }); 105 | }); 106 | 107 | it("uses the params that have been provided", () => { 108 | type Values = [Form]; 109 | type Params = { hello: string }; 110 | const schema: Schema = [ 111 | { 112 | form: { 113 | values: () => ({}), 114 | render: ({ params }) => ({ 115 | hello: params.hello, 116 | }), 117 | }, 118 | }, 119 | ]; 120 | const state: State = { 121 | points: [ 122 | { 123 | path: [{ type: "list", slot: 0 }], 124 | values: {}, 125 | }, 126 | ], 127 | inputs: { type: "list", list: {} }, 128 | }; 129 | const form = getForm( 130 | state, 131 | schema, 132 | { hello: "world" }, 133 | () => {}, 134 | () => {}, 135 | () => state, 136 | () => {} 137 | ); 138 | expect(form).toEqual({ 139 | hello: "world", 140 | }); 141 | }); 142 | }); 143 | -------------------------------------------------------------------------------- /packages/system/src/utils/schema/flow.ts: -------------------------------------------------------------------------------- 1 | import type { ItemSchema, FlowSchema } from "../../types/schema/model"; 2 | import type { Position } from "../../types/state/position"; 3 | 4 | import * as ListSchemaUtils from "./flow.list"; 5 | import * as CondSchemaUtils from "./flow.cond"; 6 | import * as LoopSchemaUtils from "./flow.loop"; 7 | import * as SwitchSchemaUtils from "./flow.switch"; 8 | 9 | /** 10 | * Type guard for `FlowSchema` objects. 11 | * 12 | * @param schema An `ItemSchema` object. 13 | * @returns A boolean indicating whether the `schema` is a `FlowSchema` object. 14 | */ 15 | export function is(schema: ItemSchema): schema is FlowSchema { 16 | return ( 17 | ListSchemaUtils.is(schema) || 18 | CondSchemaUtils.is(schema) || 19 | LoopSchemaUtils.is(schema) || 20 | SwitchSchemaUtils.is(schema) 21 | ); 22 | } 23 | 24 | /** 25 | * Returns the initial position for the given `FlowSchema` object if there is an initial position, otherwise it returns `null`. 26 | * 27 | * @param schema A `FlowSchema` object. 28 | * @param values An object containing the generated values within the multi-step form. 29 | * @returns A `Position` object representing the initial position, or `null` if there is no initial position. 30 | */ 31 | export function into(schema: FlowSchema, values: object): Position | null { 32 | if (ListSchemaUtils.is(schema)) { 33 | return ListSchemaUtils.into(schema); 34 | } 35 | if (CondSchemaUtils.is(schema)) { 36 | return CondSchemaUtils.into(schema, values); 37 | } 38 | if (LoopSchemaUtils.is(schema)) { 39 | return LoopSchemaUtils.into(schema, values); 40 | } 41 | if (SwitchSchemaUtils.is(schema)) { 42 | return SwitchSchemaUtils.into(schema, values); 43 | } 44 | throw new Error("Invalid schema"); 45 | } 46 | 47 | /** 48 | * Returns the next position for the given `FlowSchema` object if there is a next position, otherwise it returns `null`. 49 | * 50 | * @param schema A `FlowSchema` object. 51 | * @param position A `Position` object representing the current position. 52 | * @param values An object containing the generated values within the multi-step form. 53 | * @returns A `Position` object representing the next position, or `null` if there is no next position. 54 | */ 55 | export function next( 56 | schema: FlowSchema, 57 | position: Position, 58 | values: object 59 | ): Position | null { 60 | if (ListSchemaUtils.is(schema)) { 61 | return ListSchemaUtils.next(schema, position); 62 | } 63 | if (CondSchemaUtils.is(schema)) { 64 | return CondSchemaUtils.next(schema, position); 65 | } 66 | if (LoopSchemaUtils.is(schema)) { 67 | return LoopSchemaUtils.next(schema, position, values); 68 | } 69 | if (SwitchSchemaUtils.is(schema)) { 70 | return SwitchSchemaUtils.next(schema, position); 71 | } 72 | throw new Error("Invalid schema"); 73 | } 74 | 75 | /** 76 | * Returns the `ItemSchema` object at the given position within the given `FlowSchema` object. 77 | * 78 | * @param schema The `FlowSchema` object. 79 | * @param position The position within the `FlowSchema` object. 80 | * @returns The `ItemSchema` object at the given position within the `FlowSchema` object. 81 | */ 82 | export function at(schema: FlowSchema, position: Position): ItemSchema { 83 | if (ListSchemaUtils.is(schema)) { 84 | return ListSchemaUtils.at(schema, position); 85 | } 86 | if (CondSchemaUtils.is(schema)) { 87 | return CondSchemaUtils.at(schema, position); 88 | } 89 | if (LoopSchemaUtils.is(schema)) { 90 | return LoopSchemaUtils.at(schema, position); 91 | } 92 | if (SwitchSchemaUtils.is(schema)) { 93 | return SwitchSchemaUtils.at(schema, position); 94 | } 95 | throw new Error("Invalid schema"); 96 | } 97 | 98 | /** 99 | * Returns the `ItemSchema` object at the given path within the given `FlowSchema` object. 100 | * 101 | * @param schema The `FlowSchema` object. 102 | * @param path The path within the `FlowSchema` object. 103 | * @returns The `ItemSchema` object at the given path within the `FlowSchema` object. 104 | */ 105 | export function find(schema: FlowSchema, path: Position[]) { 106 | let current: ItemSchema = schema; 107 | for (const position of path) { 108 | const flow = current as FlowSchema; 109 | current = at(flow, position); 110 | } 111 | return current; 112 | } 113 | -------------------------------------------------------------------------------- /packages/system/src/utils/inputs/flow.switch.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | 3 | import type { SwitchInputs, FormInputs } from "src/types/state/inputs"; 4 | import type { Position } from "src/types/state/position"; 5 | 6 | import { getItem, setItem } from "./flow.switch"; 7 | 8 | describe("SwitchInputs", () => { 9 | describe("getItem", () => { 10 | it("returns the item at the given branch position within the given `SwitchInputs` object", () => { 11 | const item: FormInputs = { 12 | a: { 13 | data: { here: true, data: 1 }, 14 | keys: {}, 15 | }, 16 | }; 17 | const flow: SwitchInputs = { 18 | type: "switch", 19 | branches: { 20 | 1: { 21 | 1: item, 22 | }, 23 | }, 24 | default: {}, 25 | }; 26 | const position: Position = { type: "switch", branch: 1, slot: 1 }; 27 | const result = getItem(flow, position); 28 | expect(result).toBe(item); 29 | }); 30 | 31 | it("returns the item at the given default branch position within the given `SwitchInputs` object", () => { 32 | const item: FormInputs = { 33 | a: { 34 | data: { here: true, data: 1 }, 35 | keys: {}, 36 | }, 37 | }; 38 | const flow: SwitchInputs = { 39 | type: "switch", 40 | branches: {}, 41 | default: { 42 | 1: item, 43 | }, 44 | }; 45 | const position: Position = { type: "switch", branch: -1, slot: 1 }; 46 | const result = getItem(flow, position); 47 | expect(result).toBe(item); 48 | }); 49 | 50 | it("returns null when trying to get an item from a branch position that doesn't exist in the given `SwitchInputs` object", () => { 51 | const item: FormInputs = { 52 | a: { 53 | data: { here: true, data: 1 }, 54 | keys: {}, 55 | }, 56 | }; 57 | const flow: SwitchInputs = { 58 | type: "switch", 59 | branches: { 60 | 1: { 61 | 1: item, 62 | }, 63 | }, 64 | default: {}, 65 | }; 66 | const position: Position = { type: "switch", branch: 1, slot: 0 }; 67 | const result = getItem(flow, position); 68 | expect(result).toBe(null); 69 | }); 70 | 71 | it("returns null when trying to get an item from a default branch position that doesn't exist in the given `SwitchInputs` object", () => { 72 | const item: FormInputs = { 73 | a: { 74 | data: { here: true, data: 1 }, 75 | keys: {}, 76 | }, 77 | }; 78 | const flow: SwitchInputs = { 79 | type: "switch", 80 | branches: {}, 81 | default: { 82 | 1: item, 83 | }, 84 | }; 85 | const position: Position = { type: "switch", branch: -1, slot: 0 }; 86 | const result = getItem(flow, position); 87 | expect(result).toBe(null); 88 | }); 89 | }); 90 | 91 | describe("setItem", () => { 92 | it("sets the item at the given branch position within the given `SwitchInputs` object", () => { 93 | const flow: SwitchInputs = { 94 | type: "switch", 95 | branches: {}, 96 | default: {}, 97 | }; 98 | const position: Position = { type: "switch", branch: 1, slot: 1 }; 99 | const item: FormInputs = { 100 | a: { 101 | data: { here: true, data: 1 }, 102 | keys: {}, 103 | }, 104 | }; 105 | setItem(flow, position, item); 106 | const expected: SwitchInputs = { 107 | type: "switch", 108 | branches: { 109 | 1: { 110 | 1: item, 111 | }, 112 | }, 113 | default: {}, 114 | }; 115 | expect(flow).toEqual(expected); 116 | }); 117 | 118 | it("sets the item at the given default branch position within the given `SwitchInputs` object", () => { 119 | const flow: SwitchInputs = { 120 | type: "switch", 121 | branches: {}, 122 | default: {}, 123 | }; 124 | const position: Position = { type: "switch", branch: -1, slot: 1 }; 125 | const item: FormInputs = { 126 | a: { 127 | data: { here: true, data: 1 }, 128 | keys: {}, 129 | }, 130 | }; 131 | setItem(flow, position, item); 132 | const expected: SwitchInputs = { 133 | type: "switch", 134 | branches: {}, 135 | default: { 136 | 1: item, 137 | }, 138 | }; 139 | expect(flow).toEqual(expected); 140 | }); 141 | }); 142 | }); 143 | -------------------------------------------------------------------------------- /packages/system/src/utils/schema/flow.loop.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | 3 | import type { LoopSchema, ReturnSchema } from "../../types/schema/model"; 4 | import type { Position } from "src/types/state/position"; 5 | 6 | import { into, next, at } from "./flow.loop"; 7 | 8 | describe("LoopSchema", () => { 9 | describe("into", () => { 10 | it("navigates into the `LoopSchema` object", () => { 11 | const schema: LoopSchema = { 12 | loop: { 13 | while: () => true, 14 | do: [ 15 | { 16 | form: { 17 | values: () => ({}), 18 | render: () => ({}), 19 | }, 20 | }, 21 | ], 22 | }, 23 | }; 24 | const position = into(schema, {}); 25 | expect(position).toEqual({ type: "loop", slot: 0 }); 26 | }); 27 | 28 | it("doesn't navigate into the `LoopSchema` object if the condition is false", () => { 29 | const schema: LoopSchema = { 30 | loop: { 31 | while: () => false, 32 | do: [ 33 | { 34 | form: { 35 | values: () => ({}), 36 | render: () => ({}), 37 | }, 38 | }, 39 | ], 40 | }, 41 | }; 42 | const position = into(schema, {}); 43 | expect(position).toEqual(null); 44 | }); 45 | 46 | it("doesn't navigate into the `LoopSchema` object if there are no items", () => { 47 | const schema: LoopSchema = { 48 | loop: { 49 | while: () => true, 50 | do: [], 51 | }, 52 | }; 53 | const position = into(schema, {}); 54 | expect(position).toEqual(null); 55 | }); 56 | }); 57 | 58 | describe("next", () => { 59 | it("navigates to the next item in the `LoopSchema` object", () => { 60 | const schema: LoopSchema = { 61 | loop: { 62 | while: () => true, 63 | do: [ 64 | { 65 | form: { 66 | values: () => ({}), 67 | render: () => ({}), 68 | }, 69 | }, 70 | { 71 | form: { 72 | values: () => ({}), 73 | render: () => ({}), 74 | }, 75 | }, 76 | ], 77 | }, 78 | }; 79 | const current: Position = { type: "loop", slot: 0 }; 80 | const position = next(schema, current, {}); 81 | expect(position).toEqual({ type: "loop", slot: 1 }); 82 | }); 83 | 84 | it("navigates to the first item in the `LoopSchema` object", () => { 85 | const schema: LoopSchema = { 86 | loop: { 87 | while: () => true, 88 | do: [ 89 | { 90 | form: { 91 | values: () => ({}), 92 | render: () => ({}), 93 | }, 94 | }, 95 | { 96 | form: { 97 | values: () => ({}), 98 | render: () => ({}), 99 | }, 100 | }, 101 | ], 102 | }, 103 | }; 104 | const current: Position = { type: "loop", slot: 1 }; 105 | const position = next(schema, current, {}); 106 | expect(position).toEqual({ type: "loop", slot: 0 }); 107 | }); 108 | 109 | it("doesn't navigate to the next item in the `LoopSchema` object", () => { 110 | const schema: LoopSchema = { 111 | loop: { 112 | while: () => false, 113 | do: [ 114 | { 115 | form: { 116 | values: () => ({}), 117 | render: () => ({}), 118 | }, 119 | }, 120 | { 121 | form: { 122 | values: () => ({}), 123 | render: () => ({}), 124 | }, 125 | }, 126 | ], 127 | }, 128 | }; 129 | const current: Position = { type: "loop", slot: 1 }; 130 | const position = next(schema, current, {}); 131 | expect(position).toEqual(null); 132 | }); 133 | }); 134 | }); 135 | 136 | describe("at", () => { 137 | it("retrieves the item at the specified position in the `LoopSchema` object", () => { 138 | const item: ReturnSchema = { return: () => ({}) }; 139 | const schema: LoopSchema = { 140 | loop: { 141 | while: () => true, 142 | do: [{ variables: () => ({}) }, item], 143 | }, 144 | }; 145 | const position: Position = { type: "loop", slot: 1 }; 146 | const result = at(schema, position); 147 | expect(result).toBe(item); 148 | }); 149 | }); 150 | -------------------------------------------------------------------------------- /packages/system/src/utils/schema/flow.cond.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | 3 | import type { CondSchema, ReturnSchema } from "../../types/schema/model"; 4 | import type { Position } from "src/types/state/position"; 5 | 6 | import { into, next, at } from "./flow.cond"; 7 | 8 | describe("CondSchema", () => { 9 | describe("into", () => { 10 | it("navigates into the `then` path of the `CondSchema` object", () => { 11 | const schema: CondSchema = { 12 | cond: { 13 | if: () => true, 14 | then: [ 15 | { 16 | form: { 17 | values: () => ({}), 18 | render: () => ({}), 19 | }, 20 | }, 21 | ], 22 | else: [], 23 | }, 24 | }; 25 | const position = into(schema, {}); 26 | expect(position).toEqual({ type: "cond", path: "then", slot: 0 }); 27 | }); 28 | 29 | it("navigates into the `else` path of the `CondSchema` object", () => { 30 | const schema: CondSchema = { 31 | cond: { 32 | if: () => false, 33 | then: [], 34 | else: [ 35 | { 36 | form: { 37 | values: () => ({}), 38 | render: () => ({}), 39 | }, 40 | }, 41 | ], 42 | }, 43 | }; 44 | const position = into(schema, {}); 45 | expect(position).toEqual({ type: "cond", path: "else", slot: 0 }); 46 | }); 47 | 48 | it("doesn't navigate into the `CondSchema` object", () => { 49 | const schema: CondSchema = { 50 | cond: { 51 | if: () => true, 52 | then: [], 53 | else: [ 54 | { 55 | form: { 56 | values: () => ({}), 57 | render: () => ({}), 58 | }, 59 | }, 60 | ], 61 | }, 62 | }; 63 | const position = into(schema, {}); 64 | expect(position).toEqual(null); 65 | }); 66 | }); 67 | 68 | describe("next", () => { 69 | it("navigates to the next item in the `then` path of the `CondSchema` object", () => { 70 | const schema: CondSchema = { 71 | cond: { 72 | if: () => true, 73 | then: [ 74 | { 75 | form: { 76 | values: () => ({}), 77 | render: () => ({}), 78 | }, 79 | }, 80 | { 81 | form: { 82 | values: () => ({}), 83 | render: () => ({}), 84 | }, 85 | }, 86 | ], 87 | else: [], 88 | }, 89 | }; 90 | const current: Position = { type: "cond", path: "then", slot: 0 }; 91 | const position = next(schema, current); 92 | expect(position).toEqual({ type: "cond", path: "then", slot: 1 }); 93 | }); 94 | 95 | it("navigates to the next item in the `else` path of the `CondSchema` object", () => { 96 | const schema: CondSchema = { 97 | cond: { 98 | if: () => true, 99 | then: [], 100 | else: [ 101 | { 102 | form: { 103 | values: () => ({}), 104 | render: () => ({}), 105 | }, 106 | }, 107 | { 108 | form: { 109 | values: () => ({}), 110 | render: () => ({}), 111 | }, 112 | }, 113 | ], 114 | }, 115 | }; 116 | const current: Position = { type: "cond", path: "else", slot: 0 }; 117 | const position = next(schema, current); 118 | expect(position).toEqual({ type: "cond", path: "else", slot: 1 }); 119 | }); 120 | 121 | it("doesn't navigate to the next item in the `CondSchema` object", () => { 122 | const schema: CondSchema = { 123 | cond: { 124 | if: () => true, 125 | then: [ 126 | { 127 | form: { 128 | values: () => ({}), 129 | render: () => ({}), 130 | }, 131 | }, 132 | ], 133 | else: [ 134 | { 135 | form: { 136 | values: () => ({}), 137 | render: () => ({}), 138 | }, 139 | }, 140 | ], 141 | }, 142 | }; 143 | const current: Position = { type: "cond", path: "then", slot: 0 }; 144 | const position = next(schema, current); 145 | expect(position).toEqual(null); 146 | }); 147 | }); 148 | 149 | describe("at", () => { 150 | it("retrieves the item at the specified position in the `then` path of the `CondSchema` object", () => { 151 | const item: ReturnSchema = { return: () => ({}) }; 152 | const schema: CondSchema = { 153 | cond: { 154 | if: () => true, 155 | then: [{ variables: () => ({}) }, item], 156 | else: [], 157 | }, 158 | }; 159 | const position: Position = { type: "cond", path: "then", slot: 1 }; 160 | const result = at(schema, position); 161 | expect(result).toBe(item); 162 | }); 163 | 164 | it("retrieves the item at the specified position in the `else` path of the `CondSchema` object", () => { 165 | const item: ReturnSchema = { return: () => ({}) }; 166 | const schema: CondSchema = { 167 | cond: { 168 | if: () => false, 169 | then: [], 170 | else: [{ variables: () => ({}) }, item], 171 | }, 172 | }; 173 | const position: Position = { type: "cond", path: "else", slot: 1 }; 174 | const result = at(schema, position); 175 | expect(result).toBe(item); 176 | }); 177 | }); 178 | }); 179 | -------------------------------------------------------------------------------- /apps/react/src/schemas/flow.cond.tsx: -------------------------------------------------------------------------------- 1 | import type { Schema, Cond, Form, Return } from "@formity/react"; 2 | 3 | import { zodResolver } from "@hookform/resolvers/zod"; 4 | import { z } from "zod"; 5 | 6 | import { 7 | Step, 8 | Layout, 9 | MultiSelect, 10 | Listbox, 11 | YesNo, 12 | NextButton, 13 | BackButton, 14 | } from "../components"; 15 | 16 | import { MultiStep } from "../multi-step"; 17 | 18 | export type CondValues = [ 19 | Form<{ softwareDeveloper: boolean }>, 20 | Cond<{ 21 | then: [ 22 | Form<{ languages: string[] }>, 23 | Return<{ 24 | softwareDeveloper: true; 25 | languages: string[]; 26 | }> 27 | ]; 28 | else: [ 29 | Form<{ interested: string }>, 30 | Return<{ 31 | softwareDeveloper: false; 32 | interested: string; 33 | }> 34 | ]; 35 | }> 36 | ]; 37 | 38 | export const condSchema: Schema = [ 39 | { 40 | form: { 41 | values: () => ({ 42 | softwareDeveloper: [true, []], 43 | }), 44 | render: ({ values, ...rest }) => ( 45 | 46 | 54 | , 63 | ]} 64 | button={Next} 65 | /> 66 | 67 | 68 | ), 69 | }, 70 | }, 71 | { 72 | cond: { 73 | if: ({ softwareDeveloper }) => softwareDeveloper, 74 | then: [ 75 | { 76 | form: { 77 | values: () => ({ 78 | languages: [[], []], 79 | }), 80 | render: ({ values, ...rest }) => ( 81 | 82 | 90 | , 105 | ]} 106 | button={Next} 107 | back={} 108 | /> 109 | 110 | 111 | ), 112 | }, 113 | }, 114 | { 115 | return: ({ languages }) => ({ 116 | softwareDeveloper: true, 117 | languages, 118 | }), 119 | }, 120 | ], 121 | else: [ 122 | { 123 | form: { 124 | values: () => ({ 125 | interested: ["maybe", []], 126 | }), 127 | render: ({ values, ...rest }) => ( 128 | 129 | 137 | , 160 | ]} 161 | button={Next} 162 | back={} 163 | /> 164 | 165 | 166 | ), 167 | }, 168 | }, 169 | { 170 | return: ({ interested }) => ({ 171 | softwareDeveloper: false, 172 | interested, 173 | }), 174 | }, 175 | ], 176 | }, 177 | }, 178 | ]; 179 | -------------------------------------------------------------------------------- /packages/system/src/utils/inputs/flow.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | 3 | import type { FlowInputs } from "src/types/state/inputs"; 4 | import type { Position } from "src/types/state/position"; 5 | 6 | import { get, set } from "./flow"; 7 | 8 | describe("FlowInputs", () => { 9 | describe("get", () => { 10 | it("returns the value that is in the given `FlowInputs` object", () => { 11 | const flow: FlowInputs = { 12 | type: "list", 13 | list: { 14 | 1: { 15 | type: "cond", 16 | then: {}, 17 | else: { 18 | 0: { 19 | type: "loop", 20 | list: { 21 | 0: { 22 | a: { 23 | data: { here: false }, 24 | keys: { 25 | x: { 26 | data: { here: false }, 27 | keys: { 28 | y: { 29 | data: { here: true, data: 1 }, 30 | keys: {}, 31 | }, 32 | }, 33 | }, 34 | }, 35 | }, 36 | }, 37 | }, 38 | }, 39 | }, 40 | }, 41 | }, 42 | }; 43 | const path: Position[] = [ 44 | { type: "list", slot: 1 }, 45 | { type: "cond", path: "else", slot: 0 }, 46 | { type: "loop", slot: 0 }, 47 | ]; 48 | const name: string = "a"; 49 | const keys: PropertyKey[] = ["x", "y"]; 50 | const defaultValue: unknown = 2; 51 | const result = get(flow, path, name, keys, defaultValue); 52 | expect(result).toEqual(1); 53 | }); 54 | 55 | it("returns the default value if the path is not encountered in the given `FlowInputs` object", () => { 56 | const flow: FlowInputs = { 57 | type: "list", 58 | list: { 59 | 1: { 60 | type: "cond", 61 | then: {}, 62 | else: { 63 | 0: { 64 | type: "loop", 65 | list: { 66 | 0: { 67 | a: { 68 | data: { here: false }, 69 | keys: { 70 | x: { 71 | data: { here: false }, 72 | keys: { 73 | y: { 74 | data: { here: true, data: 1 }, 75 | keys: {}, 76 | }, 77 | }, 78 | }, 79 | }, 80 | }, 81 | }, 82 | }, 83 | }, 84 | }, 85 | }, 86 | }, 87 | }; 88 | const path: Position[] = [ 89 | { type: "list", slot: 1 }, 90 | { type: "cond", path: "then", slot: 0 }, 91 | { type: "loop", slot: 0 }, 92 | ]; 93 | const name: string = "a"; 94 | const keys: PropertyKey[] = ["x", "y"]; 95 | const defaultValue: unknown = 2; 96 | const result = get(flow, path, name, keys, defaultValue); 97 | expect(result).toEqual(2); 98 | }); 99 | 100 | it("returns the default value if the keys are not encountered in the given `FlowInputs` object", () => { 101 | const flow: FlowInputs = { 102 | type: "list", 103 | list: { 104 | 1: { 105 | type: "cond", 106 | then: {}, 107 | else: { 108 | 0: { 109 | type: "loop", 110 | list: { 111 | 0: { 112 | a: { 113 | data: { here: false }, 114 | keys: { 115 | x: { 116 | data: { here: false }, 117 | keys: { 118 | y: { 119 | data: { here: true, data: 1 }, 120 | keys: {}, 121 | }, 122 | }, 123 | }, 124 | }, 125 | }, 126 | }, 127 | }, 128 | }, 129 | }, 130 | }, 131 | }, 132 | }; 133 | const path: Position[] = [ 134 | { type: "list", slot: 1 }, 135 | { type: "cond", path: "else", slot: 0 }, 136 | { type: "loop", slot: 0 }, 137 | ]; 138 | const name: string = "a"; 139 | const keys: PropertyKey[] = ["x", "z"]; 140 | const defaultValue: unknown = 2; 141 | const result = get(flow, path, name, keys, defaultValue); 142 | expect(result).toEqual(2); 143 | }); 144 | }); 145 | 146 | describe("set", () => { 147 | it("sets the value in the given `FlowInputs` object", () => { 148 | const flow: FlowInputs = { type: "list", list: {} }; 149 | const path: Position[] = [ 150 | { type: "list", slot: 1 }, 151 | { type: "cond", path: "else", slot: 0 }, 152 | { type: "loop", slot: 0 }, 153 | ]; 154 | const name: string = "a"; 155 | const keys: PropertyKey[] = ["x", "y"]; 156 | const data: unknown = 1; 157 | const result = set(flow, path, name, keys, data); 158 | const expected: FlowInputs = { 159 | type: "list", 160 | list: { 161 | 1: { 162 | type: "cond", 163 | then: {}, 164 | else: { 165 | 0: { 166 | type: "loop", 167 | list: { 168 | 0: { 169 | a: { 170 | data: { here: false }, 171 | keys: { 172 | x: { 173 | data: { here: false }, 174 | keys: { 175 | y: { 176 | data: { here: true, data: 1 }, 177 | keys: {}, 178 | }, 179 | }, 180 | }, 181 | }, 182 | }, 183 | }, 184 | }, 185 | }, 186 | }, 187 | }, 188 | }, 189 | }; 190 | expect(result).toEqual(expected); 191 | }); 192 | }); 193 | }); 194 | -------------------------------------------------------------------------------- /packages/system/src/utils/inputs/flow.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ItemInputs, 3 | FlowInputs, 4 | FormInputs, 5 | } from "../../types/state/inputs"; 6 | 7 | import type { Position } from "../../types/state/position"; 8 | 9 | import * as ListInputsUtils from "./flow.list"; 10 | import * as CondInputsUtils from "./flow.cond"; 11 | import * as LoopInputsUtils from "./flow.loop"; 12 | import * as SwitchInputsUtils from "./flow.switch"; 13 | import * as FormInputsUtils from "./form"; 14 | 15 | /** 16 | * Returns the value that is in the given `FlowInputs` object using the following parameters: 17 | * 18 | * - `path`: The path within the `FlowInputs` object that contains a `FormInputs` object. 19 | * - `name`: The name of the value within the `FormInputs` object. 20 | * - `keys`: The list of keys that is used to access the value within the `FormInputs` object. 21 | * - `defaultValue`: The default value to return if the value is not found. 22 | * 23 | * @param flow The `FlowInputs` object. 24 | * @param path The path within the `FlowInputs` object that contains a `FormInputs` object. 25 | * @param name The name of the value within the `FormInputs` object. 26 | * @param keys The list of keys that is used to access the value within the `FormInputs` object. 27 | * @param defaultValue The default value to return if the value is not found. 28 | * @returns The value that is in the given `FlowInputs` object or the default value if the value is not found. 29 | */ 30 | export function get( 31 | flow: FlowInputs, 32 | path: Position[], 33 | name: string, 34 | keys: PropertyKey[], 35 | defaultValue: unknown 36 | ): unknown { 37 | let current: ItemInputs = flow; 38 | for (const position of path) { 39 | const flow = current as FlowInputs; 40 | const item = getItem(flow, position); 41 | if (item) current = item; 42 | else return defaultValue; 43 | } 44 | const form = current as FormInputs; 45 | return FormInputsUtils.get(form, name, keys, defaultValue); 46 | } 47 | 48 | /** 49 | * Sets the value in the given `FlowInputs` object using the following parameters: 50 | * 51 | * - `path`: The path within the `FlowInputs` object that contains a `FormInputs` object. 52 | * - `name`: The name of the value within the `FormInputs` object. 53 | * - `keys`: The list of keys that is used to access the value within the `FormInputs` object. 54 | * - `value`: The value to set. 55 | * 56 | * @param flow The `FlowInputs` object. 57 | * @param path The path within the `FlowInputs` object that contains a `FormInputs` object. 58 | * @param name The name of the value within the `FormInputs` object. 59 | * @param keys The list of keys that is used to access the value within the `FormInputs` object. 60 | * @param data The value to set. 61 | * @returns The updated `FlowInputs` object. 62 | */ 63 | export function set( 64 | flow: FlowInputs, 65 | path: Position[], 66 | name: string, 67 | keys: PropertyKey[], 68 | data: unknown 69 | ): FlowInputs { 70 | const updated: FlowInputs = clone(flow); 71 | let current: FlowInputs = updated; 72 | for (let i = 0; i < path.length - 1; i++) { 73 | const position = path[i]; 74 | const item = getItem(current, position); 75 | if (item) { 76 | const next = item as FlowInputs; 77 | const cloned = clone(next); 78 | setItem(current, position, cloned); 79 | current = cloned; 80 | } else { 81 | const next = create(path[i + 1]); 82 | setItem(current, position, next); 83 | current = next; 84 | } 85 | } 86 | const position = path[path.length - 1]; 87 | const item = getItem(current, position); 88 | if (item) { 89 | const form = item as FormInputs; 90 | setItem(current, position, FormInputsUtils.set(form, name, keys, data)); 91 | } else { 92 | const form: FormInputs = { [name]: { data: { here: false }, keys: {} } }; 93 | setItem(current, position, FormInputsUtils.set(form, name, keys, data)); 94 | } 95 | return updated; 96 | } 97 | 98 | /** 99 | * Creates a `FlowInputs` object. 100 | * 101 | * @returns The created `FlowInputs` object. 102 | */ 103 | export function create(position: Position): FlowInputs { 104 | switch (position.type) { 105 | case "list": 106 | return ListInputsUtils.create(); 107 | case "cond": 108 | return CondInputsUtils.create(); 109 | case "loop": 110 | return LoopInputsUtils.create(); 111 | case "switch": 112 | return SwitchInputsUtils.create(); 113 | } 114 | } 115 | 116 | /** 117 | * Clones a `FlowInputs` object. 118 | * 119 | * @param flow A `FlowInputs` object. 120 | * @returns The cloned `FlowInputs` object. 121 | */ 122 | export function clone(flow: FlowInputs): FlowInputs { 123 | switch (flow.type) { 124 | case "list": 125 | return ListInputsUtils.clone(flow); 126 | case "cond": 127 | return CondInputsUtils.clone(flow); 128 | case "loop": 129 | return LoopInputsUtils.clone(flow); 130 | case "switch": 131 | return SwitchInputsUtils.clone(flow); 132 | } 133 | } 134 | 135 | /** 136 | * Returns the `ItemInputs` object at the given position within the given `FlowInputs` object, or `null` if there is no item at the given position. 137 | * 138 | * @param flow The `FlowInputs` object. 139 | * @param position The position within the `FlowInputs` object. 140 | * @returns The `ItemInputs` object at the given position within the `FlowInputs` object, or `null` if there is no item at the given position. 141 | */ 142 | export function getItem( 143 | flow: FlowInputs, 144 | position: Position 145 | ): ItemInputs | null { 146 | switch (flow.type) { 147 | case "list": 148 | return ListInputsUtils.getItem(flow, position); 149 | case "cond": 150 | return CondInputsUtils.getItem(flow, position); 151 | case "loop": 152 | return LoopInputsUtils.getItem(flow, position); 153 | case "switch": 154 | return SwitchInputsUtils.getItem(flow, position); 155 | } 156 | } 157 | 158 | /** 159 | * Sets the `ItemInputs` object at the given position within the given `FlowInputs` object. 160 | * 161 | * @param flow The `FlowInputs` object. 162 | * @param position The position within the `FlowInputs` object. 163 | * @param item The `ItemInputs` object to set. 164 | */ 165 | export function setItem( 166 | flow: FlowInputs, 167 | position: Position, 168 | item: ItemInputs 169 | ): void { 170 | switch (flow.type) { 171 | case "list": 172 | return ListInputsUtils.setItem(flow, position, item); 173 | case "cond": 174 | return CondInputsUtils.setItem(flow, position, item); 175 | case "loop": 176 | return LoopInputsUtils.setItem(flow, position, item); 177 | case "switch": 178 | return SwitchInputsUtils.setItem(flow, position, item); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /apps/react/src/schemas/flow.switch.tsx: -------------------------------------------------------------------------------- 1 | import type { Schema, Switch, Form, Return } from "@formity/react"; 2 | 3 | import { zodResolver } from "@hookform/resolvers/zod"; 4 | import { z } from "zod"; 5 | 6 | import { 7 | Step, 8 | Layout, 9 | TextField, 10 | Listbox, 11 | NextButton, 12 | BackButton, 13 | } from "../components"; 14 | 15 | import { MultiStep } from "../multi-step"; 16 | 17 | export type SwitchValues = [ 18 | Form<{ interested: string }>, 19 | Switch<{ 20 | branches: [ 21 | [Form<{ whyYes: string }>, Return<{ interested: "yes"; whyYes: string }>], 22 | [Form<{ whyNot: string }>, Return<{ interested: "no"; whyNot: string }>], 23 | [ 24 | Form<{ whyMaybe: string }>, 25 | Return<{ interested: "maybe"; whyMaybe: string }> 26 | ] 27 | ]; 28 | default: [ 29 | Form<{ whyNotSure: string }>, 30 | Return<{ interested: "notSure"; whyNotSure: string }> 31 | ]; 32 | }> 33 | ]; 34 | 35 | export const switchSchema: Schema = [ 36 | { 37 | form: { 38 | values: () => ({ 39 | interested: ["yes", []], 40 | }), 41 | render: ({ values, ...rest }) => ( 42 | 43 | 51 | , 78 | ]} 79 | button={Next} 80 | /> 81 | 82 | 83 | ), 84 | }, 85 | }, 86 | { 87 | switch: { 88 | branches: [ 89 | { 90 | case: ({ interested }) => interested === "yes", 91 | then: [ 92 | { 93 | form: { 94 | values: () => ({ 95 | whyYes: ["", []], 96 | }), 97 | render: ({ values, ...rest }) => ( 98 | 99 | 107 | , 112 | ]} 113 | button={Next} 114 | back={} 115 | /> 116 | 117 | 118 | ), 119 | }, 120 | }, 121 | { 122 | return: ({ whyYes }) => ({ 123 | interested: "yes", 124 | whyYes, 125 | }), 126 | }, 127 | ], 128 | }, 129 | { 130 | case: ({ interested }) => interested === "no", 131 | then: [ 132 | { 133 | form: { 134 | values: () => ({ 135 | whyNot: ["", []], 136 | }), 137 | render: ({ values, ...rest }) => ( 138 | 139 | 147 | , 152 | ]} 153 | button={Next} 154 | back={} 155 | /> 156 | 157 | 158 | ), 159 | }, 160 | }, 161 | { 162 | return: ({ whyNot }) => ({ 163 | interested: "no", 164 | whyNot, 165 | }), 166 | }, 167 | ], 168 | }, 169 | { 170 | case: ({ interested }) => interested === "maybe", 171 | then: [ 172 | { 173 | form: { 174 | values: () => ({ 175 | whyMaybe: ["", []], 176 | }), 177 | render: ({ values, ...rest }) => ( 178 | 179 | 187 | , 196 | ]} 197 | button={Next} 198 | back={} 199 | /> 200 | 201 | 202 | ), 203 | }, 204 | }, 205 | { 206 | return: ({ whyMaybe }) => ({ 207 | interested: "maybe", 208 | whyMaybe, 209 | }), 210 | }, 211 | ], 212 | }, 213 | ], 214 | default: [ 215 | { 216 | form: { 217 | values: () => ({ 218 | whyNotSure: ["", []], 219 | }), 220 | render: ({ values, ...rest }) => ( 221 | 222 | 230 | , 239 | ]} 240 | button={Next} 241 | back={} 242 | /> 243 | 244 | 245 | ), 246 | }, 247 | }, 248 | { 249 | return: ({ whyNotSure }) => ({ 250 | interested: "notSure", 251 | whyNotSure, 252 | }), 253 | }, 254 | ], 255 | }, 256 | }, 257 | ]; 258 | -------------------------------------------------------------------------------- /packages/react/src/schema.ts: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | 3 | import type { Values } from "@formity/system"; 4 | 5 | import type { 6 | ItemValues, 7 | FlowValues, 8 | ListValues, 9 | CondValues, 10 | LoopValues, 11 | SwitchValues, 12 | FormValues, 13 | YieldValues, 14 | ReturnValues, 15 | VariablesValues, 16 | } from "@formity/system"; 17 | 18 | import type { Schema as SystemSchema } from "@formity/system"; 19 | 20 | import type { 21 | ItemSchema as SystemItemSchema, 22 | FlowSchema as SystemFlowSchema, 23 | ListSchema as SystemListSchema, 24 | CondSchema as SystemCondSchema, 25 | LoopSchema as SystemLoopSchema, 26 | SwitchSchema as SystemSwitchSchema, 27 | FormSchema as SystemFormSchema, 28 | YieldSchema as SystemYieldSchema, 29 | ReturnSchema as SystemReturnSchema, 30 | VariablesSchema as SystemVariablesSchema, 31 | } from "@formity/system"; 32 | 33 | import type { ModelSchema as SystemModelSchema } from "@formity/system"; 34 | 35 | import type { 36 | ModelItemSchema as SystemModelItemSchema, 37 | ModelFlowSchema as SystemModelFlowSchema, 38 | ModelListSchema as SystemModelListSchema, 39 | ModelCondSchema as SystemModelCondSchema, 40 | ModelLoopSchema as SystemModelLoopSchema, 41 | ModelSwitchSchema as SystemModelSwitchSchema, 42 | ModelFormSchema as SystemModelFormSchema, 43 | ModelYieldSchema as SystemModelYieldSchema, 44 | ModelReturnSchema as SystemModelReturnSchema, 45 | ModelVariablesSchema as SystemModelVariablesSchema, 46 | } from "@formity/system"; 47 | 48 | /** 49 | * Defines the structure and behavior of a multi-step form. 50 | * 51 | * @template V A type extending `Values` that defines the structure of the multi-step form, 52 | * including the values handled in each phase. 53 | * 54 | * @template I An object type representing additional values available during form execution, 55 | * beyond those generated by the multi-step form itself. 56 | * 57 | * @template P An object type defining the values accessible when rendering each form step in 58 | * the multi-step process. 59 | */ 60 | export type Schema< 61 | V extends Values, 62 | I extends object = object, 63 | P extends object = object 64 | > = SystemSchema; 65 | 66 | /** 67 | * Defines the structure and behavior of any element in a multi-step form. 68 | * 69 | * @template Values A type extending `ItemValues` that defines the structure of the multi-step form, 70 | * including the values handled in each phase. 71 | * 72 | * @template Inputs An object type representing additional values available during form execution, 73 | * beyond those generated by the multi-step form itself. 74 | * 75 | * @template Params An object type defining the values accessible when rendering each form step in 76 | * the multi-step process. 77 | */ 78 | export type ItemSchema< 79 | Values extends ItemValues, 80 | Inputs extends object, 81 | Params extends object 82 | > = SystemItemSchema; 83 | 84 | /** 85 | * Defines the structure and behavior of any flow element in a multi-step form. 86 | * 87 | * @template Values A type extending `FlowValues` that defines the structure of the multi-step form, 88 | * including the values handled in each phase. 89 | * 90 | * @template Inputs An object type representing additional values available during form execution, 91 | * beyond those generated by the multi-step form itself. 92 | * 93 | * @template Params An object type defining the values accessible when rendering each form step in 94 | * the multi-step process. 95 | */ 96 | export type FlowSchema< 97 | Values extends FlowValues, 98 | Inputs extends object, 99 | Params extends object 100 | > = SystemFlowSchema; 101 | 102 | /** 103 | * Defines the structure and behavior of a list element in a multi-step form. 104 | * 105 | * @template Values A type extending `ListValues` that defines the structure of the multi-step form, 106 | * including the values handled in each phase. 107 | * 108 | * @template Inputs An object type representing additional values available during form execution, 109 | * beyond those generated by the multi-step form itself. 110 | * 111 | * @template Params An object type defining the values accessible when rendering each form step in 112 | * the multi-step process. 113 | */ 114 | export type ListSchema< 115 | Values extends ListValues, 116 | Inputs extends object, 117 | Params extends object 118 | > = SystemListSchema; 119 | 120 | /** 121 | * Defines the structure and behavior of a condition element in a multi-step form. 122 | * 123 | * @template Values A type extending `CondValues` that defines the structure of the multi-step form, 124 | * including the values handled in each phase. 125 | * 126 | * @template Inputs An object type representing additional values available during form execution, 127 | * beyond those generated by the multi-step form itself. 128 | * 129 | * @template Params An object type defining the values accessible when rendering each form step in 130 | * the multi-step process. 131 | */ 132 | export type CondSchema< 133 | Values extends CondValues, 134 | Inputs extends object, 135 | Params extends object 136 | > = SystemCondSchema; 137 | 138 | /** 139 | * Defines the structure and behavior of a loop element in a multi-step form. 140 | * 141 | * @template Values A type extending `LoopValues` that defines the structure of the multi-step form, 142 | * including the values handled in each phase. 143 | * 144 | * @template Inputs An object type representing additional values available during form execution, 145 | * beyond those generated by the multi-step form itself. 146 | * 147 | * @template Params An object type defining the values accessible when rendering each form step in 148 | * the multi-step process. 149 | */ 150 | export type LoopSchema< 151 | Values extends LoopValues, 152 | Inputs extends object, 153 | Params extends object 154 | > = SystemLoopSchema; 155 | 156 | /** 157 | * Defines the structure and behavior of a switch element in a multi-step form. 158 | * 159 | * @template Values A type extending `SwitchValues` that defines the structure of the multi-step 160 | * form, including the values handled in each phase. 161 | * 162 | * @template Inputs An object type representing additional values available during form execution, 163 | * beyond those generated by the multi-step form itself. 164 | * 165 | * @template Params An object type defining the values accessible when rendering each form step in 166 | * the multi-step process. 167 | */ 168 | export type SwitchSchema< 169 | Values extends SwitchValues, 170 | Inputs extends object, 171 | Params extends object 172 | > = SystemSwitchSchema; 173 | 174 | /** 175 | * Defines the structure and behavior of a form element in a multi-step form. 176 | * 177 | * @template Values A type extending `FormValues` that defines the values of the form element. 178 | * 179 | * @template Inputs An object type representing additional values available during form execution, 180 | * beyond those generated by the multi-step form itself. 181 | * 182 | * @template Params An object type defining the values accessible when rendering each form step in 183 | * the multi-step process. 184 | */ 185 | export type FormSchema< 186 | Values extends FormValues, 187 | Inputs extends object, 188 | Params extends object 189 | > = SystemFormSchema; 190 | 191 | /** 192 | * Defines the structure and behavior of a yield element in a multi-step form. 193 | * 194 | * @template Values A type extending `YieldValues` that defines the values of the yield element. 195 | * 196 | * @template Inputs An object type representing additional values available during form execution, 197 | * beyond those generated by the multi-step form itself. 198 | */ 199 | export type YieldSchema< 200 | Values extends YieldValues, 201 | Inputs extends object 202 | > = SystemYieldSchema; 203 | 204 | /** 205 | * Defines the structure and behavior of a return element in a multi-step form. 206 | * 207 | * @template Values A type extending `ReturnValues` that defines the values of the return element. 208 | * 209 | * @template Inputs An object type representing additional values available during form execution, 210 | * beyond those generated by the multi-step form itself. 211 | */ 212 | export type ReturnSchema< 213 | Values extends ReturnValues, 214 | Inputs extends object 215 | > = SystemReturnSchema; 216 | 217 | /** 218 | * Defines the structure and behavior of a variables element in a multi-step form. 219 | * 220 | * @template Values A type extending `VariablesValues` that defines the values of the variables element. 221 | * 222 | * @template Inputs An object type representing additional values available during form execution, 223 | * beyond those generated by the multi-step form itself. 224 | */ 225 | export type VariablesSchema< 226 | Values extends VariablesValues, 227 | Inputs extends object 228 | > = SystemVariablesSchema; 229 | 230 | /** 231 | * Defines the structure and behavior of a multi-step form. 232 | */ 233 | export type ModelSchema = SystemModelSchema; 234 | 235 | /** 236 | * Defines the structure and behavior of any element in a multi-step form. 237 | */ 238 | export type ModelItemSchema = SystemModelItemSchema; 239 | 240 | /** 241 | * Defines the structure and behavior of any flow element in a multi-step form. 242 | */ 243 | export type ModelFlowSchema = SystemModelFlowSchema; 244 | 245 | /** 246 | * Defines the structure and behavior of a list element in a multi-step form. 247 | */ 248 | export type ModelListSchema = SystemModelListSchema; 249 | 250 | /** 251 | * Defines the structure and behavior of a condition element in a multi-step form. 252 | */ 253 | export type ModelCondSchema = SystemModelCondSchema; 254 | 255 | /** 256 | * Defines the structure and behavior of a loop element in a multi-step form. 257 | */ 258 | export type ModelLoopSchema = SystemModelLoopSchema; 259 | 260 | /** 261 | * Defines the structure and behavior of a switch element in a multi-step form. 262 | */ 263 | export type ModelSwitchSchema = SystemModelSwitchSchema; 264 | 265 | /** 266 | * Defines the structure and behavior of a form element in a multi-step form. 267 | */ 268 | export type ModelFormSchema = SystemModelFormSchema; 269 | 270 | /** 271 | * Defines the structure and behavior of a yield element in a multi-step form. 272 | */ 273 | export type ModelYieldSchema = SystemModelYieldSchema; 274 | 275 | /** 276 | * Defines the structure and behavior of a return element in a multi-step form. 277 | */ 278 | export type ModelReturnSchema = SystemModelReturnSchema; 279 | 280 | /** 281 | * Defines the structure and behavior of a variables element in a multi-step form. 282 | */ 283 | export type ModelVariablesSchema = SystemModelVariablesSchema; 284 | -------------------------------------------------------------------------------- /packages/system/src/utils/schema/flow.switch.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | 3 | import type { SwitchSchema, ReturnSchema } from "../../types/schema/model"; 4 | import type { Position } from "src/types/state/position"; 5 | 6 | import { into, next, at } from "./flow.switch"; 7 | 8 | describe("SwitchSchema", () => { 9 | describe("into", () => { 10 | it("navigates into a branch of the `SwitchSchema` object", () => { 11 | const schema: SwitchSchema = { 12 | switch: { 13 | branches: [ 14 | { 15 | case: () => false, 16 | then: [ 17 | { 18 | form: { 19 | values: () => ({}), 20 | render: () => ({}), 21 | }, 22 | }, 23 | ], 24 | }, 25 | { 26 | case: () => true, 27 | then: [ 28 | { 29 | form: { 30 | values: () => ({}), 31 | render: () => ({}), 32 | }, 33 | }, 34 | ], 35 | }, 36 | { 37 | case: () => true, 38 | then: [ 39 | { 40 | form: { 41 | values: () => ({}), 42 | render: () => ({}), 43 | }, 44 | }, 45 | ], 46 | }, 47 | ], 48 | default: [], 49 | }, 50 | }; 51 | const position = into(schema, {}); 52 | expect(position).toEqual({ type: "switch", branch: 1, slot: 0 }); 53 | }); 54 | 55 | it("navigates into the default branch of the `SwitchSchema` object", () => { 56 | const schema: SwitchSchema = { 57 | switch: { 58 | branches: [ 59 | { 60 | case: () => false, 61 | then: [ 62 | { 63 | form: { 64 | values: () => ({}), 65 | render: () => ({}), 66 | }, 67 | }, 68 | ], 69 | }, 70 | { 71 | case: () => false, 72 | then: [ 73 | { 74 | form: { 75 | values: () => ({}), 76 | render: () => ({}), 77 | }, 78 | }, 79 | ], 80 | }, 81 | ], 82 | default: [ 83 | { 84 | form: { 85 | values: () => ({}), 86 | render: () => ({}), 87 | }, 88 | }, 89 | ], 90 | }, 91 | }; 92 | const position = into(schema, {}); 93 | expect(position).toEqual({ type: "switch", branch: -1, slot: 0 }); 94 | }); 95 | 96 | it("doesn't navigate into the `SwitchSchema` object", () => { 97 | const schema: SwitchSchema = { 98 | switch: { 99 | branches: [ 100 | { 101 | case: () => false, 102 | then: [ 103 | { 104 | form: { 105 | values: () => ({}), 106 | render: () => ({}), 107 | }, 108 | }, 109 | ], 110 | }, 111 | { 112 | case: () => true, 113 | then: [], 114 | }, 115 | { 116 | case: () => true, 117 | then: [ 118 | { 119 | form: { 120 | values: () => ({}), 121 | render: () => ({}), 122 | }, 123 | }, 124 | ], 125 | }, 126 | ], 127 | default: [ 128 | { 129 | form: { 130 | values: () => ({}), 131 | render: () => ({}), 132 | }, 133 | }, 134 | ], 135 | }, 136 | }; 137 | const position = into(schema, {}); 138 | expect(position).toEqual(null); 139 | }); 140 | }); 141 | 142 | describe("next", () => { 143 | it("navigates to the next item in a branch of the `SwitchSchema` object", () => { 144 | const schema: SwitchSchema = { 145 | switch: { 146 | branches: [ 147 | { 148 | case: () => false, 149 | then: [], 150 | }, 151 | { 152 | case: () => true, 153 | then: [ 154 | { 155 | form: { 156 | values: () => ({}), 157 | render: () => ({}), 158 | }, 159 | }, 160 | { 161 | form: { 162 | values: () => ({}), 163 | render: () => ({}), 164 | }, 165 | }, 166 | ], 167 | }, 168 | ], 169 | default: [], 170 | }, 171 | }; 172 | const current: Position = { type: "switch", branch: 1, slot: 0 }; 173 | const position = next(schema, current); 174 | expect(position).toEqual({ type: "switch", branch: 1, slot: 1 }); 175 | }); 176 | 177 | it("navigates to the next item in the default branch of the `SwitchSchema` object", () => { 178 | const schema: SwitchSchema = { 179 | switch: { 180 | branches: [ 181 | { 182 | case: () => false, 183 | then: [], 184 | }, 185 | { 186 | case: () => false, 187 | then: [], 188 | }, 189 | ], 190 | default: [ 191 | { 192 | form: { 193 | values: () => ({}), 194 | render: () => ({}), 195 | }, 196 | }, 197 | { 198 | form: { 199 | values: () => ({}), 200 | render: () => ({}), 201 | }, 202 | }, 203 | ], 204 | }, 205 | }; 206 | const current: Position = { type: "switch", branch: -1, slot: 0 }; 207 | const position = next(schema, current); 208 | expect(position).toEqual({ type: "switch", branch: -1, slot: 1 }); 209 | }); 210 | 211 | it("doesn't navigate to the next item in the `SwitchSchema` object within a branch", () => { 212 | const schema: SwitchSchema = { 213 | switch: { 214 | branches: [ 215 | { 216 | case: () => false, 217 | then: [], 218 | }, 219 | { 220 | case: () => true, 221 | then: [ 222 | { 223 | form: { 224 | values: () => ({}), 225 | render: () => ({}), 226 | }, 227 | }, 228 | ], 229 | }, 230 | { 231 | case: () => true, 232 | then: [ 233 | { 234 | form: { 235 | values: () => ({}), 236 | render: () => ({}), 237 | }, 238 | }, 239 | ], 240 | }, 241 | ], 242 | default: [ 243 | { 244 | form: { 245 | values: () => ({}), 246 | render: () => ({}), 247 | }, 248 | }, 249 | ], 250 | }, 251 | }; 252 | const current: Position = { type: "switch", branch: 1, slot: 0 }; 253 | const position = next(schema, current); 254 | expect(position).toEqual(null); 255 | }); 256 | 257 | it("doesn't navigate to the next item in the `SwitchSchema` object within the default branch", () => { 258 | const schema: SwitchSchema = { 259 | switch: { 260 | branches: [ 261 | { 262 | case: () => false, 263 | then: [], 264 | }, 265 | { 266 | case: () => false, 267 | then: [], 268 | }, 269 | ], 270 | default: [ 271 | { 272 | form: { 273 | values: () => ({}), 274 | render: () => ({}), 275 | }, 276 | }, 277 | ], 278 | }, 279 | }; 280 | const current: Position = { type: "switch", branch: -1, slot: 0 }; 281 | const position = next(schema, current); 282 | expect(position).toEqual(null); 283 | }); 284 | }); 285 | 286 | describe("at", () => { 287 | it("retrieves the item at the specified position in a branch of the `SwitchSchema` object", () => { 288 | const item: ReturnSchema = { return: () => ({}) }; 289 | const schema: SwitchSchema = { 290 | switch: { 291 | branches: [ 292 | { 293 | case: () => false, 294 | then: [ 295 | { 296 | form: { 297 | values: () => ({}), 298 | render: () => ({}), 299 | }, 300 | }, 301 | ], 302 | }, 303 | { 304 | case: () => true, 305 | then: [ 306 | { 307 | form: { 308 | values: () => ({}), 309 | render: () => ({}), 310 | }, 311 | }, 312 | item, 313 | ], 314 | }, 315 | { 316 | case: () => true, 317 | then: [ 318 | { 319 | form: { 320 | values: () => ({}), 321 | render: () => ({}), 322 | }, 323 | }, 324 | ], 325 | }, 326 | ], 327 | default: [], 328 | }, 329 | }; 330 | const position: Position = { type: "switch", branch: 1, slot: 1 }; 331 | const result = at(schema, position); 332 | expect(result).toBe(item); 333 | }); 334 | 335 | it("retrieves the item at the specified position in the default branch of the `SwitchSchema` object", () => { 336 | const item: ReturnSchema = { return: () => ({}) }; 337 | const schema: SwitchSchema = { 338 | switch: { 339 | branches: [ 340 | { 341 | case: () => false, 342 | then: [], 343 | }, 344 | { 345 | case: () => false, 346 | then: [], 347 | }, 348 | ], 349 | default: [ 350 | { 351 | form: { 352 | values: () => ({}), 353 | render: () => ({}), 354 | }, 355 | }, 356 | item, 357 | ], 358 | }, 359 | }; 360 | const position: Position = { type: "switch", branch: -1, slot: 1 }; 361 | const result = at(schema, position); 362 | expect(result).toBe(item); 363 | }); 364 | }); 365 | }); 366 | --------------------------------------------------------------------------------