anaso_components_stela 0.0.8

Components for Ana.so's Stela API
Documentation
use std::sync::Arc;

use anaso_site_api_models::stela;
use js_sys::wasm_bindgen::JsCast;
use leptos::either::EitherOf9;
use leptos::ev;
use leptos::prelude::*;
use leptos_router::components::Redirect;
use phosphor_leptos::Icon;
use phosphor_leptos::CHECK_CIRCLE;
use server_fn::codec::MultipartData;
use web_sys::HtmlButtonElement;
use web_sys::HtmlFormElement;
use web_sys::HtmlInputElement;

use crate::FormSubmitContext;

use super::SectionCard;

use self::form_input_cf_turnstile::*;
use self::form_input_checkbox::*;
use self::form_input_image::*;
use self::form_input_markdown::*;
use self::form_input_motions::*;
use self::form_input_radio::*;
use self::form_input_subsection::*;
use self::form_input_text::*;

mod form_input_cf_turnstile;
mod form_input_checkbox;
mod form_input_image;
mod form_input_markdown;
mod form_input_motions;
mod form_input_radio;
mod form_input_subsection;
mod form_input_text;

#[component]
pub fn SectionForm(border: bool, section: Arc<stela::SectionForm>) -> impl IntoView {
    let form_submit = use_context::<FormSubmitContext>().unwrap();
    let error = RwSignal::new(None);

    let action = Action::new_local(move |data: &web_sys::FormData| {
        let data: MultipartData = data.clone().into();
        async move { form_submit.0.get_value().submit(data).await }
    });

    let value = action.value();

    let on_submit = move |event: ev::SubmitEvent| {
        event.prevent_default();
        match form_data_from_event(&event) {
            Err(_e) => {
                error.try_set(Some(String::from("error making form data")));
            }
            Ok(data) => {
                action.dispatch_local(data);
            }
        }
    };

    let error_text = Signal::derive(move || {
        value
            .get()
            .and_then(|v| v.ok())
            .and_then(|v| v.error.clone())
    });

    let success_text = Memo::new(move |_| {
        value
            .get()
            .and_then(|v| v.ok())
            .and_then(|v| v.success.clone())
    });

    let redirect = Memo::new(move |_| {
        value
            .get()
            .and_then(|v| v.ok())
            .and_then(|v| v.redirect.clone())
    });

    view! {
        {move || {
            redirect.get().map(|redirect| {
                view! {
                    <Redirect path=redirect />
                }
            })
        }}
        {move || {
            if let Some(success_text) = success_text.get() {
                view! {
                    <SectionCard border=false class="stela--form-success">
                        <div class="stela--section--padded">
                            <Icon icon=CHECK_CIRCLE size="36px" />
                            <span>{success_text}</span>
                        </div>
                    </SectionCard>
                }
            } else {
                let section = section.clone();
                let inputs = section
                    .inputs
                    .iter()
                    .map(|input| view! { <FormInput input=input.clone() /> })
                    .collect_view();
                view! {
                    <SectionCard border=border>
                        <form
                            action=form_submit.get_value().url()
                            method="POST"
                            on:submit=on_submit
                            class="stela--form stela--section--padded"
                            enctype="multipart/form-data"
                        >
                            {section.header.clone().map(|text| view! { <h2>{text}</h2> })}
                            {section.subheader.clone().map(|text| view! { <p>{text}</p> })}
                            <input type="hidden" name="form_name" value=section.form_name.clone() />
                            <input type="hidden" name="extra_data" value=section.extra_data.clone() />
                            {move || {
                                error_text
                                    .get()
                                    .map(|error_text| {
                                        view! { <p class="stela--form--error">{error_text}</p> }
                                    })
                            }}
                            {inputs}
                        </form>
                    </SectionCard>
                }
            }
        }}
    }
}

#[component]
fn FormInput(input: stela::FormInput) -> impl IntoView {
    match input {
        stela::FormInput::Text(input) => {
            EitherOf9::A(view! { <FormInputText input=input /> }.into_view())
        }
        stela::FormInput::Checkbox(input) => {
            EitherOf9::B(view! { <FormInputCheckbox input=input /> }.into_view())
        }
        stela::FormInput::CfTurnstile(input) => {
            EitherOf9::C(view! { <FormInputCfTurnstile input=input /> }.into_view())
        }
        stela::FormInput::Image(input) => {
            EitherOf9::D(view! { <FormInputImage input=input /> }.into_view())
        }
        stela::FormInput::Markdown(input) => {
            EitherOf9::E(view! { <FormInputMarkdown input=input /> }.into_view())
        }
        stela::FormInput::Motions(input) => {
            EitherOf9::F(view! { <FormInputMotions input=input /> }.into_view())
        }
        stela::FormInput::Radio(input) => {
            EitherOf9::G(view! { <FormInputRadio input=input /> }.into_view())
        }
        stela::FormInput::Subsection(input) => {
            EitherOf9::H(view! { <FormInputSubsection input=input /> }.into_view())
        }
        stela::FormInput::Tabs(_) => todo!(),
        stela::FormInput::Unknown => EitherOf9::I(().into_view()),
    }
}

fn form_data_from_event(ev: &ev::SubmitEvent) -> Result<web_sys::FormData, FromFormDataError> {
    let submitter = ev.submitter();
    let mut submitter_name_value = None;
    let opt_form = match &submitter {
        Some(el) => {
            if let Some(form) = el.dyn_ref::<HtmlFormElement>() {
                Some(form.clone())
            } else if let Some(input) = el.dyn_ref::<HtmlInputElement>() {
                submitter_name_value = Some((input.name(), input.value()));
                Some(ev.target().unwrap().unchecked_into())
            } else if let Some(button) = el.dyn_ref::<HtmlButtonElement>() {
                submitter_name_value = Some((button.name(), button.value()));
                Some(ev.target().unwrap().unchecked_into())
            } else {
                None
            }
        }
        None => ev.target().map(|form| form.unchecked_into()),
    };
    match opt_form.as_ref().map(web_sys::FormData::new_with_form) {
        None => Err(FromFormDataError::MissingForm(ev.clone().into())),
        Some(Err(e)) => Err(FromFormDataError::FormData(e)),
        Some(Ok(form_data)) => {
            if let Some((name, value)) = submitter_name_value {
                form_data
                    .append_with_str(&name, &value)
                    .map_err(FromFormDataError::FormData)?;
            }
            Ok(form_data)
        }
    }
}