Skip to content

Commit

Permalink
feat(core): new built-in maskitoChangeEventPlugin (#1338)
Browse files Browse the repository at this point in the history
  • Loading branch information
nsbarsukov authored Jun 26, 2024
1 parent 7d7e6f9 commit daa04d2
Show file tree
Hide file tree
Showing 12 changed files with 204 additions and 9 deletions.
7 changes: 5 additions & 2 deletions projects/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ export {
MASKITO_DEFAULT_OPTIONS,
} from './lib/constants';
export {Maskito} from './lib/mask';
export {
maskitoChangeEventPlugin,
maskitoInitialCalibrationPlugin,
maskitoStrictCompositionPlugin,
} from './lib/plugins';
export {
MaskitoElement,
MaskitoElementPredicate,
Expand All @@ -15,9 +20,7 @@ export {
} from './lib/types';
export {
maskitoAdaptContentEditable,
maskitoInitialCalibrationPlugin,
maskitoPipe,
maskitoStrictCompositionPlugin,
maskitoTransform,
maskitoUpdateElement,
} from './lib/utils';
30 changes: 30 additions & 0 deletions projects/core/src/lib/plugins/change-event-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type {MaskitoPlugin} from '../types';

export function maskitoChangeEventPlugin(): MaskitoPlugin {
return element => {
if (element.isContentEditable) {
return;
}

let value = element.value;

const valueListener = (): void => {
value = element.value;
};
const blurListener = (): void => {
if (element.value !== value) {
element.dispatchEvent(new Event('change', {bubbles: true}));
}
};

element.addEventListener('focus', valueListener);
element.addEventListener('change', valueListener);
element.addEventListener('blur', blurListener);

return () => {
element.removeEventListener('focus', valueListener);
element.removeEventListener('change', valueListener);
element.removeEventListener('blur', blurListener);
};
};
}
3 changes: 3 additions & 0 deletions projects/core/src/lib/plugins/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './change-event-plugin';
export * from './initial-calibration-plugin';
export * from './strict-composition-plugin';
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type {MaskitoOptions, MaskitoPlugin} from '../types';
import {maskitoUpdateElement} from './dom/update-element';
import {maskitoTransform} from './transform';
import {maskitoTransform, maskitoUpdateElement} from '../utils';

export function maskitoInitialCalibrationPlugin(
customOptions?: MaskitoOptions,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import type {ElementState, MaskitoPlugin, TypedInputEvent} from '../types';
import {maskitoUpdateElement} from './dom/update-element';
import {areElementStatesEqual} from './element-states-equality';
import {maskitoTransform} from './transform';
import {areElementStatesEqual, maskitoTransform, maskitoUpdateElement} from '../utils';

export function maskitoStrictCompositionPlugin(): MaskitoPlugin {
return (element, maskitoOptions) => {
Expand Down
2 changes: 0 additions & 2 deletions projects/core/src/lib/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,5 @@ export * from './element-states-equality';
export * from './get-line-selection';
export * from './get-not-empty-selection';
export * from './get-word-selection';
export * from './initial-calibration-plugin';
export * from './pipe';
export * from './strict-composition-plugin';
export * from './transform';
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type {MaskitoOptions} from '@maskito/core';
import {maskitoChangeEventPlugin} from '@maskito/core';
import {maskitoNumberOptionsGenerator} from '@maskito/kit';
import {createOutputSpy} from 'cypress/angular';

import {TestInput} from '../utils';

describe('maskitoChangeEventPlugin', () => {
const numberMask = maskitoNumberOptionsGenerator({
thousandSeparator: ' ',
decimalSeparator: '.',
precision: 2,
});
const maskitoOptions: MaskitoOptions = {
...numberMask,
plugins: [...numberMask.plugins, maskitoChangeEventPlugin()],
};

beforeEach(() => {
cy.mount(TestInput, {
componentProperties: {
maskitoOptions,
change: createOutputSpy('changeEvent'),
},
});
});

it('Enter only valid value (Maskito does not prevent any typed character) => only 1 change event on blur', () => {
cy.get('input').type('123').should('have.value', '123');
cy.get('@changeEvent').should('not.be.called');
cy.get('input').blur();
cy.get('@changeEvent').should('have.callCount', 1);
});

it('Enter valid value + pseudo decimal separator (Maskito replaces pseudo separator with valid one) => only 1 change event on blur', () => {
cy.get('input').type('123,').should('have.value', '123.');
cy.get('@changeEvent').should('not.be.called');
cy.get('input').blur();
cy.get('@changeEvent').should('have.callCount', 1);
});

it('Enter only decimal separator (Maskito pads it with zero) => only 1 change event on blur', () => {
cy.get('input').type('.').should('have.value', '0.');
cy.get('@changeEvent').should('not.be.called');
cy.get('input').blur();
cy.get('@changeEvent').should('have.callCount', 1);
});

it('Enter only invalid value (Maskito rejects all typed characters) => no change event', () => {
cy.get('input').type('abc').should('have.value', '');
cy.get('@changeEvent').should('not.be.called');
cy.get('input').blur();
cy.get('@changeEvent').should('not.be.called');
});

it('Enter any value value and then erase it again => no change event', () => {
cy.get('input').type('123').should('have.value', '123');
cy.get('@changeEvent').should('not.be.called');
cy.get('input').clear().blur();
cy.get('@changeEvent').should('not.be.called');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {MASKITO_DEFAULT_ELEMENT_PREDICATE} from '@maskito/core';
[attr.value]="initialValue"
[maskito]="maskitoOptions"
[maskitoElement]="maskitoElementPredicate"
(change)="change.emit($event)"
(input)="input.emit($event)"
/>
`,
Expand All @@ -31,6 +32,9 @@ export class TestInput {
@Output()
public input = new EventEmitter();

@Output()
public change = new EventEmitter();

@Input()
public maxLength = Infinity;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {ChangeDetectionStrategy, Component} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {MaskitoDirective} from '@maskito/angular';
import {TuiInputModule} from '@taiga-ui/kit';

import mask from './mask';

@Component({
standalone: true,
selector: 'plugins-change-event-doc-example-4',
imports: [FormsModule, MaskitoDirective, TuiInputModule],
template: `
<tui-input
[maskito]="maskitoOptions"
[style.max-width.rem]="20"
[(ngModel)]="value"
>
Enter number
<input
tuiTextfield
[maskito]="maskitoOptions"
(change)="log($event)"
/>
</tui-input>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PluginsDocExample4 {
protected readonly maskitoOptions = mask;
protected value = '';

protected log(anything: any): void {
console.info(anything);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type {MaskitoOptions} from '@maskito/core';
import {maskitoChangeEventPlugin} from '@maskito/core';
import {maskitoNumberOptionsGenerator} from '@maskito/kit';

const numberOptions = maskitoNumberOptionsGenerator({
precision: 2,
});

export default {
...numberOptions,
plugins: [
...numberOptions.plugins,
maskitoChangeEventPlugin(), // <--- Enable it
],
} as MaskitoOptions;
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {NextStepsComponent} from '../next-steps/next-steps.component';
import {PluginsDocExample1} from './examples/1-reject/component';
import {PluginsDocExample2} from './examples/2-initial-calibration/component';
import {PluginsDocExample3} from './examples/3-strict-composition/component';
import {PluginsDocExample4} from './examples/4-change-event/component';

@Component({
standalone: true,
Expand All @@ -22,6 +23,7 @@ import {PluginsDocExample3} from './examples/3-strict-composition/component';
PluginsDocExample1,
PluginsDocExample2,
PluginsDocExample3,
PluginsDocExample4,
],
templateUrl: './plugins.template.html',
changeDetection: ChangeDetectionStrategy.OnPush,
Expand All @@ -47,4 +49,10 @@ export default class PluginsDocPageComponent {
'./examples/3-strict-composition/mask.ts?raw'
),
};

protected readonly changeEventExample: TuiDocExample = {
[DocExamplePrimaryTab.MaskitoOptions]: import(
'./examples/4-change-event/mask.ts?raw'
),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,5 +82,44 @@
<plugins-strict-composition-doc-example-3 />
</tui-doc-example>

<tui-doc-example
id="change-event"
heading="Built-in plugin for change event"
[content]="changeEventExample"
[description]="changeEventDescription"
>
<ng-template #changeEventDescription>
Native
<a
href="https://developer.mozilla.org/en-US/docs/Web/API/Element/beforeinput_event"
target="_blank"
tuiLink
>
<code>beforeinput</code>
</a>
event default behavior is cancelled to process user entered invalid value. This causes native
<a
href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/change_event"
target="_blank"
tuiLink
>
<code>change</code>
</a>
event to
<strong>NOT</strong>
be dispatched by browser. A
<code>change</code>
event, as opposed to
<code>input</code>
, is triggered only when user left the field and value was changed during interaction. If you rely on this
behavior, add
<code>maskitoChangeEventPlugin</code>
to your mask configuration. It will dispatch synthetic
<code>change</code>
event using the same logic.
</ng-template>
<plugins-change-event-doc-example-4 />
</tui-doc-example>

<next-steps />
</tui-doc-page>

0 comments on commit daa04d2

Please sign in to comment.