Skip to content

Commit 8f30115

Browse files
Release v0.1.0 (GH-12)
2 parents f7b1e91 + f3c2793 commit 8f30115

File tree

15 files changed

+488
-55
lines changed

15 files changed

+488
-55
lines changed

README.md

+18
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,24 @@ This comprehensive toolkit features custom hooks and utility functions tailored
1010
and validation. It supports international standards, making it suitable for phone number processing applications across
1111
different countries and regions.
1212

13+
## Usage
14+
15+
This library can be used to build a phone number input component with a country selector for React applications. As well
16+
as to parse the phone metadata, validate phone numbers, format raw phone numbers into a more readable format and the
17+
opposite. You can use the [development](./development) to test and develop your own components.
18+
19+
```jsx
20+
import {getFormattedNumber, getMetadata, parsePhoneNumber, useMask} from "react-phone-hooks";
21+
22+
getMetadata("440201111111"); // ["gb", "United Kingdom", "44", "+44 (..) ... ....."]
23+
getFormattedNumber("440201111111", "+44 (..) ... ....."); // +44 (02) 011 11111
24+
parsePhoneNumber("+44 (02) 011 11111"); // {countryCode: 44, areaCode: "02", phoneNumber: "01111111", isoCode: "gb"}
25+
26+
const PhoneInput = (props) => {
27+
return <input {...useMask("+1 (...) ... ....")} {...props}/>
28+
}
29+
```
30+
1331
## Contribute
1432

1533
Any contribution is welcome. Don't hesitate to open an issue or discussion if you have questions about your project's

development/package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55
"dependencies": {
66
"@emotion/react": "^11.11.1",
77
"@emotion/styled": "^11.11.0",
8+
"@mui/base": "^5.0.0-beta.30",
9+
"@mui/joy": "^5.0.0-beta.21",
810
"@mui/material": "^5.14.18",
911
"@types/react": "^18.2.0",
1012
"@types/react-dom": "^18.2.0",
1113
"antd": "^5.8.3",
1214
"react": "^18.2.0",
1315
"react-dom": "^18.2.0",
14-
"react-phone-hooks": "file:../react-phone-hooks-0.1.0.tgz",
16+
"react-phone-hooks": "0.1.0-beta.1",
1517
"react-scripts": "^5.0.1",
1618
"typescript": "^4.9.5"
1719
},

development/src/MuiDemo.tsx

+6
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {createTheme, ThemeProvider} from "@mui/material/styles";
33
import {Button, Container, CssBaseline, TextField} from "@mui/material";
44

55
import PhoneInput from "./mui-phone";
6+
import BasePhoneInput from "./mui-phone/base";
67

