最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Nested LitElement component modal closing on re-render - Stack Overflow

programmeradmin2浏览0评论

I have a parent component called ams-card like below. Some code removed to keep it short, but full version here

Visually, put together it looks like this:

When you click on a spool, it opens a modal with more information. The issue I am having though, is something is triggering it to re-render, which closes the modal. I'm assuming at this point, its coming from the parent ams-card, but I am a little unsure.

I have an instance of these where I only render the spool once in its own right, and that never gets closed due to updates, hence why I think perhaps its something to do with the repeating, and maybe some reference clashes or something. I'm honestly not sure at this point

ams-card.ts

@customElement(AMS_CARD_NAME)
export class AMS_CARD extends LitElement {
  // private states
  @state() private _subtitle;
  //etc
  @state() private _isLoading = true;

  @provide({ context: hassContext })
  @state()
  private _hass?;

  @provide({ context: deviceEntitesContext })
  private _deviceEntities: any;

  @provide({ context: infoBarContext })
  private _infoBar: {
    active: boolean;
    title: string;
    sensors: { humidity: any; temperature: any };
  } = {
    active: false,
    title: "",
    sensors: {
      humidity: null,
      temperature: null,
    },
  };

  config: any;

  set hass(hass) {
    this._hass = hass;
    this._states = hass.states;
    this.fetchDevices();
  }

  private async fetchDevices() {
    this._isLoading = true;
    try {
      await this.filterBambuDevices();
      this._infoBar.sensors = this.returnInfoBarSensors();
    } finally {
      this._isLoading = false;
    }
  }

  setConfig(config) {
    if (!config.ams) {
      throw new Error("You need to select an AMS");
    }
    this.config = config;

    this._subtitle = config.subtitle === "" ? nothing : config.subtitle;
    this._deviceEntities = config.entities;
    this._deviceId = config.ams;
    this._style = config.style;
    this._infoBar = {
      active: config.show_info_bar ? true : false,
      title: config.subtitle === "" ? nothing : config.subtitle,
      sensors: {
        humidity: null,
        temperature: null,
      },
    };
    this._showInfoBar = config.show_info_bar ? true : false;
    this._showType = config.show_type ? true : false;
    this._customHumidity = config.custom_humidity === "" ? nothing : config.custom_humidity;
    this._customTemperature =
      config.custom_temperature === "" ? nothing : config.custom_temperature;
  }

  render() {
    if (this._isLoading) return nothing;
      return html` <vector-ams-card .showType=${this._showType} /> `;
  }

  private returnInfoBarSensors() {
    const sensors: { humidity: any; temperature: any } = {
      humidity: null,
      temperature: null,
    };
    //do stuff
    return sensors;
  }
}

this returns vector-ams-card like so:

  @consume({ context: deviceEntitesContext, subscribe: true })
  private _entities;

  @property() public showType;

  static styles = styles;

  render() {
    return html`
      <ha-card class="ha-bambulab-vector-ams-card">
        <div class="v-wrapper">
          <info-bar></info-bar>
          <div class="v-ams-container">
            ${this._entities?.spools?.map(
              (spool: { entity_id: string }) => html`
                <ha-bambulab-spool
                  .key="${spool.entity_id}"
                  style="padding: 0px 5px"
                  .entity_id="${spool.entity_id}"
                  .show_type=${this.showType}
                ></ha-bambulab-spool>
              `
            )}
          </div>
        </div>
      </ha-card>
    `;
  }
}

which finally calls the spool component and modal here

import { customElement, property, state } from "lit/decorators.js";
import { html, LitElement, nothing } from "lit";
import styles from "./spool.styles";
import "../dialog/dialog";
import { getContrastingTextColor } from "../../../utils/helpers";
import { hassContext } from "../../../utils/context";
import { consume } from "@lit/context";

@customElement("ha-bambulab-spool")
export class Spool extends LitElement {
  @consume({ context: hassContext, subscribe: true })
  private hass;

  @property({ type: Boolean }) public show_type: boolean = false;
  @property({ type: String }) public entity_id;

