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}