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

问题描述

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

我已将删除代码放入公共方法中-这正是我所需要的。

但是,根据调用方法的时间,我遇到了问题。如果调用得太早,则它还没有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>

解决方法

删除模板中的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与真实DOM不同步,这可能导致下一个渲染问题。

如果确实需要等待组件渲染,则可以使用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,这会使它更清晰一些,但我认为目前尚不容易。将来我可能会为此创建功能请求。