Sam Scolari
Sam Scolari

Reputation: 153

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.

Upvotes: 2

Views: 749

Answers (2)

vaaski
vaaski

Reputation: 1

My solution is a utility function that gets all the parents attributes, checks if they've been defined on the parent and returns the remaining ones in a Record<string, string>.

/**
 * Forwards all undeclared attributes from an element to a child.
 *
 * This is useful if you don't want to redefine all HTML attributes on a wrapping component.
 *
 * @example
 * The component `Input` forwards all undeclared attributes to the underlying `input` element.
 *
 * ```tsx
 * <div class="component-wrapper">
 *   <input {...forwardProps(this.el)} />
 * </div>
 * ```
 *
 * @param element The element to forward props from. Probably `this.el`
 * @returns A record of all attributes on the element that are not already defined on the component
 */
export const forwardProps = (element: HTMLElement): Record<string, string> => {
  if (!element.hasAttributes()) return {};

  const attributes = {};
  for (const attribute of element.attributes) {
    if (!(attribute.name in element)) {
      attributes[attribute.name] = attribute.value;
    }
  }

  return attributes;
};

Upvotes: 0

Harshal Patil
Harshal Patil

Reputation: 20980

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.

Upvotes: 2

Related Questions