首页 > 解决方案 > 等待 customElement 子级渲染以强制定位它们

问题描述

我有一个组件MyWrapper,它由左侧的流动文本和右侧的边距组成。在页边距中,有一些MarginElement组件应该与<mark>文本中的标签垂直对齐。

import {html, css, LitElement} from 'lit';
import {customElement, property} from 'lit/decorators.js';

@customElement('my-wrapper')
export class MyWrapper extends LitElement {
  
  static styles = css`:host{display:flex}`

  render() {
    return html`
        <p>
          <mark id="mark1">Lorem</mark> ipsum dolor sit amet, consectetur adipiscing elit, sed
          do eiusmod tempor incididunt ut labore et dolore magna aliqua.
          Ut enim ad minim veniam, quis nostrud exercitation ullamco
          laboris nisi ut aliquip ex ea commodo consequat. Duis aute
          irure dolor in reprehenderit in voluptate velit esse <mark id="mark2">cillium</mark>.
        </p>
        <div class="margin" style="width:50%">
           <margin-element id="m1" related-element-id="mark1">Some note</margin-element>
           <margin-element id="m2" related-element-id="mark2">Some other note</margin-element>
        </div>`;
  }
  
  updated(){this.positionMargin()}
    
  positionMargin() {
    const elements = this.shadowRoot.querySelectorAll('margin-element')
    elements.forEach(el => {
      const relatedElement = this.shadowRoot.querySelector(`#${el.relatedElementId}`)
      el.style.top = relatedElement.getBoundingClientRect().top + 'px'
    })
  }
}

@customElement('margin-element')
export class MarginElement extends LitElement {
  static styles = css`:host {position: absolute; border: 1px solid red;}`

  @property({type: String, attribute: 'related-element-id'}) relatedElementId: string

  render() {return html`<slot></slot>`}
}

我想在positionMargin()渲染MyWrapper完成后运行该函数,但我知道调用relatedElement.getBoundingClientRect() margin-element本身必须被渲染。查看文档updated()似乎是最好的电话positionMargin()。然而,似乎到那<margin-element>时还没有渲染 child 。因此,getBoundingClientRect()不可用。

如何解决?

标签: javascriptweb-componentlit-elementlit-htmllit

解决方案


当您返回一个它的数组时,MarginElement它会导致它们在当前渲染更新后触发自己的渲染生命周期,但这并不可靠。

您的主要问题是您希望垂直对齐在 DOM 中相对 y 位置运行,但 x 位置堆叠在右侧。堆叠意味着<margin-element>要么需要使用某种流/网格布局,要么它们需要相互了解,这很快就会变得令人讨厌。

相反,我会添加一个新<margin-wrapper>元素,该元素负责绝对定位它的子元素<margin-element>并替换<div class="margin">.

然后在你的positionMargin函数中设置一个你传递给它的数组<margin-wrapper .marks=${this.marksWithPositions}>

然后在<margin-wrapper>'s中render,你有一个绝对可以定位的标记数组,如果你同时得到两个标记,top你可以将后面的标记移开。

就像是:

  render() {
    return html`
      <div class="wrapper">
        <div class="content">
          <p>
            <mark id="mark1">Lorem</mark> ipsum dolor sit ... 
            <mark id="mark2">cillium</mark>.
          </p>
        </div>
        <margin-wrapper .marks=${this.marksWithPositions}>
      </div>
     <button @click=${() => this.positionMargin()}>Position!</button>
    `;
  }

  positionMargin() {
    const elements = this.shadowRoot.querySelectorAll('margin-element')
    const mwp = [];

    elements.forEach(el => {
      const markWithPos ={
        top: relatedElement.getBoundingClientRect().top,
        text: e.innerText
    });

    this.marksWithPositions = mwp; // note that this needs to be an @property or @state
  }

然后在<margin-wrapper>

render() {
    const positioned = [];
    let x = 0;
    for(const m of this.marks) {
        if(m.top > x)
            positioned.push(m);

        else 
            positioned.push({ top: x, m.text });

        x += expectedElementHeight;
    }

    return html`
<div class="margin">
    ${positioned.map(p => html`
    <margin-element style=${styleMap({top: p.top + 'px'})}>${p.text}</margin-element>`)}
</div>`;
}

或者,可以使用 做一些事情ref,例如,在每次渲染<mark ${ref(e => ...)}>时触发一个事件。<mark>

要非常小心,<margin-wrapper>渲染不会导致移动<mark>元素的布局更改,因为您可以很容易地获得循环竞争条件。您可能希望到处都有明确的宽度和高度,但如果不是,您还需要调整窗口大小和滚动观察者。


推荐阅读