leptos_use/
use_resize_observer.rs

1use crate::core::IntoElementsMaybeSignal;
2use cfg_if::cfg_if;
3use default_struct_builder::DefaultBuilder;
4use leptos::reactive::wrappers::read::Signal;
5
6cfg_if! { if #[cfg(not(feature = "ssr"))] {
7    use crate::{sendwrap_fn, use_supported};
8    use std::cell::RefCell;
9    use std::rc::Rc;
10    use wasm_bindgen::prelude::*;
11    use leptos::prelude::*;
12}}
13
14/// Reports changes to the dimensions of an Element's content or the border-box.
15///
16/// Please refer to [ResizeObserver on MDN](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver)
17/// for more details.
18///
19/// ## Demo
20///
21/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_resize_observer)
22///
23/// ## Usage
24///
25/// ```
26/// # use leptos::{html::Div, prelude::*};
27/// # use leptos_use::use_resize_observer;
28/// #
29/// # #[component]
30/// # fn Demo() -> impl IntoView {
31/// let el = NodeRef::<Div>::new();
32/// let (text, set_text) = signal("".to_string());
33///
34/// use_resize_observer(
35///     el,
36///     move |entries, observer| {
37///         let rect = entries[0].content_rect();
38///         set_text.set(format!("width: {}\nheight: {}", rect.width(), rect.height()));
39///     },
40/// );
41///
42/// view! {
43///     <div node_ref=el>{ move || text.get() }</div>
44/// }
45/// # }
46/// ```
47///
48/// ## SendWrapped Return
49///
50/// The returned closure `stop` is a sendwrapped function. It can
51/// only be called from the same thread that called `use_resize_observer`.
52///
53/// ## Server-Side Rendering
54///
55/// On the server this amounts to a no-op.
56///
57/// ## See also
58///
59/// * [`fn@crate::use_element_size`]
60pub fn use_resize_observer<Els, M, F>(
61    target: Els,
62    callback: F,
63) -> UseResizeObserverReturn<impl Fn() + Clone + Send + Sync>
64where
65    Els: IntoElementsMaybeSignal<web_sys::Element, M>,
66    F: FnMut(Vec<web_sys::ResizeObserverEntry>, web_sys::ResizeObserver) + 'static,
67{
68    use_resize_observer_with_options(target, callback, UseResizeObserverOptions::default())
69}
70
71/// Version of [`use_resize_observer`] that takes a `web_sys::ResizeObserverOptions`. See [`use_resize_observer`] for how to use.
72#[cfg_attr(feature = "ssr", allow(unused_variables, unused_mut))]
73pub fn use_resize_observer_with_options<Els, M, F>(
74    target: Els,
75    mut callback: F,
76    options: UseResizeObserverOptions,
77) -> UseResizeObserverReturn<impl Fn() + Clone + Send + Sync>
78where
79    Els: IntoElementsMaybeSignal<web_sys::Element, M>,
80    F: FnMut(Vec<web_sys::ResizeObserverEntry>, web_sys::ResizeObserver) + 'static,
81{
82    #[cfg(feature = "ssr")]
83    {
84        UseResizeObserverReturn {
85            is_supported: Signal::derive(|| true),
86            stop: || {},
87        }
88    }
89
90    #[cfg(not(feature = "ssr"))]
91    {
92        use crate::js;
93
94        let closure_js = Closure::<dyn FnMut(js_sys::Array, web_sys::ResizeObserver)>::new(
95            move |entries: js_sys::Array, observer| {
96                #[cfg(debug_assertions)]
97                let _z = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
98
99                callback(
100                    entries
101                        .to_vec()
102                        .into_iter()
103                        .map(|v| v.unchecked_into::<web_sys::ResizeObserverEntry>())
104                        .collect(),
105                    observer,
106                );
107            },
108        )
109        .into_js_value();
110
111        let observer: Rc<RefCell<Option<web_sys::ResizeObserver>>> = Rc::new(RefCell::new(None));
112
113        let is_supported = use_supported(|| js!("ResizeObserver" in &window()));
114
115        let cleanup = {
116            let observer = Rc::clone(&observer);
117
118            move || {
119                let mut observer = observer.borrow_mut();
120                if let Some(o) = observer.as_ref() {
121                    o.disconnect();
122                    *observer = None;
123                }
124            }
125        };
126
127        let targets = target.into_elements_maybe_signal();
128
129        let stop_watch = {
130            let cleanup = cleanup.clone();
131
132            let stop = Effect::watch(
133                move || targets.get(),
134                move |targets, _, _| {
135                    cleanup();
136
137                    if is_supported.get_untracked() && !targets.is_empty() {
138                        let obs = web_sys::ResizeObserver::new(
139                            closure_js.clone().as_ref().unchecked_ref(),
140                        )
141                        .expect("failed to create ResizeObserver");
142
143                        for target in targets.iter().flatten() {
144                            let target = target.clone();
145                            obs.observe_with_options(&target, &options.clone().into());
146                        }
147                        observer.replace(Some(obs));
148                    }
149                },
150                true,
151            );
152
153            move || stop.stop()
154        };
155
156        let stop = sendwrap_fn!(move || {
157            cleanup();
158            stop_watch();
159        });
160
161        on_cleanup({
162            let stop = stop.clone();
163            #[allow(clippy::redundant_closure)]
164            move || stop()
165        });
166
167        UseResizeObserverReturn { is_supported, stop }
168    }
169}
170
171/// Options for [`use_resize_observer_with_options`].
172#[derive(DefaultBuilder, Clone, Default)]
173pub struct UseResizeObserverOptions {
174    /// The box that is used to determine the dimensions of the target. Defaults to `ContentBox`.
175    #[builder(into)]
176    pub box_: Option<web_sys::ResizeObserverBoxOptions>,
177}
178
179impl From<UseResizeObserverOptions> for web_sys::ResizeObserverOptions {
180    fn from(val: UseResizeObserverOptions) -> Self {
181        let options = web_sys::ResizeObserverOptions::new();
182        options.set_box(
183            val.box_
184                .unwrap_or(web_sys::ResizeObserverBoxOptions::ContentBox),
185        );
186        options
187    }
188}
189
190/// The return value of [`use_resize_observer`].
191pub struct UseResizeObserverReturn<F: Fn() + Clone + Send + Sync> {
192    /// Whether the browser supports the ResizeObserver API
193    pub is_supported: Signal<bool>,
194    /// A function to stop and detach the ResizeObserver
195    pub stop: F,
196}