Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CNFT1-3642] Incorporated verification into form library #2130

Merged
merged 2 commits into from
Dec 30, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Controller, useFormContext } from 'react-hook-form';
import { EntryFieldsProps } from 'design-system/entry';
import { maxLengthRule } from 'validation/entry';
import { Verification } from 'libs/verification';
import { EmailField, validateEmail, PhoneNumberInputField, validPhoneNumberRule } from 'libs/demographics/contact';
import { EmailField, maybeValidateEmail, PhoneNumberInputField, validPhoneNumberRule } from 'libs/demographics/contact';
import { BasicPhoneEmail } from '../entry';
import { MaskedTextInputField } from 'design-system/input/text';

Expand All @@ -13,7 +13,10 @@ const EMAIL_LABEL = 'Email';

type BasicPhoneEmailFieldsProps = EntryFieldsProps;

export const BasicPhoneEmailFields = ({ orientation = 'horizontal' }: BasicPhoneEmailFieldsProps) => {
export const BasicPhoneEmailFields = ({
orientation = 'horizontal',
sizing = 'compact'
}: BasicPhoneEmailFieldsProps) => {
const { control } = useFormContext<{ phoneEmail: BasicPhoneEmail }>();

return (
Expand All @@ -25,12 +28,12 @@ export const BasicPhoneEmailFields = ({ orientation = 'horizontal' }: BasicPhone
render={({ field: { onChange, onBlur, value, name }, fieldState: { error } }) => (
<PhoneNumberInputField
id={name}
sizing="compact"
label={HOME_PHONE_LABEL}
value={value}
onBlur={onBlur}
onChange={onChange}
orientation={orientation}
sizing={sizing}
error={error?.message}
/>
)}
Expand All @@ -42,11 +45,11 @@ export const BasicPhoneEmailFields = ({ orientation = 'horizontal' }: BasicPhone
render={({ field: { onChange, onBlur, value, name }, fieldState: { error } }) => (
<PhoneNumberInputField
id={name}
sizing="compact"
label={WORK_PHONE_LABEL}
value={value}
onBlur={onBlur}
onChange={onChange}
sizing={sizing}
orientation={orientation}
error={error?.message}
/>
Expand All @@ -69,9 +72,9 @@ export const BasicPhoneEmailFields = ({ orientation = 'horizontal' }: BasicPhone
mask="____________________"
pattern="^\+?\d{1,20}$"
value={value}
sizing="compact"
onBlur={onBlur}
onChange={onChange}
sizing={sizing}
orientation={orientation}
error={error?.message}
/>
Expand All @@ -85,11 +88,11 @@ export const BasicPhoneEmailFields = ({ orientation = 'horizontal' }: BasicPhone
render={({ field: { onChange, onBlur, value, name }, fieldState: { error } }) => (
<PhoneNumberInputField
id={name}
sizing="compact"
label={CELL_PHONE_LABEL}
value={value}
onBlur={onBlur}
onChange={onChange}
sizing={sizing}
orientation={orientation}
error={error?.message}
/>
Expand All @@ -102,17 +105,20 @@ export const BasicPhoneEmailFields = ({ orientation = 'horizontal' }: BasicPhone
rules={maxLengthRule(100, EMAIL_LABEL)}
render={({ field: { onChange, onBlur, value, name }, fieldState: { error } }) => (
<Verification
constraint={validateEmail(EMAIL_LABEL)}
control={control}
name={name}
constraint={maybeValidateEmail(EMAIL_LABEL)}
render={({ verify, violation }) => (
<EmailField
id={name}
label={EMAIL_LABEL}
onBlur={() => {
verify(value);
verify();
onBlur();
}}
onChange={onChange}
value={value}
sizing={sizing}
orientation={orientation}
error={error?.message}
warning={violation}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import FormCard from 'components/FormCard/FormCard';
import { Input } from 'components/FormInputs/Input';
import { maxLengthRule } from 'validation/entry';
import { Verification } from 'libs/verification';
import { EmailField, validateEmail, PhoneNumberInputField, validPhoneNumberRule } from 'libs/demographics/contact';
import { EmailField, maybeValidateEmail, PhoneNumberInputField, validPhoneNumberRule } from 'libs/demographics/contact';

const HOME_PHONE_LABEL = 'Home phone';
const WORK_PHONE_LABEL = 'Work phone';
Expand Down Expand Up @@ -123,13 +123,15 @@ export default function ContactFields({ id, title }: Props) {
rules={maxLengthRule(100, EMAIL_LABEL)}
render={({ field: { onChange, onBlur, value, name }, fieldState: { error } }) => (
<Verification
constraint={validateEmail(EMAIL_LABEL)}
control={control}
name={name}
constraint={maybeValidateEmail(EMAIL_LABEL)}
render={({ verify, violation }) => (
<EmailField
id={name}
label={EMAIL_LABEL}
onBlur={() => {
verify(value);
verify();
onBlur();
}}
onChange={onChange}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ import { SingleSelect } from 'design-system/select';
import { EntryFieldsProps } from 'design-system/entry';
import { maxLengthRule, validateRequiredRule } from 'validation/entry';
import { Verification } from 'libs/verification';
import { EmailField, validateEmail, PhoneNumberInputField, validPhoneNumberRule } from 'libs/demographics/contact';
import { MaskedTextInputField } from 'design-system/input/text';
import { Input } from 'components/FormInputs/Input';
import { EmailField, PhoneNumberInputField, validPhoneNumberRule, maybeValidateEmail } from 'libs/demographics/contact';
import { MaskedTextInputField, TextInputField } from 'design-system/input/text';
import { PhoneEmailEntry } from 'apps/patient/data';
import { usePatientPhoneCodedValues } from 'apps/patient/profile/phoneEmail/usePatientPhoneCodedValues';
import { TextAreaField } from 'design-system/input/text/TextAreaField';
Expand Down Expand Up @@ -152,13 +151,15 @@ export const PhoneEmailEntryFields = ({ orientation = 'horizontal' }: PhoneEmail
rules={maxLengthRule(100, EMAIL_LABEL)}
render={({ field: { onChange, onBlur, value, name }, fieldState: { error } }) => (
<Verification
constraint={validateEmail(EMAIL_LABEL)}
control={control}
name={name}
constraint={maybeValidateEmail(EMAIL_LABEL)}
render={({ verify, violation }) => (
<EmailField
id={name}
label={EMAIL_LABEL}
onBlur={() => {
verify(value);
verify();
onBlur();
}}
onChange={onChange}
Expand All @@ -176,16 +177,13 @@ export const PhoneEmailEntryFields = ({ orientation = 'horizontal' }: PhoneEmail
name="url"
rules={maxLengthRule(100, URL_LABEL)}
render={({ field: { onChange, onBlur, value, name }, fieldState: { error } }) => (
<Input
<TextInputField
id={name}
label={URL_LABEL}
orientation={orientation}
onBlur={onBlur}
onChange={onChange}
defaultValue={value}
type="text"
htmlFor={name}
id={name}
name={name}
value={value}
orientation={orientation}
error={error?.message}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ const initial = (asOf: string = today()): Partial<PhoneEmailEntry> => ({
asOf,
type: undefined,
use: undefined,
countryCode: '',
phoneNumber: '',
extension: '',
email: '',
url: '',
comment: ''
countryCode: undefined,
phoneNumber: undefined,
extension: undefined,
email: undefined,
url: undefined,
comment: undefined
});

export { initial };
3 changes: 2 additions & 1 deletion apps/modernization-ui/src/design-system/entry/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Orientation } from 'components/Entry';
import { Orientation, Sizing } from 'components/Entry';

type EntryFieldsProps = {
orientation?: Orientation;
sizing?: Sizing;
};

export type { EntryFieldsProps };
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { validateIfPresent } from 'validation';
import { validateEmail } from './validateEmail';

/**
* Validates that the given string represents a valid email. Any failed
* validations will include the name of the field being validated.
*
* @param {string} name The name of the field being validated
* @return {Validator}
*/
const maybeValidateEmail = (name: string) => validateIfPresent(validateEmail(name));

export { maybeValidateEmail };
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { validateIfPresent } from 'validation';
import { maxLengthRule } from 'validation/entry';
import { validateEmail } from './validateEmail';
import { maybeValidateEmail } from './maybeValidateEmail';

const validEmailRule = (name: string) => ({
validate: validateIfPresent(validateEmail(name)),
validate: maybeValidateEmail(name),
...maxLengthRule(100, name)
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export { validPhoneNumberRule } from './phone/validPhoneNumberRule';

export { EmailField } from './email/EmailField';
export { validateEmail } from './email/validateEmail';
export { maybeValidateEmail } from './email/maybeValidateEmail';
export { validEmailRule } from './email/validateEmailRule';
27 changes: 17 additions & 10 deletions apps/modernization-ui/src/libs/verification/Verification.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
import { Validator } from 'validation';
import { useVerification } from './useVerification';
import { FieldPath, FieldValues } from 'react-hook-form';
import { useVerification, VerificationOptions } from './useVerification';

type VerificationRenderProps<T> = {
type VerificationRenderProps = {
violation?: string;
verify: (value?: T) => void;
verify: () => void;
};

type VerificationProps<T> = {
constraint: Validator<T>;
render: (result: VerificationRenderProps<T>) => JSX.Element;
type VerificationProps<
Values extends FieldValues = FieldValues,
Name extends FieldPath<Values> = FieldPath<Values>
> = VerificationOptions<Values, Name> & {
render: (result: VerificationRenderProps) => JSX.Element;
};

const Verification = <T,>({ constraint, render }: VerificationProps<T>) => {
const { violation, verify } = useVerification({ constraint });
const Verification = <V extends FieldValues = FieldValues, N extends FieldPath<V> = FieldPath<V>>({
name,
control,
constraint,
render
}: VerificationProps<V, N>) => {
const { violation, verify } = useVerification({ name, control, constraint });

return render({ violation, verify });
return render({ verify, violation: violation?.message });
};

export { Verification };
70 changes: 46 additions & 24 deletions apps/modernization-ui/src/libs/verification/useVerification.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,57 @@
import { useCallback, useState } from 'react';
import { validateIfPresent, Validator } from 'validation';
import { useCallback, useEffect, useState } from 'react';
import { Control, FieldPath, FieldPathValue, FieldValues, useWatch } from 'react-hook-form';
import { Validator } from 'validation';

type VerificationOptions<T> = {
constraint: Validator<T>;
type Violation<V> = {
value: V;
message: string | undefined;
};

type VerificationInteraction<T> = {
verify: (value?: T) => void;
violation?: string;
type VerificationOptions<
Values extends FieldValues = FieldValues,
Name extends FieldPath<Values> = FieldPath<Values>
> = {
name: Name;
control: Control<Values>;
constraint: Validator<FieldPathValue<Values, Name>>;
};

type VerificationInteraction<Value> = {
verify: () => void;
violation?: Violation<Value>;
};

/**
* Applies a constraint to a value and provides any violations if the constraint fails.
* Applies a constraint to a value providing any violations if the constraint fails.
*
* @param {VerificationOptions} param0 The options for the verification, including the constraint to apply for verification.
* @param {VerificationOptions} param0 The options for the verification, including
* - the name of the field that is being verified.
* - the control of the form that is being verified.
* - the constraint to apply for verification.
* @return {VerificationInteraction} The interactions with the hook providing the verify function and any violations.
*/
const useVerification = <V>({ constraint }: VerificationOptions<V>): VerificationInteraction<V> => {
const [violation, setViolation] = useState<string | undefined>();

const verify = useCallback(
(value?: V) => {
const result = validateIfPresent(constraint)(value);

if (typeof result === 'string') {
setViolation(result);
} else {
setViolation(undefined);
}
},
[setViolation, constraint]
);
const useVerification = <V extends FieldValues = FieldValues, N extends FieldPath<V> = FieldPath<V>>({
name,
control,
constraint
}: VerificationOptions<V, N>): VerificationInteraction<V> => {
const [violation, setViolation] = useState<Violation<FieldPathValue<V, N>>>();

const current = useWatch({ control, name });

useEffect(() => {
setViolation((existing) => (current !== existing?.value ? undefined : existing));
}, [current, setViolation]);

const verify = useCallback(() => {
const result = constraint(current);

if (typeof result === 'string') {
setViolation({ value: current, message: result });
} else {
setViolation(undefined);
}
}, [setViolation, constraint, current]);

return {
verify,
Expand All @@ -39,3 +60,4 @@ const useVerification = <V>({ constraint }: VerificationOptions<V>): Verificatio
};

export { useVerification };
export type { VerificationOptions };

This file was deleted.

Loading