yew_component_size/
lib.rs

1#![deny(missing_docs)]
2
3//! ![Crates.io](https://img.shields.io/crates/l/yew-component-size) ![Crates.io](https://img.shields.io/crates/v/yew-component-size)
4//!
5//! A Yew component that emits events when the parent component changes width/height.
6//! Only compatible with Yew using web_sys.
7//!
8//! # Example:
9//! ```rust
10//! let onsize = self.link.callback(|size: ComponentSize| {
11//!     // Access to `size.width` and `size.height`
12//! });
13//!
14//! html! {
15//!     // Parent that you're tracking the size of must be `position: relative`
16//!     <div style="position: relative;">
17//!         // ...
18//!         <ComponentSizeObserver onsize=onsize />
19//!     </div>   
20//! }
21//! ```
22//!
23//! # How it works
24//!
25//! This uses a trick borrowed from Svelte where we use an iframe that is positioned absolutely
26//! to fill it's parent element, and then we listen to the resize event of iframe's window.
27//!
28//! _**Note:** This incurs a small cost and so should not be used on a large number of elements at the same time._
29//!
30//! # License
31//!
32//! Licensed under either of
33//!
34//!  * Apache License, Version 2.0
35//!    ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
36//!  * MIT license
37//!    ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
38//!
39//! at your option.
40//!
41//! # Contribution
42//!
43//! Unless you explicitly state otherwise, any contribution intentionally submitted
44//! for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
45//! dual licensed as above, without any additional terms or conditions.
46
47#[cfg(feature = "serde")]
48use serde::{Deserialize, Serialize};
49use wasm_bindgen::{prelude::Closure, JsCast};
50use web_sys::HtmlIFrameElement;
51use yew::{html, Callback, Component, NodeRef, Properties};
52
53const IFRAME_STYLE: &str = "display: block; position: absolute; top: 0; left: 0; width: 100%; height: 100%; overflow: hidden; border: 0; opacity: 0; pointer-events: none; z-index: -1;";
54
55/// Yew component to observe changes to the size of the parent element.
56///
57/// See the crate documentation for an example and more information.
58#[derive(Debug)]
59pub struct ComponentSizeObserver {
60    props: Props,
61    iframe_ref: NodeRef,
62    on_resize: Option<Closure<dyn Fn()>>,
63}
64
65/// ComponentSizeObserver properties
66#[derive(Properties, Clone, PartialEq, Debug)]
67pub struct Props {
68    /// A callback that is fired when the component size changes for any reason.
69    pub onsize: Callback<ComponentSize>,
70}
71
72/// A struct containing the width and height of the component
73#[derive(Default, Clone, PartialEq, Debug)]
74#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
75pub struct ComponentSize {
76    /// Width of the component in pixels
77    pub width: f64,
78
79    /// Height of the component in pixels
80    pub height: f64,
81}
82
83impl Component for ComponentSizeObserver {
84    type Message = ();
85    type Properties = Props;
86
87    fn create(props: Self::Properties, _link: yew::ComponentLink<Self>) -> Self {
88        Self {
89            props,
90            iframe_ref: Default::default(),
91            on_resize: None,
92        }
93    }
94
95    fn update(&mut self, _msg: Self::Message) -> yew::ShouldRender {
96        false
97    }
98
99    fn change(&mut self, props: Self::Properties) -> yew::ShouldRender {
100        if self.props != props {
101            self.props = props;
102            self.add_resize_listener();
103            false
104        } else {
105            false
106        }
107    }
108
109    fn view(&self) -> yew::Html {
110        html! {
111            <iframe style=IFRAME_STYLE ref=self.iframe_ref.clone() />
112        }
113    }
114
115    fn rendered(&mut self, first_render: bool) {
116        if first_render {
117            self.add_resize_listener();
118        }
119    }
120}
121
122impl ComponentSizeObserver {
123    fn add_resize_listener(&mut self) {
124        let iframe = self.iframe_ref.cast::<HtmlIFrameElement>().unwrap();
125        let window = iframe.content_window().unwrap();
126
127        let iframe_ref = self.iframe_ref.clone();
128        let size_callback = self.props.onsize.clone();
129        let on_resize = Closure::wrap(Box::new(move || {
130            let iframe = iframe_ref.cast::<HtmlIFrameElement>().unwrap();
131            let bcr = iframe.get_bounding_client_rect();
132            size_callback.emit(ComponentSize {
133                width: bcr.width(),
134                height: bcr.height(),
135            });
136        }) as Box<dyn Fn()>);
137        window.set_onresize(Some(on_resize.as_ref().unchecked_ref()));
138        self.on_resize = Some(on_resize);
139    }
140}