when().observes()

Similar to when() simplifying event listeners, when().observes() helps using native observers both inside and outside Yozo components.

To keep things consistent and easy to use, when().observes() can be thought of as the observer variant of when().does(); their usage is very similar.

Note: when().observes() is a flow-based wrapper around native observers, most notably MutationObserver, IntersectionObserver and ResizeObserver. The options are passed to the respective observer as-is. It is probably best for authors to first get acquainted with the native forms of these APIs before using when().observes().

Syntax

when(...targets).observes(type); when(...targets).observes(type, options);

Parameters

...targets
One or more targets to be observed. The type of items passed here are dependent on the observed type, though most of the time they'll be Element objects.
type
The type of observer, as a string. The three main accepted values are 'mutation', 'intersection' or 'resize'. Other values are also supported, even for custom observers that conform to the Observer interface, though usability may vary. See the examples below for more information on uncommon or custom observers.
options optional
The options for the observer. This is passed both as second argument to the relevant observer's constructor, and as second argument to the .observe() call. This allows usage to be consistent across all three main observer types. In practice, this argument is only optional if it is also optional for the specified observer.

Return value

A Flow object that triggers whenever the observer in question observes a new change. In other words; a trigger fires at the same time that a native observer's callback (passed to the constructor) is called, with the same arguments. To achieve the same basic behavior as one might with a native observer, use .then() to listen for triggers. To disconnect the observer, use .stop(), .until(), or any other method that stops the flow. When used inside monitored contexts such as inside an effect() or connected() callback, stopping the flow manually is not not necessary; when the monitored context is cleaned up after, the observer too is disconnected and removed.

Examples

Mutations

The most commonly used observer is probably the MutationObserver. While the native API requires authors to create an observer object using the constructor, and subsequently pass a Node and relevant options to the .observe() method, the when().observes() syntax pulls it a bit more in line with how event listeners are written. For example, we can watch the attributes on a certain <div> element using

const div = document.querySelector('div'); const options = { attributes: true }; when(div).observes('mutation', options).then(records => { // an attribute changed! });

The callback in .then() also receives a second parameter, observer, beyond the records argument; however, usually this is used to disconnect the observer at some point. Since when().observes() is flow-based, it is better (and often easier) to stop the flow instead, and the flow will take care of disconnecting and cleaning up after the observer.

Intersections

Next, let's see an example for an IntersectionObserver. We'll have a graphic, and we want an animation to run once the graphic is completely visible on the screen. Additionally, we only want to run this animation once. For the IntersectionObserver, this means the options we'll need to pass are { threshold: 1 }. Unlike the native MutationObserver, the IntersectionObserver requires its argument to be passed to the constructor. For when().observes(), that fact is not relevant, and we can simply write:

const graphic = document.querySelector('#graphic'); const options = { threshold: 1 }; when(graphic).observes('intersection', options).then(() => { // animate… }).once();

Since we're working with a Flow, limiting the observer to one observation becomes simple with .once(). That then takes care of disconnecting the observer. If we wanted the animation to start running when the graphic starts being visible as opposed to being entirely visible (i.e. having a threshold of 0) then we could omit the options altogether and have an even shorter

const graphic = document.querySelector('#graphic'); when(graphic).observes('intersection').then(() => { // animate… }).once();

Custom observers

Observers follow a specific interface; specifically, they have .observe() and .disconnect() methods, and a constructor to which a callback is passed. They also generally have a name ending in Observer. The way when().observes() works is that it takes the type given and uses it to look up an observer under that name in the global scope. For example, give it 'mutation', and it find MutationObserver; but also something like 'reporting' can be passed to find the much less common ReportingObserver. It is also possible to implement your own observer and use it with when().observes(); for example, we might write a MyCustomObserver, assign it to the global scope, and observe using when(…).observes('my-custom').

Unfortunately, the two other native observer types 'performance' and 'reporting' deviate somewhat from the main three (the DOM-related) types in terms of syntax. Specifically, they do not receive a specific object to observe, but instead observe something on a document-level. The when().observes() syntax was not written for these specifically, so usage is a bit awkward. Let's do a short example for both of them.

For a native PerformanceObserver, there is only one parameter, an options argument, passed to the .observes() method. Unfortunately, that's where when().observes() passes the thing to observe. To make it work, we therefore need to pretend that this argument is the thing we're observing by passing it to when():

const options = { entryTypes: ['mark', 'measure'] }; when(options).observes('performance').then(list => { list.getEntries().forEach(entry => { // Log performance… }); });

Similarly, but differently, the ReportingObserver deviates from the DOM-related observers. This one does not expect any arguments to the .observe() method, and instead passes the options object to the constructor. As such, usage becomes awkward for a different reason; the options go in the expected place, but there is no target to pass to when(). To make it work, we'll pass null as a target, resulting in an underlying .observe(null) call, which works because the argument is not used.

const options = { types: ['deprecation'], buffered: true }; when(null).observes('reporting', options).then(reports => { // Reports observed… });

Since neither of these are particularly intuitive in usage, it is recommended either to write a wrapper around it or to simply use their native counterparts.

As mentioned previously, it is also possible to write custom observers and use them with when().observes(). A few notes about what when().observes() does with each of its arguments specifically:

Lastly, the .disconnect() method is expected to exist on the observer and called when the flow is stopped (in a .cleanup() handler). Yozo does not use the .takeRecords() method, so this method is technically optional when writing a custom observer to be used with when().observes().

See also