Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pipeline.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
- uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: -p vertigo-forms-storybook -p vertigo-forms-example-form --all-features --target wasm32-unknown-unknown -- -Dwarnings
args: -p vertigo-forms-storybook -p vertigo-forms-example-manual-form -p vertigo-forms-example-model-form --all-features --target wasm32-unknown-unknown -- -Dwarnings
name: Storybook/Examples Clippy Output

nightly-tests:
Expand Down
2 changes: 1 addition & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<!-- markdownlint-configure-file { "no-duplicate-heading": { "siblings_only": true } } -->

<!-- markdownlint-disable-next-line first-line-h1 -->
## 0.1.0 - Unreleased
## 0.1.0 - 2025-09-17

### Added

Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[workspace]
members = ["storybook", "examples/form"]
members = ["storybook", "examples/manual_form", "examples/model_form"]

[package]
name = "vertigo-forms"
Expand Down
72 changes: 67 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ Dependencies:

```toml
vertigo = "0.8"
vertigo-forms = { git = "https://github.com/vertigo-web/vertigo-forms" }
vertigo-forms = "0.1"
```

Example:
Example 1:

```rust
use vertigo::{computed_tuple, main, prelude::*};
Expand Down Expand Up @@ -50,9 +50,7 @@ fn render() -> DomNode {
engine => form_data.engine,
year => form_data.year
)
.map(|(brand, model, engine, year)|
format!("{} {}, {} ({})", brand, model, engine, year)
);
.map(|(brand, model, engine, year)| format!("{brand} {model}, {engine} ({year})"));

dom! {
<html>
Expand All @@ -73,6 +71,70 @@ fn render() -> DomNode {
}
```

Example 2:

```rust
use vertigo::{bind_rc, main, prelude::*};
use vertigo_forms::form::{DataSection as DS, FormData, FormExport, ModelForm};

#[derive(Clone, Default, PartialEq)]
struct Car {
brand: String,
model: String,
year: String,
engine: String,
}

impl From<Car> for FormData {
fn from(value: Car) -> Self {
FormData::default()
.add_top_controls()
.with(DS::with_string_field("Brand", "brand", &value.brand))
.with(DS::with_string_field("Model", "model", &value.model))
.with(DS::with_string_field("Year", "year", &value.year))
.with(DS::new("Engine").add_list_field(
"engine",
Some(&value.engine),
vec!["petrol".into(), "diesel".into(), "electric".into()],
))
}
}

impl From<FormExport> for Car {
fn from(value: FormExport) -> Self {
vertigo::log::info!("FormExport {}", value.get_string("engine"));
Self {
brand: value.get_string("brand"),
model: value.get_string("model"),
year: value.get_string("year"),
engine: value.list_or_default("engine"),
}
}
}

#[main]
fn render() -> DomNode {
let car = Value::new(Car::default());

let data_formatted =
car.map(|car| format!("{} {}, {} ({})", car.brand, car.model, car.engine, car.year));

let on_submit = bind_rc!(car, |new_model: Car| {
car.set(new_model);
});

dom! {
<html>
<head />
<body>
<ModelForm model={car} {on_submit} params={} />
<p>"Form data: " {data_formatted}</p>
</body>
</html>
}
}
```

## Storybook App

### Prepare
Expand Down
12 changes: 12 additions & 0 deletions examples/manual_form/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "vertigo-forms-example-manual-form"
version = "0.1.0"
authors = ["Michał Pokrywka <wolfmoon@o2.pl>"]
edition = "2024"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
vertigo = "0.8"
vertigo-forms = { path = "../.." }
File renamed without changes.
4 changes: 2 additions & 2 deletions examples/form/Cargo.toml → examples/model_form/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
[package]
name = "vertigo-forms-example-form"
name = "vertigo-forms-example-model-form"
version = "0.1.0"
authors = ["Michał Pokrywka <wolfmoon@o2.pl>"]
edition = "2021"
edition = "2024"

[lib]
crate-type = ["cdylib", "rlib"]
Expand Down
59 changes: 59 additions & 0 deletions examples/model_form/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use vertigo::{bind_rc, main, prelude::*};
use vertigo_forms::form::{DataSection as DS, FormData, FormExport, ModelForm};

#[derive(Clone, Default, PartialEq)]
struct Car {
brand: String,
model: String,
year: String,
engine: String,
}

impl From<Car> for FormData {
fn from(value: Car) -> Self {
FormData::default()
.add_top_controls()
.with(DS::with_string_field("Brand", "brand", &value.brand))
.with(DS::with_string_field("Model", "model", &value.model))
.with(DS::with_string_field("Year", "year", &value.year))
.with(DS::new("Engine").add_list_field(
"engine",
Some(&value.engine),
vec!["petrol".into(), "diesel".into(), "electric".into()],
))
}
}

impl From<FormExport> for Car {
fn from(value: FormExport) -> Self {
vertigo::log::info!("FormExport {}", value.get_string("engine"));
Self {
brand: value.get_string("brand"),
model: value.get_string("model"),
year: value.get_string("year"),
engine: value.list_or_default("engine"),
}
}
}

#[main]
fn render() -> DomNode {
let car = Value::new(Car::default());

let data_formatted =
car.map(|car| format!("{} {}, {} ({})", car.brand, car.model, car.engine, car.year));

let on_submit = bind_rc!(car, |new_model: Car| {
car.set(new_model);
});

dom! {
<html>
<head />
<body>
<ModelForm model={car} {on_submit} params={} />
<p>"Form data: " {data_formatted}</p>
</body>
</html>
}
}
79 changes: 43 additions & 36 deletions src/form/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
//! See story book for examples.

