formidable 0.1.0

Easily derive forms from structs for Leptos.
Documentation
#![allow(non_snake_case)]

pub mod components;
mod error;
mod name;
pub mod types;

pub use error::*;
pub use name::*;

use derive_more::Display;
pub use formidable_derive::Form;
use std::{fmt::Display, marker::PhantomData, sync::Arc};

use leptos::{ev::SubmitEvent, prelude::*, server_fn::ServerFn};

use std::fmt::Debug;

pub struct FieldConfiguration {
    pub label: Option<TextProp>,
    pub description: Option<TextProp>,
}

pub trait Form: Sized + Send + Sync + 'static {
    fn view(
        field: FieldConfiguration,
        name: Name,
        value: Option<Self>,
        callback: Option<Callback<Result<Self, FormError>>>,
    ) -> impl IntoView;
}

#[component]
pub fn FormidableCallback<T>(
    #[prop(into)] label: TextProp,
    #[prop(into)] name: Name,
    #[prop(optional)] value: Option<T>,
    #[prop(optional)] callback: Option<Callback<Result<T, FormError>>>,
) -> impl IntoView
where
    T: Form,
{
    T::view(
        FieldConfiguration {
            label: Some(label),
            description: None,
        },
        name,
        value,
        callback,
    )
}

#[component]
pub fn FormidableRwSignal<T>(
    #[prop(into)] label: TextProp,
    #[prop(into)] name: Name,
    #[prop(into)] value: RwSignal<T>,
) -> impl IntoView
where
    T: Form + Clone,
{
    let callback = Callback::new(move |form_result: Result<T, FormError>| {
        if let Ok(v) = form_result {
            value.set(v);
        }
    });

    T::view(
        FieldConfiguration {
            label: Some(label),
            description: None,
        },
        name,
        Some(value.get_untracked()),
        Some(callback),
    )
}

#[component]
pub fn FormidableServerAction<F, T>(
    #[prop(into)] label: TextProp,
    #[prop(into)] name: Name,
    #[prop(optional)] value: Option<T>,
    #[prop(optional)] callback: Option<Callback<F::Output, F::Error>>,
    #[prop(optional)] _phantom: PhantomData<F>,
) -> impl IntoView
where
    T: Form + Clone + Debug + Send + Sync + 'static,
    F: ServerFn + Clone + From<T> + Send + Sync + 'static,
    F::Output: Clone + Send + Sync + 'static,
    F::Error: Clone + Send + Sync + Display + 'static,
{
    let submit = ServerAction::<F>::new();
    let curr_value = RwSignal::new(value.as_ref().map(|v| Ok(v.clone())));
    let form_callback = Callback::new(move |form_result: Result<T, FormError>| {
        curr_value.set(Some(form_result));
    });
    let submit_disabled = Signal::derive(move || {
        curr_value
            .get()
            .and_then(|v: Result<T, FormError>| v.ok())
            .is_none()
            || submit.pending().get()
    });

    let on_submit = {
        move |ev: SubmitEvent| {
            if ev.default_prevented() {
                return;
            }

            ev.prevent_default();

            if let Some(value) = curr_value.get_untracked().and_then(|v| v.ok()) {
                submit.dispatch(value.into());
            }
        }
    };

    if let Some(callback) = callback {
        Effect::new(move || {
            if let Some(res) = submit.value().get() {
                callback.run(res.unwrap());
            }
        });
    }

    view! {
        <form on:submit=on_submit>
            {T::view(FieldConfiguration {
                label: Some(label),
                description: None,
            }, name, value, Some(form_callback)) }
            <button type="submit" disabled=submit_disabled>{t(FormMessage::SubmitButton)}</button>
            { move ||
                if submit.pending().get() {
                    Some(view! { <p class="message info-message">{t(FormMessage::SubmitPendingMessage)}</p> }.into_any())
                } else {
                    submit.value().get().map(|res| match res {
                        Ok(_) => view! { <p class="message success-message">{t(FormMessage::SubmitSuccessMessage)}</p> }.into_any(),
                        Err(_err) => view! { <p class="message error-message">{t(FormMessage::SubmitErrorMessage)}</p> }.into_any(),
                    })
                }

            }
        </form>
    }
}

#[derive(Clone)]
pub struct Translation<T>(Arc<dyn Fn(T) -> String>);

impl<T, F> From<F> for Translation<T>
where
    F: Fn(T) -> String + 'static,
{
    fn from(f: F) -> Self {
        Translation(Arc::new(f))
    }
}

impl<T> Translation<T> {
    pub fn apply(&self, text: T) -> String {
        (self.0)(text)
    }
}

pub(crate) fn t<T: Display + Clone + 'static>(text: T) -> String {
    let translation = use_context::<Translation<T>>();
    if let Some(translation) = translation {
        translation.apply(text)
    } else {
        format!("{}", text)
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Display)]
pub enum FormMessage {
    #[display("Submit")]
    SubmitButton,
    #[display("Add")]
    AddButton,
    #[display("Remove")]
    RemoveButton,
    #[display("Submitting ...")]
    SubmitPendingMessage,
    #[display("Form submitted successfully")]
    SubmitSuccessMessage,
    #[display("Error submitting form")]
    SubmitErrorMessage,
}