78
const Demo = () => {
89
const [value, setValue] = useState({});
@@ -37,6 +38,11 @@ const Demo = () => {
3738
onChange={(e) => setValue(e as any)}
3839
/>
3940
<TextField variant="filled" style={{marginTop: "1.5rem"}}/>
41+
<BasePhoneInput
42+
error={error}
43+
style={{marginTop: "1.5rem"}}
44+
onChange={(e) => setValue(e as any)}
45+
/>
4046
<div style={{display: "flex", gap: 24, marginTop: "1rem"}}>
4147
<Button type="reset">Reset Value</Button>
4248
<Button onClick={handleThemeChange}>Change Theme</Button>

development/src/ant-phone/index.tsx

+16-2
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,11 @@ const PhoneInput = forwardRef(({
4848
onChange: handleChange = () => null,
4949
onKeyDown: handleKeyDown = () => null,
5050
...antInputProps
51-
}: PhoneInputProps, ref: any) => {
51+
}: PhoneInputProps, forwardedRef: any) => {
5252
const formInstance = useFormInstance();
5353
const formContext = useContext(FormContext);
54+
const inputRef = useRef<any>(null);
55+
const selectedRef = useRef<boolean>(false);
5456
const initiatedRef = useRef<boolean>(false);
5557
const [query, setQuery] = useState<string>("");
5658
const [minWidth, setMinWidth] = useState<number>(0);
@@ -102,7 +104,8 @@ const PhoneInput = forwardRef(({
102104
}, [handleKeyDown, onKeyDownMaskHandler])
103105

104106
const onChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
105-
const formattedNumber = getFormattedNumber(event.target.value, pattern);
107+
const formattedNumber = selectedRef.current ? event.target.value : getFormattedNumber(event.target.value, pattern);
108+
selectedRef.current = false;
106109
const phoneMetadata = parsePhoneNumber(formattedNumber, countriesList);
107110
setCountryCode(phoneMetadata.isoCode as any);
108111
setValue(formattedNumber);
@@ -119,6 +122,13 @@ const PhoneInput = forwardRef(({
119122
handleMount(value);
120123
}, [handleMount, setFieldValue])
121124

125+
const ref = useCallback((node: any) => {
126+
[forwardedRef, inputRef].forEach((ref) => {
127+
if (typeof ref === "function") ref(node);
128+
else if (ref != null) ref.current = node;
129+
})
130+
}, [forwardedRef])
131+
122132
useEffect(() => {
123133
if (initiatedRef.current) return;
124134
initiatedRef.current = true;
@@ -147,6 +157,10 @@ const PhoneInput = forwardRef(({
147157
setFieldValue({...phoneMetadata, valid: (strict: boolean) => checkValidity(phoneMetadata, strict)});
148158
setCountryCode(selectedCountryCode);
149159
setValue(formattedNumber);
160+
selectedRef.current = true;
161+
const nativeInputValueSetter = (Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value") as any).set;
162+
nativeInputValueSetter.call(inputRef.current.input, formattedNumber);
163+
inputRef.current.input.dispatchEvent(new Event("change", {bubbles: true}));
150164
}}
151165
optionLabelProp="label"
152166
dropdownStyle={{minWidth}}
+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import {ChangeEvent, forwardRef, KeyboardEvent, useCallback, useEffect, useRef, useState} from "react";
2+
import {Input as BaseInput, InputProps} from "@mui/base/Input";
3+
4+
import {
5+
checkValidity,
6+
getDefaultISO2Code,
7+
getFormattedNumber,
8+
getRawValue,
9+
parsePhoneNumber,
10+
useMask,
11+
usePhone,
12+
} from "../../phone-hooks";
13+
14+
import {injectMergedStyles} from "./styles";
15+
import {PhoneInputProps, PhoneNumber} from "./types";
16+
17+
injectMergedStyles();
18+
19+
const Input = forwardRef<HTMLInputElement, InputProps>(({slotProps, ...props}, ref) => {
20+
const defaultInputProps = (slotProps?.input as any)?.className ? {} : {outline: "none", border: "none", paddingLeft: 5, width: "calc(100% - 30px)"};
21+
const defaultRootProps = (slotProps?.root as any)?.className ? {} : {background: "white", color: "black", paddingLeft: 5};
22+
return (
23+
<BaseInput
24+
ref={ref}
25+
{...props}
26+
slotProps={{
27+
...slotProps,
28+
root: {
29+
...slotProps?.root,
30+
style: {
31+
...defaultRootProps,
32+
...(slotProps?.root as any)?.style,
33+
alignItems: "center",
34+
display: "flex",
35+
}
36+
},
37+
input: {
38+
...slotProps?.input,
39+
style: {
40+
...defaultInputProps,
41+
...(slotProps?.input as any)?.style,
42+
}
43+
}
44+
}}
45+
/>
46+
)
47+
})
48+
49+
const PhoneInput = forwardRef(({
50+
value: initialValue = "",
51+
country = getDefaultISO2Code(),
52+
onlyCountries = [],
53+
excludeCountries = [],
54+
preferredCountries = [],
55+
onMount: handleMount = () => null,
56+
onInput: handleInput = () => null,
57+
onChange: handleChange = () => null,
58+
onKeyDown: handleKeyDown = () => null,
59+
...muiInputProps
60+
}: PhoneInputProps, ref: any) => {
61+
const initiatedRef = useRef<boolean>(false);
62+
const [countryCode, setCountryCode] = useState<string>(country);
63+
64+
const {
65+
value,
66+
pattern,
67+
metadata,
68+
setValue,
69+
countriesList,
70+
} = usePhone({
71+
country,
72+
countryCode,
73+
initialValue,
74+
onlyCountries,
75+
excludeCountries,
76+
preferredCountries,
77+
});
78+
79+
const {
80+
onInput: onInputMaskHandler,
81+
onKeyDown: onKeyDownMaskHandler,
82+
} = useMask(pattern);
83+
84+
const onKeyDown = useCallback((event: KeyboardEvent<HTMLInputElement>) => {
85+
onKeyDownMaskHandler(event);
86+
handleKeyDown(event);
87+
}, [handleKeyDown, onKeyDownMaskHandler])
88+
89+
const onChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
90+
const formattedNumber = getFormattedNumber(event.target.value, pattern);
91+
const phoneMetadata = parsePhoneNumber(formattedNumber, countriesList);
92+
setCountryCode(phoneMetadata.isoCode as any);
93+
setValue(formattedNumber);
94+
handleChange({...phoneMetadata, valid: (strict: boolean) => checkValidity(phoneMetadata, strict)}, event);
95+
}, [countriesList, handleChange, pattern, setValue])
96+
97+
const onInput = useCallback((event: ChangeEvent<HTMLInputElement>) => {
98+
onInputMaskHandler(event);
99+
handleInput(event);
100+
}, [handleInput, onInputMaskHandler])
101+
102+
const onMount = useCallback((value: PhoneNumber) => {
103+
handleMount(value);
104+
}, [handleMount])
105+
106+
useEffect(() => {
107+
if (initiatedRef.current) return;
108+
initiatedRef.current = true;
109+
let initialValue = getRawValue(value);
110+
if (!initialValue.startsWith(metadata?.[2] as string)) {
111+
initialValue = metadata?.[2] as string;
112+
}
113+
const formattedNumber = getFormattedNumber(initialValue, pattern);
114+
const phoneMetadata = parsePhoneNumber(formattedNumber, countriesList);
115+
onMount({...phoneMetadata, valid: (strict: boolean) => checkValidity(phoneMetadata, strict)});
116+
setCountryCode(phoneMetadata.isoCode as any);
117+
setValue(formattedNumber);
118+
}, [countriesList, metadata, onMount, pattern, setValue, value])
119+
120+
return (
121+
<Input
122+
ref={ref}
123+
type="tel"
124+
value={value}
125+
onInput={onInput}
126+
onChange={onChange}
127+
onKeyDown={onKeyDown}
128+
startAdornment={<div className={`flag ${countryCode}`}/>}
129+
{...(muiInputProps as any)}
130+
/>
131+
)
132+
})
133+
134+
export default PhoneInput;
+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import {injectStyles, jsonToCss} from "react-phone-hooks/styles";
2+
import commonStyles from "react-phone-hooks/stylesheet.json";
3+
4+
export const injectMergedStyles = () => injectStyles(jsonToCss(commonStyles));
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import {ChangeEvent, KeyboardEvent} from "react";
2+
import types from "react-phone-hooks/types";
3+
import {InputProps} from "@mui/base/Input";
4+
5+
export type PhoneNumber = types.PhoneNumber;
6+
7+
export interface PhoneInputProps extends Omit<InputProps, "onChange"> {
8+
value?: PhoneNumber | string;
9+
10+
country?: string;
11+
12+
onlyCountries?: string[];
13+
14+
excludeCountries?: string[];
15+
16+
preferredCountries?: string[];
17+
18+
onMount?(value: PhoneNumber): void;
19+
20+
onInput?(event: ChangeEvent<HTMLInputElement>): void;
21+
22+
onKeyDown?(event: KeyboardEvent<HTMLInputElement>): void;
23+
24+
onChange?(value: PhoneNumber, event: ChangeEvent<HTMLInputElement>): void;
25+
}

0 commit comments

Comments
 (0)