0

So I have the following structure with multiple <div> elements where I want to display the children inside each of it in a data attribute (data-children). Instead of manually changing the attribute, I'd like it to be dynamic with JS so it changes when I add/delete children elements.

<h2 class="amount_title" data-children="1">
  The amount of children of this element is
</h2>

<div class="parent_container">
  <div class="children_element">
    <div>
      <p>Lorem ipsum dolor sit amet.</p>
    </div>
    <button>Button</button>
  </div>
</div>

<h2 class="amount_title" data-children="2">
  The amount of children of this element is
</h2>

<div class="parent_container">
  <div class="children_element">
    <div>
      <p>Lorem ipsum dolor sit amet.</p>
    </div>
    <button>Button</button>
  </div>
  <div class="children_element">
    <div>
      <p>Lorem ipsum dolor sit amet.</p>
    </div>
    <button>Button</button>
  </div>
</div>

So right now I'm doing the following to get each element's children and populate the data-attribute of each title:

const parentContainer = document.querySelectorAll('.parent_container')
const amountTitle = document.querySelectorAll('.amount_title')

let childrenAmount1 = parentContainer[0].childElementCount
amountTitle[0].dataset.children = childrenAmount1

let childrenAmount2 = parentContainer[1].childElementCount
amountTitle[1].dataset.children = childrenAmount2

I have a few of these so it's obviously not ideal. Which function could help me to go through all of them and populate each dataset accordingly with the amount of children for each element? Thank you.

I tried to do a for () loop but all the datasets grabbed the amount of children from the last item in the array

4
  • What are the conventions for the namings? By what rules are the names chosen? Commented Sep 25, 2023 at 12:17
  • If you post the for loop you wrote, someone might be able to help you fix it. Commented Sep 25, 2023 at 12:20
  • well the for loop I used was this: for (let childrenAmount of parentContainer) { let totalChildren = childrenAmount.childElementCount; for (let titles of amountTitle) { titles.dataset.children = totalChildren; } } Commented Sep 25, 2023 at 12:28
  • @tacoshy ... A good approach/implementation does neither depend on tag-/class-names nor on a given markup structure. Commented Sep 25, 2023 at 15:24

5 Answers 5

1

Based on this configuration, your best strategy is to get the next element after each title. Although I would recommend a wrapper to set the title based on each wrapper contents .wrapper > amount_title|parent_container to loop through all wrappers first and get its children from the wrapper reference

const titles = document.querySelectorAll(".amount_title");
titles.forEach((title) => {
  const next = title.nextElementSibling;
  if (!next.classList.contains("parent_container")) { return false; } // killswitch
  title.innerHTML = `The amount of children of this element is ${ next.childElementCount }`;
});
<h2 class="amount_title" data-children="1">The amount of children of this element is </h2>
<div class="parent_container">
    <div class="children_element">
        <div>
            <p>Lorem ipsum dolor sit amet.</p>
        </div>
        <button>Button</button>
    </div>
</div>

<h2 class="amount_title" data-children="2">The amount of children of this element is </h2>
<div class="parent_container">
    <div class="children_element">
        <div>
            <p>Lorem ipsum dolor sit amet.</p>
        </div>
        <button>Button</button>
    </div>
    <div class="children_element">
        <div>
            <p>Lorem ipsum dolor sit amet.</p>
        </div>
        <button>Button</button>
    </div>
</div>


Edit: The wrapper method:

This method allows to move any element inside without caring about the order of the elements. When the other method works, this one allows you to set the title before or after the contents without breaking

const wrappers = document.querySelectorAll(".wrapper");
wrappers.forEach((wrapper) => {
  const title = wrapper.querySelector(".amount_title");
  const contents = wrapper.querySelector(".parent_container");
  if (!title || !contents) { return; } // killswitch
  title.innerText = `The amount of children of this element is ${ contents.childElementCount }`
});
<div class="wrapper">
  <h2 class="amount_title" data-children="1">The amount of children of this element is </h2>
  <div class="parent_container">
      <div class="children_element">
          <div>
              <p>Lorem ipsum dolor sit amet.</p>
          </div>
          <button>Button</button>
      </div>
  </div>
</div>

