├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── Cargo.lock ├── Cargo.toml ├── README.md ├── basics │ ├── Cargo.toml │ ├── index.html │ └── src │ │ └── main.rs ├── cards │ ├── Cargo.toml │ ├── index.html │ └── src │ │ └── main.rs ├── forms │ ├── Cargo.toml │ ├── index.html │ └── src │ │ └── main.rs ├── icons │ ├── Cargo.toml │ ├── README.md │ ├── Trunk.toml │ ├── index.html │ └── src │ │ ├── bin │ │ └── include-bootstrap-icons.rs │ │ └── main.rs └── searchable_select │ ├── Cargo.toml │ ├── index.html │ └── src │ ├── filter.rs │ ├── form.rs │ ├── headers.rs │ ├── main.rs │ ├── multiple.rs │ └── single.rs └── packages └── yew-bootstrap ├── Cargo.toml ├── README.md ├── bootstrap-icons-v1.11.3 ├── bootstrap-icons.css └── fonts │ ├── LICENSE │ ├── bootstrap-icons.woff │ └── bootstrap-icons.woff2 ├── build.rs └── src ├── component ├── accordion.rs ├── alert.rs ├── badge.rs ├── button.rs ├── button_group.rs ├── card │ ├── body.rs │ ├── card.rs │ ├── card_group.rs │ ├── image.rs │ └── mod.rs ├── column.rs ├── container.rs ├── display.rs ├── form │ ├── form_autocomplete.rs │ ├── form_control.rs │ ├── form_type.rs │ ├── mod.rs │ └── select_option.rs ├── lead.rs ├── line.rs ├── link.rs ├── list_group.rs ├── mod.rs ├── modal.rs ├── navbar.rs ├── progress.rs ├── row.rs ├── searchable_select │ ├── filter.rs │ ├── mod.rs │ └── soption.rs ├── spinner.rs └── tooltip.rs ├── icons └── mod.rs ├── lib.rs └── util ├── arrange.rs ├── bootstrap-5.1.3.min.css ├── color.rs ├── dimension.rs ├── include.rs ├── mod.rs ├── position.rs └── size.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --verbose 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | **/target/ 4 | 5 | # These are backup files generated by rustfmt 6 | **/*.rs.bk 7 | 8 | # Trunk build output 9 | **/dist/ 10 | 11 | # A place to put Jupyter notebooks and other untracked files 12 | .sandbox/ -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "packages/*" 4 | ] 5 | exclude = [ 6 | "examples/*", 7 | ] 8 | resolver = "2" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Foorack / Max Faxälv 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # yew-bootstrap 2 | `yew-bootstrap` is a collection of frontend components made to simplify the usage of [Bootstrap 5](https://getbootstrap.com/docs/5.1/getting-started/introduction/) within the [Yew framework](https://yew.rs/). 3 | 4 | Crate info 5 | API Docs 6 | 7 | This project uses [semantic versioning](https://semver.org/). 8 | 9 | # Features Implemented 10 | Check the readme for the [`yew-bootstrap` package](/packages/yew-bootstrap/README.md), or the docs.rs link above. 11 | 12 | [basics-example.webm](https://github.com/isosphere/yew-bootstrap/assets/163370/4d5e80e8-9d1a-4493-804a-20aa5bc46948) 13 | 14 | # Contributing 15 | Bug reports, feature requests, and pull requests are welcome! 16 | 17 | Please try to match your code style to the existing codebase. If you think a change in that style is warranted, feel free to make a suggestion in a new Issue. 18 | 19 | Much of this codebase uses struct components. **For new contributions please use functional components** unless you have good reason to use struct components. It is the [recommended default from Yew](https://yew.rs/docs/concepts/function-components)[^1], and we should be consistent. 20 | [^1]: > function components - the recommended way to write components when starting with Yew and when writing simple presentation logic. 21 | 22 | Please be sure to try `cargo test` before submitting a PR. 23 | 24 | All new features should have examples in their documentation via doc strings as well as an example application under `/examples/`. 25 | -------------------------------------------------------------------------------- /examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "*", 4 | ] 5 | exclude = [ 6 | "target", 7 | ".cargo" 8 | ] 9 | resolver = "2" 10 | 11 | [profile.release] 12 | lto = true 13 | codegen-units = 1 14 | panic = "abort" 15 | opt-level = "z" -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Yew Bootstrap Examples 2 | 3 | ## How to run 4 | 5 | The examples are built with [trunk](https://github.com/thedodd/trunk) which you can install with the following command: 6 | 7 | ```bash 8 | cargo install --locked trunk 9 | ``` 10 | 11 | Running an example is as easy as running a single command in the appropriate directory: 12 | 13 | ```bash 14 | # move into the directory of the example you want to run 15 | # In this case it's the todomvc example 16 | cd examples/basics 17 | 18 | # build and serve the example 19 | trunk serve --release 20 | ``` -------------------------------------------------------------------------------- /examples/basics/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "basics" 3 | version = "0.3.0" 4 | authors = ["Matthew Scheffel ", "Foorack "] 5 | edition = "2021" 6 | license = "MIT" 7 | 8 | [dependencies] 9 | gloo-console = "0.3.0" 10 | yew = { version = "0.21", features = ["csr"] } 11 | yew-bootstrap = { path = "../../packages/yew-bootstrap" } 12 | wasm-bindgen = "0.2.*" 13 | web-sys = { version = "0.3.*", features = ["Event", "HtmlElement"] } 14 | -------------------------------------------------------------------------------- /examples/basics/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Yew App 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/cards/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cards" 3 | version = "0.2.1" 4 | authors = ["Cédric Airaud ", "CraftSpider "] 5 | edition = "2021" 6 | license = "MIT" 7 | 8 | [dependencies] 9 | wasm-bindgen = "0.2.*" 10 | web-sys = { version = "0.3.*", features = ["HtmlTextAreaElement", "HtmlSelectElement"] } 11 | yew = { version = "0.21", features = ["csr"] } 12 | yew-bootstrap = { path = "../../packages/yew-bootstrap" } 13 | gloo-console = "0.3.0" 14 | -------------------------------------------------------------------------------- /examples/cards/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Yew App 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/cards/src/main.rs: -------------------------------------------------------------------------------- 1 | use yew::prelude::*; 2 | 3 | use yew_bootstrap::component::*; 4 | use yew_bootstrap::util::*; 5 | use yew_bootstrap::component::card::*; 6 | 7 | enum Msg {} 8 | 9 | struct Model {} 10 | 11 | impl Component for Model { 12 | type Message = Msg; 13 | type Properties = (); 14 | 15 | fn create(_ctx: &Context) -> Self { 16 | Self {} 17 | } 18 | 19 | fn view(&self, _ctx: &Context) -> Html { 20 | let all_colors = [ 21 | Color::Primary, 22 | Color::Secondary, 23 | Color::Success, 24 | Color::Info, 25 | Color::Warning, 26 | Color::Danger, 27 | Color::Light, 28 | Color::Dark, 29 | ]; 30 | 31 | let bg_cards = all_colors.iter() 32 | .map(|color| { 33 | let text_color = if *color == Color::Dark { 34 | Some(TextColor::White) 35 | } else { 36 | None 37 | }; 38 | 39 | html_nested! { 40 | 41 | 42 | {"Header"} 43 | 44 | {"Primary Title"} 45 | {"Some example text to make the card a little bigger, provide it some more content."} 46 | 47 | 48 | 49 | } 50 | }) 51 | .collect::>(); 52 | 53 | let border_cards = all_colors.iter() 54 | .map(|color| html_nested! { 55 | 56 | 57 | {"Header"} 58 | 59 | {"Primary Title"} 60 | {"Some example text to make the card a little bigger, provide it some more content."} 61 | 62 | 63 | 64 | }) 65 | .collect::>(); 66 | 67 | html! { 68 | <> 69 | {include_inline()} 70 |
71 | 72 |

