typescript - Hur tillämpar man de återstående props som passerar in i komponenten och inte är deklarerade med @Prop-dekoratorn?


Jag konverterar en React-komponent jag byggt till en Stencil-webbkomponent, och jag är osäker på hur jag hämtar alla props som skickats till komponenten som inte definierats med @Prop-dekoratorn. Här är min React-kod:

import { ButtonHTMLAttributes } from "react";

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

Och här är konceptuellt hur jag vill att min Stencil-kod ska fungera:

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>
    );
  }
}

Jag vill ha möjligheten att utöka vilken prop som helst som normalt skulle kunna skickas in i , avlyssna de jag vill lägga till anpassad logik för genom att deklarera dem med @Prop och sedan sprida de återstående propsen på det faktiska elementet utan att hårdkoda hundratals attribut per anpassad komponent. Tack.

Svar #1

Nej. Det är inte möjligt. Webkomponenter är lite mer komplicerade än traditionella React-komponenter.

Webbkomponent är i grunden ett förbättrat HTMLElement. Så om du försöker sprida DOM-elementet får du ingenting:

const a = document.createElement('div');
a.tabIndex = 1;

const b = { ...a } ;
// Output: {}

Så, i ditt fall är det meningslöst att göra {...this} och också {...this.restProps} betyder ingenting eftersom egenskapen restProps inte finns på ditt anpassade element.

Att nu deklarera en egendom med @Prop-dekoreraren gör två saker.

  1. Ställ in en övervakare så att när värdet på egenskapen ändras av anroparen, triggas render automatiskt.
  2. Hjälp till att etablera ett förhållande mellan property och attribute (Detta är två olika saker. I React-världen existerar inte attribut. Allt är prop).

Ta exemplet med din core-button-komponent; du kan skapa en komponent som visas nedan och försöka lägga till count-egenskapen till komponenten. Men eftersom count inte är deklarerad som en prop, kommer Stencil inte att kunna avgöra om den ska utlösa render-metoden.

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

Som en lösning kan du samla alla egenskaper i ett vanligt objekt med en funktion och sedan använda det i din renderingsmetod som:

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
    };
  }
}

Du kan ta det ett steg längre genom att skapa en automatisk hjälpare för att uppnå detta. Använd paketet som reflect-metadata och läs all reflektionsinformation för Prop-dekorationen som är satt på din CoreButton-klass och läs sedan dessa egenskaper med undantag för den du inte behöver. Du kan sedan använda denna funktion i varje komponent där du behöver detta.

Svar #2

Min lösning är en hjälpmetod som hämtar alla föräldrats attribut, kontrollerar om de har definierats på föräldern och returnerar de återstående i en 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;
};