<div class="wrapper">
  <h2 class="amount_title" data-children="2">The amount of children of this element is </h2>
  <div class="parent_container">
      <div class="children_element">
          <div>
              <p>Lorem ipsum dolor sit amet.</p>
          </div>
          <button>Button</button>
      </div>
      <div class="children_element">
          <div>
              <p>Lorem ipsum dolor sit amet.</p>
          </div>
          <button>Button</button>
      </div>
  </div>
</div>

Sign up to request clarification or add additional context in comments.

Comments

0

Simply select all Headlines and use a forEach loop to loop through all those headlines. Then select the next sibling and count all elements that has the class child_element within:

window.addEventListener('DOMContentLoaded', getAmountTitles);

function getAmountTitles() {
  // selects all h2 headliens with the amount_title class
  const HEADLINES = document.querySelectorAll('h2.amount_title');
  
  // forEach loop to iterate
  HEADLINES.forEach(headline => {
    // selects the enxt Sibling element (parent_container)
    let parent_container = headline.nextElementSibling;
    
    // gets the amount of child elements with the class children_element
    let amountChildElements = countChildrenElements(parent_container);
    
    // adds the data attribtue with the amount
    headline.dataset.children = amountChildElements;
    
    // adds the text to the headlines with the number
    headline.insertAdjacentText('beforeend', amountChildElements);
  });
}

function countChildrenElements(parentElement) {
  return (parentElement.querySelectorAll('.children_element').length);
}
<h2 class="amount_title">The amount of children of this element is </h2>

<div class="parent_container">
  <div class="children_element">
    <div>
      <p>Lorem ipsum dolor sit amet.</p>
    </div>
    <button>Button</button>
  </div>
</div>

<h2 class="amount_title">The amount of children of this element is </h2>

<div class="parent_container">
  <div class="children_element">
    <div>
      <p>Lorem ipsum dolor sit amet.</p>
    </div>
    <button>Button</button>
  </div>
  <div class="children_element">
    <div>
      <p>Lorem ipsum dolor sit amet.</p>
    </div>
    <button>Button</button>
  </div>
</div>

Comments

0

If the header (h2.amount_title) is always directly before div.parent_container you can use previousElementSibling to give [data-children] its value. The snippet uses css to display the amount of child elements.

document.querySelectorAll(`.parent_container`)
  .forEach(container => {
    container.previousElementSibling
      .dataset.children = container.childElementCount;
  });
.amount_title:after {
  content: attr(data-children);
  color: green;
}
<h2 class="amount_title" data-children="1">The amount of children of this element is </h2>
<div class="parent_container">
  <div class="children_element">
    <div>
      <p>Lorem ipsum dolor sit amet.</p>
    </div>
    <button>Button</button>
  </div>
</div>

<h2 class="amount_title" data-children="2">The amount of children of this element is </h2>
<div class="parent_container">
  <div class="children_element">
    <div>
      <p>Lorem ipsum dolor sit amet.</p>
    </div>
    <button>Button</button>
  </div>
  <div class="children_element">
    <div>
      <p>Lorem ipsum dolor sit amet.</p>
    </div>
    <button>Button</button>
  </div>
</div>

When there may be elements between h2.amount_title and div.parent_container its better to wrap the elements within a div.

document.querySelectorAll(`.parent_container`)
  .forEach(container => {
    container.closest(`.item`).querySelector(`.amount_title`)
      .dataset.children = container.childElementCount;
  });
.amount_title:after {
  content: attr(data-children);
  color: green;
}
<div class="item">
  <h2 class="amount_title" data-children="1">The amount of children of this element is </h2>
  <div class="parent_container">
    <div class="children_element">
      <div>
        <p>Lorem ipsum dolor sit amet.</p>
      </div>
      <button>Button</button>
    </div>
  </div>
</div>

<div class="item">
  <h2 class="amount_title" data-children="2">The amount of children of this element is </h2>
  <h3>**Container starts in next div</h3>
  <div class="parent_container">
    <div class="children_element">
      <div>
        <p>Lorem ipsum dolor sit amet.</p>
      </div>
      <button>Button</button>
    </div>
    <div class="children_element">
      <div>
        <p>Lorem ipsum dolor sit amet.</p>
      </div>
      <button>Button</button>
    </div>
  </div>
</div>

Comments

0

You can use the is and has selectors with the adjacent sibling combinator + to get just the .amount_title elements that have .parent_container as an adjacent sibling.

It then loops over them to update their data-children attribute and textContent. It doesn't visit any .amount_title element that doesn't have a .parent_container as an adjacent sibling, so .nextElementSibling should always return a .parent_container element.