{"Basic Cards"}

73 | 74 |

{"Card with inline body"}

75 | 76 | {"A very simple card"} 77 | 78 | 79 |

{"Card with header and footer"}

80 | 81 | {"This is the card header"} 82 | 83 | {"This is the body of the card"} 84 | 85 | {"This is the card footer"} 86 | 87 | 88 |

{"Card with title/subtitle and links"}

89 | 90 | {"Title"} 91 | {"Subtitle"} 92 | 93 | {"Card text, blah blah blah"} 94 | 95 | {"Link 1"} 96 | {"Link 2"} 97 | 98 | 99 |

{"Card with a list and header"}

100 | 101 | {"Header Text"} 102 | 103 | {"First Item"} 104 | {"Second Item"} 105 | {"Third Item"} 106 | {"Fourth Item"} 107 | 108 | 109 | 110 |

{"Cards with images"}

111 | 112 | 113 | 114 | {"Above Text"} 115 | 116 | {"Below Text"} 117 | 118 | 119 | 120 | 121 | 122 | {"Below Text"} 123 | 124 | 125 | 126 | 127 | {"Above Text"} 128 | 129 | 130 | 131 | 132 | 133 |

{"Card colors"}

134 |

{"Background colors"}

135 | 136 | {bg_cards} 137 | 138 | 139 |

{"Border colors"}

140 | 141 | {border_cards} 142 | 143 | 144 |

{"Card groups"}

145 | 146 | 147 | {"This card has a header"} 148 | 149 | {"Card title"} 150 | 151 | {"This is a wider card with supporting text below as a natural lead-in \ 152 | to additional content. This content is a little bit longer."} 153 | 154 | 155 | 156 | {"Last updated 3 mins ago"} 157 | 158 | 159 | 160 | 161 | {"Card title"} 162 | 163 | {"This card has supporting text below as a natural lead-in to \ 164 | additional content. "} 165 | 166 | 167 | 168 | {"Last updated 3 mins ago"} 169 | 170 | 171 | 172 | 173 | {"Card title"} 174 | 175 | {"This is a wider card with supporting text below as a natural lead-in \ 176 | to additional content. This card has even longer content than the \ 177 | first to show that equal height action. We need even more text for that \ 178 | so here's some more text to use. Even more words to push the text amount \ 179 | up even more. Another line or two should do it, enough to counter the header \ 180 | on the first card."} 181 | 182 | 183 | 184 | {"Last updated 3 mins ago"} 185 | 186 | 187 | 188 | 189 |
190 | { include_cdn_js() } 191 | 192 | } 193 | } 194 | } 195 | 196 | fn main() { 197 | yew::Renderer::::new().render(); 198 | } -------------------------------------------------------------------------------- /examples/forms/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "forms" 3 | version = "0.3.0" 4 | authors = ["Cédric Airaud "] 5 | edition = "2021" 6 | license = "MIT" 7 | 8 | [dependencies] 9 | wasm-bindgen = "0.2.*" 10 | web-sys = { version = "0.3.*", features = ["HtmlTextAreaElement", "HtmlSelectElement"] } 11 | yew = { version = "0.21", features = ["csr"] } 12 | yew-bootstrap = { path = "../../packages/yew-bootstrap" } 13 | gloo-console = "0.3.0" 14 | -------------------------------------------------------------------------------- /examples/forms/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Yew App 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/icons/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "icons" 3 | version = "0.2.0" 4 | authors = ["ALeX Kazik "] 5 | edition = "2021" 6 | license = "MIT" 7 | 8 | [dependencies] 9 | yew = { version = "0.21", features = ["csr"] } 10 | yew-bootstrap = { path = "../../packages/yew-bootstrap" } 11 | 12 | [[bin]] 13 | name = "include-bootstrap-icons" 14 | -------------------------------------------------------------------------------- /examples/icons/README.md: -------------------------------------------------------------------------------- 1 | # Example of automatically copying the bootstrap-icons files 2 | 3 | This is copies the required files to the `dist` directory, which is recommended. 4 | 5 | Please see [the documentation](https://docs.rs/yew-bootstrap/latest/yew_bootstrap/icons/index.html) for more information. 6 | 7 | A copy of `bootstrap-icons` is included and should change only rarely. `trunk` does not add a hash to generated files, and thus a change in those files won't be detected by `trunk`. 8 | 9 | ## Instructions 10 | 11 | 1. `Cargo.toml` 12 | 13 | Add the build-executed binary. 14 | 15 | ```toml 16 | [[bin]] 17 | name = "copy-bootstrap-icons" 18 | ``` 19 | 20 | 2. `src/bin/copy-bootstrap-icons.rs` 21 | 22 | Create the program to copy the files. 23 | 24 | ```rust 25 | use std::path::PathBuf; 26 | use yew_bootstrap::icons::BIFiles; 27 | 28 | fn main() -> Result<(), std::io::Error> { 29 | let path = PathBuf::from( 30 | std::env::var("TRUNK_STAGING_DIR").expect("Environment variable TRUNK_STAGING_DIR"), 31 | ) 32 | .join(BIFiles::NAME); 33 | if !path.is_dir() { 34 | std::fs::create_dir(&path)?; 35 | } 36 | BIFiles::copy(&path) 37 | } 38 | ``` 39 | 40 | 3. `index.html` 41 | 42 | Set base reference, link the required CSS and specify your WASM program[^1]. 43 | 44 | [^1]: Since we'll be writing a build-executed program, there are now two binaries and trunk needs to know which is your WASM binary. 45 | 46 | ```html 47 | 48 | 49 | 50 | ``` 51 | 52 | 4. `Trunk.toml` 53 | 54 | Add a hook to run the build-executed program. 55 | 56 | ```toml 57 | [[hooks]] 58 | stage = "build" 59 | command = "cargo" 60 | command_arguments = ["run", "--bin", "copy-bootstrap-icons"] 61 | ``` 62 | -------------------------------------------------------------------------------- /examples/icons/Trunk.toml: -------------------------------------------------------------------------------- 1 | [[hooks]] 2 | stage = "post_build" 3 | command = "cargo" 4 | command_arguments = ["run", "--bin", "include-bootstrap-icons"] 5 | -------------------------------------------------------------------------------- /examples/icons/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Yew App 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/icons/src/bin/include-bootstrap-icons.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use yew_bootstrap::icons::BIFiles; 3 | 4 | fn main() -> Result<(), std::io::Error> { 5 | let staging_dir = PathBuf::from( 6 | std::env::var("TRUNK_STAGING_DIR").expect("Environment variable TRUNK_STAGING_DIR"), 7 | ); 8 | 9 | let path = staging_dir.join(BIFiles::NAME); 10 | if !path.is_dir() { 11 | std::fs::create_dir(&path)?; 12 | } 13 | BIFiles::copy(&path)?; 14 | 15 | let path = staging_dir.join("index.html"); 16 | let index = std::fs::read_to_string(&path)?; 17 | let index = index.replace( 18 | "", 19 | &format!( 20 | r#""#, 21 | BIFiles::VERSION 22 | ), 23 | ); 24 | std::fs::write(&path, index)?; 25 | 26 | Ok(()) 27 | } 28 | -------------------------------------------------------------------------------- /examples/icons/src/main.rs: -------------------------------------------------------------------------------- 1 | use yew::prelude::*; 2 | use yew_bootstrap::icons::*; 3 | 4 | #[function_component(App)] 5 | fn app() -> Html { 6 | html! { 7 |

