Skip to content

Commit

Permalink
fix: stop spinner when email validation by Promise fails (#176)
Browse files Browse the repository at this point in the history
* add async validate email

* fix: stop spinner when email validation by Promise fails

---------

Co-authored-by: tom <[email protected]>
  • Loading branch information
hwonda and thomasJang authored Mar 20, 2024
1 parent faf323d commit 9363f2b
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 32 deletions.
1 change: 1 addition & 0 deletions components/Nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
79 changes: 79 additions & 0 deletions examples/AsyncValidateExample.tsx
Original file line number Diff line number Diff line change
@@ -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<string[]>([]);
const [focused, setFocused] = React.useState(false);

return (
<Container>
<form>
<h3>Email</h3>
<ReactMultiEmail
placeholder='Input your email'
emails={emails}
onChange={(_emails: string[]) => {
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 (
<div data-tag key={index}>
<div data-tag-item>{email}</div>
<span data-tag-handle onClick={() => removeEmail(index)}>
×
</span>
</div>
);
}}
onChangeInput={value => {
console.log(value);
}}
validateEmail={async email => {
await delay(100);
return isEmail(email);
}}
spinner={() => <Spinner>validating...</Spinner>}
/>
<br />
<h4>react-multi-email value</h4>
<h3>Focused: {focused ? 'true' : 'false'}</h3>
<p>{emails.join(', ') || 'empty'}</p>

<Button onClick={() => setEmails(['test', 'tt', '[email protected]'])}>
{`setEmails("['test', 'tt', '[email protected]']")`}
</Button>
<Button onClick={() => setEmails([])}>{`setEmails("[]")`}</Button>
</form>
</Container>
);
}

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;
2 changes: 1 addition & 1 deletion examples/BasicExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Button } from 'antd';

interface Props {}

function BasicExample(props: Props) {
function BasicExample(_props: Props) {
const [emails, setEmails] = React.useState<string[]>([]);
const [focused, setFocused] = React.useState(false);

Expand Down
23 changes: 23 additions & 0 deletions pages/asyncValidate.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<PageContainer>
<Container>
<div>
<h2>AsyncValidateExample</h2>
<AsyncValidateExample />
</div>
</Container>
</PageContainer>
);
};

const PageContainer = styled(BodyRoot)``;

export default Page;
39 changes: 27 additions & 12 deletions react-multi-email/ReactMultiEmail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ export interface IReactMultiEmailProps {
allowDuplicate?: boolean;
}


export function ReactMultiEmail(props: IReactMultiEmailProps) {
const {
id,
Expand Down Expand Up @@ -68,16 +67,9 @@ export function ReactMultiEmail(props: IReactMultiEmailProps) {

const [focused, setFocused] = React.useState(false);
const [emails, setEmails] = React.useState<string[]>([]);
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[] = [];
Expand Down Expand Up @@ -168,10 +160,10 @@ export function ReactMultiEmail(props: IReactMultiEmailProps) {
setSpinning(true);
if ((await validateEmail?.(value)) === true) {
addEmails(value);
setSpinning(false);
} else {
inputValue = value;
}
setSpinning(false);
}
} else {
inputValue = value;
Expand Down Expand Up @@ -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 (
<div
Expand Down
39 changes: 20 additions & 19 deletions test/emails.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { cleanup, render } from '@testing-library/react';
import { ReactMultiEmail } from '../react-multi-email';
import React from 'react';
import { act } from 'react-dom/test-utils';
import { sleep } from './utils/sleep';

afterEach(cleanup);

Expand Down Expand Up @@ -75,28 +77,27 @@ describe('ReactMultEmail emails TEST', () => {
});
});


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(
<ReactMultiEmail
validateEmail={(mockValidateEmailFunc)}
emails={['abc@gmail','abc','def', '[email protected]']}
getLabel={(email, index) => {
return (
<div data-tag key={index}>
<div data-tag-item>{email}</div>
</div>
);
}}
/>,
);

await act(async () => {
render(
<ReactMultiEmail
validateEmail={email => regex.test(email)}
emails={['abc@gmail', 'abc', 'def', '[email protected]']}
getLabel={(email, index) => {
return (
<div data-tag key={index}>
<div data-tag-item>{email}</div>
</div>
);
}}
/>,
);
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);
});
});
5 changes: 5 additions & 0 deletions test/utils/sleep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export function sleep(period: number) {
return new Promise((resolve, _reject) => {
setTimeout(resolve, period);
});
}

0 comments on commit 9363f2b

Please sign in to comment.