Skip to content

Commit

Permalink
docs: Add uncontrolled input example using React Hook Form (#1820)
Browse files Browse the repository at this point in the history
[React Hook Form](https://react-hook-form.com/) is a popular library for working with uncontrolled inputs. This PR creates an uncontrolled input example that matches the Formik one in functionality. 
I also grouped all the `<form>` examples together, and added the submit button to the `Select` example

[category:Documentation]
  • Loading branch information
vibdev authored Oct 6, 2022
1 parent fd5ab8a commit a5a53bb
Show file tree
Hide file tree
Showing 8 changed files with 228 additions and 56 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {Meta, Story} from '@storybook/addon-docs';
import {SelectWithFormik} from './examples/SelectWithFormik';

<Meta title="Examples/Select With Formik/React" />
<Meta title="Examples/Forms/Select Using Formik/React" />

# Canvas Kit Examples

## Select With Formik
## Select Using Formik

<ExampleCodeBlock code={SelectWithFormik} />
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {Meta, Story} from '@storybook/addon-docs';
import {TextInputWithFormik} from './examples/TextInputWithFormik';

<Meta title="Examples/Text Input With Formik/React" />
<Meta title="Examples/Forms/Controlled Inputs Using Formik/React" />

# Canvas Kit Examples

## Text Input With Formik
## Controlled Text Inputs Using Formik

<ExampleCodeBlock code={TextInputWithFormik} />
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {Meta, Story} from '@storybook/addon-docs';
import {TextInputWithReactHookForm} from './examples/TextInputWithReactHookForm';

<Meta title="Examples/Forms/Uncontrolled Inputs Using React Hook Form/React" />

# Canvas Kit Examples

## Uncontrolled Text Inputs Using React Hook Form

<ExampleCodeBlock code={TextInputWithReactHookForm} />
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,41 @@ import {
useFormFieldInput,
useFormFieldModel,
} from '@workday/canvas-kit-preview-react/form-field';
import {VStack} from '@workday/canvas-kit-react/layout';
import {PrimaryButton} from '@workday/canvas-kit-react/button';

export const SelectWithFormik = () => {
const formik = useFormik({
initialValues: {
selectedBook: '',
},
onSubmit: data => {
console.log(data);
onSubmit: values => {
// Send data to server
setTimeout(() => {
alert(JSON.stringify(values, null, 2));
}, 0);
},
});

const model = useFormFieldModel();
const formFieldInputProps = useFormFieldInput(model);

return (
<form onSubmit={formik.handleSubmit}>
<FormField orientation="vertical" alignItems="stretch">
<FormField.Label>Choose a book</FormField.Label>
<Select
name="selectedBook"
options={bookList}
onChange={event => formik.setFieldValue('selectedBook', event.currentTarget.value)}
value={formik.values.selectedBook}
grow
{...formFieldInputProps}
/>
</FormField>
<form onSubmit={formik.handleSubmit} action=".">
<VStack spacing="xs" alignItems="flex-start">
<FormField orientation="vertical" alignSelf="stretch" alignItems="normal">
<FormField.Label>Choose a book</FormField.Label>
<Select
name="selectedBook"
options={bookList}
onChange={event => formik.setFieldValue('selectedBook', event.currentTarget.value)}
value={formik.values.selectedBook}
grow
{...formFieldInputProps}
/>
</FormField>
<PrimaryButton type="submit">Submit</PrimaryButton>
</VStack>
</form>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,34 @@
import React from 'react';

import {useFormik} from 'formik';
import * as yup from 'yup';
import {object, SchemaOf, string} from 'yup';

import {TextInput} from '@workday/canvas-kit-preview-react/text-input';
import {HStack, VStack} from '@workday/canvas-kit-react/layout';
import {TertiaryButton, PrimaryButton} from '@workday/canvas-kit-react/button';
import {visibleIcon, invisibleIcon} from '@workday/canvas-system-icons-web';
import {useUniqueId} from '@workday/canvas-kit-react/common';

export const TextInputWithFormik = () => {
const passwordMinimum = 8;
const passwordHint = `Password should be of minimum ${passwordMinimum} characters length`;
const emailRequired = 'Email is required';
const passwordRequired = 'Password is required';
interface LoginSchema {
email: string;
password: string;
}

const validationSchema = yup.object({
email: yup
.string()
.email('Enter a valid email')
.required(emailRequired),
password: yup
.string()
.min(passwordMinimum, passwordHint)
.required(passwordRequired),
});
const passwordMinimum = 8;
const passwordHint = `Password should be of minimum ${passwordMinimum} characters length`;
const emailRequired = 'Email is required';
const passwordRequired = 'Password is required';

const validationSchema: SchemaOf<LoginSchema> = object({
email: string()
.email('Enter a valid email')
.required(emailRequired),
password: string()
.min(passwordMinimum, passwordHint)
.required(passwordRequired),
});

export const TextInputWithFormik = () => {
const passwordRef = React.useRef<HTMLInputElement>(null);
const [showPassword, setShowPassword] = React.useState(false);
const passwordId = useUniqueId();
Expand All @@ -37,8 +40,7 @@ export const TextInputWithFormik = () => {
},
validationSchema: validationSchema,
onSubmit: values => {
passwordRef.current.type = 'password';

setShowPassword(false);
// Send data to server
setTimeout(() => {
alert(JSON.stringify(values, null, 2));
Expand Down Expand Up @@ -90,7 +92,7 @@ export const TextInputWithFormik = () => {
aria-controls={`input-${passwordId}`}
onClick={() => {
setShowPassword(state => !state);
passwordRef.current.focus();
passwordRef.current?.focus();
}}
/>
</HStack>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import React from 'react';

import {useForm, FieldErrorsImpl} from 'react-hook-form';
import {object, SchemaOf, string} from 'yup';

import {TextInput} from '@workday/canvas-kit-preview-react/text-input';
import {HStack, VStack} from '@workday/canvas-kit-react/layout';
import {TertiaryButton, PrimaryButton} from '@workday/canvas-kit-react/button';
import {visibleIcon, invisibleIcon} from '@workday/canvas-system-icons-web';
import {useUniqueId} from '@workday/canvas-kit-react/common';

type YupValidationResolver = <T extends {}>(
validationSchema: SchemaOf<T>
) => (data: T) => Promise<{values: T; errors: {}} | {values: {}; errors: FieldErrorsImpl<T>}>;

const useYupValidationResolver: YupValidationResolver = validationSchema => {
return React.useCallback(
async data => {
try {
const values = await validationSchema.validate(data, {abortEarly: false});
return {values, errors: {}};
} catch (errors) {
return {
values: {},
errors: errors.inner.reduce(
(allErrors, currentError) => ({
...allErrors,
[currentError.path]: {
type: currentError.type ?? 'validation',
message: currentError.message,
},
}),
{}
),
};
}
},
[validationSchema]
);
};

interface LoginSchema {
email: string;
password: string;
}

const passwordMinimum = 8;
const passwordHint = `Password should be of minimum ${passwordMinimum} characters length`;
const emailRequired = 'Email is required';
const passwordRequired = 'Password is required';

const validationSchema: SchemaOf<LoginSchema> = object({
email: string()
.email('Enter a valid email')
.required(emailRequired),
password: string()
.min(passwordMinimum, passwordHint)
.required(passwordRequired),
});

export const TextInputWithReactHookForm = () => {
const {
handleSubmit,
register,
formState: {errors},
} = useForm<LoginSchema>({
defaultValues: {
email: '[email protected]',
password: 'foobarbaz',
},
resolver: useYupValidationResolver(validationSchema),
mode: 'onTouched',
});

const onSubmit = handleSubmit(values => {
setShowPassword(false);
// Send data to server
setTimeout(() => {
alert(JSON.stringify(values, null, 2));
}, 0);
});

const [showPassword, setShowPassword] = React.useState(false);
const passwordId = useUniqueId();
const passwordRef = React.useRef<HTMLInputElement | null>(null);
const {ref: passwordCallbackRef, ...passwordRegistration} = register('password');
const combinePasswordRef = (ref: HTMLInputElement | null) => {
passwordCallbackRef(ref);
passwordRef.current = ref;
};

return (
<form onSubmit={onSubmit} action=".">
<VStack spacing="xs" alignItems="flex-start">
<TextInput orientation="vertical" isRequired={true} hasError={!!errors.email}>
<TextInput.Label>Email</TextInput.Label>
<TextInput.Field
{...register('email')}
autoComplete="username"
placeholder="[email protected]"
/>
<TextInput.Hint>{errors.email?.message}</TextInput.Hint>
</TextInput>
<TextInput
orientation="vertical"
id={passwordId}
isRequired={true}
hasError={!!errors.password}
>
<TextInput.Label>Password</TextInput.Label>
<HStack spacing="xxs">
<TextInput.Field
{...passwordRegistration}
type={showPassword ? 'text' : 'password'}
autoComplete="current-password"
spellCheck={false}
ref={combinePasswordRef}
/>
<TertiaryButton
type="button"
icon={showPassword ? invisibleIcon : visibleIcon}
aria-label={showPassword ? 'Hide Password' : 'Show Password'}
aria-controls={`input-${passwordId}`}
onClick={() => {
setShowPassword(state => !state);
passwordRef.current?.focus();
}}
/>
</HStack>
<TextInput.Hint>{errors.password?.message || passwordHint}</TextInput.Hint>
</TextInput>

<PrimaryButton type="submit">Submit</PrimaryButton>
</VStack>
</form>
);
};
3 changes: 2 additions & 1 deletion modules/preview-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"@workday/canvas-accent-icons-web": "^3.0.0",
"@workday/canvas-kit-labs-react": "^7.3.15",
"formik": "^2.2.9",
"yup": "^0.31.1"
"react-hook-form": "7.36.1",
"yup": "^0.32.11"
}
}
Loading

0 comments on commit a5a53bb

Please sign in to comment.