javascriptstenciljsstencil-component

How to prevent onClick on disabled button in StencilJs


I have created my own custom button as below


@Component({
    tag: "idv-button",
    styleUrl: "idv-button.scss",
    shadow: true,
})
export class IdvButton {
    @Prop({ reflect: true }) text: string;
    @Prop({ reflect: true }) disabled: boolean = false;

    render(): any {
        return (
            <button disabled={this.disabled} onClick={this.onClick}>
                <span class="btn-text">{this.text}</span>
            </button>
        );
    }

    private onClick(event: Event): void {
        if (this.disabled) {
            console.log("prevent default", event); // it doesn't come here at all
            event.preventDefault();
        }
    }

}

and used it like this in another component in stenciljs

                        <idv-button
                            text="my button"
                            disabled={true}
                            onClick={this.startClick.bind(this)}>
                        </idv-button>

the problem is although the button is disabled and even I tried to prevent default, onClick still happening and this.startClick is called

How can I fix this issue?


Solution

  • I think there's some confusion about how the on... attributes work with custom elements. On a Stencil component, if an attribute starts with on, then it becomes a handler for the event with the same name as the rest of the attribute (case is converted automatically). E. g. if your component emits an event called myEvent (using Stencil's @Event decorator and EventEmitter type), then you can add a listener for that event to your component using an onMyEvent attribute. This is explained with an example in the docs here.

    In your case, you're adding an onClick attribute to your component, which handles the click event for that element, which is a native DOM event that doesn't need to be set up, i. e. any HTML or custom element triggers a click event when it is clicked (not just buttons).

    So even if your component was just like

    @Component({ tag: "idv-button" })
    export class IdvButton {
        render() {
            return <p>Hello, World!</p>;
        }
    }
    

    and you used it like

    <idv-button onClick={console.log} />
    

    you'd still receive the click event every time you click the text.


    What I think you're trying to achieve is to pass the onClick handler through to the underlying button. The easiest way to do so is to pass the handler function as a prop.

    import { Component, h, Host, Prop } from '@stencil/core';
    
    @Component({ tag: "idv-button", shadow: true })
    export class IdvButton {
        @Prop() disabled?: boolean;
        @Prop() clickHandler: (e: MouseEvent) => void;
    
        render() {
            return (
                <button disabled={this.disabled} onClick={this.clickHandler.bind(this)}>
                    <slot />
                </button>
            );
        }
    }
    
    <idv-button clickHandler={console.log}>
        My Button
    </idv-button>
    

    Not sure you actually need to bind this, and also changed it to pass the content as a slot but feel free to discard that suggestion.

    BTW it's quite tempting to call the prop onClick but that would clash with the on... event handling for the native click event, so Stencil would warn you about that at compile-time.


    Another solution is to add pointer-events: none to the host when the disabled prop is set. This is what Ionic's ion-button does (see button.tsx#L220 and button.scss#L70-L74).

    In your case it would be something like

    import { Component, Host, h, Prop } from '@stencil/core';
    
    @Component({ tag: "idv-button", shadow: true })
    export class IdvButton {
        @Prop() disabled?: boolean;
    
        render() {
            return (
                <Host style={{ pointerEvents: this.disabled ? 'none' : undefined }}>
                    <button disabled={this.disabled}>
                        <span class="btn-text">
                            <slot />
                        </span>
                    </button>
                </Host>
            );
        }
    }
    

    (the onClick attribute would work automatically on your component because it's a default event)