Позиция CustomElement после выполнения метода render()

У меня есть компонент, который состоит из плавного текста слева и поля справа. На полях есть компоненты, которые должны быть выровнены по вертикали с <mark>теги в тексте. Вот демонстрационная реализация с использованием Lit Playground.

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

@customElement('my-wrapper')
export class MyWrapper extends LitElement {
  
  static styles = css`
      .wrapper{display:flex}
      .content{width:75vw}
      .margin{width:25vw}
      p{margin:0}
      `

  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>
        <div class="margin">
           <margin-element id="m1" related-element-id="mark1">Some note</margin-element>
           <margin-element id="m2" related-element-id="mark2">Other note</margin-element>
        </div>
      </div>
     <button @click=${() => this.positionMargin()}>Position!</button>
    `;
  }
  
  positionMargin() {
    const elements = this.shadowRoot.querySelectorAll('margin-element')
    console.log(elements)
    elements.forEach(el => {
      const relatedElement = this.shadowRoot.querySelector(`#${el.relatedElementId}`)
      console.log(relatedElement)
      el.style.top = relatedElement.getBoundingClientRect().top + 'px'
    })
  }
}
}

Я хотел бы запустить функцию после завершения рендеринга, но я понимаю, что для вызова relatedElement.getBoundingClientRect() margin-element сам должен быть визуализирован.

Глядя на жизненный цикл lit с использованием await this.hasUpdatedкажется близким, но игнорирует состояние рендеринга дочерних customElements. Запрос s и использование await Promise.all(elements.map(el => el.hasUpdated)не работает, поскольку таким образом невозможно получить доступ к обещанию. Как ни странно, это работает, если <margin-element>s не создаются с использованием html-литералов, а создают объекты отдельно и передают их в html-литерал следующим образом:

       renderMargin() {
    ['mark1', 'mark2'].forEach((e) => {
       const el = new MarginElement
       el.relatedElementId = e
       el.text = e
       this.marginElements.push(el)
    })
    return this.marginElements
  }

<div class="margin" style="width:50%">
  ${this.renderMargin()}
</div>

Можно было бы вызвать marginElementRendered событие, послушайте это на MyWrapper и запускать, но это кажется странным.

Есть ли лучший способ добиться этого в освещении?

ps: перемещение позиционирования в MarginElement нет варианта, потому что полноценный positionMargin() будет следить за тем, чтобы элементы не перекрывались, что требует.

Ценю вашу помощь

2 ответа

Благодаря помощи команды Lit (спасибо!) Я смог разобраться. Решение состоит в том, чтобы дождаться завершения рендеринга дочерними элементами, прежде чем выполнять обещание.

Этого можно добиться, запросив все <margin-element> детей и ждут своих updateCompleteобещание разрешить (см. также здесь).

Таким образом, приведенный выше код работает после того, как я добавил

      async getUpdateComplete() {
        await super.getUpdateComplete();
        const marginElements = Array.from(this.shadowRoot.querySelectorAll('margin-element'));
        await Promise.all(marginElements.map(el => el.updateComplete));
        return true;
    }

Вот освещенная игровая площадка с полным примером.

Когда вы возвращаете массив MarginElement это вроде работает, потому что заставляет их запускать свои собственные жизненные циклы рендеринга после обновления текущего рендеринга, но это не будет надежным.

Ваша основная проблема заключается в том, что вам нужно вертикальное выравнивание, которое выполняется с относительной y-позицией в DOM, но x-позиция складывается с правой стороны. Укладка означает, что либо нужно использовать какой-то макет потока / сетки, либо им нужно знать друг друга, что быстро становится неприятным.

Вместо этого я бы добавил новый элемент, который отвечает за абсолютное позиционирование его дочернего элемента. <margin-element> и заменяет <div class="margin">.

Тогда в вашем positionMargin функция устанавливает массив, который вы ей передаете <margin-wrapper .marks=${this.marksWithPositions}>;

Тогда в 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
  }

Затем в :

      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 => ...)}>, чтобы запускать событие при каждом рендеринге.

Будьте очень осторожны, чтобы <margin-wrapper>рендеринг не вызывает изменение макета, которое перемещает <mark>элементов, так как вы можете очень легко получить условия круговой гонки. Вероятно, вам нужны явные значения ширины и высоты везде, но если нет, вам также понадобятся наблюдатели для изменения размера окна и прокрутки.

Другие вопросы по тегам