From 4832b610b1507719a4c5b51d946e6c42a61e9cc3 Mon Sep 17 00:00:00 2001 From: kostya Date: Wed, 5 Jun 2024 14:05:47 -0700 Subject: [PATCH 01/58] migration to 0.21 --- Cargo.toml | 9 +- examples/basic/Cargo.toml | 8 +- examples/basic/index.html | 2 +- examples/basic/src/main.rs | 104 +++++++++++++++++ src/components/dropdown.rs | 5 +- src/components/modal.rs | 212 +++++++++++++++++++++++++++-------- src/components/navbar.rs | 21 +++- src/components/pagination.rs | 8 +- src/elements/button.rs | 6 +- src/form/input.rs | 2 + src/lib.rs | 9 +- 11 files changed, 327 insertions(+), 59 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a9f221d..8b29ae2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,11 +13,12 @@ keywords = ["wasm", "web", "bulma", "sass", "yew"] [dependencies] derive_more = { version = "0.99.17", default-features = false, features = ["display"] } web-sys = { version = "0.3.61", features = ["Element", "File", "HtmlCollection", "HtmlSelectElement"] } -yew = { version = "0.20.0", features = ["csr"] } -yew-agent = "0.2.0" -yew-router = { version = "0.17.0", optional = true } -wasm-bindgen = "0.2.84" +yew = { version = "0.21.0", features = ["csr"] } +yew-agent = "0.3.0" +yew-router = { version = "0.18.0", optional = true } +wasm-bindgen = "0.2.87" serde = { version = "1.0.152", features = ["derive"] } +gloo-console = "0.3.0" [features] default = ["router"] diff --git a/examples/basic/Cargo.toml b/examples/basic/Cargo.toml index 1118d5d..e0c9dab 100644 --- a/examples/basic/Cargo.toml +++ b/examples/basic/Cargo.toml @@ -6,10 +6,14 @@ edition = "2018" [dependencies] console_error_panic_hook = "0.1" -gloo-console = "0.2" +gloo-console = "0.3.0" wasm-bindgen = "0.2" ybc = { path = "../../" } -yew = "0.20" +yew = { version = "0.21.0", features = ["csr"] } +yew-agent = "0.3.0" +postcard = "1" +serde = { version = "1", features = ["derive"] } +js-sys = "0.3" [features] default = [] diff --git a/examples/basic/index.html b/examples/basic/index.html index fdc3571..2b9bf08 100644 --- a/examples/basic/index.html +++ b/examples/basic/index.html @@ -4,7 +4,7 @@ Trunk | Yew | YBC - + diff --git a/examples/basic/src/main.rs b/examples/basic/src/main.rs index 3c8e93c..e7ee73c 100644 --- a/examples/basic/src/main.rs +++ b/examples/basic/src/main.rs @@ -1,14 +1,20 @@ #![recursion_limit = "1024"] +use std::rc::Rc; use console_error_panic_hook::set_once as set_panic_hook; use wasm_bindgen::prelude::*; use ybc::TileCtx::{Ancestor, Child, Parent}; use yew::prelude::*; + +use ybc::{ NavBurgerCloserState}; + #[function_component(App)] pub fn app() -> Html { + let state = Rc::new(NavBurgerCloserState { total_clicks: 0 }); html! { <> + > context={state}> Html { }} /> + >> Html { {"YBC"}

{"A Yew component library based on the Bulma CSS framework."}

+ + + + + +
@@ -97,3 +110,94 @@ fn main() { yew::Renderer::::new().render(); } + + +use ybc::ModalCloserProvider; +use ybc::ModalCloserContext; + + +#[function_component] +pub fn MyModal1() -> Html { + let msg_ctx = use_context::().unwrap(); + let onclick = { + Callback::from(move |e:MouseEvent| msg_ctx.dispatch( "id0-close".to_string())) + }; + let on_click_cb = Callback::from(move |e: AttrValue| { + gloo_console::log!("Button clicked!"); + }); + html! { + + {"Open Modal"} + + }} + // on_clicked={on_click_cb} + body={ + html!{ + +

{"This is the body of the modal."}

+
+ } + } + footer={html!( + <> + + {"Save changes"} + + + {"Close"} + + + )} + /> + } +} + +#[function_component] +pub fn MyModal2() -> Html { + let msg_ctx = use_context::().unwrap(); + let onclick = { + Callback::from(move |e:MouseEvent| msg_ctx.dispatch( "id2-close".to_string())) + }; + let msg_ctx2 = use_context::().unwrap(); + let onsave = { + Callback::from(move |e:MouseEvent| msg_ctx2.dispatch( "id2-close".to_string())) + }; + let on_click_cb = Callback::from(move |e: AttrValue| { + gloo_console::log!("Button clicked!"); + }); + html! { + + {"Open Modal"} + + }} + // on_clicked={on_click_cb} + body={ + html!{ + +

{"This is the body of the modal2."}

+
+ } + } + footer={html!( + <> + + {"Save changes"} + + + {"Close"} + + + )} + /> + } +} \ No newline at end of file diff --git a/src/components/dropdown.rs b/src/components/dropdown.rs index fe8ec5a..a56a357 100644 --- a/src/components/dropdown.rs +++ b/src/components/dropdown.rs @@ -61,7 +61,10 @@ impl Component for Dropdown { class.push("is-hoverable"); Callback::noop() } else { - ctx.link().callback(|_| DropdownMsg::Open) + ctx.link().callback(|event: MouseEvent| { + event.prevent_default(); + DropdownMsg::Open + }) }; let overlay = if self.is_menu_active { class.push("is-active"); diff --git a/src/components/modal.rs b/src/components/modal.rs index a86ffbd..11ecbb8 100644 --- a/src/components/modal.rs +++ b/src/components/modal.rs @@ -1,10 +1,12 @@ use std::collections::HashSet; - -use serde::{Deserialize, Serialize}; +use std::rc::Rc; +use wasm_bindgen::JsCast; use yew::prelude::*; -use yew_agent::{use_bridge, HandlerId, Public, UseBridgeHandle, Worker, WorkerLink}; +use yew_agent::worker::{HandlerId, Worker, WorkerScope}; + +use yew_agent::prelude::*; /// Modal actions. pub enum ModalMsg { @@ -13,6 +15,8 @@ pub enum ModalMsg { CloseFromAgent(ModalCloseMsg), } +pub type ModalCloserContext = UseReducerHandle; + #[derive(Clone, Debug, Properties, PartialEq)] pub struct ModalProps { /// The ID of this modal, used for triggering close events from other parts of the app. @@ -36,34 +40,33 @@ pub struct ModalProps { #[function_component(Modal)] pub fn modal(props: &ModalProps) -> Html { let is_active = use_state(|| false); - + let id = props.id.clone(); + let closer_ctx = use_context::().expect("Modal closer in context"); let mut class = Classes::from("modal"); class.push(props.classes.clone()); + let (_id, closed) = match closer_ctx.0.contains("-") { + true => { + let result = closer_ctx.0.split("-").collect::>(); + (result[0], result[1] == "close") + } + false => (closer_ctx.0.as_str(), false), + }; - let (opencb, closecb) = if *is_active { + let (opencb, closecb) = if _id == id && *is_active { class.push("is-active"); let is_active = is_active.clone(); (Callback::noop(), Callback::from(move |_| is_active.set(false))) - } else { + } else if _id == id { let is_active = is_active.clone(); (Callback::from(move |_| is_active.set(true)), Callback::noop()) + } else { + (Callback::noop(), Callback::noop()) }; - { - let id = props.id.clone(); - - let _bridge: UseBridgeHandle = use_bridge(move |response: ModalCloseMsg| { - if response.0 == id { - is_active.set(false); - } else { - } - }); - } - html! { <>
@@ -85,9 +88,9 @@ pub fn modal(props: &ModalProps) -> Html { #[derive(Clone, Debug, Properties, PartialEq)] pub struct ModalCardProps { /// The ID of this modal, used for triggering close events from other parts of the app. - pub id: String, + pub id: AttrValue, /// The title of this modal. - pub title: String, + pub title: AttrValue, /// The content to be placed in the `modal-card-body` not including the modal-card-header / /// modal-card-title, which is handled by the `modal_title` prop. #[prop_or_default] @@ -110,34 +113,130 @@ pub struct ModalCardProps { /// in your app for maximum flexibility. #[function_component(ModalCard)] pub fn modal_card(props: &ModalCardProps) -> Html { + let id = props.id.clone(); + let closer_ctx = use_context::().expect("Modal closer in context"); + + // gloo_console::log!("closer_ctx: full ID {}", closer_ctx.0.as_str()); + + let (_id, closed) = match closer_ctx.0.contains("-") { + true => { + let result = closer_ctx.0.split("-").collect::>(); + (result[0], result[1] == "close") + } + false => (closer_ctx.0.as_str(), false), + }; let is_active = use_state(|| false); + // gloo_console::log!("closer_ctx: {:?} id:{:?} is closed:", _id.clone().into(), id.clone().into(), closed); + + if _id == id && closed { + is_active.set(false); + closer_ctx.dispatch(id.clone()); + // gloo_console::log!("closed!"); + } + let mut class = Classes::from("modal"); class.push(props.classes.clone()); - let (opencb, closecb) = if *is_active { + let (opencb, closecb) = if _id == id && *is_active { + class.push("is-active"); + // gloo_console::log!("is_active=true call"); let is_active = is_active.clone(); - (Callback::noop(), Callback::from(move |_| is_active.set(false))) - } else { + (Callback::noop(), Callback::from(move |e:MouseEvent| { + let target = e.target(); + gloo_console::log!("Close event from modal-card: {:?}"); + // Check if the target is an element that you want to ignore + if let Some(target) = target { + let target_element = target.dyn_into::().unwrap(); + if target_element.id().starts_with("modal-ignore-") { + // If the target is an element to ignore, stop the event propagation + e.stop_propagation(); + gloo_console::log!("Ignoring event"); + return; + } + } + is_active.set(false) + })) + } else if _id == id { let is_active = is_active.clone(); - + gloo_console::log!("is_active=false call"); (Callback::from(move |_| is_active.set(true)), Callback::noop()) + } else { + gloo_console::log!("NOOP call"); + (Callback::noop(), Callback::noop()) + }; + + html! { + <> +
+ {props.trigger.clone()} +
+
+ + + +
+ + } +} + +#[function_component(ModalCard2)] +pub fn modal_card2(props: &ModalCardProps) -> Html { + let id = props.id.clone(); + let closer_ctx = use_context::().expect("Modal closer in context"); + + // gloo_console::log!("closer_ctx: full ID {}", closer_ctx.0.as_str()); + let action = closer_ctx.0.as_str(); + let (_id, closed) = match action.contains("-") { + true => { + let result = action.split("-").collect::>(); + (result[0], result[1] == "close") + } + false => (action, false), }; + let is_active = use_state(|| false); - { - let id = props.id.clone(); + // gloo_console::log!("closer_ctx: {:?} id:{:?} is closed:", _id.clone().into(), id.clone().into(), closed); - let _bridge: UseBridgeHandle = use_bridge(move |response: ModalCloseMsg| { - if response.0 == id { - is_active.set(false); - } else { - } - }); + if _id == id && closed { + is_active.set(false); + closer_ctx.dispatch(id.clone()); + gloo_console::log!("closed!"); } + let mut class = Classes::from("modal"); + class.push(props.classes.clone()); + + let (opencb, closecb) = if _id == id && *is_active { + class.push("is-active"); + gloo_console::log!("is_active=true call"); + + let is_active = is_active.clone(); + + (Callback::noop(), Callback::from(move |_| is_active.set(false))) + } else if _id == id { + let is_active = is_active.clone(); + gloo_console::log!("is_active=false call"); + (Callback::from(move |_| is_active.set(true)), Callback::noop()) + } else { + gloo_console::log!("NOOP call"); + (Callback::noop(), Callback::noop()) + }; + html! { <>
@@ -169,8 +268,16 @@ pub fn modal_card(props: &ModalCardProps) -> Html { /// /// The ID provided in this message must match the ID of the modal which is to be closed, else /// the message will be ignored. -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct ModalCloseMsg(pub String); +#[derive(Clone, Debug, PartialEq)] +pub struct ModalCloseMsg(pub AttrValue); + +impl Reducible for ModalCloseMsg { + type Action = AttrValue; + + fn reduce(self: Rc, action: Self::Action) -> Rc { + ModalCloseMsg { 0: action }.into() + } +} /// An agent used for being able to close `Modal` & `ModalCard` instances by ID. /// @@ -213,7 +320,7 @@ pub struct ModalCloseMsg(pub String); /// This pattern allows you to communicate with a modal by its given ID, allowing /// you to close the modal from anywhere in your application. pub struct ModalCloser { - link: WorkerLink, + link: WorkerScope, subscribers: HashSet, } @@ -222,27 +329,42 @@ impl Worker for ModalCloser { type Message = (); // The agent receives requests to close modals by ID. type Output = ModalCloseMsg; - type Reach = Public; // The agent forwards the input to all registered modals. - fn create(link: WorkerLink) -> Self { - Self { link, subscribers: HashSet::new() } + fn create(link: &WorkerScope) -> Self { + Self { link: link.clone(), subscribers: HashSet::new() } + } + + fn update(&mut self, scope: &WorkerScope, _: Self::Message) {} + + fn connected(&mut self, scope: &WorkerScope, id: HandlerId) { + self.subscribers.insert(id); } - fn update(&mut self, _: Self::Message) {} + fn disconnected(&mut self, scope: &WorkerScope, id: HandlerId) { + self.subscribers.remove(&id); + } - fn handle_input(&mut self, msg: Self::Input, _: HandlerId) { + fn received(&mut self, scope: &WorkerScope, msg: Self::Input, id: HandlerId) { for cmp in self.subscribers.iter() { self.link.respond(*cmp, msg.clone()); } } +} +#[derive(Properties, Debug, PartialEq)] +pub struct ModalCloserProviderProps { + #[prop_or_default] + pub children: Html, + pub id: String, +} - fn connected(&mut self, id: HandlerId) { - self.subscribers.insert(id); - } - - fn disconnected(&mut self, id: HandlerId) { - self.subscribers.remove(&id); +#[function_component] +pub fn ModalCloserProvider(props: &ModalCloserProviderProps) -> Html { + let msg = use_reducer(|| ModalCloseMsg { 0: props.id.clone().into() }); + html! { + context={ msg }> + { props.children.clone() } + > } } diff --git a/src/components/navbar.rs b/src/components/navbar.rs index 1280583..fd9b850 100644 --- a/src/components/navbar.rs +++ b/src/components/navbar.rs @@ -1,11 +1,19 @@ use derive_more::Display; +use std::rc::Rc; use yew::prelude::*; use crate::components::dropdown::DropdownMsg; +#[derive(Clone, Eq, PartialEq)] +pub struct NavBurgerCloserState { + /// The total number of clicks received. + pub total_clicks: u32, +} + /// The message type used by the `Navbar` component. pub enum NavbarMsg { ToggleMenu, + CloseEvent(Rc), } #[derive(Clone, Debug, Properties, PartialEq)] @@ -56,6 +64,8 @@ pub struct NavbarProps { /// [https://bulma.io/documentation/components/navbar/](https://bulma.io/documentation/components/navbar/) pub struct Navbar { is_menu_open: bool, + _listener: ContextHandle>, + state: Rc, } impl Component for Navbar { @@ -63,7 +73,11 @@ impl Component for Navbar { type Properties = NavbarProps; fn create(_ctx: &Context) -> Self { - Self { is_menu_open: false } + let (state, _listener) = _ctx + .link() + .context::>(_ctx.link().callback(NavbarMsg::CloseEvent)) + .expect("context to be set"); + Self { is_menu_open: false, _listener, state } } fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool { @@ -71,6 +85,11 @@ impl Component for Navbar { NavbarMsg::ToggleMenu => { self.is_menu_open = !self.is_menu_open; } + NavbarMsg::CloseEvent(state) => { + self.state = state; + // gloo_console::log!("state: {:?}", self.state.total_clicks); + self.is_menu_open = false; + } } true } diff --git a/src/components/pagination.rs b/src/components/pagination.rs index 3145d28..fd27ecf 100644 --- a/src/components/pagination.rs +++ b/src/components/pagination.rs @@ -64,6 +64,8 @@ pub struct PaginationItemProps { /// The click handler for this component. #[prop_or_default] pub onclick: Callback, + #[prop_or_default] + pub current: bool, } /// A pagination element representing a link to a page number, the previous page or the next page. @@ -71,8 +73,12 @@ pub struct PaginationItemProps { /// [https://bulma.io/documentation/components/pagination/](https://bulma.io/documentation/components/pagination/) #[function_component(PaginationItem)] pub fn pagination_item(props: &PaginationItemProps) -> Html { + let effective_class = match props.current { + true => format!("{} is-current", props.item_type.to_string()), + false => format!("{}", props.item_type.to_string()), + }; html! { - + {props.children.clone()} } diff --git a/src/elements/button.rs b/src/elements/button.rs index 426e5f0..daffc42 100644 --- a/src/elements/button.rs +++ b/src/elements/button.rs @@ -61,6 +61,9 @@ pub struct ButtonProps { /// Disable this component. #[prop_or_default] pub disabled: bool, + #[prop_or_default] + pub id: String, + } /// A button element. @@ -74,8 +77,9 @@ pub fn button(props: &ButtonProps) -> Html { props.loading.then_some("is-loading"), props.r#static.then_some("is-static") ); + let _id = props.id.clone(); html! { - } diff --git a/src/form/input.rs b/src/form/input.rs index 17e221b..1dd7450 100644 --- a/src/form/input.rs +++ b/src/form/input.rs @@ -90,4 +90,6 @@ pub enum InputType { Email, #[display(fmt = "tel")] Tel, + #[display(fmt = "number")] + Number, } diff --git a/src/lib.rs b/src/lib.rs index c52614a..330aca6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,10 +39,13 @@ pub use components::card::{ pub use components::dropdown::{Dropdown, DropdownMsg, DropdownProps}; pub use components::menu::{Menu, MenuLabel, MenuLabelProps, MenuList, MenuListProps, MenuProps}; pub use components::message::{Message, MessageBody, MessageBodyProps, MessageHeader, MessageHeaderProps, MessageProps}; -pub use components::modal::{Modal, ModalCard, ModalCardProps, ModalCloseMsg, ModalCloser, ModalMsg, ModalProps}; +pub use components::modal::{ + Modal, ModalCard, ModalCardProps, ModalCloseMsg, ModalCloser, ModalCloserContext, ModalCloserProvider, ModalCloserProviderProps, ModalMsg, + ModalProps, +}; pub use components::navbar::{ - Navbar, NavbarDivider, NavbarDividerProps, NavbarDropdown, NavbarDropdownProps, NavbarFixed, NavbarItem, NavbarItemProps, NavbarItemTag, - NavbarMsg, NavbarProps, + NavBurgerCloserState, Navbar, NavbarDivider, NavbarDividerProps, NavbarDropdown, NavbarDropdownProps, NavbarFixed, NavbarItem, NavbarItemProps, + NavbarItemTag, NavbarMsg, NavbarProps, }; pub use components::pagination::{ Pagination, PaginationEllipsis, PaginationItem, PaginationItemProps, PaginationItemRouter, PaginationItemType, PaginationProps, From 0a7dbd51c3eb9f0ace25516fc3072d081c99075e Mon Sep 17 00:00:00 2001 From: kostya Date: Wed, 5 Jun 2024 14:20:14 -0700 Subject: [PATCH 02/58] migration to 0.21 --- Cargo.toml | 9 +- examples/basic/Cargo.toml | 8 +- examples/basic/index.html | 2 +- examples/basic/src/main.rs | 104 ----------------- src/components/dropdown.rs | 5 +- src/components/modal.rs | 212 ++++++++--------------------------- src/components/navbar.rs | 21 +--- src/components/pagination.rs | 8 +- src/elements/button.rs | 6 +- src/form/input.rs | 2 - src/lib.rs | 9 +- 11 files changed, 59 insertions(+), 327 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8b29ae2..a9f221d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,12 +13,11 @@ keywords = ["wasm", "web", "bulma", "sass", "yew"] [dependencies] derive_more = { version = "0.99.17", default-features = false, features = ["display"] } web-sys = { version = "0.3.61", features = ["Element", "File", "HtmlCollection", "HtmlSelectElement"] } -yew = { version = "0.21.0", features = ["csr"] } -yew-agent = "0.3.0" -yew-router = { version = "0.18.0", optional = true } -wasm-bindgen = "0.2.87" +yew = { version = "0.20.0", features = ["csr"] } +yew-agent = "0.2.0" +yew-router = { version = "0.17.0", optional = true } +wasm-bindgen = "0.2.84" serde = { version = "1.0.152", features = ["derive"] } -gloo-console = "0.3.0" [features] default = ["router"] diff --git a/examples/basic/Cargo.toml b/examples/basic/Cargo.toml index e0c9dab..1118d5d 100644 --- a/examples/basic/Cargo.toml +++ b/examples/basic/Cargo.toml @@ -6,14 +6,10 @@ edition = "2018" [dependencies] console_error_panic_hook = "0.1" -gloo-console = "0.3.0" +gloo-console = "0.2" wasm-bindgen = "0.2" ybc = { path = "../../" } -yew = { version = "0.21.0", features = ["csr"] } -yew-agent = "0.3.0" -postcard = "1" -serde = { version = "1", features = ["derive"] } -js-sys = "0.3" +yew = "0.20" [features] default = [] diff --git a/examples/basic/index.html b/examples/basic/index.html index 2b9bf08..fdc3571 100644 --- a/examples/basic/index.html +++ b/examples/basic/index.html @@ -4,7 +4,7 @@ Trunk | Yew | YBC - + diff --git a/examples/basic/src/main.rs b/examples/basic/src/main.rs index e7ee73c..3c8e93c 100644 --- a/examples/basic/src/main.rs +++ b/examples/basic/src/main.rs @@ -1,20 +1,14 @@ #![recursion_limit = "1024"] -use std::rc::Rc; use console_error_panic_hook::set_once as set_panic_hook; use wasm_bindgen::prelude::*; use ybc::TileCtx::{Ancestor, Child, Parent}; use yew::prelude::*; - -use ybc::{ NavBurgerCloserState}; - #[function_component(App)] pub fn app() -> Html { - let state = Rc::new(NavBurgerCloserState { total_clicks: 0 }); html! { <> - > context={state}> Html { }} /> - >> Html { {"YBC"}

{"A Yew component library based on the Bulma CSS framework."}

- - - - - -
@@ -110,94 +97,3 @@ fn main() { yew::Renderer::::new().render(); } - - -use ybc::ModalCloserProvider; -use ybc::ModalCloserContext; - - -#[function_component] -pub fn MyModal1() -> Html { - let msg_ctx = use_context::().unwrap(); - let onclick = { - Callback::from(move |e:MouseEvent| msg_ctx.dispatch( "id0-close".to_string())) - }; - let on_click_cb = Callback::from(move |e: AttrValue| { - gloo_console::log!("Button clicked!"); - }); - html! { - - {"Open Modal"} - - }} - // on_clicked={on_click_cb} - body={ - html!{ - -

{"This is the body of the modal."}

-
- } - } - footer={html!( - <> - - {"Save changes"} - - - {"Close"} - - - )} - /> - } -} - -#[function_component] -pub fn MyModal2() -> Html { - let msg_ctx = use_context::().unwrap(); - let onclick = { - Callback::from(move |e:MouseEvent| msg_ctx.dispatch( "id2-close".to_string())) - }; - let msg_ctx2 = use_context::().unwrap(); - let onsave = { - Callback::from(move |e:MouseEvent| msg_ctx2.dispatch( "id2-close".to_string())) - }; - let on_click_cb = Callback::from(move |e: AttrValue| { - gloo_console::log!("Button clicked!"); - }); - html! { - - {"Open Modal"} - - }} - // on_clicked={on_click_cb} - body={ - html!{ - -

