/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow strict-local
*/
import type ReadOnlyNode from 'react-native/src/private/webapis/dom/nodes/ReadOnlyNode';
import type NodeList from 'react-native/src/private/webapis/dom/oldstylecollections/NodeList';
import {RNTesterThemeContext} from '../../components/RNTesterTheme';
import * as React from 'react';
import {type ElementRef, useContext, useEffect, useRef, useState} from 'react';
import {Pressable, ScrollView, StyleSheet, Text, View} from 'react-native';
import ReadOnlyElement from 'react-native/src/private/webapis/dom/nodes/ReadOnlyElement';
import MutationObserver from 'react-native/src/private/webapis/mutationobserver/MutationObserver';
export const name = 'MutationObserver Example';
export const title = name;
export const description =
'- Tap on elements to append a child.\n- Long tap on elements to remove them.';
export function render(): React.Node {
return ;
}
const nextIdByPrefix: Map = new Map();
function generateId(prefix: string): string {
let nextId = nextIdByPrefix.get(prefix);
if (nextId == null) {
nextId = 1;
}
nextIdByPrefix.set(prefix, nextId + 1);
return prefix + nextId;
}
const rootId = generateId('example-item-');
function useTemporaryValue(duration: number = 2000): [?T, (?T) => void] {
const [value, setValue] = useState(null);
useEffect(() => {
const timeoutId = setTimeout(() => {
setValue(null);
}, duration);
return () => clearTimeout(timeoutId);
// we need to set the timer every time the value changes
}, [duration, value]);
return [value, setValue];
}
function MutationObserverExample(): React.Node {
const parentViewRef = useRef>(null);
const [showExample, setShowExample] = useState(true);
const theme = useContext(RNTesterThemeContext);
const [message, setMessage] = useTemporaryValue();
useEffect(() => {
const parentNode = parentViewRef.current;
if (!parentNode) {
return;
}
const mutationObserver = new MutationObserver(records => {
const messages = [];
records.forEach(record => {
if (record.addedNodes.length > 0) {
console.log(
'MutationObserverExample: added nodes',
nodeListToString(record.addedNodes),
);
messages.push(`Added nodes: ${nodeListToString(record.addedNodes)}`);
}
if (record.removedNodes.length > 0) {
console.log(
'MutationObserverExample: removed nodes',
nodeListToString(record.removedNodes),
);
messages.push(
`Removed nodes: ${nodeListToString(record.removedNodes)}`,
);
}
});
setMessage(messages.join(',\n'));
});
// $FlowExpectedError[incompatible-call]
mutationObserver.observe(parentNode, {
subtree: true,
childList: true,
});
return () => {
console.log('MutationObserverExample: disconnecting mutation observer');
mutationObserver.disconnect();
nextIdByPrefix.clear();
};
}, [setMessage]);
const exampleId = showExample ? rootId : '';
return (
<>
{showExample ? (
setShowExample(false)}
/>
) : null}
{message}
>
);
}
function ExampleItem(props: {
id: string,
label: string,
onRemove?: () => void,
}): React.Node {
const theme = useContext(RNTesterThemeContext);
const [children, setChildren] = useState<
$ReadOnlyArray<[string, React.Node]>,
>([]);
return (
{
props.onRemove?.();
}}
onPress={() => {
const id = generateId(props.label + '-');
setChildren(prevChildren => [
...prevChildren,
[
id,
{
setChildren(prevChildren2 =>
prevChildren2.filter(pair => pair[0] !== id),
);
}}
/>,
],
]);
}}>
{props.label != null ? (
{props.label}
) : null}
{children.map(([id, child]) => child)}
);
}
function nodeListToString(nodeList: NodeList): string {
return [...nodeList]
.map(
node => (node instanceof ReadOnlyElement && node.id) || '',
)
.join(', ');
}
const styles = StyleSheet.create({
parent: {
flex: 1,
backgroundColor: 'white',
},
item: {
backgroundColor: 'rgba(0, 0, 0, 0.5)',
flex: 1,
gap: 16,
minHeight: 50,
padding: 40,
},
label: {
position: 'absolute',
top: 0,
right: 0,
fontSize: 10,
},
message: {
padding: 10,
},
});