diff --git a/scratch.md b/scratch.md index e8065a7..0ffd88b 100644 --- a/scratch.md +++ b/scratch.md @@ -1,136 +1,86 @@ -Forms should be built for this application with a series of JSON-type files. -Each form will have a series of sections, and each section will have a series of -questions. Each question has a type and a label - question styles may come later. -It may be possible for us to call out specific question types without needing -to parse the types separately, but that feels not-really-worth-it. +To track errors and force form input validation, we're going to create a custom `ValidatedForm` component. -Example: +This component will do two things: + - Provide a submit button (that is disabled if the form is invalid, still thinking about that) + - Define a validation method that will be called on submit that's updated on a per-input basis + (this likely looks like a `Vec>`) -``` -{ - "SectionDefinitions": [ - "SectionTitle": [ - "text": { - "label": "First name question!", - "placeholder": "John", - }, - "text": { - "label": "Last name question!", - "placeholder": "Smith", - } - ], - "Section Two": [ - "text": { - "label": "Section two testing question", - "placeholder": "Some example here", - }, - "select": { - "label": "This is a dropdown element!", - "placeholder": "option1", - "possibleValues": [ - "option1": "something", - "option2": "another something", - ] - } - ] - ] -} -``` +In order for this to work, the form will `provide_context` to the `Vec`, and then it will check this +vector on submit. It doesn't worry about the states inbetween, only during the submission logic. -...and so on. Here's a likely set of structures to use: +The inputs start off with a default state, which will be defined by that input's validation method. +This means that every input will run the validation method on mount and on each change. The first time the +function is called should be during mount, which will set the default state. This does mean that any inputs +that are marked as required will always be invalid on mount, and so we need to determine when these +error messages should be shown and updated. -``` -struct FormDefinitionDoc { - Vec
section_defs; - // and any extra values that should be applied - // across the entire form, like settings. +I think they should be shown in these situations: + - Text inputs: on blur - we don't want to scream at users as they type. + - Selectable inputs: on change and form submit - only show the message if the value is invalid, and not at mount. + +To check values for the selectable inputs at submit time, we'll probably need to provide a refresh method +that can be called on submit. This method must somehow be callable from the outside of the component and +will show the error messages. We'd likely store all this in a struct of some kind, which could contain an +`error` and a `show_message` signal. + +We already know that we can send any `Signal` to another component, so we can just send the `show_message` +signal to the parent component without any issues. We'll just use that signal to show the error messages +in the component as well. Usually we don't want to edit too many signals outside their local context, +but this is a special case – documentation is therefore incredibly important! Remember that we may also +edit the `show_message` signal within its local context as well, so we need to make sure there are no cyclical +dependencies. Alternatively, we could not edit the `show_message` signal at all locally and just use it +to trigger side effects that show messages. Have to think about this more before implementation. + +Here's a rough sketch of it all put together: +```rust +enum ValidationState { + /// A valid input! + Valid, + /// An invalid input with an error message. + Invalid(String) } -struct Section { - Vec questions; - // each section may have a series of settings including how to submit, - // when to submit, and where to submit to at the very least. +// We'll store this in a Vec> +struct InputState { + /// Determines if there is an error and provides an error message. + error: Signal, + /// Determines if the form has requested to show error messages. + show_message: RwSignal } -struct QuestionComponent { - label: String, - placeholder: String, - possible_values: HashMap, - // there are definitely values that i'll find while creating this structure. +// Define a new type for ease of use. +type ValidationVec = Vec>; + +// Parent form component +#[component] +fn Form() -> impl IntoView { + let validation_states: ValidationVec = Vec::new(); + + provide_context(validation_states); + + /* All other form logic */ +} + +fn validation_function(input: String) -> ValidationState { + // Validation logic is specific to each input type. +} + +// Child component +#[component] +fn AnyInput() -> impl IntoView { + let validation_states = use_context::().expect("Couldn't find form validation context"); + let current_input = RwSignal::new(String::new()); // Obviously, this would have a default value. + let error_signal = RwSignal::new(validation_function(current_input.get())); + let show_errors = RwSignal::new(false); // Don't show them on mount! + + // See below for InputState definition + let error_state = InputState { + error: error_signal.clone().into(), + show_message: show_errors.clone() + }; + + validation_states.push(RwSignal::new()) + + /* All other input logic */ } ``` - -Types of questions that are required, starting with the providers. - -Providers must be able to: - - - Log into the website using a username and password (handled by AWS) - - Create and manage multiple scholarships. - - Input scholarship information for each scholarship. - - View students that have applied. - - View student application information (should just be labels??) - - Buttons are a given. Duh. - -Students must be able to: - - - Log in using their Region 15 email (handled by AWS) - - Create and manage their general application. - - Create and manage applications specific to a single scholarship. - - View their acceptance status (need more confirmation) - -Administrators must be able to: - - - Log in using their Region 15 email (handled by AWS) - - Manage student scholarship applications. - - Manage scholarship provider account information. - - Create/manage scholarship information. - - Download student applications if needed. - - Statistics would be nice, but are likely a future thing. - -Right off the bad, I think I can see a few types of buttons and text inputs, -along with dropdowns, checkboxes, our custom `MultiEntry` element from last -time. That means no new options, but it does mean that the `FormDefinition` -needs to be able to keep track of all this information. - -These are some example `QuestionComponent` objects: - -Text fields: - -``` -{ - question_type: "TEXT" | "EMAIL" | "NUMBER", - label: "The question label", - name: "input_name[jsdoc]", - placeholder: "An example input" -} -``` - -Dropdowns/checkboxes/radios: - -``` -{ - question_type: "DROPDOWN" | "CHECKBOX" | "RADIO", - label: "The question label", - name: "input_name[jsdoc]", - placeholder: "The pre-selected option value", - options: [ - "optionValue": "Option Text" - ] -} -``` - -`MultiEntry`: - -``` -{ - question_type: "MULTI", - label: "The question label", - name: "input_name[jsdoc]" -} -``` - -The real question here: how can we store this information in a database? And -this means specifically within DynamoDB, because this is not an easy structure -to store in there. -That's going to be the hardest apart and I'll likely involve the AI in that -planning. diff --git a/src/components/checkbox.rs b/src/components/checkbox.rs index ba711dc..287b1a7 100644 --- a/src/components/checkbox.rs +++ b/src/components/checkbox.rs @@ -9,7 +9,7 @@ pub fn Checkbox( #[prop(into)] on_change: Callback<(), ()>, #[prop(into)] value: String, #[prop(into)] name: String, - #[prop(default = RwSignal::new(false))] disabled: RwSignal, + #[prop(into)] disabled: Signal, ) -> impl IntoView { let id = create_unique_id(&name, &value); @@ -53,7 +53,7 @@ pub fn CheckboxList( #[prop(into)] data_member: String, #[prop()] data_map: RwSignal>, #[prop()] items: Vec, - #[prop(default = RwSignal::new(false))] disabled: RwSignal, + #[prop(optional, into)] disabled: Signal, #[prop(optional, into)] label: String, ) -> impl IntoView { view! { diff --git a/src/components/select.rs b/src/components/select.rs index 9f5fe3c..aaa91c7 100644 --- a/src/components/select.rs +++ b/src/components/select.rs @@ -15,7 +15,7 @@ pub fn Select( #[prop(into)] data_member: String, #[prop()] data_map: RwSignal>, #[prop(optional, into)] label: String, - #[prop(default = RwSignal::new(false))] disabled: RwSignal, + #[prop(optional, into)] disabled: Signal, ) -> impl IntoView { let on_change = { let data_member = data_member.clone(); diff --git a/src/pages/home_page.rs b/src/pages/home_page.rs index ac81f31..a7f2ce2 100644 --- a/src/pages/home_page.rs +++ b/src/pages/home_page.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; // Server dependencies #[cfg(feature = "ssr")] use aws_sdk_dynamodb::{Client, error::ProvideErrorMetadata, types::AttributeValue}; @@ -17,10 +18,10 @@ use leptos::leptos_dom::logging::console_log; use leptos::prelude::*; use leptos_oidc::{AuthLoaded, Authenticated}; use base64::Engine; +use leptos::logging::log; use leptos::task::spawn_local; use leptos::web_sys::HtmlAnchorElement; use leptos::wasm_bindgen::JsCast; -use traits::{AsReactive, ReactiveCapture}; use crate::pages::utils::get_user_claims; /// # Get Student Info @@ -141,222 +142,208 @@ pub async fn create_example_pdf() -> Result, ServerFnError> { /// The main home page component. Contains a simple contact form. #[component] pub fn HomePage() -> impl IntoView { - // Creates a reactive value to update the button + //#region Server Resources let user_claims = get_user_claims(); + let user_subject = Memo::new(move |_| { + user_claims.get() + .map(|claim| claim.claims.subject.clone()) + }); - // Note that the value passed in MUST be equatable. - // We get/unwrap the value repeatedly until we get a simple string value, then clone it so that - // we don't lose access to it in the future, should we need it again. - let server_resource: Resource = Resource::new( - move || user_claims.get().map(|claim| claim.claims.subject.clone()), - async |opt_username| match opt_username { - Some(subject) => get_submission(subject.clone()).await.unwrap_or_else(|e| { - console_log(e.to_string().as_str()); - ExpandableInfo::new(subject) - }), - None => ExpandableInfo::new("".to_owned()), - }, + let form_data = RwSignal::new(HashMap::new()); + + let server_resource = Resource::new( + move || user_subject.get(), + async move |subject| get_submission(subject.unwrap_or_default()).await ); let submit_action = ServerAction::::new(); + + Effect::new(move || { + match server_resource.get() { + Some(Ok(submission)) => { + form_data.set(submission.data); + } + Some(Err(err)) => { + log!("Error while getting information: {:?}", err); + } + _ => { + log!("An unknown error occurred."); + } + } + }); + //#endregion + //#region Component State + let elements_disabled = Signal::derive(move || { + submit_action.pending().get() + }); + //#endregion + //#region Event logic + let on_submit = move |_| { + let mut exp_info = ExpandableInfo::new(user_subject.get().unwrap_or_default()); + exp_info.data = form_data.get(); + submit_action.dispatch(CreateSampleSubmission { + student_info: exp_info + }); + }; + + let pdf_button_click = move |_| { + console_log("Attempting to get PDF from server endpoint"); + spawn_local(async move { + let result = create_example_pdf().await; + if let Ok(bytes) = result { + let base64 = base64::engine::general_purpose::STANDARD + .encode(bytes); + let data_url = format!( + "data:application/pdf;base64,{}", + base64, + ); + console_log("Found document, opening in new tab..."); + let link = document() + .create_element("a") + .unwrap() + .dyn_into::() + .unwrap(); + link.set_href(&*data_url); + link.set_target("_blank"); + link.click(); + } + }); + }; + //#endregion view! { // Replace this fallback with a real loading screen. - {move || { server_resource .get() .map(|submission| { - let expandable_react = submission.as_reactive(); - let elements_disabled = RwSignal::new(false); - let result_msg = Signal::derive(move || { - if submit_action.pending().get() { - elements_disabled.set(true); - "Sending request...".to_owned() - } else { - elements_disabled.set(false); - if let Some(result) = submit_action.value().get() { - match result { - Ok(_) => "Request sent successfully.".to_owned(), - Err(e) => format!("Failed to send request: {:?}", e), - } - } else { - "".to_owned() - } - } - }); view! {
- -

- "Region 15 Scholarship Application (DEMO)" -

-
- -

- "This is a very simple demonstration of the scholarship application written in Rust." -

-
- - // Text { data_member, label, placeholder } - - - - - - - - - - - - - - - - - // Radio { data_member, label, items } - - - - // Checkbox { data_member, label, items } - - - - // Select { data_member, label, items } - + + + + +
} }) }} - + + "Get PDF" +