{"I"}{BI::HEART_FILL}{BI::GEAR_FILL}

8 | } 9 | } 10 | 11 | fn main() { 12 | yew::Renderer::::new().render(); 13 | } 14 | -------------------------------------------------------------------------------- /examples/searchable_select/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "searchable_select" 3 | version = "0.0.1" 4 | authors = ["Cédric Airaud "] 5 | edition = "2021" 6 | license = "MIT" 7 | 8 | [dependencies] 9 | wasm-bindgen = "0.2.*" 10 | yew = { version = "0.21", features = ["csr"] } 11 | yew-bootstrap = { path = "../../packages/yew-bootstrap", features=["searchable_select"] } 12 | gloo-console = "0.3.*" 13 | -------------------------------------------------------------------------------- /examples/searchable_select/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Yew App 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/searchable_select/src/filter.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | use yew::prelude::*; 3 | use yew_bootstrap::component::{Container, ContainerSize, SearchableSelect, SOption, filter_case, FilterFn}; 4 | 5 | /// Properties for [Filter] 6 | #[derive(Properties, PartialEq)] 7 | pub struct FilterProps { 8 | /// List of items 9 | pub items: Vec, 10 | } 11 | 12 | /// Searchable select example with custom filter functions 13 | /// 14 | /// Show how to customize the filter function in the control 15 | #[function_component] 16 | pub fn Filter(props: &FilterProps) -> Html { 17 | // Get value of selected items. Empty string when none is selected 18 | let selected = use_state(|| AttrValue::from("")); 19 | 20 | // Create the options from the list of items and indicate which one is 21 | // selected, if any 22 | let options: Vec = props.items.iter().enumerate().map(|(i, item)| { 23 | let value = AttrValue::from(i.to_string()); 24 | SOption { 25 | selected: value == *selected, 26 | value, 27 | title: item.clone(), 28 | ..SOption::default() 29 | } 30 | }).collect(); 31 | 32 | // Called when an item selection is change. This is called both when an 33 | // item is selected and an item is deselected. 34 | let onselectchange = { 35 | let selected = selected.clone(); 36 | Callback::from(move |(value, to_selected): (AttrValue, bool)| { 37 | if to_selected { 38 | selected.set(value) 39 | } 40 | }) 41 | }; 42 | 43 | let title = (*selected != "").then_some(format!("Selected value: {}", *selected)); 44 | 45 | html! { 46 | 47 |

{ "Searchable Select Demo - Filter example" }

48 |

49 | { "By default filtering uses case-insensitive filtering. You can change to case-sensitive 50 | by using the " }{ "filter_case()" }{ "function." } 51 |

52 | 53 | 60 | 61 |

62 | { "You can also create your own filter. You need to create a function that returns 63 | a " }{ "FilterFn" }{ " structure with the wanted functionality. 64 | In this case, options are filtered when title starts with provided text (case 65 | sensitive)."} 66 |

67 | 68 | 75 |
76 | } 77 | } 78 | 79 | fn filter_custom() -> FilterFn { 80 | FilterFn(Rc::new( 81 | |value: AttrValue, option: &SOption| { 82 | option.title.to_string().starts_with(value.as_str()) 83 | } 84 | )) 85 | } -------------------------------------------------------------------------------- /examples/searchable_select/src/form.rs: -------------------------------------------------------------------------------- 1 | use yew::prelude::*; 2 | use yew_bootstrap::component::{Container, ContainerSize, SearchableSelect, SOption}; 3 | use yew_bootstrap::component::form::{FormControl, FormControlType, FormControlValidation, SelectOption}; 4 | 5 | /// Properties for [Form] 6 | #[derive(Properties, PartialEq)] 7 | pub struct FormProps { 8 | /// List of items 9 | pub items: Vec, 10 | } 11 | 12 | /// Searchable select example in a form 13 | /// 14 | /// Show how to handle a SearchableSelect inside a form 15 | #[function_component] 16 | pub fn Form(props: &FormProps) -> Html { 17 | // Get value of selected items. Empty string when none is selected 18 | let selected = use_state(|| AttrValue::from("")); 19 | 20 | // Create the options from the list of items and indicate which one is 21 | // selected, if any 22 | let options: Vec = props.items.iter().enumerate().map(|(i, item)| { 23 | let value = AttrValue::from(i.to_string()); 24 | SOption { 25 | selected: value == *selected, 26 | value, 27 | title: item.clone(), 28 | ..SOption::default() 29 | } 30 | }).collect(); 31 | 32 | // Called when an item selection is change. This is called both when an 33 | // item is selected and an item is deselected. 34 | let onselectchange = { 35 | let selected = selected.clone(); 36 | Callback::from(move |(value, to_selected): (AttrValue, bool)| { 37 | if to_selected { 38 | selected.set(value) 39 | } 40 | }) 41 | }; 42 | 43 | let title = (*selected != "").then_some(format!("Selected value: {}", *selected)); 44 | 45 | html! { 46 | 47 |

{ "Searchable Select Demo - Use in a form" }

48 |

49 | { "You can use this component in forms:" } 50 |

51 |
    52 |
  • { "Set " }{ "id" }{" if needed,"}
  • 53 |
  • { "You can set the validation field."}
  • 54 |
55 |

{ "Note: some limitations exist, for example floating labels are not available."}

