/** * 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, }, });