This is unreleased documentation for Yew Next version.
For up-to-date documentation, see the latest version on docs.rs.

yew/dom_bundle/btag/
attributes.rs

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                // Refresh value from the DOM. It might have changed.
29                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
59/// Able to have its value read or set
60pub(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        // IMPORTANT! This parameter has to be set every time it's explicitly given
71        // to prevent strange behaviour in the browser when the DOM changes
72        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        // IMPORTANT! This parameter has to be set every time it's explicitly given
82        // to prevent strange behaviour in the browser when the DOM changes
83        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    /// Convert [Attributes] pair to [HashMap]s and patch changes to `el`.
133    /// Works with any [Attributes] variants.
134    #[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        // Update existing or set new
154        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        // Remove missing
164        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; // reborrow it immutably from here
235        match (bundle, ancestor) {
236            // Hot path
237            (Self::Static(new), Self::Static(old)) if ptr_eq(new, old) => (),
238            // Hot path
239            (
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                // Double zipping does not optimize well, so use asserts and unsafe instead
250                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            // For VTag's constructed outside the html! macro
279            (Self::IndexMap(new), Self::IndexMap(ref old)) => {
280                Self::apply_diff_index_maps(el, new, old);
281            }
282            // Cold path. Happens only with conditional swapping and reordering of `VTag`s with the
283            // same tag and no keys.
284            (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}