use std::rc::Rc;
use vertigo::{AttrGroup, Css, Value, bind, bind_rc, component, css, dom};
use vertigo::{AttrGroup, Computed, Css, Value, bind, bind_rc, component, css, dom};

use crate::{TabsParams, ValidationErrors};

Expand All @@ -26,7 +26,7 @@ pub struct FormParams<T: 'static> {
pub delete_label: Rc<String>,
pub validate: Option<ValidateFunc<T>>,
pub validation_errors: Value<ValidationErrors>,
pub operation: Value<Operation>,
pub operation: Option<Value<Operation>>,
pub saving_label: Rc<String>,
pub saved_label: Rc<String>,
pub tabs_params: Option<TabsParams>,
Expand Down Expand Up @@ -63,33 +63,35 @@ impl<T: 'static> Default for FormParams<T> {
///
/// Use `f` attribute group to pass anything to underlying <form> element (ex. `f:css="my_styles"`)
#[component]
pub fn ModelForm<'a, T>(
model: &'a T,
pub fn ModelForm<T: Clone + PartialEq>(
model: Computed<T>,
on_submit: Rc<dyn Fn(T)>,
params: FormParams<T>,
f: AttrGroup,
s: AttrGroup,
) where
FormData: From<&'a T>,
FormData: From<T>,
T: From<FormExport> + 'static,
{
let form_data = Rc::new(FormData::from(model));
model.render_value(move |model| {
let form_data = Rc::new(FormData::from(model));

let on_submit = bind_rc!(form_data, |form_export: FormExport| {
on_submit(T::from(form_export));
});
let on_submit = bind_rc!(on_submit, form_data, |form_export: FormExport| {
on_submit(T::from(form_export));
});

let mut form_component = Form {
form_data,
on_submit,
params,
}
.into_component();
let mut form_component = Form {
form_data,
on_submit,
params: params.clone(),
}
.into_component();

form_component.f = f;
form_component.s = s;
form_component.f = f.clone();
form_component.s = s.clone();

form_component.mount()
form_component.mount()
})
}

/// Renders a form for provided [FormData] that upon "Save" allows to grab updated fields from [FormExport].
Expand Down Expand Up @@ -141,23 +143,25 @@ pub fn Form<T>(
let errors = validation_errors
.render_value_option(|errs| errs.get("submit").map(|err| dom! { <span>{err}</span> }));

let operation_str = bind!(
params.saving_label,
params.saved_label,
params.operation.render_value_option(move |oper| {
let mut css = ctrl_item_css.clone();
match oper {
Operation::Saving => Some(saving_label.clone()),
Operation::Success => Some(saved_label.clone()),
Operation::Error(err) => {
css += css! {"color: red;"};
Some(err)
let operation_str = params.operation.as_ref().map(|operation| {
bind!(
params.saving_label,
params.saved_label,
operation.render_value_option(move |oper| {
let mut css = ctrl_item_css.clone();
match oper {
Operation::Saving => Some(saving_label.clone()),
Operation::Success => Some(saved_label.clone()),
Operation::Error(err) => {
css += css! {"color: red;"};
Some(err)
}
_ => None,
}
_ => None,
}
.map(|operation_str| dom! { <span {css}>{operation_str}</span> })
})
);
.map(|operation_str| dom! { <span {css}>{operation_str}</span> })
})
)
});

if controls.is_empty() {
None
Expand All @@ -170,7 +174,7 @@ pub fn Form<T>(
<div css={css_controls}>
{..controls}
{errors}
{operation_str}
{..operation_str}
</div>
})
}
Expand Down Expand Up @@ -200,7 +204,10 @@ pub fn Form<T>(
let form_css = params.css + params.add_css;

let on_submit = bind_rc!(form_data, validation_errors, || {
params.operation.set(Operation::Saving);
params
.operation
.as_ref()
.inspect(|operation| operation.set(Operation::Saving));
let model = form_data.export();
let valid = if let Some(validate) = &params.validate {
validate(&model.clone().into(), validation_errors.clone())
Expand Down
2 changes: 1 addition & 1 deletion storybook/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "vertigo-forms-storybook"
version = "0.1.0"
authors = ["Michał Pokrywka <wolfmoon@o2.pl>"]
edition = "2021"
edition = "2024"

[lib]
crate-type = ["cdylib", "rlib"]
Expand Down
25 changes: 9 additions & 16 deletions storybook/src/form/form1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ pub struct MyModel {
pub dimension_y: String,
}

impl From<&MyModel> for FormData {
fn from(value: &MyModel) -> Self {
impl From<MyModel> for FormData {
fn from(value: MyModel) -> Self {
Self::default()
.with(DataSection::with_string_field("Slug", "slug", &value.slug))
.with(DataSection::with_string_field("Name", "name", &value.name))
Expand Down Expand Up @@ -47,28 +47,21 @@ pub fn Form1() {
dimension_y: "80".to_string(),
});

let my_model_clone = my_model.clone();
let form = my_model.render_value(move |model| {
let on_submit = bind_rc!(my_model_clone, |new_model: MyModel| {
my_model_clone.set(new_model);
});
let on_submit = bind_rc!(my_model, |new_model: MyModel| {
my_model.set(new_model);
});

dom! {
dom! {
<div>
<h4>"Form 1:"</h4>
<ModelForm
model={&&model}
model={my_model.clone()}
{on_submit}
params={FormParams {
add_css: css! {"width: 400px;"},
..Default::default()
}}
/>
}
});

dom! {
<div>
<h4>"Form 1:"</h4>
{form}
<h4>"Model 1:"</h4>
<p>{my_model.map(|m| m.slug)} " / " {my_model.map(|m| m.name)}</p>
<p>{my_model.map(|m| m.dimension_x)} "x" {my_model.map(|m| m.dimension_y)}</p>
Expand Down
Loading
Loading