window.onload = () => {
  // Select all .amount_title elements with a following
  // .parent_container sibling
  document.querySelectorAll(':is(.amount_title):has(+ .parent_container)')
    // For each such element, get the next element sibling's count
    // of .children_elements and do updates
    .forEach(h => {
      let count = h.nextElementSibling.querySelectorAll('.children_element').length;
      h.textContent += count;
      h.dataset.children = count;
    }
  );
};

console.log(document.querySelectorAll(':is(.amount_title):has(+ .parent_container)').length);
<h2 class="amount_title" data-children="">The amount of children of this element is </h2>

<div class="parent_container">
  <div class="children_element">
    <div>
      <p>Lorem ipsum dolor sit amet.</p>
    </div>
    <button>Button</button>
  </div>
</div>

<h2 class="amount_title" data-children="">No .parent_container: </h2>

<h2 class="amount_title" data-children="">No .children_element: </h2>
<div class="parent_container"></div>


<h2 class="amount_title" data-children="">The amount of children of this element is </h2>

<div class="parent_container">
  <div class="children_element">
    <div>
      <p>Lorem ipsum dolor sit amet.</p>
    </div>
    <button>Button</button>
  </div>
  <div class="children_element">
    <div>
      <p>Lorem ipsum dolor sit amet.</p>
    </div>
    <button>Button</button>
  </div>
</div>

Comments

0

Since the OP already does make use of a data-* global attribute like data-children, the OP might think about a generic implementation of displaying/updating child-count changes which will be fully agnostic to DOM structures, thus it always is going to work, regardless of chosen tag-names, class-names and nesting of markup structures.

One, for instance, could take inspiration from the HTML for attribute. Just the minimum set of related data-* attributes like ...

<tagName
  data-child-count="2"
  data-child-count-for="23fd215b-4c2a-4861-b53a-5c517e67a56e"
>
  The amount of children of the next element is 2.
</tagName>

... and ...

<tagName data-child-count-id="23fd215b-4c2a-4861-b53a-5c517e67a56e">
  <!--
    ... any child structure like ...
  -->
  <tagName></tagName>
  <tagName></tagName>
</tagName>

... already enables a fully generic implementation of displaying/updating child-count changes.

For instance [data-child-count-for="23fd215b-4c2a-4861-b53a-5c517e67a56e"] marks an element which has to display/update a child-count change. The element's related dataset.childCountFor-value does identify the element where the child-count change is expected to happen. It is the very element which was marked by e.g. [data-child-count-id="23fd215b-4c2a-4861-b53a-5c517e67a56e"].

The next provided example code shows how the function, the OP is looking for, would work upon such a generic markup.

function updateChildCount(childCountNode) {
  const parentId = childCountNode.dataset.childCountFor;
  const parentNode =  document
    .querySelector(`[data-child-count-id="${ parentId ?? '' }"]`);

  if (parentNode) {
    const childCount = parentNode.childElementCount;

    childCountNode.dataset.childCount = childCount;
    childCountNode.textContent =
      `The amount of children of the next element is ${ childCount }.`;
  }
}
function updateAnyChildCount() {
  document
    .querySelectorAll('[data-child-count-for]')
    .forEach(updateChildCount);
}

// (re)render initial child-count values.
updateAnyChildCount();
body { margin: 0; }
p { margin: 4px 0; }
h2 { font-size: 1em; margin: 8px 0 4px 0; }
button[data-add],
button[data-delete] { float: right; margin-left: 2px; }
<h2
  data-child-count="unknown"
  data-child-count-for="23fd215b-4c2a-4861-b53a-5c517e67a56e"
>
  The amount of children of the next element is unknown.
</h2>

<div data-child-count-id="23fd215b-4c2a-4861-b53a-5c517e67a56e">
  <div>
    <p>Lorem ipsum dolor sit amet.</p>
    <button>Button</button>

    <button data-add>clone 'n append</button>
    <button data-delete>delete</button>
  </div>
</div>

<h2
  data-child-count="unknown"
  data-child-count-for="69e7b5bf-698f-4492-94a0-e48243fcf1f9"
>
  The amount of children of the next element is unknown.
</h2>