{"This is the body of the modal2."}

-
- } - } - footer={html!( - <> - - {"Save changes"} - - - {"Close"} - - - )} - /> - } -} \ No newline at end of file diff --git a/src/components/dropdown.rs b/src/components/dropdown.rs index a56a357..fe8ec5a 100644 --- a/src/components/dropdown.rs +++ b/src/components/dropdown.rs @@ -61,10 +61,7 @@ impl Component for Dropdown { class.push("is-hoverable"); Callback::noop() } else { - ctx.link().callback(|event: MouseEvent| { - event.prevent_default(); - DropdownMsg::Open - }) + ctx.link().callback(|_| DropdownMsg::Open) }; let overlay = if self.is_menu_active { class.push("is-active"); diff --git a/src/components/modal.rs b/src/components/modal.rs index 11ecbb8..a86ffbd 100644 --- a/src/components/modal.rs +++ b/src/components/modal.rs @@ -1,12 +1,10 @@ use std::collections::HashSet; -use std::rc::Rc; -use wasm_bindgen::JsCast; -use yew::prelude::*; +use serde::{Deserialize, Serialize}; -use yew_agent::worker::{HandlerId, Worker, WorkerScope}; +use yew::prelude::*; -use yew_agent::prelude::*; +use yew_agent::{use_bridge, HandlerId, Public, UseBridgeHandle, Worker, WorkerLink}; /// Modal actions. pub enum ModalMsg { @@ -15,8 +13,6 @@ pub enum ModalMsg { CloseFromAgent(ModalCloseMsg), } -pub type ModalCloserContext = UseReducerHandle; - #[derive(Clone, Debug, Properties, PartialEq)] pub struct ModalProps { /// The ID of this modal, used for triggering close events from other parts of the app. @@ -40,33 +36,34 @@ pub struct ModalProps { #[function_component(Modal)] pub fn modal(props: &ModalProps) -> Html { let is_active = use_state(|| false); - let id = props.id.clone(); - let closer_ctx = use_context::().expect("Modal closer in context"); + let mut class = Classes::from("modal"); class.push(props.classes.clone()); - let (_id, closed) = match closer_ctx.0.contains("-") { - true => { - let result = closer_ctx.0.split("-").collect::>(); - (result[0], result[1] == "close") - } - false => (closer_ctx.0.as_str(), false), - }; - let (opencb, closecb) = if _id == id && *is_active { + let (opencb, closecb) = if *is_active { class.push("is-active"); let is_active = is_active.clone(); (Callback::noop(), Callback::from(move |_| is_active.set(false))) - } else if _id == id { + } else { let is_active = is_active.clone(); (Callback::from(move |_| is_active.set(true)), Callback::noop()) - } else { - (Callback::noop(), Callback::noop()) }; + { + let id = props.id.clone(); + + let _bridge: UseBridgeHandle = use_bridge(move |response: ModalCloseMsg| { + if response.0 == id { + is_active.set(false); + } else { + } + }); + } + html! { <>
@@ -88,9 +85,9 @@ pub fn modal(props: &ModalProps) -> Html { #[derive(Clone, Debug, Properties, PartialEq)] pub struct ModalCardProps { /// The ID of this modal, used for triggering close events from other parts of the app. - pub id: AttrValue, + pub id: String, /// The title of this modal. - pub title: AttrValue, + pub title: String, /// The content to be placed in the `modal-card-body` not including the modal-card-header / /// modal-card-title, which is handled by the `modal_title` prop. #[prop_or_default] @@ -113,130 +110,34 @@ pub struct ModalCardProps { /// in your app for maximum flexibility. #[function_component(ModalCard)] pub fn modal_card(props: &ModalCardProps) -> Html { - let id = props.id.clone(); - let closer_ctx = use_context::().expect("Modal closer in context"); - - // gloo_console::log!("closer_ctx: full ID {}", closer_ctx.0.as_str()); - - let (_id, closed) = match closer_ctx.0.contains("-") { - true => { - let result = closer_ctx.0.split("-").collect::>(); - (result[0], result[1] == "close") - } - false => (closer_ctx.0.as_str(), false), - }; let is_active = use_state(|| false); - // gloo_console::log!("closer_ctx: {:?} id:{:?} is closed:", _id.clone().into(), id.clone().into(), closed); - - if _id == id && closed { - is_active.set(false); - closer_ctx.dispatch(id.clone()); - // gloo_console::log!("closed!"); - } - let mut class = Classes::from("modal"); class.push(props.classes.clone()); - let (opencb, closecb) = if _id == id && *is_active { - + let (opencb, closecb) = if *is_active { class.push("is-active"); - // gloo_console::log!("is_active=true call"); let is_active = is_active.clone(); - (Callback::noop(), Callback::from(move |e:MouseEvent| { - let target = e.target(); - gloo_console::log!("Close event from modal-card: {:?}"); - // Check if the target is an element that you want to ignore - if let Some(target) = target { - let target_element = target.dyn_into::().unwrap(); - if target_element.id().starts_with("modal-ignore-") { - // If the target is an element to ignore, stop the event propagation - e.stop_propagation(); - gloo_console::log!("Ignoring event"); - return; - } - } - is_active.set(false) - })) - } else if _id == id { - let is_active = is_active.clone(); - gloo_console::log!("is_active=false call"); - (Callback::from(move |_| is_active.set(true)), Callback::noop()) + (Callback::noop(), Callback::from(move |_| is_active.set(false))) } else { - gloo_console::log!("NOOP call"); - (Callback::noop(), Callback::noop()) - }; - - html! { - <> -
- {props.trigger.clone()} -
-
- - - -
- - } -} + let is_active = is_active.clone(); -#[function_component(ModalCard2)] -pub fn modal_card2(props: &ModalCardProps) -> Html { - let id = props.id.clone(); - let closer_ctx = use_context::().expect("Modal closer in context"); - - // gloo_console::log!("closer_ctx: full ID {}", closer_ctx.0.as_str()); - let action = closer_ctx.0.as_str(); - let (_id, closed) = match action.contains("-") { - true => { - let result = action.split("-").collect::>(); - (result[0], result[1] == "close") - } - false => (action, false), + (Callback::from(move |_| is_active.set(true)), Callback::noop()) }; - let is_active = use_state(|| false); - // gloo_console::log!("closer_ctx: {:?} id:{:?} is closed:", _id.clone().into(), id.clone().into(), closed); + { + let id = props.id.clone(); - if _id == id && closed { - is_active.set(false); - closer_ctx.dispatch(id.clone()); - gloo_console::log!("closed!"); + let _bridge: UseBridgeHandle = use_bridge(move |response: ModalCloseMsg| { + if response.0 == id { + is_active.set(false); + } else { + } + }); } - let mut class = Classes::from("modal"); - class.push(props.classes.clone()); - - let (opencb, closecb) = if _id == id && *is_active { - class.push("is-active"); - gloo_console::log!("is_active=true call"); - - let is_active = is_active.clone(); - - (Callback::noop(), Callback::from(move |_| is_active.set(false))) - } else if _id == id { - let is_active = is_active.clone(); - gloo_console::log!("is_active=false call"); - (Callback::from(move |_| is_active.set(true)), Callback::noop()) - } else { - gloo_console::log!("NOOP call"); - (Callback::noop(), Callback::noop()) - }; - html! { <>
@@ -268,16 +169,8 @@ pub fn modal_card2(props: &ModalCardProps) -> Html { /// /// The ID provided in this message must match the ID of the modal which is to be closed, else /// the message will be ignored. -#[derive(Clone, Debug, PartialEq)] -pub struct ModalCloseMsg(pub AttrValue); - -impl Reducible for ModalCloseMsg { - type Action = AttrValue; - - fn reduce(self: Rc, action: Self::Action) -> Rc { - ModalCloseMsg { 0: action }.into() - } -} +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct ModalCloseMsg(pub String); /// An agent used for being able to close `Modal` & `ModalCard` instances by ID. /// @@ -320,7 +213,7 @@ impl Reducible for ModalCloseMsg { /// This pattern allows you to communicate with a modal by its given ID, allowing /// you to close the modal from anywhere in your application. pub struct ModalCloser { - link: WorkerScope, + link: WorkerLink, subscribers: HashSet, } @@ -329,42 +222,27 @@ impl Worker for ModalCloser { type Message = (); // The agent receives requests to close modals by ID. type Output = ModalCloseMsg; + type Reach = Public; // The agent forwards the input to all registered modals. - fn create(link: &WorkerScope) -> Self { - Self { link: link.clone(), subscribers: HashSet::new() } - } - - fn update(&mut self, scope: &WorkerScope, _: Self::Message) {} - - fn connected(&mut self, scope: &WorkerScope, id: HandlerId) { - self.subscribers.insert(id); + fn create(link: WorkerLink) -> Self { + Self { link, subscribers: HashSet::new() } } - fn disconnected(&mut self, scope: &WorkerScope, id: HandlerId) { - self.subscribers.remove(&id); - } + fn update(&mut self, _: Self::Message) {} - fn received(&mut self, scope: &WorkerScope, msg: Self::Input, id: HandlerId) { + fn handle_input(&mut self, msg: Self::Input, _: HandlerId) { for cmp in self.subscribers.iter() { self.link.respond(*cmp, msg.clone()); } } -} -#[derive(Properties, Debug, PartialEq)] -pub struct ModalCloserProviderProps { - #[prop_or_default] - pub children: Html, - pub id: String, -} -#[function_component] -pub fn ModalCloserProvider(props: &ModalCloserProviderProps) -> Html { - let msg = use_reducer(|| ModalCloseMsg { 0: props.id.clone().into() }); - html! { - context={ msg }> - { props.children.clone() } - > + fn connected(&mut self, id: HandlerId) { + self.subscribers.insert(id); + } + + fn disconnected(&mut self, id: HandlerId) { + self.subscribers.remove(&id); } } diff --git a/src/components/navbar.rs b/src/components/navbar.rs index fd9b850..1280583 100644 --- a/src/components/navbar.rs +++ b/src/components/navbar.rs @@ -1,19 +1,11 @@ use derive_more::Display; -use std::rc::Rc; use yew::prelude::*; use crate::components::dropdown::DropdownMsg; -#[derive(Clone, Eq, PartialEq)] -pub struct NavBurgerCloserState { - /// The total number of clicks received. - pub total_clicks: u32, -} - /// The message type used by the `Navbar` component. pub enum NavbarMsg { ToggleMenu, - CloseEvent(Rc), } #[derive(Clone, Debug, Properties, PartialEq)] @@ -64,8 +56,6 @@ pub struct NavbarProps { /// [https://bulma.io/documentation/components/navbar/](https://bulma.io/documentation/components/navbar/) pub struct Navbar { is_menu_open: bool, - _listener: ContextHandle>, - state: Rc, } impl Component for Navbar { @@ -73,11 +63,7 @@ impl Component for Navbar { type Properties = NavbarProps; fn create(_ctx: &Context) -> Self { - let (state, _listener) = _ctx - .link() - .context::>(_ctx.link().callback(NavbarMsg::CloseEvent)) - .expect("context to be set"); - Self { is_menu_open: false, _listener, state } + Self { is_menu_open: false } } fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool { @@ -85,11 +71,6 @@ impl Component for Navbar { NavbarMsg::ToggleMenu => { self.is_menu_open = !self.is_menu_open; } - NavbarMsg::CloseEvent(state) => { - self.state = state; - // gloo_console::log!("state: {:?}", self.state.total_clicks); - self.is_menu_open = false; - } } true } diff --git a/src/components/pagination.rs b/src/components/pagination.rs index fd27ecf..3145d28 100644 --- a/src/components/pagination.rs +++ b/src/components/pagination.rs @@ -64,8 +64,6 @@ pub struct PaginationItemProps { /// The click handler for this component. #[prop_or_default] pub onclick: Callback, - #[prop_or_default] - pub current: bool, } /// A pagination element representing a link to a page number, the previous page or the next page. @@ -73,12 +71,8 @@ pub struct PaginationItemProps { /// [https://bulma.io/documentation/components/pagination/](https://bulma.io/documentation/components/pagination/) #[function_component(PaginationItem)] pub fn pagination_item(props: &PaginationItemProps) -> Html { - let effective_class = match props.current { - true => format!("{} is-current", props.item_type.to_string()), - false => format!("{}", props.item_type.to_string()), - }; html! { - + {props.children.clone()} } diff --git a/src/elements/button.rs b/src/elements/button.rs index daffc42..426e5f0 100644 --- a/src/elements/button.rs +++ b/src/elements/button.rs @@ -61,9 +61,6 @@ pub struct ButtonProps { /// Disable this component. #[prop_or_default] pub disabled: bool, - #[prop_or_default] - pub id: String, - } /// A button element. @@ -77,9 +74,8 @@ pub fn button(props: &ButtonProps) -> Html { props.loading.then_some("is-loading"), props.r#static.then_some("is-static") ); - let _id = props.id.clone(); html! { - } diff --git a/src/form/input.rs b/src/form/input.rs index 1dd7450..17e221b 100644 --- a/src/form/input.rs +++ b/src/form/input.rs @@ -90,6 +90,4 @@ pub enum InputType { Email, #[display(fmt = "tel")] Tel, - #[display(fmt = "number")] - Number, } diff --git a/src/lib.rs b/src/lib.rs index 330aca6..c52614a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,13 +39,10 @@ pub use components::card::{ pub use components::dropdown::{Dropdown, DropdownMsg, DropdownProps}; pub use components::menu::{Menu, MenuLabel, MenuLabelProps, MenuList, MenuListProps, MenuProps}; pub use components::message::{Message, MessageBody, MessageBodyProps, MessageHeader, MessageHeaderProps, MessageProps}; -pub use components::modal::{ - Modal, ModalCard, ModalCardProps, ModalCloseMsg, ModalCloser, ModalCloserContext, ModalCloserProvider, ModalCloserProviderProps, ModalMsg, - ModalProps, -}; +pub use components::modal::{Modal, ModalCard, ModalCardProps, ModalCloseMsg, ModalCloser, ModalMsg, ModalProps}; pub use components::navbar::{ - NavBurgerCloserState, Navbar, NavbarDivider, NavbarDividerProps, NavbarDropdown, NavbarDropdownProps, NavbarFixed, NavbarItem, NavbarItemProps, - NavbarItemTag, NavbarMsg, NavbarProps, + Navbar, NavbarDivider, NavbarDividerProps, NavbarDropdown, NavbarDropdownProps, NavbarFixed, NavbarItem, NavbarItemProps, NavbarItemTag, + NavbarMsg, NavbarProps, }; pub use components::pagination::{ Pagination, PaginationEllipsis, PaginationItem, PaginationItemProps, PaginationItemRouter, PaginationItemType, PaginationProps, From 2af7b601007bd1774b1049690170ddca1d3974b3 Mon Sep 17 00:00:00 2001 From: kostya Date: Wed, 5 Jun 2024 14:30:39 -0700 Subject: [PATCH 03/58] migration to 0.21 --- Cargo.toml | 10 +++++----- examples/basic/Cargo.toml | 6 +++--- src/components/modal.rs | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a9f221d..9c36184 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "ybc" version = "0.4.0" description = "A Yew component library based on the Bulma CSS framework." -authors = ["Anthony Dodd "] +authors = ["Anthony Dodd ", "Konstantin Pupkov "] edition = "2021" license = "MIT/Apache-2.0" repository = "https://github.com/thedodd/ybc" @@ -13,10 +13,10 @@ keywords = ["wasm", "web", "bulma", "sass", "yew"] [dependencies] derive_more = { version = "0.99.17", default-features = false, features = ["display"] } web-sys = { version = "0.3.61", features = ["Element", "File", "HtmlCollection", "HtmlSelectElement"] } -yew = { version = "0.20.0", features = ["csr"] } -yew-agent = "0.2.0" -yew-router = { version = "0.17.0", optional = true } -wasm-bindgen = "0.2.84" +yew = { version = "0.21.0", features = ["csr"] } +yew-agent = "0.3.0" +yew-router = { version = "0.18.0", optional = true } +wasm-bindgen = "0.2" serde = { version = "1.0.152", features = ["derive"] } [features] diff --git a/examples/basic/Cargo.toml b/examples/basic/Cargo.toml index 1118d5d..f0cc65f 100644 --- a/examples/basic/Cargo.toml +++ b/examples/basic/Cargo.toml @@ -6,10 +6,10 @@ edition = "2018" [dependencies] console_error_panic_hook = "0.1" -gloo-console = "0.2" +gloo-console = "0.3.0" wasm-bindgen = "0.2" -ybc = { path = "../../" } -yew = "0.20" +ybc = { path = "../.." } +yew = "0.21" [features] default = [] diff --git a/src/components/modal.rs b/src/components/modal.rs index a86ffbd..d2b2c02 100644 --- a/src/components/modal.rs +++ b/src/components/modal.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use yew::prelude::*; -use yew_agent::{use_bridge, HandlerId, Public, UseBridgeHandle, Worker, WorkerLink}; +// use yew_agent::{use_bridge, HandlerId, Public, UseBridgeHandle, Worker, WorkerLink}; /// Modal actions. pub enum ModalMsg { From 0587d3e72754b312cec5bd5cbdd8b87d0530d4c6 Mon Sep 17 00:00:00 2001 From: kostya Date: Wed, 5 Jun 2024 14:31:55 -0700 Subject: [PATCH 04/58] migration to 0.21 --- src/components/modal.rs | 212 +++++++++++++++++++++++++++++++--------- 1 file changed, 167 insertions(+), 45 deletions(-) diff --git a/src/components/modal.rs b/src/components/modal.rs index d2b2c02..11ecbb8 100644 --- a/src/components/modal.rs +++ b/src/components/modal.rs @@ -1,10 +1,12 @@ use std::collections::HashSet; - -use serde::{Deserialize, Serialize}; +use std::rc::Rc; +use wasm_bindgen::JsCast; use yew::prelude::*; -// use yew_agent::{use_bridge, HandlerId, Public, UseBridgeHandle, Worker, WorkerLink}; +use yew_agent::worker::{HandlerId, Worker, WorkerScope}; + +use yew_agent::prelude::*; /// Modal actions. pub enum ModalMsg { @@ -13,6 +15,8 @@ pub enum ModalMsg { CloseFromAgent(ModalCloseMsg), } +pub type ModalCloserContext = UseReducerHandle; + #[derive(Clone, Debug, Properties, PartialEq)] pub struct ModalProps { /// The ID of this modal, used for triggering close events from other parts of the app. @@ -36,34 +40,33 @@ pub struct ModalProps { #[function_component(Modal)] pub fn modal(props: &ModalProps) -> Html { let is_active = use_state(|| false); - + let id = props.id.clone(); + let closer_ctx = use_context::().expect("Modal closer in context"); let mut class = Classes::from("modal"); class.push(props.classes.clone()); + let (_id, closed) = match closer_ctx.0.contains("-") { + true => { + let result = closer_ctx.0.split("-").collect::>(); + (result[0], result[1] == "close") + } + false => (closer_ctx.0.as_str(), false), + }; - let (opencb, closecb) = if *is_active { + let (opencb, closecb) = if _id == id && *is_active { class.push("is-active"); let is_active = is_active.clone(); (Callback::noop(), Callback::from(move |_| is_active.set(false))) - } else { + } else if _id == id { let is_active = is_active.clone(); (Callback::from(move |_| is_active.set(true)), Callback::noop()) + } else { + (Callback::noop(), Callback::noop()) }; - { - let id = props.id.clone(); - - let _bridge: UseBridgeHandle = use_bridge(move |response: ModalCloseMsg| { - if response.0 == id { - is_active.set(false); - } else { - } - }); - } - html! { <>
@@ -85,9 +88,9 @@ pub fn modal(props: &ModalProps) -> Html { #[derive(Clone, Debug, Properties, PartialEq)] pub struct ModalCardProps { /// The ID of this modal, used for triggering close events from other parts of the app. - pub id: String, + pub id: AttrValue, /// The title of this modal. - pub title: String, + pub title: AttrValue, /// The content to be placed in the `modal-card-body` not including the modal-card-header / /// modal-card-title, which is handled by the `modal_title` prop. #[prop_or_default] @@ -110,34 +113,130 @@ pub struct ModalCardProps { /// in your app for maximum flexibility. #[function_component(ModalCard)] pub fn modal_card(props: &ModalCardProps) -> Html { + let id = props.id.clone(); + let closer_ctx = use_context::().expect("Modal closer in context"); + + // gloo_console::log!("closer_ctx: full ID {}", closer_ctx.0.as_str()); + + let (_id, closed) = match closer_ctx.0.contains("-") { + true => { + let result = closer_ctx.0.split("-").collect::>(); + (result[0], result[1] == "close") + } + false => (closer_ctx.0.as_str(), false), + }; let is_active = use_state(|| false); + // gloo_console::log!("closer_ctx: {:?} id:{:?} is closed:", _id.clone().into(), id.clone().into(), closed); + + if _id == id && closed { + is_active.set(false); + closer_ctx.dispatch(id.clone()); + // gloo_console::log!("closed!"); + } + let mut class = Classes::from("modal"); class.push(props.classes.clone()); - let (opencb, closecb) = if *is_active { + let (opencb, closecb) = if _id == id && *is_active { + class.push("is-active"); + // gloo_console::log!("is_active=true call"); let is_active = is_active.clone(); - (Callback::noop(), Callback::from(move |_| is_active.set(false))) - } else { + (Callback::noop(), Callback::from(move |e:MouseEvent| { + let target = e.target(); + gloo_console::log!("Close event from modal-card: {:?}"); + // Check if the target is an element that you want to ignore + if let Some(target) = target { + let target_element = target.dyn_into::().unwrap(); + if target_element.id().starts_with("modal-ignore-") { + // If the target is an element to ignore, stop the event propagation + e.stop_propagation(); + gloo_console::log!("Ignoring event"); + return; + } + } + is_active.set(false) + })) + } else if _id == id { let is_active = is_active.clone(); - + gloo_console::log!("is_active=false call"); (Callback::from(move |_| is_active.set(true)), Callback::noop()) + } else { + gloo_console::log!("NOOP call"); + (Callback::noop(), Callback::noop()) + }; + + html! { + <> +
+ {props.trigger.clone()} +
+
+ + + +
+ + } +} + +#[function_component(ModalCard2)] +pub fn modal_card2(props: &ModalCardProps) -> Html { + let id = props.id.clone(); + let closer_ctx = use_context::().expect("Modal closer in context"); + + // gloo_console::log!("closer_ctx: full ID {}", closer_ctx.0.as_str()); + let action = closer_ctx.0.as_str(); + let (_id, closed) = match action.contains("-") { + true => { + let result = action.split("-").collect::>(); + (result[0], result[1] == "close") + } + false => (action, false), }; + let is_active = use_state(|| false); - { - let id = props.id.clone(); + // gloo_console::log!("closer_ctx: {:?} id:{:?} is closed:", _id.clone().into(), id.clone().into(), closed); - let _bridge: UseBridgeHandle = use_bridge(move |response: ModalCloseMsg| { - if response.0 == id { - is_active.set(false); - } else { - } - }); + if _id == id && closed { + is_active.set(false); + closer_ctx.dispatch(id.clone()); + gloo_console::log!("closed!"); } + let mut class = Classes::from("modal"); + class.push(props.classes.clone()); + + let (opencb, closecb) = if _id == id && *is_active { + class.push("is-active"); + gloo_console::log!("is_active=true call"); + + let is_active = is_active.clone(); + + (Callback::noop(), Callback::from(move |_| is_active.set(false))) + } else if _id == id { + let is_active = is_active.clone(); + gloo_console::log!("is_active=false call"); + (Callback::from(move |_| is_active.set(true)), Callback::noop()) + } else { + gloo_console::log!("NOOP call"); + (Callback::noop(), Callback::noop()) + }; + html! { <>
@@ -169,8 +268,16 @@ pub fn modal_card(props: &ModalCardProps) -> Html { /// /// The ID provided in this message must match the ID of the modal which is to be closed, else /// the message will be ignored. -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct ModalCloseMsg(pub String); +#[derive(Clone, Debug, PartialEq)] +pub struct ModalCloseMsg(pub AttrValue); + +impl Reducible for ModalCloseMsg { + type Action = AttrValue; + + fn reduce(self: Rc, action: Self::Action) -> Rc { + ModalCloseMsg { 0: action }.into() + } +} /// An agent used for being able to close `Modal` & `ModalCard` instances by ID. /// @@ -213,7 +320,7 @@ pub struct ModalCloseMsg(pub String); /// This pattern allows you to communicate with a modal by its given ID, allowing /// you to close the modal from anywhere in your application. pub struct ModalCloser { - link: WorkerLink, + link: WorkerScope, subscribers: HashSet, } @@ -222,27 +329,42 @@ impl Worker for ModalCloser { type Message = (); // The agent receives requests to close modals by ID. type Output = ModalCloseMsg; - type Reach = Public; // The agent forwards the input to all registered modals. - fn create(link: WorkerLink) -> Self { - Self { link, subscribers: HashSet::new() } + fn create(link: &WorkerScope) -> Self { + Self { link: link.clone(), subscribers: HashSet::new() } + } + + fn update(&mut self, scope: &WorkerScope, _: Self::Message) {} + + fn connected(&mut self, scope: &WorkerScope, id: HandlerId) { + self.subscribers.insert(id); } - fn update(&mut self, _: Self::Message) {} + fn disconnected(&mut self, scope: &WorkerScope, id: HandlerId) { + self.subscribers.remove(&id); + } - fn handle_input(&mut self, msg: Self::Input, _: HandlerId) { + fn received(&mut self, scope: &WorkerScope, msg: Self::Input, id: HandlerId) { for cmp in self.subscribers.iter() { self.link.respond(*cmp, msg.clone()); } } +} +#[derive(Properties, Debug, PartialEq)] +pub struct ModalCloserProviderProps { + #[prop_or_default] + pub children: Html, + pub id: String, +} - fn connected(&mut self, id: HandlerId) { - self.subscribers.insert(id); - } - - fn disconnected(&mut self, id: HandlerId) { - self.subscribers.remove(&id); +#[function_component] +pub fn ModalCloserProvider(props: &ModalCloserProviderProps) -> Html { + let msg = use_reducer(|| ModalCloseMsg { 0: props.id.clone().into() }); + html! { + context={ msg }> + { props.children.clone() } + > } } From 8383158fff94a18cdd389f9a13da224c554c0af0 Mon Sep 17 00:00:00 2001 From: kostya Date: Wed, 5 Jun 2024 14:39:31 -0700 Subject: [PATCH 05/58] migration to 0.21 --- Cargo.toml | 1 + examples/basic/Cargo.toml | 2 +- examples/basic/index.html | 2 +- examples/basic/src/main.rs | 104 +++++++++++++++++++++++++++++++++++ src/components/dropdown.rs | 5 +- src/components/navbar.rs | 21 ++++++- src/components/pagination.rs | 8 ++- src/elements/button.rs | 6 +- src/form/input.rs | 2 + src/lib.rs | 9 ++- 10 files changed, 151 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9c36184..aaf0d47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ yew-agent = "0.3.0" yew-router = { version = "0.18.0", optional = true } wasm-bindgen = "0.2" serde = { version = "1.0.152", features = ["derive"] } +gloo-console = "0.3.0" [features] default = ["router"] diff --git a/examples/basic/Cargo.toml b/examples/basic/Cargo.toml index f0cc65f..47f139c 100644 --- a/examples/basic/Cargo.toml +++ b/examples/basic/Cargo.toml @@ -6,7 +6,7 @@ edition = "2018" [dependencies] console_error_panic_hook = "0.1" -gloo-console = "0.3.0" +gloo-console = "0.3" wasm-bindgen = "0.2" ybc = { path = "../.." } yew = "0.21" diff --git a/examples/basic/index.html b/examples/basic/index.html index fdc3571..2b9bf08 100644 --- a/examples/basic/index.html +++ b/examples/basic/index.html @@ -4,7 +4,7 @@ Trunk | Yew | YBC - + diff --git a/examples/basic/src/main.rs b/examples/basic/src/main.rs index 3c8e93c..e7ee73c 100644 --- a/examples/basic/src/main.rs +++ b/examples/basic/src/main.rs @@ -1,14 +1,20 @@ #![recursion_limit = "1024"] +use std::rc::Rc; use console_error_panic_hook::set_once as set_panic_hook; use wasm_bindgen::prelude::*; use ybc::TileCtx::{Ancestor, Child, Parent}; use yew::prelude::*; + +use ybc::{ NavBurgerCloserState}; + #[function_component(App)] pub fn app() -> Html { + let state = Rc::new(NavBurgerCloserState { total_clicks: 0 }); html! { <> + > context={state}> Html { }} /> + >> Html { {"YBC"}

{"A Yew component library based on the Bulma CSS framework."}

+ + + + + +
@@ -97,3 +110,94 @@ fn main() { yew::Renderer::::new().render(); } + + +use ybc::ModalCloserProvider; +use ybc::ModalCloserContext; + + +#[function_component] +pub fn MyModal1() -> Html { + let msg_ctx = use_context::().unwrap(); + let onclick = { + Callback::from(move |e:MouseEvent| msg_ctx.dispatch( "id0-close".to_string())) + }; + let on_click_cb = Callback::from(move |e: AttrValue| { + gloo_console::log!("Button clicked!"); + }); + html! { + + {"Open Modal"} + + }} + // on_clicked={on_click_cb} + body={ + html!{ + +

{"This is the body of the modal."}

+
+ } + } + footer={html!( + <> + + {"Save changes"} + + + {"Close"} + + + )} + /> + } +} + +#[function_component] +pub fn MyModal2() -> Html { + let msg_ctx = use_context::().unwrap(); + let onclick = { + Callback::from(move |e:MouseEvent| msg_ctx.dispatch( "id2-close".to_string())) + }; + let msg_ctx2 = use_context::().unwrap(); + let onsave = { + Callback::from(move |e:MouseEvent| msg_ctx2.dispatch( "id2-close".to_string())) + }; + let on_click_cb = Callback::from(move |e: AttrValue| { + gloo_console::log!("Button clicked!"); + }); + html! { + + {"Open Modal"} + + }} + // on_clicked={on_click_cb} + body={ + html!{ + +