  @state() private color;
  @state() private name;
  @state() private active;
  @state() private remaining;
  @state() private tag_uid;
  @state() private state;
  @state() private remainHeight: number = 95;
  @state() private resizeObserver: ResizeObserver | null = null;
  @state() private _dialogOpen: boolean = false;

  static styles = styles;

  connectedCallback() {
    super.connectedCallback();
    this.updateFromHass();

    // Create a bound instance method to avoid creating new functions on each resize
    this._handleResize = this._handleResize.bind(this);

    // Start observing the parent element for size changes
    this.resizeObserver = new ResizeObserver(this._handleResize);
    const rootNode = this.getRootNode() as ShadowRoot;
    const parent = this.parentElement || (rootNode instanceof ShadowRoot ? rootNode.host : null);
    if (parent) {
      this.resizeObserver.observe(parent);
    }
  }

  private _handleResize() {
    // Only update if the component is still connected to the DOM
    if (this.isConnected) {
      this.calculateHeights();
      this.updateLayers();
    }
  }

  disconnectedCallback() {
    super.disconnectedCallback();
    if (this.resizeObserver) {
      this.resizeObserver.disconnect();
      this.resizeObserver = null;
    }
  }

  firstUpdated() {
    this.updateLayers();
  }

  private _handleClick() {
    this._dialogOpen = true;
  }

  private _closeDialog() {
    this._dialogOpen = false;
  }

  render() {
    console.log("spool component render");
    return html`
      ${this.modal()}
      <div class="ha-bambulab-spool-card-container">
        <div
          class="ha-bambulab-spool-card-holder"
          style="border-color: ${this.active
            ? this.hass.states[this.entity_id]?.attributes.color
            : "#808080"}"
          @click=${this._handleClick}
        >
          <div class="ha-bambulab-spool-container">
            <div class="ha-bambulab-spool-side"></div>
            <div
              class="string-roll-container"
              style="${this.active ? "animation: wiggle 3s linear infinite" : nothing}"
            >
              <div
                class="v-string-roll"
                id="v-string-roll"
                style="background: ${this.hass.states[this.entity_id]?.attributes
                  .color}; height: ${this.remainHeight.toFixed(2)}%"
              >
                ${this.active ? html`<div class="v-reflection"></div>` : nothing}
                ${this.hass.states[this.entity_id]?.attributes?.remain > 0
                  ? html`
                      <div class="remaining-percent">
                        <p>${this.hass.states[this.entity_id]?.attributes?.remain}%</p>
                      </div>
                    `
                  : nothing}
              </div>
            </div>
            <div class="ha-bambulab-spool-side"></div>
          </div>
        </div>
        ${this.show_type
          ? html` <div class="ha-bambulab-spool-info-container">
              <div class="ha-bambulab-spool-info-wrapper">
                <div class="ha-bambulab-spool-info">
                  ${this.hass.states[this.entity_id]?.attributes.name}
                </div>
              </div>
            </div>`
          : nothing}
      </div>
    `;
  }

  modal() {
    if (!this._dialogOpen) return nothing;

    console.log("spool component modal rendered");
    return html`
      <ha-dialog
        id="confirmation-popup"
        .open=${this._dialogOpen}
        @closed=${this._closeDialog}
        heading="title"
      >
        <ha-dialog-header slot="heading">
          <ha-icon-button slot="navigationIcon" dialogAction="cancel"
            ><ha-icon icon="mdi:close"></ha-icon
          ></ha-icon-button>
          <div slot="title">${this.hass.states[this.entity_id].attributes.friendly_name}</div>
        </ha-dialog-header>
        <div class="ha-bambulab-spool-modal-container">
          <div class="filament-title section-title">Filament Information</div>
          <div class="div2 item-title">Filament</div>
          <div class="div3 item-value">${this.hass.states[this.entity_id].attributes.name}</div>
          <div class="div4 item-value">
            <span
              style="background-color: ${this.hass.states[this.entity_id].attributes
                .color}; color: ${getContrastingTextColor(
                this.hass.states[this.entity_id].attributes.color
              )}; padding: 5px 10px; border-radius: 5px;"
              >${this.hass.states[this.entity_id].attributes.color}</span
            >
          </div>
          <div class="div5 item-title">Color</div>
          <div class="div6 section-title">Nozzle Temperature</div>
          <div class="div7 item-title">Minimum</div>
          <div class="div8 item-value">
            ${this.hass.states[this.entity_id].attributes.nozzle_temp_min}
          </div>
          <div class="div9 item-value ">
            ${this.hass.states[this.entity_id].attributes.nozzle_temp_max}
          </div>
          <div class="div10 item-title">Maximum</div>
          <div class="action-buttons">
            <mwc-button class="action-button" @click=${this._closeDialog}>Load</mwc-button>
            <mwc-button class="action-button" @click=${this._closeDialog}>Unload</mwc-button>
          </div>
        </div>
      </ha-dialog>
    `;
  }

