Created
February 19, 2024 22:36
-
-
Save boyswan/6d5d7c14e3927c3383842d6608729c63 to your computer and use it in GitHub Desktop.
leptos form experiment
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use validator::{Validate, ValidationErrors}; | |
macro_rules! form_field_vec_methods { | |
($field_name:ident, $field_type:ty) => { | |
paste::item! { | |
pub fn [<err_ $field_name>](&self) -> Option<String> { | |
self.0.get().is_valid(stringify!($field_name)) | |
} | |
} | |
paste::item! { | |
pub fn [<get_ $field_name>](&self) -> Vec<FormItem<$field_type>> { | |
self.0.get().$field_name.clone() | |
} | |
} | |
paste::item! { | |
pub fn [<add_ $field_name>](&mut self) { | |
self.0.update(|f| f.$field_name.push(FormItem(create_rw_signal(Default::default())))); | |
} | |
} | |
}; | |
} | |
macro_rules! form_field_string_methods { | |
($field_name:ident) => { | |
paste::item! { | |
pub fn [<err_ $field_name>](&self) -> Option<String> { | |
self.0.get().is_valid(stringify!($field_name)) | |
} | |
} | |
paste::item! { | |
pub fn [<set_ $field_name>](&mut self, ev: Event) { | |
let value = event_target_value(&ev); | |
self.0.update(|f| f.$field_name = value); | |
} | |
} | |
paste::item! { | |
pub fn [<get_ $field_name>](&self) -> String { | |
self.0.get().$field_name.clone() | |
} | |
} | |
}; | |
} | |
trait ValidKey { | |
fn is_valid(&self, key: &str) -> Option<String>; | |
} | |
impl<T: Validate> ValidKey for T { | |
fn is_valid(&self, key: &str) -> Option<String> { | |
match self.validate() { | |
Ok(()) => None, | |
Err(emap) => emap | |
.field_errors() | |
.get(key) | |
.and_then(|e| e.first()) | |
.and_then(|x| x.message.clone()) | |
.map(|x| x.to_string()), | |
} | |
} | |
} | |
#[derive(Debug, Clone, Default)] | |
pub struct FormItem<T>(pub RwSignal<T>) | |
where | |
T: Serialize + Validate + Clone + Default + 'static; | |
impl<T> Serialize for FormItem<T> | |
where | |
T: Serialize + Clone + Default + 'static + Validate + Serialize, | |
{ | |
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> | |
where | |
S: serde::Serializer, | |
{ | |
// Serialize the inner value of RwSignal, assuming read access is infallible | |
self.0.get().serialize(serializer) | |
} | |
} | |
impl<'de, T> Deserialize<'de> for FormItem<T> | |
where | |
T: DeserializeOwned + Clone + Default + 'static + Validate + Serialize, | |
{ | |
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> | |
where | |
D: serde::Deserializer<'de>, | |
{ | |
// Deserialize directly into the type T | |
let value = T::deserialize(deserializer)?; | |
Ok(FormItem(create_rw_signal(value))) | |
} | |
} | |
impl<T> Validate for FormItem<T> | |
where | |
T: Serialize + Validate + Clone + Default, | |
{ | |
fn validate(&self) -> Result<(), ValidationErrors> { | |
self.0.get().validate() | |
} | |
} | |
impl<T: Serialize + Validate + Clone + Default> Copy for FormItem<T> {} | |
#[derive(Serialize, Deserialize, Clone, Copy)] | |
#[serde(transparent)] | |
pub struct FormMain(pub RwSignal<ExampleFormData>); | |
#[derive(Serialize, Deserialize, Validate, Debug, Clone, Default)] | |
pub struct ExampleFormDeepEntry { | |
#[validate(length(min = 5, message = "Minimum 5 characters"))] | |
baz: String, | |
} | |
#[derive(Serialize, Deserialize, Validate, Debug, Clone, Default)] | |
pub struct ExampleFormEntry { | |
#[validate(length(min = 5, message = "Minimum 5 characters"))] | |
foo: String, | |
#[validate] | |
#[serde(default)] | |
deeps: Vec<FormItem<ExampleFormDeepEntry>>, | |
} | |
#[derive(Serialize, Deserialize, Validate, Debug, Clone, Default)] | |
pub struct ExampleFormData { | |
#[validate(length(min = 5, message = "Minimum 5 characters"))] | |
first_name: String, | |
#[validate(length(min = 2, message = "Minimum 2 characters"))] | |
last_name: String, | |
#[validate] | |
#[serde(default)] | |
entries: Vec<FormItem<ExampleFormEntry>>, | |
} | |
impl TryFrom<ExampleFormData> for clean::CleanExampleFormData { | |
type Error = serde_json::Error; | |
fn try_from(value: ExampleFormData) -> Result<Self, Self::Error> { | |
let json = serde_json::to_string(&value)?; | |
serde_json::from_str(&*json) | |
} | |
} | |
impl FormItem<ExampleFormDeepEntry> { | |
form_field_string_methods!(baz); | |
} | |
impl FormItem<ExampleFormEntry> { | |
form_field_string_methods!(foo); | |
form_field_vec_methods!(deeps, ExampleFormDeepEntry); | |
} | |
impl FormMain { | |
form_field_string_methods!(first_name); | |
form_field_string_methods!(last_name); | |
form_field_vec_methods!(entries, ExampleFormEntry); | |
} | |
#[component] | |
pub fn FormInput<S, G, E>(mut set: S, get: G, err: E) -> impl IntoView | |
where | |
S: FnMut(Event) -> () + 'static, | |
G: Fn() -> String + 'static, | |
E: Fn() -> Option<String> + 'static, | |
{ | |
view! { | |
<div> | |
<input type="text" on:input=move |ev| set(ev) prop:value=move || get()/> | |
{move || err()} | |
</div> | |
} | |
} | |
#[derive(Serialize, Deserialize, Validate, Debug, Clone, Default)] | |
pub struct Deep { | |
#[validate(length(min = 5, message = "Minimum 5 characters"))] | |
foo: String, | |
bar: String, | |
} | |
#[derive(Serialize, Deserialize, Validate, Debug, Clone, Default)] | |
pub struct Form { | |
title: String, | |
deep: Deep, | |
items: Option<Vec<String>>, | |
} | |
// println!("{:?}", data.entries.first().map(|f| f.get().foo)); | |
#[server] | |
pub async fn do_form(data: ExampleFormData) -> Result<usize, ServerFnError> { | |
let clean: clean::CleanExampleFormData = data.clone().try_into()?; | |
println!("clean {:?}", clean); | |
println!("data {:?}", data); | |
// if (clean.validate().is_err()) { | |
// return Err(ServerFnError::new("Invalid form")); | |
// } | |
// insert a simulated wait | |
tokio::time::sleep(std::time::Duration::from_millis(1250)).await; | |
Ok(0) | |
} | |
#[component] | |
pub fn WithActionForm() -> impl IntoView { | |
let mut formr = FormMain(create_rw_signal(ExampleFormData::default())); | |
let action = create_server_action::<DoForm>(); | |
let submit = move |_| { | |
action.dispatch(DoForm { | |
data: formr.0.get(), | |
}) | |
}; | |
view! { | |
<h3>Using <code>"<ActionForm/>"</code></h3> | |
<p> | |
<code>"<ActionForm/>"</code> | |
"lets you use an HTML " | |
<code>"<form>"</code> | |
"to call a server function in a way that gracefully degrades." | |
</p> | |
<form class="flex flex-col gap-2"> | |
<FormInput | |
set=move |ev| formr.set_first_name(ev) | |
get=move || formr.get_first_name() | |
err=move || formr.err_first_name() | |
/> | |
<FormInput | |
set=move |ev| formr.set_last_name(ev) | |
get=move || formr.get_last_name() | |
err=move || formr.err_last_name() | |
/> | |
<For | |
each=move || formr.get_entries().into_iter().enumerate() | |
key=move |(i, _)| *i | |
children=move |(_, mut entry): (usize, FormItem<ExampleFormEntry>)| { | |
view! { | |
<div> | |
<FormInput | |
set=move |ev| entry.set_foo(ev) | |
get=move || entry.get_foo() | |
err=move || entry.err_foo() | |
/> | |
<For | |
each=move || entry.get_deeps().into_iter().enumerate() | |
key=move |(i, _)| *i | |
children=move | | |
(_, mut deep): (usize, FormItem<ExampleFormDeepEntry>)| | |
{ | |
view! { | |
<FormInput | |
set=move |ev| deep.set_baz(ev) | |
get=move || deep.get_baz() | |
err=move || deep.err_baz() | |
/> | |
} | |
} | |
/> | |
<button type="button" on:click=move |_| entry.add_deeps()> | |
add baz entry | |
</button> | |
</div> | |
} | |
} | |
/> | |
<button type="button" on:click=move |_| formr.add_entries()> | |
add form entry | |
</button> | |
<button disabled=false type="button" on:click=submit> | |
hit | |
</button> | |
</form> | |
<Transition> | |
<p>You submitted: {move || format!("{:?}", action.input().get())}</p> | |
</Transition> | |
<p>The result was: {move || format!("{:?}", action.value().get())}</p> | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment