1use std::collections::HashMap;
2use std::ops::Deref;
3
4use indexmap::IndexMap;
5use wasm_bindgen::{intern, JsValue};
6use web_sys::{Element, HtmlInputElement as InputElement, HtmlTextAreaElement as TextAreaElement};
7use yew::AttrValue;
8
9use super::Apply;
10use crate::dom_bundle::BSubtree;
11use crate::virtual_dom::vtag::{InputFields, TextareaFields, Value};
12use crate::virtual_dom::{AttributeOrProperty, Attributes};
13
14impl<T: AccessValue> Apply for Value<T> {
15 type Bundle = Self;
16 type Element = T;
17
18 fn apply(self, _root: &BSubtree, el: &Self::Element) -> Self {
19 if let Some(v) = self.deref() {
20 el.set_value(v);
21 }
22 self
23 }
24
25 fn apply_diff(self, _root: &BSubtree, el: &Self::Element, bundle: &mut Self) {
26 match (self.deref(), (*bundle).deref()) {
27 (Some(new), Some(_)) => {
28 if new.as_ref() != el.value() {
30 el.set_value(new);
31 }
32 }
33 (Some(new), None) => el.set_value(new),
34 (None, Some(_)) => el.set_value(""),
35 (None, None) => (),
36 }
37 }
38}
39
40macro_rules! impl_access_value {
41 ($( $type:ty )*) => {
42 $(
43 impl AccessValue for $type {
44 #[inline]
45 fn value(&self) -> String {
46 <$type>::value(&self)
47 }
48
49 #[inline]
50 fn set_value(&self, v: &str) {
51 <$type>::set_value(&self, v)
52 }
53 }
54 )*
55 };
56}
57impl_access_value! {InputElement TextAreaElement}
58
59pub(super) trait AccessValue {
61 fn value(&self) -> String;
62 fn set_value(&self, v: &str);
63}
64
65impl Apply for InputFields {
66 type Bundle = Self;
67 type Element = InputElement;
68
69 fn apply(mut self, root: &BSubtree, el: &Self::Element) -> Self {
70 if let Some(checked) = self.checked {
73 el.set_checked(checked);
74 }
75
76 self.value = self.value.apply(root, el);
77 self
78 }
79
80 fn apply_diff(self, root: &BSubtree, el: &Self::Element, bundle: &mut Self) {
81 if let Some(checked) = self.checked {
84 el.set_checked(checked);
85 }
86
87 self.value.apply_diff(root, el, &mut bundle.value);
88 }
89}
90
91impl Apply for TextareaFields {
92 type Bundle = Value<TextAreaElement>;
93 type Element = TextAreaElement;
94
95 fn apply(self, root: &BSubtree, el: &Self::Element) -> Self::Bundle {
96 if let Some(def) = self.defaultvalue {
97 _ = el.set_default_value(def.as_str());
98 }
99 self.value.apply(root, el)
100 }
101
102 fn apply_diff(self, root: &BSubtree, el: &Self::Element, bundle: &mut Self::Bundle) {
103 self.value.apply_diff(root, el, bundle)
104 }
105}
106
107impl Attributes {
108 #[cold]
109 fn apply_diff_index_maps(
110 el: &Element,
111 new: &IndexMap<AttrValue, AttributeOrProperty>,
112 old: &IndexMap<AttrValue, AttributeOrProperty>,
113 ) {
114 for (key, value) in new.iter() {
115 match old.get(key) {
116 Some(old_value) => {
117 if value != old_value {
118 Self::set(el, key, value);
119 }
120 }
121 None => Self::set(el, key, value),
122 }
123 }
124
125 for (key, value) in old.iter() {
126 if !new.contains_key(key) {
127 Self::remove(el, key, value);
128 }
129 }
130 }
131
132 #[cold]
135 fn apply_diff_as_maps<'a>(el: &Element, new: &'a Self, old: &'a Self) {
136 fn collect(src: &Attributes) -> HashMap<&str, &AttributeOrProperty> {
137 use Attributes::*;
138
139 match src {
140 Static(arr) => (*arr).iter().map(|(k, v)| (*k, v)).collect(),
141 Dynamic { keys, values } => keys
142 .iter()
143 .zip(values.iter())
144 .filter_map(|(k, v)| v.as_ref().map(|v| (*k, v)))
145 .collect(),
146 IndexMap(m) => m.iter().map(|(k, v)| (k.as_ref(), v)).collect(),
147 }
148 }
149
150 let new = collect(new);
151 let old = collect(old);
152
153 for (k, new) in new.iter() {
155 if match old.get(k) {
156 Some(old) => old != new,
157 None => true,
158 } {
159 Self::set(el, k, new);
160 }
161 }
162
163 for (k, old_value) in old.iter() {
165 if !new.contains_key(k) {
166 Self::remove(el, k, old_value);
167 }
168 }
169 }
170
171 fn set(el: &Element, key: &str, value: &AttributeOrProperty) {
172 match value {
173 AttributeOrProperty::Attribute(value) => el
174 .set_attribute(intern(key), value)
175 .expect("invalid attribute key"),
176 AttributeOrProperty::Static(value) => el
177 .set_attribute(intern(key), value)
178 .expect("invalid attribute key"),
179 AttributeOrProperty::Property(value) => {
180 let key = JsValue::from_str(key);
181 js_sys::Reflect::set(el.as_ref(), &key, value).expect("could not set property");
182 }
183 }
184 }
185
186 fn remove(el: &Element, key: &str, old_value: &AttributeOrProperty) {
187 match old_value {
188 AttributeOrProperty::Attribute(_) | AttributeOrProperty::Static(_) => el
189 .remove_attribute(intern(key))
190 .expect("could not remove attribute"),
191 AttributeOrProperty::Property(_) => {
192 let key = JsValue::from_str(key);
193 js_sys::Reflect::set(el.as_ref(), &key, &JsValue::UNDEFINED)
194 .expect("could not remove property");
195 }
196 }
197 }
198}
199
200impl Apply for Attributes {
201 type Bundle = Self;
202 type Element = Element;
203
204 fn apply(self, _root: &BSubtree, el: &Element) -> Self {
205 match &self {
206 Self::Static(arr) => {
207 for (k, v) in arr.iter() {
208 Self::set(el, k, v);
209 }
210 }
211 Self::Dynamic { keys, values } => {
212 for (k, v) in keys.iter().zip(values.iter()) {
213 if let Some(v) = v {
214 Self::set(el, k, v)
215 }
216 }
217 }
218 Self::IndexMap(m) => {
219 for (k, v) in m.iter() {
220 Self::set(el, k, v)
221 }
222 }
223 }
224 self
225 }
226
227 fn apply_diff(self, _root: &BSubtree, el: &Element, bundle: &mut Self) {
228 #[inline]
229 fn ptr_eq<T>(a: &[T], b: &[T]) -> bool {
230 std::ptr::eq(a, b)
231 }
232
233 let ancestor = std::mem::replace(bundle, self);
234 let bundle = &*bundle; match (bundle, ancestor) {
236 (Self::Static(new), Self::Static(old)) if ptr_eq(new, old) => (),
238 (
240 Self::Dynamic {
241 keys: new_k,
242 values: new_v,
243 },
244 Self::Dynamic {
245 keys: old_k,
246 values: old_v,
247 },
248 ) if ptr_eq(new_k, old_k) => {
249 assert_eq!(new_k.len(), new_v.len());
251 assert_eq!(new_k.len(), old_v.len());
252 for i in 0..new_k.len() {
253 macro_rules! key {
254 () => {
255 unsafe { new_k.get_unchecked(i) }
256 };
257 }
258 macro_rules! set {
259 ($new:expr) => {
260 Self::set(el, key!(), $new)
261 };
262 }
263
264 match unsafe { (new_v.get_unchecked(i), old_v.get_unchecked(i)) } {
265 (Some(new), Some(old)) => {
266 if new != old {
267 set!(new);
268 }
269 }
270 (Some(new), None) => set!(new),
271 (None, Some(old)) => {
272 Self::remove(el, key!(), old);
273 }
274 (None, None) => (),
275 }
276 }
277 }
278 (Self::IndexMap(new), Self::IndexMap(ref old)) => {
280 Self::apply_diff_index_maps(el, new, old);
281 }
282 (new, ref ancestor) => {
285 Self::apply_diff_as_maps(el, new, ancestor);
286 }
287 }
288 }
289}
290
291#[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
292#[cfg(test)]
293mod tests {
294 use std::rc::Rc;
295 use std::time::Duration;
296
297 use gloo::utils::document;
298 use js_sys::Reflect;
299 use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
300
301 use super::*;
302 use crate::{function_component, html, Html};
303
304 wasm_bindgen_test_configure!(run_in_browser);
305
306 fn create_element() -> (Element, BSubtree) {
307 let element = document()
308 .create_element("a")
309 .expect("failed to create element");
310 let btree = BSubtree::create_root(&element);
311 (element, btree)
312 }
313
314 #[test]
315 fn properties_are_set() {
316 let attrs = indexmap::indexmap! {
317 AttrValue::Static("href") => AttributeOrProperty::Property(JsValue::from_str("https://example.com/")),
318 AttrValue::Static("alt") => AttributeOrProperty::Property(JsValue::from_str("somewhere")),
319 };
320 let attrs = Attributes::IndexMap(Rc::new(attrs));
321 let (element, btree) = create_element();
322 attrs.apply(&btree, &element);
323 assert_eq!(
324 Reflect::get(element.as_ref(), &JsValue::from_str("href"))
325 .expect("no href")
326 .as_string()
327 .expect("not a string"),
328 "https://example.com/",
329 "property `href` not set properly"
330 );
331 assert_eq!(
332 Reflect::get(element.as_ref(), &JsValue::from_str("alt"))
333 .expect("no alt")
334 .as_string()
335 .expect("not a string"),
336 "somewhere",
337 "property `alt` not set properly"
338 );
339 }
340
341 #[test]
342 fn respects_apply_as() {
343 let attrs = indexmap::indexmap! {
344 AttrValue::Static("href") => AttributeOrProperty::Attribute(AttrValue::from("https://example.com/")),
345 AttrValue::Static("alt") => AttributeOrProperty::Property(JsValue::from_str("somewhere")),
346 };
347 let attrs = Attributes::IndexMap(Rc::new(attrs));
348 let (element, btree) = create_element();
349 attrs.apply(&btree, &element);
350 assert_eq!(
351 element.outer_html(),
352 "<a href=\"https://example.com/\"></a>",
353 "should be set as attribute"
354 );
355 assert_eq!(
356 Reflect::get(element.as_ref(), &JsValue::from_str("alt"))
357 .expect("no alt")
358 .as_string()
359 .expect("not a string"),
360 "somewhere",
361 "property `alt` not set properly"
362 );
363 }
364
365 #[test]
366 fn class_is_always_attrs() {
367 let attrs = Attributes::Static(&[("class", AttributeOrProperty::Static("thing"))]);
368
369 let (element, btree) = create_element();
370 attrs.apply(&btree, &element);
371 assert_eq!(element.get_attribute("class").unwrap(), "thing");
372 }
373
374 #[test]
375 async fn macro_syntax_works() {
376 #[function_component]
377 fn Comp() -> Html {
378 html! { <a href="https://example.com/" ~alt={"abc"} ~data-bool={JsValue::from_bool(true)} /> }
379 }
380
381 let output = document().get_element_by_id("output").unwrap();
382 yew::Renderer::<Comp>::with_root(output.clone()).render();
383
384 gloo::timers::future::sleep(Duration::from_secs(1)).await;
385 let element = output.query_selector("a").unwrap().unwrap();
386 assert_eq!(
387 element.get_attribute("href").unwrap(),
388 "https://example.com/"
389 );
390
391 assert_eq!(
392 Reflect::get(element.as_ref(), &JsValue::from_str("alt"))
393 .expect("no alt")
394 .as_string()
395 .expect("not a string"),
396 "abc",
397 "property `alt` not set properly"
398 );
399
400 assert!(
401 Reflect::get(element.as_ref(), &JsValue::from_str("data-bool"))
402 .expect("no alt")
403 .as_bool()
404 .expect("not a bool"),
405 "property `alt` not set properly"
406 );
407 }
408}