{"This is the body of the modal2."}

+
+ } + } + footer={html!( + <> + + {"Save changes"} + + + {"Close"} + + + )} + /> + } +} \ No newline at end of file diff --git a/src/components/dropdown.rs b/src/components/dropdown.rs index fe8ec5a..a56a357 100644 --- a/src/components/dropdown.rs +++ b/src/components/dropdown.rs @@ -61,7 +61,10 @@ impl Component for Dropdown { class.push("is-hoverable"); Callback::noop() } else { - ctx.link().callback(|_| DropdownMsg::Open) + ctx.link().callback(|event: MouseEvent| { + event.prevent_default(); + DropdownMsg::Open + }) }; let overlay = if self.is_menu_active { class.push("is-active"); diff --git a/src/components/navbar.rs b/src/components/navbar.rs index 1280583..fd9b850 100644 --- a/src/components/navbar.rs +++ b/src/components/navbar.rs @@ -1,11 +1,19 @@ use derive_more::Display; +use std::rc::Rc; use yew::prelude::*; use crate::components::dropdown::DropdownMsg; +#[derive(Clone, Eq, PartialEq)] +pub struct NavBurgerCloserState { + /// The total number of clicks received. + pub total_clicks: u32, +} + /// The message type used by the `Navbar` component. pub enum NavbarMsg { ToggleMenu, + CloseEvent(Rc), } #[derive(Clone, Debug, Properties, PartialEq)] @@ -56,6 +64,8 @@ pub struct NavbarProps { /// [https://bulma.io/documentation/components/navbar/](https://bulma.io/documentation/components/navbar/) pub struct Navbar { is_menu_open: bool, + _listener: ContextHandle>, + state: Rc, } impl Component for Navbar { @@ -63,7 +73,11 @@ impl Component for Navbar { type Properties = NavbarProps; fn create(_ctx: &Context) -> Self { - Self { is_menu_open: false } + let (state, _listener) = _ctx + .link() + .context::>(_ctx.link().callback(NavbarMsg::CloseEvent)) + .expect("context to be set"); + Self { is_menu_open: false, _listener, state } } fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool { @@ -71,6 +85,11 @@ impl Component for Navbar { NavbarMsg::ToggleMenu => { self.is_menu_open = !self.is_menu_open; } + NavbarMsg::CloseEvent(state) => { + self.state = state; + // gloo_console::log!("state: {:?}", self.state.total_clicks); + self.is_menu_open = false; + } } true } diff --git a/src/components/pagination.rs b/src/components/pagination.rs index 3145d28..fd27ecf 100644 --- a/src/components/pagination.rs +++ b/src/components/pagination.rs @@ -64,6 +64,8 @@ pub struct PaginationItemProps { /// The click handler for this component. #[prop_or_default] pub onclick: Callback, + #[prop_or_default] + pub current: bool, } /// A pagination element representing a link to a page number, the previous page or the next page. @@ -71,8 +73,12 @@ pub struct PaginationItemProps { /// [https://bulma.io/documentation/components/pagination/](https://bulma.io/documentation/components/pagination/) #[function_component(PaginationItem)] pub fn pagination_item(props: &PaginationItemProps) -> Html { + let effective_class = match props.current { + true => format!("{} is-current", props.item_type.to_string()), + false => format!("{}", props.item_type.to_string()), + }; html! { - + {props.children.clone()} } diff --git a/src/elements/button.rs b/src/elements/button.rs index 426e5f0..daffc42 100644 --- a/src/elements/button.rs +++ b/src/elements/button.rs @@ -61,6 +61,9 @@ pub struct ButtonProps { /// Disable this component. #[prop_or_default] pub disabled: bool, + #[prop_or_default] + pub id: String, + } /// A button element. @@ -74,8 +77,9 @@ pub fn button(props: &ButtonProps) -> Html { props.loading.then_some("is-loading"), props.r#static.then_some("is-static") ); + let _id = props.id.clone(); html! { - } diff --git a/src/form/input.rs b/src/form/input.rs index 17e221b..1dd7450 100644 --- a/src/form/input.rs +++ b/src/form/input.rs @@ -90,4 +90,6 @@ pub enum InputType { Email, #[display(fmt = "tel")] Tel, + #[display(fmt = "number")] + Number, } diff --git a/src/lib.rs b/src/lib.rs index c52614a..330aca6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,10 +39,13 @@ pub use components::card::{ pub use components::dropdown::{Dropdown, DropdownMsg, DropdownProps}; pub use components::menu::{Menu, MenuLabel, MenuLabelProps, MenuList, MenuListProps, MenuProps}; pub use components::message::{Message, MessageBody, MessageBodyProps, MessageHeader, MessageHeaderProps, MessageProps}; -pub use components::modal::{Modal, ModalCard, ModalCardProps, ModalCloseMsg, ModalCloser, ModalMsg, ModalProps}; +pub use components::modal::{ + Modal, ModalCard, ModalCardProps, ModalCloseMsg, ModalCloser, ModalCloserContext, ModalCloserProvider, ModalCloserProviderProps, ModalMsg, + ModalProps, +}; pub use components::navbar::{ - Navbar, NavbarDivider, NavbarDividerProps, NavbarDropdown, NavbarDropdownProps, NavbarFixed, NavbarItem, NavbarItemProps, NavbarItemTag, - NavbarMsg, NavbarProps, + NavBurgerCloserState, Navbar, NavbarDivider, NavbarDividerProps, NavbarDropdown, NavbarDropdownProps, NavbarFixed, NavbarItem, NavbarItemProps, + NavbarItemTag, NavbarMsg, NavbarProps, }; pub use components::pagination::{ Pagination, PaginationEllipsis, PaginationItem, PaginationItemProps, PaginationItemRouter, PaginationItemType, PaginationProps, From 14e16a39b2ac3e09d3197cba1524f659e7cd1e1a Mon Sep 17 00:00:00 2001 From: kostya Date: Wed, 5 Jun 2024 14:43:58 -0700 Subject: [PATCH 06/58] migration to 0.21 --- README.md | 2 +- examples/basic/src/main.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 03e0442..e5c8ee2 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ ybc = "*" ### add bulma #### add bulma css (no customizations) -This project works perfectly well if you just include the Bulma CSS in your HTML, [as described here](https://bulma.io/documentation/overview/start/). The following link in your HTML head should do the trick: ``. +This project works perfectly well if you just include the Bulma CSS in your HTML, [as described here](https://bulma.io/documentation/overview/start/). The following link in your HTML head should do the trick: ``. #### add bulma sass (allows customization & themes) However, if you want to customize Bulma to match your style guidelines, then you will need to have a copy of the Bulma SASS locally, and then import Bulma after you've defined your customizations, [as described here](https://bulma.io/documentation/customize/). diff --git a/examples/basic/src/main.rs b/examples/basic/src/main.rs index e7ee73c..0154562 100644 --- a/examples/basic/src/main.rs +++ b/examples/basic/src/main.rs @@ -37,7 +37,7 @@ pub fn app() -> Html { - + {"YBC"} @@ -120,7 +120,7 @@ use ybc::ModalCloserContext; pub fn MyModal1() -> Html { let msg_ctx = use_context::().unwrap(); let onclick = { - Callback::from(move |e:MouseEvent| msg_ctx.dispatch( "id0-close".to_string())) + Callback::from(move |e:MouseEvent| msg_ctx.dispatch( "id0-close".to_string().parse().unwrap())) }; let on_click_cb = Callback::from(move |e: AttrValue| { gloo_console::log!("Button clicked!"); @@ -161,11 +161,11 @@ pub fn MyModal1() -> Html { pub fn MyModal2() -> Html { let msg_ctx = use_context::().unwrap(); let onclick = { - Callback::from(move |e:MouseEvent| msg_ctx.dispatch( "id2-close".to_string())) + Callback::from(move |e:MouseEvent| msg_ctx.dispatch( "id2-close".to_string().parse().unwrap())) }; let msg_ctx2 = use_context::().unwrap(); let onsave = { - Callback::from(move |e:MouseEvent| msg_ctx2.dispatch( "id2-close".to_string())) + Callback::from(move |e:MouseEvent| msg_ctx2.dispatch( "id2-close".to_string().parse().unwrap())) }; let on_click_cb = Callback::from(move |e: AttrValue| { gloo_console::log!("Button clicked!"); From 131b3fa31f5df02f7436c950e2a1f51aa87a3013 Mon Sep 17 00:00:00 2001 From: kostya Date: Wed, 5 Jun 2024 14:49:05 -0700 Subject: [PATCH 07/58] remove debug comments --- src/components/modal.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/modal.rs b/src/components/modal.rs index 11ecbb8..b0808eb 100644 --- a/src/components/modal.rs +++ b/src/components/modal.rs @@ -147,14 +147,14 @@ pub fn modal_card(props: &ModalCardProps) -> Html { (Callback::noop(), Callback::from(move |e:MouseEvent| { let target = e.target(); - gloo_console::log!("Close event from modal-card: {:?}"); + // gloo_console::log!("Close event from modal-card: {:?}"); // Check if the target is an element that you want to ignore if let Some(target) = target { let target_element = target.dyn_into::().unwrap(); if target_element.id().starts_with("modal-ignore-") { // If the target is an element to ignore, stop the event propagation e.stop_propagation(); - gloo_console::log!("Ignoring event"); + // gloo_console::log!("Ignoring event"); return; } } @@ -162,10 +162,10 @@ pub fn modal_card(props: &ModalCardProps) -> Html { })) } else if _id == id { let is_active = is_active.clone(); - gloo_console::log!("is_active=false call"); + // gloo_console::log!("is_active=false call"); (Callback::from(move |_| is_active.set(true)), Callback::noop()) } else { - gloo_console::log!("NOOP call"); + // gloo_console::log!("NOOP call"); (Callback::noop(), Callback::noop()) }; @@ -215,7 +215,7 @@ pub fn modal_card2(props: &ModalCardProps) -> Html { if _id == id && closed { is_active.set(false); closer_ctx.dispatch(id.clone()); - gloo_console::log!("closed!"); + // gloo_console::log!("closed!"); } let mut class = Classes::from("modal"); @@ -223,17 +223,17 @@ pub fn modal_card2(props: &ModalCardProps) -> Html { let (opencb, closecb) = if _id == id && *is_active { class.push("is-active"); - gloo_console::log!("is_active=true call"); + // gloo_console::log!("is_active=true call"); let is_active = is_active.clone(); (Callback::noop(), Callback::from(move |_| is_active.set(false))) } else if _id == id { let is_active = is_active.clone(); - gloo_console::log!("is_active=false call"); + // gloo_console::log!("is_active=false call"); (Callback::from(move |_| is_active.set(true)), Callback::noop()) } else { - gloo_console::log!("NOOP call"); + // gloo_console::log!("NOOP call"); (Callback::noop(), Callback::noop()) }; From 21236f94a0702bc0c9c2bf983fe11fdcffd4eb6d Mon Sep 17 00:00:00 2001 From: kostya Date: Wed, 5 Jun 2024 14:49:51 -0700 Subject: [PATCH 08/58] remove debug comments --- src/components/modal.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/components/modal.rs b/src/components/modal.rs index b0808eb..1c5a6ce 100644 --- a/src/components/modal.rs +++ b/src/components/modal.rs @@ -286,13 +286,7 @@ impl Reducible for ModalCloseMsg { /// /// First, in your component which is using this modal, configure a `ModalCloser` dispatcher. /// ```rust -/// use yew::agent::Dispatcher; -/// use yew::prelude::*; -/// // .. snip .. -/// fn create(props: Self::Properties, link: ComponentLink) -> Self { -/// let bridge = ModalCloser::dispatcher(); -/// Self { link, props, bridge } -/// } + /// ``` /// /// Next, in your component's `view` method, setup a callback to handle your component's close From a14f0e810f017214cc06f6937009b54e1675ba53 Mon Sep 17 00:00:00 2001 From: kostya Date: Wed, 5 Jun 2024 15:08:40 -0700 Subject: [PATCH 09/58] remove debug comments --- src/components/modal.rs | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/src/components/modal.rs b/src/components/modal.rs index 1c5a6ce..a6c3b01 100644 --- a/src/components/modal.rs +++ b/src/components/modal.rs @@ -116,8 +116,6 @@ pub fn modal_card(props: &ModalCardProps) -> Html { let id = props.id.clone(); let closer_ctx = use_context::().expect("Modal closer in context"); - // gloo_console::log!("closer_ctx: full ID {}", closer_ctx.0.as_str()); - let (_id, closed) = match closer_ctx.0.contains("-") { true => { let result = closer_ctx.0.split("-").collect::>(); @@ -127,12 +125,9 @@ pub fn modal_card(props: &ModalCardProps) -> Html { }; let is_active = use_state(|| false); - // gloo_console::log!("closer_ctx: {:?} id:{:?} is closed:", _id.clone().into(), id.clone().into(), closed); - if _id == id && closed { is_active.set(false); closer_ctx.dispatch(id.clone()); - // gloo_console::log!("closed!"); } let mut class = Classes::from("modal"); @@ -141,20 +136,16 @@ pub fn modal_card(props: &ModalCardProps) -> Html { let (opencb, closecb) = if _id == id && *is_active { class.push("is-active"); - // gloo_console::log!("is_active=true call"); let is_active = is_active.clone(); (Callback::noop(), Callback::from(move |e:MouseEvent| { let target = e.target(); - // gloo_console::log!("Close event from modal-card: {:?}"); - // Check if the target is an element that you want to ignore if let Some(target) = target { let target_element = target.dyn_into::().unwrap(); if target_element.id().starts_with("modal-ignore-") { // If the target is an element to ignore, stop the event propagation e.stop_propagation(); - // gloo_console::log!("Ignoring event"); return; } } @@ -165,7 +156,6 @@ pub fn modal_card(props: &ModalCardProps) -> Html { // gloo_console::log!("is_active=false call"); (Callback::from(move |_| is_active.set(true)), Callback::noop()) } else { - // gloo_console::log!("NOOP call"); (Callback::noop(), Callback::noop()) }; @@ -199,7 +189,6 @@ pub fn modal_card2(props: &ModalCardProps) -> Html { let id = props.id.clone(); let closer_ctx = use_context::().expect("Modal closer in context"); - // gloo_console::log!("closer_ctx: full ID {}", closer_ctx.0.as_str()); let action = closer_ctx.0.as_str(); let (_id, closed) = match action.contains("-") { true => { @@ -210,12 +199,10 @@ pub fn modal_card2(props: &ModalCardProps) -> Html { }; let is_active = use_state(|| false); - // gloo_console::log!("closer_ctx: {:?} id:{:?} is closed:", _id.clone().into(), id.clone().into(), closed); if _id == id && closed { is_active.set(false); closer_ctx.dispatch(id.clone()); - // gloo_console::log!("closed!"); } let mut class = Classes::from("modal"); @@ -230,10 +217,8 @@ pub fn modal_card2(props: &ModalCardProps) -> Html { (Callback::noop(), Callback::from(move |_| is_active.set(false))) } else if _id == id { let is_active = is_active.clone(); - // gloo_console::log!("is_active=false call"); (Callback::from(move |_| is_active.set(true)), Callback::noop()) } else { - // gloo_console::log!("NOOP call"); (Callback::noop(), Callback::noop()) }; @@ -302,14 +287,6 @@ impl Reducible for ModalCloseMsg { /// /> /// ``` /// -/// Finally, in your component's `update` method, send the `ModalCloseMsg` over to the agent which -/// will forward the message to the modal to cause it to close. -/// ```rust -/// fn update(&mut self, msg: Self::Message) -> ShouldRender { -/// self.bridge.send(msg); -/// true -/// } -/// ``` /// /// This pattern allows you to communicate with a modal by its given ID, allowing /// you to close the modal from anywhere in your application. From d3c2c8ee5b31d5d73dae992c7b2acfd8c0179207 Mon Sep 17 00:00:00 2001 From: kostya Date: Wed, 5 Jun 2024 15:09:23 -0700 Subject: [PATCH 10/58] remove debug comments --- src/components/navbar.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/navbar.rs b/src/components/navbar.rs index fd9b850..e8c5796 100644 --- a/src/components/navbar.rs +++ b/src/components/navbar.rs @@ -87,7 +87,6 @@ impl Component for Navbar { } NavbarMsg::CloseEvent(state) => { self.state = state; - // gloo_console::log!("state: {:?}", self.state.total_clicks); self.is_menu_open = false; } } From a01f6073f393ecc480cec5e1f636e56e216bf3dd Mon Sep 17 00:00:00 2001 From: kostya Date: Wed, 5 Jun 2024 15:25:18 -0700 Subject: [PATCH 11/58] add Calendar with datetime type --- src/components/calendar.rs | 138 +++++++++++++++++++++++++++++++++++++ src/components/mod.rs | 1 + src/lib.rs | 2 + 3 files changed, 141 insertions(+) create mode 100644 src/components/calendar.rs diff --git a/src/components/calendar.rs b/src/components/calendar.rs new file mode 100644 index 0000000..e6a25bf --- /dev/null +++ b/src/components/calendar.rs @@ -0,0 +1,138 @@ +use wasm_bindgen::closure::Closure; +use wasm_bindgen::JsValue; +use wasm_bindgen::prelude::wasm_bindgen; +use web_sys::Element; +use yew::{Callback, Component, Context, Html}; +use yew::prelude::*; + +pub struct Calendar { + format: String, + date: Option, + id: String, +} + + +#[derive(Clone, PartialEq, Properties)] +pub struct CalendarProps { + pub id: String, + #[prop_or_default("yyyy-MM-dd")] + pub date_format: String, + #[prop_or_default("HH:mm")] + pub time_format: String, + pub date: Option, + pub on_date_changed: Callback, + pub class: Vec, +} + +pub enum Msg { + DateChanged(String), +} + +impl Component for Calendar { + type Message = Msg; + type Properties = CalendarProps; + + fn create(ctx: &Context) -> Self { + Calendar { + format: "%Y-%m-%d %H:%M".to_string(), + date: ctx.props().date.clone(), + id: ctx.props().id.clone(), + } + } + + fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { + match msg { + Msg::DateChanged(date) => { + self.date = Some(date.clone()); + ctx.props().on_date_changed.emit(date); + true + } + } + } + + fn view(&self, ctx: &Context) -> Html { + let _value = self.date.clone(); + let _id = self.id.clone(); + let classes = classes!("input", ctx.props().class.clone()); + html! { + } + } + + fn rendered(&mut self, ctx: &Context, first_render: bool) { + if first_render { + let window = web_sys::window().expect("no global `window` exists"); + let document = window.document().expect("should have a document on window"); + + let element = document + .get_element_by_id(self.id.as_str()) + .expect(format!("should have #{} on the page", self.id.as_str()).as_str()); + + // Clone the link from the context + let link = ctx.link().clone(); + + // Move the cloned link into the closure + let callback = Closure::wrap(Box::new(move |date: JsValue| { + let date_str = date.as_string().unwrap_or_default(); + link.send_message(Msg::DateChanged(date_str)); + }) as Box); + + unsafe { + setup_date_picker(&element, callback.as_ref(), &JsValue::from(self.date.as_ref().unwrap_or(&"".to_string())), + &JsValue::from(ctx.props().date_format.as_str()), &JsValue::from(ctx.props().time_format.as_str())); + } + + // Don't forget to forget the callback, otherwise it will be cleaned up when it goes out of scope. + callback.forget(); + } + } + + + fn destroy(&mut self, ctx: &Context) { + unsafe { + detach_date_picker(&JsValue::from(self.id.as_str())); + } + } +} + +#[wasm_bindgen(inline_js = r#" +let init = new Map(); +export function setup_date_picker(element, callback, initial_date, date_format, time_format) { + // console.log('Setting up date picker ID:' + element.id); + if (!init.has(element.id)) { + let calendarInstances = bulmaCalendar.attach(element, { + type: 'datetime', + color: 'info', + lang: 'en', + dateFormat: dateFormat, + timeFormat: timeFormat, + // other options... + }); + init.set(element.id, calendarInstances[0]); + let calendarInstance = calendarInstances[0]; + calendarInstance.on('select', function(datepicker) { + // console.log("Selected date: " + datepicker.data.value()); + callback(datepicker.data.value()); + // You can add more code here to handle the selected date + }); + } + // console.log('Setting up date picker:' + initialDate); + // console.dir(bulmaCalendar); + init.get(element.id).value(initialDate); + + // Define the callback function here + + +} + +export function detach_date_picker(id) { + init.delete(id); + // console.log('Detaching date picker #id='+id+'!'); +} + + +"#)] +extern "C" { + fn setup_date_picker(element: &Element, callback: &JsValue, initial_date: &JsValue, date_format: &JsValue, time_format: &JsValue); + fn detach_date_picker(id: &JsValue); +} diff --git a/src/components/mod.rs b/src/components/mod.rs index 3044693..64809df 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -8,3 +8,4 @@ pub mod navbar; pub mod pagination; pub mod panel; pub mod tabs; +pub mod calendar; diff --git a/src/lib.rs b/src/lib.rs index 330aca6..391bb78 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -53,6 +53,8 @@ pub use components::pagination::{ pub use components::panel::{Panel, PanelBlock, PanelBlockProps, PanelProps, PanelTabs, PanelTabsProps}; pub use components::tabs::{Tabs, TabsProps}; +pub use components::calendar::{Calendar, CalendarProps}; + // elements pub use elements::block::{Block, BlockProps}; pub use elements::button::{ From cfb00ba8ebd976f20b283f474f1d81c623a6e170 Mon Sep 17 00:00:00 2001 From: goodidea-kp <33940472+goodidea-kp@users.noreply.github.com> Date: Wed, 5 Jun 2024 17:28:22 -0500 Subject: [PATCH 12/58] Create rust.yml --- .github/workflows/rust.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/rust.yml diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..000bb2c --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,22 @@ +name: Rust + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Build + run: cargo build --verbose + - name: Run tests + run: cargo test --verbose From 67edac791d702b5a4a96c1b00f38318346daab7c Mon Sep 17 00:00:00 2001 From: kostya Date: Wed, 5 Jun 2024 15:32:12 -0700 Subject: [PATCH 13/58] fix tests --- src/components/modal.rs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/components/modal.rs b/src/components/modal.rs index a6c3b01..6c519df 100644 --- a/src/components/modal.rs +++ b/src/components/modal.rs @@ -272,24 +272,6 @@ impl Reducible for ModalCloseMsg { /// First, in your component which is using this modal, configure a `ModalCloser` dispatcher. /// ```rust -/// ``` -/// -/// Next, in your component's `view` method, setup a callback to handle your component's close -/// event. ```rust -/// let closer = self.link.callback(|_| ModalCloseMsg("modal-0".into())); -/// // ... snip ... -/// {"Close"} -/// } -/// /> -/// ``` -/// -/// -/// This pattern allows you to communicate with a modal by its given ID, allowing -/// you to close the modal from anywhere in your application. pub struct ModalCloser { link: WorkerScope, subscribers: HashSet, From da2fc54a8b1a02370e39976300f495eae067380d Mon Sep 17 00:00:00 2001 From: kostya Date: Thu, 6 Jun 2024 12:12:49 -0700 Subject: [PATCH 14/58] fix defect --- src/components/calendar.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/calendar.rs b/src/components/calendar.rs index e6a25bf..b72c657 100644 --- a/src/components/calendar.rs +++ b/src/components/calendar.rs @@ -104,8 +104,8 @@ export function setup_date_picker(element, callback, initial_date, date_format, type: 'datetime', color: 'info', lang: 'en', - dateFormat: dateFormat, - timeFormat: timeFormat, + dateFormat: date_format, + timeFormat: time_format, // other options... }); init.set(element.id, calendarInstances[0]); @@ -118,7 +118,7 @@ export function setup_date_picker(element, callback, initial_date, date_format, } // console.log('Setting up date picker:' + initialDate); // console.dir(bulmaCalendar); - init.get(element.id).value(initialDate); + init.get(element.id).value(initial_date); // Define the callback function here From 34c6e26b36e8e1376af8db8675d2d383b4219022 Mon Sep 17 00:00:00 2001 From: kostya Date: Thu, 6 Jun 2024 13:35:52 -0700 Subject: [PATCH 15/58] fix defect in format --- src/components/calendar.rs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/components/calendar.rs b/src/components/calendar.rs index b72c657..d871d2e 100644 --- a/src/components/calendar.rs +++ b/src/components/calendar.rs @@ -15,9 +15,9 @@ pub struct Calendar { #[derive(Clone, PartialEq, Properties)] pub struct CalendarProps { pub id: String, - #[prop_or_default("yyyy-MM-dd")] + #[prop_or_default] pub date_format: String, - #[prop_or_default("HH:mm")] + #[prop_or_default] pub time_format: String, pub date: Option, pub on_date_changed: Callback, @@ -78,8 +78,20 @@ impl Component for Calendar { }) as Box); unsafe { + let _date_format = if ctx.props().date_format.trim().len() == 0 { + "yyyy-MM-dd".to_string() + } else { + ctx.props().date_format.trim().to_string() + }; + + let _time_format = if ctx.props().time_format.trim().len() == 0 { + "HH:mm".to_string() + } else { + ctx.props().time_format.trim().to_string() + }; + setup_date_picker(&element, callback.as_ref(), &JsValue::from(self.date.as_ref().unwrap_or(&"".to_string())), - &JsValue::from(ctx.props().date_format.as_str()), &JsValue::from(ctx.props().time_format.as_str())); + &JsValue::from(_date_format), &JsValue::from(_time_format)); } // Don't forget to forget the callback, otherwise it will be cleaned up when it goes out of scope. @@ -98,7 +110,7 @@ impl Component for Calendar { #[wasm_bindgen(inline_js = r#" let init = new Map(); export function setup_date_picker(element, callback, initial_date, date_format, time_format) { - // console.log('Setting up date picker ID:' + element.id); + // console.log('Setting up date picker ID:' + element.id + 'date format:' + date_format + ' time format:' + time_format); if (!init.has(element.id)) { let calendarInstances = bulmaCalendar.attach(element, { type: 'datetime', @@ -116,7 +128,7 @@ export function setup_date_picker(element, callback, initial_date, date_format, // You can add more code here to handle the selected date }); } - // console.log('Setting up date picker:' + initialDate); + // console.log('Setting up date picker:' + initial_date); // console.dir(bulmaCalendar); init.get(element.id).value(initial_date); From 49e2d1961d61bb5690b48c21934091531e064b0c Mon Sep 17 00:00:00 2001 From: kostya Date: Sun, 9 Jun 2024 11:50:26 -0700 Subject: [PATCH 16/58] added calendar use example, fix minor warnings --- examples/basic/index.html | 5 +++-- examples/basic/src/main.rs | 26 ++++++++++++++++++++++---- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/examples/basic/index.html b/examples/basic/index.html index 2b9bf08..25e2401 100644 --- a/examples/basic/index.html +++ b/examples/basic/index.html @@ -1,11 +1,11 @@ - + Trunk | Yew | YBC - + @@ -15,5 +15,6 @@ + diff --git a/examples/basic/src/main.rs b/examples/basic/src/main.rs index 0154562..acd6f27 100644 --- a/examples/basic/src/main.rs +++ b/examples/basic/src/main.rs @@ -5,6 +5,7 @@ use console_error_panic_hook::set_once as set_panic_hook; use wasm_bindgen::prelude::*; use ybc::TileCtx::{Ancestor, Child, Parent}; use yew::prelude::*; +use ybc::Calendar; use ybc::{ NavBurgerCloserState}; @@ -12,6 +13,14 @@ use ybc::{ NavBurgerCloserState}; #[function_component(App)] pub fn app() -> Html { let state = Rc::new(NavBurgerCloserState { total_clicks: 0 }); + let cb_date_changed = Callback::from(|date: String| { + gloo_console::log!("Date changed: {}", date); + }); + + let calendar_departure_date = html! { + + }; + html! { <> > context={state}> @@ -80,6 +89,15 @@ pub fn app() -> Html { + + + + + {calendar_departure_date} + + + + @@ -116,8 +134,8 @@ use ybc::ModalCloserProvider; use ybc::ModalCloserContext; -#[function_component] -pub fn MyModal1() -> Html { +#[function_component(MyModal1)] +pub fn my_modal1() -> Html { let msg_ctx = use_context::().unwrap(); let onclick = { Callback::from(move |e:MouseEvent| msg_ctx.dispatch( "id0-close".to_string().parse().unwrap())) @@ -157,8 +175,8 @@ pub fn MyModal1() -> Html { } } -#[function_component] -pub fn MyModal2() -> Html { +#[function_component(MyModal2)] +pub fn my_modal2() -> Html { let msg_ctx = use_context::().unwrap(); let onclick = { Callback::from(move |e:MouseEvent| msg_ctx.dispatch( "id2-close".to_string().parse().unwrap())) From bf5f8bba82af92775e01041daa8e1be5b551ef32 Mon Sep 17 00:00:00 2001 From: kostya Date: Sun, 9 Jun 2024 12:01:15 -0700 Subject: [PATCH 17/58] added calendar use example, fix minor warnings --- examples/basic/index.html | 2 + examples/basic/src/main.rs | 8 ++ src/components/accordion.rs | 142 ++++++++++++++++++++++++++++++++++++ src/components/calendar.rs | 34 ++++----- src/components/mod.rs | 1 + src/components/modal.rs | 2 - src/lib.rs | 1 + 7 files changed, 169 insertions(+), 21 deletions(-) create mode 100644 src/components/accordion.rs diff --git a/examples/basic/index.html b/examples/basic/index.html index 25e2401..d10afc1 100644 --- a/examples/basic/index.html +++ b/examples/basic/index.html @@ -6,6 +6,7 @@ Trunk | Yew | YBC + @@ -16,5 +17,6 @@ + diff --git a/examples/basic/src/main.rs b/examples/basic/src/main.rs index acd6f27..685b745 100644 --- a/examples/basic/src/main.rs +++ b/examples/basic/src/main.rs @@ -81,6 +81,14 @@ pub fn app() -> Html { {"YBC"}

{"A Yew component library based on the Bulma CSS framework."}

+ + +

{"This is the content of the first accordion."}

+
+ +

{"This is the content of the second accordion."}

+
+
diff --git a/src/components/accordion.rs b/src/components/accordion.rs new file mode 100644 index 0000000..2d11510 --- /dev/null +++ b/src/components/accordion.rs @@ -0,0 +1,142 @@ +use wasm_bindgen::JsValue; +use wasm_bindgen::prelude::wasm_bindgen; +use web_sys::Element; +use yew::prelude::*; + + +#[function_component(AccordionItem)] +pub fn accordion_item(props: &AccordionItemProps) -> Html { + let accordion_classes = if props.open { + "accordion is-active" + } else { + "accordion" + }; + html! { +
+
+

{&props.title}

+
+
+
+ {props.children.clone()} +
+
+
+ } +} + +#[derive(Clone, Debug, PartialEq, Properties)] +pub struct AccordionsProps { + pub children: ChildrenWithProps, + pub id: String, +} + +pub struct Accordions { + props: AccordionsProps, + id: String, +} + +#[derive(Properties, Clone, PartialEq)] +pub struct AccordionItemProps { + pub title: String, + pub children: Children, + #[prop_or_default] + pub open: bool, + #[prop_or_else(Callback::noop)] + pub on_toggle: Callback, + #[prop_or_default] + pub id: String, +} + +impl Component for Accordions { + type Message = (); + type Properties = AccordionsProps; + + fn create(ctx: &Context) -> Self { + Self { + props: ctx.props().clone(), + id: format!("accordion-{}", ctx.props().id), + } + } + + fn update(&mut self, ctx: &Context, _msg: Self::Message) -> bool { + self.props = ctx.props().clone(); + true + } + + fn view(&self, ctx: &Context) -> Html { + html! { +
+ {for ctx.props().children.iter().map(|child| { + html! {child.clone()} + })} +
+ } + } + + fn rendered(&mut self, ctx: &Context, first_render: bool) { + if first_render { + let window = web_sys::window().expect("no global `window` exists"); + let document = window.document().expect("should have a document on window"); + + let element = document + .get_element_by_id(self.id.as_str()) + .expect(format!("should have #{} on the page", self.id.as_str()).as_str()); + + setup_accordion(&element); + } + } + + + fn destroy(&mut self, ctx: &Context) { + detach_accordion(&JsValue::from(self.id.as_str())); + } +} + +#[wasm_bindgen(inline_js = r#" +let accordionInstances = null; +export function setup_accordion(element) { + // console.log('Setting up accordion ID:' + element.id); + if (accordionInstances === null) { + accordionInstances = bulmaAccordion.attach('#' + element.id); + return; + } + + // Check if the accordion is already attached + for (let i = 0; i < accordionInstances.length; i++) { + if (accordionInstances[i].element && accordionInstances[i].element.id === element.id) { + // console.log('Accordion already attached to #id=' + element.id); + return; + } + } + + // If not attached, attach it + let newAccordion = bulmaAccordion.attach('#' + element.id); + accordionInstances.push(newAccordion); + // console.log('Accordion successfully attached to #id=' + element.id); + +} + +export function detach_accordion(id) { + for (let i = 0; i < accordionInstances.length; i++) { + if (accordionInstances[i] && accordionInstances[i].element && accordionInstances[i].element.id === id) { + // console.log('Detaching accordion #id='+id+'!'); + accordionInstances[i].destroy(); + accordionInstances.splice(i, 1); + // console.log(accordionInstances); // Log the accordionInstances array + break; + } + } + + if (accordionInstances.length === 0) { + accordionInstances = null; + // console.log('Detached accordion from all!'); + } +} + + +"#)] +extern "C" { + fn setup_accordion(element: &Element); + fn detach_accordion(id: &JsValue); +} diff --git a/src/components/calendar.rs b/src/components/calendar.rs index d871d2e..baee78a 100644 --- a/src/components/calendar.rs +++ b/src/components/calendar.rs @@ -77,22 +77,20 @@ impl Component for Calendar { link.send_message(Msg::DateChanged(date_str)); }) as Box); - unsafe { - let _date_format = if ctx.props().date_format.trim().len() == 0 { - "yyyy-MM-dd".to_string() - } else { - ctx.props().date_format.trim().to_string() - }; - - let _time_format = if ctx.props().time_format.trim().len() == 0 { - "HH:mm".to_string() - } else { - ctx.props().time_format.trim().to_string() - }; - - setup_date_picker(&element, callback.as_ref(), &JsValue::from(self.date.as_ref().unwrap_or(&"".to_string())), - &JsValue::from(_date_format), &JsValue::from(_time_format)); - } + let _date_format = if ctx.props().date_format.trim().len() == 0 { + "yyyy-MM-dd".to_string() + } else { + ctx.props().date_format.trim().to_string() + }; + + let _time_format = if ctx.props().time_format.trim().len() == 0 { + "HH:mm".to_string() + } else { + ctx.props().time_format.trim().to_string() + }; + + setup_date_picker(&element, callback.as_ref(), &JsValue::from(self.date.as_ref().unwrap_or(&"".to_string())), + &JsValue::from(_date_format), &JsValue::from(_time_format)); // Don't forget to forget the callback, otherwise it will be cleaned up when it goes out of scope. callback.forget(); @@ -101,9 +99,7 @@ impl Component for Calendar { fn destroy(&mut self, ctx: &Context) { - unsafe { - detach_date_picker(&JsValue::from(self.id.as_str())); - } + detach_date_picker(&JsValue::from(self.id.as_str())); } } diff --git a/src/components/mod.rs b/src/components/mod.rs index 64809df..36b1a22 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -9,3 +9,4 @@ pub mod pagination; pub mod panel; pub mod tabs; pub mod calendar; +pub mod accordion; diff --git a/src/components/modal.rs b/src/components/modal.rs index 6c519df..543ca93 100644 --- a/src/components/modal.rs +++ b/src/components/modal.rs @@ -6,8 +6,6 @@ use yew::prelude::*; use yew_agent::worker::{HandlerId, Worker, WorkerScope}; -use yew_agent::prelude::*; - /// Modal actions. pub enum ModalMsg { Open, diff --git a/src/lib.rs b/src/lib.rs index 391bb78..9314a46 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -50,6 +50,7 @@ pub use components::navbar::{ pub use components::pagination::{ Pagination, PaginationEllipsis, PaginationItem, PaginationItemProps, PaginationItemRouter, PaginationItemType, PaginationProps, }; +pub use components::accordion::{Accordions, AccordionsProps, AccordionItemProps, AccordionItem}; pub use components::panel::{Panel, PanelBlock, PanelBlockProps, PanelProps, PanelTabs, PanelTabsProps}; pub use components::tabs::{Tabs, TabsProps}; From b695db75040774d48de8071fc83f7a6d1ab66800 Mon Sep 17 00:00:00 2001 From: goodidea-kp <33940472+goodidea-kp@users.noreply.github.com> Date: Mon, 10 Jun 2024 14:35:54 -0500 Subject: [PATCH 18/58] Update ci.yaml --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0d2d1c9..d488e88 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,7 +12,7 @@ jobs: toolchain: stable target: wasm32-unknown-unknown default: true - components: clippy, rustfmt + components: clippy, rustfmt, cargo - uses: actions-rs/cargo@v1 with: command: clippy @@ -35,7 +35,7 @@ jobs: toolchain: stable target: wasm32-unknown-unknown default: true - components: clippy, rustfmt + components: clippy, rustfmt, cargo - name: fetch trunk run: wget -qO- https://github.com/thedodd/trunk/releases/download/v0.16.0/trunk-x86_64-unknown-linux-gnu.tar.gz | tar -xzf- - name: build example From 61eb95193893f02e5eddb22d36be7a04d8364f14 Mon Sep 17 00:00:00 2001 From: kostya Date: Mon, 10 Jun 2024 13:09:31 -0700 Subject: [PATCH 19/58] formatting --- examples/basic/src/main.rs | 28 +++++++++------------------- src/components/accordion.rs | 32 +++++++++++++------------------- src/components/calendar.rs | 15 +++++++++------ src/components/mod.rs | 4 ++-- src/components/modal.rs | 28 +++++++++++++++------------- src/elements/button.rs | 1 - src/lib.rs | 2 +- 7 files changed, 49 insertions(+), 61 deletions(-) diff --git a/examples/basic/src/main.rs b/examples/basic/src/main.rs index 685b745..333dc3a 100644 --- a/examples/basic/src/main.rs +++ b/examples/basic/src/main.rs @@ -1,14 +1,15 @@ #![recursion_limit = "1024"] -use std::rc::Rc; use console_error_panic_hook::set_once as set_panic_hook; +use std::rc::Rc; use wasm_bindgen::prelude::*; +use ybc::Calendar; use ybc::TileCtx::{Ancestor, Child, Parent}; use yew::prelude::*; -use ybc::Calendar; - -use ybc::{ NavBurgerCloserState}; +use ybc::ModalCloserContext; +use ybc::ModalCloserProvider; +use ybc::NavBurgerCloserState; #[function_component(App)] pub fn app() -> Html { @@ -137,17 +138,10 @@ fn main() { yew::Renderer::::new().render(); } - -use ybc::ModalCloserProvider; -use ybc::ModalCloserContext; - - #[function_component(MyModal1)] pub fn my_modal1() -> Html { let msg_ctx = use_context::().unwrap(); - let onclick = { - Callback::from(move |e:MouseEvent| msg_ctx.dispatch( "id0-close".to_string().parse().unwrap())) - }; + let onclick = { Callback::from(move |e: MouseEvent| msg_ctx.dispatch("id0-close".to_string().parse().unwrap())) }; let on_click_cb = Callback::from(move |e: AttrValue| { gloo_console::log!("Button clicked!"); }); @@ -186,13 +180,9 @@ pub fn my_modal1() -> Html { #[function_component(MyModal2)] pub fn my_modal2() -> Html { let msg_ctx = use_context::().unwrap(); - let onclick = { - Callback::from(move |e:MouseEvent| msg_ctx.dispatch( "id2-close".to_string().parse().unwrap())) - }; + let onclick = { Callback::from(move |e: MouseEvent| msg_ctx.dispatch("id2-close".to_string().parse().unwrap())) }; let msg_ctx2 = use_context::().unwrap(); - let onsave = { - Callback::from(move |e:MouseEvent| msg_ctx2.dispatch( "id2-close".to_string().parse().unwrap())) - }; + let onsave = { Callback::from(move |e: MouseEvent| msg_ctx2.dispatch("id2-close".to_string().parse().unwrap())) }; let on_click_cb = Callback::from(move |e: AttrValue| { gloo_console::log!("Button clicked!"); }); @@ -226,4 +216,4 @@ pub fn my_modal2() -> Html { )} /> } -} \ No newline at end of file +} diff --git a/src/components/accordion.rs b/src/components/accordion.rs index 2d11510..9ebf6e2 100644 --- a/src/components/accordion.rs +++ b/src/components/accordion.rs @@ -1,28 +1,23 @@ -use wasm_bindgen::JsValue; use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::JsValue; use web_sys::Element; use yew::prelude::*; - #[function_component(AccordionItem)] pub fn accordion_item(props: &AccordionItemProps) -> Html { - let accordion_classes = if props.open { - "accordion is-active" - } else { - "accordion" - }; + let accordion_classes = if props.open { "accordion is-active" } else { "accordion" }; html! { -
-
-

