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}