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/// > Make sure you follow the [instructions in Server-Side Rendering](https://leptos-use.rs/server_side_rendering.html).
56///
57/// On the server this amounts to a no-op.
58///
59/// ## See also
60///
61/// * [`fn@crate::use_element_size`]
62pub fn use_resize_observer<Els, M, F>(
63    target: Els,
64    callback: F,
65) -> UseResizeObserverReturn<impl Fn() + Clone + Send + Sync>
66where
67    Els: IntoElementsMaybeSignal<web_sys::Element, M>,
68    F: FnMut(Vec<web_sys::ResizeObserverEntry>, web_sys::ResizeObserver) + 'static,
69{
70    use_resize_observer_with_options(target, callback, UseResizeObserverOptions::default())
71}
72
73/// Version of [`use_resize_observer`] that takes a `web_sys::ResizeObserverOptions`. See [`use_resize_observer`] for how to use.
74#[cfg_attr(feature = "ssr", allow(unused_variables, unused_mut))]
75pub fn use_resize_observer_with_options<Els, M, F>(
76    target: Els,
77    mut callback: F,
78    options: UseResizeObserverOptions,
79) -> UseResizeObserverReturn<impl Fn() + Clone + Send + Sync>
80where
81    Els: IntoElementsMaybeSignal<web_sys::Element, M>,
82    F: FnMut(Vec<web_sys::ResizeObserverEntry>, web_sys::ResizeObserver) + 'static,
83{
84    #[cfg(feature = "ssr")]
85    {
86        UseResizeObserverReturn {
87            is_supported: Signal::derive(|| true),
88            stop: || {},
89        }
90    }
91
92    #[cfg(not(feature = "ssr"))]
93    {
94        use crate::js;
95
96        let closure_js = Closure::<dyn FnMut(js_sys::Array, web_sys::ResizeObserver)>::new(
97            move |entries: js_sys::Array, observer| {
98                #[cfg(debug_assertions)]
99                let _z = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
100
101                callback(
102                    entries
103                        .to_vec()
104                        .into_iter()
105                        .map(|v| v.unchecked_into::<web_sys::ResizeObserverEntry>())
106                        .collect(),
107                    observer,
108                );
109            },
110        )
111        .into_js_value();
112
113        let observer: Rc<RefCell<Option<web_sys::ResizeObserver>>> = Rc::new(RefCell::new(None));
114
115        let is_supported = use_supported(|| js!("ResizeObserver" in &window()));
116
117        let cleanup = {
118            let observer = Rc::clone(&observer);
119
120            move || {
121                let mut observer = observer.borrow_mut();
122                if let Some(o) = observer.as_ref() {
123                    o.disconnect();
124                    *observer = None;
125                }
126            }
127        };
128
129        let targets = target.into_elements_maybe_signal();
130
131        let stop_watch = {
132            let cleanup = cleanup.clone();
133
134            let stop = Effect::watch(
135                move || targets.get(),
136                move |targets, _, _| {
137                    cleanup();
138
139                    if is_supported.get_untracked() && !targets.is_empty() {
140                        let obs = web_sys::ResizeObserver::new(
141                            closure_js.clone().as_ref().unchecked_ref(),
142                        )
143                        .expect("failed to create ResizeObserver");
144
145                        for target in targets.iter().flatten() {
146                            let target = target.clone();
147                            obs.observe_with_options(&target, &options.clone().into());
148                        }
149                        observer.replace(Some(obs));
150                    }
151                },
152                true,
153            );
154
155            move || stop.stop()
156        };
157
158        let stop = sendwrap_fn!(move || {
159            cleanup();
160            stop_watch();
161        });
162
163        on_cleanup({
164            let stop = stop.clone();
165            #[allow(clippy::redundant_closure)]
166            move || stop()
167        });
168
169        UseResizeObserverReturn { is_supported, stop }
170    }
171}
172
173/// Options for [`use_resize_observer_with_options`].
174#[derive(DefaultBuilder, Clone, Default)]
175pub struct UseResizeObserverOptions {
176    /// The box that is used to determine the dimensions of the target. Defaults to `ContentBox`.
177    #[builder(into)]
178    pub box_: Option<web_sys::ResizeObserverBoxOptions>,
179}
180
181impl From<UseResizeObserverOptions> for web_sys::ResizeObserverOptions {
182    fn from(val: UseResizeObserverOptions) -> Self {
183        let options = web_sys::ResizeObserverOptions::new();
184        options.set_box(
185            val.box_
186                .unwrap_or(web_sys::ResizeObserverBoxOptions::ContentBox),
187        );
188        options
189    }
190}
191
192/// The return value of [`use_resize_observer`].
193pub struct UseResizeObserverReturn<F: Fn() + Clone + Send + Sync> {
194    /// Whether the browser supports the ResizeObserver API
195    pub is_supported: Signal<bool>,
196    /// A function to stop and detach the ResizeObserver
197    pub stop: F,
198}