ridstack-form 0.1.0

End-to-End Type-safe form handling for Dioxus applications
use crate::prelude::*;
use dioxus::prelude::*;
#[derive(Store, PartialEq, Eq, Debug)]
struct TestData {
    name: String,
}

#[derive(Clone, Store, Default)]
struct TestFieldState {
    name: FieldState<String, FormError>,
}

#[derive(Store, Clone, PartialEq)]
struct TestForm {
    field_state: Store<TestFieldState>,
    form_state: Store<FormState>,
}

impl FormSubmitInput for TestForm {
    type SubmitInput = TestData;
    fn build_submit_input(&mut self) -> Option<Self::SubmitInput> {
        Some(TestData {
            name: self.field_state.name().data().peek().cloned(),
        })
    }
}

impl GetFieldRegistry for TestForm {
    type FieldRegistry = Self;
    fn get_field_registry(&self) -> Self::FieldRegistry {
        self.clone()
    }
}

#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
struct GetName;

impl FieldStateProvider<GetName> for TestForm {
    type FieldValue = String;
    type FieldError = FormError;
    fn get_field_state(
        &self,
        field: &GetName,
    ) -> Store<FieldState<Self::FieldValue, Self::FieldError>> {
        self.field_state.name().into()
    }
    fn name(&self, field: &GetName) -> &'static str {
        "name"
    }
}

impl CalculateCanSubmit for TestForm {
    fn calculate_can_submit(&mut self) -> bool {
        let name_can_submit = self.field_state.name().can_submit();
        let name_is_validating = self.field_state.name().is_validating()();
        if !name_can_submit || name_is_validating {
            self.form_state.can_submit().set(false);
            return false;
        }
        return true;
    }
}

impl GetFormState for TestForm {
    fn get_form_state(&self) -> Store<FormState> {
        self.form_state.clone()
    }
}

impl InitForm for TestForm {
    fn use_form() -> Self {
        let form_state = use_init_form_state();
        let field_state = use_store(|| TestFieldState::default());
        Self {
            field_state,
            form_state,
        }
    }
}

#[derive(Debug, thiserror::Error, PartialEq, Eq, PartialOrd, Ord, Clone)]
enum FormError {
    #[error("This error should never occur.")]
    Infallible,
    #[error("Name already exists.")]
    NameAlreadyExists,
    #[error("Name shouldn't start with R")]
    NameShouldntStartWithR,
}
impl FieldError for FormError {}
impl From<Infallible> for FormError {
    fn from(_value: Infallible) -> Self {
        Self::Infallible
    }
}

impl ValidateOnSubmit for TestForm {
    fn validate_on_submit(&mut self) -> std::pin::Pin<Box<impl Future<Output = ()>>> {
        if matches!(
            self.field_state.name().validate_on()(),
            ValidateOn::All | ValidateOn::Submit
        ) {
            self.field_state.name().validate();
        }
        Box::pin(async move {})
    }
}

#[component]
fn TestFormComponent(on_submit: EventHandler<TestData>, children: Element) -> Element {
    rsx! {
        Form::<TestForm> {
            on_submit,
            {children}
        }
    }
}

#[component]
fn TestFieldComponent<TField>(
    component: Callback<FieldApi<TestForm, TField>, Element>,
    field: TField,

    #[props(optional)] validate_on: ValidateOn,
    #[props(optional, default = Vec::with_capacity(0))] validators: Validators<
        <<TestForm as GetFieldRegistry>::FieldRegistry as FieldStateProvider<TField>>::FieldValue,
        <<TestForm as GetFieldRegistry>::FieldRegistry as FieldStateProvider<TField>>::FieldError,
    >,
    #[props(optional, default = Vec::with_capacity(0))] async_validators: AsyncValidators<
        <<TestForm as GetFieldRegistry>::FieldRegistry as FieldStateProvider<TField>>::FieldValue,
        <<TestForm as GetFieldRegistry>::FieldRegistry as FieldStateProvider<TField>>::FieldError,
    >,
) -> Element
where
    TField: Clone + PartialEq + 'static,
    TestForm: FieldStateProvider<TField>,
{
    rsx! {
        Field::<TestForm, TField> {
            validate_on,
            validators,
            async_validators,
            field,
            component
        }
    }
}

// reusable component
#[component]
fn TextField<TForm, TField>(
    #[props(optional)] id: String,
    field_api: ReadSignal<FieldApi<TForm, TField>>,
    #[props(extends=GlobalAttributes,extends=input)] attributes: Vec<Attribute>,
) -> Element
where
    TForm: FieldStateProvider<TField>,
    TField: Clone + PartialEq + 'static,
    TForm::FieldValue: FieldValue<PrimitiveValue = String>,
{
    rsx! {
        label{
            r#for: &id
        }
        input{
            id,
            value: field_api().value(),
            onblur: field_api.peek().handlers().on_blur(),
            oninput: move|evt|{
                let _ = field_api.peek().handlers().on_input()(Either::Right(evt.value()));

            },
            ..attributes
        }
        div{if let Some(err) = (field_api().state().errors)().get(0){

            p{
                {err.error_value()}
            }
        }
        }
    }
}

#[component]
fn Test() -> Element {
    use_form_provider::<TestForm>();
    rsx! {
        TestFormComponent {
            on_submit:|d|{
                print!("{:?}", d);
            },
            TestFieldComponent {
                field: GetName,
                validate_on: ValidateOn::Input,
                validators: vec![Validator::new(|input:&String|{
                    if input.starts_with("R"){
                        return Err(FormError::NameShouldntStartWithR)
                    }
                    Ok(())
                }, 0)],
                async_validators: vec![AsyncValidator::new(|input:&String|{

                    let input = input.clone();
                    Box::pin(async move {
                        if input.eq(&"John Doe".to_string()) {
                            return Err(FormError::NameAlreadyExists);
                        }
                        Ok(())
                    })
                }, 0)],
                component: |field_api| rsx!{TextField { field_api }}


            }
        }
    }
}