Skip to content

Instantly share code, notes, and snippets.

@simonauner
Created October 5, 2022 06:56
Show Gist options
  • Save simonauner/3d4fcec52a3dedc5c6a60271c8790c5b to your computer and use it in GitHub Desktop.
Save simonauner/3d4fcec52a3dedc5c6a60271c8790c5b to your computer and use it in GitHub Desktop.
Simple TypeScript Input Mask
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')
})
})
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