简体   繁体   中英

Why callback not detecting latest value of class instance variable

I have a web component which is basically a class:

class NavList extends HTMLElement {
      _wrapper;
      _observer;
      _observerActive = true;

      get observerState() {
        return this._observerActive;
      }

 render() {
        this._wrapper.innerHTML = "";
        const activeList = window.location.hash.slice(1);
        const container = htmlToElement(`<nav class="navlist"></nav>`);
        for (let list in veritabani) {
          container.appendChild(
            htmlToElement(
              `<a href=#${list} class="nav-entry ${
                activeList === list ? "active" : ""
              }">${list}</div>`
            )
          );
        }

        // prevent observer from changing hash during smooth scrolling
        container.addEventListener("click", this.disableObserver);

        this._wrapper.appendChild(container);

      }

      observe() {
        let options = {
          root: document.querySelector(".check-list"),
          rootMargin: "0px",
          threshold: 0.4,
        };

        let observer = new IntersectionObserver(
          this.observerCallback.bind(this),
          options
        );
        this._observer = observer;
        const targets = document.querySelectorAll("check-list");
        console.log("observer target:", targets);

        for (let target of targets) {
          observer.observe(target);
        }
      }

      observerCallback(entries, observer) {
        console.log("observer active?", this.observerState);
        entries.forEach((entry) => {
          if (entry.isIntersecting && this.observerState) {
            const targetListName = entry.target.getAttribute("list");
            console.log(entry, targetListName);
            window.location.hash = targetListName;
            this.render();
          }
        });
      }

      disableObserver() {
        this._observerActive = false;
        console.log("observer disabled", this._observerActive);
        function enableObserver() {
          this._observerActive = true;
          console.log("observer enabled", this._observerActive);
        }
        const timer = setTimeout(enableObserver, 2000);
      }

      connectedCallback() {
        console.log("hash", window.location.hash);
        // wrapper for entire checklist element
        const wrapper = this.appendChild(
          htmlToElement(
            `<span class="navlist-wrapper ${window.location.hash}"></span>`
          )
        );

        this._wrapper = wrapper;

        this.render();

        setTimeout(() => {
          this.observe();
        }, 1);
      }

 // more code below

As you can see, I have an intersection observer and I am trying to disable its callback when an anchor is clicked.

The observer detects elements on the page and changes the URL hash so that the visible element name is highlighted on the navlist , this works fine but interferes with the function of the navlist since clicking on navlist entry should also scroll the page to that element!

My solution is to disable the intersection observer's callback after a navlist entry is clicked using setTimout :

      disableObserver() {
        this._observerActive = false;
        console.log("observer disabled", this._observerActive);
        function enableObserver() {
          this._observerActive = true;
          console.log("observer enabled", this._observerActive);
        }
        const timer = setTimeout(enableObserver, 2000);
      }

The above code sets an instance variable to false after a click on navlist , the variable changes state to false but the observer's callback does not see the change and uses the old state which is true by default.

My Question : Why is this happening? and how can I fix it?

I tried delaying the callback function thinking that it is being activated before the state change, but it did not work.

UPDATE : Here is a link to a live demo of what I am doing

I found a solution though I still do not quite understand whats happening.

The solution is to move the flag _observerActive outside of the class Navlist :

   let OBSERVER_STATE = true;
    class NavList extends HTMLElement {
      _wrapper;
      _observer;

      render() {
        this._wrapper.innerHTML = "";
        const activeList = window.location.hash.slice(1);
        const container = htmlToElement(`<nav class="navlist"></nav>`);
        for (let list in veritabani) {
          console.log(`active? ${list}===${activeList}`);

          container.appendChild(
            htmlToElement(
              `<a href=#${list} class="nav-entry ${
                activeList === list ? "active" : ""
              }">${list}</div>`
            )
          );
        }

        // prevent observer from changing hash during smooth scrolling
        container.addEventListener("click", this.disableObserver);

        const addButton = htmlToElement(
          `<button class="nav-add">
            <span class="nav-add-content">
              <span class="material-symbols-outlined">add_circle</span>
              <p>Yeni list</p>
            </span>
            </button>`
        );

        addButton.addEventListener("click", this.addList.bind(this));

        this._wrapper.appendChild(container);
        this._wrapper.appendChild(addButton);
      }

      disableObserver() {
        OBSERVER_STATE = false;
        console.log("observer disabled", this.OBSERVER_STATE);
        function enableObserver() {
          OBSERVER_STATE = true;
          console.log("observer enabled", OBSERVER_STATE);
        }
        const timer = setTimeout(enableObserver, 2000);
      }

