SharpLon
SharpLon

Reputation: 15

Can't grab dynamically created elements via querySelector using insertAdjacentHTML

I'm trying to grab an element from page and add an addEventHandler to it. Element (link with class .catalog__link) was dynamically created in another external function when the page was loaded using insertAdjacentHTML. I invoke them both in third js file via import keyword. Everything loads perfectly on the page, the elements are created for me, but I can’t grab them from another function connected to the page. Here’s examples of codes. I have two external function, combined in one js file and simple html page

This is fillCatalog.js (First file)


const row = document.querySelector('.catalog__row');

export const fill = function (brand) {
  fetch(`./data/${brand}.json`)
    .then(function (response) {
      return response.json();
    })
    .then(function (data) {
      let products = [...data.products];

      products.forEach(product => {
       
        row.insertAdjacentHTML(
          'afterbegin',
          ` <a class="catalog__link" href="#" >
              <div class="catalog__product">
              <div class="catalog__product-img">
              <img class="catalog__productImg" src=${product['img-src']} alt="" srcset="" />
              </div>
              <h3 class="catalog__product-model">${product['model']}</h3>
              <p class="catalog__product-brand">${product['brand']}</p>
              <span class="catalog__product-price">${product['price']}</span>
              
          </div></a>`
        );
      });
    });
};

This is productSave.js (Second file)


class Product {
  constructor(cardImg, cardName, cardBrand = '', cardPrice) {
    this.cardImg = cardImg;
    this.cardName = cardName;
    this.cardBrand = cardBrand;
    this.cardPrice = cardPrice;
  }
}

const links = document.querySelectorAll('.catalog__link');

export const productSave = function () {
  window.addEventListener('DOMContentLoaded', () => {
    console.log(links);
    links.forEach(link => {
      link.addEventListener('click', e => {
        productItem = link.querySelector('.catalog__product');
        const newProduct = new Product(
          productItem.querySelector('catalog__productImg').src,
          productItem.querySelector('.catalog__product-model').textContent,
          productItem.querySelector('.catalog__product-brand').textContent,
          productItem
            .querySelector('.catalog__product-price-price')
            .textContent.replace(/\D/g, '')
        );
        localStorage.setItem('newCard', JSON.stringify(newProduct));
        console.log(card.querySelector('.card__name').textContent);
      });
    });
  });
};

This is third file where I invoke external function

loadContent.js

import { fill } from './fillCatalog.js';
import { productSave } from './productSave.js';
fill('jordans');
productSave();

Simple HTML

<html lang="ru">
  <head>
    <!--=include head.html-->
    </style>
  </head>
  <body>
<div class="catalog">
  <div class="catalog__content">
    <div class="catalog__row"></div>
  </div>
</div>

  </body>

  <script type="module" src="../js/goodscart.js"></script>
  <script type="module" src="../js/loadContent.js"></script>
</html>

I've tried use beforeend afterent and etc. Tried also use getElementsByTag, it returns empty HtmlCollection[]. After insertAdjacentHTML, can't select like usually this links.

Could anyone help me please with this issue? I can't find solution for that. Thank you

Upvotes: 0

Views: 123

Answers (3)

Vincent Menzel
Vincent Menzel

Reputation: 1055

You need to define links inside the function to avoid it being initialized before the elements are rendered.

Initialization of links

class Product {
...
}

// since this is not in a function it is initialized on import so 
// before you render the `catalog__link` 
const links = document.querySelectorAll('.catalog__link'); // <-- This

export const productSave = function () {
// <-- Should be here
...
};

Handling the fill Promise

Futhermore there is a fetch reuqest inside fill which returned a promise.

Then you can properly chain the call to the following function like so

// productSave will only be executed after the 
// fetch from fill has concluded sucesscully
fill('jordans').then(productSave);
export const fill = function (brand) {
  return fetch(`./data/${brand}.json`).then(...)
  // return as a promise
};

Workaround to detect classes in dom

You can also implement a detector that waits for at least one link to be rendered. This Promise would resolve after at least 1 item with catalog__link can be found. Then you can chain your logic with .then(...). This one also includes a timeout of 10 seconds which executes .catch(...) (if present otherwise throws) if the element was not found in time.

new Promise((res, rej) => {
    const timeout = setTimeout(() => {
        clearTimeout(timeout);
        clearInterval(interval);
        rej("Not Found")
    }, 10000);
    const interval = setInterval(() => {
        const links = document.querySelectorAll('.catalog__link');
        if (links.length == 0) {
            return;
        }
        clearInterval(interval);
        clearTimeout(timeout);
        res(links)
    },200);
});

Upvotes: 0

Andy
Andy

Reputation: 63524

  1. fill is an async process due to the fetch but you're calling productSave immediately after calling it so the DOM hasn't been built by the time the code tries to pick up the elements.

  2. Move your links caching within the productSave function.

It maybe easier to separate out the asynchronous parts from the synchronous parts. In this example there's a separate getData function which is awaited and then the data can be filled, and saved.

// `getData` is only responsible for the async process
async function getData(brand) {
  try {
    const res = await fetch(`./data/${brand}.json`);
    if (res.ok) return res.json();
    throw new Error();
  } catch (err) {
    console.log(err);
  }
}

// `fill` creates the HTML
function fill({ products }) {
  const row = document.querySelector('.catalog__row');
  products.forEach(product => {
    row.insertAdjacentHTML('afterbegin', yourTemplateString);
  });
}

// `productSave` adds the listeners
function productSave() {
  const links = document.querySelectorAll('.catalog__link');
  links.forEach(link => ...etc )}
}

// `main` needs to be async because it needs to `await`
// the parsed JSON from `getData` - but after that you can call the
// functions in series
async function main() {
  const data = await getData('jordans');
  fill(data);
  productSave();
}

main();

Upvotes: 0

SharpLon
SharpLon

Reputation: 15

Thank you very much,Vincent. You gave me good direction to search solution.

i put it all to setTimeout. And it's working good. I'm not strong enough in JS to use and manipulate promises. Thank you

export const productSave = function () {
  window.addEventListener('DOMContentLoaded', () => {
    setTimeout(() => {
      const links = document.querySelectorAll('.catalog__link');

      links.forEach(link => {
        link.addEventListener('click', e => {
          const product = link.querySelector('.catalog__product');

          console.log(product);

          const newProduct = new Product(
            product.querySelector('.catalog__productImg').src,
            product.querySelector('.catalog__product-model').textContent,
            product.querySelector('.catalog__product-brand').textContent,
            product
              .querySelector('.catalog__product-price')
              .textContent.replace(/\D/g, '')
          );

          console.log(newProduct);
          localStorage.setItem('newCard', JSON.stringify(newProduct));
        });
      });
    }, 500);
  });
};

Upvotes: 0

Related Questions