From 5743a0ad807d1d344a6702b3571159db0d34a6aa Mon Sep 17 00:00:00 2001 From: tom Date: Tue, 19 Mar 2024 10:32:37 +0900 Subject: [PATCH] add async validate email --- components/Nav.tsx | 1 + examples/AsyncValidateExample.tsx | 79 +++++++++++++++++++++++++++ examples/BasicExample.tsx | 2 +- pages/asyncValidate.tsx | 23 ++++++++ react-multi-email/ReactMultiEmail.tsx | 37 +++++++++---- test/emails.test.tsx | 39 ++++++------- test/utils/sleep.ts | 5 ++ 7 files changed, 155 insertions(+), 31 deletions(-) create mode 100644 examples/AsyncValidateExample.tsx create mode 100644 pages/asyncValidate.tsx create mode 100644 test/utils/sleep.ts diff --git a/components/Nav.tsx b/components/Nav.tsx index dd0651d..c58b9f1 100644 --- a/components/Nav.tsx +++ b/components/Nav.tsx @@ -25,6 +25,7 @@ function Nav(_props: Props) { { label: `Basic`, key: '/' }, { label: `CustomizeStyle`, key: '/customizeStyle' }, { label: `DisableOnBlurValidation`, key: '/disableOnBlurValidation' }, + { label: `AsyncValidate`, key: '/asyncValidate' }, ]} onTabClick={async activeKey => { await router.push(activeKey); diff --git a/examples/AsyncValidateExample.tsx b/examples/AsyncValidateExample.tsx new file mode 100644 index 0000000..bde2fd8 --- /dev/null +++ b/examples/AsyncValidateExample.tsx @@ -0,0 +1,79 @@ +import * as React from 'react'; +import styled from '@emotion/styled'; +import { isEmail, ReactMultiEmail } from '../react-multi-email'; +import { Button } from 'antd'; + +interface Props {} + +export const delay = (ms: number) => new Promise(res => setTimeout(() => res(undefined), ms)); + +function AsyncValidateExample(_props: Props) { + const [emails, setEmails] = React.useState([]); + const [focused, setFocused] = React.useState(false); + + return ( + +
+

Email

+ { + setEmails(_emails); + }} + autoFocus={true} + onFocus={() => setFocused(true)} + onBlur={() => setFocused(false)} + onKeyDown={evt => { + console.log(evt); + }} + onKeyUp={evt => { + console.log(evt); + }} + getLabel={(email, index, removeEmail) => { + return ( +
+
{email}
+ removeEmail(index)}> + × + +
+ ); + }} + onChangeInput={value => { + console.log(value); + }} + validateEmail={async email => { + await delay(100); + return isEmail(email); + }} + spinner={() => validating...} + /> +
+

react-multi-email value

+

Focused: {focused ? 'true' : 'false'}

+

{emails.join(', ') || 'empty'}

+ + + + +
+ ); +} + +const Container = styled.div` + font-size: 13px; +`; +const Spinner = styled.div` + position: absolute; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background-color: rgba(255, 255, 255, 0.8); +`; + +export default AsyncValidateExample; diff --git a/examples/BasicExample.tsx b/examples/BasicExample.tsx index b132b4a..78586cc 100644 --- a/examples/BasicExample.tsx +++ b/examples/BasicExample.tsx @@ -5,7 +5,7 @@ import { Button } from 'antd'; interface Props {} -function BasicExample(props: Props) { +function BasicExample(_props: Props) { const [emails, setEmails] = React.useState([]); const [focused, setFocused] = React.useState(false); diff --git a/pages/asyncValidate.tsx b/pages/asyncValidate.tsx new file mode 100644 index 0000000..cf7590b --- /dev/null +++ b/pages/asyncValidate.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import type { NextPage } from 'next'; +import { Container } from '../components/Layouts'; +import styled from '@emotion/styled'; +import BodyRoot from '../components/BodyRoot'; +import AsyncValidateExample from '../examples/AsyncValidateExample'; + +const Page: NextPage = () => { + return ( + + +
+

AsyncValidateExample

+ +
+
+
+ ); +}; + +const PageContainer = styled(BodyRoot)``; + +export default Page; diff --git a/react-multi-email/ReactMultiEmail.tsx b/react-multi-email/ReactMultiEmail.tsx index ec0919f..078557d 100644 --- a/react-multi-email/ReactMultiEmail.tsx +++ b/react-multi-email/ReactMultiEmail.tsx @@ -35,7 +35,6 @@ export interface IReactMultiEmailProps { allowDuplicate?: boolean; } - export function ReactMultiEmail(props: IReactMultiEmailProps) { const { id, @@ -68,16 +67,9 @@ export function ReactMultiEmail(props: IReactMultiEmailProps) { const [focused, setFocused] = React.useState(false); const [emails, setEmails] = React.useState([]); - const [inputValue, setInputValue] = React.useState(initialInputValue); + const [inputValue, setInputValue] = React.useState(''); const [spinning, setSpinning] = React.useState(false); - const initialEmailAddress = (emails?: string[]) => { - if (typeof emails === 'undefined') return []; - - const validEmails = emails.filter(email => validateEmail ? validateEmail(email) : isEmailFn(email)); - return validEmails; - }; - const findEmailAddress = React.useCallback( async (value: string, isEnter?: boolean) => { const validEmails: string[] = []; @@ -280,8 +272,31 @@ export function ReactMultiEmail(props: IReactMultiEmailProps) { }, [onFocus]); React.useEffect(() => { - setEmails(initialEmailAddress(props.emails)); - }, [props.emails]); + setInputValue(initialInputValue); + }, [initialInputValue]); + + React.useEffect(() => { + if (validateEmail) { + (async () => { + setSpinning(true); + + const validEmails: string[] = []; + for await (const email of props.emails ?? []) { + if (await validateEmail(email)) { + validEmails.push(email); + } + } + setEmails(validEmails); + + setSpinning(false); + })(); + } else { + const validEmails = props.emails?.filter(email => { + return isEmailFn(email); + }); + setEmails(validEmails ?? []); + } + }, [props.emails, validateEmail]); return (
{ }); }); - it('Emails with custom validation', async () => { - const regex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]/; - - const mockValidateEmailFunc = jest.fn().mockImplementation((email) => regex.test(email)); - - render( - { - return ( -
-
{email}
-
- ); - }} - />, - ); + + await act(async () => { + render( + regex.test(email)} + emails={['abc@gmail', 'abc', 'def', 'abc@def.com']} + getLabel={(email, index) => { + return ( +
+
{email}
+
+ ); + }} + />, + ); + await sleep(100); + }); const emailsWrapper = document.querySelector('.data-labels'); // 4 emails are passed to the component, but only 2 are valid based on the custom validation. expect(emailsWrapper?.childElementCount).toEqual(2); -}); \ No newline at end of file +}); diff --git a/test/utils/sleep.ts b/test/utils/sleep.ts new file mode 100644 index 0000000..8e023b1 --- /dev/null +++ b/test/utils/sleep.ts @@ -0,0 +1,5 @@ +export function sleep(period: number) { + return new Promise((resolve, _reject) => { + setTimeout(resolve, period); + }); +}