  updateLayers() {
    // Query the #string-roll element inside this component's shadow DOM
    const stringRoll = (this.renderRoot as ShadowRoot).getElementById("v-string-roll");
    if (!stringRoll) return;

    const stringWidth = 2; // matches .string-layer width in CSS
    const rollWidth = stringRoll.offsetWidth; // container width

    // Calculate how many lines fit
    const numLayers = Math.floor(rollWidth / (stringWidth * 2)); // 2 = line width + gap

    // Clear previous layers
    const previousLayers = this.renderRoot.querySelectorAll(".v-string-layer");
    previousLayers.forEach((layer) => layer.remove());

    // Add new layers
    for (let i = 0; i < numLayers; i++) {
      const layer = document.createElement("div");
      layer.classList.add("v-string-layer");

      // Calculate left position = (index + 1) * (width*2) - width
      const leftPosition = (i + 1) * (stringWidth * 2) - stringWidth;
      layer.style.left = `${leftPosition}px`;

      stringRoll.appendChild(layer);
    }
  }

  isAllZeros(str) {
    return /^0+$/.test(str);
  }

  calculateHeights() {
    // Skip calculation if modal is open to prevent unwanted updates
    if (this._dialogOpen) return;

    const maxHeightPercentage = 95;
    const minHeightPercentage = 12;

    // If not a Bambu Spool or remaining is less than 0
    if (
      this.isAllZeros(this.hass.states[this.entity_id]?.attributes.tag_uid) ||
      this.hass.states[this.entity_id]?.attributes?.remain < 0
    ) {
      this.remainHeight = maxHeightPercentage;
    } else {
      // Get the container's height
      const container = this.renderRoot.querySelector(
        ".string-roll-container"
      ) as HTMLElement | null;
      const containerHeight = container?.offsetHeight || 0;

      // Calculate heights in pixels
      const maxHeightPx = containerHeight * (maxHeightPercentage / 100);
      const minHeightPx = containerHeight * (minHeightPercentage / 100);

      // Calculate remain height based on the remain percentage
      const remainPercentage = Math.min(
        Math.max(this.hass.states[this.entity_id]?.attributes?.remain, 0),
        100
      );
      this.remainHeight = minHeightPx + (maxHeightPx - minHeightPx) * (remainPercentage / 100);

      // Convert back to percentage of container
      this.remainHeight = (this.remainHeight / containerHeight) * 100;
    }

    // Ensure remainHeight is always a number and doesn't exceed maxHeightPercentage
    this.remainHeight = Math.min(
      Number(this.remainHeight) || maxHeightPercentage,
      maxHeightPercentage
    );
    this.requestUpdate();
  }

  // Add willUpdate lifecycle method to handle hass changes
  willUpdate(changedProperties) {
    if (changedProperties.has("hass")) {
      // Skip the update if dialog is open to prevent unwanted re-renders
      if (!this._dialogOpen) {
        this.updateFromHass();
      }
    }
  }

  // New method to handle state updates
  private updateFromHass() {
    if (!this.hass || !this.entity_id) return;
    console.log("updateFromHass");

    const newActive =
      this.hass.states[this.entity_id]?.attributes.active ||
      this.hass.states[this.entity_id]?.attributes.in_use
        ? true
        : false;

    // Only update if the active state has changed
    if (this.active !== newActive) {
      this.active = newActive;
      // Only recalculate heights if dialog is not open
      if (!this._dialogOpen) {
        this.calculateHeights();
      }
    }
  }
}
发布评论

评论列表(0)

  1. 暂无评论