{&props.title}

-
-
-
- {props.children.clone()} -
-
-
- } +
+
+

{&props.title}

+
+
+
+ {props.children.clone()} +
+
+
+ } } #[derive(Clone, Debug, PartialEq, Properties)] @@ -87,7 +82,6 @@ impl Component for Accordions { } } - fn destroy(&mut self, ctx: &Context) { detach_accordion(&JsValue::from(self.id.as_str())); } diff --git a/src/components/calendar.rs b/src/components/calendar.rs index baee78a..222e616 100644 --- a/src/components/calendar.rs +++ b/src/components/calendar.rs @@ -1,9 +1,9 @@ use wasm_bindgen::closure::Closure; -use wasm_bindgen::JsValue; use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::JsValue; use web_sys::Element; -use yew::{Callback, Component, Context, Html}; use yew::prelude::*; +use yew::{Callback, Component, Context, Html}; pub struct Calendar { format: String, @@ -11,7 +11,6 @@ pub struct Calendar { id: String, } - #[derive(Clone, PartialEq, Properties)] pub struct CalendarProps { pub id: String, @@ -89,15 +88,19 @@ impl Component for Calendar { ctx.props().time_format.trim().to_string() }; - setup_date_picker(&element, callback.as_ref(), &JsValue::from(self.date.as_ref().unwrap_or(&"".to_string())), - &JsValue::from(_date_format), &JsValue::from(_time_format)); + setup_date_picker( + &element, + callback.as_ref(), + &JsValue::from(self.date.as_ref().unwrap_or(&"".to_string())), + &JsValue::from(_date_format), + &JsValue::from(_time_format), + ); // Don't forget to forget the callback, otherwise it will be cleaned up when it goes out of scope. callback.forget(); } } - fn destroy(&mut self, ctx: &Context) { detach_date_picker(&JsValue::from(self.id.as_str())); } diff --git a/src/components/mod.rs b/src/components/mod.rs index 36b1a22..60b2072 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,4 +1,6 @@ +pub mod accordion; pub mod breadcrumb; +pub mod calendar; pub mod card; pub mod dropdown; pub mod menu; @@ -8,5 +10,3 @@ pub mod navbar; pub mod pagination; pub mod panel; pub mod tabs; -pub mod calendar; -pub mod accordion; diff --git a/src/components/modal.rs b/src/components/modal.rs index 543ca93..372ec2a 100644 --- a/src/components/modal.rs +++ b/src/components/modal.rs @@ -132,23 +132,25 @@ pub fn modal_card(props: &ModalCardProps) -> Html { class.push(props.classes.clone()); let (opencb, closecb) = if _id == id && *is_active { - class.push("is-active"); let is_active = is_active.clone(); - (Callback::noop(), Callback::from(move |e:MouseEvent| { - let target = e.target(); - if let Some(target) = target { - let target_element = target.dyn_into::().unwrap(); - if target_element.id().starts_with("modal-ignore-") { - // If the target is an element to ignore, stop the event propagation - e.stop_propagation(); - return; + ( + Callback::noop(), + Callback::from(move |e: MouseEvent| { + let target = e.target(); + if let Some(target) = target { + let target_element = target.dyn_into::().unwrap(); + if target_element.id().starts_with("modal-ignore-") { + // If the target is an element to ignore, stop the event propagation + e.stop_propagation(); + return; + } } - } - is_active.set(false) - })) + is_active.set(false) + }), + ) } else if _id == id { let is_active = is_active.clone(); // gloo_console::log!("is_active=false call"); @@ -197,7 +199,6 @@ pub fn modal_card2(props: &ModalCardProps) -> Html { }; let is_active = use_state(|| false); - if _id == id && closed { is_active.set(false); closer_ctx.dispatch(id.clone()); @@ -303,6 +304,7 @@ impl Worker for ModalCloser { } } } + #[derive(Properties, Debug, PartialEq)] pub struct ModalCloserProviderProps { #[prop_or_default] diff --git a/src/elements/button.rs b/src/elements/button.rs index daffc42..70a1023 100644 --- a/src/elements/button.rs +++ b/src/elements/button.rs @@ -63,7 +63,6 @@ pub struct ButtonProps { pub disabled: bool, #[prop_or_default] pub id: String, - } /// A button element. diff --git a/src/lib.rs b/src/lib.rs index 9314a46..a56aec9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,6 +32,7 @@ pub use columns::{Column, ColumnProps, Columns, ColumnsProps}; pub use common::{Alignment, Size}; // components +pub use components::accordion::{AccordionItem, AccordionItemProps, Accordions, AccordionsProps}; pub use components::breadcrumb::{Breadcrumb, BreadcrumbProps, BreadcrumbSeparator, BreadcrumbSize}; pub use components::card::{ Card, CardContent, CardContentProps, CardFooter, CardFooterProps, CardHeader, CardHeaderProps, CardImage, CardImageProps, CardProps, @@ -50,7 +51,6 @@ pub use components::navbar::{ pub use components::pagination::{ Pagination, PaginationEllipsis, PaginationItem, PaginationItemProps, PaginationItemRouter, PaginationItemType, PaginationProps, }; -pub use components::accordion::{Accordions, AccordionsProps, AccordionItemProps, AccordionItem}; pub use components::panel::{Panel, PanelBlock, PanelBlockProps, PanelProps, PanelTabs, PanelTabsProps}; pub use components::tabs::{Tabs, TabsProps}; From 139d50f2b0242d667b0dc69904a8d75ed134fd71 Mon Sep 17 00:00:00 2001 From: goodidea-kp <33940472+goodidea-kp@users.noreply.github.com> Date: Mon, 10 Jun 2024 15:21:35 -0500 Subject: [PATCH 20/58] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e5c8ee2..c120295 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ First, add this library to your `Cargo.toml` dependencies. ```toml [dependencies] -ybc = "*" +ybc = { git = "https://github.com/goodidea-kp/ybc.git" } ``` ### add bulma From 3df8a06506d67493dd074862fa13827f4c653721 Mon Sep 17 00:00:00 2001 From: kostya Date: Thu, 13 Jun 2024 12:27:42 -0700 Subject: [PATCH 21/58] fix defect and add step --- src/components/calendar.rs | 19 +++++++--- src/form/input.rs | 71 ++++++++++++++++++++++++++++++++------ 2 files changed, 75 insertions(+), 15 deletions(-) diff --git a/src/components/calendar.rs b/src/components/calendar.rs index 222e616..cb44a10 100644 --- a/src/components/calendar.rs +++ b/src/components/calendar.rs @@ -73,8 +73,14 @@ impl Component for Calendar { // Move the cloned link into the closure let callback = Closure::wrap(Box::new(move |date: JsValue| { let date_str = date.as_string().unwrap_or_default(); + // gloo_console::log!("Date changed: {}", date_str.clone()); link.send_message(Msg::DateChanged(date_str)); }) as Box); + let link = ctx.link().clone(); + let clear_callback = Closure::wrap(Box::new(move || { + // gloo_console::log!("Date cleared"); + link.send_message(Msg::DateChanged("".to_string())); + }) as Box); let _date_format = if ctx.props().date_format.trim().len() == 0 { "yyyy-MM-dd".to_string() @@ -91,6 +97,7 @@ impl Component for Calendar { setup_date_picker( &element, callback.as_ref(), + clear_callback.as_ref(), &JsValue::from(self.date.as_ref().unwrap_or(&"".to_string())), &JsValue::from(_date_format), &JsValue::from(_time_format), @@ -98,6 +105,7 @@ impl Component for Calendar { // Don't forget to forget the callback, otherwise it will be cleaned up when it goes out of scope. callback.forget(); + clear_callback.forget(); } } @@ -108,7 +116,7 @@ impl Component for Calendar { #[wasm_bindgen(inline_js = r#" let init = new Map(); -export function setup_date_picker(element, callback, initial_date, date_format, time_format) { +export function setup_date_picker(element, callback, clear_callback, initial_date, date_format, time_format) { // console.log('Setting up date picker ID:' + element.id + 'date format:' + date_format + ' time format:' + time_format); if (!init.has(element.id)) { let calendarInstances = bulmaCalendar.attach(element, { @@ -125,7 +133,10 @@ export function setup_date_picker(element, callback, initial_date, date_format, // console.log("Selected date: " + datepicker.data.value()); callback(datepicker.data.value()); // You can add more code here to handle the selected date - }); + }); + calendarInstance.on('clear', function(datepicker) { + clear_callback(); + }); } // console.log('Setting up date picker:' + initial_date); // console.dir(bulmaCalendar); @@ -138,12 +149,12 @@ export function setup_date_picker(element, callback, initial_date, date_format, export function detach_date_picker(id) { init.delete(id); - // console.log('Detaching date picker #id='+id+'!'); + console.log('Detaching date picker #id='+id+'!'); } "#)] extern "C" { - fn setup_date_picker(element: &Element, callback: &JsValue, initial_date: &JsValue, date_format: &JsValue, time_format: &JsValue); + fn setup_date_picker(element: &Element, callback: &JsValue, clear_callback: &JsValue,initial_date: &JsValue, date_format: &JsValue, time_format: &JsValue); fn detach_date_picker(id: &JsValue); } diff --git a/src/form/input.rs b/src/form/input.rs index 1dd7450..6c0dca8 100644 --- a/src/form/input.rs +++ b/src/form/input.rs @@ -40,6 +40,9 @@ pub struct InputProps { /// Make this component static. #[prop_or_default] pub r#static: bool, + + #[prop_or_default] + pub step: f32, } /// A text input element. @@ -59,21 +62,67 @@ pub fn input(props: &InputProps) -> Html { props.loading.then_some("is-loading"), props.r#static.then_some("is-static"), ); - let oninput = props.update.reform(|ev: web_sys::InputEvent| { + let oninput_text = props.update.reform(|ev: web_sys::InputEvent| { let input: HtmlInputElement = ev.target_dyn_into().expect_throw("event target should be an input"); input.value() }); + + + let input_ref = use_node_ref(); + + let oninput_number =props.update.reform({ + let input_ref = input_ref.clone(); + move |_| { + if let Some(input) = input_ref.cast::() { + input.set_custom_validity(""); + input.check_validity(); + return input.value(); + }; + "".to_string() + } + }); + + let oninvalid = Callback::from({ + let input_ref = input_ref.clone(); + move |_| { + if let Some(input) = input_ref.cast::() { + if input.value() == "" { + input.set_custom_validity(""); + } else { + input.set_custom_validity("Please enter a number with up to two decimal places."); + } + } + } + }); + html! { - + if props.r#type == InputType::Number { + + } else { + + } } } From 4bf15d868b77bd385f60aebe8e76775c1c99c850 Mon Sep 17 00:00:00 2001 From: kostya Date: Thu, 13 Jun 2024 12:31:05 -0700 Subject: [PATCH 22/58] formatting --- src/components/calendar.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/calendar.rs b/src/components/calendar.rs index cb44a10..7def5b2 100644 --- a/src/components/calendar.rs +++ b/src/components/calendar.rs @@ -155,6 +155,6 @@ export function detach_date_picker(id) { "#)] extern "C" { - fn setup_date_picker(element: &Element, callback: &JsValue, clear_callback: &JsValue,initial_date: &JsValue, date_format: &JsValue, time_format: &JsValue); + fn setup_date_picker(element: &Element, callback: &JsValue, clear_callback: &JsValue, initial_date: &JsValue, date_format: &JsValue, time_format: &JsValue); fn detach_date_picker(id: &JsValue); } From 39c10a0bc556f157b1af5bc8e4f0a5e23aa974bc Mon Sep 17 00:00:00 2001 From: kostya Date: Thu, 13 Jun 2024 12:35:15 -0700 Subject: [PATCH 23/58] formatting --- src/components/calendar.rs | 4 +++- src/form/input.rs | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/calendar.rs b/src/components/calendar.rs index 7def5b2..c168c77 100644 --- a/src/components/calendar.rs +++ b/src/components/calendar.rs @@ -155,6 +155,8 @@ export function detach_date_picker(id) { "#)] extern "C" { - fn setup_date_picker(element: &Element, callback: &JsValue, clear_callback: &JsValue, initial_date: &JsValue, date_format: &JsValue, time_format: &JsValue); + fn setup_date_picker( + element: &Element, callback: &JsValue, clear_callback: &JsValue, initial_date: &JsValue, date_format: &JsValue, time_format: &JsValue, + ); fn detach_date_picker(id: &JsValue); } diff --git a/src/form/input.rs b/src/form/input.rs index 6c0dca8..f3ee864 100644 --- a/src/form/input.rs +++ b/src/form/input.rs @@ -67,10 +67,9 @@ pub fn input(props: &InputProps) -> Html { input.value() }); - let input_ref = use_node_ref(); - let oninput_number =props.update.reform({ + let oninput_number = props.update.reform({ let input_ref = input_ref.clone(); move |_| { if let Some(input) = input_ref.cast::() { From 80c65f8033c2a9bbc973cc9fd3118db009c99279 Mon Sep 17 00:00:00 2001 From: kostya Date: Sat, 15 Jun 2024 13:29:59 -0700 Subject: [PATCH 24/58] simplify and fix defect --- src/components/calendar.rs | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/src/components/calendar.rs b/src/components/calendar.rs index c168c77..cbdf294 100644 --- a/src/components/calendar.rs +++ b/src/components/calendar.rs @@ -67,20 +67,16 @@ impl Component for Calendar { .get_element_by_id(self.id.as_str()) .expect(format!("should have #{} on the page", self.id.as_str()).as_str()); + // Clone the link from the context let link = ctx.link().clone(); // Move the cloned link into the closure let callback = Closure::wrap(Box::new(move |date: JsValue| { let date_str = date.as_string().unwrap_or_default(); - // gloo_console::log!("Date changed: {}", date_str.clone()); + gloo_console::log!("Date changed: {}", date_str.clone()); link.send_message(Msg::DateChanged(date_str)); }) as Box); - let link = ctx.link().clone(); - let clear_callback = Closure::wrap(Box::new(move || { - // gloo_console::log!("Date cleared"); - link.send_message(Msg::DateChanged("".to_string())); - }) as Box); let _date_format = if ctx.props().date_format.trim().len() == 0 { "yyyy-MM-dd".to_string() @@ -97,15 +93,11 @@ impl Component for Calendar { setup_date_picker( &element, callback.as_ref(), - clear_callback.as_ref(), &JsValue::from(self.date.as_ref().unwrap_or(&"".to_string())), &JsValue::from(_date_format), &JsValue::from(_time_format), ); - - // Don't forget to forget the callback, otherwise it will be cleaned up when it goes out of scope. callback.forget(); - clear_callback.forget(); } } @@ -116,7 +108,7 @@ impl Component for Calendar { #[wasm_bindgen(inline_js = r#" let init = new Map(); -export function setup_date_picker(element, callback, clear_callback, initial_date, date_format, time_format) { +export function setup_date_picker(element, callback, initial_date, date_format, time_format) { // console.log('Setting up date picker ID:' + element.id + 'date format:' + date_format + ' time format:' + time_format); if (!init.has(element.id)) { let calendarInstances = bulmaCalendar.attach(element, { @@ -130,15 +122,20 @@ export function setup_date_picker(element, callback, clear_callback, initial_dat init.set(element.id, calendarInstances[0]); let calendarInstance = calendarInstances[0]; calendarInstance.on('select', function(datepicker) { - // console.log("Selected date: " + datepicker.data.value()); + // console.log("Selected date: " + datepicker.data.value()); callback(datepicker.data.value()); // You can add more code here to handle the selected date }); calendarInstance.on('clear', function(datepicker) { - clear_callback(); + callback(''); + }); + calendarInstance.on('validate', function(datepicker) { + // console.log("Selected date: " + datepicker.data.value()); + callback(datepicker.data.value()); + // You can add more code here to handle the selected date }); } - // console.log('Setting up date picker:' + initial_date); + console.log('Setting up date picker:' + initial_date); // console.dir(bulmaCalendar); init.get(element.id).value(initial_date); @@ -156,7 +153,7 @@ export function detach_date_picker(id) { "#)] extern "C" { fn setup_date_picker( - element: &Element, callback: &JsValue, clear_callback: &JsValue, initial_date: &JsValue, date_format: &JsValue, time_format: &JsValue, + element: &Element, callback: &JsValue, initial_date: &JsValue, date_format: &JsValue, time_format: &JsValue, ); fn detach_date_picker(id: &JsValue); } From 2b152707f600a40e810044d58b1375240b094866 Mon Sep 17 00:00:00 2001 From: kostya Date: Sat, 15 Jun 2024 13:32:21 -0700 Subject: [PATCH 25/58] remove logging --- src/components/calendar.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/calendar.rs b/src/components/calendar.rs index cbdf294..e86eb4d 100644 --- a/src/components/calendar.rs +++ b/src/components/calendar.rs @@ -74,7 +74,7 @@ impl Component for Calendar { // Move the cloned link into the closure let callback = Closure::wrap(Box::new(move |date: JsValue| { let date_str = date.as_string().unwrap_or_default(); - gloo_console::log!("Date changed: {}", date_str.clone()); + // gloo_console::log!("Date changed: {}", date_str.clone()); link.send_message(Msg::DateChanged(date_str)); }) as Box); @@ -135,7 +135,7 @@ export function setup_date_picker(element, callback, initial_date, date_format, // You can add more code here to handle the selected date }); } - console.log('Setting up date picker:' + initial_date); + // console.log('Setting up date picker:' + initial_date); // console.dir(bulmaCalendar); init.get(element.id).value(initial_date); @@ -146,7 +146,7 @@ export function setup_date_picker(element, callback, initial_date, date_format, export function detach_date_picker(id) { init.delete(id); - console.log('Detaching date picker #id='+id+'!'); + // console.log('Detaching date picker #id='+id+'!'); } From 99bbf84f4c321300cd3f5c7f308b318d78513ce2 Mon Sep 17 00:00:00 2001 From: kostya Date: Sat, 15 Jun 2024 13:35:10 -0700 Subject: [PATCH 26/58] formatting --- src/components/calendar.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/calendar.rs b/src/components/calendar.rs index e86eb4d..b5d7370 100644 --- a/src/components/calendar.rs +++ b/src/components/calendar.rs @@ -67,7 +67,6 @@ impl Component for Calendar { .get_element_by_id(self.id.as_str()) .expect(format!("should have #{} on the page", self.id.as_str()).as_str()); - // Clone the link from the context let link = ctx.link().clone(); @@ -149,11 +148,8 @@ export function detach_date_picker(id) { // console.log('Detaching date picker #id='+id+'!'); } - "#)] extern "C" { - fn setup_date_picker( - element: &Element, callback: &JsValue, initial_date: &JsValue, date_format: &JsValue, time_format: &JsValue, - ); + fn setup_date_picker(element: &Element, callback: &JsValue, initial_date: &JsValue, date_format: &JsValue, time_format: &JsValue); fn detach_date_picker(id: &JsValue); } From 27fe1b5ba229e0f8d2dc627ec1f6b5d32790242e Mon Sep 17 00:00:00 2001 From: kostya Date: Fri, 21 Jun 2024 09:18:14 -0700 Subject: [PATCH 27/58] add autocomplete --- examples/basic/index.html | 2 + examples/basic/src/main.rs | 33 ++++++ src/components/autocomplete.rs | 183 +++++++++++++++++++++++++++++++++ src/components/mod.rs | 1 + src/lib.rs | 1 + 5 files changed, 220 insertions(+) create mode 100644 src/components/autocomplete.rs diff --git a/examples/basic/index.html b/examples/basic/index.html index d10afc1..eb83f58 100644 --- a/examples/basic/index.html +++ b/examples/basic/index.html @@ -7,6 +7,7 @@ + @@ -18,5 +19,6 @@ + diff --git a/examples/basic/src/main.rs b/examples/basic/src/main.rs index 333dc3a..5cee96f 100644 --- a/examples/basic/src/main.rs +++ b/examples/basic/src/main.rs @@ -18,6 +18,12 @@ pub fn app() -> Html { gloo_console::log!("Date changed: {}", date); }); + let cb_on_update = Callback::from(|tag: String| { + gloo_console::log!("Tag updated: {}", tag); + }); + let cb_on_remove = Callback::from(|tag: String| { + gloo_console::log!("Tag removed: {}", tag); + }); let calendar_departure_date = html! { }; @@ -107,6 +113,33 @@ pub fn app() -> Html {
+ + + + + + + + + + + + + + diff --git a/src/components/autocomplete.rs b/src/components/autocomplete.rs new file mode 100644 index 0000000..bddf3ea --- /dev/null +++ b/src/components/autocomplete.rs @@ -0,0 +1,183 @@ +use wasm_bindgen::closure::Closure; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::JsCast; +use wasm_bindgen::JsValue; +use web_sys::js_sys::{Reflect, JSON}; +use web_sys::{js_sys, Element}; +use yew::prelude::*; + +pub struct AutoComplete { + current_selector: String, + id: String, + items: Vec, +} + +#[derive(Clone, PartialEq, Properties)] +pub struct AutoCompleteProps { + #[prop_or("autocomplete".to_string())] + pub id: String, + #[prop_or(10)] + pub max_items: u32, + #[prop_or_default] + pub items: Vec, + pub on_update: Callback, + pub on_remove: Callback, + #[prop_or("".to_string())] + pub current_selector: String, + #[prop_or("Choose Tags".to_string())] + pub placeholder: String, + #[prop_or(classes!("".to_string()))] + pub classes: Classes, + #[prop_or(false)] + pub is_multiple: bool, +} + +pub enum Msg { + Added(String), + Removed(String), +} + +impl Component for AutoComplete { + type Message = Msg; + type Properties = AutoCompleteProps; + fn create(ctx: &Context) -> Self { + Self { + current_selector: ctx.props().current_selector.clone(), + id: ctx.props().id.clone(), + items: ctx.props().items.clone(), + } + } + + fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { + match msg { + Msg::Added(tag) => { + ctx.props().on_update.emit(tag); + // gloo_console::log!("Added: {}", tag.as_str()); + } + Msg::Removed(tag) => { + ctx.props().on_remove.emit(tag); + // gloo_console::log!("Removed: {}", tag.as_str()); + } + } + true + } + + fn view(&self, ctx: &Context) -> Html { + let items = self + .items + .iter() + .clone() + .map(|item| { + if item == &self.current_selector { + html! { + + } + } else { + html! { + + } + } + }) + .collect::(); + if ctx.props().is_multiple { + html! { +
+ +
+ } + } else { + html! { + + } + } + } + + fn rendered(&mut self, ctx: &Context, first_render: bool) { + if first_render { + let _max_items = ctx.props().max_items; + let _is_multiple = ctx.props().is_multiple; + let window = web_sys::window().expect("no global `window` exists"); + let document = window.document().expect("should have a document on window"); + + let element = document + .get_element_by_id(self.id.as_str()) + .expect(format!("should have #{} on the page", self.id.as_str()).as_str()); + + // Clone the link from the context + let link = ctx.link().clone(); + + // Move the cloned link into the closure + let callback = Closure::wrap(Box::new(move |tag: JsValue| { + // gloo_console::log!("Value changed: {}", tag.clone()); + let command: js_sys::Object = JSON::parse(tag.as_string().unwrap().as_str()).unwrap().dyn_into().unwrap(); + let op = Reflect::get(&command, &JsValue::from_str("op")).unwrap(); + let value = Reflect::get(&command, &JsValue::from_str("value")).unwrap(); + if op.as_string().unwrap() == "add" { + link.send_message(Msg::Added(value.as_string().unwrap())); + } else { + link.send_message(Msg::Removed(value.as_string().unwrap())); + } + }) as Box); + + setup_autocomplete(&element, callback.as_ref(), &JsValue::from(_max_items), &JsValue::from(_is_multiple)); + callback.forget(); + } + } + + fn destroy(&mut self, ctx: &Context) { + detach_autocomplete(&JsValue::from(self.id.as_str())); + } +} + +#[wasm_bindgen(inline_js = r#" +let init = new Map(); +export function setup_autocomplete(element, callback, max_tags, is_multiple) { + // Attach Bulma autocomplete here + // console.log('Setting up autocomplete ID:' + element.id); + if (!init.has(element.id)) { + let autocompleteInstance = BulmaTagsInput.attach( element, { + maxTags: max_tags, + }); + let autocomplete = autocompleteInstance[0]; + // console.log('Attached autocomplete:'+element.id + ' ' + autocomplete); + autocomplete.on('after.add', function(tag) { + // console.log('tag:'+tag); + if (is_multiple) { + callback('{"op":"add","value":"'+tag.item.value+'"}'); + } else { + callback('{"op":"add","value":"'+tag.item+'"}'); + } + }); + autocomplete.on('after.remove', function(tag) { + // console.log('tag2:'+tag); + if (is_multiple) { + callback('{"op":"remove","value":"'+tag.value+'"}'); + } else { + callback('{"op":"remove","value":"'+tag+'"}'); + } + }); + + init.set(element.id, autocomplete); + + } + + + // Call callback when an item is selected +} + +export function detach_autocomplete(id) { + init.delete(id); + // console.log('Detached autocomplete:'+id); +} +"#)] +extern "C" { + fn setup_autocomplete(element: &Element, callback: &JsValue, max_tags: &JsValue, is_multiple: &JsValue); + fn detach_autocomplete(id: &JsValue); +} diff --git a/src/components/mod.rs b/src/components/mod.rs index 60b2072..d723c56 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,4 +1,5 @@ pub mod accordion; +pub mod autocomplete; pub mod breadcrumb; pub mod calendar; pub mod card; diff --git a/src/lib.rs b/src/lib.rs index a56aec9..56bb77a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,6 +33,7 @@ pub use common::{Alignment, Size}; // components pub use components::accordion::{AccordionItem, AccordionItemProps, Accordions, AccordionsProps}; +pub use components::autocomplete::{AutoComplete, AutoCompleteProps}; pub use components::breadcrumb::{Breadcrumb, BreadcrumbProps, BreadcrumbSeparator, BreadcrumbSize}; pub use components::card::{ Card, CardContent, CardContentProps, CardFooter, CardFooterProps, CardHeader, CardHeaderProps, CardImage, CardImageProps, CardProps, From c225aca09d9d6476df6e5a11c1d0545181d37550 Mon Sep 17 00:00:00 2001 From: kostya Date: Sat, 22 Jun 2024 16:37:15 -0700 Subject: [PATCH 28/58] Update autocomplete component with dynamic fetching Expanded the autocomplete component functionalities by implementing dynamic fetching from an API endpoint. Also added new properties for handling this feature. Moreover, some existing properties were adjusted and unused ones were removed to optimize the component. In addition, a sample usage of these new capabilities was provided in the basic example. --- examples/basic/src/main.rs | 26 +++++- src/components/autocomplete.rs | 143 ++++++++++++++++++++++++++------- 2 files changed, 137 insertions(+), 32 deletions(-) diff --git a/examples/basic/src/main.rs b/examples/basic/src/main.rs index 5cee96f..0615d45 100644 --- a/examples/basic/src/main.rs +++ b/examples/basic/src/main.rs @@ -28,6 +28,9 @@ pub fn app() -> Html { }; + let items: UseStateHandle> = use_state(|| vec!["Apple".to_string(), "Banana".to_string(), "Cherry".to_string()]); + + html! { <> > context={state}> @@ -119,11 +122,16 @@ pub fn app() -> Html { @@ -138,6 +146,18 @@ pub fn app() -> Html { /> + + + + + diff --git a/src/components/autocomplete.rs b/src/components/autocomplete.rs index bddf3ea..1df3e9e 100644 --- a/src/components/autocomplete.rs +++ b/src/components/autocomplete.rs @@ -9,7 +9,6 @@ use yew::prelude::*; pub struct AutoComplete { current_selector: String, id: String, - items: Vec, } #[derive(Clone, PartialEq, Properties)] @@ -26,10 +25,20 @@ pub struct AutoCompleteProps { pub current_selector: String, #[prop_or("Choose Tags".to_string())] pub placeholder: String, - #[prop_or(classes!("".to_string()))] + #[prop_or(classes ! ("".to_string()))] pub classes: Classes, - #[prop_or(false)] - pub is_multiple: bool, + #[prop_or(true)] + pub case_sensitive: bool, + #[prop_or("".to_string())] + pub data_item_text: String, + #[prop_or("".to_string())] + pub data_item_value: String, + #[prop_or("".to_string())] + pub data_item: String, + #[prop_or("".to_string())] + pub url_for_fetch: String, + #[prop_or("".to_string())] + pub auth_header: String, } pub enum Msg { @@ -44,7 +53,6 @@ impl Component for AutoComplete { Self { current_selector: ctx.props().current_selector.clone(), id: ctx.props().id.clone(), - items: ctx.props().items.clone(), } } @@ -63,10 +71,7 @@ impl Component for AutoComplete { } fn view(&self, ctx: &Context) -> Html { - let items = self - .items - .iter() - .clone() + let items = ctx.props().items.iter() .map(|item| { if item == &self.current_selector { html! { @@ -79,22 +84,47 @@ impl Component for AutoComplete { } }) .collect::(); - if ctx.props().is_multiple { + if ctx.props().items.len() > 0 && ctx.props().data_item.len() == 0 { html! {
} + } else if ctx.props().data_item_text.len() > 0 && ctx.props().data_item_value.len() > 0 { + let has_value = self.current_selector.len() > 0; + if has_value { + let value = format!("{{\"{}\":\"{}\"}}",ctx.props().data_item_value.clone(),self.current_selector.clone()); + html! { + + } + } + else { + html! { + + } + } } else { html! { + id={self.id.clone()} data-placeholder={ctx.props().placeholder.clone()} value={self.current_selector.clone()} /> } } } @@ -102,7 +132,9 @@ impl Component for AutoComplete { fn rendered(&mut self, ctx: &Context, first_render: bool) { if first_render { let _max_items = ctx.props().max_items; - let _is_multiple = ctx.props().is_multiple; + let _case_sensitive = ctx.props().case_sensitive; + let _url_for_fetch = ctx.props().url_for_fetch.clone(); + let _auth_header = ctx.props().auth_header.clone(); let window = web_sys::window().expect("no global `window` exists"); let document = window.document().expect("should have a document on window"); @@ -125,8 +157,24 @@ impl Component for AutoComplete { link.send_message(Msg::Removed(value.as_string().unwrap())); } }) as Box); - - setup_autocomplete(&element, callback.as_ref(), &JsValue::from(_max_items), &JsValue::from(_is_multiple)); + if _url_for_fetch.len() == 0 { + setup_static_autocomplete( + &element, + callback.as_ref(), + &JsValue::from(_max_items), + &JsValue::from(_case_sensitive), + ); + } else { + setup_dynamic_autocomplete( + &element, + callback.as_ref(), + &JsValue::from(_max_items), + &JsValue::from(_url_for_fetch), + &JsValue::from(_auth_header), + &JsValue::from(_case_sensitive), + &JsValue::from(ctx.props().data_item_value.clone()), + ); + } callback.forget(); } } @@ -138,46 +186,83 @@ impl Component for AutoComplete { #[wasm_bindgen(inline_js = r#" let init = new Map(); -export function setup_autocomplete(element, callback, max_tags, is_multiple) { +export function setup_dynamic_autocomplete(element, callback, max_tags, url_for_fetch, auth_header, case_sensitive, data_item_value) { // Attach Bulma autocomplete here - // console.log('Setting up autocomplete ID:' + element.id); + console.log('Setting up dynamic autocomplete ID:' + element.id + ' fetch:' + url_for_fetch + ' auth:' + auth_header + ' case:' + case_sensitive + ' max:' + max_tags); if (!init.has(element.id)) { + console.log('Setting up dynamic autocomplete ID:' + element.id); let autocompleteInstance = BulmaTagsInput.attach( element, { maxTags: max_tags, + caseSensitive: case_sensitive, + source: async function(value) { + console.log('Fetching data for:'+value); + return await fetch(url_for_fetch + value) + .then(function(response) { + if (response.status !== 200) { + throw new Error('Failed to fetch data'); + } + return response.json(); + });}, }); let autocomplete = autocompleteInstance[0]; // console.log('Attached autocomplete:'+element.id + ' ' + autocomplete); autocomplete.on('after.add', function(tag) { - // console.log('tag:'+tag); - if (is_multiple) { - callback('{"op":"add","value":"'+tag.item.value+'"}'); + // console.log(tag); + callback('{"op":"add","value":"'+tag.item[data_item_value]+'"}'); + }); + autocomplete.on('after.remove', function(tag) { + // console.log(tag); + callback('{"op":"remove","value":"'+tag[data_item_value]+'"}'); + }); + + init.set(element.id, autocomplete); + } +} + +export function setup_static_autocomplete(element, callback, max_tags, case_sensitive) { + // Attach Bulma autocomplete here + console.log('Setting up static autocomplete ID:' + element.id + ' case:' + case_sensitive + ' max:' + max_tags); + if (!init.has(element.id)) { + let autocompleteInstance = BulmaTagsInput.attach( element, { + maxTags: max_tags, + caseSensitive: case_sensitive, + }); + let autocomplete = autocompleteInstance[0]; + // console.log('Attached autocomplete:'+element.id + ' ' + autocomplete); + autocomplete.on('after.add', function(tag) { + // console.log(tag); + if (tag.item && tag.item.value) { + callback('{"op":"add","value":"'+tag.item.value+'"}'); + } else if (tag.value) { + callback('{"op":"add","value":"'+tag.value+'"}'); } else { - callback('{"op":"add","value":"'+tag.item+'"}'); + callback('{"op":"add","value":"'+tag.item+'"}'); } }); autocomplete.on('after.remove', function(tag) { - // console.log('tag2:'+tag); - if (is_multiple) { - callback('{"op":"remove","value":"'+tag.value+'"}'); + // console.log(tag); + if (tag.item && tag.item.value) { + callback('{"op":"remove","value":"'+tag.item.value+'"}'); + } else if (tag.value) { + callback('{"op":"remove","value":"'+tag.value+'"}'); } else { - callback('{"op":"remove","value":"'+tag+'"}'); + callback('{"op":"remove","value":"'+tag+'"}'); } }); init.set(element.id, autocomplete); } - - - // Call callback when an item is selected } export function detach_autocomplete(id) { init.delete(id); // console.log('Detached autocomplete:'+id); } + "#)] extern "C" { - fn setup_autocomplete(element: &Element, callback: &JsValue, max_tags: &JsValue, is_multiple: &JsValue); + fn setup_dynamic_autocomplete(element: &Element, callback: &JsValue, max_tags: &JsValue, url_to_fetch: &JsValue, auth_header: &JsValue, case_sensitive: &JsValue, data_item_value: &JsValue); + fn setup_static_autocomplete(element: &Element, callback: &JsValue, max_tags: &JsValue, case_sensitive: &JsValue); fn detach_autocomplete(id: &JsValue); } From 64259cc2207689adc669e117500de48457be7009 Mon Sep 17 00:00:00 2001 From: kostya Date: Sun, 23 Jun 2024 08:28:02 -0700 Subject: [PATCH 29/58] Refactor autocomplete component and remove unnecessary logs This commit simplifies the autocomplete component by eliminating "current_selector" as a direct property and fetching it from props instead. It significantly reduces redundancy. Additionally, few debug console logs have been commented out to improve readability and maintain code cleanliness. The autocomplete setup functions have also been refactored for more straightforward usage. --- examples/basic/src/main.rs | 2 -- src/components/autocomplete.rs | 58 +++++++++++++--------------------- 2 files changed, 22 insertions(+), 38 deletions(-) diff --git a/examples/basic/src/main.rs b/examples/basic/src/main.rs index 0615d45..ff1bc3c 100644 --- a/examples/basic/src/main.rs +++ b/examples/basic/src/main.rs @@ -27,10 +27,8 @@ pub fn app() -> Html { let calendar_departure_date = html! { }; - let items: UseStateHandle> = use_state(|| vec!["Apple".to_string(), "Banana".to_string(), "Cherry".to_string()]); - html! { <> > context={state}> diff --git a/src/components/autocomplete.rs b/src/components/autocomplete.rs index 1df3e9e..80e83d6 100644 --- a/src/components/autocomplete.rs +++ b/src/components/autocomplete.rs @@ -7,7 +7,6 @@ use web_sys::{js_sys, Element}; use yew::prelude::*; pub struct AutoComplete { - current_selector: String, id: String, } @@ -50,10 +49,7 @@ impl Component for AutoComplete { type Message = Msg; type Properties = AutoCompleteProps; fn create(ctx: &Context) -> Self { - Self { - current_selector: ctx.props().current_selector.clone(), - id: ctx.props().id.clone(), - } + Self { id: ctx.props().id.clone() } } fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { @@ -71,9 +67,13 @@ impl Component for AutoComplete { } fn view(&self, ctx: &Context) -> Html { - let items = ctx.props().items.iter() + let current_selector = ctx.props().current_selector.clone(); + let items = ctx + .props() + .items + .iter() .map(|item| { - if item == &self.current_selector { + if item == ¤t_selector { html! { } @@ -90,41 +90,29 @@ impl Component for AutoComplete {
} } else if ctx.props().data_item_text.len() > 0 && ctx.props().data_item_value.len() > 0 { - let has_value = self.current_selector.len() > 0; - if has_value { - let value = format!("{{\"{}\":\"{}\"}}",ctx.props().data_item_value.clone(),self.current_selector.clone()); - html! { - - } - } - else { - html! { + let has_value = current_selector.len() > 0; + let value = format!("{{\"{}\":\"{}\"}}", ctx.props().data_item_value.as_str(), current_selector); + html! { - } + id={self.id.clone()} data-placeholder={ctx.props().placeholder.clone()} + value={if has_value {value} else {"{}".to_string()}} /> } } else { html! { + id={self.id.clone()} data-placeholder={ctx.props().placeholder.clone()} value={current_selector.clone()} /> } } } @@ -158,12 +146,7 @@ impl Component for AutoComplete { } }) as Box); if _url_for_fetch.len() == 0 { - setup_static_autocomplete( - &element, - callback.as_ref(), - &JsValue::from(_max_items), - &JsValue::from(_case_sensitive), - ); + setup_static_autocomplete(&element, callback.as_ref(), &JsValue::from(_max_items), &JsValue::from(_case_sensitive)); } else { setup_dynamic_autocomplete( &element, @@ -190,12 +173,12 @@ export function setup_dynamic_autocomplete(element, callback, max_tags, url_for_ // Attach Bulma autocomplete here console.log('Setting up dynamic autocomplete ID:' + element.id + ' fetch:' + url_for_fetch + ' auth:' + auth_header + ' case:' + case_sensitive + ' max:' + max_tags); if (!init.has(element.id)) { - console.log('Setting up dynamic autocomplete ID:' + element.id); + // console.log('Setting up dynamic autocomplete ID:' + element.id); let autocompleteInstance = BulmaTagsInput.attach( element, { maxTags: max_tags, caseSensitive: case_sensitive, source: async function(value) { - console.log('Fetching data for:'+value); + // console.log('Fetching data for:'+value); return await fetch(url_for_fetch + value) .then(function(response) { if (response.status !== 200) { @@ -221,7 +204,7 @@ export function setup_dynamic_autocomplete(element, callback, max_tags, url_for_ export function setup_static_autocomplete(element, callback, max_tags, case_sensitive) { // Attach Bulma autocomplete here - console.log('Setting up static autocomplete ID:' + element.id + ' case:' + case_sensitive + ' max:' + max_tags); + // console.log('Setting up static autocomplete ID:' + element.id + ' case:' + case_sensitive + ' max:' + max_tags); if (!init.has(element.id)) { let autocompleteInstance = BulmaTagsInput.attach( element, { maxTags: max_tags, @@ -262,7 +245,10 @@ export function detach_autocomplete(id) { "#)] extern "C" { - fn setup_dynamic_autocomplete(element: &Element, callback: &JsValue, max_tags: &JsValue, url_to_fetch: &JsValue, auth_header: &JsValue, case_sensitive: &JsValue, data_item_value: &JsValue); + fn setup_dynamic_autocomplete( + element: &Element, callback: &JsValue, max_tags: &JsValue, url_to_fetch: &JsValue, auth_header: &JsValue, case_sensitive: &JsValue, + data_item_value: &JsValue, + ); fn setup_static_autocomplete(element: &Element, callback: &JsValue, max_tags: &JsValue, case_sensitive: &JsValue); fn detach_autocomplete(id: &JsValue); } From 5805e739c4dfae42af4a963c591021d4b383fd97 Mon Sep 17 00:00:00 2001 From: kostya Date: Sun, 23 Jun 2024 08:31:13 -0700 Subject: [PATCH 30/58] Change field label and placeholder in AutoComplete Updated the label and placeholder for the AutoComplete field in 'examples/basic/src/main.rs'. Previously, the field was for entering tags, but now it is dedicated to entering country code. The change also contains an updated help string pointing out that country code data is sourced from 'https://restcountries.com/v3.1/name/'. --- examples/basic/src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/basic/src/main.rs b/examples/basic/src/main.rs index ff1bc3c..5aead49 100644 --- a/examples/basic/src/main.rs +++ b/examples/basic/src/main.rs @@ -116,7 +116,7 @@ pub fn app() -> Html { - + Html { on_update={cb_on_update.clone()} on_remove={cb_on_remove.clone()} case_sensitive={false} - placeholder={"Enter country name"} + placeholder={"Enter country code"} /> From 7e5ddf52d361803103f744d5bf2592cde9d1022b Mon Sep 17 00:00:00 2001 From: kostya Date: Sun, 23 Jun 2024 08:35:34 -0700 Subject: [PATCH 31/58] Remove console log from autocomplete setup The console log statement used for debugging purposes while setting up dynamic autocomplete has been commented out. This was cluttering up the console with unnecessary messages and no longer provides any meaningful information during runtime. --- src/components/autocomplete.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/autocomplete.rs b/src/components/autocomplete.rs index 80e83d6..6b32f8c 100644 --- a/src/components/autocomplete.rs +++ b/src/components/autocomplete.rs @@ -171,7 +171,7 @@ impl Component for AutoComplete { let init = new Map(); export function setup_dynamic_autocomplete(element, callback, max_tags, url_for_fetch, auth_header, case_sensitive, data_item_value) { // Attach Bulma autocomplete here - console.log('Setting up dynamic autocomplete ID:' + element.id + ' fetch:' + url_for_fetch + ' auth:' + auth_header + ' case:' + case_sensitive + ' max:' + max_tags); + // console.log('Setting up dynamic autocomplete ID:' + element.id + ' fetch:' + url_for_fetch + ' auth:' + auth_header + ' case:' + case_sensitive + ' max:' + max_tags); if (!init.has(element.id)) { // console.log('Setting up dynamic autocomplete ID:' + element.id); let autocompleteInstance = BulmaTagsInput.attach( element, { From aa949d27ee51b753816202be0201d8304caf4948 Mon Sep 17 00:00:00 2001 From: kostya Date: Sun, 23 Jun 2024 15:01:04 -0700 Subject: [PATCH 32/58] Refactor string properties to use Rc Modified the AutoComplete and AutoCompleteProps structures to replace numerous String properties with Rc for better memory efficiency. This reduces the overhead of cloning strings and improves performance by sharing ownership of the same data accross multiple instances. The changes are reflected and implemented in both the src/components/autocomplete.rs and examples/basic/src/main.rs files. --- examples/basic/src/main.rs | 20 ++++++------ src/components/autocomplete.rs | 59 +++++++++++++++++----------------- 2 files changed, 39 insertions(+), 40 deletions(-) diff --git a/examples/basic/src/main.rs b/examples/basic/src/main.rs index 5aead49..4e00882 100644 --- a/examples/basic/src/main.rs +++ b/examples/basic/src/main.rs @@ -120,16 +120,16 @@ pub fn app() -> Html { @@ -137,8 +137,8 @@ pub fn app() -> Html { @@ -148,8 +148,8 @@ pub fn app() -> Html { , } #[derive(Clone, PartialEq, Properties)] pub struct AutoCompleteProps { - #[prop_or("autocomplete".to_string())] - pub id: String, + #[prop_or("".to_string().into())] + pub id: Rc, #[prop_or(10)] pub max_items: u32, #[prop_or_default] pub items: Vec, pub on_update: Callback, pub on_remove: Callback, - #[prop_or("".to_string())] - pub current_selector: String, - #[prop_or("Choose Tags".to_string())] - pub placeholder: String, + #[prop_or("".to_string().into())] + pub current_selector: Rc, + #[prop_or("Choose Tags".to_string().into())] + pub placeholder: Rc, #[prop_or(classes ! ("".to_string()))] pub classes: Classes, #[prop_or(true)] pub case_sensitive: bool, - #[prop_or("".to_string())] - pub data_item_text: String, - #[prop_or("".to_string())] - pub data_item_value: String, - #[prop_or("".to_string())] - pub data_item: String, - #[prop_or("".to_string())] - pub url_for_fetch: String, - #[prop_or("".to_string())] - pub auth_header: String, + #[prop_or("".to_string().into())] + pub data_item_text: Rc, + #[prop_or("".to_string().into())] + pub data_item_value: Rc, + #[prop_or("".to_string().into())] + pub url_for_fetch: Rc, + #[prop_or("".to_string().into())] + pub auth_header: Rc, } pub enum Msg { @@ -67,24 +66,24 @@ impl Component for AutoComplete { } fn view(&self, ctx: &Context) -> Html { - let current_selector = ctx.props().current_selector.clone(); + let current_selector = ctx.props().current_selector.to_string(); let items = ctx .props() .items .iter() .map(|item| { - if item == ¤t_selector { + if item == current_selector.as_str() { html! { - + } } else { html! { - + } } }) .collect::(); - if ctx.props().items.len() > 0 && ctx.props().data_item.len() == 0 { + if ctx.props().items.len() > 0 && ctx.props().data_item_text.len() == 0 && ctx.props().data_item_value.len() == 0 { html! {
} } else { @@ -127,8 +126,8 @@ impl Component for AutoComplete { let document = window.document().expect("should have a document on window"); let element = document - .get_element_by_id(self.id.as_str()) - .expect(format!("should have #{} on the page", self.id.as_str()).as_str()); + .get_element_by_id(&*self.id) + .expect(format!("should have #{} on the page", self.id).as_str()); // Clone the link from the context let link = ctx.link().clone(); @@ -152,10 +151,10 @@ impl Component for AutoComplete { &element, callback.as_ref(), &JsValue::from(_max_items), - &JsValue::from(_url_for_fetch), - &JsValue::from(_auth_header), + &JsValue::from(_url_for_fetch.to_string()), + &JsValue::from(_auth_header.to_string()), &JsValue::from(_case_sensitive), - &JsValue::from(ctx.props().data_item_value.clone()), + &JsValue::from(ctx.props().data_item_value.to_string()), ); } callback.forget(); @@ -163,7 +162,7 @@ impl Component for AutoComplete { } fn destroy(&mut self, ctx: &Context) { - detach_autocomplete(&JsValue::from(self.id.as_str())); + detach_autocomplete(&JsValue::from(self.id.as_ptr())); } } From 81e93c4621128d2ddaa9af99c10c0e5188f395ce Mon Sep 17 00:00:00 2001 From: kostya Date: Sun, 23 Jun 2024 19:50:46 -0700 Subject: [PATCH 33/58] Replace String with Rc in accordion component The commit updates the type of 'id' and 'title' fields from String to Rc in the accordion and accordion item components. This change optimizes memory usage, as Rc shares ownership of a string across multiple components, reducing the need for repeated allocations and deallocations. --- examples/basic/src/main.rs | 6 +++--- src/components/accordion.rs | 19 +++++++++---------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/examples/basic/src/main.rs b/examples/basic/src/main.rs index 4e00882..42b8e0b 100644 --- a/examples/basic/src/main.rs +++ b/examples/basic/src/main.rs @@ -89,11 +89,11 @@ pub fn app() -> Html { {"YBC"}

{"A Yew component library based on the Bulma CSS framework."}

- - + +

{"This is the content of the first accordion."}

- +

{"This is the content of the second accordion."}

diff --git a/src/components/accordion.rs b/src/components/accordion.rs index 9ebf6e2..a8c8611 100644 --- a/src/components/accordion.rs +++ b/src/components/accordion.rs @@ -1,3 +1,4 @@ +use std::rc::Rc; use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::JsValue; use web_sys::Element; @@ -23,24 +24,23 @@ pub fn accordion_item(props: &AccordionItemProps) -> Html { #[derive(Clone, Debug, PartialEq, Properties)] pub struct AccordionsProps { pub children: ChildrenWithProps, - pub id: String, + pub id: Rc, } pub struct Accordions { props: AccordionsProps, - id: String, } #[derive(Properties, Clone, PartialEq)] pub struct AccordionItemProps { - pub title: String, + pub title: Rc, pub children: Children, #[prop_or_default] pub open: bool, #[prop_or_else(Callback::noop)] pub on_toggle: Callback, - #[prop_or_default] - pub id: String, + #[prop_or("".into())] + pub id: Rc, } impl Component for Accordions { @@ -50,7 +50,6 @@ impl Component for Accordions { fn create(ctx: &Context) -> Self { Self { props: ctx.props().clone(), - id: format!("accordion-{}", ctx.props().id), } } @@ -61,7 +60,7 @@ impl Component for Accordions { fn view(&self, ctx: &Context) -> Html { html! { -
+
{for ctx.props().children.iter().map(|child| { html! {child.clone()} })} @@ -75,15 +74,15 @@ impl Component for Accordions { let document = window.document().expect("should have a document on window"); let element = document - .get_element_by_id(self.id.as_str()) - .expect(format!("should have #{} on the page", self.id.as_str()).as_str()); + .get_element_by_id(ctx.props().id.to_string().as_str()) + .expect(format!("should have #{} on the page", ctx.props().id).as_str()); setup_accordion(&element); } } fn destroy(&mut self, ctx: &Context) { - detach_accordion(&JsValue::from(self.id.as_str())); + detach_accordion(&JsValue::from_str(&ctx.props().id)); } } From f7fd9e10588483a0ce9cd6233b13e65d7aac40f5 Mon Sep 17 00:00:00 2001 From: kostya Date: Mon, 24 Jun 2024 12:44:38 -0700 Subject: [PATCH 34/58] Add initial value support to the dynamic autocomplete function The dynamic autocomplete function now accommodates an "initial value" parameter. This allows pre-population of input fields with an initial value. Meanwhile, some minor refactoring was done in the accordion component for code readability. --- src/components/accordion.rs | 4 +--- src/components/autocomplete.rs | 8 ++++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/accordion.rs b/src/components/accordion.rs index a8c8611..d4f61c5 100644 --- a/src/components/accordion.rs +++ b/src/components/accordion.rs @@ -48,9 +48,7 @@ impl Component for Accordions { type Properties = AccordionsProps; fn create(ctx: &Context) -> Self { - Self { - props: ctx.props().clone(), - } + Self { props: ctx.props().clone() } } fn update(&mut self, ctx: &Context, _msg: Self::Message) -> bool { diff --git a/src/components/autocomplete.rs b/src/components/autocomplete.rs index 1f05ca0..8fc1634 100644 --- a/src/components/autocomplete.rs +++ b/src/components/autocomplete.rs @@ -155,6 +155,7 @@ impl Component for AutoComplete { &JsValue::from(_auth_header.to_string()), &JsValue::from(_case_sensitive), &JsValue::from(ctx.props().data_item_value.to_string()), + &JsValue::from(ctx.props().current_selector.to_string()), ); } callback.forget(); @@ -168,7 +169,7 @@ impl Component for AutoComplete { #[wasm_bindgen(inline_js = r#" let init = new Map(); -export function setup_dynamic_autocomplete(element, callback, max_tags, url_for_fetch, auth_header, case_sensitive, data_item_value) { +export function setup_dynamic_autocomplete(element, callback, max_tags, url_for_fetch, auth_header, case_sensitive, data_item_value, initial_value) { // Attach Bulma autocomplete here // console.log('Setting up dynamic autocomplete ID:' + element.id + ' fetch:' + url_for_fetch + ' auth:' + auth_header + ' case:' + case_sensitive + ' max:' + max_tags); if (!init.has(element.id)) { @@ -196,6 +197,9 @@ export function setup_dynamic_autocomplete(element, callback, max_tags, url_for_ // console.log(tag); callback('{"op":"remove","value":"'+tag[data_item_value]+'"}'); }); + if (initial_value.length > 0) { + autocomplete.add('{"'+data_item_value+'":"'+initial_value+'"}'); + } init.set(element.id, autocomplete); } @@ -246,7 +250,7 @@ export function detach_autocomplete(id) { extern "C" { fn setup_dynamic_autocomplete( element: &Element, callback: &JsValue, max_tags: &JsValue, url_to_fetch: &JsValue, auth_header: &JsValue, case_sensitive: &JsValue, - data_item_value: &JsValue, + data_item_value: &JsValue, initial_value: &JsValue, ); fn setup_static_autocomplete(element: &Element, callback: &JsValue, max_tags: &JsValue, case_sensitive: &JsValue); fn detach_autocomplete(id: &JsValue); From 8d5559ae69a5732e2788f3b381ae505c0bfe776e Mon Sep 17 00:00:00 2001 From: kostya Date: Sun, 30 Jun 2024 12:38:26 -0700 Subject: [PATCH 35/58] Update progress bar functionality This commit introduces changes to the progress bar functionality in the UI. Now, if a progress bar value is set to -1.0, it will be rendered without a value, indicating an indefinite loading state. It also includes an example displaying this new feature. --- examples/basic/src/main.rs | 1 + src/elements/progress.rs | 14 ++++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/examples/basic/src/main.rs b/examples/basic/src/main.rs index 42b8e0b..a3c93a1 100644 --- a/examples/basic/src/main.rs +++ b/examples/basic/src/main.rs @@ -156,6 +156,7 @@ pub fn app() -> Html { /> + diff --git a/src/elements/progress.rs b/src/elements/progress.rs index dad017e..85bcf0d 100644 --- a/src/elements/progress.rs +++ b/src/elements/progress.rs @@ -21,9 +21,15 @@ pub fn progress(props: &ProgressProps) -> Html { let max = props.max.to_string(); let value = props.value.to_string(); let value_txt = format!("{}%", value); - html! { - - {value_txt} - + if props.value == -1.0 { + html! { + + } + } else { + html! { + + {value_txt} + + } } } From 40f626f78554b3406cba54974c1e31172a8123c4 Mon Sep 17 00:00:00 2001 From: kostya Date: Sat, 6 Jul 2024 21:07:47 -0700 Subject: [PATCH 36/58] Add interactive textarea and ChatGPT icon Added a new interactive textarea with a ChatGPT icon in the form section. The updated textarea reflects user input and updates parent component with the new value. The changes also include the addition of ChatGPT icon in the form section and style changes to support the new icon placement. --- examples/basic/index.html | 2 + examples/basic/src/chatgpt.svg | 1 + examples/basic/src/index.scss | 7 +++ examples/basic/src/main.rs | 8 +++- src/form/textarea.rs | 82 +++++++++++++++++++++++++++------- 5 files changed, 83 insertions(+), 17 deletions(-) create mode 100644 examples/basic/src/chatgpt.svg diff --git a/examples/basic/index.html b/examples/basic/index.html index eb83f58..510f684 100644 --- a/examples/basic/index.html +++ b/examples/basic/index.html @@ -12,7 +12,9 @@ + + diff --git a/examples/basic/src/chatgpt.svg b/examples/basic/src/chatgpt.svg new file mode 100644 index 0000000..7f2cf6a --- /dev/null +++ b/examples/basic/src/chatgpt.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/basic/src/index.scss b/examples/basic/src/index.scss index 35a1942..364e6c6 100644 --- a/examples/basic/src/index.scss +++ b/examples/basic/src/index.scss @@ -1,3 +1,10 @@ @charset "utf-8"; html {} + +.ribbon { + position:absolute; + top:0; + right:0; + z-index:1; +} \ No newline at end of file diff --git a/examples/basic/src/main.rs b/examples/basic/src/main.rs index a3c93a1..b344986 100644 --- a/examples/basic/src/main.rs +++ b/examples/basic/src/main.rs @@ -27,6 +27,9 @@ pub fn app() -> Html { let calendar_departure_date = html! { }; + let cb_on_text_update = Callback::from(|tag: String| { + gloo_console::log!("Tex updated: {}", tag); + }); let items: UseStateHandle> = use_state(|| vec!["Apple".to_string(), "Banana".to_string(), "Cherry".to_string()]); html! { @@ -44,7 +47,7 @@ pub fn app() -> Html { navend={html!{ <> - + {"Trunk"} @@ -157,6 +160,9 @@ pub fn app() -> Html { + diff --git a/src/form/textarea.rs b/src/form/textarea.rs index 460f99a..113ad48 100644 --- a/src/form/textarea.rs +++ b/src/form/textarea.rs @@ -1,9 +1,8 @@ +use crate::{Icon, Size}; use wasm_bindgen::UnwrapThrowExt; use web_sys::HtmlTextAreaElement; use yew::prelude::*; -use crate::Size; - #[derive(Clone, Debug, Properties, PartialEq)] pub struct TextAreaProps { /// The `name` attribute for this form element. @@ -40,6 +39,9 @@ pub struct TextAreaProps { /// Make this component static. #[prop_or_default] pub r#static: bool, + + #[prop_or_default] + pub is_genai: bool, } /// A multiline textarea component. @@ -59,20 +61,68 @@ pub fn text_area(props: &TextAreaProps) -> Html { props.r#static.then_some("is-static"), props.fixed_size.then_some("has-fixed-size"), ); - let oninput = props.update.reform(|ev: web_sys::InputEvent| { - let input: HtmlTextAreaElement = ev.target_dyn_into().expect_throw("event target should be a text area"); - input.value() - }); + let genai = use_state(|| props.is_genai); + let value = use_state(|| props.value.clone()); + let input_ref = use_node_ref(); + let oninput = { + let value = value.clone(); + let update = props.update.clone(); + let _genai = genai.clone(); + Callback::from(move |ev: InputEvent| { + let input: HtmlTextAreaElement = ev.target_dyn_into().expect_throw("event target should be a text area"); + _genai.set(false); + let input_value = input.value(); + value.set(input_value.clone()); // Update the local state + update.emit(input_value); // Emit the new value to the parent component + }) + }; + { + let value = value.clone(); + use_effect_with(props.value.clone(), move |value_prop| { + value.set(value_prop.clone()); + || () + }); + } + { + let gen1 = genai.clone(); + use_effect_with(props.is_genai, move |value_prop| { + gen1.set(value_prop.clone()); + || () + }); + } + html! { -