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

yew/virtual_dom/
mod.rs

1//! This module contains Yew's implementation of a reactive virtual DOM.
2
3#[doc(hidden)]
4pub mod key;
5#[doc(hidden)]
6pub mod listeners;
7#[doc(hidden)]
8pub mod vcomp;
9#[doc(hidden)]
10pub mod vlist;
11#[doc(hidden)]
12pub mod vnode;
13#[doc(hidden)]
14pub mod vportal;
15#[doc(hidden)]
16pub mod vraw;
17#[doc(hidden)]
18pub mod vsuspense;
19#[doc(hidden)]
20pub mod vtag;
21#[doc(hidden)]
22pub mod vtext;
23
24use std::hint::unreachable_unchecked;
25use std::rc::Rc;
26
27use indexmap::IndexMap;
28use wasm_bindgen::JsValue;
29
30#[doc(inline)]
31pub use self::key::Key;
32#[doc(inline)]
33pub use self::listeners::*;
34#[doc(inline)]
35pub use self::vcomp::{VChild, VComp};
36#[doc(hidden)]
37pub use self::vlist::FullyKeyedState;
38#[doc(inline)]
39pub use self::vlist::VList;
40#[doc(inline)]
41pub use self::vnode::VNode;
42#[doc(inline)]
43pub use self::vportal::VPortal;
44#[doc(inline)]
45pub use self::vraw::VRaw;
46#[doc(inline)]
47pub use self::vsuspense::VSuspense;
48#[doc(inline)]
49pub use self::vtag::VTag;
50#[doc(inline)]
51pub use self::vtext::VText;
52
53/// Attribute value
54pub type AttrValue = implicit_clone::unsync::IString;
55
56#[cfg(any(feature = "ssr", feature = "hydration"))]
57mod feat_ssr_hydration {
58    #[cfg(debug_assertions)]
59    type ComponentName = &'static str;
60    #[cfg(not(debug_assertions))]
61    type ComponentName = std::marker::PhantomData<()>;
62
63    #[cfg(feature = "hydration")]
64    use std::borrow::Cow;
65
66    /// A collectable.
67    ///
68    /// This indicates a kind that can be collected from fragment to be processed at a later time
69    pub enum Collectable {
70        Component(ComponentName),
71        Raw,
72        Suspense,
73    }
74
75    impl Collectable {
76        #[cfg(not(debug_assertions))]
77        #[inline(always)]
78        pub fn for_component<T: 'static>() -> Self {
79            use std::marker::PhantomData;
80            // This suppresses the clippy lint about unused generic.
81            // We inline this function
82            // so the function body is copied to its caller and generics get optimised away.
83            let _comp_type: PhantomData<T> = PhantomData;
84            Self::Component(PhantomData)
85        }
86
87        #[cfg(debug_assertions)]
88        pub fn for_component<T: 'static>() -> Self {
89            let comp_name = std::any::type_name::<T>();
90            Self::Component(comp_name)
91        }
92
93        pub fn open_start_mark(&self) -> &'static str {
94            match self {
95                Self::Component(_) => "<[",
96                Self::Raw => "<#",
97                Self::Suspense => "<?",
98            }
99        }
100
101        pub fn close_start_mark(&self) -> &'static str {
102            match self {
103                Self::Component(_) => "</[",
104                Self::Raw => "</#",
105                Self::Suspense => "</?",
106            }
107        }
108
109        pub fn end_mark(&self) -> &'static str {
110            match self {
111                Self::Component(_) => "]>",
112                Self::Raw => ">",
113                Self::Suspense => ">",
114            }
115        }
116
117        #[cfg(feature = "hydration")]
118        pub fn name(&self) -> Cow<'static, str> {
119            match self {
120                #[cfg(debug_assertions)]
121                Self::Component(m) => format!("Component({m})").into(),
122                #[cfg(not(debug_assertions))]
123                Self::Component(_) => "Component".into(),
124                Self::Raw => "Raw".into(),
125                Self::Suspense => "Suspense".into(),
126            }
127        }
128    }
129}
130
131#[cfg(any(feature = "ssr", feature = "hydration"))]
132pub(crate) use feat_ssr_hydration::*;
133
134#[cfg(feature = "ssr")]
135mod feat_ssr {
136    use std::fmt::Write;
137
138    use super::*;
139    use crate::platform::fmt::BufWriter;
140
141    impl Collectable {
142        pub(crate) fn write_open_tag(&self, w: &mut BufWriter) {
143            let _ = w.write_str("<!--");
144            let _ = w.write_str(self.open_start_mark());
145
146            #[cfg(debug_assertions)]
147            match self {
148                Self::Component(type_name) => {
149                    let _ = w.write_str(type_name);
150                }
151                Self::Raw => {}
152                Self::Suspense => {}
153            }
154
155            let _ = w.write_str(self.end_mark());
156            let _ = w.write_str("-->");
157        }
158
159        pub(crate) fn write_close_tag(&self, w: &mut BufWriter) {
160            let _ = w.write_str("<!--");
161            let _ = w.write_str(self.close_start_mark());
162
163            #[cfg(debug_assertions)]
164            match self {
165                Self::Component(type_name) => {
166                    let _ = w.write_str(type_name);
167                }
168                Self::Raw => {}
169                Self::Suspense => {}
170            }
171
172            let _ = w.write_str(self.end_mark());
173            let _ = w.write_str("-->");
174        }
175    }
176}
177
178/// Defines if the [`Attributes`] is set as element's attribute or property and its value.
179#[expect(missing_docs)]
180#[derive(PartialEq, Clone, Debug)]
181pub enum AttributeOrProperty {
182    Attribute(AttrValue),
183    Property(JsValue),
184}
185
186fn is_valid_attr_name(attr: &str) -> bool {
187    // https://dom.spec.whatwg.org/#valid-attribute-local-name specifies:
188    // > at least one character, no ASCII whitespace, no \x00, \x2F (/), \x3D (=), \x3E (>)
189    // Browsers are more strict in setAttribute(), and the parser for a html document is too.
190    // The parser specifies that names must consist of
191    // > one or more [unicode] characters other than the space characters [ \t\r\n\f],
192    // > U+0000 NULL, U+0022 QUOTATION MARK ("), U+0027 APOSTROPHE ('), U+003E GREATER-THAN SIGN
193    // > (>),
194    // > U+002F SOLIDUS (/), and U+003D EQUALS SIGN (=) characters, the control characters , [...]
195    // ref https://w3c.github.io/html-reference/syntax.html#attribute-name
196    // A fun thing is trying to use non-ascii-whitespace WHITE_SPACE characters such as " "
197    // as part of an attribute name, which seem to get accepted by the parser but not by
198    // setAttribute.
199
200    // Anyway, the goal here is to allow a reasonable subset of names that will parse correctly
201    // in all browsers when delivered via SSR and also work correctly when used in calls to
202    // setAttribute. Here is what we will prohibit:
203    // - all whitespace characters (unicode WS) and control characters (unicode Cc)
204    // - all syntactically special characters for XHTML parsing [/"'>=]
205    // - disallow "high" unicode characters in the range [#x10000-#xEFFFF]
206    // see also: https://github.com/whatwg/dom/issues/849
207
208    // If you really need attribute names that are not covered by this open an issue and a self-help
209    // group. Know that I do care for your pain.
210    !attr.is_empty()
211        && attr.chars().all(|c| match c {
212            c if c.is_control() => false,
213            '/' | '"' | '\'' | '>' | '=' => false,
214            c if c.is_whitespace() => false,
215            // For example, try the following in a browser of your choice:
216            // ```
217            // let d = document.createElement("div")
218            // d.setAttribute("𐊖𐊗𐊒𐊓", "value2")
219            // ```
220            // At the time of writing, the above works in chrome (131) but not in firefox (136)
221            '\u{10000}'.. => false,
222            _ => true,
223        })
224}
225
226#[track_caller]
227fn validate_attr_name(attr: &str) {
228    assert!(
229        is_valid_attr_name(attr),
230        "{attr:?} is not a valid attribute name"
231    );
232}
233
234/// A collection of attributes for an element
235#[derive(PartialEq, Clone, Debug)]
236#[non_exhaustive]
237pub enum Attributes {
238    #[doc(hidden)]
239    #[deprecated = "Attribute names are not validated. Use one of the conversion functions"]
240    Static(&'static [(&'static str, AttributeOrProperty)]),
241
242    #[doc(hidden)]
243    #[deprecated = "Attribute names are not validated. Use one of the conversion functions"]
244    Dynamic {
245        /// Attribute keys. Includes both always set and optional attribute keys.
246        keys: &'static [&'static str],
247
248        /// Attribute values. Matches [keys](Attributes::Dynamic::keys). Optional attributes are
249        /// designated by setting [None].
250        values: Box<[Option<AttributeOrProperty>]>,
251    },
252
253    #[doc(hidden)]
254    #[deprecated = "Attribute names are not validated. Use one of the conversion functions"]
255    IndexMap(Rc<IndexMap<AttrValue, AttributeOrProperty>>),
256}
257
258impl Attributes {
259    /// Static list of attributes.
260    ///
261    /// Allows optimizing comparison to a simple pointer equality check and reducing allocations,
262    /// if the attributes do not change on a node.
263    #[track_caller]
264    pub fn from_static(statics: &'static [(&'static str, AttributeOrProperty)]) -> Self {
265        for &(key, _) in statics {
266            validate_attr_name(key); // Not in a closure for #[track_caller]
267        }
268        Self::from_static_unchecked(statics)
269    }
270
271    /// Same as [Self::from_static] but without verifying keys. This can lead to loss of
272    /// validity of an SSR document!
273    pub fn from_static_unchecked(statics: &'static [(&'static str, AttributeOrProperty)]) -> Self {
274        #[expect(deprecated)]
275        Self::Static(statics)
276    }
277
278    /// Static list of attribute keys with possibility to exclude attributes and dynamic attribute
279    /// values.
280    ///
281    /// Allows optimizing comparison to a simple pointer equality check and reducing allocations,
282    /// if the attributes keys do not change on a node.
283    #[track_caller]
284    pub fn from_dynamic_values(
285        keys: &'static [&'static str],
286        values: Box<[Option<AttributeOrProperty>]>,
287    ) -> Self {
288        for &key in keys {
289            validate_attr_name(key); // Not in a closure for #[track_caller]
290        }
291        Self::from_dynamic_values_unchecked(keys, values)
292    }
293
294    /// Same as [Self::from_dynamic_values] but without verifying keys. This can lead to loss of
295    /// validity of an SSR document!
296    pub fn from_dynamic_values_unchecked(
297        keys: &'static [&'static str],
298        values: Box<[Option<AttributeOrProperty>]>,
299    ) -> Self {
300        #[expect(deprecated)]
301        Self::Dynamic { keys, values }
302    }
303
304    /// IndexMap is used to provide runtime attribute deduplication in cases where the html! macro
305    /// was not used to guarantee it.
306    #[track_caller]
307    pub fn from_index_map(map: Rc<IndexMap<AttrValue, AttributeOrProperty>>) -> Self {
308        for (key, _) in map.iter() {
309            validate_attr_name(key); // Not in a closure for #[track_caller]
310        }
311        Self::from_index_map_unchecked(map)
312    }
313
314    /// Same as [Self::from_index_map] but without verifying keys. This can lead to loss of validity
315    /// of an SSR document!
316    pub fn from_index_map_unchecked(map: Rc<IndexMap<AttrValue, AttributeOrProperty>>) -> Self {
317        #[expect(deprecated)]
318        Self::IndexMap(map)
319    }
320
321    /// Validate a single attribute name for validity to be used as a key for an attribute or
322    /// property. All keys must be valid according to this method when constructing
323    /// [`Attributes`]. Usually, this is ensured by the usage context.
324    ///
325    /// Specifically, this checks that the passed string is a valid XHTML attribute name. This
326    /// implies that it consists of at least one character, does not contain whitespace or other
327    /// special characters.
328    pub fn is_valid_attr_key(name: &str) -> bool {
329        is_valid_attr_name(name)
330    }
331
332    /// Construct a default Attributes instance
333    pub fn new() -> Self {
334        Self::default()
335    }
336
337    /// Return iterator over attribute key-value pairs.
338    /// This function is suboptimal and does not inline well. Avoid on hot paths.
339    ///
340    /// This function only returns attributes
341    pub fn iter<'a>(&'a self) -> Box<dyn Iterator<Item = (&'a str, &'a str)> + 'a> {
342        #[expect(deprecated)]
343        match self {
344            Self::Static(arr) => Box::new(arr.iter().filter_map(|(k, v)| match v {
345                AttributeOrProperty::Attribute(v) => Some((*k, v.as_ref())),
346                AttributeOrProperty::Property(_) => None,
347            })),
348            Self::Dynamic { keys, values } => {
349                Box::new(keys.iter().zip(values.iter()).filter_map(|(k, v)| match v {
350                    Some(AttributeOrProperty::Attribute(v)) => Some((*k, v.as_ref())),
351                    _ => None,
352                }))
353            }
354            Self::IndexMap(m) => Box::new(m.iter().filter_map(|(k, v)| match v {
355                AttributeOrProperty::Attribute(v) => Some((k.as_ref(), v.as_ref())),
356                _ => None,
357            })),
358        }
359    }
360
361    /// Get a mutable reference to the underlying `IndexMap`. Deprecated for
362    /// [`Self::get_mut_index_map_unchecked`].
363    #[doc(hidden)]
364    #[deprecated = "Attribute names are not validated. Use `get_mut_index_map_unchecked` to signal \
365                    this properly and validate your modifications."]
366    pub fn get_mut_index_map(&mut self) -> &mut IndexMap<AttrValue, AttributeOrProperty> {
367        self.get_mut_index_map_unchecked()
368    }
369
370    /// Get a mutable reference to the underlying `IndexMap`.
371    /// If the attributes are stored in the `Vec` variant, it will be converted.
372    /// The caller is responsible to check that inserted attribute names are valid, see
373    /// [`Self::is_valid_attr_key`]
374    pub fn get_mut_index_map_unchecked(&mut self) -> &mut IndexMap<AttrValue, AttributeOrProperty> {
375        macro_rules! unpack {
376            () => {
377                #[expect(deprecated)]
378                match self {
379                    Self::IndexMap(m) => Rc::make_mut(m),
380                    // SAFETY: unreachable because we set self to the `IndexMap` variant above.
381                    _ => unsafe { unreachable_unchecked() },
382                }
383            };
384        }
385
386        #[expect(deprecated)]
387        match self {
388            Self::IndexMap(m) => Rc::make_mut(m),
389            Self::Static(arr) => {
390                *self = Self::IndexMap(Rc::new(
391                    arr.iter().map(|(k, v)| ((*k).into(), v.clone())).collect(),
392                ));
393                unpack!()
394            }
395            Self::Dynamic { keys, values } => {
396                *self = Self::from_index_map_unchecked(Rc::new(
397                    std::mem::take(values)
398                        .iter_mut()
399                        .zip(keys.iter())
400                        .filter_map(|(v, k)| v.take().map(|v| (AttrValue::from(*k), v)))
401                        .collect(),
402                ));
403                unpack!()
404            }
405        }
406    }
407}
408
409impl From<IndexMap<AttrValue, AttrValue>> for Attributes {
410    fn from(map: IndexMap<AttrValue, AttrValue>) -> Self {
411        let v = map
412            .into_iter()
413            .map(|(k, v)| (k, AttributeOrProperty::Attribute(v)))
414            .collect();
415        Self::from_index_map(Rc::new(v))
416    }
417}
418
419impl From<IndexMap<&'static str, AttrValue>> for Attributes {
420    fn from(v: IndexMap<&'static str, AttrValue>) -> Self {
421        let v = v
422            .into_iter()
423            .map(|(k, v)| (AttrValue::Static(k), (AttributeOrProperty::Attribute(v))))
424            .collect();
425        Self::from_index_map(Rc::new(v))
426    }
427}
428
429impl From<IndexMap<&'static str, JsValue>> for Attributes {
430    fn from(v: IndexMap<&'static str, JsValue>) -> Self {
431        let v = v
432            .into_iter()
433            .map(|(k, v)| (AttrValue::Static(k), (AttributeOrProperty::Property(v))))
434            .collect();
435        Self::from_index_map(Rc::new(v))
436    }
437}
438
439impl Default for Attributes {
440    fn default() -> Self {
441        Self::from_static(&[])
442    }
443}