tachys/view/
any_view.rs

1#[cfg(feature = "ssr")]
2use super::MarkBranch;
3use super::{
4    add_attr::AddAnyAttr, Mountable, Position, PositionState, Render,
5    RenderHtml,
6};
7use crate::{
8    html::attribute::Attribute, hydration::Cursor, ssr::StreamBuilder,
9};
10use std::{
11    any::{Any, TypeId},
12    fmt::Debug,
13};
14#[cfg(feature = "ssr")]
15use std::{future::Future, pin::Pin};
16
17/// A type-erased view. This can be used if control flow requires that multiple different types of
18/// view must be received, and it is either impossible or too cumbersome to use the `EitherOf___`
19/// enums.
20///
21/// It can also be used to create recursive components, which otherwise cannot return themselves
22/// due to the static typing of the view tree.
23///
24/// Generally speaking, using `AnyView` restricts the amount of information available to the
25/// compiler and should be limited to situations in which it is necessary to preserve the maximum
26/// amount of type information possible.
27pub struct AnyView {
28    type_id: TypeId,
29    value: Box<dyn Any + Send>,
30    build: fn(Box<dyn Any>) -> AnyViewState,
31    rebuild: fn(TypeId, Box<dyn Any>, &mut AnyViewState),
32    // Without erasure, tuples of attrs created by default cause too much type explosion to enable.
33    #[cfg(erase_components)]
34    add_any_attr: fn(
35        Box<dyn Any>,
36        crate::html::attribute::any_attribute::AnyAttribute,
37    ) -> AnyView,
38    // The fields below are cfg-gated so they will not be included in WASM bundles if not needed.
39    // Ordinarily, the compiler can simply omit this dead code because the methods are not called.
40    // With this type-erased wrapper, however, the compiler is not *always* able to correctly
41    // eliminate that code.
42    #[cfg(feature = "ssr")]
43    html_len: usize,
44    #[cfg(feature = "ssr")]
45    to_html: fn(Box<dyn Any>, &mut String, &mut Position, bool, bool),
46    #[cfg(feature = "ssr")]
47    to_html_async:
48        fn(Box<dyn Any>, &mut StreamBuilder, &mut Position, bool, bool),
49    #[cfg(feature = "ssr")]
50    to_html_async_ooo:
51        fn(Box<dyn Any>, &mut StreamBuilder, &mut Position, bool, bool),
52    #[cfg(feature = "ssr")]
53    #[allow(clippy::type_complexity)]
54    resolve: fn(Box<dyn Any>) -> Pin<Box<dyn Future<Output = AnyView> + Send>>,
55    #[cfg(feature = "ssr")]
56    dry_resolve: fn(&mut Box<dyn Any + Send>),
57    #[cfg(feature = "hydrate")]
58    #[cfg(feature = "hydrate")]
59    #[allow(clippy::type_complexity)]
60    hydrate_from_server:
61        fn(Box<dyn Any>, &Cursor, &PositionState) -> AnyViewState,
62}
63
64/// Retained view state for [`AnyView`].
65pub struct AnyViewState {
66    type_id: TypeId,
67    state: Box<dyn Any>,
68    unmount: fn(&mut dyn Any),
69    mount: fn(
70        &mut dyn Any,
71        parent: &crate::renderer::types::Element,
72        marker: Option<&crate::renderer::types::Node>,
73    ),
74    insert_before_this: fn(&dyn Any, child: &mut dyn Mountable) -> bool,
75}
76
77impl Debug for AnyViewState {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        f.debug_struct("AnyViewState")
80            .field("type_id", &self.type_id)
81            .field("state", &self.state)
82            .field("unmount", &self.unmount)
83            .field("mount", &self.mount)
84            .field("insert_before_this", &self.insert_before_this)
85            .finish()
86    }
87}
88
89/// Allows converting some view into [`AnyView`].
90pub trait IntoAny {
91    /// Converts the view into a type-erased [`AnyView`].
92    fn into_any(self) -> AnyView;
93}
94
95fn mount_any<T>(
96    state: &mut dyn Any,
97    parent: &crate::renderer::types::Element,
98    marker: Option<&crate::renderer::types::Node>,
99) where
100    T: Render,
101    T::State: 'static,
102{
103    let state = state
104        .downcast_mut::<T::State>()
105        .expect("AnyViewState::as_mountable couldn't downcast state");
106    state.mount(parent, marker)
107}
108
109fn unmount_any<T>(state: &mut dyn Any)
110where
111    T: Render,
112    T::State: 'static,
113{
114    let state = state
115        .downcast_mut::<T::State>()
116        .expect("AnyViewState::unmount couldn't downcast state");
117    state.unmount();
118}
119
120fn insert_before_this<T>(state: &dyn Any, child: &mut dyn Mountable) -> bool
121where
122    T: Render,
123    T::State: 'static,
124{
125    let state = state
126        .downcast_ref::<T::State>()
127        .expect("AnyViewState::insert_before_this couldn't downcast state");
128    state.insert_before_this(child)
129}
130
131impl<T> IntoAny for T
132where
133    T: Send,
134    T: RenderHtml + 'static,
135    T::State: 'static,
136{
137    fn into_any(self) -> AnyView {
138        #[cfg(feature = "ssr")]
139        let html_len = self.html_len();
140
141        let value = Box::new(self) as Box<dyn Any + Send>;
142
143        match value.downcast::<AnyView>() {
144            // if it's already an AnyView, we don't need to double-wrap it
145            Ok(any_view) => *any_view,
146            Err(value) => {
147                #[cfg(feature = "ssr")]
148                let dry_resolve = |value: &mut Box<dyn Any + Send>| {
149                    let value = value
150                        .downcast_mut::<T>()
151                        .expect("AnyView::resolve could not be downcast");
152                    value.dry_resolve();
153                };
154
155                #[cfg(feature = "ssr")]
156                let resolve = |value: Box<dyn Any>| {
157                    let value = value
158                        .downcast::<T>()
159                        .expect("AnyView::resolve could not be downcast");
160                    Box::pin(async move { value.resolve().await.into_any() })
161                        as Pin<Box<dyn Future<Output = AnyView> + Send>>
162                };
163                #[cfg(feature = "ssr")]
164                let to_html =
165                    |value: Box<dyn Any>,
166                     buf: &mut String,
167                     position: &mut Position,
168                     escape: bool,
169                     mark_branches: bool| {
170                        let type_id = mark_branches
171                            .then(|| format!("{:?}", TypeId::of::<T>()))
172                            .unwrap_or_default();
173                        let value = value
174                            .downcast::<T>()
175                            .expect("AnyView::to_html could not be downcast");
176                        if mark_branches {
177                            buf.open_branch(&type_id);
178                        }
179                        value.to_html_with_buf(
180                            buf,
181                            position,
182                            escape,
183                            mark_branches,
184                        );
185                        if mark_branches {
186                            buf.close_branch(&type_id);
187                        }
188                    };
189                #[cfg(feature = "ssr")]
190                let to_html_async =
191                    |value: Box<dyn Any>,
192                     buf: &mut StreamBuilder,
193                     position: &mut Position,
194                     escape: bool,
195                     mark_branches: bool| {
196                        let type_id = mark_branches
197                            .then(|| format!("{:?}", TypeId::of::<T>()))
198                            .unwrap_or_default();
199                        let value = value
200                            .downcast::<T>()
201                            .expect("AnyView::to_html could not be downcast");
202                        if mark_branches {
203                            buf.open_branch(&type_id);
204                        }
205                        value.to_html_async_with_buf::<false>(
206                            buf,
207                            position,
208                            escape,
209                            mark_branches,
210                        );
211                        if mark_branches {
212                            buf.close_branch(&type_id);
213                        }
214                    };
215                #[cfg(feature = "ssr")]
216                let to_html_async_ooo =
217                    |value: Box<dyn Any>,
218                     buf: &mut StreamBuilder,
219                     position: &mut Position,
220                     escape: bool,
221                     mark_branches: bool| {
222                        let value = value
223                            .downcast::<T>()
224                            .expect("AnyView::to_html could not be downcast");
225                        value.to_html_async_with_buf::<true>(
226                            buf,
227                            position,
228                            escape,
229                            mark_branches,
230                        );
231                    };
232                let build = |value: Box<dyn Any>| {
233                    let value = value
234                        .downcast::<T>()
235                        .expect("AnyView::build couldn't downcast");
236                    let state = Box::new(value.build());
237
238                    AnyViewState {
239                        type_id: TypeId::of::<T>(),
240                        state,
241
242                        mount: mount_any::<T>,
243                        unmount: unmount_any::<T>,
244                        insert_before_this: insert_before_this::<T>,
245                    }
246                };
247                #[cfg(feature = "hydrate")]
248                let hydrate_from_server =
249                    |value: Box<dyn Any>,
250                     cursor: &Cursor,
251                     position: &PositionState| {
252                        let value = value.downcast::<T>().expect(
253                            "AnyView::hydrate_from_server couldn't downcast",
254                        );
255                        let state =
256                            Box::new(value.hydrate::<true>(cursor, position));
257
258                        AnyViewState {
259                            type_id: TypeId::of::<T>(),
260                            state,
261
262                            mount: mount_any::<T>,
263                            unmount: unmount_any::<T>,
264                            insert_before_this: insert_before_this::<T>,
265                        }
266                    };
267
268                let rebuild =
269                    |new_type_id: TypeId,
270                     value: Box<dyn Any>,
271                     state: &mut AnyViewState| {
272                        let value = value
273                            .downcast::<T>()
274                            .expect("AnyView::rebuild couldn't downcast value");
275                        if new_type_id == state.type_id {
276                            let state = state.state.downcast_mut().expect(
277                                "AnyView::rebuild couldn't downcast state",
278                            );
279                            value.rebuild(state);
280                        } else {
281                            let mut new = value.into_any().build();
282                            state.insert_before_this(&mut new);
283                            state.unmount();
284                            *state = new;
285                        }
286                    };
287
288                // Without erasure, tuples of attrs created by default cause too much type explosion to enable.
289                #[cfg(erase_components)]
290                let add_any_attr = |value: Box<dyn Any>, attr: crate::html::attribute::any_attribute::AnyAttribute| {
291                    let value = value
292                        .downcast::<T>()
293                        .expect("AnyView::add_any_attr could not be downcast");
294                    value.add_any_attr(attr).into_any()
295                };
296
297                AnyView {
298                    type_id: TypeId::of::<T>(),
299                    value,
300                    build,
301                    rebuild,
302                    // Without erasure, tuples of attrs created by default cause too much type explosion to enable.
303                    #[cfg(erase_components)]
304                    add_any_attr,
305                    #[cfg(feature = "ssr")]
306                    resolve,
307                    #[cfg(feature = "ssr")]
308                    dry_resolve,
309                    #[cfg(feature = "ssr")]
310                    html_len,
311                    #[cfg(feature = "ssr")]
312                    to_html,
313                    #[cfg(feature = "ssr")]
314                    to_html_async,
315                    #[cfg(feature = "ssr")]
316                    to_html_async_ooo,
317                    #[cfg(feature = "hydrate")]
318                    hydrate_from_server,
319                }
320            }
321        }
322    }
323}
324
325impl Render for AnyView {
326    type State = AnyViewState;
327
328    fn build(self) -> Self::State {
329        (self.build)(self.value)
330    }
331
332    fn rebuild(self, state: &mut Self::State) {
333        (self.rebuild)(self.type_id, self.value, state)
334    }
335}
336
337impl AddAnyAttr for AnyView {
338    type Output<SomeNewAttr: Attribute> = Self;
339
340    #[allow(unused_variables)]
341    fn add_any_attr<NewAttr: Attribute>(
342        self,
343        attr: NewAttr,
344    ) -> Self::Output<NewAttr>
345    where
346        Self::Output<NewAttr>: RenderHtml,
347    {
348        // Without erasure, tuples of attrs created by default cause too much type explosion to enable.
349        #[cfg(erase_components)]
350        {
351            use crate::html::attribute::any_attribute::IntoAnyAttribute;
352
353            let attr = attr.into_cloneable_owned();
354            (self.add_any_attr)(self.value, attr.into_any_attr())
355        }
356        #[cfg(not(erase_components))]
357        {
358            self
359        }
360    }
361}
362
363impl RenderHtml for AnyView {
364    type AsyncOutput = Self;
365
366    fn dry_resolve(&mut self) {
367        #[cfg(feature = "ssr")]
368        {
369            (self.dry_resolve)(&mut self.value)
370        }
371        #[cfg(not(feature = "ssr"))]
372        panic!(
373            "You are rendering AnyView to HTML without the `ssr` feature \
374             enabled."
375        );
376    }
377
378    async fn resolve(self) -> Self::AsyncOutput {
379        #[cfg(feature = "ssr")]
380        {
381            (self.resolve)(self.value).await
382        }
383        #[cfg(not(feature = "ssr"))]
384        panic!(
385            "You are rendering AnyView to HTML without the `ssr` feature \
386             enabled."
387        );
388    }
389
390    const MIN_LENGTH: usize = 0;
391
392    fn to_html_with_buf(
393        self,
394        buf: &mut String,
395        position: &mut Position,
396        escape: bool,
397        mark_branches: bool,
398    ) {
399        #[cfg(feature = "ssr")]
400        (self.to_html)(self.value, buf, position, escape, mark_branches);
401        #[cfg(not(feature = "ssr"))]
402        {
403            _ = mark_branches;
404            _ = buf;
405            _ = position;
406            _ = escape;
407            panic!(
408                "You are rendering AnyView to HTML without the `ssr` feature \
409                 enabled."
410            );
411        }
412    }
413
414    fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
415        self,
416        buf: &mut StreamBuilder,
417        position: &mut Position,
418        escape: bool,
419        mark_branches: bool,
420    ) where
421        Self: Sized,
422    {
423        #[cfg(feature = "ssr")]
424        if OUT_OF_ORDER {
425            (self.to_html_async_ooo)(
426                self.value,
427                buf,
428                position,
429                escape,
430                mark_branches,
431            );
432        } else {
433            (self.to_html_async)(
434                self.value,
435                buf,
436                position,
437                escape,
438                mark_branches,
439            );
440        }
441        #[cfg(not(feature = "ssr"))]
442        {
443            _ = buf;
444            _ = position;
445            _ = escape;
446            _ = mark_branches;
447            panic!(
448                "You are rendering AnyView to HTML without the `ssr` feature \
449                 enabled."
450            );
451        }
452    }
453
454    fn hydrate<const FROM_SERVER: bool>(
455        self,
456        cursor: &Cursor,
457        position: &PositionState,
458    ) -> Self::State {
459        #[cfg(feature = "hydrate")]
460        if FROM_SERVER {
461            (self.hydrate_from_server)(self.value, cursor, position)
462        } else {
463            panic!(
464                "hydrating AnyView from inside a ViewTemplate is not \
465                 supported."
466            );
467        }
468        #[cfg(not(feature = "hydrate"))]
469        {
470            _ = cursor;
471            _ = position;
472            panic!(
473                "You are trying to hydrate AnyView without the `hydrate` \
474                 feature enabled."
475            );
476        }
477    }
478
479    fn html_len(&self) -> usize {
480        #[cfg(feature = "ssr")]
481        {
482            self.html_len
483        }
484        #[cfg(not(feature = "ssr"))]
485        {
486            0
487        }
488    }
489}
490
491impl Mountable for AnyViewState {
492    fn unmount(&mut self) {
493        (self.unmount)(&mut *self.state)
494    }
495
496    fn mount(
497        &mut self,
498        parent: &crate::renderer::types::Element,
499        marker: Option<&crate::renderer::types::Node>,
500    ) {
501        (self.mount)(&mut *self.state, parent, marker)
502    }
503
504    fn insert_before_this(&self, child: &mut dyn Mountable) -> bool {
505        (self.insert_before_this)(&*self.state, child)
506    }
507}
508/*
509#[cfg(test)]
510mod tests {
511    use super::IntoAny;
512    use crate::{
513        html::element::{p, span},
514        renderer::mock_dom::MockDom,
515        view::{any_view::AnyView, RenderHtml},
516    };
517
518    #[test]
519    fn should_handle_html_creation() {
520        let x = 1;
521        let mut buf = String::new();
522        let view: AnyView<MockDom> = if x == 0 {
523            p((), "foo").into_any()
524        } else {
525            span((), "bar").into_any()
526        };
527        view.to_html(&mut buf, &Default::default());
528        assert_eq!(buf, "<span>bar</span><!>");
529    }
530}
531 */