typescriptweb-componentstenciljs

How to apply the remaining props that are passed into component and aren't declared with the @Prop decorator?


I am converting a React component I built into a Stencil web component, and I am unsure of how to retrieve all the props passed into the component that weren't defined with the @Prop decorator. Here is my React code:

import { ButtonHTMLAttributes } from "react";

export default function CoreButton({
  name,
  children,
  ...props
}: ButtonHTMLAttributes<HTMLButtonElement>) {
  return (
    <button
      name={`example ${name}`}
      {...props}
    >
      {children}
    </button>
  );
}

And here is conceptually how I want my Stencil code to work:

import { Component, Prop, h } from '@stencil/core';

@Component({
  tag: 'core-button',
})
export class CoreButton {
  @Prop() name: string;

  render() {
    return (
      <button name={`example ${this.name}`} {...this.restProps}>
        <slot />
      </button>
    );
  }
}

I want the ability to extend any prop that would normally be able to be passed into , intercept the ones I want to add custom logic too by declaring them with @Prop and then spread the remaining props onto the actual element without hard coding 100s of attributes per custom component. Thanks.


Solution

  • Nope. That is not possible. Web components are bit more convoluted that traditional React components.

    Web Component is basically an enhanced HTMLElement So if you try to spread the DOM element, you get nothing:

    const a = document.createElement('div');
    a.tabIndex = 1;
    
    const b = { ...a } ;
    // Output: {}
    

    So, in your case doing {...this} is meaningless and also {...this.restProps} means nothing since the property restProps doesn't exist on your custom element.

    Now declaring a property using @Prop decorator is doing two things.

    1. Setup a watcher so that when the value of the propety is changed by the caller, the render is automatically triggered.
    2. Help setup up relationship between property and attribute (These are two different things. In react worlds, attributes do not exist. Everything is prop).

    Take example of you core-button component; you can create a component as shown below and try to add count property to the component. But since, the count is not declared as a prop, Stencil will not be able to figure out if it should trigger the render method.

    // Custom element
    const a = document.createElement('core-button');
    a.count= 10;
    

    As a workaround, you can collect all the properties into a plain object using some function and then use that in your render method like:

    import { Component, Prop, h } from '@stencil/core';
    
    @Component({
      tag: 'core-button',
    })
    export class CoreButton {
      @Prop() name: string;
      @Prop() count: number;
      @Prop() disabled: boolean;
    
      render() {
    
        const restProps = this.collect();
    
        return (
          <button name={`example ${this.name}`} {...restProps}>
            <slot />
          </button>
        );
      }
    
      collect() {
        return {
          count: this.count,
          disabled: this.disabled
        };
      }
    }
    

    You can take it one step further by creating a automatic helper to achieve this. Use the package like reflect-metadata and read all the reflection information for the Prop decorator set on your CoreButton class and then read those properties excluding the one that you don't need. You can then use this function in every component where you need this.