Skip to main content

leptos_use/
use_element_size.rs

1use crate::core::IntoElementMaybeSignal;
2use crate::core::Size;
3use cfg_if::cfg_if;
4use default_struct_builder::DefaultBuilder;
5use leptos::prelude::*;
6use leptos::reactive::wrappers::read::Signal;
7
8cfg_if! { if #[cfg(not(feature = "ssr"))] {
9    use crate::{use_resize_observer_with_options, UseResizeObserverOptions};
10    use crate::{watch_with_options, WatchOptions};
11    use wasm_bindgen::JsCast;
12}}
13
14/// Reactive size of an HTML element.
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_element_size)
22///
23/// ## Usage
24///
25/// ```
26/// # use leptos::{html::Div, prelude::*};
27/// # use leptos_use::{use_element_size, UseElementSizeReturn};
28/// #
29/// # #[component]
30/// # fn Demo() -> impl IntoView {
31/// let el = NodeRef::<Div>::new();
32///
33/// let UseElementSizeReturn { width, height } = use_element_size(el);
34///
35/// view! {
36///     <div node_ref=el>
37///         "Width: " {width}
38///         "Height: " {height}
39///     </div>
40/// }
41/// # }
42/// ```
43///
44/// ## Server-Side Rendering
45///
46/// > Make sure you follow the [instructions in Server-Side Rendering](https://leptos-use.rs/server_side_rendering.html).
47///
48/// On the server the returned signals always contain the value of the `initial_size` option.
49///
50/// ## See also
51///
52/// - [`fn@crate::use_resize_observer`]
53pub fn use_element_size<El, M>(target: El) -> UseElementSizeReturn
54where
55    El: IntoElementMaybeSignal<web_sys::Element, M>,
56{
57    use_element_size_with_options(target, UseElementSizeOptions::default())
58}
59
60/// Version of [`use_element_size`] that takes a `UseElementSizeOptions`. See [`use_element_size`] for how to use.
61#[cfg_attr(feature = "ssr", allow(unused_variables))]
62pub fn use_element_size_with_options<El, M>(
63    target: El,
64    options: UseElementSizeOptions,
65) -> UseElementSizeReturn
66where
67    El: IntoElementMaybeSignal<web_sys::Element, M>,
68{
69    let UseElementSizeOptions { box_, initial_size } = options;
70
71    let (width, set_width) = signal(initial_size.width);
72    let (height, set_height) = signal(initial_size.height);
73
74    #[cfg(not(feature = "ssr"))]
75    {
76        let box_ = box_.unwrap_or(web_sys::ResizeObserverBoxOptions::ContentBox);
77
78        let target = target.into_element_maybe_signal();
79
80        let is_svg = move || {
81            if let Some(target) = target.get_untracked() {
82                target
83                    .namespace_uri()
84                    .map(|ns| ns.contains("svg"))
85                    .unwrap_or(false)
86            } else {
87                false
88            }
89        };
90
91        let _ = use_resize_observer_with_options(
92            target,
93            move |entries, _| {
94                let entry = &entries[0];
95
96                let box_size = match box_ {
97                    web_sys::ResizeObserverBoxOptions::ContentBox => entry.content_box_size(),
98                    web_sys::ResizeObserverBoxOptions::BorderBox => entry.border_box_size(),
99                    web_sys::ResizeObserverBoxOptions::DevicePixelContentBox => {
100                        entry.device_pixel_content_box_size()
101                    }
102                    _ => unreachable!(),
103                };
104
105                if is_svg() {
106                    if let Some(target) = target.get()
107                        && let Ok(Some(styles)) = window().get_computed_style(&target)
108                    {
109                        set_height.set(
110                            styles
111                                .get_property_value("height")
112                                .map(|v| v.parse().unwrap_or_default())
113                                .unwrap_or_default(),
114                        );
115                        set_width.set(
116                            styles
117                                .get_property_value("width")
118                                .map(|v| v.parse().unwrap_or_default())
119                                .unwrap_or_default(),
120                        );
121                    }
122                } else if !box_size.is_null() && !box_size.is_undefined() && box_size.length() > 0 {
123                    let format_box_size = if box_size.is_array() {
124                        box_size.to_vec()
125                    } else {
126                        vec![box_size.into()]
127                    };
128
129                    set_width.set(format_box_size.iter().fold(0.0, |acc, v| {
130                        acc + v
131                            .as_ref()
132                            .clone()
133                            .unchecked_into::<web_sys::ResizeObserverSize>()
134                            .inline_size()
135                    }));
136                    set_height.set(format_box_size.iter().fold(0.0, |acc, v| {
137                        acc + v
138                            .as_ref()
139                            .clone()
140                            .unchecked_into::<web_sys::ResizeObserverSize>()
141                            .block_size()
142                    }))
143                } else {
144                    // fallback
145                    set_width.set(entry.content_rect().width());
146                    set_height.set(entry.content_rect().height())
147                }
148            },
149            UseResizeObserverOptions::default().box_(box_),
150        );
151
152        let _ = watch_with_options(
153            move || target.get(),
154            move |ele, _, _| {
155                if ele.is_some() {
156                    set_width.set(initial_size.width);
157                    set_height.set(initial_size.height);
158                } else {
159                    set_width.set(0.0);
160                    set_height.set(0.0);
161                }
162            },
163            WatchOptions::default().immediate(false),
164        );
165    }
166
167    UseElementSizeReturn {
168        width: width.into(),
169        height: height.into(),
170    }
171}
172
173#[derive(DefaultBuilder, Default)]
174/// Options for [`use_element_size_with_options`].
175pub struct UseElementSizeOptions {
176    /// Initial size returned before any measurements on the `target` are done. Also the value reported
177    /// at first when the `target` is a signal and changes.
178    initial_size: Size,
179
180    /// The box that is used to determine the dimensions of the target. Defaults to `ContentBox`.
181    #[builder(into)]
182    pub box_: Option<web_sys::ResizeObserverBoxOptions>,
183}
184
185/// The return value of [`use_element_size`].
186pub struct UseElementSizeReturn {
187    /// The width of the element.
188    pub width: Signal<f64>,
189    /// The height of the element.
190    pub height: Signal<f64>,
191}