yew_virtual_scroller/
lib.rs1#![deny(missing_docs)]
2
3use std::{
62 cmp::{max, min},
63 fmt::Debug,
64 ops::Range,
65 rc::Rc,
66};
67use web_sys::Element;
68use yew::{html, Classes, Component, NodeRef, Properties};
69use yew_component_size::{ComponentSize, ComponentSizeObserver};
70
71const WINDOW_STYLES: &str = "will-change:transform;";
72
73pub struct VirtualScroller<T>
77where
78 T: Into<yew::Html> + Clone + PartialEq + Debug + 'static,
79{
80 pub props: Props<T>,
82
83 link: yew::ComponentLink<Self>,
84 viewport_ref: NodeRef,
85 viewport_height: f64,
86 content_window: Option<ContentWindow>,
87}
88
89#[derive(Properties, Clone, PartialEq, Debug)]
91pub struct Props<T>
92where
93 T: Into<yew::Html> + Clone + PartialEq + Debug + 'static,
94{
95 pub items: Rc<Vec<T>>,
98
99 pub row_height: f64,
101
102 #[prop_or_default]
104 pub class: Classes,
105}
106
107#[doc(hidden)]
108pub enum Msg {
109 CalculateViewport,
110 UpdateViewportHeight(f64),
111 CalculateWindowContent,
112}
113
114struct ContentWindow {
115 start_y: f64,
116 visible_range: Range<usize>,
117}
118
119impl<T> Component for VirtualScroller<T>
120where
121 T: Into<yew::Html> + Clone + PartialEq + Debug + 'static,
122{
123 type Message = Msg;
124
125 type Properties = Props<T>;
126
127 fn create(props: Self::Properties, link: yew::ComponentLink<Self>) -> Self {
128 Self {
129 props,
130 link,
131 viewport_ref: Default::default(),
132 viewport_height: 0f64,
133 content_window: None,
134 }
135 }
136
137 fn update(&mut self, msg: Self::Message) -> yew::ShouldRender {
138 match msg {
139 Msg::CalculateViewport => {
140 let viewport = self.viewport_ref.cast::<Element>().unwrap();
141 self.viewport_height = viewport.client_height() as f64;
142 true
143 }
144 Msg::UpdateViewportHeight(height) => {
145 self.viewport_height = height;
146 true
147 }
148 Msg::CalculateWindowContent => {
149 let node_padding: usize = 0;
150 let viewport = self.viewport_ref.cast::<Element>().unwrap();
151 let scroll_top = viewport.scroll_top() as f64;
152 let start_node = max(
153 0,
154 ((scroll_top / self.props.row_height).floor() as isize)
155 - (node_padding as isize),
156 ) as usize;
157 let total_visible = min(
158 ((self.viewport_height / self.props.row_height).ceil()) as usize
159 + 2 * node_padding,
160 self.props.items.len() - start_node,
161 );
162 let start_y = (start_node as f64) * self.props.row_height;
163 let end_node = start_node + total_visible;
164 self.content_window = Some(ContentWindow {
165 start_y,
166 visible_range: start_node..end_node,
167 });
168 true
169 }
170 }
171 }
172
173 fn change(&mut self, props: Self::Properties) -> yew::ShouldRender {
174 if self.props != props {
175 let should_rerender = self.props.class != props.class;
176 self.props = props;
177 self.link.send_message(Msg::CalculateWindowContent);
178 should_rerender
179 } else {
180 false
181 }
182 }
183
184 fn view(&self) -> yew::Html {
185 let total_content_height = (self.props.items.len() as f64) * self.props.row_height;
186 let content_style = format!("height: {}px", total_content_height);
187
188 let (window_style, windowed_items) = match &self.content_window {
189 Some(cw) => (
190 format!("{}transform: translateY({}px);", WINDOW_STYLES, cw.start_y),
191 (&self.props.items[cw.visible_range.clone()]).into(),
192 ),
193 None => (WINDOW_STYLES.to_string(), vec![]),
194 };
195 let items = windowed_items.into_iter().map(|item| item.into());
196
197 let onscroll = self.link.callback(|_| Msg::CalculateWindowContent);
198 let onsize = self.link.batch_callback(|rect: ComponentSize| {
199 vec![
200 Msg::UpdateViewportHeight(rect.height),
201 Msg::CalculateWindowContent,
202 ]
203 });
204
205 html! {
206 <div ref=self.viewport_ref.clone() onscroll=onscroll class=self.props.class.clone() style="position: relative; overflow: auto">
207 <div style=content_style>
208 <div style=window_style>
209 {for items}
210 </div>
211 </div>
212 <ComponentSizeObserver onsize=onsize />
213 </div>
214 }
215 }
216
217 fn rendered(&mut self, first_render: bool) {
218 if first_render {
219 self.link
220 .send_message_batch(vec![Msg::CalculateViewport, Msg::CalculateWindowContent]);
221 }
222 }
223}