56 | 57 | 64 | FormControlValidation::Invalid("You need to select a value".into()), 75 | "0" | "1" | "2" => FormControlValidation::Valid(Some("Selection at the top".into())), 76 | _ => FormControlValidation::None, 77 | } 78 | } 79 | /> 80 | 88 | { 89 | props.items.iter().enumerate().map(|(i, item)| html! { 90 | 91 | }).collect::() 92 | } 93 | 94 |
95 | } 96 | } -------------------------------------------------------------------------------- /examples/searchable_select/src/headers.rs: -------------------------------------------------------------------------------- 1 | use yew::prelude::*; 2 | use yew_bootstrap::component::{Container, ContainerSize, SearchableSelect, SOption}; 3 | 4 | /// Properties for [Headers] 5 | #[derive(Properties, PartialEq)] 6 | pub struct HeadersProps { 7 | /// List of items 8 | pub items: Vec, 9 | } 10 | 11 | /// Searchable select example with headers 12 | /// 13 | /// Shows the possibility to add headers to the list of options 14 | #[function_component] 15 | pub fn Headers(props: &HeadersProps) -> Html { 16 | // Get value of selected items. Empty string when none is selected 17 | let selected = use_state(|| AttrValue::from("")); 18 | 19 | // Create the options from the list of items and indicate which one is 20 | // selected, if any 21 | let options: Vec = props.items.iter().enumerate().map(|(i, item)| { 22 | let value = AttrValue::from(i.to_string()); 23 | SOption { 24 | selected: value == *selected, 25 | header: item.contains("Header"), 26 | disabled: item.contains("Date"), 27 | value, 28 | title: item.clone(), 29 | ..SOption::default() 30 | } 31 | }).collect(); 32 | 33 | // Called when an item selection is change. This is called both when an 34 | // item is selected and an item is deselected. 35 | let onselectchange = { 36 | let selected = selected.clone(); 37 | Callback::from(move |(value, to_selected): (AttrValue, bool)| { 38 | if to_selected { 39 | selected.set(value) 40 | } 41 | }) 42 | }; 43 | 44 | let title = (*selected != "").then_some(format!("Selected value: {}", *selected)); 45 | 46 | html! { 47 | 48 |

{ "Searchable Select Demo - Headers and disabled" }

49 |

50 | { "Options are divided in headers which appear as dividers. Headers cannot be selected 51 | and assumed to start a new group. If a group becomes empty after filtering, it is 52 | removed from the list." 53 | } 54 |

55 |

56 | { "Some entries can also be marked as 'disabled'. These entries cannot be selected either."} 57 |

58 | 59 | 65 |
66 | } 67 | } 68 | 69 | -------------------------------------------------------------------------------- /examples/searchable_select/src/main.rs: -------------------------------------------------------------------------------- 1 | use yew::prelude::*; 2 | use yew_bootstrap::util::{include_inline, include_cdn_js}; 3 | 4 | mod single; 5 | mod multiple; 6 | mod headers; 7 | mod form; 8 | mod filter; 9 | 10 | use single::*; 11 | use multiple::*; 12 | use headers::*; 13 | use form::*; 14 | use filter::*; 15 | 16 | #[function_component(App)] 17 | fn app() -> Html { 18 | // Example list of items 19 | let items = [ 20 | AttrValue::from("Apple"), 21 | AttrValue::from("Banana"), 22 | AttrValue::from("Cherry"), 23 | AttrValue::from("Date"), 24 | AttrValue::from("Elderberry"), 25 | AttrValue::from("Fig"), 26 | AttrValue::from("Grapes"), 27 | ]; 28 | 29 | let items_with_headers = [ 30 | AttrValue::from("Apple"), 31 | AttrValue::from("Header 1"), 32 | AttrValue::from("Banana"), 33 | AttrValue::from("Cherry"), 34 | AttrValue::from("Date"), 35 | AttrValue::from("Header 2"), 36 | AttrValue::from("Elderberry"), 37 | AttrValue::from("Fig"), 38 | AttrValue::from("Grapes"), 39 | ]; 40 | 41 | html! { 42 | <> 43 | { include_inline() } 44 | 45 | 46 | 47 |
48 | 49 |

50 | { include_cdn_js() } 51 | 52 | } 53 | } 54 | 55 | fn main() { 56 | yew::Renderer::::new().render(); 57 | } 58 | 59 | -------------------------------------------------------------------------------- /examples/searchable_select/src/multiple.rs: -------------------------------------------------------------------------------- 1 | use yew::prelude::*; 2 | use yew_bootstrap::component::{Container, ContainerSize, SearchableSelect, SOption}; 3 | use std::collections::HashSet; 4 | 5 | /// Properties for [Multiple] 6 | #[derive(Properties, PartialEq)] 7 | pub struct MultipleProps { 8 | /// List of items 9 | pub items: Vec, 10 | } 11 | 12 | /// Multiple searchable select example 13 | /// 14 | /// Show how to handle a SearchableSelect to select a multiple 15 | /// element and get the value 16 | #[function_component] 17 | pub fn Multiple(props: &MultipleProps) -> Html { 18 | // Hashset of selected items (by value) 19 | let selected = use_state(HashSet::::default); 20 | 21 | // Create the options from the list of items and indicate which one is 22 | // selected, if any 23 | let options: Vec = props.items.iter().enumerate().map(|(i, item)| { 24 | let value = AttrValue::from(i.to_string()); 25 | SOption { 26 | selected: (*selected).contains(&i), 27 | value, 28 | title: item.clone(), 29 | ..SOption::default() 30 | } 31 | }).collect(); 32 | 33 | // Called when an item selection is change. This is called both when an 34 | // item is selected and an item is deselected. 35 | let onselectchange = { 36 | let selected = selected.clone(); 37 | Callback::from(move |(value, to_selected): (AttrValue, bool)| { 38 | let index = value.parse().unwrap(); 39 | let mut set = (*selected).clone(); 40 | if to_selected { 41 | set.insert(index); 42 | } else { 43 | set.remove(&index); 44 | } 45 | selected.set(set); 46 | }) 47 | }; 48 | 49 | let selected_list: Vec = (*selected).iter().map( 50 | |value| value.to_string() 51 | ).collect(); 52 | 53 | let title: Option = (!(*selected).is_empty()).then_some( 54 | format!("Selected value: {}", selected_list.join(", ")) 55 | ); 56 | 57 | html! { 58 | 59 |

{ "Searchable Select Demo - Multiple selection" }

60 |

61 | { "Try typing in the dropdown and select a fruit. Multiple selections are possible, 62 | with the mouse or by selecting an entry and pressing Enter. "} 63 |

64 |

65 | { "The property "}{"keep_open"}{ " ensures that the dropdown does not 66 | close when an item is selected."} 67 |

68 |

69 | {"When an option is selected, handler "}{ "onselectchange" } {" is called with 70 | the value of the item and "}{ "selected" }{ " field saying if it will be selected 71 | or not. This is used to maintain the list of selected items."} 72 |

