diff --git a/projects/demo-integrations/cypress/tests/kit/number/number-prefix-postfix.cy.ts b/projects/demo-integrations/cypress/tests/kit/number/number-prefix-postfix.cy.ts
index 1021ca5ff..dd7ec1f58 100644
--- a/projects/demo-integrations/cypress/tests/kit/number/number-prefix-postfix.cy.ts
+++ b/projects/demo-integrations/cypress/tests/kit/number/number-prefix-postfix.cy.ts
@@ -270,4 +270,43 @@ describe('Number | Prefix & Postfix', () => {
);
});
});
+
+ describe('runtime changes of postfix', () => {
+ beforeEach(() => {
+ cy.visit(DemoPath.Cypress);
+ cy.get('#runtime-postfix-changes input')
+ .focus()
+ .should('have.value', '1 year')
+ .as('input');
+ });
+
+ it('1| year => Type 0 => 10| years', () => {
+ cy.get('@input')
+ .type('{moveToStart}{rightArrow}')
+ .type('0')
+ .should('have.value', '10 years')
+ .should('have.prop', 'selectionStart', '10'.length)
+ .should('have.prop', 'selectionEnd', '10'.length);
+ });
+
+ it('10| years => Backspace => 1| year', () => {
+ cy.get('@input')
+ .type('{moveToStart}{rightArrow}')
+ .type('0')
+ .should('have.value', '10 years')
+ .type('{backspace}')
+ .should('have.value', '1 year')
+ .should('have.prop', 'selectionStart', '1'.length)
+ .should('have.prop', 'selectionEnd', '1'.length);
+ });
+
+ it('select all + delete', () => {
+ cy.get('@input')
+ .should('have.value', '1 year')
+ .type('{selectAll}{del}')
+ .should('have.value', '')
+ .should('have.prop', 'selectionStart', 0)
+ .should('have.prop', 'selectionEnd', 0);
+ });
+ });
});
diff --git a/projects/demo/src/pages/cypress/cypress.module.ts b/projects/demo/src/pages/cypress/cypress.module.ts
index 45ae709ad..8ece9432e 100644
--- a/projects/demo/src/pages/cypress/cypress.module.ts
+++ b/projects/demo/src/pages/cypress/cypress.module.ts
@@ -11,6 +11,7 @@ import {CypressDocPageComponent} from './cypress.component';
import {TestDocExample1} from './examples/1-predicate/component';
import {TestDocExample2} from './examples/2-native-max-length/component';
import {TestDocExample3} from './examples/3-mirrored-prefix-postfix/component';
+import {TestDocExample4, TestPipe4} from './examples/4-runtime-postfix-changes/component';
@NgModule({
imports: [
@@ -27,6 +28,8 @@ import {TestDocExample3} from './examples/3-mirrored-prefix-postfix/component';
TestDocExample1,
TestDocExample2,
TestDocExample3,
+ TestDocExample4,
+ TestPipe4,
],
exports: [CypressDocPageComponent],
})
diff --git a/projects/demo/src/pages/cypress/cypress.template.html b/projects/demo/src/pages/cypress/cypress.template.html
index 635ff1621..541889ab2 100644
--- a/projects/demo/src/pages/cypress/cypress.template.html
+++ b/projects/demo/src/pages/cypress/cypress.template.html
@@ -8,6 +8,10 @@
+
+
diff --git a/projects/demo/src/pages/cypress/examples/4-runtime-postfix-changes/component.ts b/projects/demo/src/pages/cypress/examples/4-runtime-postfix-changes/component.ts
new file mode 100644
index 000000000..0214af6d7
--- /dev/null
+++ b/projects/demo/src/pages/cypress/examples/4-runtime-postfix-changes/component.ts
@@ -0,0 +1,51 @@
+import {
+ ChangeDetectionStrategy,
+ Component,
+ ElementRef,
+ Pipe,
+ PipeTransform,
+ ViewChild,
+} from '@angular/core';
+import {MaskitoOptions} from '@maskito/core';
+import {maskitoNumberOptionsGenerator, maskitoParseNumber} from '@maskito/kit';
+
+@Pipe({
+ name: 'calculateMask',
+})
+export class TestPipe4 implements PipeTransform {
+ transform(postfix: string): MaskitoOptions {
+ return maskitoNumberOptionsGenerator({
+ postfix,
+ precision: 2,
+ thousandSeparator: ' ',
+ });
+ }
+}
+
+@Component({
+ selector: 'test-doc-example-4',
+ template: `
+
+ `,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class TestDocExample4 {
+ @ViewChild('inputRef', {read: ElementRef, static: true})
+ readonly inputRef!: ElementRef;
+
+ get parsedValue(): number {
+ return maskitoParseNumber(this.inputRef.nativeElement.value);
+ }
+
+ readonly pluralize = {
+ one: ` year`,
+ few: ` years`,
+ many: ` years`,
+ other: ` years`,
+ };
+}
diff --git a/projects/kit/src/lib/masks/number/number-mask.ts b/projects/kit/src/lib/masks/number/number-mask.ts
index 432a6272c..db254e52f 100644
--- a/projects/kit/src/lib/masks/number/number-mask.ts
+++ b/projects/kit/src/lib/masks/number/number-mask.ts
@@ -18,6 +18,7 @@ import {
} from './plugins';
import {
createDecimalZeroPaddingPostprocessor,
+ createInitializationOnlyPreprocessor,
createMinMaxPostprocessor,
createNonRemovableCharsDeletionPreprocessor,
createNotEmptyIntegerPartPreprocessor,
@@ -67,6 +68,11 @@ export function maskitoNumberOptionsGenerator({
isNegativeAllowed: min < 0,
}),
preprocessors: [
+ createInitializationOnlyPreprocessor({
+ decimalSeparator,
+ decimalPseudoSeparators,
+ pseudoMinuses,
+ }),
createPseudoCharactersPreprocessor(CHAR_MINUS, pseudoMinuses),
createPseudoCharactersPreprocessor(decimalSeparator, decimalPseudoSeparators),
createNotEmptyIntegerPartPreprocessor({decimalSeparator, precision}),
diff --git a/projects/kit/src/lib/masks/number/processors/index.ts b/projects/kit/src/lib/masks/number/processors/index.ts
index d0b72579f..46d6c9f67 100644
--- a/projects/kit/src/lib/masks/number/processors/index.ts
+++ b/projects/kit/src/lib/masks/number/processors/index.ts
@@ -1,4 +1,5 @@
export * from './decimal-zero-padding-postprocessor';
+export * from './initialization-only-preprocessor';
export * from './leading-zeroes-validation-postprocessor';
export * from './min-max-postprocessor';
export * from './non-removable-chars-deletion-preprocessor';
diff --git a/projects/kit/src/lib/masks/number/processors/initialization-only-preprocessor.ts b/projects/kit/src/lib/masks/number/processors/initialization-only-preprocessor.ts
new file mode 100644
index 000000000..955a818e9
--- /dev/null
+++ b/projects/kit/src/lib/masks/number/processors/initialization-only-preprocessor.ts
@@ -0,0 +1,50 @@
+import {MaskitoPreprocessor, maskitoTransform} from '@maskito/core';
+
+import {generateMaskExpression} from '../utils';
+
+/**
+ * This preprocessor works only once at initialization phase (when `new Maskito(...)` is executed).
+ * This preprocessor helps to avoid conflicts during transition from one mask to another (for the same input).
+ * For example, the developer changes postfix (or other mask's props) during run-time.
+ * ```
+ * let maskitoOptions = maskitoNumberOptionsGenerator({postfix: ' year'});
+ * // [3 seconds later]
+ * maskitoOptions = maskitoNumberOptionsGenerator({postfix: ' years'});
+ * ```
+ */
+export function createInitializationOnlyPreprocessor({
+ decimalSeparator,
+ decimalPseudoSeparators,
+ pseudoMinuses,
+}: {
+ decimalSeparator: string;
+ decimalPseudoSeparators: readonly string[];
+ pseudoMinuses: readonly string[];
+}): MaskitoPreprocessor {
+ let isInitializationPhase = true;
+ const cleanNumberMask = generateMaskExpression({
+ decimalSeparator,
+ decimalPseudoSeparators,
+ pseudoMinuses,
+ prefix: '',
+ postfix: '',
+ thousandSeparator: '',
+ precision: Infinity,
+ isNegativeAllowed: true,
+ });
+
+ return ({elementState, data}) => {
+ if (!isInitializationPhase) {
+ return {elementState, data};
+ }
+
+ isInitializationPhase = false;
+
+ return {
+ elementState: maskitoTransform(elementState, {
+ mask: cleanNumberMask,
+ }),
+ data,
+ };
+ };
+}
diff --git a/projects/kit/src/lib/masks/number/tests/number-mask.spec.ts b/projects/kit/src/lib/masks/number/tests/number-mask.spec.ts
index 4f1b49493..a43096744 100644
--- a/projects/kit/src/lib/masks/number/tests/number-mask.spec.ts
+++ b/projects/kit/src/lib/masks/number/tests/number-mask.spec.ts
@@ -1,13 +1,17 @@
-import {maskitoTransform} from '@maskito/core';
+import {MASKITO_DEFAULT_OPTIONS, MaskitoOptions, maskitoTransform} from '@maskito/core';
import {maskitoNumberOptionsGenerator} from '../number-mask';
-describe('Number', () => {
- describe('`precision` is `0` and it is paste from clipboard', () => {
- const options = maskitoNumberOptionsGenerator({
- decimalSeparator: ',',
- decimalPseudoSeparators: ['.'],
- precision: 0,
+describe('Number (maskitoTransform)', () => {
+ describe('`precision` is `0`', () => {
+ let options: MaskitoOptions = MASKITO_DEFAULT_OPTIONS;
+
+ beforeEach(() => {
+ options = maskitoNumberOptionsGenerator({
+ decimalSeparator: ',',
+ decimalPseudoSeparators: ['.'],
+ precision: 0,
+ });
});
it('drops decimal part (123,45)', () => {
@@ -17,5 +21,9 @@ describe('Number', () => {
it('drops decimal part (123.45)', () => {
expect(maskitoTransform('123.45', options)).toBe('123');
});
+
+ it('keeps minus sign (-123)', () => {
+ expect(maskitoTransform('-123', options)).toBe('−123');
+ });
});
});
diff --git a/projects/kit/src/lib/masks/number/utils/generate-mask-expression.ts b/projects/kit/src/lib/masks/number/utils/generate-mask-expression.ts
index f0d1c014f..e75e3a543 100644
--- a/projects/kit/src/lib/masks/number/utils/generate-mask-expression.ts
+++ b/projects/kit/src/lib/masks/number/utils/generate-mask-expression.ts
@@ -10,6 +10,8 @@ export function generateMaskExpression({
thousandSeparator,
prefix,
postfix,
+ decimalPseudoSeparators = [],
+ pseudoMinuses = [],
}: {
decimalSeparator: string;
isNegativeAllowed: boolean;
@@ -17,18 +19,22 @@ export function generateMaskExpression({
thousandSeparator: string;
prefix: string;
postfix: string;
+ decimalPseudoSeparators?: readonly string[];
+ pseudoMinuses?: readonly string[];
}): MaskitoMask {
const computedPrefix = computeAllOptionalCharsRegExp(prefix);
const digit = '\\d';
- const optionalMinus = isNegativeAllowed ? `${CHAR_MINUS}?` : '';
+ const optionalMinus = isNegativeAllowed
+ ? `[${CHAR_MINUS}${pseudoMinuses.map(x => `\\${x}`).join('')}]?`
+ : '';
const integerPart = thousandSeparator
? `[${digit}${escapeRegExp(thousandSeparator)}]*`
: `[${digit}]*`;
const decimalPart =
precision > 0
- ? `(${escapeRegExp(decimalSeparator)}${digit}{0,${
- Number.isFinite(precision) ? precision : ''
- }})?`
+ ? `([${escapeRegExp(decimalSeparator)}${decimalPseudoSeparators
+ .map(escapeRegExp)
+ .join('')}]${digit}{0,${Number.isFinite(precision) ? precision : ''}})?`
: '';
const computedPostfix = computeAllOptionalCharsRegExp(postfix);