Skip to content

Infra/masked input to ts #1976

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
May 8, 2022
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
import _ from 'lodash';
import React, {Component} from 'react';
import {ScrollView, StyleSheet} from 'react-native';
import {Typography, View, Text, MaskedInput, Button} from 'react-native-ui-lib'; //eslint-disable-line
import {Typography, View, Text, MaskedInput, Button, Colors} from 'react-native-ui-lib'; //eslint-disable-line

export default class MaskedInputScreen extends Component {
constructor(props) {
super(props);

this.state = {
error: '',
timeValue: '15'
};
}
minInput = React.createRef<any>();
priceInput = React.createRef<any>();
pinCodeInput = React.createRef<any>();
state = {
error: '',
timeValue: '15'
};

componentDidMount() {
setTimeout(() => {
this.minput.focus();
this.minInput.current.focus();
}, 500);
}

clearInputs = () => {
this.minput.clear();
this.priceInput.clear();
this.minInput.current.clear();
this.priceInput.current.clear();
this.pinCodeInput.current.clear();
};

renderTimeText(value) {
renderTimeText(value: string) {
const paddedValue = _.padStart(value, 4, '0');
const hours = paddedValue.substr(0, 2);
const minutes = paddedValue.substr(2, 2);
Expand All @@ -39,7 +39,7 @@ export default class MaskedInputScreen extends Component {
);
}

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

renderPINCode = (value = '') => {
return (
<View row centerH>
{_.times(4, i => {
return (
<View key={i} marginR-s3 center style={styles.pinCodeSquare}>
<Text h1>{value[i]}</Text>
</View>
);
})}
</View>
);
};

render() {
const {timeValue} = this.state;

Expand All @@ -70,22 +84,39 @@ export default class MaskedInputScreen extends Component {
Time Format
</Text>
<MaskedInput
ref={r => (this.minput = r)}
migrate
ref={this.minInput}
renderMaskedText={this.renderTimeText}
formatter={(value: string) => value?.replace(/\D/g, '')}
keyboardType={'numeric'}
maxLength={4}
value={timeValue}
onChangeText={value => this.setState({timeValue: value})}
initialValue={timeValue}
// onChangeText={value => this.setState({timeValue: value})}
/>

<Text text70 marginT-40>
Price/Discount
</Text>
<MaskedInput
ref={r => (this.priceInput = r)}
migrate
ref={this.priceInput}
renderMaskedText={this.renderPrice}
formatter={(value: string) => value?.replace(/\D/g, '')}
keyboardType={'numeric'}
/>

<Text text70 marginT-s5 marginB-s4>
PIN Code
</Text>
<MaskedInput
migrate
maxLength={4}
ref={this.pinCodeInput}
renderMaskedText={this.renderPINCode}
formatter={(value: string) => value?.replace(/\D/g, '')}
keyboardType={'numeric'}
/>

<View centerH marginT-100>
<Button label="Clear All" onPress={this.clearInputs}/>
</View>
Expand All @@ -107,5 +138,12 @@ const styles = StyleSheet.create({
header: {
...Typography.text60,
marginVertical: 20
},
pinCodeSquare: {
width: 50,
height: 70,
borderWidth: 2,
borderColor: Colors.grey30,
borderRadius: 4
}
});
17 changes: 17 additions & 0 deletions src/components/maskedInput/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React, {forwardRef} from 'react';
// @ts-expect-error
import MaskedInputOld from './old';
import MaskedInputNew, {MaskedInputProps} from './new';

function MaskedInputMigrator(props: any, refToForward: any) {
const {migrate, ...others} = props;

if (migrate) {
return <MaskedInputNew {...others} ref={refToForward}/>;
} else {
return <MaskedInputOld {...others} ref={refToForward}/>;
}
}

export {MaskedInputProps};
export default forwardRef(MaskedInputMigrator);
129 changes: 129 additions & 0 deletions src/components/maskedInput/new.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import React, {
useCallback,
useEffect,
useRef,
useState,
useImperativeHandle,
forwardRef,
ReactElement,
ForwardedRef
} from 'react';
import _ from 'lodash';
import {StyleSheet, Keyboard, TextInput, TextInputProps, StyleProp, ViewStyle} from 'react-native';
import View from '../view';
import Text from '../text';
import TouchableOpacity from '../touchableOpacity';

export interface MaskedInputProps extends Omit<TextInputProps, 'value'> {
/**
* Initial value to pass to masked input
*/
initialValue?: string;
/**
* callback for rendering the custom input out of the value returns from the actual input
*/
renderMaskedText?: ReactElement;
/**
* Custom formatter for the input value
*/
formatter?: (value?: string) => string | undefined;
/**
* container style for the masked input container
*/
containerStyle: StyleProp<ViewStyle>;
}

/**
* @description: Mask Input to create custom looking inputs with custom formats
* @gif: https://camo.githubusercontent.com/61eedb65e968845d5eac713dcd21a69691571fb1/68747470733a2f2f6d656469612e67697068792e636f6d2f6d656469612f4b5a5a7446666f486f454b334b2f67697068792e676966
* @example: https://github.com/wix/react-native-ui-lib/blob/master/demo/src/screens/componentScreens/MaskedInputScreen.js
*/
function MaskedInput(props: MaskedInputProps, ref: ForwardedRef<any>) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've written this in the main PR area, they should make that more noticeable somehow...

Consider moving the screen to TS as well.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely. Fixed and also add a new cool example (:

const {initialValue, formatter = _.identity, containerStyle, renderMaskedText, onChangeText, ...others} = props;
const [value, setValue] = useState(initialValue);
const inputRef = useRef<TextInput>();
const keyboardDidHideListener = useRef<any>();

useImperativeHandle(ref, () => {
return {
isFocused: () => inputRef.current?.isFocused(),
focus,
blur: () => inputRef.current?.blur(),
clear: () => {
inputRef.current?.clear();
setValue('');
// NOTE: This fixes an RN issue - when triggering imperative clear method, it doesn't call onChangeText
onChangeText?.('');
}
};
});

useEffect(() => {
if (initialValue !== value) {
setValue(initialValue);
}
}, [initialValue]);

useEffect(() => {
keyboardDidHideListener.current = Keyboard.addListener('keyboardDidHide', () => {
if (inputRef.current?.isFocused()) {
inputRef.current?.blur();
}
});

return () => keyboardDidHideListener.current.remove();
}, []);

const _onChangeText = useCallback((value: string) => {
const formattedValue = formatter(value) ?? '';
setValue(formattedValue);
onChangeText?.(formattedValue);
},
[onChangeText, formatter]);

const focus = useCallback(() => {
inputRef.current?.focus();
}, []);

const _renderMaskedText = () => {
if (_.isFunction(renderMaskedText)) {
return renderMaskedText(value);
}
return <Text>{value}</Text>;
};

return (
<TouchableOpacity style={containerStyle} activeOpacity={1} onPress={focus}>
<TextInput
{...others}
value={value}
// @ts-expect-error
ref={inputRef}
style={styles.hiddenInput}
enableErrors={false}
hideUnderline
placeholder=""
caretHidden
multiline={false}
onChangeText={_onChangeText}
/>
<View style={styles.maskedInputWrapper}>{_renderMaskedText()}</View>
</TouchableOpacity>
);
}

const styles = StyleSheet.create({
hiddenInput: {
...StyleSheet.absoluteFillObject,
zIndex: 1,
color: 'transparent',
backgroundColor: 'transparent',
height: undefined
},
maskedInputWrapper: {
zIndex: 0
}
});

MaskedInput.displayName = 'MaskedInput';
export default forwardRef(MaskedInput);
File renamed without changes.
3 changes: 1 addition & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,7 @@ export {default as Icon, IconProps} from './components/icon';
export {default as Image, ImageProps} from './components/image';
export {default as ListItem, ListItemProps} from './components/listItem';
export {default as LoaderScreen, LoaderScreenProps} from './components/loaderScreen';
// @ts-expect-error
export {default as MaskedInput} from './components/maskedInput';
export {default as MaskedInput, MaskedInputProps} from './components/maskedInput';
export {default as Modal, ModalProps, ModalTopBarProps} from './components/modal';
export {default as Overlay, OverlayTypes} from './components/overlay';
export {default as PageControl, PageControlProps} from './components/pageControl';
Expand Down