73 | 74 | 81 |
82 | } 83 | } -------------------------------------------------------------------------------- /examples/searchable_select/src/single.rs: -------------------------------------------------------------------------------- 1 | use yew::prelude::*; 2 | use yew_bootstrap::component::{Container, ContainerSize, SearchableSelect, SOption}; 3 | 4 | /// Properties for [Single] 5 | #[derive(Properties, PartialEq)] 6 | pub struct SingleProps { 7 | /// List of items 8 | pub items: Vec, 9 | } 10 | 11 | /// Single searchable select example 12 | /// 13 | /// Show how to handle a SearchableSelect to select a single 14 | /// element and get the value 15 | #[function_component] 16 | pub fn Single(props: &SingleProps) -> Html { 17 | // Get value of selected items. Empty string when none is selected 18 | let selected = use_state(|| AttrValue::from("")); 19 | 20 | // Create the options from the list of items and indicate which one is 21 | // selected, if any 22 | let options: Vec = props.items.iter().enumerate().map(|(i, item)| { 23 | let value = AttrValue::from(i.to_string()); 24 | SOption { 25 | selected: value == *selected, 26 | value, 27 | title: item.clone(), 28 | ..SOption::default() 29 | } 30 | }).collect(); 31 | 32 | // Called when an item selection is change. This is called both when an 33 | // item is selected and an item is deselected. 34 | let onselectchange = { 35 | let selected = selected.clone(); 36 | Callback::from(move |(value, to_selected): (AttrValue, bool)| { 37 | if to_selected { 38 | selected.set(value) 39 | } 40 | }) 41 | }; 42 | 43 | let title = (*selected != "").then_some(format!("Selected value: {}", *selected)); 44 | 45 | html! { 46 | 47 |

{ "Searchable Select Demo - Single selection" }

48 |

49 | { "Try typing in the dropdown and select a fruit. You have multiple possibilities 50 | to select an item:" } 51 |

52 |
    53 |
  • { "Click on an item," }
  • 54 |
  • { "When filtering, the first element becomes active and you can press Enter to select it,"}
  • 55 |
  • { "Or use arrow keys to select the one you want then press Enter."}
  • 56 |
57 |

58 | {"When an option is selected, handler "}{ "onselectchange" } {" is called with 59 | the value of the item and "}{ "selected" }{ " field saying if it will be selected 60 | or not. Beware that the function is called both for elements gettings selecting and loosing selection."} 61 |

62 |

63 | { "When an element is selected, the title shows the index of the element, provided as "}{"value"} 64 | { " field." } 65 |

