24

Is there any way to force an update/run of an IntersectionObserver instance? The callback will be executed by default, when the viewport has changed. But I'm looking for a way to to execute it when other events happen, like a change of elements.

An Example:

On initialization everything works as expected. But when you change the position of the #red element, nothing happens.

// elements
let green = document.querySelector('#green');
let red = document.querySelector('#red');

// observer callback
let callback = entries => {
  entries.forEach(entry => {
    let isInside = entry.intersectionRatio >= 1 ? "fully" : "NOT";
    console.log("#" + entry.target.id + " is " + isInside + " inside #container");
  });
};

// start observer
let options = {root: document.querySelector('#container')};
let observer = new IntersectionObserver(callback, options);
observer.observe(green);
observer.observe(red);

// button action
document.querySelector('button').addEventListener('click', () => {
  red.style.right = red.style.right == "" ? "0px" : "";
});
#container {
  width: 100px;
  height: 100px;
  background: blue;
  position: relative;
}

#green, #red {
  width: 50px;
  height: 50px;
  background: green;
  position: absolute;
}

#red {
  background: red;
  right: -10px;
}
<button>move #red</button>
<br /><br />
<div id="container">
  <div id="green"></div>
  <div id="red"></div>
</div>

Is there any way to make this working? Only thing that would work is to unobserve the element and start observing it again. This may be work for an single element, but not if the Observer has hundreds of elements to watch.

// elements
let green = document.querySelector('#green');
let red = document.querySelector('#red');

// observer callback
let callback = entries => {
  entries.forEach(entry => {
    let isInside = entry.intersectionRatio >= 1 ? "fully" : "NOT";
    console.log("#" + entry.target.id + " is " + isInside + " inside #container");
  });
};

// start observer
let options = {root: document.querySelector('#container')};
let observer = new IntersectionObserver(callback, options);
observer.observe(green);
observer.observe(red);

// button action
document.querySelector('button').addEventListener('click', () => {
  red.style.right = red.style.right == "" ? "0px" : "";
  observer.unobserve(red);
  observer.observe(red);
});
#container {
  width: 100px;
  height: 100px;
  background: blue;
  position: relative;
}

#green, #red {
  width: 50px;
  height: 50px;
  background: green;
  position: absolute;
}

#red {
  background: red;
  right: -10px;
}
<button>move #red</button>
<br /><br />
<div id="container">
  <div id="green"></div>
  <div id="red"></div>
</div>
eisbehr
  • 12,243
  • 7
  • 38
  • 63

4 Answers4

1

I don't think it is possible to force the intersection observer to update without calling unobserve/observe on the node, but you can do this for all observed nodes by saving them in a set:

class IntersectionObserverManager {
    constructor(observer) {
        this._observer = observer;
        this._observedNodes = new Set();
    }
    observe(node) {
        this._observedNodes.add(node);
        this._observer.observe(node);
    }
    unobserve(node) {
        this._observedNodes.remove(node);
        this._observer.unobserve(node);
    }
    disconnect() {
        this._observedNodes.clear();
        this._observer.disconnect();
    }
    refresh() {
        for (let node of this._observedNodes) {
            this._observer.unobserve(node);
            this._observer.observe(node);
        }
    }
}

Edit: use a Set instead of a WeakSet since they are iterable so there is no need to check if the element is being observed for each element in the body. Be carefull to call unobseve in order to avoid memory problems.

Isidrok
  • 2,015
  • 10
  • 15
  • That is basically just the same thing i posted in my example. Only difference, it's wrapped in a class. But this is not practicable if we're talking about hundreds or maybe thousands of elements, as mentioned in the question. It's slow and to manual. Therefore, thanks for your time, but it's not a solution to me. ;) – eisbehr Jun 17 '20 at 09:55
  • Its not the same, it also updates children if you moved a container – Isidrok Jun 17 '20 at 09:56
  • Well, yes. But it's the same as it still only `unobserve` and `observe` single elements. You just put a nice class and loop around, but basically it is. And by checking children too, its even slower. ;) – eisbehr Jun 17 '20 at 10:00
  • @eisbehr is this really that slow? Theoretically (I didn't tried myself), the slow part is the recalc of all the boxes in the CSSOM, but that has to be done only once, and would have to happen anyway. After that, it's just a simple filter by boundary check that should take like no time. – Kaiido Jun 20 '20 at 02:28
  • @Kaiido you are right, in fact calling observe/unobserve on `n` elements won't trigger the observer callback `n` times. Since it is being done in the same `call stack` and the observer queue is bound to the `event loop` only 1 call will be made with every element whose position was subject to change as its entries. – Isidrok Jun 20 '20 at 06:36
  • I found this also works fine: in refresh(), start with this._observer.disconnect(); and remove the individual unobserve(node) calls. Should in theory be a tad faster, not that I could or even tried to measure it. – Pete Lomax Dec 01 '20 at 23:22
1

You just need to set threshold: 1.0 for your Intersection observer. This is a tricky parameter to comprehend. Threshold defines the percentage of the intersection at which the Observer should trigger the callback.

The default value is 0 which means callback will be triggered either when the very first or very last pixel of an element intersects a border of the capturing frame. Your element never completely leaves the capturing frame. This is why callback is never called.

If we set the threshold to 1 we tell the observer to trigger our callback when the element is 100% within the frame. It means the callback will be triggered on change in this state of 100% inclusiveness. I hope that sounds understandable :)

