skip to Main Content

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

5

Answers


  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>
    Login or Signup to reply.
  2. 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>
    Login or Signup to reply.
  3. 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>
    Login or Signup to reply.
  4. 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>
    Login or Signup to reply.
  5. 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 just some more of the already introduced data-* global attributes (in cooperation with an element’s related dataset-property).

    Already the minimum set of related data-* attributes like …

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

    … and …

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

    … enables a fully generic implementation of displaying/updating child-count changes; it even is fully agnostic to DOM structures. It does work regardless of chosen tag-names, class-names and nesting of markup structures.

    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 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*childCounts*}/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);
    
    // 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) {
    
          if (button.matches('[data-add]')) {
            const { parentNode } = button;
    
            parentNode
              .parentNode
              .appendChild(
                parentNode.cloneNode(true)
              );
          } else {
            button
              .parentNode
              .remove();
          }
        }
      });
    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>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search