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/// On the server this amounts to a no-op.
58pub fn use_mutation_observer<El, M, F>(
59    target: El,
60    callback: F,
61) -> UseMutationObserverReturn<impl Fn() + Clone + Send + Sync>
62where
63    El: IntoElementsMaybeSignal<web_sys::Element, M>,
64    F: FnMut(Vec<web_sys::MutationRecord>, web_sys::MutationObserver) + 'static,
65{
66    use_mutation_observer_with_options(target, callback, UseMutationObserverOptions::default())
67}
68
69/// Version of [`use_mutation_observer`] that takes a `UseMutationObserverOptions`. See [`use_mutation_observer`] for how to use.
70#[cfg_attr(feature = "ssr", allow(unused_variables, unused_mut))]
71pub fn use_mutation_observer_with_options<El, M, F>(
72    target: El,
73    mut callback: F,
74    options: UseMutationObserverOptions,
75) -> UseMutationObserverReturn<impl Fn() + Clone + Send + Sync>
76where
77    El: IntoElementsMaybeSignal<web_sys::Element, M>,
78    F: FnMut(Vec<web_sys::MutationRecord>, web_sys::MutationObserver) + 'static,
79{
80    #[cfg(feature = "ssr")]
81    {
82        UseMutationObserverReturn {
83            is_supported: Signal::derive(|| true),
84            stop: || {},
85        }
86    }
87
88    #[cfg(not(feature = "ssr"))]
89    {
90        use crate::js;
91        use send_wrapper::SendWrapper;
92
93        let closure_js = Closure::<dyn FnMut(js_sys::Array, web_sys::MutationObserver)>::new(
94            move |entries: js_sys::Array, observer| {
95                #[cfg(debug_assertions)]
96                let _z = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
97
98                callback(
99                    entries
100                        .to_vec()
101                        .into_iter()
102                        .map(|v| v.unchecked_into::<web_sys::MutationRecord>())
103                        .collect(),
104                    observer,
105                );
106            },
107        )
108        .into_js_value();
109
110        let observer: Rc<RefCell<Option<web_sys::MutationObserver>>> = Rc::new(RefCell::new(None));
111
112        let is_supported = use_supported(|| js!("MutationObserver" in &window()));
113
114        let cleanup = {
115            let observer = Rc::clone(&observer);
116
117            move || {
118                let mut observer = observer.borrow_mut();
119                if let Some(o) = observer.as_ref() {
120                    o.disconnect();
121                    *observer = None;
122                }
123            }
124        };
125
126        let targets = target.into_elements_maybe_signal();
127
128        let stop_watch = {
129            let cleanup = cleanup.clone();
130
131            let stop = Effect::watch(
132                move || targets.get(),
133                move |targets, _, _| {
134                    cleanup();
135
136                    if is_supported.get() && !targets.is_empty() {
137                        let obs =
138                            web_sys::MutationObserver::new(closure_js.as_ref().unchecked_ref())
139                                .expect("failed to create MutationObserver");
140
141                        for target in targets.iter().flatten() {
142                            let target = target.clone();
143                            let _ = obs.observe_with_options(&target, &options.clone().into());
144                        }
145
146                        observer.replace(Some(obs));
147                    }
148                },
149                true,
150            );
151
152            move || stop.stop()
153        };
154
155        let stop = sendwrap_fn!(move || {
156            cleanup();
157            stop_watch();
158        });
159
160        on_cleanup({
161            let stop = SendWrapper::new(stop.clone());
162            #[allow(clippy::redundant_closure)]
163            move || stop()
164        });
165
166        UseMutationObserverReturn { is_supported, stop }
167    }
168}
169
170/// Options for [`use_mutation_observer_with_options`].
171#[derive(DefaultBuilder, Clone, Default)]
172pub struct UseMutationObserverOptions {
173    /// Set to `true` to extend monitoring to the entire subtree of nodes rooted at `target`.
174    /// All of the other properties are then extended to all of the nodes in the subtree
175    /// instead of applying solely to the `target` node. The default value is `false`.
176    subtree: bool,
177
178    /// Set to `true` to monitor the target node (and, if `subtree` is `true`, its descendants)
179    /// for the addition of new child nodes or removal of existing child nodes.
180    /// The default value is `false`.
181    child_list: bool,
182
183    /// Set to `true` to watch for changes to the value of attributes on the node or nodes being
184    /// monitored. The default value is `true` if either of `attribute_filter` or
185    /// `attribute_old_value` is specified, otherwise the default value is `false`.
186    attributes: bool,
187
188    /// An array of specific attribute names to be monitored. If this property isn't included,
189    /// changes to all attributes cause mutation notifications.
190    #[builder(into)]
191    attribute_filter: Option<Vec<String>>,
192
193    /// Set to `true` to record the previous value of any attribute that changes when monitoring
194    /// the node or nodes for attribute changes; See
195    /// [Monitoring attribute values](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe#monitoring_attribute_values)
196    /// for an example of watching for attribute changes and recording values.
197    /// The default value is `false`.
198    attribute_old_value: bool,
199
200    /// Set to `true` to monitor the specified target node
201    /// (and, if `subtree` is `true`, its descendants)
202    /// for changes to the character data contained within the node or nodes.
203    /// The default value is `true` if `character_data_old_value` is specified,
204    /// otherwise the default value is `false`.
205    #[builder(into)]
206    character_data: Option<bool>,
207
208    /// Set to `true` to record the previous value of a node's text whenever the text changes on
209    /// nodes being monitored. The default value is `false`.
210    character_data_old_value: bool,
211}
212
213impl From<UseMutationObserverOptions> for web_sys::MutationObserverInit {
214    fn from(val: UseMutationObserverOptions) -> Self {
215        let UseMutationObserverOptions {
216            subtree,
217            child_list,
218            attributes,
219            attribute_filter,
220            attribute_old_value,
221            character_data,
222            character_data_old_value,
223        } = val;
224
225        let init = Self::new();
226
227        init.set_subtree(subtree);
228        init.set_child_list(child_list);
229        init.set_attributes(attributes);
230        init.set_attribute_old_value(attribute_old_value);
231        init.set_character_data_old_value(character_data_old_value);
232
233        if let Some(attribute_filter) = attribute_filter {
234            let array = js_sys::Array::from_iter(attribute_filter.into_iter().map(JsValue::from));
235            init.set_attribute_filter(array.unchecked_ref());
236        }
237        if let Some(character_data) = character_data {
238            init.set_character_data(character_data);
239        }
240
241        init
242    }
243}
244
245/// The return value of [`use_mutation_observer`].
246pub struct UseMutationObserverReturn<F: Fn() + Clone + Send + Sync> {
247    /// Whether the browser supports the MutationObserver API
248    pub is_supported: Signal<bool>,
249    /// A function to stop and detach the MutationObserver
250    pub stop: F,
251}