Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): new built-in maskitoChangeEventPlugin #1338

Merged
merged 5 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>
Loading