// elements
let green = document.querySelector('#green');
let red = document.querySelector('#red');

// observer callback
let callback = entries => {
  entries.forEach(entry => {
    let isInside = entry.intersectionRatio >= 1 ? "fully" : "NOT";
    console.log("#" + entry.target.id + " is " + isInside + " inside #container");
  });
};

// start observer
let options = {root: document.querySelector('#container'), threshold: 1.0 };
let observer = new IntersectionObserver(callback, options);
observer.observe(green);
observer.observe(red);

// button action
document.querySelector('button').addEventListener('click', () => {
  red.style.right = red.style.right == "" ? "0px" : "";
});
#container {
  width: 100px;
  height: 100px;
  background: blue;
  position: relative;
}

#green, #red {
  width: 50px;
  height: 50px;
  background: green;
  position: absolute;
}

#red {
  background: red;
  right: -10px;
}
<button>move #red</button>
<br /><br />
<div id="container">
  <div id="green"></div>
  <div id="red"></div>
</div>
Georgy Nemtsov
  • 786
  • 1
  • 8
  • 19
-1

I may not get the question right, what I understand is you want to trigger the IntersectionObserver, so your callback get called. Why don't you call it directly?

document.querySelector('button').addEventListener('click', () => {
  red.style.right = red.style.right == "" ? "0px" : "";
  callback(red);
});
Torsten Becker
  • 4,330
  • 2
  • 21
  • 22
  • They want the IntersectionObserver to update its internal intersections list. For instance in OP's snippet, the green block is always "fully" inside the blue, the red one is only after the user clicks on the button. But to know that, the internal checks of the IntersectionObserver needs to be triggered again, because normally only scrolling or the first observe will trigger this internal check. – Kaiido Jun 22 '20 at 10:06
  • @Kaiido is right, that's totally not what was asked for. ;) Thanks for your time. – eisbehr Jun 22 '20 at 16:04
-2

One way to do it is to use MutationObserver. If a mutation happens (in this case style change) call observe/unobserve on the element that has been changed. This way you don't have to do it for all elements.

Here is an example:

// observer callback
let callback = entries => {
  entries.forEach(entry => {
    let isInside = entry.intersectionRatio >= 1 ? "fully" : "NOT";
    console.log("#" + entry.target.id + " is " + isInside + " inside #container");
  });
};

// start observer
let options = {
  root: document.querySelector('#container')
};
let observer = new IntersectionObserver(callback, options);

const boxes = document.querySelectorAll('#container > div');
boxes.forEach(box => {
  observer.observe(box);
});

// button action
document.querySelector('button').addEventListener('click', () => {
  red.style.right = red.style.right == "" ? "0px" : "";
});



// Mutation observer
const targetNode = document.querySelector('#container');

// Options for the observer (which mutations to observe). We only need style changes so we set attributes to true. Also, we are observing the children of container so subtree is true
const config = {
  attributes: true,
  childList: false,
  subtree: true,
  attributeFilter: ["style"]
};

// Callback function to execute when mutations are observed
const mutationCallback = (mutationsList, mutationObserver) => {
  for (let mutation of mutationsList) {
    if (mutation.type === 'attributes') {
      console.log('The ' + mutation.attributeName + ' attribute was modified.');
      observer.unobserve(mutation.target);
      observer.observe(mutation.target);
    }
  }
};

// Create an observer instance linked to the callback function
const mutationObserver = new MutationObserver(mutationCallback);

// Start observing the target node for configured mutations
mutationObserver.observe(targetNode, config);
#container {
  width: 300px;
  height: 300px;
  background: lightblue;
  position: relative;
}

#green,
#red {
  width: 100px;
  height: 100px;
  background: green;
  position: absolute;
}

#red {
  background: purple;
  right: -10px;
}
<button>move #red</button>
<br /><br />
<div id="container">
  <div id="green"></div>
  <div id="red"></div>
</div>
Kalimah
  • 11,217
  • 11
  • 43
  • 80
  • "This way you don't have to do it for all elements." Yes they would still have to unobserve reobeserve all the targets of the IntersectionObserver, by appending the element you possibly change the position of all the elements in the page, not just the one that has been appended. And OP is in control of when they do append that element, so the MutationObserver serves no purpose since they already have access to this exact moment. – Kaiido Jun 20 '20 at 02:25
  • In this OP there is no mention of appending elements. The example provided is style change. This can be easily filtered using `attributeFilter: ["style"]` – Kalimah Jun 22 '20 at 09:49
  • That it's appended or unhidden, or adding a margin or anything else that changes the layout is exactly the same, they need to check all the nodes, and they are responsible for this change thus also have access to the moment when this happens -> a MutationObserver is useless. – Kaiido Jun 22 '20 at 10:02