从 StencilJS 组件内部渲染 React 组件并将插槽映射到 props.children

IT技术 reactjs typescript web-component stenciljs
2021-05-09 04:01:56

我想将现有的 React 组件包装在 StencilJS 组件中。

我通过在 StencilJScomponentDidRender钩子内调用 ReactDom.render并在渲染后将其移动到 react 子元素中来工作,但我想知道是否有更好的方法来实现这一点。

我不喜欢这需要宿主内部的两个包装器元素,并且手动将插槽移动到 React 组件中感觉非常讨厌。

示例代码 - 我在这里尝试渲染的现有 React 组件是来自 react-bootstrap 项目的 Bootstrap Alert,仅作为示例。

import {
    Component,
    ComponentInterface,
    Host,
    h,
    Element,
    Prop,
    Event,
} from '@stencil/core';
import { Alert, AlertProps } from 'react-bootstrap';
import ReactDOM from 'react-dom';
import React from 'react';

@Component({
    tag: 'my-alert',
    styleUrl: 'my-alert.css',
    shadow: false,
})
export class MyAlert implements ComponentInterface, AlertProps {
    @Element() el: HTMLElement;

    @Prop() bsPrefix?: string;
    @Prop() variant?:
        | 'primary'
        | 'secondary'
        | 'success'
        | 'danger'
        | 'warning'
        | 'info'
        | 'dark'
        | 'light';
    @Prop() dismissible?: boolean;
    @Prop() show?: boolean;
    @Event() onClose?: () => void;
    @Prop() closeLabel?: string;
    @Prop() transition?: React.ElementType;

    componentDidRender() {
        const wrapperEl = this.el.getElementsByClassName('alert-wrapper')[0];
        const slotEl = this.el.getElementsByClassName('slot-wrapper')[0];

        const alertProps: AlertProps = {
            variant: this.variant,
            dismissible: this.dismissible,
            show: this.show,
            onClose: this.onClose,
            closeLabel: this.closeLabel,
            transition: this.transition,
        };

        ReactDOM.render(
            React.createElement(
                Alert,
                alertProps,
                React.createElement('div', { className: 'tmp-react-child-el-class-probs-should-be-a-guid-or-something' })
            ),
            wrapperEl
        );

        const reactChildEl = this.el.getElementsByClassName(
            'tmp-react-child-el-class-probs-should-be-a-guid-or-something'
        )[0];
        reactChildEl.appendChild(slotEl);
    }

    render() {
        return (
            <Host>
                <div class="alert-wrapper"></div>
                <div class="slot-wrapper">
                    <slot />
                </div>
            </Host>
        );
    }
}

1个回答

只是一些提示:

  • 您不需要wrapperEl,您可以将您的react组件渲染到宿主元素中this.el
  • 由于您没有使用shadow,您可以克隆组件的插槽内容并将其用作react组件的子项。
  • this.el.getElementsByClassName('slot-wrapper')[0]您也可以使用this.el.querySelector('.slot-wrapper'),或使用元素引用来代替
  • onClose不应用作props名称,因为on...这也是绑定事件处理程序的方式,例如,如果您的组件发出'click'事件,那么您可以通过onClick在组件上设置处理程序来为其绑定处理程序。虽然你已经把它装饰成一个事件,但这不起作用 afaik。

关于codesandbox.io的工作示例:
https ://codesandbox.io/s/stencil-react-mv5p4 ? file =/ src/components/my-alert/my-alert.tsx
(这也适用于重新渲染)

import {
  Component,
  ComponentInterface,
  Host,
  h,
  Element,
  Prop,
  Event,
} from '@stencil/core';
import { Alert, AlertProps } from 'react-bootstrap';
import ReactDOM from 'react-dom';
import React from 'react';

@Component({
  tag: 'my-alert',
  shadow: false,
})
export class MyAlert implements ComponentInterface {
  @Element() host: HTMLMyAlertElement;

  @Prop() bsPrefix?: string;
  @Prop() variant?:
    | 'primary'
    | 'secondary'
    | 'success'
    | 'danger'
    | 'warning'
    | 'info'
    | 'dark'
    | 'light';
  @Prop() dismissible?: boolean;
  @Prop() show?: boolean;
  @Prop() closeHandler?: () => void;
  @Prop() closeLabel?: string;
  @Prop() transition?: React.ElementType;

  originalContent: any[];

  componentDidLoad() {
    // clone the original (slotted) content
    this.originalContent = Array.from(this.host.childNodes).map(node =>
      node.cloneNode(true),
    );

    this.componentDidUpdate();
  }

  componentDidUpdate() {
    const alertProps: AlertProps = {
      variant: this.variant,
      dismissible: this.dismissible,
      show: this.show,
      onClose: this.closeHandler,
      closeLabel: this.closeLabel,
      transition: this.transition,
      ref: el => el?.append(...this.originalContent), // content injected here
    };

    ReactDOM.render(React.createElement(Alert, alertProps), this.host);
  }

  render() {
    return (
      <Host>
        <slot />
      </Host>
    );
  }
}