      addList() {
        const inputGroup = htmlToElement(`
          <div class="input-group">

          </div>`);
        const input = inputGroup.appendChild(
          htmlToElement(`
        <input placeholder="Liste Adi Giriniz"></input>`)
        );
        const button = inputGroup.appendChild(
          htmlToElement(`
            <button>&#10004;</button>`)
        );

        button.addEventListener("click", () =>
          this.addNewCheckList(input.value)
        );
        input.addEventListener("keypress", (e) => {
          if (e.key === "Enter") {
            console.log(input.value);
            this.addNewCheckList(input.value);
          }
        });
        const addButton = document.querySelector(".nav-add");
        console.log(this._wrapper);
        this._wrapper.replaceChild(inputGroup, addButton);
      }

      addNewCheckList(baslik) {
        veritabani[baslik] = {};
        const checkListContainer = document.querySelector(".check-list");
        const newCheckList = htmlToElement(`
              <check-list
                baslik="${baslik} Listem"
                list="${baslik}"
                placeholder="�� A��klamas�..."
              ></check-list>`);
        checkListContainer.appendChild(newCheckList);
        this._observer.observe(newCheckList);
        this.render();
        newCheckList.scrollIntoView();
      }

      observe() {
        let options = {
          root: document.querySelector(".check-list"),
          rootMargin: "0px",
          threshold: 0.4,
        };

        let observer = new IntersectionObserver(
          this.observerCallback.bind(this),
          options
        );
        this._observer = observer;
        const targets = document.querySelectorAll("check-list");
        console.log("observer target:", targets);

        for (let target of targets) {
          observer.observe(target);
        }
      }

      observerCallback(entries, observer) {
        console.log("observer active?", OBSERVER_STATE);
        entries.forEach((entry) => {
          if (entry.isIntersecting && OBSERVER_STATE) {
            const targetListName = entry.target.getAttribute("list");
            window.location.hash = targetListName;
            this.render();
          }
        });
      }

      connectedCallback() {
        console.log("hash", window.location.hash);
        // wrapper for entire checklist element
        const wrapper = this.appendChild(
          htmlToElement(
            `<span class="navlist-wrapper ${window.location.hash}"></span>`
          )
        );

        this._wrapper = wrapper;

        this.render();

        setTimeout(() => {
          this.observe();
        }, 1);
      }
    }

If I understand correctly, you want to

  • Create a nav list that renders a link for each anchor (id) on a page.
  • When a target scrolls into view, highlight the associated link and update the location hash
  • When clicking on a link in the Navbar, scroll to the target and update the location hash

You don't have to keep track of the IntersectObserver state and you don't have to disable it. Just use pushState() instead of location.hash to update the hash. https://developer.mozilla.org/en-US/docs/Web/API/History/pushState

index.html

<head>
    <style>
        /* Makes sure that the first section scrolls up enough to trigger the effect */
        section { scroll-margin: 20px }
    </style>
</head>
<body>
    <div class="sidebar">
        <nav-bar></nav-bar>
    </div>
    <div class="content">
        
        <section id="One">
            <header><h1>One</h1></header>
            <p> A bunch of text</p>
        </section>

        <!-- A Bunch of Sections -->

    </div>
</body>

component.js

export class NavList extends HTMLElement {
    #template = `
    <style>
        .visible {
            background-color: orange;
        }
    </style>
    <menu></menu>
    `;

    constructor() {
        super();
        this.shadow = this.attachShadow({mode: "open"});
    }

    connectedCallback() {
        const li = document.createElement('li');
        const a = document.createElement('a');
        this.tmpl = document.createRange().createContextualFragment(this.#template);
        this.menu = this.tmpl.querySelector('menu');
        this.anchors = document.querySelectorAll('[id]');
        this.observer = new IntersectionObserver( entries => {
            const entry = entries.shift();
            const id = entry.target.getAttribute('id');
            const link = this.menu.querySelector(`a[href="#${id}"]`);
            Array.from(this.menu.querySelectorAll('a')).map(a => a.classList.remove('visible'));
            link.classList.add('visible');
            history.pushState({}, '', `#${id}`);
        }, {threshold: 1});

        for (let anchor of this.anchors) {
            const item = li.cloneNode();
            const link = a.cloneNode();
            const id = anchor.getAttribute('id');
            link.setAttribute('href', `#${id}`);
            link.innerText = id;
            link.addEventListener('click', evt => this.clicked(evt));
            item.append(link);
            this.menu.append(item);
            this.observer.observe(anchor);
        }

        this.render();
    }

    disconnectedCallback() {
        this.observer.disconnect();
    }

    clicked(evt) {
        evt.preventDefault();
        const target = evt.target.getAttribute('href');
        const elem = document.querySelector(target);
        elem.scrollIntoView({behavior: "smooth"});
        history.pushState({}, '', `#${target}`);
    }

  render() {
    this.shadow.append(this.tmpl);
  }


}

customElements.define('nav-list', NavList);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM