如何在 StencilJS 组件中安全地操作 DOM?

How can I safely manipulate DOM in a StencilJS component?

我正在尝试从使用 StencilJS 制作的组件中安全地删除 DOM 节点。

我已将删除代码放在 public 方法中 - 这正是我需要的。

但是,根据调用此方法的时刻,我遇到了问题。如果调用得太早,它还没有 DOM 节点引用 - 它是 undefined.

下面的代码显示了组件代码(使用 StencilJS)和 HTML 页面。

在页面脚本中调用 alert.dismiss() 有问题。单击按钮调用相同的方法工作正常。

有安全的方法来做到这一点remove()? StencilJS 是否提供一些资源,一些我应该测试或我应该等待的东西?

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

@Component({
  tag: 'my-alert',
  scoped: true
})

export class Alert {

  // Reference to dismiss button
  dismissButton: HTMLButtonElement;
  
  /**
   * StencilJS lifecycle methods
   */

  componentDidLoad() {
    // Dismiss button click handler
    this.dismissButton.addEventListener('click', () => this.dismiss());
  }

  // If this method is called from "click" event (handler above), everything is ok.
  // If it is called from a script executed on the page, this.dismissButton may be undefined.
  @Method()
  async dismiss() {
    // Remove button from DOM
    // ** But this.dismissButton is undefined before `render` **
    this.dismissButton.remove();
  }

  render() {
    return (
      <div>
        <slot/>
        <button ref={el => this.dismissButton = el as HTMLButtonElement} >
          Dismiss
        </button>
      </div>
    );
  }
}
<!DOCTYPE html>
<html lang="pt-br">
<head>
  <title>App</title>
</head>
<body>

  <my-alert>Can be dismissed.</my-alert>


  <script type="module">
    import { defineCustomElements } from './node_modules/my-alert/alert.js';
    defineCustomElements();
  
    (async () => {
      await customElements.whenDefined('my-alert');
      let alert = document.querySelector('my-alert');
      // ** Throw an error, because `this.dismissButton`
      // is undefined at this moment.
      await alert.dismiss(); 
    })();

  </script>
</body>
</html>

有多种方法可以删除 Stencil 中的 DOM 个节点。

最简单的方法是只调用元素上的 remove(),就像任何其他元素一样:

document.querySelector('my-alert').remove();

另一种方法是拥有一个管理 my-alert 消息的父容器。这对于通知之类的事情特别有用。

@Component({...})
class MyAlertManager {
  @Prop({ mutable: true }) alerts = ['alert 1'];

  removeAlert(alert: string) {
    const index = this.alerts.indexOf(alert);

    this.alerts = [
      ...this.alerts.slice(0, index),
      ...this.alerts.slice(index + 1, 0),
    ];
  }

  render() {
    return (
      <Host>
        {this.alerts.map(alert => <my-alert text={alert} />)}
      </Host>
    );
  }
}

还有其他选项,选择哪一个将取决于具体的用例。

更新

在您的具体情况下,我将有条件地呈现关闭按钮:

export class Alert {
  @State() shouldRenderDismissButton = true;

  @Method()
  async dismiss() {
    this.shouldRenderDismissButton = false;
  }

  render() {
    return (
      <div>
        <slot/>
        {this.shouldRenderDismissButton && <button onClick={() => this.dismiss()}>
          Dismiss
        </button>
      </div>
    );
  }
}

通常我不建议直接在 Stencil 组件中手动操作 DOM,因为这可能会导致下一次渲染出现问题,因为虚拟 DOM 与真实 [=38] 不同步=].

如果你真的需要等待组件渲染你可以使用 Promise:

class Alert {
  loadPromiseResolve;
  loadPromise = new Promise(resolve => this.loadPromiseResolve = resolve);

  @Method()
  async dismiss() {
    // Wait for load
    await this.loadPromise;

    // Remove button from DOM
    this.dismissButton.remove();
  }

  componentDidLoad() {
    this.loadPromiseResolve();
  }
}

我之前曾问过 a question about waiting for the next render 这会使它更干净一些,但我认为目前这不太容易实现。将来我可能会为此创建一个功能请求。