Reputation: 153
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
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
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.
render
is automatically triggered.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