import { IFormController, IComponentOptions, IModule } from 'angular'; import { FormikErrors } from 'formik'; import { SchemaOf } 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; values: TValue; errors?: FormikErrors | ArrayError; validationContext?: object; // optional context to pass to yup validation, for example, external data } type WithFormFieldProps = TProps & FormFieldProps; /** * Utility function to use for wrapping react components with form validation * when used inside an angular form, it will set the form to invalid if the component values are invalid. * * this registers two angularjs components: * 1. the react component with r2a wrapping * 2. an angularjs component that handles form validation */ export function withFormValidation( ngModule: IModule, Component: ComponentType>, componentName: string, propNames: PropNames[], schemaBuilder: (data?: TData) => SchemaOf ) { const reactComponentName = `react${_.upperFirst(componentName)}`; ngModule .component( reactComponentName, r2a(Component, [ 'errors', 'onChange', 'values', 'validationContext', ...propNames, ]) ) .component( componentName, createFormValidationComponent( reactComponentName, propNames, schemaBuilder ) ); } export function createFormValidationComponent( componentName: string, props: Array, schemaBuilder: (data?: TData) => SchemaOf ): IComponentOptions { const kebabName = _.kebabCase(componentName); const propsWithErrors = [...props, 'errors', 'values']; return { template: ` <${kebabName} ${propsWithErrors .filter((p) => p !== 'onChange') .map((p) => `${_.kebabCase(p)}="$ctrl.${p}"`) .join(' ')} on-change="($ctrl.handleChange)" > `, controller: createFormValidatorController(schemaBuilder), bindings: Object.fromEntries( [ ...propsWithErrors, 'validationData', 'onChange', 'validationContext', ].map((p) => [p, '<']) ), }; } export function createFormValidatorController( schemaBuilder: (data?: TData) => SchemaOf ) { return class FormValidatorController { errors?: FormikErrors = {}; $async: (fn: () => Promise) => Promise; form?: IFormController; values?: TFormModel; validationData?: TData; validationContext?: object; 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, this.validationContext); }); } async runValidation(value: TFormModel, validationContext?: object) { return this.$async(async () => { this.form?.$setValidity('form', true, this.form); this.errors = await validateForm( () => schemaBuilder(this.validationData), value, validationContext ); if (this.errors && Object.keys(this.errors).length > 0) { this.form?.$setValidity('form', false, this.form); } }); } async $onChanges(changes: { values?: { currentValue: TFormModel } }) { if (changes.values) { await this.runValidation( changes.values.currentValue, this.validationContext ); } } }; }