leptos_form_core/
components.rs

1//! Common form components
2
3use leptos::*;
4use std::rc::Rc;
5
6pub trait OnError<I: 'static, T: Clone + 'static, IV: IntoView + 'static>:
7    Fn(ServerFnError, Action<I, Result<T, ServerFnError>>) -> IV + 'static
8{
9}
10
11pub trait OnSuccess<I: 'static, T: Clone + 'static, IV: IntoView + 'static>:
12    Fn(T, Action<I, Result<T, ServerFnError>>) -> IV + 'static
13{
14}
15
16impl<I: 'static, T: Clone + 'static, IV: IntoView + 'static, F> OnError<I, T, IV> for F where
17    F: Fn(ServerFnError, Action<I, Result<T, ServerFnError>>) -> IV + 'static
18{
19}
20impl<I: 'static, T: Clone + 'static, IV: IntoView + 'static, F> OnSuccess<I, T, IV> for F where
21    F: Fn(T, Action<I, Result<T, ServerFnError>>) -> IV + 'static
22{
23}
24
25pub struct LeptosFormChildren(pub Rc<dyn Fn() -> View + 'static>);
26
27impl<T: IntoView, F: Fn() -> T + 'static> From<F> for LeptosFormChildren {
28    fn from(f: F) -> Self {
29        Self(Rc::new(move || <T as IntoView>::into_view(f())))
30    }
31}
32
33#[component]
34pub fn FormSubmissionHandler<IV1: IntoView + 'static, IV2: IntoView + 'static, I: 'static, T: Clone + 'static>(
35    action: Action<I, Result<T, ServerFnError>>,
36    #[prop(optional)] on_error: Option<Rc<dyn OnError<I, T, IV2>>>,
37    #[prop(optional)] on_success: Option<Rc<dyn OnSuccess<I, T, IV1>>>,
38    #[allow(unused_variables)]
39    #[prop(optional)]
40    error_view_ty: Option<std::marker::PhantomData<IV2>>,
41    #[allow(unused_variables)]
42    #[prop(optional)]
43    success_view_ty: Option<std::marker::PhantomData<IV1>>,
44) -> impl IntoView {
45    view! {{move || match action.pending().get() {
46        true => view! { <div>"Loading..."</div> }.into_view(),
47        false => match action.value().get() {
48            Some(Ok(ok)) => match on_success.clone() {
49                Some(on_success) => on_success(ok, action).into_view(),
50                None => View::default(),
51            },
52            Some(Err(err)) => match on_error.clone() {
53                Some(on_error) => on_error(err, action).into_view(),
54                None => view! {
55                    <div>
56                    {move || match err.clone() {
57                        ServerFnError::Request(err) => err,
58                        ServerFnError::ServerError(err) => err,
59                        _ => "Internal Error".to_string(),
60                    }}
61                    </div>
62                }.into_view(),
63            },
64            None => View::default(),
65        }
66    }}}
67}
68
69/// Aderived signal returning a style string which should be placed on the top level component's `style:opacity` prop
70pub type StyleSignal = Rc<dyn Fn() -> Option<&'static str>>;
71
72#[component]
73pub fn MaterialIcon(
74    d: &'static str,
75    #[prop(optional_no_strip, into)] id: Option<Oco<'static, str>>,
76    #[prop(optional_no_strip, into)] class: Option<Oco<'static, str>>,
77    #[prop(optional_no_strip, into)] cursor: Option<StyleSignal>,
78    #[prop(optional_no_strip, into)] height: Option<usize>,
79    #[prop(optional_no_strip, into)] opacity: Option<StyleSignal>,
80    #[prop(optional_no_strip, into)] style: Option<Oco<'static, str>>,
81    #[prop(optional_no_strip, into)] width: Option<usize>,
82) -> impl IntoView {
83    let transform = svg_transform(24, 24, height, width);
84    let style = match (style, transform) {
85        (Some(style), Some(transform)) => Some(format!("{transform} {style}")),
86        (Some(style), None) => Some(style.to_string()),
87        (None, Some(transform)) => Some(transform),
88        (None, None) => None,
89    };
90    let cursor = move || cursor.clone().and_then(|x| x());
91    let opacity = move || opacity.clone().and_then(|x| x());
92    view! {
93        <svg
94            id=id
95            class=class
96            xmlns="http://www.w3.org/2000/svg"
97            height="24"
98            width="24"
99            viewBox="0 -960 960 960"
100            fill="currentColor"
101            style:cursor=cursor
102            style:opacity=opacity
103            style=style
104        >
105            <path d={d} />
106        </svg>
107    }
108}
109
110fn svg_transform(
111    default_height: usize,
112    default_width: usize,
113    height: Option<usize>,
114    width: Option<usize>,
115) -> Option<String> {
116    let height = height.unwrap_or(default_height);
117    let width = width.unwrap_or(default_width);
118
119    let xscale = (width != default_width).then_some(width as f32 / default_width as f32);
120    let yscale = (height != default_height).then_some(height as f32 / default_height as f32);
121    if xscale.is_none() && yscale.is_none() {
122        return None;
123    }
124
125    let xtranslate = xscale.map(|xscale| (xscale - 1.) * default_width as f32 / 2.);
126    let ytranslate = yscale.map(|yscale| (yscale - 1.) * default_height as f32 / 2.);
127
128    let xscale = xscale.map(|val| val.to_string()).unwrap_or_default();
129    let yscale = yscale.map(|val| val.to_string()).unwrap_or_default();
130    let xtranslate = xtranslate.map(|val| val.to_string()).unwrap_or_default();
131    let ytranslate = ytranslate.map(|val| val.to_string()).unwrap_or_default();
132
133    Some(format!(
134        "transform: translate({xtranslate} {ytranslate}) scale({xscale} {yscale});"
135    ))
136}
137
138#[component]
139pub fn MaterialClose(
140    #[prop(optional_no_strip, into)] id: Option<Oco<'static, str>>,
141    #[prop(optional_no_strip, into)] class: Option<Oco<'static, str>>,
142    #[prop(optional_no_strip, into)] cursor: Option<StyleSignal>,
143    #[prop(optional_no_strip, into)] height: Option<usize>,
144    #[prop(optional_no_strip, into)] opacity: Option<StyleSignal>,
145    #[prop(optional_no_strip, into)] style: Option<Oco<'static, str>>,
146    #[prop(optional_no_strip, into)] width: Option<usize>,
147) -> impl IntoView {
148    view! {
149        <MaterialIcon
150            id=id
151            class=class
152            cursor=cursor
153            d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"
154            height=height
155            opacity=opacity
156            style=style
157            width=width
158        />
159    }
160}