JavaScript querySelectorAll() 仍然发现 DOM 个已被删除的元素

JavaScript querySelectorAll() still discovers DOM elements that have been removed

问题

我正在创建一个寻宝 Web 应用程序,它允许您动态添加和删除寻宝点。我分别通过 .createElement() 和 .remove() 方法执行此操作。

配置完所有点后,我使用 querySelectorAll() 获取所有元素(每个节点都是使用自定义 Web 组件创建的),遍历它们,获取所有信息(标题、位置、线索等)。 ) 并为每个点创建一个 object ,然后将其放入数组中。但是,如果我在尝试保存之前或之后删除了一个节点,则删除的元素不会从 querySelectorAll() 返回的列表中删除。它抛出错误:

Uncaught TypeError: markers[i].shadowRoot.querySelector(...) is null

到达任何删除点的点时。

网页组件移除方法

// Deletes point marker
deletePoint() {
    const delPoint = this.shadowRoot.querySelector(".del-btn");
    let pointMarker = delPoint.parentNode.parentNode.parentNode;
    pointMarker.remove();
};

添加和保存功能

const addPoint = document.querySelector(".add");
const savePoints = document.querySelector(".save");
var data = [];
// Defines markers in preperation for later
let markers = null

// Adds point-marker element to markers div
addPoint.addEventListener("click", () => {
    const pointContainer = document.querySelector(".markers");
    const node = document.createElement("point-marker");
    pointContainer.appendChild(node);
});

// Grabs all point-marker elements, grabs relevant data and adds it to data array
savePoints.addEventListener("click", () => {
    // clears data
    data = []
    markers = document.querySelectorAll("point-marker");
    // Iterates through markers
    for (i = 0; i < markers.length; i++) {
        console.log(`i: ${i}`)
        // Grabs all relevant info
        let name = markers[i].shadowRoot.querySelector(".name").textContent;
        let location = markers[i].shadowRoot.querySelector(".location").textContent;
        let clue = markers[i].shadowRoot.querySelector("#clue").value;
        // Saves all relevant info in object form
        point = {
            id: `${i}`,
            name: `${name}`,
            location: `${location} ${i}`,
            clue: `${clue}`
        }
        // Adds point to data
        data.push(point)
    }
    console.log(data)
});

我相当确定 .remove() 方法没有从 DOM 中完全删除元素是一个问题,因为添加元素时它不会导致问题,但找不到另一种方法。

如果对您有帮助,这里是完整的代码片段:

// === script.js ====
// Declares template variable, containing the html template for the component
const template = document.createElement("template");
template.innerHTML = `
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.15.3/css/all.css" integrity="sha384-SZXxX4whJ79/gErwcOYf+zWLeJdY/qpuqC4cAa9rOGUstPomtqpuNWT9wdPEn2fk" crossorigin="anonymous">
    <link rel="stylesheet" href="css/style.css">
    <style>
        @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap');
        .point-marker {
            color: var(--tertiary-color);
            background-color: var(--secondary-color);
            padding: 2rem;
            border-radius: 20px;
            margin: 1rem 0;
        }
        
        .point-marker h2 {
            line-height: 1rem;
        }
        
        .point-marker textarea {
            width: 100%;
            height: 100px;
            border-radius: 20px;
            resize: vertical;
            padding: .5rem;
            margin: 1rem 0;
        }
        
        .btn {
            background-color: var(--primary-color);
            border: none;
            padding: .5rem 1rem;
            min-width: 200px;
            color: var(--tertiary-color);
            border-radius: 10px;
            font-weight: bold;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: medium;
            cursor: pointer;
        }
        
        .del-btn {
            background-color: var(--fail-color);
        }
        
        .btns {
            display: flex;
            width: 100%;
            justify-content: space-evenly;
        }
        
        .coll-content {
            max-height: 0;
            overflow: hidden;
            transition: max-height 250ms ease-in-out;
        }
        
        .collapse-icon {
            font-size: large;
        }
        
        .const-content {
            display: flex;
            align-items: center;
            justify-content: space-between;
            cursor: pointer;
        }
    </style>

    <section class="point-marker">
    <div class="const-content">
        <h2 class="name">New Point</h2>
        <i class="fas fa-minus collapse-icon"></i>
    </div>
    <div class="coll-content">
        <p>Location: <p class="location">location</p></p>
        <p>Clue:</p>
        <textarea name="clue" id="clue" cols="30" rows="10"></textarea>
        <div class="btns">
            <button class="btn loc-btn">SET CURRENT LOCATION</button>
            <button class="btn del-btn">DELETE POINT</button>
        </div>
    </div>
    </section>
`;

// Declares class PointMarker and casts it as an HTML element
class PointMarker extends HTMLElement {
  // Initialises the class every time new object is made
  constructor() {
    super();

    //  Declares shadow DOM and sets it to open
    this.attachShadow({
      mode: "open"
    });

    this.shadowRoot.appendChild(template.content.cloneNode(true));


    setTimeout(() => {
      const coll = this.shadowRoot.querySelector(".const-content");
      coll.nextElementSibling.style.maxHeight = `${coll.nextElementSibling.scrollHeight}px`;
    }, 100)

    const name = this.shadowRoot.querySelector(".name")
    name.contentEditable = "true";


  };

  // Collapses or expands the collapsable content
  expandCollapse() {
    const coll = this.shadowRoot.querySelector(".const-content");
    let content = coll.nextElementSibling;
    if (content.style.maxHeight) {
      content.style.maxHeight = null;
    } else {
      content.style.maxHeight = `${content.scrollHeight + 30}px`;
    };
  };