66 | 67 | 73 |
74 | } 75 | } -------------------------------------------------------------------------------- /packages/yew-bootstrap/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "yew-bootstrap" 3 | version = "0.13.0" 4 | authors = ["Matthew Scheffel ", "Foorack "] 5 | edition = "2021" 6 | license = "MIT" 7 | readme = "README.md" 8 | keywords = ["yew", "bootstrap", "web"] 9 | categories = ["gui", "web-programming"] 10 | description = "Bootstrap 5 Components for Yew" 11 | repository = "https://github.com/isosphere/yew-bootstrap/" 12 | 13 | [lib] 14 | # You should include "rlib" (the default crate type) otherwise your crate can't be used as a Rust library 15 | # which, among other things, breaks unit testing 16 | crate-type = ["rlib", "cdylib"] 17 | name = "yew_bootstrap" 18 | 19 | [dependencies] 20 | yew = { version = "0.21", features = ["csr"] } 21 | gloo-console = "0.3" 22 | wasm-bindgen = "0.2.*" 23 | web-sys = { version = "0.3.*", features = ["HtmlElement", "MediaQueryList", "MediaQueryListEvent", "ScrollBehavior", "ScrollIntoViewOptions", "ScrollLogicalPosition"] } 24 | popper-rs = { version = "0.3.0", features = ["yew"] } 25 | gloo-utils = "0.2.0" 26 | 27 | # Dependencies for feature searchable_select 28 | js-sys = { version = "0.3", optional = true } 29 | 30 | [dev-dependencies] 31 | wasm-bindgen = "0.2" 32 | web-sys = { version = "0.3.*", features = ["HtmlTextAreaElement", "HtmlSelectElement"] } 33 | 34 | [features] 35 | searchable_select = ["dep:js-sys"] 36 | 37 | [build-dependencies] 38 | convert_case = { version = "0.6.0", default-features = false } 39 | anyhow = { version = "1.0.75", default-features = false, features = ["std"] } 40 | 41 | -------------------------------------------------------------------------------- /packages/yew-bootstrap/README.md: -------------------------------------------------------------------------------- 1 | ## Usage 2 | 3 | This project assumes that you have an existing web application that uses the [Yew framework](https://yew.rs/). 4 | 5 | Add the dependency next to the regular yew dependency: 6 | 7 | ```toml 8 | [dependencies] 9 | yew = "0.21" 10 | yew-bootstrap = "*" 11 | ``` 12 | 13 | To use form callback functions, the following dependencies should be added: 14 | 15 | ```toml 16 | [dependencies] 17 | wasm-bindgen = "0.2.*" 18 | web-sys = { version = "0.3.*", features = ["HtmlTextAreaElement", "HtmlSelectElement"] } 19 | ``` 20 | 21 | Some components need features to be enabled, for example: 22 | 23 | ```toml 24 | [dependencies] 25 | yew = "0.21" 26 | yew-bootstrap = { version = "*", features = ["searchable_select"] } 27 | ``` 28 | 29 | Then in the beginning of your application, include the `include_cdn()` or `include_inline()` function to load the required CSS. Some components require the Bootstrap JavaScript 30 | library to be loaded - for these you can use the `include_cdn_js()` function. It is recommended that you put this at the bottom of your `html!{}` macro, as done below: 31 | 32 | ```Rust 33 | fn view(&self, _ctx: &Context) -> Html { 34 | html! { 35 | <> 36 | {include_cdn()} 37 | 38 | {include_cdn_js()} 39 | 40 | } 41 | } 42 | ``` 43 | 44 | Check `main.rs` for example usage for every implemented component. 45 | 46 | ## Version Convention 47 | This project uses [semantic versioning](https://semver.org/). 48 | 49 | ## Coverage 50 | 51 | ### Core Content 52 | 53 | - [X] Container ([component::Container]) 54 | - [X] Grid ([component::Row], [component::Column]) 55 | - [x] Display headings ([component::Display]) 56 | - [x] Lead ([component::Lead]) 57 | - [ ] Blockquote 58 | - [ ] Image/Figure 59 | - [ ] Table 60 | - [x] Forms ([component::form::FormControl]) 61 | 62 | ### Components 63 | 64 | - [x] Accordion ([component::Accordion]) 65 | - [x] Alert ([component::Alert]) 66 | - [x] Badge ([component::Badge]) 67 | - [ ] Breadcrumb 68 | - [x] Button ([component::Button]) 69 | - [x] Button group ([component::ButtonGroup]) 70 | - [x] Card ([component::Card], [component::CardGroup]) 71 | - [ ] Carousel 72 | - [ ] Close button 73 | - [ ] Collapse 74 | - [ ] Dropdown 75 | - [x] List group ([component::ListGroup], [component::ListGroupItem]) 76 | - [x] Modal ([component::Modal]) 77 | - [x] Navbar ([component::NavBar], [component::NavItem], [component::NavDropdown], [component::NavDropdownItem]) 78 | - [ ] Navs & tabs 79 | - [ ] Offcanvas 80 | - [ ] Pagination 81 | - [ ] Placeholders 82 | - [ ] Popovers 83 | - [x] Progress ([component::Progress], [component::ProgressBar]) 84 | - [ ] Scrollspy 85 | - [x] Spinner ([component::Spinner]) 86 | - [ ] Toast 87 | - [x] Tooltips ([component::Tooltip]) 88 | 89 | ### Helpers 90 | 91 | - [ ] Clearfix 92 | - [x] Colored links ([component::Link]) 93 | - [ ] Stacks 94 | - [x] Stretched ([component::Link] with `stretched={true}>`) 95 | - [ ] Text truncation 96 | - [X] Vertical/Horizontal rule/line ([component::Line]) 97 | 98 | ### Extra components 99 | 100 | Some additional components are provided, not strictly part of Bootstrap, but based on Boostrap 101 | components. 102 | 103 | - [x] Searchable select ([component::SearchableSelect]) - Requires feature "searchable_select" 104 | 105 | A component similar to a Select showing a field to filter the items. 106 | 107 | ## Features 108 | 109 | ### `searchable_select` 110 | 111 | Enables the [component::SearchableSelect], which requires additional dependencies. 112 | 113 | ## Examples 114 | 115 | Several examples are provided: 116 | 117 | - `examples/basics`: Components 118 | - `examples/forms`: Form fields 119 | - `examples/searchable_select`: Searchable Select component 120 | 121 | To run an example: 122 | 123 | ```bash 124 | cd examples/ 125 | trunk --serve 126 | ``` 127 | -------------------------------------------------------------------------------- /packages/yew-bootstrap/bootstrap-icons-v1.11.3/fonts/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019-2024 The Bootstrap Authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/yew-bootstrap/bootstrap-icons-v1.11.3/fonts/bootstrap-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/isosphere/yew-bootstrap/f23c9dac1500a7aa061ccf400054a53841d1ac64/packages/yew-bootstrap/bootstrap-icons-v1.11.3/fonts/bootstrap-icons.woff -------------------------------------------------------------------------------- /packages/yew-bootstrap/bootstrap-icons-v1.11.3/fonts/bootstrap-icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/isosphere/yew-bootstrap/f23c9dac1500a7aa061ccf400054a53841d1ac64/packages/yew-bootstrap/bootstrap-icons-v1.11.3/fonts/bootstrap-icons.woff2 -------------------------------------------------------------------------------- /packages/yew-bootstrap/build.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use convert_case::{Case, Casing}; 3 | use std::collections::BTreeMap; 4 | use std::env; 5 | use std::io::Write; 6 | use std::path::Path; 7 | 8 | fn bootstrap_icons(output: &mut W) -> Result<()> { 9 | let mut sorted_icons = BTreeMap::new(); 10 | for ln in include_str!("bootstrap-icons-v1.11.3/bootstrap-icons.css").lines() { 11 | if let Some(ln) = ln.strip_prefix(".bi-") { 12 | if let Some((lower_kebab_case, _)) = ln.split_once("::before") { 13 | if !lower_kebab_case.is_empty() { 14 | let mut upper_snake_case = lower_kebab_case 15 | .from_case(Case::Kebab) 16 | .to_case(Case::UpperSnake); 17 | if upper_snake_case 18 | .chars() 19 | .all(|c| c == '_' || c.is_ascii_alphanumeric()) 20 | { 21 | let first = upper_snake_case.chars().next().unwrap(); 22 | if !first.is_ascii_alphabetic() && first != '_' { 23 | upper_snake_case.insert(0, '_'); 24 | } 25 | sorted_icons.insert(lower_kebab_case, format!( 26 | " pub const {upper_snake_case}: BI = BI(\"\");\n" 27 | )); 28 | } 29 | } 30 | } 31 | } 32 | } 33 | output.write_all("#[allow(missing_docs)]\n".as_bytes())?; 34 | output.write_all("impl BI{\n".as_bytes())?; 35 | sorted_icons 36 | .values() 37 | .try_for_each(|i| output.write_all(i.as_bytes()))?; 38 | output.write_all("}\n".as_bytes())?; 39 | Ok(()) 40 | } 41 | 42 | fn main() -> Result<()> { 43 | let out_dir = env::var_os("OUT_DIR").context("OUT_DIR not found")?; 44 | let dest_path = Path::new(&out_dir).join("bootstrap_icons_generated.rs"); 45 | bootstrap_icons(&mut std::fs::File::create(dest_path)?) 46 | } 47 | -------------------------------------------------------------------------------- /packages/yew-bootstrap/src/component/accordion.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use yew::prelude::*; 4 | 5 | /// # Properties of [AccordionHeader] 6 | #[derive(Properties, Clone, PartialEq)] 7 | struct AccordionHeaderProps { 8 | /// The html id of this component 9 | #[prop_or_default] 10 | heading_id: AttrValue, 11 | 12 | /// The title displayed in the header 13 | #[prop_or_default] 14 | title: AttrValue, 15 | 16 | /// Classes attached to the button holding the title 17 | #[prop_or_default] 18 | button_classes: Classes, 19 | 20 | /// The html id of associated collapse for this [AccordionItem] 21 | #[prop_or_default] 22 | collapse_id: AttrValue, 23 | 24 | /// If the associated accordion collapse is open 25 | #[prop_or_default] 26 | expanded: bool 27 | } 28 | 29 | /// # Accordion Header 30 | /// Used with [crate::component::AccordionItem] to create accordion drop downs 31 | /// This represents the title of the accordion item that is always visible 32 | /// 33 | /// See [AccordionHeaderProps] for a listing of properties 34 | /// 35 | /// This component is not meant to be used stand-alone as it's only rendered inside of Accordions 36 | #[function_component] 37 | fn AccordionHeader(props: &AccordionHeaderProps) -> Html { 38 | html! { 39 |

40 | 50 |

