import { IFormController, IComponentOptions, IModule } from 'angular'; import { FormikErrors } from 'formik'; import { SchemaOf, object } from 'yup'; import _ from 'lodash'; import { ComponentType } from 'react'; import { PropNames, r2a } from '@/react-tools/react2angular'; import { validateForm } from '@@/form-components/validate-form'; import { ArrayError } from '@@/form-components/InputList/InputList'; interface FormFieldProps { onChange(values: TValue): void; // update the values for the entire form object used in yup validation, not just one input. values: TValue; // current values errors?: FormikErrors | ArrayError; } type WithFormFieldProps = TProps & FormFieldProps; type ValidationResult = FormikErrors | undefined; /** * This utility function is used for wrapping React components with form validation. * When used inside an Angular form, it sets the form to invalid if the component values are invalid. * This function registers two AngularJS components: * 1. The React component with r2a wrapping: * - `onChange` and `values` must be manually passed to the React component from an Angular view. * - `errors` will be automatically passed to the React component and updated by the validation component. * 2. An AngularJS component that handles form validation, based on a yup validation schema: * - `validationData` can optionally be passed to the React component from an Angular view, which can be used in validation. * * @example * // Usage in Angular view * * */ export function withFormValidation( ngModule: IModule, Component: ComponentType>, componentName: string, propNames: PropNames[], schemaBuilder: (validationData?: TData) => SchemaOf, isPrimitive = false ) { const reactComponentName = `react${_.upperFirst(componentName)}`; ngModule .component( reactComponentName, r2a(Component, ['errors', 'onChange', 'values', ...propNames]) ) .component( componentName, createFormValidationComponent( reactComponentName, propNames, schemaBuilder, isPrimitive ) ); } export function createFormValidationComponent( componentName: string, propNames: Array, schemaBuilder: (validationData?: TData) => SchemaOf, isPrimitive = false ): IComponentOptions { const kebabName = _.kebabCase(componentName); const propsWithErrors = [...propNames, 'errors', 'values']; return { template: ` <${kebabName} ${propsWithErrors .filter((p) => p !== 'onChange') .map((p) => `${_.kebabCase(p)}="$ctrl.${p}"`) .join(' ')} on-change="($ctrl.handleChange)" > `, controller: createFormValidatorController(schemaBuilder, isPrimitive), bindings: Object.fromEntries( [...propsWithErrors, 'validationData', 'onChange'].map((p) => [p, '<']) ), }; } function createFormValidatorController( schemaBuilder: (validationData?: TData) => SchemaOf, isPrimitive = false ) { return class FormValidatorController { errors?: FormikErrors; $async: (fn: () => Promise) => Promise; form?: IFormController; values?: TFormModel; validationData?: TData; onChange?: (value: TFormModel) => void; /* @ngInject */ constructor($async: (fn: () => Promise) => Promise) { this.$async = $async; this.handleChange = this.handleChange.bind(this); this.runValidation = this.runValidation.bind(this); } async handleChange(newValues: TFormModel) { return this.$async(async () => { this.onChange?.(newValues); await this.runValidation(newValues); }); } async runValidation(value: TFormModel) { return this.$async(async () => { this.form?.$setValidity('form', true, this.form); const schema = schemaBuilder(this.validationData); this.errors = await validate(schema, value, isPrimitive); if (this.errors && Object.keys(this.errors).length > 0) { this.form?.$setValidity('form', false, this.form); } }); } async $onChanges(changes: { values?: { currentValue: TFormModel }; validationData?: { currentValue: TData }; }) { if (changes.values) { await this.runValidation(changes.values.currentValue); } // also run validation if validationData changes if (changes.validationData) { await this.runValidation(this.values!); } } }; } async function validate( schema: SchemaOf, value: TFormModel, isPrimitive: boolean ): Promise> { if (isPrimitive) { const result = await validateForm<{ value: TFormModel }>( () => object({ value: schema }), { value } ); return result?.value as ValidationResult; } return validateForm(() => schema, value); }