hashira/app/
app_nested.rs

1use crate::actions::Action;
2use crate::components::id::PageId;
3use crate::components::PageComponent;
4use crate::routing::{ClientPageRoute, Route};
5use serde::de::DeserializeOwned;
6use std::{collections::HashMap, marker::PhantomData};
7use yew::html::ChildrenProps;
8use yew::BaseComponent;
9
10/// Marker to specify a nested route should be inserted at the root of the router,
11/// and not as a sub route.
12///
13/// This is just a workaround for allowing to insert actions in specify path.
14#[allow(dead_code)]
15pub(crate) struct InsertInRootRoute;
16
17/// Represents a nested route in a `App`.
18#[derive(Default)]
19pub struct AppNested<BASE> {
20    // Inner server routes
21    #[cfg(not(feature = "client"))]
22    pub(crate) server_router: HashMap<String, Route>,
23
24    // Inner page router
25    pub(crate) page_router: HashMap<String, ClientPageRoute>,
26
27    //
28    _marker: PhantomData<BASE>,
29}
30
31impl<BASE> AppNested<BASE>
32where
33    BASE: BaseComponent<Properties = ChildrenProps> + 'static,
34{
35    /// Creates a new nested route.
36    pub fn new() -> Self {
37        AppNested {
38            #[cfg(not(feature = "client"))]
39            server_router: HashMap::new(),
40            page_router: HashMap::new(),
41            _marker: PhantomData,
42        }
43    }
44
45    /// Adds a route handler.
46    #[cfg_attr(feature = "client", allow(unused_mut, unused_variables))]
47    pub fn route(mut self, route: Route) -> Self {
48        #[cfg(not(feature = "client"))]
49        {
50            let path = route.path().to_owned(); // To please the borrow checker
51            self.server_router.insert(path, route);
52        }
53
54        self
55    }
56
57    /// Adds a page for the given route.
58    #[cfg_attr(feature = "client", allow(unused_variables))]
59    pub fn page<COMP>(mut self) -> Self
60    where
61        COMP: PageComponent,
62        COMP::Properties: DeserializeOwned,
63    {
64        // Pages must had a path defined
65        let route = COMP::route().unwrap_or_else(|| {
66            panic!(
67                "`{}` is not declaring a route",
68                std::any::type_name::<COMP>()
69            )
70        });
71
72        self.add_component::<COMP>(route);
73
74        #[cfg(not(feature = "client"))]
75        {
76            use crate::app::{RenderContext, RenderLayout, RequestContext};
77            use crate::routing::HandlerKind;
78
79            let mut route = Route::get(route, move |ctx: RequestContext, body| {
80                let head = super::page_head::PageHead::new();
81                let render_layout = ctx.app_data::<RenderLayout>().cloned().unwrap();
82                let render_ctx = RenderContext::new(ctx, head, render_layout);
83
84                // Returns the future
85                COMP::render::<BASE>(render_ctx, body)
86            });
87
88            route.extensions_mut().insert(HandlerKind::Page);
89            self.route(route)
90        }
91
92        // In the client we don't add pages, just the component
93        #[cfg(feature = "client")]
94        self
95    }
96
97    /// Register a server action.
98    pub fn action<A>(self) -> Self
99    where
100        A: Action,
101    {
102        #[cfg(not(feature = "client"))]
103        {
104            use crate::app::RequestContext;
105            use crate::web::{Body, IntoJsonResponse, Response};
106            use crate::routing::HandlerKind;
107            
108            let route = A::route().to_string();
109            let method = A::method();
110            let mut route = Route::new(&route, method, |ctx: RequestContext, body: Body| async move {
111                let output = crate::try_response!(A::call(ctx, body).await);
112                let json_res = crate::try_response!(output.into_json_response());
113                let (parts, body) = json_res.into_parts();
114                let bytes = crate::try_response!(serde_json::to_vec(&body));
115                let body = Body::from(bytes);
116                Response::from_parts(parts, body)
117            });
118
119            route.extensions_mut().insert(InsertInRootRoute);
120            route.extensions_mut().insert(HandlerKind::Action);
121            self.route(route)
122        }
123
124        #[cfg(feature = "client")]
125        self
126    }
127
128    fn add_component<COMP>(&mut self, path: &str)
129    where
130        COMP: PageComponent,
131        COMP::Properties: DeserializeOwned,
132    {
133        use crate::components::AnyComponent;
134
135        log::debug!(
136            "Registering component `{}` on path: {path}",
137            std::any::type_name::<COMP>()
138        );
139
140        self.page_router.insert(
141            path.to_owned(),
142            ClientPageRoute {
143                path: path.to_owned(),
144                page_id: PageId::of::<COMP>(),
145                component: AnyComponent::<serde_json::Value>::new(|props_json| {
146                    let props = serde_json::from_value(props_json).unwrap_or_else(|err| {
147                        panic!(
148                            "Failed to deserialize `{}` component props. {err}",
149                            std::any::type_name::<COMP>()
150                        )
151                    });
152
153                    yew::html! {
154                        <COMP ..props/>
155                    }
156                }),
157            },
158        );
159    }
160}
161
162/// Creates a new nested app.
163pub fn nested<BASE>() -> AppNested<BASE>
164where
165    BASE: BaseComponent<Properties = ChildrenProps> + 'static,
166{
167    AppNested::new()
168}