2

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?

Reza
  • 18,865
  • 13
  • 88
  • 163
  • 1
    How about adding `pointer-events: none` to the button? – terrymorse Dec 29 '20 at 23:02
  • @terrymorse yes, that solves the issue, btw is there any way for doing that with stencil not css? – Reza Dec 29 '20 at 23:06
  • I don't know stenciljs, sorry. But it seems strange that a disabled button would receive any click events. Did you view the button's html in the browser source window, just to be sure the disabled attribute is there? – terrymorse Dec 29 '20 at 23:32
  • @terrymorse so techincally a new component will be created around button, the problem is click on the real button and outer component are not tied together – Reza Dec 30 '20 at 00:08

3 Answers3

2

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)

Simon Hänisch
  • 4,740
  • 2
  • 30
  • 42
2

Problem Analysis

Disabled is not Disabled

One problem is, that <idv-button disabled> evaluates to this.disabled==="" (empty string), which is interpreted as false instead of true.

Chrome Captures onClick

Another problem, with the above fixed, it then does work in Firefox, but not in Chrome. Chrome does not give you a chance to handle onClickon your own, Chrome handles it.

Reserved onClick

The reserved onClick is handled completely differently. All on…-properties can take functions in a string in a HTML-environment. But if you define your own function, not named with on…-prefix, you cannot add the function directyly in HTML. So, you must define your own function with a different name, because of the special behaviour of onClick, so you need to handle it specially in plain HTML. In JSX all is as expected.

Hint: The name onClick is reserved, so it is implicit and does not need to be added as @Prop.

Function Properties in Plain HTML

If you need t add a function in an plain HTML environment (not JSX), you need to add the function separately. See also: Pass functions to stencil component.

<idv-button>...</idv-button>
<script>
  document.currentScript.previousElementSibling.clickHandler =
      function() { ... }
</script>

(add function clickHandler to the element above the script)

Simple Solution

BTW: I prefer to have the text inside the tag, so I use <slot/>

Component Definition

The following solves all these problems and works perfectly:

Component({tag: 'idv-button'})

export class IdvButton {

  @Prop() disabled?: any
  @Prop() clickHandler: (e: MouseEvent) => void;

  render() {
    return (
      <button
        disabled={this.disabled || this.disabled === ""}
        onClick={this.clickHandler}
      >
        <slot />
      </button>

    )
  }

}

Use in Non-JSX Plain-HTML

To use that in plain HTML, you need to add the function in separate JavaScript (here I add the same function to all idv-button-elements):

<html>
  <head>
    <meta charset="utf-8" />
    <script type="module" src="/build/my-webcomponents.esm.js"></script>
    <script nomodule src="/build/my-webcomponents.js"></script>
  </head>
  <body>
    <idv-button>Enabled</idv-button>
    <idv-button disabled>Disabled</idv-button>
    <script>
      var allButtonInstances = document.querySelectorAll('idv-button')
      allButtonInstances.forEach(e => e.clickHandler = function() {
        alert('here');
      })
    </script>
  </body>
</html>
Marc Wäckerlin
  • 662
  • 6
  • 7
0

I'm late to the party but, what about using {...(this.disabled || null)} instead of disabled={this.disabled}?

etb
  • 21
  • 2