leptos_use/
use_mutation_observer.rs

1use crate::core::IntoElementsMaybeSignal;
2use cfg_if::cfg_if;
3use default_struct_builder::DefaultBuilder;
4use leptos::reactive::wrappers::read::Signal;
5use wasm_bindgen::prelude::*;
6
7cfg_if! { if #[cfg(not(feature = "ssr"))] {
8    use crate::{sendwrap_fn, use_supported};
9    use leptos::prelude::*;
10    use std::cell::RefCell;
11    use std::rc::Rc;
12}}
13
14/// Reactive [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver).
15///
16/// Watch for changes being made to the DOM tree.
17///
18/// ## Demo
19///
20/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_mutation_observer)
21///
22/// ## Usage
23///
24/// ```
25/// # use leptos::prelude::*;
26/// # use leptos::html::Pre;
27/// # use leptos_use::{use_mutation_observer_with_options, UseMutationObserverOptions};
28/// #
29/// # #[component]
30/// # fn Demo() -> impl IntoView {
31/// let el = NodeRef::<Pre>::new();
32/// let (text, set_text) = signal("".to_string());
33///
34/// use_mutation_observer_with_options(
35///     el,
36///     move |mutations, _| {
37///         if let Some(mutation) = mutations.first() {
38///             set_text.update(|text| *text = format!("{text}\n{:?}", mutation.attribute_name()));
39///         }
40///     },
41///     UseMutationObserverOptions::default().attributes(true),
42/// );
43///
44/// view! {
45///     <pre node_ref=el>{ text }</pre>
46/// }
47/// # }
48/// ```
49///
50/// ## SendWrapped Return
51///
52/// The returned closure `stop` is a sendwrapped function. It can
53/// only be called from the same thread that called `use_mouse_in_element`.
54///
55/// ## Server-Side Rendering
56///
57/// > Make sure you follow the [instructions in Server-Side Rendering](https://leptos-use.rs/server_side_rendering.html).
58///
59/// On the server this amounts to a no-op.
60pub fn use_mutation_observer<El, M, F>(
61    target: El,
62    callback: F,
63) -> UseMutationObserverReturn<impl Fn() + Clone + Send + Sync>
64where
65    El: IntoElementsMaybeSignal<web_sys::Element, M>,
66    F: FnMut(Vec<web_sys::MutationRecord>, web_sys::MutationObserver) + 'static,
67{
68    use_mutation_observer_with_options(target, callback, UseMutationObserverOptions::default())
69}
70
71/// Version of [`use_mutation_observer`] that takes a `UseMutationObserverOptions`. See [`use_mutation_observer`] for how to use.
72#[cfg_attr(feature = "ssr", allow(unused_variables, unused_mut))]
73pub fn use_mutation_observer_with_options<El, M, F>(
74    target: El,
75    mut callback: F,
76    options: UseMutationObserverOptions,
77) -> UseMutationObserverReturn<impl Fn() + Clone + Send + Sync>
78where
79    El: IntoElementsMaybeSignal<web_sys::Element, M>,
80    F: FnMut(Vec<web_sys::MutationRecord>, web_sys::MutationObserver) + 'static,
81{
82    #[cfg(feature = "ssr")]
83    {
84        UseMutationObserverReturn {
85            is_supported: Signal::derive(|| true),
86            stop: || {},
87        }
88    }
89
90    #[cfg(not(feature = "ssr"))]
91    {
92        use crate::js;
93        use send_wrapper::SendWrapper;
94
95        let closure_js = Closure::<dyn FnMut(js_sys::Array, web_sys::MutationObserver)>::new(
96            move |entries: js_sys::Array, observer| {
97                #[cfg(debug_assertions)]
98                let _z = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
99
100                callback(
101                    entries
102                        .to_vec()
103                        .into_iter()
104                        .map(|v| v.unchecked_into::<web_sys::MutationRecord>())
105                        .collect(),
106                    observer,
107                );
108            },
109        )
110        .into_js_value();
111
112        let observer: Rc<RefCell<Option<web_sys::MutationObserver>>> = Rc::new(RefCell::new(None));
113
114        let is_supported = use_supported(|| js!("MutationObserver" in &window()));
115
116        let cleanup = {
117            let observer = Rc::clone(&observer);
118
119            move || {
120                let mut observer = observer.borrow_mut();
121                if let Some(o) = observer.as_ref() {
122                    o.disconnect();
123                    *observer = None;
124                }
125            }
126        };
127
128        let targets = target.into_elements_maybe_signal();
129
130        let stop_watch = {
131            let cleanup = cleanup.clone();
132
133            let stop = Effect::watch(
134                move || targets.get(),
135                move |targets, _, _| {
136                    cleanup();
137
138                    if is_supported.get() && !targets.is_empty() {
139                        let obs =
140                            web_sys::MutationObserver::new(closure_js.as_ref().unchecked_ref())
141                                .expect("failed to create MutationObserver");
142
143                        for target in targets.iter().flatten() {
144                            let target = target.clone();
145                            let _ = obs.observe_with_options(&target, &options.clone().into());
146                        }
147
148                        observer.replace(Some(obs));
149                    }
150                },
151                true,
152            );
153
154            move || stop.stop()
155        };
156
157        let stop = sendwrap_fn!(move || {
158            cleanup();
159            stop_watch();
160        });
161
162        on_cleanup({
163            let stop = SendWrapper::new(stop.clone());
164            #[allow(clippy::redundant_closure)]
165            move || stop()
166        });
167
168        UseMutationObserverReturn { is_supported, stop }
169    }
170}
171
172/// Options for [`use_mutation_observer_with_options`].
173#[derive(DefaultBuilder, Clone, Default)]
174pub struct UseMutationObserverOptions {
175    /// Set to `true` to extend monitoring to the entire subtree of nodes rooted at `target`.
176    /// All of the other properties are then extended to all of the nodes in the subtree
177    /// instead of applying solely to the `target` node. The default value is `false`.
178    subtree: bool,
179
180    /// Set to `true` to monitor the target node (and, if `subtree` is `true`, its descendants)
181    /// for the addition of new child nodes or removal of existing child nodes.
182    /// The default value is `false`.
183    child_list: bool,
184
185    /// Set to `true` to watch for changes to the value of attributes on the node or nodes being
186    /// monitored. The default value is `true` if either of `attribute_filter` or
187    /// `attribute_old_value` is specified, otherwise the default value is `false`.
188    attributes: bool,
189
190    /// An array of specific attribute names to be monitored. If this property isn't included,
191    /// changes to all attributes cause mutation notifications.
192    #[builder(into)]
193    attribute_filter: Option<Vec<String>>,
194
195    /// Set to `true` to record the previous value of any attribute that changes when monitoring
196    /// the node or nodes for attribute changes; See
197    /// [Monitoring attribute values](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe#monitoring_attribute_values)
198    /// for an example of watching for attribute changes and recording values.
199    /// The default value is `false`.
200    attribute_old_value: bool,
201
202    /// Set to `true` to monitor the specified target node
203    /// (and, if `subtree` is `true`, its descendants)
204    /// for changes to the character data contained within the node or nodes.
205    /// The default value is `true` if `character_data_old_value` is specified,
206    /// otherwise the default value is `false`.
207    #[builder(into)]
208    character_data: Option<bool>,
209
210    /// Set to `true` to record the previous value of a node's text whenever the text changes on
211    /// nodes being monitored. The default value is `false`.
212    character_data_old_value: bool,
213}
214
215impl From<UseMutationObserverOptions> for web_sys::MutationObserverInit {
216    fn from(val: UseMutationObserverOptions) -> Self {
217        let UseMutationObserverOptions {
218            subtree,
219            child_list,
220            attributes,
221            attribute_filter,
222            attribute_old_value,
223            character_data,
224            character_data_old_value,
225        } = val;
226
227        let init = Self::new();
228
229        init.set_subtree(subtree);
230        init.set_child_list(child_list);
231        init.set_attributes(attributes);
232        init.set_attribute_old_value(attribute_old_value);
233        init.set_character_data_old_value(character_data_old_value);
234
235        if let Some(attribute_filter) = attribute_filter {
236            let array = js_sys::Array::from_iter(attribute_filter.into_iter().map(JsValue::from));
237            init.set_attribute_filter(array.unchecked_ref());
238        }
239        if let Some(character_data) = character_data {
240            init.set_character_data(character_data);
241        }
242
243        init
244    }
245}
246
247/// The return value of [`use_mutation_observer`].
248pub struct UseMutationObserverReturn<F: Fn() + Clone + Send + Sync> {
249    /// Whether the browser supports the MutationObserver API
250    pub is_supported: Signal<bool>,
251    /// A function to stop and detach the MutationObserver
252    pub stop: F,
253}