/* * This file belongs to Hoist, an application development toolkit * developed by Extremely Heavy Industries (www.xh.io | info@xh.io) * * Copyright © 2026 Extremely Heavy Industries Inc. */ import { DefaultHoistProps, elementFactory, hoistCmp, HoistProps, TestSupportProps, uses } from '@xh/hoist/core'; import {useCached} from '@xh/hoist/utils/react'; import equal from 'fast-deep-equal'; import {createContext, useContext} from 'react'; import {BaseFormFieldProps} from './BaseFormFieldProps'; import {FormModel} from './FormModel'; /** @internal */ export interface FormContextType { /** Defaults props to be applied to contained fields. */ fieldDefaults?: Partial & DefaultHoistProps; /** Reference to associated FormModel. */ model?: FormModel; /** * Not rendered into the DOM directly - `Form` is a context provider and not a concrete * component - but will auto-generate and apply a testId of `${formTestId}-${fieldName}` * for every child {@link FormField} component, providing a centralized way to wire up * a form and all of its fields for testing. */ testId?: string; } /** @internal */ export const FormContext = createContext({}); const formContextProvider = elementFactory(FormContext.Provider); export interface FormProps extends HoistProps, TestSupportProps { /** * Defaults for certain props on child/nested FormFields. * Note there are both desktop and mobile implementations of FormField. * @see FormField */ fieldDefaults?: Partial & DefaultHoistProps; } /** * Wrapper component for a data-input form. This is the top-level entry point (along with its model * class, FormModel) for building a form of arbitrary complexity with support for data binding, * default / initial values, client-side validation rules, and nested sub-forms. * * This container accepts any manner of child components for layout or other purposes, but is * primarily designed to apply defaults to and manage data binding for FormField components, which * can be nested at any level below this parent component. * * @see FormModel * @see FieldModel * @see FormField * @see HoistInput */ export const [Form, form] = hoistCmp.withFactory({ displayName: 'Form', model: uses(FormModel, {publishMode: 'none'}), render({model, fieldDefaults = {}, testId, children}) { // gather own and inherited field defaults... const parentDefaults = useContext(FormContext).fieldDefaults; if (parentDefaults) fieldDefaults = {...parentDefaults, ...fieldDefaults}; // ...and deliver as a cached context to avoid spurious re-renders const formContext = useCached( { model, fieldDefaults, testId }, (a, b) => a.model === b.model && equal(a.fieldDefaults, b.fieldDefaults) ); return formContextProvider({value: formContext, items: children}); } });