51 | } 52 | } 53 | 54 | /// # Properties of [AccordionCollapse] 55 | #[derive(Properties, Clone, PartialEq)] 56 | struct AccordionCollapseProps { 57 | /// Parent [Accordion] html id attribute 58 | #[prop_or(AttrValue::from("main-accordion"))] 59 | parent_id: AttrValue, 60 | 61 | /// Html id of this component 62 | #[prop_or_default] 63 | collapse_id: AttrValue, 64 | 65 | /// Html id of associated header for this [AccordionItem] 66 | #[prop_or_default] 67 | heading_id: AttrValue, 68 | 69 | /// Opening this item will close other items in the [Accordion] 70 | #[prop_or_default] 71 | stay_open: bool, 72 | 73 | /// Classes attached to the div 74 | #[prop_or_default] 75 | class: Classes, 76 | 77 | /// Inner components 78 | #[prop_or_default] 79 | children: Children, 80 | } 81 | 82 | /// # Accordion Collapse 83 | /// Used with [crate::component::AccordionItem] to create accordion drop downs 84 | /// This represents the body of the accordion item that can be opened/closed 85 | /// 86 | /// See [AccordionCollapseProps] for a listing of properties 87 | /// 88 | /// This component is not meant to be used stand-alone as it's only rendered inside of Accordions 89 | #[function_component] 90 | fn AccordionCollapse(props: &AccordionCollapseProps) -> Html { 91 | if props.stay_open { 92 | return html! { 93 |
94 | { for props.children.iter() } 95 |
96 | } 97 | } 98 | html! { 99 |
100 | { for props.children.iter() } 101 |
102 | } 103 | } 104 | 105 | /// # Properties of [AccordionItem] 106 | #[derive(Properties, Clone, PartialEq)] 107 | pub struct AccordionItemProps { 108 | /// Text displayed in this items heading 109 | #[prop_or_default] 110 | pub title: AttrValue, 111 | 112 | /// Item is currently open 113 | #[prop_or_default] 114 | pub expanded: bool, 115 | 116 | /// Inner components (displayed in the [AccordionCollapse]) 117 | #[prop_or_default] 118 | pub children: Children, 119 | 120 | /// Opening this item doesn't close other items 121 | #[prop_or_default] 122 | stay_open: bool, 123 | 124 | /// Html id attribute of parent [Accordion] 125 | #[prop_or(AttrValue::from("main-accordion"))] 126 | parent_id: AttrValue, 127 | 128 | /// Position in the parent [Accordion] 129 | #[prop_or_default] 130 | item_id: usize, 131 | } 132 | 133 | /// # A singular accordion item, child of [Accordion] 134 | /// Used as a child of [Accordion] to create an accordion menu. 135 | /// 136 | /// Child components will be displayed in the body of the accordion item 137 | #[function_component] 138 | pub fn AccordionItem(props: &AccordionItemProps) -> Html { 139 | let heading_id = format!("{}-heading-{}", props.parent_id, props.item_id); 140 | let collapse_id = format!("{}-collapse-{}", props.parent_id, props.item_id); 141 | 142 | let mut button_classes = classes!("accordion-button"); 143 | let mut collapse_classes = classes!("accordion-collapse", "collapse"); 144 | 145 | // TODO: Maybe hook up the `expanded` property to some state depending on `stay_open` 146 | // 147 | // I think in the bootstrap docs this is really only meant to show one item as expanded after loading the page 148 | // However as it currently is, users may be able to set this on multiple items at once 149 | // This is probably fine during initial page load since they can be closed individually 150 | // But it acts weird if an end-user were to open another item as it would close all of them unless `stay_open` is true 151 | // 152 | // Additionally if some other part of the page is setup to use state to open an item 153 | // This will cause 2 items to be open at once even if the `stay_open` flag is false 154 | // There's no real harm putting the closing of accordion items on the user, but it would be nice if there were 155 | // some sort of built in way to handle this 156 | // 157 | // I use ssr in my project so ideally this would also not interfere with rendering server side 158 | if !props.expanded { 159 | button_classes.push("collapsed"); 160 | } else { 161 | collapse_classes.push("show"); 162 | } 163 | 164 | html! { 165 |
166 | 173 | 180 |
181 | { for props.children.iter() } 182 |
183 |
184 |
185 | } 186 | } 187 | 188 | /// # Properties of [Accordion] 189 | #[derive(Properties, Clone, PartialEq)] 190 | pub struct AccordionProps { 191 | /// Html id of the accordion - should be unique within it's page 192 | #[prop_or(AttrValue::from("main-accordion"))] 193 | pub id: AttrValue, 194 | 195 | /// Accordion is flush with the container and removes some styling elements 196 | #[prop_or_default] 197 | pub flush: bool, 198 | 199 | /// Opening an item won't close other items in the accordion 200 | #[prop_or_default] 201 | pub stay_open: bool, 202 | 203 | // The [AccordionItem] instances controlled by this accordion 204 | #[prop_or_default] 205 | pub children: ChildrenWithProps, 206 | } 207 | 208 | /// # Accordion 209 | /// [Accordion] is used to group several [crate::component::AccordionItem] instances together. 210 | /// 211 | /// See [AccordionProps] for a listing of properties. 212 | /// 213 | /// See [bootstrap docs](https://getbootstrap.com/docs/5.0/components/accordion/) for a full demo of accordions 214 | /// 215 | /// Basic example of using an Accordion 216 | /// 217 | /// ```rust 218 | /// use yew::prelude::*; 219 | /// use yew_bootstrap::component::{Accordion, AccordionItem}; 220 | /// fn test() -> Html { 221 | /// html!{ 222 | /// 223 | /// 224 | ///

{"Some text inside "}{"THE BODY"}{" of the accordion item"}

225 | ///
226 | /// 227 | ///

{"Some other text under another accordion"}

