Modern Web Weekly #64
The modern web, tested and explained in plain English
Better drag-and-drop with Observer
If you’ve ever built drag-and-drop functionality, you know that this can be tricky to implement and can result in brittle code that’s hard to follow.
You need to implement a handler for the mousemove event, but this handler should only be called when it’s preceded by a mousedown event on the element that needs to be dragged. And when the mouseup event fires, the handler for mousemove needs to be removed:
document.addEventListener('mousedown' e => {
// get the element the event was fired on
const dragElement = e.target;
const handleMouseMove = e = {
//handle moving the element
}
// add the mousemove event handler
document.addEventListener('mousemove', handleMouseMove);
// add the mouseup event handler
document.addEventListener(’mouseup’, e = {
// remove the mousemove event handler
document.removeEventListener(‘mousemove’, handleMouseMove);
});
}We need to nest event handlers, and as this code grows, it gets more complex and harder to follow.
Luckily, we can use the Observable API to make this code simpler and easier to follow.
This API implements the Observer/Observable pattern in which an Observable fires a stream of events that the Observer can subscribe to.
The Observable API adds a when() method to HTMLElement which takes the name of an event and returns a new Observable that adds an event handler to the element when its subscribe() is called.
You can then call methods like filter, map, reduce and takeUntil to turn event handling into a declarative flow that’s much easier to reason about and to compose.
For example, here’s how you can add a click event handler to a container and only handle the event when it takes place on a child element that has class=”foo” using the filter method:
container
.when(’click’)
.filter((e) => e.target.matches(’.foo’))
.map((e) => ({ x: e.clientX, y: e.clientY }))
.subscribe({ next: handleClickAtPoint });After filtering, we return the clientX and clientY properties of the event using map() and send this data to the handleClickAtPoint() function.
Observable has other useful methods like takeUntil that invokes the handler function until another event takes place. For example, here’s how you can handle the mousemove event on <body> until the mouseup event takes place, after which mousemove will no longer be handled:
document.body
.when(’mousemove’)
.takeUntil(document.when(’mouseup’))To implement drag-and-drop, we need to start by adding a mousedown event handler to document, and then we need to get the element on which the event took place. Let’s say we only want to drag an element with class=”box”. We can use the filter method for this:
document.body
.when(’mousedown’)
.filter(e => e.target.classList.contains(’box’))Now, the mousedown event will only take place when it’s fired on the element with class=”box”.
After that, we need to get the starting position of this element, and then we add the mousemove event handler that should be invoked until the mouseup event takes place.
The actual event handler isn’t added until the subscribe method of the Observable is called, so this is where the event is handled. Note that Observables are “lazy” and don’t emit any events until they are subscribed to.
The subscribe method is passed an object with a next method that gets the event as its only argument:
document.body
.when(’mousedown’)
.filter(e => e.target.classList.contains(’box’))
.subscribe({
next: e => {
// the target of the event is the element with
// class="box"
const {target} = e;
// get the starting position of the element
const startX = e.clientX - target.offsetLeft;
const startY = e.clientY - target.offsetTop;
});Now that we have the starting position of the element to be dragged, we can add the mousemove event handler that is invoked until the mouseup event takes place. We then call map to get the current position of the mouse while it’s moving, and then call subscribe to handle the event, which means updating the position of the element with CSS:
document.body
.when(’mousemove’)
.takeUntil(document.when(’mouseup’))
.map((e) => ({
// return the current position of the mouse as {x, y}
x: e.clientX - startX,
y: e.clientY - startY
}))
.subscribe({
next: ({x, y}) => {
// update the position of the dragged element
target.style.left = `${x}px`;
target.style.top = `${y}px`;
}
}); And this is what the completed code looks like:
document.body
.when(’mousedown’)
.filter(e => e.target.classList.contains(’box’))
.subscribe({
next: e => {
const {target} = e;
const startX = e.clientX - target.offsetLeft;
const startY = e.clientY - target.offsetTop;
document.body
.when(’mousemove’)
.takeUntil(document.when(’mouseup’))
.map((e) => ({
x: e.clientX - startX,
y: e.clientY - startY
}))
.subscribe({
next: ({x, y}) => {
target.style.left = `${x}px`;
target.style.top = `${y}px`;
}
});
}
});Instead of a bunch of nested event handlers, we now have an easy-to-follow, declarative flow that is much easier to understand and reason about.
Check out this codepen for the demo.
The Observable API is supported in all Chromium-based browsers and Safari. At the time of writing, the Observable API needs to be explicitly enabled in the feature flags in Safari (Develop menu > Feature Flags).
This demo contains two polyfills for HTMLElement.when and Observable.takeUntil that are not yet supported by Safari.
In addition to methods like filter and map, Observable has other, array-like methods like flatMap, some, forEach, find, and reduce that I will write about in future editions of Modern Web Weekly.
Throttle individual requests in Chrome Canary
This is one I’ve been waiting for, and it’s now implemented in Chrome Canary, throttling of individual requests.
You’ll need to explicitly enable this by visiting chrome://flags/#devtools-individual-request-throttling, and then you can right-click any request in the Network tab to throttle it, just like you already could with all requests at once.
You can also throttle all requests from a specific domain.
I picked this one up from Stefan Judis’ newsletter Web Weekly, which brings you weekly news about web fundamentals and the latest browser technologies. It’s a very nice overview of what’s happening on the web, and I consider it a nice “companion” to my own newsletter. Definitely worth subscribing to, which you can do here.
Indicate unread notifications with the Badging API
I updated my push notifications demo, which now shows a badge on the app icon with the number of unread push notifications using the Badging API.
This API is supported in Chrome, Edge, and Safari, and is typically used in conjunction with the “push” event handler in a service worker. It consists of two methods on the navigator object, setAppBadge and clearAppBadge:
// sets a badge with the number 3
navigator.setAppBadge(3)
// clears the badge
navigator.clearAppBadge()
// also clears the badge
navigator.setAppBadge(0)
// sets a badge with a dot
navigator.setAppBadge()Badges are typically shown when notifications are unread, and in the demo of What PWA Can Do Today, I chose to show badges when the PWA is not running:
When the PWA is opened and then moved to the background, the badge will be cleared on iOS. On Android, the badge can only be cleared when the notifications are clicked or cleared.
In the “push” event handler, I show the notification and then check if the service worker has active clients, which means apps that are not in the background or not running. If this is not the case, the badge is either set or, when it’s already shown, the number of unread notifications is incremented:
e.waitUntil(
self.registration.showNotification(title, options)
.then(hasActiveClients)
.then((activeClients) => {
if(!activeClients) {
updateBadges();
}
})Here’s the implementation of hasActiveClients:
const hasActiveClients = async () => {
const clients = await self.clients.matchAll({
includeUncontrolled: true,
});
return clients.some(({visibilityState}) =>
visibilityState === ‘visible’);
};The number of unread notifications is stored in IndexedDB and is updated in the updateBadges function:
const updateBadges = async (inc = 1) => {
// open the IndexedDB store
const store = await openStore('badgeStore', ‘readwrite’);
// get the number of unread notifications or 0 if not stored
const result = await store.get(’num’) || {num: ‘num’, count: 0};
// get the current number of unread notifications and increment it
// by the "inc" argument (1 if not given)
const numBadges = result.count;
const count = Math.max(0, numBadges + inc);
// store the updated number
await store.put({num: ‘num’, count});
// set the badge number
navigator.setAppBadge(count);
}For brevity, I omitted the implementation of openStore, but you can find it here.
PWA Audit: on your way to a great PWA
Do you already have a PWA, but are you running into issues with performance, security, or functionality? Or are you not sure how to make your PWA better?
I can help you by running an audit of your PWA
I will evaluate it on more than 35 criteria and provide you with clear and actionable instructions on how to improve it. No generic stuff that you can get anywhere, but an in-depth quality checkup to get you on your way to a great PWA.
Some of the criteria I will evaluate it on are:
Installability
Cross-device and cross-platform compatibility
Offline support
Usability
Effective use of modern web APIs
Performance
Security
Your investment in the improvement of your PWA through the audit is $499, excluding VAT (where applicable).
If you want to request an audit or first would like to know more, you can
fill out the form or book a call with me.
Coming soon to a browser near you:
caret-shape and caret-animation
I must admit I didn’t even know about the caret-color CSS property that enables you to configure the color of the caret. It’s even supported in all major browsers, and now we have two upcoming properties that let you customize the caret even further: caret-shape and caret-animation.
The caret is the visual indicator that appears in editable elements (like input fields or elements with contenteditable) to indicate where the next character will be inserted or deleted.
The caret-shape property lets you change the shape of the caret and takes the following values:
auto: the default value. The shape is determined by the platformbar: the caret appears as a thin vertical lineblock: the caret appears as a rectangleunderscore: the caret appears as a horizontal line underneath the next character
The caret-animation property takes two values:
auto: the default value. The caret blinks on and offmanual: the caret doesn’t blink
If you want to add a custom animation for the caret, you set caret-animation: manual and then you simply set animation.
For example:
@keyframes custom-caret-animation {
from {
caret-shape: block;
}
to {
caret-shape: underscore;
}
}
.foo {
caret-animation: manual;
animation: custom-caret-animation 1s infinite linear;
}caret-shape and caret-animation are currently only supported in Chrome Canary 144.
Check this demo by Una Kravetz for some examples.
The State of PWAs on YouTube
On October 1, I gave my talk The State of PWAs at Frontkon in Brno, Czech Republic and this is now available on YouTube:


