diff --git a/web/src/components/mod.rs b/web/src/components/mod.rs index 5b83ad9..17bf760 100644 --- a/web/src/components/mod.rs +++ b/web/src/components/mod.rs @@ -15,6 +15,7 @@ pub mod add_recipe; pub mod categories; pub mod footer; pub mod header; +pub mod number_field; pub mod recipe; pub mod recipe_list; pub mod recipe_plan; @@ -27,6 +28,7 @@ pub use add_recipe::*; pub use categories::*; pub use footer::*; pub use header::*; +pub use number_field::*; pub use recipe::*; pub use recipe_list::*; pub use recipe_plan::*; diff --git a/web/src/components/number_field.rs b/web/src/components/number_field.rs new file mode 100644 index 0000000..a104551 --- /dev/null +++ b/web/src/components/number_field.rs @@ -0,0 +1,74 @@ +// Copyright 2023 Jeremy Wall (Jeremy@marzhilsltudios.com) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use sycamore::prelude::*; +use tracing::debug; +use web_sys::{Event, HtmlInputElement}; + +use crate::js_lib; + +#[derive(Props)] +pub struct NumberProps<'ctx, F> +where + F: Fn(Event), +{ + name: String, + on_change: Option, + min: i32, + counter: &'ctx Signal, +} + +#[component] +pub fn NumberField<'ctx, F, G: Html>(cx: Scope<'ctx>, props: NumberProps<'ctx, F>) -> View +where + F: Fn(web_sys::Event) + 'ctx, +{ + let NumberProps { + name, + on_change, + min, + counter, + } = props; + + let id = name.clone(); + let inc_target_id = id.clone(); + let dec_target_id = id.clone(); + let min_field = format!("{}", min); + + view! {cx, + div() { + input(type="number", id=id, name=name, class="item-count-sel", min=min_field, max="99", step="1", bind:value=counter, on:input=move |evt| { + on_change.as_ref().map(|f| f(evt)); + }) + span(class="item-count-inc-dec", on:click=move |_| { + let i: i32 = counter.get_untracked().parse().unwrap(); + let target = js_lib::get_element_by_id::(&inc_target_id).unwrap().expect(&format!("No such element with id {}", inc_target_id)); + counter.set(format!("{}", i+1)); + debug!(counter=%(counter.get_untracked()), "set counter to new value"); + // We force an input event to get triggered for our target. + target.dispatch_event(&web_sys::Event::new("input").expect("Failed to create new event")).expect("Failed to dispatch event to target"); + }) { "▲" } + " " + span(class="item-count-inc-dec", on:click=move |_| { + let i: i32 = counter.get_untracked().parse().unwrap(); + let target = js_lib::get_element_by_id::(&dec_target_id).unwrap().expect(&format!("No such element with id {}", dec_target_id)); + if i > min { + counter.set(format!("{}", i-1)); + debug!(counter=%(counter.get_untracked()), "set counter to new value"); + // We force an input event to get triggered for our target. + target.dispatch_event(&web_sys::Event::new("input").expect("Failed to create new event")).expect("Failed to dispatch event to target"); + } + }) { "▼" } + } + } +} diff --git a/web/src/components/recipe_selection.rs b/web/src/components/recipe_selection.rs index e3a09f8..7be063f 100644 --- a/web/src/components/recipe_selection.rs +++ b/web/src/components/recipe_selection.rs @@ -17,6 +17,7 @@ use sycamore::prelude::*; use tracing::{debug, instrument}; use crate::app_state::{Message, StateHandler}; +use crate::components::NumberField; #[derive(Props)] pub struct RecipeCheckBoxProps<'ctx> { @@ -37,26 +38,32 @@ pub fn RecipeSelection<'ctx, G: Html>( let RecipeCheckBoxProps { i, title, sh } = props; let id = Rc::new(i); let id_clone = id.clone(); - let count = create_signal( - cx, - sh.get_value( - |state| match state.get_untracked().recipe_counts.get(id_clone.as_ref()) { - Some(count) => format!("{}", count), - None => "0".to_owned(), - }, - ), - ); + let id_for_count = id.clone(); + let current_count = sh.get_selector(cx, move |state| { + *state + .get() + .recipe_counts + .get(id_for_count.as_ref()) + .unwrap() + }); + let count = create_signal(cx, format!("{}", *current_count.get_untracked())); + create_effect(cx, || { + let updated_count = format!("{}", current_count.get()); + if updated_count != count.get_untracked().as_ref() { + count.set(updated_count); + } + }); let title = title.get().clone(); - let for_id = id.clone(); let href = format!("/ui/recipe/view/{}", id); let name = format!("recipe_id:{}", id); + let for_id = name.clone(); view! {cx, div() { label(for=for_id) { a(href=href) { (*title) } } - input(type="number", class="item-count-sel", min="0", bind:value=count, name=name, on:change=move |_| { - debug!(idx=%id, count=%(*count.get()), "setting recipe count"); - sh.dispatch(cx, Message::UpdateRecipeCount(id.as_ref().clone(), count.get().parse().expect("Count is not a valid usize"))); - }) + NumberField(name=name, counter=count, min=0, on_change=Some(move |_| { + debug!(idx=%id, count=%(*count.get_untracked()), "setting recipe count"); + sh.dispatch(cx, Message::UpdateRecipeCount(id.as_ref().clone(), count.get_untracked().parse().expect("Count is not a valid usize"))); + })) } } } diff --git a/web/src/js_lib.rs b/web/src/js_lib.rs index c8081c3..286cf67 100644 --- a/web/src/js_lib.rs +++ b/web/src/js_lib.rs @@ -12,7 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. use js_sys::Date; -use web_sys::{window, Storage}; +use wasm_bindgen::JsCast; +use web_sys::{window, Element, Storage}; pub fn get_storage() -> Storage { window() @@ -25,3 +26,18 @@ pub fn get_storage() -> Storage { pub fn get_ms_timestamp() -> u32 { Date::new_0().get_milliseconds() } + +pub fn get_element_by_id(id: &str) -> Result, Element> +where + E: JsCast, +{ + match window() + .expect("No window present") + .document() + .expect("No document in window") + .get_element_by_id(id) + { + Some(e) => e.dyn_into::().map(|e| Some(e)), + None => Ok(None), + } +} diff --git a/web/static/app.css b/web/static/app.css index e250842..ea0c443 100644 --- a/web/static/app.css +++ b/web/static/app.css @@ -18,6 +18,7 @@ --tab-border-width: 3px; --tab-border-style: solid; --tab-border-radius: 15px; + --unicode-button-size: 2em; } @media print { @@ -71,4 +72,8 @@ nav>h1 { .destructive { background-color: firebrick !important; +} + +.item-count-inc-dec { + font-size: var(--unicode-button-size); } \ No newline at end of file