228 | /// 229 | ///
230 | ///
231 | /// } 232 | /// } 233 | /// ``` 234 | /// 235 | /// 236 | /// Example of using an Accordion while mapping a list to AccordionItem children 237 | /// 238 | /// ```rust 239 | /// use yew::{prelude::*, virtual_dom::VChild}; 240 | /// use yew_bootstrap::component::{Accordion, AccordionItem}; 241 | /// fn test() -> Html { 242 | /// let items = vec![("title1", "body1"), ("title2", "body2")]; 243 | /// html! { 244 | /// 245 | /// { 246 | /// items.iter().map(|item| { 247 | /// html_nested! { 248 | /// 249 | /// {item.0.clone()} 250 | /// 251 | /// } 252 | /// }).collect::>>() 253 | /// } 254 | /// 255 | /// } 256 | /// } 257 | /// ``` 258 | #[function_component] 259 | pub fn Accordion(props: &AccordionProps) -> Html { 260 | let mut classes = classes!("accordion"); 261 | if props.flush { 262 | classes.push("accordion-flush"); 263 | } 264 | 265 | html! { 266 |
267 | { 268 | for props.children.iter().enumerate().map(|(index, mut child)| { 269 | let child_props = Rc::make_mut(&mut child.props); 270 | child_props.item_id = index; 271 | child_props.parent_id = props.id.clone(); 272 | child_props.stay_open = props.stay_open; 273 | child 274 | }) 275 | } 276 |
277 | } 278 | } -------------------------------------------------------------------------------- /packages/yew-bootstrap/src/component/alert.rs: -------------------------------------------------------------------------------- 1 | use yew::prelude::*; 2 | 3 | use crate::util::Color; 4 | 5 | /// # Alert component 6 | /// Used alongside [crate::util::Color] to create Alert components 7 | /// 8 | /// See [AlertProps] for a listing of properties 9 | /// 10 | /// ## Example 11 | /// ```rust 12 | /// use yew::prelude::*; 13 | /// use yew_bootstrap::component::Alert; 14 | /// use yew_bootstrap::util::Color; 15 | /// fn test() -> Html { 16 | /// html!{ 17 | /// 18 | /// {"This is a primary alert!"} 19 | /// 20 | /// } 21 | /// } 22 | /// ``` 23 | pub struct Alert {} 24 | 25 | /// # Properties of [Alert] 26 | #[derive(Properties, Clone, PartialEq)] 27 | pub struct AlertProps { 28 | /// CSS class 29 | #[prop_or_default] 30 | pub class: String, 31 | 32 | /// Inner components 33 | #[prop_or_default] 34 | pub children: Children, 35 | 36 | /// Color style, default [Color::Primary] 37 | #[prop_or(Color::Primary)] 38 | pub style: Color, 39 | 40 | /// Optional text placed before the children 41 | #[prop_or_default] 42 | pub text: String, 43 | } 44 | 45 | impl Component for Alert { 46 | type Message = (); 47 | type Properties = AlertProps; 48 | 49 | fn create(_ctx: &Context) -> Self { 50 | Self {} 51 | } 52 | 53 | fn view(&self, ctx: &Context) -> Html { 54 | let props = ctx.props(); 55 | let mut classes = Classes::new(); 56 | classes.push("alert"); 57 | classes.push(format!("alert-{}", props.style)); 58 | classes.push(props.class.clone()); 59 | 60 | html! { 61 | 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/yew-bootstrap/src/component/badge.rs: -------------------------------------------------------------------------------- 1 | use yew::prelude::*; 2 | 3 | use crate::util::{Color, ArrangeX, ArrangeY}; 4 | 5 | /// # Badge component 6 | /// Used alongside [crate::util::Color] to create Badge components 7 | /// 8 | /// See [BadgeProps] for a listing of properties 9 | /// 10 | /// ## Example 11 | /// ```rust 12 | /// use yew::prelude::*; 13 | /// use yew_bootstrap::component::Badge; 14 | /// use yew_bootstrap::util::Color; 15 | /// fn test() -> Html { 16 | /// html!{ 17 | /// 18 | /// {"This is a primary badge!"} 19 | /// 20 | /// } 21 | /// } 22 | /// ``` 23 | pub struct Badge {} 24 | 25 | /// # Properties of [Badge] 26 | #[derive(Properties, Clone, PartialEq)] 27 | pub struct BadgeProps { 28 | /// CSS class 29 | #[prop_or_default] 30 | pub class: String, 31 | 32 | /// Inner components 33 | #[prop_or_default] 34 | pub children: Children, 35 | 36 | /// Show badge more rounded as pill 37 | #[prop_or_default] 38 | pub pill: bool, 39 | 40 | /// Show badge positioned 41 | #[prop_or_default] 42 | pub position: Option<(ArrangeX, ArrangeY)>, 43 | 44 | /// Color style, default [Color::Primary] 45 | #[prop_or(Color::Primary)] 46 | pub style: Color, 47 | 48 | /// Optional text placed before the children 49 | #[prop_or_default] 50 | pub text: String, 51 | } 52 | 53 | impl Component for Badge { 54 | type Message = (); 55 | type Properties = BadgeProps; 56 | 57 | fn create(_ctx: &Context) -> Self { 58 | Self {} 59 | } 60 | 61 | fn view(&self, ctx: &Context) -> Html { 62 | let props = ctx.props(); 63 | let mut classes = Classes::new(); 64 | match &props.position { 65 | Some(position) => { 66 | classes.push("position-absolute".to_string()); 67 | classes.push(format!("{}", position.0)); 68 | classes.push(format!("{}", position.1)); 69 | classes.push("translate-middle".to_string()); 70 | } 71 | None => {} 72 | } 73 | classes.push("badge"); 74 | if props.pill { 75 | classes.push("rounded-pill"); 76 | } 77 | classes.push(format!("bg-{}", props.style)); 78 | if [Color::Warning, Color::Info, Color::Light].contains(&props.style) { 79 | classes.push("text-dark"); 80 | } 81 | classes.push(props.class.clone()); 82 | 83 | html! { 84 | 87 | { &props.text } 88 | { for props.children.iter() } 89 | 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /packages/yew-bootstrap/src/component/button.rs: -------------------------------------------------------------------------------- 1 | use crate::util::Color; 2 | use yew::prelude::*; 3 | 4 | #[derive(Clone, PartialEq, Eq)] 5 | pub enum ButtonSize { 6 | Large, 7 | Normal, 8 | Small, 9 | } 10 | 11 | impl Default for ButtonSize { 12 | fn default() -> Self { 13 | ButtonSize::Normal 14 | } 15 | } 16 | 17 | /// # Button component 18 | /// Button with various properties, including support for opening or closing a modal 19 | /// dialog [crate::component::Modal]. 20 | /// 21 | /// Buttons can be grouped in a [crate::component::ButtonGroup]. 22 | /// 23 | /// See [ButtonProps] for a listing of properties. 24 | /// 25 | /// ## Example 26 | /// Example of a simple button: 27 | /// 28 | /// ```rust 29 | /// use yew::prelude::*; 30 | /// use yew_bootstrap::component::Button; 31 | /// use yew_bootstrap::util::Color; 32 | /// fn test() -> Html { 33 | /// html!{ 34 | /// 52 | /// 53 | /// 56 | /// 57 | /// } 58 | /// } 59 | /// ``` 60 | /// 61 | /// A button may also link to a web page. 62 | /// 63 | /// ```rust 64 | /// use yew::prelude::*; 65 | /// use yew_bootstrap::component::Button; 66 | /// use yew_bootstrap::util::Color; 67 | /// fn test() -> Html { 68 | /// html!{ 69 | /// 206 | } 207 | } else if let Some(url) = props.url.as_ref().filter(|_| !props.disabled) { 208 | html! { 209 | 220 | { &props.text } 221 | { for props.children.iter() } 222 | 223 | } 224 | } else { 225 | html! { 226 | 238 | } 239 | } 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /packages/yew-bootstrap/src/component/button_group.rs: -------------------------------------------------------------------------------- 1 | use yew::prelude::*; 2 | 3 | /// # Button group 4 | /// [ButtonGroup] is used to group several [crate::component::Button] instances together. 5 | /// Buttons can be arranged vertically. 6 | /// 7 | /// See [ButtonGroupProps] for a listing of properties. 8 | /// 9 | /// ## Example 10 | /// Example of a simple button group: 11 | /// 12 | /// ```rust 13 | /// use yew::prelude::*; 14 | /// use yew_bootstrap::component::{Button, ButtonGroup}; 15 | /// use yew_bootstrap::util::Color; 16 | /// fn test() -> Html { 17 | /// html!{ 18 | /// 19 | ///