  // Deletes point marker
  deletePoint() {
    this.disconnectedCallback();
    const delPoint = this.shadowRoot.querySelector(".del-btn");
    let pointMarker = delPoint.parentNode.parentNode.parentNode;
    pointMarker.remove();
    pointMarker = null;
  };

  // Adds event listener on all elements with class of const-content or del-btn
  connectedCallback() {
    this.shadowRoot.querySelector(".collapse-icon").addEventListener("click", () => this.expandCollapse());
    this.shadowRoot.querySelector(".del-btn").addEventListener("click", () => this.deletePoint());
    console.log("connectedCallback() called");
    console.log(this.isConnected)
  };

  // Adds event listener on all elements with class of del-btn
  disconnectedCallback() {
    this.shadowRoot.querySelector(".collapse-icon").removeEventListener("click", () => this.expandCollapse());
    this.shadowRoot.querySelector(".del-btn").removeEventListener("click", () => this.deletePoint());
    console.log("disconnectedCallback() called");
    console.log(this.isConnected)
  };
};

// Defines <point-marker>
window.customElements.define("point-marker", PointMarker);

const addPoint = document.querySelector(".add");
const savePoints = document.querySelector(".save");
// Defines markers in preperation for later

// Adds point-marker element to markers div
addPoint.addEventListener("click", () => {
  const pointContainer = document.querySelector(".markers");
  const node = document.createElement("point-marker");
  pointContainer.appendChild(node);
});

// Grabs all point-marker elements, grabs relevant data and adds it to data array
savePoints.addEventListener("click", () => {
  // clears data
  let data = []
  markers = document.querySelectorAll("point-marker");

  // Iterates through markers
  for (i = 0; i < markers.length; i++) {
    // Grabs all relevant info
    let name = markers[i].shadowRoot.querySelector(".name").textContent;
    let location = markers[i].shadowRoot.querySelector(".location").textContent;
    let clue = markers[i].shadowRoot.querySelector("#clue").value;

    // Saves all relevant info in object form
    let point = {}
    point = {
      id: `${i}`,
      name: `${name}`,
      location: `${location} ${i}`,
      clue: `${clue}`
    }

    // Adds point to data
    data.push(point)
    console.log(data)
  }
  return data;
});
/* style.css */
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap');
:root {
  --primary-color: #FA4D05;
  --secondary-color: #333;
  --tertiary-color: #fff;
  --success-color: #97FD87;
  --fail-color: #FF5555;
  --bg-color: #E5E5E5;
  --font-color: #808080;
}

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  font-family: 'Roboto', sans-serif;
}

html {
  scroll-behavior: smooth;
}

body {
  min-height: 100vh;
  line-height: 2;
  color: var(--primary-color);
}

h1 {
  font-size: 36px;
}

h2 {
  font-size: 24px;
}

nav {
  display: flex;
  background-color: var(--secondary-color);
  justify-content: space-between;
  align-items: center;
  height: 65px;
  padding-left: 5rem;
  /* color: var(--primary-color); */
}

nav ul {
  list-style: none;
  display: flex;
  justify-content: space-evenly;
  width: 50%;
}

main {
  display: flex;
  flex-direction: column;
  padding: 2rem;
}

main h1 {
  margin-bottom: 1rem;
}

.btn {
  background-color: var(--primary-color);
  border: none;
  padding: .5rem 1rem;
  min-width: 200px;
  color: var(--tertiary-color);
  border-radius: 10px;
  font-weight: bold;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: medium;
  cursor: pointer;
}

.add-point {
  background-color: var(--bg-color);
  color: var(--font-color);
  margin: 1rem 0;
  border-radius: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}

.save {
  background-color: var(--success-color);
}
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.15.3/css/all.css" integrity="sha384-SZXxX4whJ79/gErwcOYf+zWLeJdY/qpuqC4cAa9rOGUstPomtqpuNWT9wdPEn2fk" crossorigin="anonymous">
  <title>Create A Hunt</title>
</head>

<body>
  <header>
    <nav>
      <h2>HOME</h2>
      <ul>
        <li>
          <h2>HUNT</h2>
        </li>
        <li>
          <h2>CREATE</h2>
        </li>
      </ul>
    </nav>
  </header>

  <main>
    <h1>CREATE A HUNT</h1>
    <div class="markers">
    </div>

    <button class="btn add-point add">
            <h2>Add Point +</h2>
        </button>

    <button class="btn add-point save">
            <h2>Save Points</h2>
        </button>

  </main>

  <script src="script.js"></script>
  <script src="/components/pointMarker.js"></script>
</body>

</html>

TL;DR

使用 .remove() 方法删除的元素仍由 .querySelectorAll() 方法拾取,大概是因为它没有从 DOM 中完全删除它。

  // Deletes point marker
  deletePoint() {
    this.disconnectedCallback();
    const delPoint = this.shadowRoot.querySelector(".del-btn");
    let pointMarker = delPoint.parentNode.parentNode.parentNode;
    pointMarker.remove();
    pointMarker = null;
 };

这不会删除点标记。它删除了点标记的内容,但点标记仍然存在。

  // Deletes point marker
  deletePoint() {
    this.disconnectedCallback();
    this.remove();
  };

这会从页面中删除实际元素,然后您的代码就可以正常工作了。