taiga-family/maskito

๐Ÿž - Plugins, that set value into input don't call onInput in React

aktanoff opened this issue ยท 8 comments

Which package(s) are the source of the bug?

@maskito/kit

Playground Link

https://stackblitz.com/edit/stackblitz-starters-chezz9?description=React%20%20%20TypeScript%20starter%20project&file=src%2FApp.tsx&title=React%20Starter

Description

Leading zero plugin and others doesn't work on blur, it sets the value, but this change don't fire in React's syntheticevent handler onInput, and value in state and value in input become different.

By little research i found out that it's because bubbles sets to false by default, and react don't handle events like this.
I've made a little example of how this works without maskito (just with react and dispatchEvent)
https://codesandbox.io/p/sandbox/pensive-shamir?file=%2Fsrc%2FApp.tsx%3A47%2C40
You can click here on buttons and it will dispatch events with and without bubbles parameter.

And example of how this works with maskito
https://stackblitz.com/edit/stackblitz-starters-chezz9?description=React%20%20%20TypeScript%20starter%20project&file=src%2FApp.tsx&title=React%20Starter

  1. Open link
  2. Type '000123'
  3. Blur input somehow

Expected:
Value above input is '123'

Actual:
Value above input is '000 123'

To fix you can just add bubbles: true in plugins dispatchEvents
For example here https://github.com/taiga-family/maskito/blob/main/projects/kit/src/lib/masks/number/plugins/leading-zeroes-validation.plugin.ts#L35C29-L35C29

Maskito version

1.9.0

Which browsers have you used?

  • Chrome
  • Firefox
  • Safari
  • Edge

Which operating systems have you used?

  • macOS
  • Windows
  • Linux
  • iOS
  • Android

@aktanoff
Thanks for your research!

I think we can create utility maskitoUpdateValue (@maskito/kit)

export function maskitoUpdateValue(
    element: HTMLInputElement | HTMLTextAreaElement,
    value: string,
): void {
    element.value = value;
    element.dispatchEvent(
        new Event(
            'input',
            /**
             * Good explanation why we need it
             */
            {bubbles: true},
        ),
    );
}

And use it everywhere inside all plugins.
@aktanoff Do you want to contribute to our project ?

maskitoUpdateValue or maskitoSetElementValue ? ๐Ÿค”

UPDATE: i think that the second option is better

What do you think ?

@nsbarsukov,
thanks for answer!
yeah, i can contribute, but did i got it right?
we need to write the utility, export it outside of the package, and replace with it all dispatchEvents calls in @maskito/kit plugins?

we need to write the utility, export it outside of the package, and replace with it all dispatchEvents calls in @maskito/kit plugins?

@aktanoff yep, all's right)

I will really appreciate if you also add a simple unit test for react package (inside this folder) for this case. But it is optional, if you will have enough time for it.

I forgot that @maskito/core also contains a single built-in plugin maskitoInitialCalibrationPlugin with the same behaviour:

element.value = maskitoTransform(element.value, customOptions || options);
element.dispatchEvent(new Event('input'));

Let's put utility maskitoSetElementValue inside @maskito/core package.

maskitoUpdateValue or maskitoSetElementValue ? ๐Ÿค”

UPDATE: i think that the second option is better

What do you think ?

I like maskitoUpdateValue better, since it does not just set value but updates everybody through event. And also it is shorter, 4 words seem too long to me :)

I like maskitoUpdateValue better, since it does not just set value but updates everybody through event. And also it is shorter, 4 words seem too long to me :)

I agree that maskitoSetElementValue is too long.
However, maskitoUpdateValue is not enough descriptive โ€“ it is not obvious that it updates native element value (and not internal Maskito's value or something else).

I have one more proposal โ€“ maskitoUpdateElement:

import {ElementState} from '../types';

export function maskitoUpdateElement(
    element: HTMLInputElement | HTMLTextAreaElement,
    valueOrElementState: ElementState | string,
): void {
    if (typeof valueOrElementState === 'string') {
        element.value = valueOrElementState;
    } else {
        const [from, to] = valueOrElementState.selection;

        element.value = valueOrElementState.value;
        element.setSelectionRange?.(from, to);
    }

    element.dispatchEvent(
        new Event(
            'input',
            /**
             * Good explanation why we need it
             */
            {bubbles: true},
        ),
    );
}

For example, we can use it here:

element.value = maskitoTransform(element.value, customOptions || options);
element.dispatchEvent(new Event('input'));
element.setSelectionRange?.(from, to);

maskitoUpdateElement(element, {
    value: maskitoTransform(element.value, customOptions || options),
    selection: [from, to],
});

@waterplea @aktanoff what do you think about it ?

maskitoUpdateElement +1