use-mask-input 3.9.0 → 3.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/antd.cjs +14 -12
- package/dist/antd.cjs.map +1 -1
- package/dist/antd.d.cts +3 -2
- package/dist/antd.d.ts +3 -2
- package/dist/antd.js +6 -4
- package/dist/antd.js.map +1 -1
- package/dist/{chunk-X5SEJVSB.cjs → chunk-DTC7JTZP.cjs} +78 -26
- package/dist/{chunk-X5SEJVSB.cjs.map → chunk-DTC7JTZP.cjs.map} +1 -1
- package/dist/{chunk-ICLWBMH4.js → chunk-TVCNC3TP.js} +77 -27
- package/dist/{chunk-ICLWBMH4.js.map → chunk-TVCNC3TP.js.map} +1 -1
- package/dist/{index-BoaVtWUr.d.cts → index-D8KkaDbQ.d.cts} +7 -3
- package/dist/{index-BoaVtWUr.d.ts → index-D8KkaDbQ.d.ts} +7 -3
- package/dist/index.cjs +45 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -4
- package/dist/index.d.ts +5 -4
- package/dist/index.js +30 -23
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/antd/useMaskInputAntd.spec.tsx +22 -0
- package/src/antd/useMaskInputAntd.ts +11 -7
- package/src/api/useHookFormMask.ts +4 -1
- package/src/api/useMaskInput.spec.tsx +18 -0
- package/src/api/useMaskInput.ts +11 -5
- package/src/api/withHookFormMask.spec.ts +43 -74
- package/src/api/withHookFormMask.ts +18 -10
- package/src/api/withMask.spec.ts +16 -0
- package/src/api/withMask.ts +11 -8
- package/src/api/withTanStackFormMask.ts +19 -10
- package/src/index.tsx +2 -0
- package/src/types/index.ts +8 -2
- package/src/utils/index.ts +6 -1
- package/src/utils/maskHelpers.ts +44 -1
|
@@ -3,10 +3,13 @@ import { useCallback, useEffect, useRef } from 'react';
|
|
|
3
3
|
import withMask from '../api/withMask';
|
|
4
4
|
import { resolveInputRef } from '../core';
|
|
5
5
|
import isServer from '../utils/isServer';
|
|
6
|
+
import { getUnmaskedValue, setUnmaskedValue } from '../utils';
|
|
6
7
|
|
|
7
8
|
import type { InputRef } from 'antd';
|
|
8
9
|
|
|
9
|
-
import type { Mask, Options } from '../types';
|
|
10
|
+
import type { Mask, Options, UnmaskedValueApi } from '../types';
|
|
11
|
+
|
|
12
|
+
type UseMaskInputAntdReturn = ((input: InputRef | null) => void) & UnmaskedValueApi;
|
|
10
13
|
|
|
11
14
|
interface UseMaskInputOptions {
|
|
12
15
|
mask: Mask;
|
|
@@ -23,14 +26,13 @@ interface UseMaskInputOptions {
|
|
|
23
26
|
* @param props.options - Optional mask configuration options
|
|
24
27
|
* @returns A ref callback function to attach to the Ant Design Input element
|
|
25
28
|
*/
|
|
26
|
-
export default function useMaskInputAntd(props: UseMaskInputOptions):
|
|
27
|
-
input: InputRef | null
|
|
28
|
-
) => void {
|
|
29
|
+
export default function useMaskInputAntd(props: UseMaskInputOptions): UseMaskInputAntdReturn {
|
|
29
30
|
const { mask, register, options } = props;
|
|
30
31
|
const ref = useRef<HTMLInputElement | null>(null);
|
|
31
32
|
const maskRef = useRef(mask);
|
|
32
33
|
const optionsRef = useRef(options);
|
|
33
34
|
const maskedElementRef = useRef<HTMLInputElement | null>(null);
|
|
35
|
+
const unmaskedValue = useCallback(() => getUnmaskedValue(ref.current), []);
|
|
34
36
|
|
|
35
37
|
maskRef.current = mask;
|
|
36
38
|
optionsRef.current = options;
|
|
@@ -55,10 +57,12 @@ export default function useMaskInputAntd(props: UseMaskInputOptions): (
|
|
|
55
57
|
}, [register]);
|
|
56
58
|
|
|
57
59
|
if (isServer) {
|
|
58
|
-
|
|
60
|
+
const noop = (() => {
|
|
59
61
|
// server doesn't have dom, so just do nothing
|
|
60
|
-
};
|
|
62
|
+
}) as unknown as UseMaskInputAntdReturn;
|
|
63
|
+
|
|
64
|
+
return setUnmaskedValue(noop, () => '');
|
|
61
65
|
}
|
|
62
66
|
|
|
63
|
-
return refCallback;
|
|
67
|
+
return setUnmaskedValue(refCallback as UseMaskInputAntdReturn, unmaskedValue);
|
|
64
68
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { useLayoutEffect, useMemo, useRef } from 'react';
|
|
2
2
|
|
|
3
3
|
import { applyMaskToElement } from '../core';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
flow, getUnmaskedValue, makeMaskCacheKey, setPrevRef, setUnmaskedValue,
|
|
6
|
+
} from '../utils';
|
|
5
7
|
|
|
6
8
|
import type { RefCallback } from 'react';
|
|
7
9
|
import type {
|
|
@@ -93,6 +95,7 @@ export default function useHookFormMask<
|
|
|
93
95
|
...registerReturn,
|
|
94
96
|
ref: entry.stableRef,
|
|
95
97
|
} as UseHookFormMaskReturn<T>;
|
|
98
|
+
setUnmaskedValue(result, () => getUnmaskedValue(entry?.element ?? null));
|
|
96
99
|
|
|
97
100
|
setPrevRef(result, ref);
|
|
98
101
|
|
|
@@ -29,6 +29,7 @@ describe('useMaskInput', () => {
|
|
|
29
29
|
it('returns a ref callback function', () => {
|
|
30
30
|
const { result } = renderHook(() => useMaskInput({ mask: '999-999' }));
|
|
31
31
|
expect(typeof result.current).toBe('function');
|
|
32
|
+
expect(typeof result.current.unmaskedValue).toBe('function');
|
|
32
33
|
});
|
|
33
34
|
|
|
34
35
|
it('handles null input', () => {
|
|
@@ -56,6 +57,23 @@ describe('useMaskInput', () => {
|
|
|
56
57
|
expect(inputmask).toHaveBeenCalled();
|
|
57
58
|
});
|
|
58
59
|
|
|
60
|
+
it('exposes the unmasked value from the masked input', () => {
|
|
61
|
+
const input = document.createElement('input');
|
|
62
|
+
vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as any);
|
|
63
|
+
|
|
64
|
+
const { result } = renderHook(() => useMaskInput({ mask: '999-999' }));
|
|
65
|
+
|
|
66
|
+
act(() => {
|
|
67
|
+
result.current(input);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
input.inputmask = {
|
|
71
|
+
unmaskedvalue: vi.fn(() => '2026-04-01'),
|
|
72
|
+
} as any;
|
|
73
|
+
|
|
74
|
+
expect(result.current.unmaskedValue()).toBe('2026-04-01');
|
|
75
|
+
});
|
|
76
|
+
|
|
59
77
|
it('handles ref object', () => {
|
|
60
78
|
const input = document.createElement('input');
|
|
61
79
|
const ref = { current: input };
|
package/src/api/useMaskInput.ts
CHANGED
|
@@ -5,8 +5,11 @@ import {
|
|
|
5
5
|
import { resolveInputRef } from '../core';
|
|
6
6
|
import withMask from './withMask';
|
|
7
7
|
import isServer from '../utils/isServer';
|
|
8
|
+
import { getUnmaskedValue, setUnmaskedValue } from '../utils';
|
|
8
9
|
|
|
9
|
-
import type {
|
|
10
|
+
import type {
|
|
11
|
+
Input, Mask, Options, UseMaskInputReturn,
|
|
12
|
+
} from '../types';
|
|
10
13
|
|
|
11
14
|
interface UseMaskInputOptions {
|
|
12
15
|
mask: Mask;
|
|
@@ -24,11 +27,12 @@ interface UseMaskInputOptions {
|
|
|
24
27
|
* @param props.options - Optional mask configuration options
|
|
25
28
|
* @returns A ref callback function to attach to the input element
|
|
26
29
|
*/
|
|
27
|
-
export default function useMaskInput(props: UseMaskInputOptions):
|
|
30
|
+
export default function useMaskInput(props: UseMaskInputOptions): UseMaskInputReturn {
|
|
28
31
|
const { mask, register, options } = props;
|
|
29
32
|
const ref = useRef<HTMLInputElement | null>(null);
|
|
30
33
|
const maskRef = useRef(mask);
|
|
31
34
|
const optionsRef = useRef(options);
|
|
35
|
+
const unmaskedValue = useCallback(() => getUnmaskedValue(ref.current), []);
|
|
32
36
|
|
|
33
37
|
const refCallback = useCallback((input: Input | null): void => {
|
|
34
38
|
if (!input) {
|
|
@@ -46,10 +50,12 @@ export default function useMaskInput(props: UseMaskInputOptions): ((input: Input
|
|
|
46
50
|
}, [register]);
|
|
47
51
|
|
|
48
52
|
if (isServer) {
|
|
49
|
-
|
|
53
|
+
const noop = (() => {
|
|
50
54
|
// server doesn't have dom, so just do nothing
|
|
51
|
-
};
|
|
55
|
+
}) as unknown as UseMaskInputReturn;
|
|
56
|
+
|
|
57
|
+
return setUnmaskedValue(noop, () => '');
|
|
52
58
|
}
|
|
53
59
|
|
|
54
|
-
return refCallback;
|
|
60
|
+
return setUnmaskedValue(refCallback, unmaskedValue);
|
|
55
61
|
}
|
|
@@ -18,6 +18,18 @@ vi.mock('inputmask', () => ({
|
|
|
18
18
|
})),
|
|
19
19
|
}));
|
|
20
20
|
|
|
21
|
+
const createRegister = (
|
|
22
|
+
overrides: Partial<UseHookFormMaskReturn<FieldValues>> = {},
|
|
23
|
+
): UseHookFormMaskReturn<FieldValues> => ({
|
|
24
|
+
prevRef: vi.fn(),
|
|
25
|
+
ref: vi.fn(),
|
|
26
|
+
onChange: vi.fn(),
|
|
27
|
+
onBlur: vi.fn(),
|
|
28
|
+
name: 'phone',
|
|
29
|
+
unmaskedValue: vi.fn(() => ''),
|
|
30
|
+
...overrides,
|
|
31
|
+
});
|
|
32
|
+
|
|
21
33
|
describe('withHookFormMask', () => {
|
|
22
34
|
beforeEach(() => {
|
|
23
35
|
vi.clearAllMocks();
|
|
@@ -25,13 +37,7 @@ describe('withHookFormMask', () => {
|
|
|
25
37
|
|
|
26
38
|
it('returns register object with masked ref', () => {
|
|
27
39
|
const originalRef = vi.fn();
|
|
28
|
-
const register
|
|
29
|
-
prevRef: vi.fn(),
|
|
30
|
-
ref: originalRef,
|
|
31
|
-
onChange: vi.fn(),
|
|
32
|
-
onBlur: vi.fn(),
|
|
33
|
-
name: 'phone',
|
|
34
|
-
};
|
|
40
|
+
const register = createRegister({ ref: originalRef });
|
|
35
41
|
const maskFn = vi.fn();
|
|
36
42
|
vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as any);
|
|
37
43
|
|
|
@@ -42,18 +48,13 @@ describe('withHookFormMask', () => {
|
|
|
42
48
|
expect(result.onChange).toBe(register.onChange);
|
|
43
49
|
expect(result.onBlur).toBe(register.onBlur);
|
|
44
50
|
expect(result.name).toBe(register.name);
|
|
51
|
+
expect(typeof result.unmaskedValue).toBe('function');
|
|
45
52
|
});
|
|
46
53
|
|
|
47
54
|
it('applies mask when ref is called', () => {
|
|
48
55
|
const input = document.createElement('input');
|
|
49
56
|
const originalRef = vi.fn();
|
|
50
|
-
const register
|
|
51
|
-
prevRef: vi.fn(),
|
|
52
|
-
ref: originalRef,
|
|
53
|
-
onChange: vi.fn(),
|
|
54
|
-
onBlur: vi.fn(),
|
|
55
|
-
name: 'phone',
|
|
56
|
-
};
|
|
57
|
+
const register = createRegister({ ref: originalRef });
|
|
57
58
|
const maskFn = vi.fn();
|
|
58
59
|
vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as any);
|
|
59
60
|
|
|
@@ -63,16 +64,27 @@ describe('withHookFormMask', () => {
|
|
|
63
64
|
expect(maskFn).toHaveBeenCalled();
|
|
64
65
|
});
|
|
65
66
|
|
|
67
|
+
it('exposes the unmasked value from the masked input', () => {
|
|
68
|
+
const input = document.createElement('input');
|
|
69
|
+
const originalRef = vi.fn();
|
|
70
|
+
const register = createRegister({ ref: originalRef });
|
|
71
|
+
const maskFn = vi.fn();
|
|
72
|
+
vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as any);
|
|
73
|
+
|
|
74
|
+
const result = withHookFormMask(register, '999-999');
|
|
75
|
+
result.ref?.(input);
|
|
76
|
+
|
|
77
|
+
input.inputmask = {
|
|
78
|
+
unmaskedvalue: vi.fn(() => '2026-04-01'),
|
|
79
|
+
} as any;
|
|
80
|
+
|
|
81
|
+
expect(result.unmaskedValue()).toBe('2026-04-01');
|
|
82
|
+
});
|
|
83
|
+
|
|
66
84
|
it('calls original ref after applying mask', () => {
|
|
67
85
|
const input = document.createElement('input');
|
|
68
86
|
const originalRef = vi.fn();
|
|
69
|
-
const register
|
|
70
|
-
ref: originalRef,
|
|
71
|
-
prevRef: vi.fn(),
|
|
72
|
-
onChange: vi.fn(),
|
|
73
|
-
onBlur: vi.fn(),
|
|
74
|
-
name: 'phone',
|
|
75
|
-
};
|
|
87
|
+
const register = createRegister({ ref: originalRef });
|
|
76
88
|
const maskFn = vi.fn();
|
|
77
89
|
vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as any);
|
|
78
90
|
|
|
@@ -85,13 +97,7 @@ describe('withHookFormMask', () => {
|
|
|
85
97
|
it('works with alias masks', () => {
|
|
86
98
|
const input = document.createElement('input');
|
|
87
99
|
const originalRef = vi.fn();
|
|
88
|
-
const register:
|
|
89
|
-
ref: originalRef,
|
|
90
|
-
prevRef: vi.fn(),
|
|
91
|
-
onChange: vi.fn(),
|
|
92
|
-
onBlur: vi.fn(),
|
|
93
|
-
name: 'cpf',
|
|
94
|
-
};
|
|
100
|
+
const register = createRegister({ ref: originalRef, name: 'cpf' });
|
|
95
101
|
const maskFn = vi.fn();
|
|
96
102
|
vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as any);
|
|
97
103
|
|
|
@@ -104,13 +110,7 @@ describe('withHookFormMask', () => {
|
|
|
104
110
|
it('works with custom options', () => {
|
|
105
111
|
const input = document.createElement('input');
|
|
106
112
|
const originalRef = vi.fn();
|
|
107
|
-
const register
|
|
108
|
-
prevRef: vi.fn(),
|
|
109
|
-
ref: originalRef,
|
|
110
|
-
onChange: vi.fn(),
|
|
111
|
-
onBlur: vi.fn(),
|
|
112
|
-
name: 'phone',
|
|
113
|
-
};
|
|
113
|
+
const register = createRegister({ ref: originalRef });
|
|
114
114
|
const maskFn = vi.fn();
|
|
115
115
|
vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as any);
|
|
116
116
|
|
|
@@ -121,13 +121,10 @@ describe('withHookFormMask', () => {
|
|
|
121
121
|
});
|
|
122
122
|
|
|
123
123
|
it('handles null ref gracefully', () => {
|
|
124
|
-
const register
|
|
124
|
+
const register = createRegister({
|
|
125
125
|
prevRef: null as unknown as RefCallback<HTMLElement | null>,
|
|
126
126
|
ref: null as unknown as RefCallback<HTMLElement | null>,
|
|
127
|
-
|
|
128
|
-
onBlur: vi.fn(),
|
|
129
|
-
name: 'phone',
|
|
130
|
-
};
|
|
127
|
+
});
|
|
131
128
|
|
|
132
129
|
const result = withHookFormMask(register, '999-999');
|
|
133
130
|
expect(result.ref).toBeNull();
|
|
@@ -137,13 +134,7 @@ describe('withHookFormMask', () => {
|
|
|
137
134
|
|
|
138
135
|
it('handles null input in ref callback', () => {
|
|
139
136
|
const originalRef = vi.fn();
|
|
140
|
-
const register
|
|
141
|
-
prevRef: vi.fn(),
|
|
142
|
-
ref: originalRef,
|
|
143
|
-
onChange: vi.fn(),
|
|
144
|
-
onBlur: vi.fn(),
|
|
145
|
-
name: 'phone',
|
|
146
|
-
};
|
|
137
|
+
const register = createRegister({ ref: originalRef });
|
|
147
138
|
const maskFn = vi.fn();
|
|
148
139
|
vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as any);
|
|
149
140
|
|
|
@@ -155,13 +146,7 @@ describe('withHookFormMask', () => {
|
|
|
155
146
|
|
|
156
147
|
it('returns the same ref callback reference across multiple calls (stable identity)', () => {
|
|
157
148
|
const originalRef = vi.fn();
|
|
158
|
-
const register
|
|
159
|
-
prevRef: vi.fn(),
|
|
160
|
-
ref: originalRef,
|
|
161
|
-
onChange: vi.fn(),
|
|
162
|
-
onBlur: vi.fn(),
|
|
163
|
-
name: 'phone',
|
|
164
|
-
};
|
|
149
|
+
const register = createRegister({ ref: originalRef });
|
|
165
150
|
|
|
166
151
|
const first = withHookFormMask(register, '999-999');
|
|
167
152
|
const second = withHookFormMask(register, '999-999');
|
|
@@ -171,20 +156,8 @@ describe('withHookFormMask', () => {
|
|
|
171
156
|
|
|
172
157
|
it('returns different ref callbacks for different field/mask combinations', () => {
|
|
173
158
|
const originalRef = vi.fn();
|
|
174
|
-
const registerPhone:
|
|
175
|
-
|
|
176
|
-
ref: originalRef,
|
|
177
|
-
onChange: vi.fn(),
|
|
178
|
-
onBlur: vi.fn(),
|
|
179
|
-
name: 'phone',
|
|
180
|
-
};
|
|
181
|
-
const registerCpf: UseHookFormMaskReturn<FieldValues> = {
|
|
182
|
-
prevRef: vi.fn(),
|
|
183
|
-
ref: originalRef,
|
|
184
|
-
onChange: vi.fn(),
|
|
185
|
-
onBlur: vi.fn(),
|
|
186
|
-
name: 'cpf',
|
|
187
|
-
};
|
|
159
|
+
const registerPhone = createRegister({ ref: originalRef, name: 'phone' });
|
|
160
|
+
const registerCpf = createRegister({ ref: originalRef, name: 'cpf' });
|
|
188
161
|
|
|
189
162
|
const phone = withHookFormMask(registerPhone, '999-999');
|
|
190
163
|
const cpf = withHookFormMask(registerCpf, 'cpf');
|
|
@@ -195,12 +168,8 @@ describe('withHookFormMask', () => {
|
|
|
195
168
|
it('returns a new ref callback when the original ref changes', () => {
|
|
196
169
|
const ref1 = vi.fn();
|
|
197
170
|
const ref2 = vi.fn();
|
|
198
|
-
const register1:
|
|
199
|
-
|
|
200
|
-
};
|
|
201
|
-
const register2: UseHookFormMaskReturn<FieldValues> = {
|
|
202
|
-
prevRef: vi.fn(), ref: ref2, onChange: vi.fn(), onBlur: vi.fn(), name: 'phone',
|
|
203
|
-
};
|
|
171
|
+
const register1 = createRegister({ prevRef: vi.fn(), ref: ref1 });
|
|
172
|
+
const register2 = createRegister({ prevRef: vi.fn(), ref: ref2 });
|
|
204
173
|
|
|
205
174
|
const result1 = withHookFormMask(register1, '999-999');
|
|
206
175
|
const result2 = withHookFormMask(register2, '999-999');
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { applyMaskToElement } from '../core';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
getUnmaskedValue, makeMaskCacheKey, setPrevRef, setUnmaskedValue,
|
|
4
|
+
} from '../utils';
|
|
3
5
|
|
|
4
6
|
import type { RefCallback } from 'react';
|
|
5
7
|
import type { FieldValues } from 'react-hook-form';
|
|
@@ -8,6 +10,10 @@ import type {
|
|
|
8
10
|
Mask, Options, UseFormRegisterReturn, UseHookFormMaskReturn,
|
|
9
11
|
} from '../types';
|
|
10
12
|
|
|
13
|
+
type MaskedRefCallback = RefCallback<HTMLElement | null> & {
|
|
14
|
+
currentElement?: HTMLElement | null;
|
|
15
|
+
};
|
|
16
|
+
|
|
11
17
|
const refCache = new WeakMap<
|
|
12
18
|
RefCallback<HTMLElement | null>,
|
|
13
19
|
Map<string, RefCallback<HTMLElement | null>>
|
|
@@ -36,6 +42,7 @@ export default function withHookFormMask(
|
|
|
36
42
|
...register,
|
|
37
43
|
ref: null as unknown as RefCallback<HTMLElement | null>,
|
|
38
44
|
} as UseHookFormMaskReturn<FieldValues>;
|
|
45
|
+
setUnmaskedValue(result, () => '');
|
|
39
46
|
setPrevRef(result, ref);
|
|
40
47
|
return result;
|
|
41
48
|
}
|
|
@@ -47,20 +54,21 @@ export default function withHookFormMask(
|
|
|
47
54
|
const cacheKey = makeMaskCacheKey(register.name, mask);
|
|
48
55
|
|
|
49
56
|
if (!maskCache?.has(cacheKey)) {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
);
|
|
57
|
+
const maskedRef = ((input: HTMLElement | null) => {
|
|
58
|
+
maskedRef.currentElement = input;
|
|
59
|
+
if (input) applyMaskToElement(input, mask, options);
|
|
60
|
+
return ref(input);
|
|
61
|
+
}) as MaskedRefCallback;
|
|
62
|
+
|
|
63
|
+
maskCache?.set(cacheKey, maskedRef);
|
|
58
64
|
}
|
|
59
65
|
|
|
66
|
+
const maskedRef = maskCache?.get(cacheKey) as MaskedRefCallback | undefined;
|
|
60
67
|
const result = {
|
|
61
68
|
...register,
|
|
62
|
-
ref:
|
|
69
|
+
ref: maskedRef,
|
|
63
70
|
} as UseHookFormMaskReturn<FieldValues>;
|
|
71
|
+
setUnmaskedValue(result, () => getUnmaskedValue(maskedRef?.currentElement ?? null));
|
|
64
72
|
|
|
65
73
|
setPrevRef(result, ref);
|
|
66
74
|
|
package/src/api/withMask.spec.ts
CHANGED
|
@@ -24,6 +24,7 @@ describe('withMask', () => {
|
|
|
24
24
|
it('returns a function', () => {
|
|
25
25
|
const result = withMask('999-999');
|
|
26
26
|
expect(typeof result).toBe('function');
|
|
27
|
+
expect(typeof result.unmaskedValue).toBe('function');
|
|
27
28
|
});
|
|
28
29
|
|
|
29
30
|
it('applies mask to input element', () => {
|
|
@@ -37,6 +38,21 @@ describe('withMask', () => {
|
|
|
37
38
|
expect(maskFn).toHaveBeenCalledWith(input);
|
|
38
39
|
});
|
|
39
40
|
|
|
41
|
+
it('exposes the unmasked value from the masked input', () => {
|
|
42
|
+
const input = document.createElement('input');
|
|
43
|
+
const maskFn = vi.fn();
|
|
44
|
+
vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as any);
|
|
45
|
+
|
|
46
|
+
const refCallback = withMask('999-999');
|
|
47
|
+
refCallback(input);
|
|
48
|
+
|
|
49
|
+
input.inputmask = {
|
|
50
|
+
unmaskedvalue: vi.fn(() => '2026-04-01'),
|
|
51
|
+
} as any;
|
|
52
|
+
|
|
53
|
+
expect(refCallback.unmaskedValue()).toBe('2026-04-01');
|
|
54
|
+
});
|
|
55
|
+
|
|
40
56
|
it('does nothing if input is null', () => {
|
|
41
57
|
const maskFn = vi.fn();
|
|
42
58
|
vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as any);
|
package/src/api/withMask.ts
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
import inputmask from 'inputmask';
|
|
3
3
|
|
|
4
4
|
import { getMaskOptions } from '../core/maskConfig';
|
|
5
|
-
import { makeMaskCacheKey } from '../utils';
|
|
5
|
+
import { getUnmaskedValue, makeMaskCacheKey, setUnmaskedValue } from '../utils';
|
|
6
6
|
import isServer from '../utils/isServer';
|
|
7
7
|
import interopDefaultSync from '../utils/moduleInterop';
|
|
8
8
|
|
|
9
|
-
import type { Input, Mask, Options } from '../types';
|
|
9
|
+
import type { Input, Mask, Options, UseMaskInputReturn } from '../types';
|
|
10
10
|
|
|
11
|
-
const callbackCache = new Map<string,
|
|
11
|
+
const callbackCache = new Map<string, UseMaskInputReturn>();
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Higher-order function that creates a ref callback for applying input masks.
|
|
@@ -18,26 +18,29 @@ const callbackCache = new Map<string, (input: Input | null) => void>();
|
|
|
18
18
|
* @param options - Optional mask configuration options
|
|
19
19
|
* @returns A ref callback function that applies the mask
|
|
20
20
|
*/
|
|
21
|
-
export default function withMask(mask: Mask, options?: Options):
|
|
21
|
+
export default function withMask(mask: Mask, options?: Options): UseMaskInputReturn {
|
|
22
22
|
// without options, we cant cache, so we always return a fresh callback. :P
|
|
23
23
|
if (!options) {
|
|
24
24
|
const cacheKey = makeMaskCacheKey('', mask);
|
|
25
25
|
if (callbackCache.has(cacheKey)) {
|
|
26
|
-
return callbackCache.get(cacheKey) as
|
|
26
|
+
return callbackCache.get(cacheKey) as UseMaskInputReturn;
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
let currentInput: Input | null = null;
|
|
31
|
+
|
|
32
|
+
const callback = ((input: Input | null): void => {
|
|
31
33
|
if (isServer || mask === null || !input) return;
|
|
32
34
|
|
|
35
|
+
currentInput = input;
|
|
33
36
|
const maskInput = interopDefaultSync(inputmask)(getMaskOptions(mask, options));
|
|
34
37
|
maskInput.mask(input as HTMLElement);
|
|
35
|
-
};
|
|
38
|
+
}) as UseMaskInputReturn;
|
|
36
39
|
|
|
37
40
|
if (!options) {
|
|
38
41
|
const cacheKey = makeMaskCacheKey('', mask);
|
|
39
42
|
callbackCache.set(cacheKey, callback);
|
|
40
43
|
}
|
|
41
44
|
|
|
42
|
-
return callback;
|
|
45
|
+
return setUnmaskedValue(callback, () => getUnmaskedValue(currentInput));
|
|
43
46
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { applyMaskToElement } from '../core';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
getUnmaskedValue, makeMaskCacheKey, setPrevRef, setUnmaskedValue,
|
|
4
|
+
} from '../utils';
|
|
3
5
|
|
|
4
6
|
import type { RefCallback } from 'react';
|
|
5
7
|
|
|
@@ -7,6 +9,10 @@ import type {
|
|
|
7
9
|
Mask, Options, TanStackFormInputProps, UseTanStackFormMaskReturn,
|
|
8
10
|
} from '../types';
|
|
9
11
|
|
|
12
|
+
type MaskedRefCallback = RefCallback<HTMLElement | null> & {
|
|
13
|
+
currentElement?: HTMLElement | null;
|
|
14
|
+
};
|
|
15
|
+
|
|
10
16
|
const refCache = new WeakMap<
|
|
11
17
|
RefCallback<HTMLElement | null>,
|
|
12
18
|
Map<string, RefCallback<HTMLElement | null>>
|
|
@@ -24,12 +30,15 @@ export default function withTanStackFormMask<T extends TanStackFormInputProps>(
|
|
|
24
30
|
const { ref } = inputProps;
|
|
25
31
|
|
|
26
32
|
if (!ref) {
|
|
33
|
+
let currentElement: HTMLElement | null = null;
|
|
27
34
|
const result = {
|
|
28
35
|
...inputProps,
|
|
29
36
|
ref: ((input: HTMLElement | null) => {
|
|
37
|
+
currentElement = input;
|
|
30
38
|
if (input) applyMaskToElement(input, mask, options);
|
|
31
39
|
}) as RefCallback<HTMLElement | null>,
|
|
32
40
|
} as unknown as UseTanStackFormMaskReturn<T>;
|
|
41
|
+
setUnmaskedValue(result, () => getUnmaskedValue(currentElement));
|
|
33
42
|
|
|
34
43
|
setPrevRef(result, ref);
|
|
35
44
|
return result;
|
|
@@ -43,21 +52,21 @@ export default function withTanStackFormMask<T extends TanStackFormInputProps>(
|
|
|
43
52
|
const cacheKey = makeMaskCacheKey(inputProps.name ?? '', mask);
|
|
44
53
|
|
|
45
54
|
if (!maskCache?.has(cacheKey)) {
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
55
|
+
const maskedRef = ((input: HTMLElement | null) => {
|
|
56
|
+
maskedRef.currentElement = input;
|
|
57
|
+
if (input) applyMaskToElement(input, mask, options);
|
|
58
|
+
ref(input);
|
|
59
|
+
}) as MaskedRefCallback;
|
|
50
60
|
|
|
51
|
-
maskCache?.set(
|
|
52
|
-
cacheKey,
|
|
53
|
-
flow(applyMaskToRef, ref) as RefCallback<HTMLElement | null>,
|
|
54
|
-
);
|
|
61
|
+
maskCache?.set(cacheKey, maskedRef);
|
|
55
62
|
}
|
|
56
63
|
|
|
64
|
+
const maskedRef = maskCache?.get(cacheKey) as MaskedRefCallback | undefined;
|
|
57
65
|
const result = {
|
|
58
66
|
...inputProps,
|
|
59
|
-
ref:
|
|
67
|
+
ref: maskedRef,
|
|
60
68
|
} as unknown as UseTanStackFormMaskReturn<T>;
|
|
69
|
+
setUnmaskedValue(result, () => getUnmaskedValue(maskedRef?.currentElement ?? null));
|
|
61
70
|
|
|
62
71
|
setPrevRef(result, ref);
|
|
63
72
|
return result;
|
package/src/index.tsx
CHANGED
package/src/types/index.ts
CHANGED
|
@@ -30,9 +30,15 @@ export type Mask = 'datetime'
|
|
|
30
30
|
export type Options = MaskOptions;
|
|
31
31
|
export type Input = HTMLInputElement | HTMLTextAreaElement | HTMLElement;
|
|
32
32
|
|
|
33
|
+
export interface UnmaskedValueApi {
|
|
34
|
+
unmaskedValue: () => string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type UseMaskInputReturn = RefCallback<HTMLElement | null> & UnmaskedValueApi;
|
|
38
|
+
|
|
33
39
|
export interface UseHookFormMaskReturn<
|
|
34
40
|
T extends FieldValues,
|
|
35
|
-
> extends UseFormRegisterReturn<Path<T
|
|
41
|
+
> extends UseFormRegisterReturn<Path<T>>, UnmaskedValueApi {
|
|
36
42
|
ref: RefCallback<HTMLElement | null>;
|
|
37
43
|
prevRef: RefCallback<HTMLElement | null>;
|
|
38
44
|
}
|
|
@@ -47,4 +53,4 @@ export type UseTanStackFormMaskReturn<T extends TanStackFormInputProps = TanStac
|
|
|
47
53
|
Omit<T, 'ref'> & {
|
|
48
54
|
ref: RefCallback<HTMLElement | null>;
|
|
49
55
|
prevRef: RefCallback<HTMLElement | null> | undefined;
|
|
50
|
-
};
|
|
56
|
+
} & UnmaskedValueApi;
|
package/src/utils/index.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
export { default as flow } from './flow';
|
|
2
2
|
export { default as isServer } from './isServer';
|
|
3
3
|
export { default as moduleInterop } from './moduleInterop';
|
|
4
|
-
export {
|
|
4
|
+
export {
|
|
5
|
+
getUnmaskedValue,
|
|
6
|
+
makeMaskCacheKey,
|
|
7
|
+
setPrevRef,
|
|
8
|
+
setUnmaskedValue,
|
|
9
|
+
} from './maskHelpers';
|
package/src/utils/maskHelpers.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { findInputElement, resolveInputRef } from '../core/elementResolver';
|
|
2
|
+
|
|
3
|
+
import type { Input, Mask, UnmaskedValueApi } from '../types';
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Builds a stable string key from a field name and mask, used to cache ref
|
|
@@ -20,3 +22,44 @@ export function setPrevRef(result: object, ref: unknown): void {
|
|
|
20
22
|
configurable: true,
|
|
21
23
|
});
|
|
22
24
|
}
|
|
25
|
+
|
|
26
|
+
function resolveUnmaskedInput(input: Input | null): HTMLInputElement | HTMLTextAreaElement | null {
|
|
27
|
+
const resolved = resolveInputRef(input);
|
|
28
|
+
if (!resolved) return null;
|
|
29
|
+
|
|
30
|
+
const inputElement = findInputElement(resolved);
|
|
31
|
+
if (inputElement) {
|
|
32
|
+
return inputElement as HTMLInputElement | HTMLTextAreaElement;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return resolved as HTMLInputElement | HTMLTextAreaElement;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getUnmaskedValue(input: Input | null): string {
|
|
39
|
+
const element = resolveUnmaskedInput(input);
|
|
40
|
+
if (!element) return '';
|
|
41
|
+
|
|
42
|
+
const inputmask = (element as HTMLInputElement).inputmask as
|
|
43
|
+
| { unmaskedvalue?: () => string }
|
|
44
|
+
| undefined;
|
|
45
|
+
|
|
46
|
+
if (inputmask && typeof inputmask.unmaskedvalue === 'function') {
|
|
47
|
+
return inputmask.unmaskedvalue();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return 'value' in element ? element.value : '';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function setUnmaskedValue<T extends object>(
|
|
54
|
+
result: T,
|
|
55
|
+
getter: () => string,
|
|
56
|
+
): T & UnmaskedValueApi {
|
|
57
|
+
Object.defineProperty(result, 'unmaskedValue', {
|
|
58
|
+
value: getter,
|
|
59
|
+
enumerable: false,
|
|
60
|
+
writable: true,
|
|
61
|
+
configurable: true,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return result as T & UnmaskedValueApi;
|
|
65
|
+
}
|