<div data-child-count-id="69e7b5bf-698f-4492-94a0-e48243fcf1f9">
  <div>
    <p>Lorem ipsum dolor sit amet.</p>
    <button>Button</button>

    <button data-add>clone 'n append</button>
    <button data-delete>delete</button>
  </div>
  <div>
    <p>Lorem ipsum dolor sit amet.</p>
    <button>Button</button>

    <button data-add>clone 'n append</button>
    <button data-delete>delete</button>
  </div>
  <div>
    <p>Lorem ipsum dolor sit amet.</p>
    <button>Button</button>

    <button data-add>clone 'n append</button>
    <button data-delete>delete</button>
  </div>
</div>

<script>
// - example's helper functionality which is going to invoke
//   the child-count update-function.
document
  .addEventListener('click', ({ target }) => {

    const button = target.closest('[data-add], [data-delete]');
    if (button) {

      // mutate the DOM.

      if (button.matches('[data-add]')) {
        const { parentNode } = button;

        parentNode
          .parentNode
          .appendChild(
            parentNode.cloneNode(true)
          );
      } else {
        button
          .parentNode
          .remove();
      }

      // invoke the child-count update-function.
      updateAnyChildCount();
    }
  });
</script>

The above implementation could even be changed to just updating the data-child-count attribute. And since the element which features said attribute does always carry the current child-count via this attribute's value, one could introduce an additional single css-rule as simple as ...

[data-child-count]:after {
  content: attr(data-child-count);
}

... in order to always assure an up-to-date UI as well.

The following, slightly changed, example code just proves the last statement.

function updateChildCount(childCountNode) {
  const parentId = childCountNode.dataset.childCountFor;
  const parentNode =  document
    .querySelector(`[data-child-count-id="${ parentId ?? '' }"]`);

  if (parentNode) {
    childCountNode.dataset.childCount = parentNode.childElementCount;
  }
}
function updateAnyChildCount() {
  document
    .querySelectorAll('[data-child-count-for]')
    .forEach(updateChildCount);
}
body { margin: 0; }
p { margin: 4px 0; }
h2 { font-size: 1em; margin: 8px 0 4px 0; }
button[data-add],
button[data-delete] { float: right; margin-left: 2px; }

[data-child-count]:after {
  content: attr(data-child-count);
}
<h2
  data-child-count="1"
  data-child-count-for="23fd215b-4c2a-4861-b53a-5c517e67a56e"
>
  The amount of children of the next element is
</h2>

<div data-child-count-id="23fd215b-4c2a-4861-b53a-5c517e67a56e">
  <div>
    <p>Lorem ipsum dolor sit amet.</p>
    <button>Button</button>

    <button data-add>clone 'n append</button>
    <button data-delete>delete</button>
  </div>
</div>

<h2
  data-child-count="3"
  data-child-count-for="69e7b5bf-698f-4492-94a0-e48243fcf1f9"
>
  The next element's child-count is
</h2>

<div data-child-count-id="69e7b5bf-698f-4492-94a0-e48243fcf1f9">
  <div>
    <p>Lorem ipsum dolor sit amet.</p>
    <button>Button</button>

    <button data-add>clone 'n append</button>
    <button data-delete>delete</button>
  </div>
  <div>
    <p>Lorem ipsum dolor sit amet.</p>
    <button>Button</button>

    <button data-add>clone 'n append</button>
    <button data-delete>delete</button>
  </div>
  <div>
    <p>Lorem ipsum dolor sit amet.</p>
    <button>Button</button>

    <button data-add>clone 'n append</button>
    <button data-delete>delete</button>
  </div>
</div>

<script>
// - example's helper functionality which is going to invoke
//   the child-count update-function.
document
  .addEventListener('click', ({ target }) => {

    const button = target.closest('[data-add], [data-delete]');
    if (button) {

      // mutate the DOM.

      if (button.matches('[data-add]')) {
        const { parentNode } = button;

        parentNode
          .parentNode
          .appendChild(
            parentNode.cloneNode(true)
          );
      } else {
        button
          .parentNode
          .remove();
      }

      // invoke the child-count update-function.
      updateAnyChildCount();
    }
  });
</script>

In addition the OP should make use of MutationObserver and MutationRecord where the latter gets provided as an array of records to an observer's callback/handler function.

As for the OP's use case, one just needs to have a look at/into both NodeList's of each record which are addedNodes and removedNodes. A child-count change has happened whenever either of the node-list's carries at least a single element-reference.

The decision for MutationObserver enables a maximum generic implementation for it decouples the change-handling/management entirely from any code which would trigger DOM-mutations such as element-insertion and -deletion.

