Skip to content

Commit a7be991

Browse files
authored
RemoteMutationObserver improvements (#583)
1 parent 25c858e commit a7be991

File tree

8 files changed

+582
-20
lines changed

8 files changed

+582
-20
lines changed

.changeset/big-lions-train.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
'@remote-dom/core': minor
3+
---
4+
5+
Improved `RemoteMutationObserver` to support automatic emptying and observing multiple nodes
6+
7+
If you are observing a multi-node list — such as a `DocumentFragment` or `<template>` element — you can now provide a custom `id` option when observing each node. This allows you to treat the observer as a kind of "virtual root" for a list of nodes, similar to the role the `DocumentFragment` plays in the DOM. You are responsible for giving each node a unique ID, and this class will take care of correctly attaching that node to the root of the remote tree.
8+
9+
```js
10+
const observer = new RemoteMutationObserver(connection);
11+
12+
let id = 0;
13+
for (const child of documentFragment.childNodes) {
14+
observer.observe(child, {
15+
id: `DocumentFragment:${id++}`,
16+
});
17+
}
18+
```
19+
20+
You can also now provide an `empty` option to `RemoteMutationObserver.disconnect()` in order to clear out children in remote environment:
21+
22+
```js
23+
const observer = new RemoteMutationObserver(connection);
24+
25+
observer.observe(container);
26+
27+
observer.disconnect({empty: true});
28+
```

.changeset/eleven-sloths-worry.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@remote-dom/core': minor
3+
---
4+
5+
Expose `remoteId()` and `setRemoteId()` for getting and setting a Node's remote ID

packages/core/source/elements.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export {RemoteEvent} from './elements/RemoteEvent.ts';
2626
export {RemoteMutationObserver} from './elements/RemoteMutationObserver.ts';
2727

2828
export {
29+
remoteId,
30+
setRemoteId,
2931
connectRemoteNode,
3032
disconnectRemoteNode,
3133
serializeRemoteNode,

packages/core/source/elements/RemoteMutationObserver.ts

Lines changed: 71 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import {
22
remoteId,
3+
setRemoteId,
34
connectRemoteNode,
45
disconnectRemoteNode,
56
serializeRemoteNode,
6-
REMOTE_IDS,
77
} from './internals.ts';
88
import {
99
ROOT_ID,
@@ -32,7 +32,10 @@ import type {RemoteConnection, RemoteMutationRecord} from '../types.ts';
3232
* observer.observe(document.body);
3333
*/
3434
export class RemoteMutationObserver extends MutationObserver {
35-
constructor(private readonly connection: RemoteConnection) {
35+
readonly connection: RemoteConnection;
36+
readonly #observed: Set<Node>;
37+
38+
constructor(connection: RemoteConnection) {
3639
super((records) => {
3740
const addedNodes: Node[] = [];
3841
const remoteRecords: RemoteMutationRecord[] = [];
@@ -102,6 +105,9 @@ export class RemoteMutationObserver extends MutationObserver {
102105

103106
connection.mutate(remoteRecords);
104107
});
108+
109+
this.connection = connection;
110+
this.#observed = new Set();
105111
}
106112

107113
/**
@@ -112,6 +118,18 @@ export class RemoteMutationObserver extends MutationObserver {
112118
observe(
113119
target: Node,
114120
options?: MutationObserverInit & {
121+
/**
122+
* The ID of the root element. If you do not provide an ID, a default value
123+
* considered to be the “root” element will be used. This means that its remote
124+
* attributes, properties, event listeners, and children will all be sent as the root
125+
* element to the remote receiver.
126+
*
127+
* You need to override the default behavior if you are wanting to observe a set of
128+
* nodes, and send each of them to the remote receiver. This may be needed when observing
129+
* a `DocumentFragment` or `<template>` element, which allow for multiple children.
130+
*/
131+
id?: string;
132+
115133
/**
116134
* Whether to send the initial state of the tree to the mutation
117135
* callback.
@@ -121,24 +139,37 @@ export class RemoteMutationObserver extends MutationObserver {
121139
initial?: boolean;
122140
},
123141
) {
124-
REMOTE_IDS.set(target, ROOT_ID);
142+
const id = options?.id ?? ROOT_ID;
143+
setRemoteId(target, id);
144+
this.#observed.add(target);
125145

126146
if (options?.initial !== false && target.childNodes.length > 0) {
127-
const records: RemoteMutationRecord[] = [];
128-
129-
for (let i = 0; i < target.childNodes.length; i++) {
130-
const node = target.childNodes[i]!;
131-
connectRemoteNode(node, this.connection);
132-
133-
records.push([
134-
MUTATION_TYPE_INSERT_CHILD,
135-
ROOT_ID,
136-
serializeRemoteNode(node),
137-
i,
147+
if (id !== ROOT_ID) {
148+
this.connection.mutate([
149+
[
150+
MUTATION_TYPE_INSERT_CHILD,
151+
ROOT_ID,
152+
serializeRemoteNode(target),
153+
this.#observed.size - 1,
154+
],
138155
]);
139-
}
156+
} else if (target.childNodes.length > 0) {
157+
const records: RemoteMutationRecord[] = [];
158+
159+
for (let i = 0; i < target.childNodes.length; i++) {
160+
const node = target.childNodes[i]!;
161+
connectRemoteNode(node, this.connection);
162+
163+
records.push([
164+
MUTATION_TYPE_INSERT_CHILD,
165+
ROOT_ID,
166+
serializeRemoteNode(node),
167+
i,
168+
]);
169+
}
140170

141-
this.connection.mutate(records);
171+
this.connection.mutate(records);
172+
}
142173
}
143174

144175
super.observe(target, {
@@ -149,6 +180,30 @@ export class RemoteMutationObserver extends MutationObserver {
149180
...options,
150181
});
151182
}
183+
184+
disconnect({empty = false}: {empty?: boolean} = {}) {
185+
if (empty && this.#observed.size > 0) {
186+
const records: RemoteMutationRecord[] = [];
187+
188+
for (const node of this.#observed) {
189+
disconnectRemoteNode(node);
190+
const id = remoteId(node);
191+
192+
if (id === ROOT_ID) {
193+
for (let i = 0; i < node.childNodes.length; i++) {
194+
records.push([MUTATION_TYPE_REMOVE_CHILD, id, 0]);
195+
}
196+
} else {
197+
records.push([MUTATION_TYPE_REMOVE_CHILD, ROOT_ID, 0]);
198+
}
199+
}
200+
201+
this.connection.mutate(records);
202+
}
203+
204+
this.#observed.clear();
205+
super.disconnect();
206+
}
152207
}
153208

154209
function indexOf(node: Node, list: NodeList) {

packages/core/source/elements/RemoteRootElement.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ import {ROOT_ID, MUTATION_TYPE_INSERT_CHILD} from '../constants.ts';
22
import type {RemoteConnection, RemoteMutationRecord} from '../types.ts';
33

44
import {
5+
setRemoteId,
56
remoteConnection,
67
connectRemoteNode,
78
serializeRemoteNode,
89
updateRemoteElementProperty,
910
callRemoteElementMethod,
10-
REMOTE_IDS,
1111
} from './internals.ts';
1212

1313
/**
@@ -35,7 +35,7 @@ import {
3535
export class RemoteRootElement extends HTMLElement {
3636
constructor() {
3737
super();
38-
REMOTE_IDS.set(this, ROOT_ID);
38+
setRemoteId(this, ROOT_ID);
3939
}
4040

4141
connect(connection: RemoteConnection): void {

packages/core/source/elements/internals.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,16 @@ export function remoteId(node: Node) {
2727
let remoteID = REMOTE_IDS.get(node);
2828
if (remoteID == null) {
2929
remoteID = String(id++);
30-
REMOTE_IDS.set(node, remoteID);
30+
setRemoteId(node, remoteID);
3131
}
3232

3333
return remoteID;
3434
}
3535

36+
export function setRemoteId(node: Node, id: string) {
37+
REMOTE_IDS.set(node, id);
38+
}
39+
3640
export const REMOTE_PROPERTIES = new WeakMap<Node, Record<string, any>>();
3741

3842
/**

0 commit comments

Comments
 (0)