Skip to content

Instantly share code, notes, and snippets.

@boyswan
Created February 19, 2024 22:36
Show Gist options
  • Save boyswan/6d5d7c14e3927c3383842d6608729c63 to your computer and use it in GitHub Desktop.
Save boyswan/6d5d7c14e3927c3383842d6608729c63 to your computer and use it in GitHub Desktop.
leptos form experiment
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