In order to not being forced to having to observe the entire DOM-tree one would make use of a component-like approach based on the already introduced/proposed data-* attributes in cooperation with an element's related dataset-property.

The next provided example code demonstrates how one would identify such a loosely coupled (markup and DOM -wise) component, and how one would create a mutation observer for each such component.

function getChildCountCopy(template, count) {
  return template
    .replace(/\$\{\s*childCount\s*\}/g, count);
}
function renderChildCount(childCountNode, observedNode) {
  const childCount = observedNode.childElementCount;

  childCountNode.textContent = getChildCountCopy(
    childCountNode.dataset.childCountTemplate, childCount,
  );
  childCountNode.dataset.childCount = childCount;
}

function handleChildCountChangeForBoundNodes(
  records/*, observer,*/
) {
  const { childCountNode, observedNode } = this;

  records
    .forEach(record => {
      const { addedNodes, removedNodes } = record;

      if (addedNodes.length + removedNodes.length > 0) {

        renderChildCount(childCountNode, observedNode);
      }
    });
}
function initializeChildCountMutationObserver(
  childCountNode, observedNode,
) {
  const observer = new MutationObserver(
    handleChildCountChangeForBoundNodes
      .bind({
        childCountNode,
        observedNode,
      }),
  );
  observer
    .observe(observedNode, {
      childList: true,
      //  subtree: true,
      // attributes: true,
    });
}

function enableInstantChildCount(childCountNode) {
  const { childCountFor: observedId } = childCountNode.dataset;
  
  const observedNode = document
    .querySelector(`[data-child-count-id="${ observedId ?? '' }"]`);

  if (observedNode) {

    // (re)render initial/current child-count.
    renderChildCount(childCountNode, observedNode);

    // register handling of element related DOM mutations.
    initializeChildCountMutationObserver(
      childCountNode, observedNode,
    );
  }
}
document
  .querySelectorAll('[data-child-count-for]')
  .forEach(enableInstantChildCount);
body { margin: 0; }
p { margin: 4px 0; }
h2 { font-size: 1em; margin: 8px 0 4px 0; }
button[data-add],
button[data-delete] { float: right; margin-left: 2px; }
<h2
  data-child-count="1"
  data-child-count-for="23fd215b-4c2a-4861-b53a-5c517e67a56e"
  data-child-count-template="The amount of children of the next element is ${ childCount }."
>
  The amount of children of the next element is 1.
</h2>

<div data-child-count-id="23fd215b-4c2a-4861-b53a-5c517e67a56e">
  <div>
    <p>Lorem ipsum dolor sit amet.</p>
    <button>Button</button>

    <button data-add>clone 'n append</button>
    <button data-delete>delete</button>
  </div>
</div>

<h2
  data-child-count-for="69e7b5bf-698f-4492-94a0-e48243fcf1f9"
  data-child-count-template="The next element's child-count is ${ childCount }."
>
  The next element's child-count is unknown.
</h2>

<div data-child-count-id="69e7b5bf-698f-4492-94a0-e48243fcf1f9">
  <div>
    <p>Lorem ipsum dolor sit amet.</p>
    <button>Button</button>

    <button data-add>clone 'n append</button>
    <button data-delete>delete</button>
  </div>
  <div>
    <p>Lorem ipsum dolor sit amet.</p>
    <button>Button</button>

    <button data-add>clone 'n append</button>
    <button data-delete>delete</button>
  </div>
  <div>
    <p>Lorem ipsum dolor sit amet.</p>
    <button>Button</button>

    <button data-add>clone 'n append</button>
    <button data-delete>delete</button>
  </div>
</div>

<script>
// example's helper functionality which is going to trigger DOM mutations.
document
  .addEventListener('click', ({ target }) => {

    const button = target.closest('[data-add], [data-delete]');
    if (button) {

      // mutate the DOM.

      if (button.matches('[data-add]')) {
        const { parentNode } = button;

        parentNode
          .parentNode
          .appendChild(
            parentNode.cloneNode(true)
          );
      } else {
        button
          .parentNode
          .remove();
      }
    }
  });
</script>

1 Comment

@MarcosCuadros ... The OP might have a look into the late provided above solution which is based on an entirely generic approach which makes one not worry neither about the source of DOM-mutations nor about future markup changes (like tag-/css-names and markup nesting).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.