Created
October 5, 2022 06:56
-
-
Save simonauner/3d4fcec52a3dedc5c6a60271c8790c5b to your computer and use it in GitHub Desktop.
Simple TypeScript Input Mask
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { createMask } from './InputMask' | |
describe('InputMask', () => { | |
const mask = '##-##-####' | |
it.each([ | |
['1', '1'], | |
['12', '12-'], | |
['12-3', '12-3'], | |
['12-34', '12-34-'], | |
['12-34-5', '12-34-5'], | |
['12-34-5678', '12-34-5678'], | |
])( | |
'should only add trailing mask characters valid for current input length (%p => %p)', | |
(input, result) => { | |
const onInputChange = createMask(mask) | |
expect(onInputChange(input)).toBe(result) | |
}, | |
) | |
it.each([ | |
['12-', '12-'], | |
['12-34', '12-34-'], | |
])('should allow inputing mask characters (%p => %p)', (input, result) => { | |
const onInputChange = createMask(mask) | |
expect(onInputChange(input)).toBe(result) | |
}) | |
it.each([ | |
['a', ''], | |
['1a', '1'], | |
['12a', '12-'], | |
['12-a', '12-'], | |
])( | |
'should not allow inputing invalid characters (%p => %p)', | |
(input, result) => { | |
const onInputChange = createMask(mask) | |
expect(onInputChange(input)).toBe(result) | |
}, | |
) | |
it('should handle pasting the entire input in one go', () => { | |
const onInputChange = createMask(mask) | |
expect(onInputChange('12345678')).toBe('12-34-5678') | |
}) | |
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
type MaskCharacterData = { | |
canBeReplaced: boolean | |
inputIndex: number | |
maskCharacter: string | |
} | |
const isValidCharacterReplacement = ( | |
maskCharacter: string, | |
inputCharacter: string, | |
) => { | |
return maskCharacter === '#' && !isNaN(parseInt(inputCharacter, 10)) | |
} | |
export const createMask = (mask: string) => { | |
let inputIndex = -1 | |
const maskData = [...mask].map((maskCharacter) => { | |
const data: MaskCharacterData = { | |
canBeReplaced: false, | |
maskCharacter, | |
inputIndex: -1, | |
} | |
if (maskCharacter === '#') { | |
data.canBeReplaced = true | |
inputIndex += 1 | |
data.inputIndex = inputIndex | |
} else { | |
data.inputIndex = inputIndex | |
inputIndex += 1 | |
} | |
return data | |
}) | |
const onInputChange = (input: string) => { | |
let result = '' | |
let insertedMaskedCharacters = 0 | |
maskData.forEach((maskCharacterData, index) => { | |
if ( | |
maskCharacterData.canBeReplaced && | |
// TODO: verify that input[index] is an allowed character | |
isValidCharacterReplacement( | |
maskCharacterData.maskCharacter, | |
input[maskCharacterData.inputIndex - insertedMaskedCharacters], | |
) | |
) { | |
result += input[maskCharacterData.inputIndex - insertedMaskedCharacters] | |
} else if ( | |
!maskCharacterData.canBeReplaced && | |
// This is to make sure that we do not add mask characters before we know that characters _after_ the mask has been entered | |
result[maskCharacterData.inputIndex] | |
) { | |
result += maskCharacterData.maskCharacter | |
// This is to handle if all input is pasted in one go. If it is, we do not have an | |
// input that changes with each inputChange, and thus no inserted masked values along | |
// the way (like, "12" becoming "12-" and so on with each key stroke). | |
// | |
// We know, if the input[index] (for example "-") and maskCharacterData.maskCharacter | |
// (for example also "-") are equal, that we did not need to inject any character. | |
// | |
// But if they're not equal, it means that we must keep track of how many characters we | |
// have inserted and retract when we pick characters from the input above. | |
if (input[index] !== maskCharacterData.maskCharacter) { | |
insertedMaskedCharacters += 1 | |
} | |
} | |
}) | |
return result | |
} | |
return onInputChange | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment