Note that text fields only accept Region 15 email addresses right now. We should add extra domains in the future.
148 lines
4.7 KiB
Rust
148 lines
4.7 KiB
Rust
use leptos::logging::log;
|
|
use leptos::prelude::*;
|
|
use leptos::web_sys::SubmitEvent;
|
|
use crate::components::ActionButton;
|
|
|
|
/// Indicates the current state of an input.
|
|
#[derive(Default, Debug, Clone)]
|
|
pub enum ValidationState {
|
|
/// A valid input.
|
|
#[default]
|
|
Valid,
|
|
/// An invalid input with an error message.
|
|
Invalid(String)
|
|
}
|
|
|
|
/// A struct that contains all information required for a form to make real-time validation
|
|
/// decisions, including display of error messages.
|
|
#[derive(Debug, Clone)]
|
|
pub struct InputState {
|
|
/// The name of the input. This is used to identify the input in the form.
|
|
pub input_name: String,
|
|
/// Indicates the current state of the input.
|
|
error: Signal<ValidationState>,
|
|
/// Indicates whether the form has requested the error state. This means that the input
|
|
/// should show their error appearances immediately.
|
|
form_requested_validation: RwSignal<bool>,
|
|
}
|
|
|
|
impl InputState {
|
|
/// Creates a new [`InputState`] with the given error and form requested validation signals.
|
|
pub fn new(
|
|
name: impl Into<String>,
|
|
error: impl Into<Signal<ValidationState>>,
|
|
form_requested_errors: RwSignal<bool>
|
|
) -> Self {
|
|
Self {
|
|
input_name: name.into(),
|
|
error: error.into(),
|
|
form_requested_validation: form_requested_errors
|
|
}
|
|
}
|
|
|
|
pub fn error(&self) -> Signal<ValidationState> {
|
|
self.error.clone()
|
|
}
|
|
}
|
|
|
|
/// A list of [`InputState`]s.
|
|
pub type ValidatorList = Vec<RwSignal<InputState>>;
|
|
|
|
/// A struct that keeps track of all components that must be validated.
|
|
/// All of our custom-made components must be placed within an area that contains context with
|
|
/// this struct.
|
|
#[derive(Debug, Clone)]
|
|
pub struct FormValidationRegistry {
|
|
pub validators: RwSignal<ValidatorList>
|
|
}
|
|
|
|
/// # Validated Form Component
|
|
///
|
|
/// A simple HTML `<form>` wrapper that executes validation logic using a [`FormValidationRegistry`].
|
|
/// It will ensure that any invalid inputs are caught before submission. All input components that
|
|
/// are used within this form should call `use_context::<FormValidationRegistry>()` to register
|
|
/// themselves with the form.
|
|
///
|
|
/// Example usage in the view macro:
|
|
/// ```
|
|
/// view! {
|
|
/// <ValidatedForm on_submit=Callback::new(move || {/* something here */})>
|
|
/// {/* As many input components as you want */}
|
|
/// </ValidatedForm>
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// Example input component implementation:
|
|
/// ```
|
|
/// fn SomeComponent() -> impl IntoView {
|
|
/// // Get FormValidationContext
|
|
/// let context = use_context::<FormValidationRegistry>()
|
|
/// .expect("Couldn't find FormValidationRegsitry context");
|
|
///
|
|
/// // Register with the form
|
|
/// let error = RwSignal::new(ValidationState::Valid);
|
|
/// let form_requested_errors = RwSignal::new(false);
|
|
/// let state = InputState::new(
|
|
/// "the_input_name",
|
|
/// error.clone(),
|
|
/// form_requested_errors.clone()
|
|
/// );
|
|
/// context.validators.update(|list| list.push(RwSignal::new(state)));
|
|
///
|
|
/// /* Other input view logic */
|
|
/// }
|
|
/// ```
|
|
#[component]
|
|
pub fn ValidatedForm(
|
|
children: Children,
|
|
/// A callback that runs when the form successfully validates.
|
|
#[prop(into)] on_submit: Callback<()>,
|
|
) -> impl IntoView {
|
|
let validators = RwSignal::new(vec![]);
|
|
|
|
// Provide context to all children nested within this component. They will be able to access
|
|
// this no matter how nested they are.
|
|
provide_context(FormValidationRegistry {validators});
|
|
|
|
let can_submit = Memo::new(move |_| {
|
|
log!("Checking {} inputs...", validators.get().len());
|
|
validators
|
|
.with(|list| {
|
|
list.iter()
|
|
// TODO all() short circuits on the first false
|
|
.all(|v| {
|
|
// Not sure how this will work reactively.
|
|
let result = matches!(v.get().error().get(), ValidationState::Valid);
|
|
v.update(|validator| validator.form_requested_validation.set(true));
|
|
result
|
|
})
|
|
})
|
|
});
|
|
|
|
let submit_success_event = move |e: SubmitEvent| {
|
|
e.prevent_default();
|
|
|
|
if !can_submit.get() {
|
|
log!("Form failed to submit, there are errors in form inputs.");
|
|
return;
|
|
}
|
|
|
|
log!("Form validation successful.");
|
|
on_submit.run(());
|
|
};
|
|
|
|
view! {
|
|
<form novalidate on:submit=submit_success_event>
|
|
{children()}
|
|
<ActionButton
|
|
on:click=move |_| {
|
|
log!("{:?}", can_submit.get());
|
|
}
|
|
button_type="submit"
|
|
>
|
|
"Submit form (but actually)"
|
|
</ActionButton>
|
|
</form>
|
|
}
|
|
}
|