Skip to content

Commit bd45ce7

Browse files
ethansharM-i-k-e-l
andauthored
Infra/masked input to ts (#1976)
* Rename file to tsx * Migrate MaskedInput to TS and remove usaged of legacy TextField component * Create a MaskedInputMigrator * PR fixes and I changed the component to be uncontrolled because of the formatter prop * Update src/components/maskedInput/new.tsx Co-authored-by: Miki Leib <[email protected]> * Update src/components/maskedInput/new.tsx Co-authored-by: Miki Leib <[email protected]> * Migrate MaskedInput screen to TS * Add pin code example to MaskedInput screen * Fix PR comments Co-authored-by: Miki Leib <[email protected]>
1 parent d74f5a6 commit bd45ce7

File tree

5 files changed

+203
-20
lines changed

5 files changed

+203
-20
lines changed

demo/src/screens/componentScreens/MaskedInputScreen.js renamed to demo/src/screens/componentScreens/MaskedInputScreen.tsx

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,30 @@
11
import _ from 'lodash';
22
import React, {Component} from 'react';
33
import {ScrollView, StyleSheet} from 'react-native';
4-
import {Typography, View, Text, MaskedInput, Button} from 'react-native-ui-lib'; //eslint-disable-line
4+
import {Typography, View, Text, MaskedInput, Button, Colors} from 'react-native-ui-lib'; //eslint-disable-line
55

66
export default class MaskedInputScreen extends Component {
7-
constructor(props) {
8-
super(props);
9-
10-
this.state = {
11-
error: '',
12-
timeValue: '15'
13-
};
14-
}
7+
minInput = React.createRef<any>();
8+
priceInput = React.createRef<any>();
9+
pinCodeInput = React.createRef<any>();
10+
state = {
11+
error: '',
12+
timeValue: '15'
13+
};
1514

1615
componentDidMount() {
1716
setTimeout(() => {
18-
this.minput.focus();
17+
this.minInput.current.focus();
1918
}, 500);
2019
}
2120

2221
clearInputs = () => {
23-
this.minput.clear();
24-
this.priceInput.clear();
22+
this.minInput.current.clear();
23+
this.priceInput.current.clear();
24+
this.pinCodeInput.current.clear();
2525
};
2626

27-
renderTimeText(value) {
27+
renderTimeText(value: string) {
2828
const paddedValue = _.padStart(value, 4, '0');
2929
const hours = paddedValue.substr(0, 2);
3030
const minutes = paddedValue.substr(2, 2);
@@ -39,7 +39,7 @@ export default class MaskedInputScreen extends Component {
3939
);
4040
}
4141

42-
renderPrice(value) {
42+
renderPrice(value: string) {
4343
const hasValue = Boolean(value && value.length > 0);
4444
return (
4545
<View row center>
@@ -56,6 +56,20 @@ export default class MaskedInputScreen extends Component {
5656
);
5757
}
5858

59+
renderPINCode = (value = '') => {
60+
return (
61+
<View row centerH>
62+
{_.times(4, i => {
63+
return (
64+
<View key={i} marginR-s3 center style={styles.pinCodeSquare}>
65+
<Text h1>{value[i]}</Text>
66+
</View>
67+
);
68+
})}
69+
</View>
70+
);
71+
};
72+
5973
render() {
6074
const {timeValue} = this.state;
6175

@@ -70,22 +84,39 @@ export default class MaskedInputScreen extends Component {
7084
Time Format
7185
</Text>
7286
<MaskedInput
73-
ref={r => (this.minput = r)}
87+
migrate
88+
ref={this.minInput}
7489
renderMaskedText={this.renderTimeText}
90+
formatter={(value: string) => value?.replace(/\D/g, '')}
7591
keyboardType={'numeric'}
7692
maxLength={4}
77-
value={timeValue}
78-
onChangeText={value => this.setState({timeValue: value})}
93+
initialValue={timeValue}
94+
// onChangeText={value => this.setState({timeValue: value})}
7995
/>
8096

8197
<Text text70 marginT-40>
8298
Price/Discount
8399
</Text>
84100
<MaskedInput
85-
ref={r => (this.priceInput = r)}
101+
migrate
102+
ref={this.priceInput}
86103
renderMaskedText={this.renderPrice}
104+
formatter={(value: string) => value?.replace(/\D/g, '')}
87105
keyboardType={'numeric'}
88106
/>
107+
108+
<Text text70 marginT-s5 marginB-s4>
109+
PIN Code
110+
</Text>
111+
<MaskedInput
112+
migrate
113+
maxLength={4}
114+
ref={this.pinCodeInput}
115+
renderMaskedText={this.renderPINCode}
116+
formatter={(value: string) => value?.replace(/\D/g, '')}
117+
keyboardType={'numeric'}
118+
/>
119+
89120
<View centerH marginT-100>
90121
<Button label="Clear All" onPress={this.clearInputs}/>
91122
</View>
@@ -107,5 +138,12 @@ const styles = StyleSheet.create({
107138
header: {
108139
...Typography.text60,
109140
marginVertical: 20
141+
},
142+
pinCodeSquare: {
143+
width: 50,
144+
height: 70,
145+
borderWidth: 2,
146+
borderColor: Colors.grey30,
147+
borderRadius: 4
110148
}
111149
});

src/components/maskedInput/index.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import React, {forwardRef} from 'react';
2+
// @ts-expect-error
3+
import MaskedInputOld from './old';
4+
import MaskedInputNew, {MaskedInputProps} from './new';
5+
6+
function MaskedInputMigrator(props: any, refToForward: any) {
7+
const {migrate, ...others} = props;
8+
9+
if (migrate) {
10+
return <MaskedInputNew {...others} ref={refToForward}/>;
11+
} else {
12+
return <MaskedInputOld {...others} ref={refToForward}/>;
13+
}
14+
}
15+
16+
export {MaskedInputProps};
17+
export default forwardRef(MaskedInputMigrator);

src/components/maskedInput/new.tsx

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import React, {
2+
useCallback,
3+
useEffect,
4+
useRef,
5+
useState,
6+
useImperativeHandle,
7+
forwardRef,
8+
ReactElement,
9+
ForwardedRef
10+
} from 'react';
11+
import _ from 'lodash';
12+
import {StyleSheet, Keyboard, TextInput, TextInputProps, StyleProp, ViewStyle} from 'react-native';
13+
import View from '../view';
14+
import Text from '../text';
15+
import TouchableOpacity from '../touchableOpacity';
16+
17+
export interface MaskedInputProps extends Omit<TextInputProps, 'value'> {
18+
/**
19+
* Initial value to pass to masked input
20+
*/
21+
initialValue?: string;
22+
/**
23+
* callback for rendering the custom input out of the value returns from the actual input
24+
*/
25+
renderMaskedText?: ReactElement;
26+
/**
27+
* Custom formatter for the input value
28+
*/
29+
formatter?: (value?: string) => string | undefined;
30+
/**
31+
* container style for the masked input container
32+
*/
33+
containerStyle: StyleProp<ViewStyle>;
34+
}
35+
36+
/**
37+
* @description: Mask Input to create custom looking inputs with custom formats
38+
* @gif: https://camo.githubusercontent.com/61eedb65e968845d5eac713dcd21a69691571fb1/68747470733a2f2f6d656469612e67697068792e636f6d2f6d656469612f4b5a5a7446666f486f454b334b2f67697068792e676966
39+
* @example: https://github.com/wix/react-native-ui-lib/blob/master/demo/src/screens/componentScreens/MaskedInputScreen.js
40+
*/
41+
function MaskedInput(props: MaskedInputProps, ref: ForwardedRef<any>) {
42+
const {initialValue, formatter = _.identity, containerStyle, renderMaskedText, onChangeText, ...others} = props;
43+
const [value, setValue] = useState(initialValue);
44+
const inputRef = useRef<TextInput>();
45+
const keyboardDidHideListener = useRef<any>();
46+
47+
useImperativeHandle(ref, () => {
48+
return {
49+
isFocused: () => inputRef.current?.isFocused(),
50+
focus,
51+
blur: () => inputRef.current?.blur(),
52+
clear: () => {
53+
inputRef.current?.clear();
54+
setValue('');
55+
// NOTE: This fixes an RN issue - when triggering imperative clear method, it doesn't call onChangeText
56+
onChangeText?.('');
57+
}
58+
};
59+
});
60+
61+
useEffect(() => {
62+
if (initialValue !== value) {
63+
setValue(initialValue);
64+
}
65+
}, [initialValue]);
66+
67+
useEffect(() => {
68+
keyboardDidHideListener.current = Keyboard.addListener('keyboardDidHide', () => {
69+
if (inputRef.current?.isFocused()) {
70+
inputRef.current?.blur();
71+
}
72+
});
73+
74+
return () => keyboardDidHideListener.current.remove();
75+
}, []);
76+
77+
const _onChangeText = useCallback((value: string) => {
78+
const formattedValue = formatter(value) ?? '';
79+
setValue(formattedValue);
80+
onChangeText?.(formattedValue);
81+
},
82+
[onChangeText, formatter]);
83+
84+
const focus = useCallback(() => {
85+
inputRef.current?.focus();
86+
}, []);
87+
88+
const _renderMaskedText = () => {
89+
if (_.isFunction(renderMaskedText)) {
90+
return renderMaskedText(value);
91+
}
92+
return <Text>{value}</Text>;
93+
};
94+
95+
return (
96+
<TouchableOpacity style={containerStyle} activeOpacity={1} onPress={focus}>
97+
<TextInput
98+
{...others}
99+
value={value}
100+
// @ts-expect-error
101+
ref={inputRef}
102+
style={styles.hiddenInput}
103+
enableErrors={false}
104+
hideUnderline
105+
placeholder=""
106+
caretHidden
107+
multiline={false}
108+
onChangeText={_onChangeText}
109+
/>
110+
<View style={styles.maskedInputWrapper}>{_renderMaskedText()}</View>
111+
</TouchableOpacity>
112+
);
113+
}
114+
115+
const styles = StyleSheet.create({
116+
hiddenInput: {
117+
...StyleSheet.absoluteFillObject,
118+
zIndex: 1,
119+
color: 'transparent',
120+
backgroundColor: 'transparent',
121+
height: undefined
122+
},
123+
maskedInputWrapper: {
124+
zIndex: 0
125+
}
126+
});
127+
128+
MaskedInput.displayName = 'MaskedInput';
129+
export default forwardRef(MaskedInput);

src/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,7 @@ export {default as Icon, IconProps} from './components/icon';
8686
export {default as Image, ImageProps} from './components/image';
8787
export {default as ListItem, ListItemProps} from './components/listItem';
8888
export {default as LoaderScreen, LoaderScreenProps} from './components/loaderScreen';
89-
// @ts-expect-error
90-
export {default as MaskedInput} from './components/maskedInput';
89+
export {default as MaskedInput, MaskedInputProps} from './components/maskedInput';
9190
export {default as Modal, ModalProps, ModalTopBarProps} from './components/modal';
9291
export {default as Overlay, OverlayTypes} from './components/overlay';
9392
export {default as PageControl, PageControlProps} from './components/pageControl';

0 commit comments

Comments
 (0)