Skip to content

Commit

Permalink
Add an example of subforms
Browse files Browse the repository at this point in the history
  • Loading branch information
Justin Wernick committed Feb 10, 2021
1 parent 06d9be1 commit a975067
Show file tree
Hide file tree
Showing 3 changed files with 242 additions and 3 deletions.
3 changes: 1 addition & 2 deletions README.md
Expand Up @@ -85,8 +85,7 @@ TODO: Document validation rules using the newtype pattern
- [Submit attempted tracking](./structform/tests/submit_attempted_example.rs)
- [Custom submit function](./structform/tests/custom_submit_function_example.rs)
- [Validation rules](./structform/tests/validation_example.rs)
- TODO: Subforms example
- TODO: Optional subforms example
- [Subforms and optional subforms](./structform/tests/subforms_example.rs)
- TODO: List of subforms example

## License
Expand Down
2 changes: 1 addition & 1 deletion structform/tests/login_example.rs
Expand Up @@ -27,7 +27,7 @@ struct LoginForm {

// Apart from deriving the StructForm trait, this will also create an
// enum for us to refer to the various fields. The derived code will look like this:
// ```rust
// ```
// pub enum LoginFormField {
// Username,
// Password,
Expand Down
240 changes: 240 additions & 0 deletions structform/tests/subforms_example.rs
@@ -0,0 +1,240 @@
use structform::{
derive_form_input, impl_text_input_with_stringops, ParseAndFormat, ParseError, StructForm,
};

// This example shows creating forms over nested data structures.

// This example builds on the [login example](./login_example.rs).
// This example is written assuming that you're already familiar with
// the login example, so if not please refer to that first.

// Often for larger forms, the strongly typed model isn't just a flat
// series of fields. It often has nested structs, like the addresses
// here.

#[derive(Default, Debug, PartialEq, Eq)]
struct UserDetails {
username: String,
primary_address: Address,
secondary_address: Option<Address>,
}

#[derive(Default, Clone, Debug, PartialEq, Eq)]
struct Address {
street_address: String,
city: String,
country: String,
}

// When we create our StructForm for capturing these user details, we
// need a form for both UserDetails and Address. The Address form is
// included in the UserDetails form as a subform. The derive macro can
// automatically identify optional subforms, but it needs the
// `#[structform(subform)]` annotation to help it identify required
// subforms.

#[derive(Default, Clone, StructForm)]
#[structform(model = "UserDetails")]
struct UserDetailsForm {
username: FormTextInput<String>,
#[structform(subform)]
primary_address: AddressForm,
secondary_address: Option<AddressForm>,
}

#[derive(Default, Clone, StructForm)]
#[structform(model = "Address")]
struct AddressForm {
street_address: FormTextInput<String>,
city: FormTextInput<String>,
country: FormTextInput<String>,
}

// These two derivations of StructForms generates the following field definitions:
// ```
// pub enum UserDetailsFormField {
// Username,
// PrimaryAddress(AddressFormField),
// ToggleSecondaryAddress,
// SecondaryAddress(AddressFormField),
// }
// pub enum AddressFormField {
// StreetAddress,
// City,
// Country,
// }
// ```

// These inputs are the same as the login example. See that example
// for more details.

derive_form_input! {FormTextInput}
impl_text_input_with_stringops!(FormTextInput, String);

#[test]
fn set_input_delegates_to_subform() {
let mut form = UserDetailsForm::default();

// The `UserDetailsFormField` has one field for the whole of a
// required subform, that can contain any of the subform's fields.

assert_eq!(form.primary_address.city.value, Err(ParseError::Required));
form.set_input(
UserDetailsFormField::PrimaryAddress(AddressFormField::City),
"Johannesburg".to_string(),
);
assert_eq!(
form.primary_address.city.value,
Ok("Johannesburg".to_string())
);
}

#[test]
fn optional_subforms_can_be_toggled_on_and_off() {
let mut form = UserDetailsForm::default();

// The `UserDetailsFormField` has two fields for an optional
// subform: one that toggles it between `Some` and `None`, and
// another that sends data.

// By default, an optional subform will not be included.
assert!(form.secondary_address.is_none());

// You can send `set_update` for the secondary form, and it won't
// crash, but it also won't do anything. Actually doing this is
// probably a logic error in your frontend.
form.set_input(
UserDetailsFormField::SecondaryAddress(AddressFormField::City),
"Johannesburg".to_string(),
);
assert!(form.secondary_address.is_none());

// Rather before using the secondary address, you need to toggle
// it to `Some`. In this case, the string passed to set_input is
// ignored. It works well if you tie this message to the changed
// event on an HTML checkbox, and only show the rest of the
// secondary address in your HTML if `secondary_address` is Some.
form.set_input(UserDetailsFormField::ToggleSecondaryAddress, "".to_string());
assert!(form.secondary_address.is_some());
assert_eq!(
form.secondary_address.as_ref().unwrap().city.value,
Err(ParseError::Required)
);

// Now that we've toggled secondary_address to Some, we can fill
// in its fields.
form.set_input(
UserDetailsFormField::SecondaryAddress(AddressFormField::City),
"Johannesburg".to_string(),
);
assert_eq!(
form.secondary_address.as_ref().unwrap().city.value,
Ok("Johannesburg".to_string())
);
}

#[test]
fn the_subforms_are_populated_when_initializing_from_an_existing_model() {
// If you're editing an existing model, you can construct your
// StructForm from that model. Subforms will also be prepopulated
// appropriately.

let model = UserDetails {
username: "justin".to_string(),
primary_address: Address {
street_address: "123 StructForm Drive".to_string(),
city: "Johannesburg".to_string(),
country: "South Africa".to_string(),
},
secondary_address: Some(Address {
street_address: "321 StructForm Laan".to_string(),
city: "Pretoria".to_string(),
country: "South Africa".to_string(),
}),
};

let form = UserDetailsForm::new(&model);

assert_eq!(form.username.input, "justin".to_string());
assert_eq!(
form.primary_address.street_address.input,
"123 StructForm Drive".to_string()
);
assert!(form.secondary_address.is_some());
assert_eq!(
form.secondary_address.unwrap().street_address.input,
"321 StructForm Laan".to_string()
);
}

#[test]
fn the_whole_form_can_be_completed() {
let mut form = UserDetailsForm::default();

form.set_input(UserDetailsFormField::Username, "justin".to_string());

// Any required fields in subforms are also required to submit the
// main form.
assert_eq!(form.submit(), Err(ParseError::Required));

form.set_input(
UserDetailsFormField::PrimaryAddress(AddressFormField::StreetAddress),
"123 StructForm Drive".to_string(),
);
form.set_input(
UserDetailsFormField::PrimaryAddress(AddressFormField::City),
"Johannesburg".to_string(),
);
form.set_input(
UserDetailsFormField::PrimaryAddress(AddressFormField::Country),
"South Africa".to_string(),
);

// Optional subforms are not required
assert_eq!(
form.submit(),
Ok(UserDetails {
username: "justin".to_string(),
primary_address: Address {
street_address: "123 StructForm Drive".to_string(),
city: "Johannesburg".to_string(),
country: "South Africa".to_string(),
},
secondary_address: None,
})
);

// However, if an optional subform is toggled to Some, it is required.
form.set_input(UserDetailsFormField::ToggleSecondaryAddress, "".to_string());
assert_eq!(form.submit(), Err(ParseError::Required));

form.set_input(
UserDetailsFormField::SecondaryAddress(AddressFormField::StreetAddress),
"321 StructForm Laan".to_string(),
);
form.set_input(
UserDetailsFormField::SecondaryAddress(AddressFormField::City),
"Pretoria".to_string(),
);
form.set_input(
UserDetailsFormField::SecondaryAddress(AddressFormField::Country),
"South Africa".to_string(),
);

assert_eq!(
form.submit(),
Ok(UserDetails {
username: "justin".to_string(),
primary_address: Address {
street_address: "123 StructForm Drive".to_string(),
city: "Johannesburg".to_string(),
country: "South Africa".to_string(),
},
secondary_address: Some(Address {
street_address: "321 StructForm Laan".to_string(),
city: "Pretoria".to_string(),
country: "South Africa".to_string(),
}),
})
);
}

0 comments on commit a975067

Please sign in to comment.