cpr-mask
Version:
A masking component for react
182 lines (179 loc) • 6.91 kB
JavaScript
import React from 'react';
import PropTypes from 'prop-types';
import { find } from 'lodash';
import { valueToMask, checkMasks, getPlaceholder, handleValidChars, handleNoMasks, handleMasks, getNewMaskCursor, testForValidChars } from './cpr-mask.helpers.js';
export default class InputControl extends React.Component {
constructor(props) {
super();
let encodedValue = props.encoder(props.initialValue);
const mask = find(props.masks, (mask) => {
return mask.condition(encodedValue);
});
let maskValue;
if (!encodedValue) {
maskValue = "";
encodedValue = "";
} else if (props.validChars) {
if (testForValidChars(encodedValue, props.validChars)) maskValue = props.initialValue;
else {
maskValue = "";
encodedValue = "";
}
} else {
maskValue = props.initialValue && mask ? valueToMask(encodedValue, mask.pattern, props.filler) : "";
}
this.state = {
maskValue: maskValue,
value: encodedValue,
maskPattern: mask ? mask.pattern : null,
isDirty: false,
isFocused: false
}
}
componentWillMount() {
//Certain masks would not be compatible with each other. This function insures that they are.
if (!checkMasks(this.props.masks)) throw Error("Incompatible masks")
}
componentWillReceiveProps(props) {
//If the initial value is changing the component should update to be the new initial value
let encodedValue = props.encoder(props.initialValue);
if (props.validChars) {
if (testForValidChars(encodedValue, props.validChars)) {
this.setState({
maskValue: encodedValue,
value: encodedValue || "",
})
}
} else {
const mask = find(props.masks, (mask) => {
return mask.condition(encodedValue);
})
this.setState({
maskValue: (props.initialValue && mask) ? valueToMask(encodedValue, mask.pattern, props.filler) : "",
value: encodedValue || "",
maskPattern: mask ? mask.pattern : null,
});
}
}
render() {
const inputProps = this.props.inputProps || {};
const { isDirty, isFocused, value, maskValue } = this.state;
return (
<div className={`
${this.props.className}
${(isDirty || !isFocused) && !this.props.validateMethod(value)
? this.props.invalidClass
: ""}`}>
<div style={{display: "flex", alignItems: "center"}}>
{this.props.sideChars.left
? <span>{this.props.sideChars.left}</span>
: null
}
<input
type="text"
disabled={this.props.disabled}
ref={(inputRef) => this.input = inputRef}
value={this.props.masks.length ? maskValue : value}
onChange={this.handleChange.bind(this)}
onBlur={this.handleBlur.bind(this)}
onMouseUp={this.selectionHandle.bind(this)}
className={this.props.inputClass}
style={{textAlign: this.props.alignment}}
onFocus={this.onFocus.bind(this)}
placeholder={getPlaceholder(this.props.placeholder, this.props.masks, this.props.validChars, this.props.filler)}
{...inputProps}
/>
{this.props.sideChars.right
? <span style={{marginLeft: 5}}>{this.props.sideChars.right}</span>
: null
}
</div>
{(isDirty || !isFocused) && !this.props.validateMethod(value)
? <span className={this.props.nonValidMsgClass}>{`${this.props.nonValidMsg}`}</span>
: null
}
</div>
)
}
selectionHandle(e) {
if (this.state.maskPattern) {
let newCursor = getNewMaskCursor(this.state.value, this.state.maskPattern);
if (this.input.selectionStart > newCursor) this.input.setSelectionRange(newCursor, newCursor);
}
}
onFocus(e) {
if (this.props.onFocus) this.props.onFocus(this.props.decoder ? this.props.decoder(this.state.value) : this.state.value);
this.setState({isFocused: true});
}
handleBlur() {
//Show error messages on blur and run props.onBlur
if (this.props.onBlur) this.props.onBlur(this.props.decoder ? this.props.decoder(this.state.value) : this.state.value);
this.setState({
isDirty: true,
isFocused: false
})
}
handleChange(e) {
//if there is a validChars prop then it takes precedence over masks
//it only checks to see that the input characters match the validChars regex
if (this.props.validChars) handleValidChars.call(this, e.target.value);
//If there is no validChars and no mask you just have an input basically
else if (!this.props.masks.length) handleNoMasks.call(this, e.target.value);
//If there are masks there's some work to be done
else handleMasks.call(this, e.target.value)
}
static defaultProps = {
filler: " ",
initialValue: "",
alignment: "left",
invalidClass: "",
inputLabel: "",
className: "",
masks: [],
validateMethod() {
return true;
},
sideChars: {},
encoder: value => value,
decoder: value => value,
}
static propTypes = {
//name given to the surrounding div
className: PropTypes.string,
//class given to the input field
inputClass: PropTypes.string,
//What value the component starts with
initialValue: PropTypes.string,
//class given to the error message
invalidClass: PropTypes.string,
//Decides which side of an input field the text appears on
alignment: PropTypes.oneOf(["left", "right"]),
//The message displayed when the input is invalid
nonValidMsg: PropTypes.string,
//The class applied to the nonValidMsg span
nonValidMsgClass: PropTypes.string,
//Characters to display on either side of the input
placeholder: PropTypes.string,
sideChars: PropTypes.shape({
left: PropTypes.string,
right: PropTypes.string
}),
//The variety of masks that could be displayed
//If there is no placeholder and no validChars the first mask's pattern will be used to make a placeholder
masks: PropTypes.arrayOf(PropTypes.shape({
condition: PropTypes.function,
pattern: PropTypes.string.isRequired
})),
//Regex that decides the validity of your input
validChars: PropTypes.object, //is really a regex
//Function that decides if your input is valid and whether or not to show the invalid message and invalid classes
validateMethod: PropTypes.func,
//Function that can transform the value before passing it up
decoder: PropTypes.func,
//Function that changes value as it comes into cpr-mask
//Encoder should probably be used if you're using a decoder
encoder: PropTypes.func,
// Extra props to be placed on the <input /> (i.e., htmlFor, autocomplete, etc)
inputProps: PropTypes.object,
}
}