diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 24f946b1d83..f7180013e61 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -2,7 +2,7 @@ github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: deniskolodin -open_collective: # Replace with a single Open Collective username +open_collective: yew ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000000..0b9ab3afb68 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,40 @@ +--- +name: Bug report +about: Create a report to help us improve Yew +title: '' +labels: bug +assignees: '' + +--- + +**Problem** + + +**Steps To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Environment:** + - Yew version [e.g. v0.10, `master`] + - Rust version [e.g. 1.40.0] + - Target if relevant [e.g. `wasm32-unknown-emscripten`] + - `stdweb` / `web-sys` version [e.g. web-sys v0.3.33] + - OS: [e.g. macos] + - Browser [e.g. chrome, safari] + - Browser version [e.g. 22] + +**Questionnaire** + + +- [ ] I'm interested in fixing this myself but don't know where to start +- [ ] I would like to fix and I have a solution +- [ ] I don't have time to fix this right now, but maybe later diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000000..b4ebf2a4b2d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,22 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: feature +assignees: '' + +--- + + + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.travis.yml b/.travis.yml index 7f0db07a1b0..96124adf0ad 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,40 +4,40 @@ branches: - trying - master -language: rust - dist: trusty +language: rust sudo: false addons: chrome: stable -cache: - timeout: 1000 - directories: - - $HOME/.cargo - - $HOME/.rustup - - $HOME/.local/share/cargo-web/emscripten +cache: cargo +before_cache: + - ./ci/clear_cache.sh rust: - - 1.35.0 # min supported + - 1.39.0 # min supported - stable - beta - - nightly matrix: allow_failures: - - rust: nightly + - rust: beta fast_finish: true install: - nvm install 9 - rustup component add rustfmt + - rustup component add clippy - rustup target add wasm32-unknown-unknown - - cargo install --force --version 0.2.42 -- wasm-bindgen-cli - - curl --retry 5 -LO https://chromedriver.storage.googleapis.com/2.41/chromedriver_linux64.zip + - cargo install cargo-update || true + - cargo install-update-config --version =0.2.58 wasm-bindgen-cli + - cargo install-update wasm-bindgen-cli + - LATEST_CHROMEDRIVER_VERSION=`curl -s "https://chromedriver.storage.googleapis.com/LATEST_RELEASE"` + - curl --retry 5 -LO "https://chromedriver.storage.googleapis.com/${LATEST_CHROMEDRIVER_VERSION}/chromedriver_linux64.zip" - unzip chromedriver_linux64.zip - ./ci/install_cargo_web.sh script: - - cargo fmt --all -- --check + - ./ci/run_checks.sh - CHROMEDRIVER=$(pwd)/chromedriver ./ci/run_tests.sh + - CHROMEDRIVER=$(pwd)/chromedriver ./ci/check_examples.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 988d7181e82..4e35085604d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,506 @@ # Changelog -## ✨ **0.9** *(TBD)* +## ✨ **0.11** *(2020-01-06)* + +This release aims to lay the groundwork for Yew component libraries and clean up the API for the ever elusive 1.0 release. + +### Transition Guide + +This release comes with a lot of breaking changes. We understand it's a hassle to update projects but the Yew team felt it was necessary to rip a few bandaids off now as we approach a 1.0 release in the (hopefully) near future. To ease the transition, here's a guide which outlines the main refactoring you will need to do for your project. (Note: all of the changes are reflected in the many example projects if you would like a proper reference example) + +#### 1. Callback syntax + +This is the main painful breaking change. It applies to both element listeners as well as `Component` callback properties. A good rule of thumb is that your components will now have to retain a `ComponentLink` to create callbacks on demand or initialize callbacks in your component's `create()` method. + +Before: +```rust +struct Model; + +enum Msg { + Click, +} + +impl Component for Model { + type Message = Msg; + type Properties = (); + + fn create(_: Self::Properties, _: ComponentLink) -> Self { + Model + } + + fn update(&mut self, msg: Self::Message) -> ShouldRender { + match msg { + Msg::Click => true, + } + } + + fn view(&self) -> Html { + // BEFORE: Callbacks were created implicitly from this closure syntax + html! { + + } + } +} +``` + +After: +```rust +struct Model { + link: ComponentLink, +} + +enum Msg { + Click, +} + +impl Component for Model { + type Message = Msg; + type Properties = (); + + fn create(_: Self::Properties, link: ComponentLink) -> Self { + Model { link } + } + + fn update(&mut self, msg: Self::Message) -> ShouldRender { + match msg { + Msg::Click => true, + } + } + + fn view(&self) -> Html { + // AFTER: Callbacks need to be explicitly created now + let onclick = self.link.callback(|_| Msg::Click); + html! { + + } + } +} +``` + +If a closure has a parameter you will now need to specify the parameter's type. A tip for finding the appropriate type is to search Yew's repo for the HTML attribute the closure is assigned to. + +For example, `onkeydown` of ` +} +``` + +#### 2. Method Renames + +It should be safe to do a project-wide find/replace for the following: + +- `send_self(` -> `send_message(` +- `send_back(` -> `callback(` +- `response(` -> `respond(` +- `AgentUpdate` -> `AgentLifecycleEvent` + +These renames will probably require some more care: + +- `fn handle(` -> `fn handle_input(` *(for Agent trait implementations)* + +#### 3. Drop Generic Types for `Html` -> `Html` + +:tada: We are pretty excited about this change! The generic type parameter +was confusing and restrictive and is now a thing of the past! + +Before: +```rust +impl Component for Model { + // ... + + fn view(&self) -> Html { + html! { /* ... */ } + } +} +``` + +After: +```rust +impl Component for Model { + // ... + + fn view(&self) -> Html { + html! { /* ... */ } + } +} +``` + +#### 4. Properties must implement `Clone` + +In yew v0.8 we removed the requirement that component properties implement `Clone` +and in this release we are adding the requirement again. This change is needed +to improve the ergonomics of nested components. The only time properties will be +cloned is when a wrapper component re-renders nested children components. + +- #### ⚡️ Features + + - Added `html_nested!` macro to support nested iterable children access. [[@trivigy], [#843](https://github.com/yewstack/yew/pull/843)] + - Added `bincode` to the list of supported formats. [[@serzhiio], [#806](https://github.com/yewstack/yew/pull/806)] + - Added a `noop()` convenience method to `Callback` which creates a no-op callback. [[@mdtusz], [#793](https://github.com/yewstack/yew/pull/793)] + - The `html!` macro now accepts a `Callback` for element listeners. [[@jstarry], [#777](https://github.com/yewstack/yew/pull/777)] + + ```rust + struct Model { + onclick: Callback, + } + + enum Msg { + Click, + } + + impl Component for Model { + type Message = Msg; + type Properties = (); + + fn create(_: Self::Properties, link: ComponentLink) -> Self { + Model { + onclick: link.callback(|_| Msg::Click), + } + } + + fn update(&mut self, msg: Self::Message) -> ShouldRender { + match msg { + Msg::Click => true, + } + } + + fn view(&self) -> Html { + html! { + + } + } + } + ``` + + - Add `send_message_batch` method to `ComponentLink`. [[@hgzimmerman], [#748](https://github.com/yewstack/yew/pull/748)] + - Allow compilation to `wasi` target without `wasm_bindgen`. [[@dunnock], [#746](https://github.com/yewstack/yew/pull/746)] + - `AgentLink` now implements `Clone` which enables `Future` usage without explicit Yew framework support. [[@izissise], [#802](https://github.com/yewstack/yew/pull/802)] + - `ComponentLink` now implements `Clone` which enables `Future` usage without explicit Yew framework support. [[@hgzimmerman], [#749](https://github.com/yewstack/yew/pull/749)] + + ```rust + use wasm_bindgen::JsValue; + use wasm_bindgen_futures::future_to_promise; + + // future must implement `Future + 'static` + let link = self.link.clone(); + let js_future = async move { + link.send_message(future.await); + Ok(JsValue::NULL) + }; + + future_to_promise(js_future); + ``` + +- #### 🛠 Fixes + + - Fixed handling of boolean tag attributes. [[@mrh0057], [#840](https://github.com/yewstack/yew/pull/840)] + - Improved nested component ergonomics. [[@jstarry], [#780](https://github.com/yewstack/yew/pull/780)] + + ```rust + fn view(&self) -> Html { + html! { + + // This is now valid. (before #780, this would cause a lifetime + // compile error because children nodes were moved into a closure) + + + } + } + ``` + + - Creating a `Callback` with `ComponentLink` is no longer restricted to mutable references, improving ergonomics. [[@jstarry], [#780](https://github.com/yewstack/yew/pull/780)] + - The `Callback` `reform` method no longer consumes self making it easier to "reverse map" a `Callback`. [[@jstarry], [#779](https://github.com/yewstack/yew/pull/779)] + + ```rust + pub struct ListHeader { + props: Props, + } + + #[derive(Properties, Clone)] + pub struct Props { + #[props(required)] + pub on_hover: Callback, + #[props(required)] + pub text: String, + } + + impl Component for ListHeader { + type Message = (); + type Properties = Props; + + fn create(props: Self::Properties, _: ComponentLink) -> Self { + ListHeader { props } + } + + fn update(&mut self, _: Self::Message) -> ShouldRender { + false + } + + fn view(&self) -> Html { + let onmouseover = self.props.on_hover.reform(|_| Hovered::Header); + html! { +
+ { &self.props.text } +
+ } + } + } + ``` + + - Reduced allocations in the `Classes` `to_string` method. [[@hgzimmerman], [#772](https://github.com/yewstack/yew/pull/772)] + - Empty string class names are now filtered out to prevent panics. [[@jstarry], [#770](https://github.com/yewstack/yew/pull/770)] + +- #### 🚨 Breaking changes + + - Components with generic args now need to be closed with the full type path. (e.g. `html! { >>}`) [[@jstarry], [#837](https://github.com/yewstack/yew/pull/837)] + - Changed `VTag` listener type from `Box` to `Rc`. [[@jstarry], [#786](https://github.com/yewstack/yew/pull/786)] + - `Properties` need to implement `Clone` again in order to improve nested component ergonomics. [[@jstarry], [#786](https://github.com/yewstack/yew/pull/786)] + - Removed `send_future` method from `ComponentLink` since it is no longer necessary for using Futures with Yew. [[@hgzimmerman], [#799](https://github.com/yewstack/yew/pull/799)] + - Removed generic type parameter from `Html` and all virtual node types: `VNode`, `VComp`, `VTag`, `VList`, `VText`, etc. [[@jstarry], [#783](https://github.com/yewstack/yew/pull/783)] + - Removed support for macro magic closure syntax for element listeners. (See transition guide for how to pass a `Callback` explicitly instead). [[@jstarry], [#782](https://github.com/yewstack/yew/pull/782)] + - Renamed `Agent` methods and event type for clarity. `handle` -> `handle_input`, `AgentUpdate` -> `AgentLifecycleEvent`, `response` -> `respond`. [[@philip-peterson], [#751](https://github.com/yewstack/yew/pull/751)] + - The `ComponentLink` `send_back` method has been renamed to `callback` for clarity. [[@jstarry], [#780](https://github.com/yewstack/yew/pull/780)] + - The `ComponentLink` `send_self` and `send_self_batch` methods have been renamed to `send_message` and `send_message_batch` for clarity. [[@jstarry], [#780](https://github.com/yewstack/yew/pull/780)] + - The `Agent` `send_back` method has been renamed to `callback` for clarity. [[@jstarry], [#780](https://github.com/yewstack/yew/pull/780)] + - The `VTag` `children` value type has changed from `Vec` to `VList`. [[@jstarry], [#754](https://github.com/yewstack/yew/pull/754)] + + +## ✨ **0.10** *(2019-11-11)* - #### ⚡️ Features + - `Future` support :tada: A `Component` can update following the completion of a `Future`. Check out [this example](https://github.com/yewstack/yew/tree/master/examples/futures) to see how it works. This approach was borrowed from a fork of Yew called [`plaster`](https://github.com/carlosdp/plaster) created by [@carlosdp]. [[@hgzimmerman], [#717](https://github.com/yewstack/yew/pull/717)] + - Added the `agent` and `services` features so that this functionality can be disabled (useful if you are switching to using `Future`s). [[@hgzimmerman], [#684](https://github.com/yewstack/yew/pull/684)] + - Add `ref` keyword for allowing a `Component` to have a direct reference to its rendered elements. For example, you can now easily focus an `` element after mounting. [[@jstarry], [#715](https://github.com/yewstack/yew/pull/715)] + + ```rust + use stdweb::web::html_element::InputElement; + use stdweb::web::IHtmlElement; + use yew::*; + + pub struct Input { + node_ref: NodeRef, + } + + impl Component for Input { + type Message = (); + type Properties = (); + + fn create(_: Self::Properties, _: ComponentLink) -> Self { + Input { + node_ref: NodeRef::default(), + } + } + + fn mounted(&mut self) -> ShouldRender { + if let Some(input) = self.node_ref.try_into::() { + input.focus(); + } + false + } + + fn update(&mut self, _: Self::Message) -> ShouldRender { + false + } + + fn view(&self) -> Html { + html! { + + } + } + } + ``` + + - Make `Agent` related types `public` to allow other crates to create custom agents. [[@dunnock], [#721](https://github.com/yewstack/yew/pull/721)] + - `Component::change` will now return `false` for components that have `Component::Properties == ()`. [[@kellytk], [#690](https://github.com/yewstack/yew/pull/690)]] + - Updated `wasm-bindgen` dependency to `0.2.54`. Please update your `wasm-bindgen-cli` tool by running `cargo install --force --version 0.2.54 -- wasm-bindgen-cli`. [[@jstarry], [#730](https://github.com/yewstack/yew/pull/730)], [[@ctaggart], [#681](https://github.com/yewstack/yew/pull/681)] + +- #### 🛠 Fixes + + - Fixed the mount order of components. The root component will be mounted after all descendants have been mounted. [[@jstarry], [#725](https://github.com/yewstack/yew/pull/725)] + - All public items now implement `Debug`. [[@hgzimmerman], [#673](https://github.com/yewstack/yew/pull/673)] + +- #### 🚨 Breaking changes + + - Minimum rustc version has been bumped to `1.39.0` for `Future` support. [[@jstarry], [#730](https://github.com/yewstack/yew/pull/730)] + - `Component` now has a required `view` method and automatically implements the `Renderable` trait. The `view` method in the `Renderable` trait has been renamed to `render`. [[@jstarry], [#563](https://github.com/yewstack/yew/pull/563)] + + Before: + ```rust + impl Component for Model { + type Message = Msg; + type Properties = (); + + fn create(_: Self::Properties, _: ComponentLink) -> Self { + Model {} + } + + fn update(&mut self, msg: Self::Message) -> ShouldRender { + true + } + } + + impl Renderable for Model { + fn view(&self) -> Html { + html! { "hello" } + } + } + ``` + + After: + ```rust + impl Component for Model { + type Message = Msg; + type Properties = (); + + fn create(_: Self::Properties, _: ComponentLink) -> Self { + Model {} + } + + fn update(&mut self, msg: Self::Message) -> ShouldRender { + true + } + + fn view(&self) -> Html { + html! { "hello" } + } + } + ``` + + - Removed the `Transferable` trait since it did no more than extend the serde `Serialize` and `Deserialize` traits. [[@hgzimmerman], [#319](https://github.com/yewstack/yew/pull/319)] + + Before: + ```rust + impl Transferable for Input {} + #[derive(Serialize, Deserialize)] + pub enum Input { + Connect, + } + ``` + + After: + ```rust + #[derive(Serialize, Deserialize)] + pub enum Input { + Connect, + } + ``` + - `WebSocketService::connect` will now return a `Result` in order to stop panicking on malformed urls. [[@lizhaoxian], [#727](https://github.com/yewstack/yew/pull/727)] + - `VTag` now is boxed within `VNode` to shrink the size of its enum representation. [[@hgzimmerman], [#675](https://github.com/yewstack/yew/pull/675)] + +## ✨ **0.9.2** *(2019-10-12)* + +- #### 🛠 Fixes + + - Fix `yew-macro` dependency version + +## ✨ **0.9.1** *(2019-10-12)* + +Happy Canadian Thanksgiving! 🦃 + +- #### ⚡️ Features + + - Implemented `Default` trait for `VNode` so that `unwrap_or_default` can be called on `Option>`. [[@hgzimmerman], [#672](https://github.com/yewstack/yew/pull/672)] + - Implemented `PartialEq` trait for `Classes` so that is more ergonomic to use `Classes` type in component props. [[@hgzimmerman], [#680](https://github.com/yewstack/yew/pull/680)] + - Updated `wasm-bindgen` dependency to `0.2.50`. Please update your `wasm-bindgen-cli` tool by running `cargo install --force --version 0.2.50 -- wasm-bindgen-cli`. [[@jstarry], [#695](https://github.com/yewstack/yew/pull/695)] + +- #### 🛠 Fixes + + - Fixed issue where text nodes were sometimes rendered out of order. [[@jstarry], [#697](https://github.com/yewstack/yew/pull/697)] + - Fixed regression introduced in 0.9.0 that prevented tag attributes from updating properly. [[@jstarry], [#698](https://github.com/yewstack/yew/pull/698)] + - Fixed emscripten builds by pinning the version for the `ryu` downstream dependency. [[@jstarry], [#703](https://github.com/yewstack/yew/pull/703)] + - Updated `stdweb` to `0.4.20` which fixed emscripten builds and unblocked updating `wasm-bindgen` to `0.2.50`. [[@ctaggart], [@jstarry], [#683](https://github.com/yewstack/yew/pull/683), [#694](https://github.com/yewstack/yew/pull/694)] + - Cleaned up build warnings for missing `dyn` keywords. [[@benreyn], [#687](https://github.com/yewstack/yew/pull/687)] + +## ✨ **0.9** *(2019-09-27)* + +- #### ⚡️ Features + + - New `KeyboardService` for setting up key listeners on browsers which support the feature. [[@hgzimmerman], [#647](https://github.com/yewstack/yew/pull/647)] + - `ComponentLink` can now create a `Callback` with more than one `Message`. The `Message`'s will be batched together so that the `Component` will not be re-rendered more than necessary. [[@stkevintan], [#660](https://github.com/yewstack/yew/pull/660)] + - `Message`'s to `Public` `Agent`'s will now be queued if the `Agent` hasn't finished setting up yet. [[@serzhiio], [#596](https://github.com/yewstack/yew/pull/596)] + - `Agent`'s can now be connected to without a `Callback`. Instead of creating a bridge to the agent, create a dispatcher like so: `MyAgent::dispatcher()`. [[@hgzimmerman], [#639](https://github.com/yewstack/yew/pull/639)] + - `Component`'s can now accept children in the `html!` macro. [[@jstarry], [#589](https://github.com/yewstack/yew/pull/589)] + + ```rust + // app.rs + + html! { + + + + } + ``` + + ```rust + // my_list.rs + + use yew::prelude::*; + + pub struct MyList(Props); + + #[derive(Properties)] + pub struct Props { + #[props(required)] + pub name: String, + pub children: Children, + } + + impl Renderable for MyList { + fn view(&self) -> Html { + html! {{ + self.props.children.iter().collect::>() + }} + } + } + ``` + + - `Iterator`s can now be rendered in the `html!` macro without using the `for` keyword. [[@hgzimmerman], [#622](https://github.com/yewstack/yew/pull/622)] + + Before: + ```rust + html! {{ + for self.props.items.iter().map(renderItem) + }} + ``` + + After: + ```rust + html! {{ + self.props.items.iter().map(renderItem).collect::>() + }} + ``` + + - Closures are now able to be transformed into optional `Callback` properties. [[@Wodann], [#612](https://github.com/yewstack/yew/pull/612)] + - Improved CSS class ergonomics with new `Classes` type. [[@DenisKolodin], [#585](https://github.com/yewstack/yew/pull/585)], [[@hgzimmerman], [#626](https://github.com/yewstack/yew/pull/626)] + - Touch events are now supported `
` [[@boydjohnson], [#584](https://github.com/yewstack/yew/pull/584)], [[@jstarry], [#656](https://github.com/yewstack/yew/pull/656)] + - The `Component` trait now has an `mounted` method which can be implemented to react to when your components have been mounted to the DOM. [[@hgzimmerman], [#583](https://github.com/yewstack/yew/pull/583)] + - Additional Fetch options `mode`, `cache`, and `redirect` are now supported [[@davidkna], [#579](https://github.com/yewstack/yew/pull/579)] - The derive props macro now supports Properties with lifetimes [[@jstarry], [#580](https://github.com/yewstack/yew/pull/580)] - New `ResizeService` for registering for `window` size updates [[@hgzimmerman], [#577](https://github.com/yewstack/yew/pull/577)] - #### 🛠 Fixes + - Fixed JS typo in RenderService. This was causing animation frames to not be dropped correctly. [[@jstarry], [#658](https://github.com/yewstack/yew/pull/658)] + - Fixed `VNode` orphaning bug when destroying `VTag` elements. This caused some `Component`s to not be properly destroyed when they should have been. [[@hgzimmerman], [#651](https://github.com/yewstack/yew/pull/651)] + - Fix mishandling of Properties `where` clause in derive_props macro [[@astraw], [#640](https://github.com/yewstack/yew/pull/640)] + - #### 🚨 Breaking changes + None + ## ✨ **0.8** *(2019-08-10)* ***Props! Props! Props!*** @@ -148,11 +638,26 @@ This release introduces the concept of an `Agent`. Agents are separate activitie ## ✨ **0.1** *(2017-12-31)* [Web Workers API]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API +[@astraw]: https://github.com/astraw +[@boydjohnson]: https://github.com/boydjohnson +[@carlosdp]: https://github.com/carlosdp [@charvp]: https://github.com/charvp +[@ctaggart]: https://github.com/ctaggart +[@davidkna]: https://github.com/davidkna [@DenisKolodin]: https://github.com/DenisKolodin [@dermetfan]: https://github.com/dermetfan +[@dunnock]: https://github.com/dunnock [@hgzimmerman]: https://github.com/hgzimmerman +[@izissise]: https://github.com/izissise [@jstarry]: https://github.com/jstarry [@kellytk]: https://github.com/kellytk +[@lizhaoxian]: https://github.com/lizhaoxian +[@mdtusz]: https://github.com/mdtusz +[@mrh0057]: https://github.com/mrh0057 +[@philip-peterson]: https://github.com/philip-peterson +[@serzhiio]: https://github.com/serzhiio +[@stkevintan]: https://github.com/stkevintan [@tiziano88]: https://github.com/tiziano88 +[@trivigy]: https://github.com/trivigy [@totorigolo]: https://github.com/totorigolo +[@Wodann]: https://github.com/Wodann diff --git a/Cargo.toml b/Cargo.toml index 98c4418619f..6398428894f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,13 @@ [package] name = "yew" -version = "0.9.0" +version = "0.11.1" edition = "2018" authors = [ "Denis Kolodin ", - "Justin Starry ", + "Justin Starry ", ] -repository = "https://github.com/DenisKolodin/yew" -homepage = "https://github.com/DenisKolodin/yew" +repository = "https://github.com/yewstack/yew" +homepage = "https://github.com/yewstack/yew" documentation = "https://docs.rs/yew/" license = "MIT/Apache-2.0" readme = "README.md" @@ -16,42 +16,51 @@ categories = ["gui", "web-programming"] description = "A framework for making client-side single-page apps" [badges] -travis-ci = { repository = "DenisKolodin/yew" } +travis-ci = { repository = "yewstack/yew" } [dependencies] +anyhow = "1" anymap = "0.12" -bincode = "=1.0.1" -failure = "0.1" -http = "0.1" +bincode = { version = "~1.2.1", optional = true } +http = "0.2" indexmap = "1.0.2" log = "0.4" proc-macro-hack = "0.5" proc-macro-nested = "0.1" -rmp-serde = { version = "0.13.7", optional = true } +rmp-serde = { version = "0.14.0", optional = true } serde = { version = "1.0", features = ["derive"] } -serde_cbor = { version = "0.9.0", optional = true } +serde_cbor = { version = "0.11.1", optional = true } serde_json = "1.0" serde_yaml = { version = "0.8.3", optional = true } slab = "0.4" -stdweb = "^0.4.16" -toml = { version = "0.4", optional = true } -yew-macro = { version = "0.9.0", path = "crates/macro" } +stdweb = "0.4.20" +thiserror = "1" +toml = { version = "0.5", optional = true } +yew-macro = { version = "0.11.1", path = "crates/macro" } -[target.'cfg(all(target_arch = "wasm32", not(cargo_web)))'.dependencies] -wasm-bindgen = "=0.2.42" +[target.'cfg(all(target_arch = "wasm32", not(target_os="wasi"), not(cargo_web)))'.dependencies] +wasm-bindgen = "0.2.58" + +[target.'cfg(all(target_arch = "wasm32", not(target_os="wasi"), not(cargo_web)))'.dev-dependencies] +wasm-bindgen-test = "0.3.4" + +[target.'cfg(target_os = "emscripten")'.dependencies] +ryu = "1.0.2" # 1.0.1 breaks emscripten [dev-dependencies] serde_derive = "1" trybuild = "1.0" -rustversion = "0.1" - -[target.'cfg(all(target_arch = "wasm32", not(cargo_web)))'.dev-dependencies] -wasm-bindgen-test = "0.2" +rustversion = "1.0" +rmp-serde = "0.14.0" +bincode = "~1.2.1" [features] -default = [] +default = ["services", "agent"] +doc_test = [] web_test = [] wasm_test = [] +services = [] +agent = ["bincode"] yaml = ["serde_yaml"] msgpack = ["rmp-serde"] cbor = ["serde_cbor"] @@ -59,25 +68,4 @@ cbor = ["serde_cbor"] [workspace] members = [ "crates/macro", - "examples/counter", - "examples/crm", - "examples/custom_components", - "examples/dashboard", - "examples/file_upload", - "examples/fragments", - "examples/game_of_life", - "examples/inner_html", - "examples/js_callback", - "examples/large_table", - "examples/minimal", - "examples/mount_point", - "examples/multi_thread", - "examples/npm_and_rest", - "examples/routing", - "examples/server", - "examples/showcase", - "examples/textarea", - "examples/timer", - "examples/todomvc", - "examples/two_apps", ] diff --git a/README.md b/README.md index e009a0f69fd..8b3cb0afbbf 100644 --- a/README.md +++ b/README.md @@ -8,20 +8,26 @@

- Rust / Wasm UI framework + Rust / Wasm client web app framework

Build Status Gitter Chat - Rustc Version 1.35+ + Rustc Version 1.39+

+ Website + | + API Docs + | Examples | Changelog | + Roadmap + | Code of Conduct

@@ -47,18 +53,18 @@ and uses a local scheduler attached to a thread for concurrent tasks. This framework is designed to be compiled into modern browsers' runtimes: wasm, asm.js, emscripten. -To prepare the development environment use the installation instruction here: [wasm-and-rust](https://github.com/raphamorim/wasm-and-rust). - -### Clean MVC approach inspired by Elm and Redux +### Architecture inspired by Elm and Redux Yew implements strict application state management based on message passing and updates: `src/main.rs` ```rust -use yew::{html, Component, ComponentLink, Html, Renderable, ShouldRender}; +use yew::{html, Component, ComponentLink, Html, ShouldRender}; -struct Model { } +struct Model { + link: ComponentLink, +} enum Msg { DoIt, @@ -70,8 +76,8 @@ impl Component for Model { type Message = Msg; type Properties = (); - fn create(_: Self::Properties, _: ComponentLink) -> Self { - Model { } + fn create(_: Self::Properties, link: ComponentLink) -> Self { + Model { link } } fn update(&mut self, msg: Self::Message) -> ShouldRender { @@ -82,13 +88,12 @@ impl Component for Model { } } } -} -impl Renderable for Model { - fn view(&self) -> Html { + fn view(&self) -> Html { + let onclick = self.link.callback(|_| Msg::DoIt); html! { // Render your model here - + } } } @@ -123,12 +128,11 @@ html! { } ``` -### Agents - actors model inspired by Erlang and Actix +### Agents - actor model inspired by Erlang and Actix Every `Component` can spawn an agent and attach to it. -Agents are separate tasks that work concurrently. - -Create your worker/agent (in `context.rs` for example): +Agents can coordinate global state, spawn long-running tasks, and offload tasks to a web worker. +They run independently of components, but hook nicely into their update mechanism. ```rust use yew::worker::*; @@ -149,27 +153,28 @@ pub enum Response { impl Agent for Worker { // Available: - // - `Job` (one per bridge) - // - `Context` (shared in the same thread) - // - `Public` (separate thread). - type Reach = Context; // Spawn only one instance per thread (all components could reach this) + // - `Job` (one per bridge on the main thread) + // - `Context` (shared in the main thread) + // - `Private` (one per bridge in a separate thread) + // - `Public` (shared in a separate thread) + type Reach = Context; // Spawn only one instance on the main thread (all components can share this agent) type Message = Msg; type Input = Request; type Output = Response; - // Create an instance with a link to agent's environment. + // Create an instance with a link to the agent. fn create(link: AgentLink) -> Self { Worker { link } } - // Handle inner messages (of services of `send_back` callbacks) + // Handle inner messages (from callbacks) fn update(&mut self, msg: Self::Message) { /* ... */ } // Handle incoming messages from components of other agents. - fn handle(&mut self, msg: Self::Input, who: HandlerId) { + fn handle_input(&mut self, msg: Self::Input, who: HandlerId) { match msg { Request::Question(_) => { - self.link.response(who, Response::Answer("That's cool!".into())); + self.link.respond(who, Response::Answer("That's cool!".into())); }, } } @@ -193,7 +198,7 @@ impl Component for Model { type Properties = (); fn create(_: Self::Properties, link: ComponentLink) -> Self { - let callback = link.send_back(|_| Msg::ContextMsg); + let callback = link.callback(|_| Msg::ContextMsg); // `Worker::bridge` spawns an instance if no one is available let context = context::Worker::bridge(callback); // Connected! :tada: Model { context } @@ -205,7 +210,7 @@ You can use as many agents as you want. For example you could separate all inter with a server to a separate thread (a real OS thread because Web Workers map to the native threads). > **REMEMBER!** Not every API is available for every environment. For example you can't use -`StorageService` from a separate thread. It won't work with `Public` agents, +`StorageService` from a separate thread. It won't work with `Public` or `Private` agents, only with `Job` and `Context` ones. ### Components @@ -218,6 +223,9 @@ html! { } ``` @@ -232,7 +240,7 @@ Properties are also pure Rust types with strict type-checking during the compila ```rust // my_button.rs -#[derive(Properties, PartialEq)] +#[derive(Clone, Properties, PartialEq)] pub struct Properties { pub hidden: bool, #[props(required)] @@ -256,7 +264,7 @@ html! { ### Fragments -Yew supports fragments: elements without a parent which could be attached somewhere later. +Yew supports fragments: elements without a parent which can be attached to one somewhere else. ```rust html! { @@ -268,12 +276,9 @@ html! { } ``` -### Virtual DOM, independent loops, fine updates +### Virtual DOM -Yew uses its own **virtual-dom** implementation. It updates the browser's DOM -with tiny patches when properties of elements have changed. Every component lives -in its own independent loop interacting with the environment (`Scope`) through message passing -and supports a fine control of rendering. +Yew uses its own **virtual-dom** implementation. It updates the browser's DOM with tiny patches when properties of elements have changed. Every component can be interacted with using its (`Scope`) to pass messages and trigger updates. The `ShouldRender` returns the value which informs the loop when the component should be re-rendered: @@ -291,9 +296,8 @@ fn update(&mut self, msg: Self::Message) -> ShouldRender { } ``` -Using `ShouldRender` is more effective than comparing the model after every update because not every model -change leads to a view update. It allows the framework to skip the model comparison checks entirely. -This also allows you to control updates as precisely as possible. +Using `ShouldRender` is more effective than comparing the model after every update because not every change to the model +causes an update to the view. It allows the framework to only compare parts of the model essential to rendering the view. ### Rust/JS/C-style comments in templates @@ -319,8 +323,8 @@ Use external crates and put values from them into the template: extern crate chrono; use chrono::prelude::*; -impl Renderable for Model { - fn view(&self) -> Html { +impl Renderable for Model { + fn render(&self) -> Html { html! {

{ Local::now() }

} @@ -328,7 +332,7 @@ impl Renderable for Model { } ``` -> Some crates don't support the true wasm target (`wasm32-unknown-unknown`) yet. +> Some crates don't support the `wasm32-unknown-unknown` target yet. ### Services @@ -339,11 +343,14 @@ It's a handy alternative to subscriptions. Implemented: * `IntervalService` * `RenderService` +* `ResizeService` * `TimeoutService` * `StorageService` * `DialogService` +* `ConsoleService` * `FetchService` * `WebSocketService` +* `KeyboardService` ```rust use yew::services::{ConsoleService, TimeoutService}; @@ -358,8 +365,8 @@ impl Component for Model { fn update(&mut self, msg: Self::Message) -> ShouldRender { match msg { Msg::Fire => { - let send_msg = self.link.send_back(|_| Msg::Timeout); - self.timeout.spawn(Duration::from_secs(5), send_msg); + let timeout = self.link.callback(|_| Msg::Timeout); + self.timeout.spawn(Duration::from_secs(5), timeout); } Msg::Timeout => { self.console.log("Timeout!"); @@ -370,7 +377,7 @@ impl Component for Model { ``` Can't find an essential service? Want to use a library from `npm`? -You can reuse `JavaScript` libraries with `stdweb` capabilities and create +You can wrap `JavaScript` libraries using `stdweb` and create your own service implementation. Here's an example below of how to wrap the [ccxt](https://www.npmjs.com/package/ccxt) library: @@ -443,7 +450,7 @@ your project's `Cargo.toml`: ```toml [dependencies] -yew = { git = "https://github.com/DenisKolodin/yew", features = ["toml", "yaml", "msgpack", "cbor"] } +yew = { git = "https://github.com/yewstack/yew", features = ["toml", "yaml", "msgpack", "cbor"] } ``` ## Development setup @@ -470,6 +477,16 @@ cargo build --target wasm32-unknown-unknown ``` ### Running Tests +For the tests to work one have to ensure that `wasm-bindgen-cli` is installed. +[Instructions](https://rustwasm.github.io/docs/wasm-bindgen/wasm-bindgen-test/usage.html#install-the-test-runner) + +Additionally a webdriver must be installed locally and configured to be on the +`PATH`. Currently supports `geckodriver`, `chromedriver`, and `safaridriver`, +although more driver support may be added! You can download these at: + +* geckodriver - https://github.com/mozilla/geckodriver/releases +* chromedriver - http://chromedriver.chromium.org/downloads +* safaridriver - should be preinstalled on OSX ```bash ./ci/run_tests.sh @@ -509,3 +526,39 @@ if you tell the `cargo-web` to build for them using the `--target` parameter. [todomvc]: examples/todomvc [two_apps]: examples/two_apps [cargo-web]: https://github.com/koute/cargo-web + + +## Project templates + +* [`yew-wasm-pack-template`](https://github.com/yewstack/yew-wasm-pack-template) +* [`yew-wasm-pack-minimal`](https://github.com/yewstack/yew-wasm-pack-minimal) + +## Contributors + +### Code Contributors + +This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)]. + + +### Financial Contributors + +Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/yew/contribute)] + +#### Individuals + + + +#### Organizations + +Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/yew/contribute)] + + + + + + + + + + + diff --git a/ci/check_examples.sh b/ci/check_examples.sh new file mode 100755 index 00000000000..7b46284a6c1 --- /dev/null +++ b/ci/check_examples.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +echo "$(rustup default)" | grep -q "1.39.0" +emscripten_supported=$? +set -euxo pipefail # https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ + +# Showcase includes all other examples +cd examples/showcase + +# TODO Can't build some demos with release, need fix + +if [ "$emscripten_supported" == "0" ]; then + # TODO - Emscripten builds are broken on rustc > 1.39.0 + cargo web build --target asmjs-unknown-emscripten + cargo web build --target wasm32-unknown-emscripten +fi + +# TODO showcase doesn't support wasm-bindgen yet +cargo web build --target wasm32-unknown-unknown + +# Reset cwd +cd ../.. diff --git a/ci/clear_cache.sh b/ci/clear_cache.sh new file mode 100755 index 00000000000..f5227d7be2d --- /dev/null +++ b/ci/clear_cache.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -x + +# inspired by https://github.com/rust-analyzer/rust-analyzer/blob/master/.travis.yml +find ./target/debug -maxdepth 1 -type f -delete +find ./target/tests/target/debug -maxdepth 1 -type f -delete +find ./target/asmjs-unknown-emscripten/debug -maxdepth 1 -type f -delete +find ./target/wasm32-unknown-emscripten/debug -maxdepth 1 -type f -delete +find ./target/wasm32-unknown-unknown/debug -maxdepth 1 -type f -delete +rm -fr ./target/debug/{deps,.fingerprint}/{*yew*,*\.was,*\.js*,*test*} +rm -fr ./target/tests/target/debug/{deps,.fingerprint}/{*yew*,*\.was,*\.js*,*test*} +rm -fr ./target/asmjs-unknown-emscripten/debug/{deps,.fingerprint}/{*yew*,*\.was,*\.js*,*test*} +rm -fr ./target/wasm32-unknown-emscripten/debug/{deps,.fingerprint}/{*yew*,*\.was*,*\.js*,*test*} +rm -fr ./target/wasm32-unknown-unknown/debug/{deps,.fingerprint}/{*yew*,*\.was*,*\.js*,*test*} +rm -fr ./target/debug/incremental +rm -fr ./target/tests/target/debug/incremental +rm -fr ./target/asmjs-unknown-emscripten/debug/incremental +rm -fr ./target/wasm32-unknown-emscripten/debug/incremental +rm -fr ./target/wasm32-unknown-unknown/debug/incremental +rm -f ./target/.rustc_info.json +rm -f ./target/tests/target/.rustc_info.json +rm -fr ./target/wasm32-unknown-unknown/wbg-tmp +rm -fr /home/travis/.cargo/registry/index/github.com-* diff --git a/ci/run_checks.sh b/ci/run_checks.sh new file mode 100755 index 00000000000..65e7ce91cd6 --- /dev/null +++ b/ci/run_checks.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +echo "$(rustup default)" | grep -q "stable" +if [ "$?" != "0" ]; then + # only run checks on stable + exit 0 +fi + +set -euxo pipefail +cargo fmt --all -- --check +cargo clippy -- --deny=warnings diff --git a/ci/run_tests.sh b/ci/run_tests.sh index 6bba2d2f655..6f80d409d2c 100755 --- a/ci/run_tests.sh +++ b/ci/run_tests.sh @@ -1,62 +1,16 @@ #!/usr/bin/env bash - -# Originally this ci script borrowed from https://github.com/koute/stdweb -# because both use `cargo-web` tool to check the compilation. - -set -euo pipefail -IFS=$'\n\t' - -set +e -echo "$(rustc --version)" | grep -q "nightly" -if [ "$?" = "0" ]; then - export IS_NIGHTLY=1 -else - export IS_NIGHTLY=0 +echo "$(rustup default)" | grep -q "1.39.0" +emscripten_supported=$? +set -euxo pipefail # https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ + +if [ "$emscripten_supported" == "0" ]; then + # TODO - Emscripten builds are broken on rustc > 1.39.0 + cargo web test --features web_test --target asmjs-unknown-emscripten + cargo web test --features web_test --target wasm32-unknown-emscripten fi -set -e - -echo "Is Rust from nightly: $IS_NIGHTLY" - -echo "Testing for asmjs-unknown-emscripten..." -cargo web test --features web_test --target=asmjs-unknown-emscripten - -echo "Testing for wasm32-unknown-emscripten..." -cargo web test --features web_test --target=wasm32-unknown-emscripten - -echo "Testing for wasm32-unknown-unknown..." -cargo test --features wasm_test --target=wasm32-unknown-unknown -echo "Testing html macro..." +cargo test --features wasm_test --target wasm32-unknown-unknown cargo test --test macro_test - -echo "Testing derive props macro..." cargo test --test derive_props_test - -echo "Testing macro docs..." -(cd crates/macro && cargo test) - -check_example() { - echo "Checking example [$2]" - pushd $2 > /dev/null - cargo web build --target=$1 - popd > /dev/null - - # TODO Can't build some demos with release, need fix - # cargo web build --release $CARGO_WEB_ARGS -} - -check_all_examples() { - echo "Checking examples on $1..." - for EXAMPLE in $(pwd)/examples/showcase/sub/*; do - if [ -d "$EXAMPLE" ]; then - check_example $1 $EXAMPLE - fi - done -} - -# Check showcase only to speed up a building with CI -# Showcase includes all other examples -SHOWCASE=$(pwd)/examples/showcase -check_example asmjs-unknown-emscripten $SHOWCASE -check_example wasm32-unknown-emscripten $SHOWCASE -check_example wasm32-unknown-unknown $SHOWCASE +cargo test --doc --all-features +(cd crates/macro && cargo test --doc) diff --git a/crates/macro/Cargo.toml b/crates/macro/Cargo.toml index a53d70e3bd6..573dbb95b2e 100644 --- a/crates/macro/Cargo.toml +++ b/crates/macro/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "yew-macro" -version = "0.9.0" +version = "0.11.1" edition = "2018" -authors = ["Justin Starry "] -repository = "https://github.com/DenisKolodin/yew" -homepage = "https://github.com/DenisKolodin/yew" +authors = ["Justin Starry "] +repository = "https://github.com/yewstack/yew" +homepage = "https://github.com/yewstack/yew" documentation = "https://docs.rs/yew-macro/" license = "MIT/Apache-2.0" keywords = ["web", "wasm", "frontend", "webasm", "webassembly"] @@ -12,7 +12,7 @@ categories = ["gui", "web-programming", "wasm"] description = "A framework for making client-side single-page apps" [badges] -travis-ci = { repository = "DenisKolodin/yew" } +travis-ci = { repository = "yewstack/yew" } [lib] proc-macro = true @@ -21,12 +21,15 @@ proc-macro = true boolinator = "2.4.0" lazy_static = "1.3.0" proc-macro-hack = "0.5" -proc-macro2 = "0.4" -quote = "0.6" -syn = { version = "^0.15.34", features = ["full", "extra-traits"] } +proc-macro2 = "1.0" +quote = "1.0" +syn = { version = "1.0", features = ["full", "extra-traits"] } [dev-dependencies] yew = { path = "../.." } [build-dependencies] -autocfg = "0.1.3" +autocfg = "1.0.0" + +[features] +doc_test = [] diff --git a/crates/macro/src/derive_props/builder.rs b/crates/macro/src/derive_props/builder.rs index e374e3a7158..d17ae1ca343 100644 --- a/crates/macro/src/derive_props/builder.rs +++ b/crates/macro/src/derive_props/builder.rs @@ -9,7 +9,6 @@ use super::generics::{to_arguments, with_param_bounds, GenericArguments}; use super::{DerivePropsInput, PropField}; use proc_macro2::{Ident, Span}; use quote::{quote, ToTokens}; -use std::iter; pub struct PropsBuilder<'a> { builder_name: &'a Ident, @@ -36,9 +35,6 @@ impl ToTokens for PropsBuilder<'_> { .. } = props; - let step_trait_repeat = iter::repeat(step_trait); - let vis_repeat = iter::repeat(&vis); - let build_step = self.build_step(); let impl_steps = self.impl_steps(); let set_fields = self.set_fields(); @@ -59,21 +55,23 @@ impl ToTokens for PropsBuilder<'_> { let builder = quote! { #( #[doc(hidden)] - #vis_repeat struct #step_names; + #vis struct #step_names; )* #[doc(hidden)] #vis trait #step_trait {} - #(impl #step_trait_repeat for #step_names {})* + #(impl #step_trait for #step_names {})* #[doc(hidden)] - #vis struct #builder_name#step_generics { + #vis struct #builder_name#step_generics + #where_clause + { wrapped: ::std::boxed::Box<#wrapper_name#ty_generics>, _marker: ::std::marker::PhantomData<#step_generic_param>, } - #(#impl_steps)* + #impl_steps impl#impl_generics #builder_name<#generic_args> #where_clause { #[doc(hidden)] diff --git a/crates/macro/src/derive_props/field.rs b/crates/macro/src/derive_props/field.rs index b216b66db0e..a1a393850ad 100644 --- a/crates/macro/src/derive_props/field.rs +++ b/crates/macro/src/derive_props/field.rs @@ -1,24 +1,35 @@ use super::generics::GenericArguments; use proc_macro2::{Ident, Span}; -use quote::quote; +use quote::{quote, quote_spanned}; use std::cmp::{Ord, Ordering, PartialEq, PartialOrd}; use std::convert::TryFrom; use syn::parse::Result; -use syn::punctuated; use syn::spanned::Spanned; -use syn::{Error, Field, Meta, MetaList, NestedMeta, Type, Visibility}; +use syn::{ + Error, ExprPath, Field, Lit, Meta, MetaList, MetaNameValue, NestedMeta, Type, Visibility, +}; + +#[derive(PartialEq, Eq)] +enum PropAttr { + Required { wrapped_name: Ident }, + Default { default: ExprPath }, + None, +} #[derive(Eq)] pub struct PropField { ty: Type, name: Ident, - wrapped_name: Option, + attr: PropAttr, } impl PropField { /// All required property fields are wrapped in an `Option` pub fn is_required(&self) -> bool { - self.wrapped_name.is_some() + match self.attr { + PropAttr::Required { .. } => true, + _ => false, + } } /// This step name is descriptive to help a developer realize they missed a required prop @@ -32,13 +43,16 @@ impl PropField { /// Used to transform the `PropWrapper` struct into `Properties` pub fn to_field_setter(&self) -> proc_macro2::TokenStream { let name = &self.name; - if let Some(wrapped_name) = &self.wrapped_name { - quote! { - #name: self.wrapped.#wrapped_name.unwrap(), + match &self.attr { + PropAttr::Required { wrapped_name } => { + quote! { + #name: self.wrapped.#wrapped_name.unwrap(), + } } - } else { - quote! { - #name: self.wrapped.#name, + _ => { + quote! { + #name: self.wrapped.#name, + } } } } @@ -46,28 +60,52 @@ impl PropField { /// Wrap all required props in `Option` pub fn to_field_def(&self) -> proc_macro2::TokenStream { let ty = &self.ty; - if let Some(wrapped_name) = &self.wrapped_name { - quote! { - #wrapped_name: ::std::option::Option<#ty>, + match &self.attr { + PropAttr::Required { wrapped_name } => { + quote! { + #wrapped_name: ::std::option::Option<#ty>, + } } - } else { - let name = &self.name; - quote! { - #name: #ty, + _ => { + let name = &self.name; + quote! { + #name: #ty, + } } } } /// All optional props must implement the `Default` trait pub fn to_default_setter(&self) -> proc_macro2::TokenStream { - if let Some(wrapped_name) = &self.wrapped_name { - quote! { - #wrapped_name: ::std::default::Default::default(), + match &self.attr { + PropAttr::Required { wrapped_name } => { + quote! { + #wrapped_name: ::std::option::Option::None, + } } - } else { - let name = &self.name; - quote! { - #name: ::std::default::Default::default(), + PropAttr::Default { default } => { + let name = &self.name; + let ty = &self.ty; + let span = default.span(); + // Hacks to avoid misleading error message. + quote_spanned! {span=> + #name: { + match true { + #[allow(unreachable_code)] + false => { + let __unreachable: #ty = ::std::unreachable!(); + __unreachable + }, + true => #default() + } + }, + } + } + PropAttr::None => { + let name = &self.name; + quote! { + #name: ::std::default::Default::default(), + } } } } @@ -79,64 +117,77 @@ impl PropField { generic_arguments: &GenericArguments, vis: &Visibility, ) -> proc_macro2::TokenStream { - let Self { - name, - ty, - wrapped_name, - } = self; - if let Some(wrapped_name) = wrapped_name { - quote! { - #[doc(hidden)] - #vis fn #name(mut self, #name: #ty) -> #builder_name<#generic_arguments> { - self.wrapped.#wrapped_name = ::std::option::Option::Some(#name); - #builder_name { - wrapped: self.wrapped, - _marker: ::std::marker::PhantomData, + let Self { name, ty, attr } = self; + match attr { + PropAttr::Required { wrapped_name } => { + quote! { + #[doc(hidden)] + #vis fn #name(mut self, #name: #ty) -> #builder_name<#generic_arguments> { + self.wrapped.#wrapped_name = ::std::option::Option::Some(#name); + #builder_name { + wrapped: self.wrapped, + _marker: ::std::marker::PhantomData, + } } } } - } else { - quote! { - #[doc(hidden)] - #vis fn #name(mut self, #name: #ty) -> #builder_name<#generic_arguments> { - self.wrapped.#name = #name; - self + _ => { + quote! { + #[doc(hidden)] + #vis fn #name(mut self, #name: #ty) -> #builder_name<#generic_arguments> { + self.wrapped.#name = #name; + self + } } } } } - // Detect the `#[props(required)]` attribute which denotes required fields - fn required_wrapper(named_field: &syn::Field) -> Result> { + // Detect `#[props(required)]` or `#[props(default="...")]` attribute + fn attribute(named_field: &syn::Field) -> Result { let meta_list = if let Some(meta_list) = Self::find_props_meta_list(named_field) { meta_list } else { - return Ok(None); + return Ok(PropAttr::None); }; - let expected_required = syn::Error::new(meta_list.span(), "expected `props(required)`"); + let expected_attr = syn::Error::new( + meta_list.span(), + "expected `props(required)` or `#[props(default=\"...\")]`", + ); let first_nested = if let Some(first_nested) = meta_list.nested.first() { first_nested } else { - return Err(expected_required); + return Err(expected_attr); }; + match first_nested { + NestedMeta::Meta(Meta::Path(word_path)) => { + if !word_path.is_ident("required") { + return Err(expected_attr); + } - let word_ident = match first_nested { - punctuated::Pair::End(NestedMeta::Meta(Meta::Word(ident))) => ident, - _ => return Err(expected_required), - }; + if let Some(ident) = &named_field.ident { + let wrapped_name = Ident::new(&format!("{}_wrapper", ident), Span::call_site()); + Ok(PropAttr::Required { wrapped_name }) + } else { + unreachable!() + } + } + NestedMeta::Meta(Meta::NameValue(name_value)) => { + let MetaNameValue { path, lit, .. } = name_value; - if word_ident != "required" { - return Err(expected_required); - } + if !path.is_ident("default") { + return Err(expected_attr); + } - if let Some(ident) = &named_field.ident { - Ok(Some(Ident::new( - &format!("{}_wrapper", ident), - Span::call_site(), - ))) - } else { - unreachable!() + if let Lit::Str(lit_str) = lit { + let default = lit_str.parse()?; + Ok(PropAttr::Default { default }) + } else { + Err(expected_attr) + } + } + _ => Err(expected_attr), } } @@ -149,7 +200,7 @@ impl PropField { _ => None, })?; - if meta_list.ident == "props" { + if meta_list.path.is_ident("props") { Some(meta_list) } else { None @@ -162,7 +213,7 @@ impl TryFrom for PropField { fn try_from(field: Field) -> Result { Ok(PropField { - wrapped_name: Self::required_wrapper(&field)?, + attr: Self::attribute(&field)?, ty: field.ty, name: field.ident.unwrap(), }) @@ -171,13 +222,29 @@ impl TryFrom for PropField { impl PartialOrd for PropField { fn partial_cmp(&self, other: &PropField) -> Option { - self.name.partial_cmp(&other.name) + if self.name == other.name { + Some(Ordering::Equal) + } else if self.name == "children" { + Some(Ordering::Greater) + } else if other.name == "children" { + Some(Ordering::Less) + } else { + self.name.partial_cmp(&other.name) + } } } impl Ord for PropField { fn cmp(&self, other: &PropField) -> Ordering { - self.name.cmp(&other.name) + if self.name == other.name { + Ordering::Equal + } else if self.name == "children" { + Ordering::Greater + } else if other.name == "children" { + Ordering::Less + } else { + self.name.cmp(&other.name) + } } } diff --git a/crates/macro/src/derive_props/wrapper.rs b/crates/macro/src/derive_props/wrapper.rs index 82cb3aab903..d9b85cd3b12 100644 --- a/crates/macro/src/derive_props/wrapper.rs +++ b/crates/macro/src/derive_props/wrapper.rs @@ -24,7 +24,9 @@ impl ToTokens for PropsWrapper<'_> { let wrapper_default_setters = self.default_setters(); let wrapper = quote! { - struct #wrapper_name#generics { + struct #wrapper_name#generics + #where_clause + { #(#wrapper_field_defs)* } diff --git a/crates/macro/src/html_tree/html_component.rs b/crates/macro/src/html_tree/html_component.rs index 725b939502d..ff4f53b1cce 100644 --- a/crates/macro/src/html_tree/html_component.rs +++ b/crates/macro/src/html_tree/html_component.rs @@ -1,125 +1,187 @@ use super::HtmlProp; use super::HtmlPropSuffix; +use super::HtmlTreeNested; use crate::PeekValue; use boolinator::Boolinator; use proc_macro2::Span; use quote::{quote, quote_spanned, ToTokens}; +use std::cmp::Ordering; use syn::buffer::Cursor; use syn::parse; use syn::parse::{Parse, ParseStream, Result as ParseResult}; +use syn::punctuated::Punctuated; use syn::spanned::Spanned; -use syn::{Ident, Token, Type}; +use syn::{ + AngleBracketedGenericArguments, Expr, GenericArgument, Ident, Path, PathArguments, PathSegment, + Token, Type, TypePath, +}; -pub struct HtmlComponent(HtmlComponentInner); +pub struct HtmlComponent { + ty: Type, + props: Props, + children: Vec, +} impl PeekValue<()> for HtmlComponent { fn peek(cursor: Cursor) -> Option<()> { - let (punct, cursor) = cursor.punct()?; - (punct.as_char() == '<').as_option()?; - - HtmlComponent::peek_type(cursor) + HtmlComponentOpen::peek(cursor) + .or_else(|| HtmlComponentClose::peek(cursor)) + .map(|_| ()) } } impl Parse for HtmlComponent { fn parse(input: ParseStream) -> ParseResult { - let lt = input.parse::()?; - let HtmlPropSuffix { stream, div, gt } = input.parse()?; - if div.is_none() { - return Err(syn::Error::new_spanned( - HtmlComponentTag { lt, gt }, - "expected component tag be of form `< .. />`", - )); + if HtmlComponentClose::peek(input.cursor()).is_some() { + return match input.parse::() { + Ok(close) => Err(syn::Error::new_spanned( + close, + "this close tag has no corresponding open tag", + )), + Err(err) => Err(err), + }; } - match parse(stream) { - Ok(comp) => Ok(HtmlComponent(comp)), - Err(err) => { - if err.to_string().starts_with("unexpected end of input") { - Err(syn::Error::new_spanned(div, err.to_string())) - } else { - Err(err) + let open = input.parse::()?; + // Return early if it's a self-closing tag + if open.div.is_some() { + return Ok(HtmlComponent { + ty: open.ty, + props: open.props, + children: Vec::new(), + }); + } + + let mut children: Vec = vec![]; + loop { + if input.is_empty() { + return Err(syn::Error::new_spanned( + open, + "this open tag has no corresponding close tag", + )); + } + if let Some(ty) = HtmlComponentClose::peek(input.cursor()) { + if open.ty == ty { + break; } } + + children.push(input.parse()?); } + + input.parse::()?; + + Ok(HtmlComponent { + ty: open.ty, + props: open.props, + children, + }) } } impl ToTokens for HtmlComponent { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - let HtmlComponentInner { ty, props } = &self.0; - let vcomp_scope = Ident::new("__yew_vcomp_scope", Span::call_site()); + let Self { + ty, + props, + children, + } = self; - let validate_props = if let Some(Props::List(ListProps(vec_props))) = props { + let validate_props = if let Props::List(ListProps { props, .. }) = props { let prop_ref = Ident::new("__yew_prop_ref", Span::call_site()); - let check_props = vec_props.iter().map(|HtmlProp { label, .. }| { + let check_props = props.iter().map(|HtmlProp { label, .. }| { quote! { #prop_ref.#label; } }); + let check_children = if !children.is_empty() { + quote! { #prop_ref.children; } + } else { + quote! {} + }; + // This is a hack to avoid allocating memory but still have a reference to a props // struct so that attributes can be checked against it #[cfg(has_maybe_uninit)] let unallocated_prop_ref = quote! { - let #prop_ref: <#ty as ::yew::html::Component>::Properties = unsafe { ::std::mem::MaybeUninit::uninit().assume_init() }; + let #prop_ref: <#ty as ::yew::html::Component>::Properties = unsafe { + ::std::mem::MaybeUninit::uninit().assume_init() + }; }; #[cfg(not(has_maybe_uninit))] let unallocated_prop_ref = quote! { - let #prop_ref: <#ty as ::yew::html::Component>::Properties = unsafe { ::std::mem::uninitialized() }; + let #prop_ref: <#ty as ::yew::html::Component>::Properties = unsafe { + ::std::mem::uninitialized() + }; }; quote! { #unallocated_prop_ref + #check_children #(#check_props)* } } else { quote! {} }; - let init_props = if let Some(props) = props { - match props { - Props::List(ListProps(vec_props)) => { - let set_props = vec_props.iter().map(|HtmlProp { label, value }| { - quote_spanned! { value.span()=> - .#label(<::yew::virtual_dom::vcomp::VComp<_> as ::yew::virtual_dom::vcomp::Transformer<_, _, _>>::transform(#vcomp_scope.clone(), #value)) - } - }); - - quote! { - <<#ty as ::yew::html::Component>::Properties as ::yew::html::Properties>::builder() - #(#set_props)* - .build() - } - } - Props::With(WithProps(props)) => quote! { #props }, + let set_children = if !children.is_empty() { + quote! { + .children(::yew::html::ChildrenRenderer::new({ + let mut v = ::std::vec::Vec::new(); + #(v.extend(::yew::utils::NodeSeq::from(#children));)* + v + })) } } else { - quote! { - <<#ty as ::yew::html::Component>::Properties as ::yew::html::Properties>::builder().build() + quote! {} + }; + + let init_props = match props { + Props::List(ListProps { props, .. }) => { + let set_props = props.iter().map(|HtmlProp { label, value }| { + quote_spanned! { value.span()=> .#label( + <::yew::virtual_dom::vcomp::VComp as ::yew::virtual_dom::Transformer<_, _>>::transform( + #value + ) + )} + }); + + quote! { + <<#ty as ::yew::html::Component>::Properties as ::yew::html::Properties>::builder() + #(#set_props)* + #set_children + .build() + } } + Props::With(WithProps { props, .. }) => quote! { #props }, + Props::None => quote! { + <<#ty as ::yew::html::Component>::Properties as ::yew::html::Properties>::builder() + #set_children + .build() + }, }; let validate_comp = quote_spanned! { ty.span()=> - trait __yew_validate_comp { - type C: ::yew::html::Component; - } - impl __yew_validate_comp for () { - type C = #ty; - } + trait __yew_validate_comp: ::yew::html::Component {} + impl __yew_validate_comp for #ty {} + }; + + let node_ref = if let Some(node_ref) = props.node_ref() { + quote_spanned! { node_ref.span()=> #node_ref } + } else { + quote! { ::yew::html::NodeRef::default() } }; tokens.extend(quote! {{ - // Validation nevers executes at runtime + // These validation checks show a nice error message to the user. + // They do not execute at runtime if false { #validate_comp #validate_props } - let #vcomp_scope: ::yew::virtual_dom::vcomp::ScopeHolder<_> = ::std::default::Default::default(); - ::yew::virtual_dom::VNode::VComp( - ::yew::virtual_dom::VComp::new::<#ty>(#init_props, #vcomp_scope) - ) + ::yew::virtual_dom::VChild::<#ty>::new(#init_props, #node_ref) }}); } } @@ -135,13 +197,38 @@ impl HtmlComponent { Some(cursor) } - fn peek_type(mut cursor: Cursor) -> Option<()> { + fn path_arguments(cursor: Cursor) -> Option<(PathArguments, Cursor)> { + let (punct, cursor) = cursor.punct()?; + (punct.as_char() == '<').as_option()?; + + let (ty, cursor) = Self::peek_type(cursor)?; + + let (punct, cursor) = cursor.punct()?; + (punct.as_char() == '>').as_option()?; + + Some(( + PathArguments::AngleBracketed(AngleBracketedGenericArguments { + colon2_token: None, + lt_token: Token![<](Span::call_site()), + args: vec![GenericArgument::Type(ty)].into_iter().collect(), + gt_token: Token![>](Span::call_site()), + }), + cursor, + )) + } + + fn peek_type(mut cursor: Cursor) -> Option<(Type, Cursor)> { let mut colons_optional = true; let mut last_ident = None; + let mut leading_colon = None; + let mut segments = Punctuated::new(); loop { let mut post_colons_cursor = cursor; if let Some(c) = Self::double_colon(post_colons_cursor) { + if colons_optional { + leading_colon = Some(Token![::](Span::call_site())); + } post_colons_cursor = c; } else if !colons_optional { break; @@ -149,7 +236,15 @@ impl HtmlComponent { if let Some((ident, c)) = post_colons_cursor.ident() { cursor = c; - last_ident = Some(ident); + last_ident = Some(ident.clone()); + let arguments = if let Some((args, c)) = Self::path_arguments(cursor) { + cursor = c; + args + } else { + PathArguments::None + }; + + segments.push(PathSegment { ident, arguments }); } else { break; } @@ -160,43 +255,102 @@ impl HtmlComponent { let type_str = last_ident?.to_string(); type_str.is_ascii().as_option()?; - type_str.bytes().next()?.is_ascii_uppercase().as_option() + type_str.bytes().next()?.is_ascii_uppercase().as_option()?; + + Some(( + Type::Path(TypePath { + qself: None, + path: Path { + leading_colon, + segments, + }, + }), + cursor, + )) } } -pub struct HtmlComponentInner { +struct HtmlComponentOpen { + lt: Token![<], ty: Type, - props: Option, + props: Props, + div: Option, + gt: Token![>], } -impl Parse for HtmlComponentInner { +impl PeekValue for HtmlComponentOpen { + fn peek(cursor: Cursor) -> Option { + let (punct, cursor) = cursor.punct()?; + (punct.as_char() == '<').as_option()?; + let (typ, _) = HtmlComponent::peek_type(cursor)?; + return Some(typ); + } +} + +impl Parse for HtmlComponentOpen { fn parse(input: ParseStream) -> ParseResult { + let lt = input.parse::()?; let ty = input.parse()?; // backwards compat let _ = input.parse::(); + let HtmlPropSuffix { stream, div, gt } = input.parse()?; + let props = parse(stream)?; + + Ok(HtmlComponentOpen { + lt, + ty, + props, + div, + gt, + }) + } +} - let props = if let Some(prop_type) = Props::peek(input.cursor()) { - match prop_type { - PropType::List => input.parse().map(Props::List).map(Some)?, - PropType::With => input.parse().map(Props::With).map(Some)?, - } - } else { - None - }; - - Ok(HtmlComponentInner { ty, props }) +impl ToTokens for HtmlComponentOpen { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let HtmlComponentOpen { lt, gt, .. } = self; + tokens.extend(quote! {#lt#gt}); } } -struct HtmlComponentTag { +struct HtmlComponentClose { lt: Token![<], + div: Token![/], + ty: Type, gt: Token![>], } -impl ToTokens for HtmlComponentTag { +impl PeekValue for HtmlComponentClose { + fn peek(cursor: Cursor) -> Option { + let (punct, cursor) = cursor.punct()?; + (punct.as_char() == '<').as_option()?; + + let (punct, cursor) = cursor.punct()?; + (punct.as_char() == '/').as_option()?; + + let (typ, cursor) = HtmlComponent::peek_type(cursor)?; + + let (punct, _) = cursor.punct()?; + (punct.as_char() == '>').as_option()?; + + return Some(typ); + } +} +impl Parse for HtmlComponentClose { + fn parse(input: ParseStream) -> ParseResult { + Ok(HtmlComponentClose { + lt: input.parse()?, + div: input.parse()?, + ty: input.parse()?, + gt: input.parse()?, + }) + } +} + +impl ToTokens for HtmlComponentClose { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - let HtmlComponentTag { lt, gt } = self; - tokens.extend(quote! {#lt#gt}); + let HtmlComponentClose { lt, div, ty, gt } = self; + tokens.extend(quote! {#lt#div#ty#gt}); } } @@ -208,6 +362,17 @@ enum PropType { enum Props { List(ListProps), With(WithProps), + None, +} + +impl Props { + fn node_ref(&self) -> Option<&Expr> { + match self { + Props::List(ListProps { node_ref, .. }) => node_ref.as_ref(), + Props::With(WithProps { node_ref, .. }) => node_ref.as_ref(), + Props::None => None, + } + } } impl PeekValue for Props { @@ -223,7 +388,21 @@ impl PeekValue for Props { } } -struct ListProps(Vec); +impl Parse for Props { + fn parse(input: ParseStream) -> ParseResult { + match Props::peek(input.cursor()) { + Some(PropType::List) => input.parse().map(Props::List), + Some(PropType::With) => input.parse().map(Props::With), + None => Ok(Props::None), + } + } +} + +struct ListProps { + props: Vec, + node_ref: Option, +} + impl Parse for ListProps { fn parse(input: ParseStream) -> ParseResult { let mut props: Vec = Vec::new(); @@ -231,7 +410,12 @@ impl Parse for ListProps { props.push(input.parse::()?); } + let ref_position = props.iter().position(|p| p.label.to_string() == "ref"); + let node_ref = ref_position.and_then(|i| Some(props.remove(i).value)); for prop in &props { + if prop.label.to_string() == "ref" { + return Err(syn::Error::new_spanned(&prop.label, "too many refs set")); + } if prop.label.to_string() == "type" { return Err(syn::Error::new_spanned(&prop.label, "expected identifier")); } @@ -242,17 +426,29 @@ impl Parse for ListProps { // alphabetize props.sort_by(|a, b| { - a.label - .to_string() - .partial_cmp(&b.label.to_string()) - .unwrap() + if a.label == b.label { + Ordering::Equal + } else if a.label.to_string() == "children" { + Ordering::Greater + } else if b.label.to_string() == "children" { + Ordering::Less + } else { + a.label + .to_string() + .partial_cmp(&b.label.to_string()) + .unwrap() + } }); - Ok(ListProps(props)) + Ok(ListProps { props, node_ref }) } } -struct WithProps(Ident); +struct WithProps { + props: Ident, + node_ref: Option, +} + impl Parse for WithProps { fn parse(input: ParseStream) -> ParseResult { let with = input.parse::()?; @@ -261,6 +457,18 @@ impl Parse for WithProps { } let props = input.parse::()?; let _ = input.parse::(); - Ok(WithProps(props)) + + // Check for the ref tag after `with` + let mut node_ref = None; + if let Some(ident) = input.cursor().ident() { + let prop = input.parse::()?; + if ident.0 == "ref" { + node_ref = Some(prop.value); + } else { + return Err(syn::Error::new_spanned(&prop.label, "unexpected token")); + } + } + + Ok(WithProps { props, node_ref }) } } diff --git a/crates/macro/src/html_tree/html_iterable.rs b/crates/macro/src/html_tree/html_iterable.rs index 47fac43a8ad..ba80943d2b5 100644 --- a/crates/macro/src/html_tree/html_iterable.rs +++ b/crates/macro/src/html_tree/html_iterable.rs @@ -40,9 +40,8 @@ impl ToTokens for HtmlIterable { fn to_tokens(&self, tokens: &mut TokenStream) { let expr = &self.0; let new_tokens = quote_spanned! {expr.span()=> { - let mut __yew_vlist = ::yew::virtual_dom::VList::new(); - let __yew_nodes: &mut ::std::iter::Iterator = &mut(#expr); - for __yew_node in __yew_nodes.into_iter() { + let mut __yew_vlist = ::yew::virtual_dom::VList::default(); + for __yew_node in #expr { __yew_vlist.add_child(__yew_node.into()); } ::yew::virtual_dom::VNode::from(__yew_vlist) diff --git a/crates/macro/src/html_tree/html_list.rs b/crates/macro/src/html_tree/html_list.rs index 5da411c0333..a8cffcad49e 100644 --- a/crates/macro/src/html_tree/html_list.rs +++ b/crates/macro/src/html_tree/html_list.rs @@ -49,12 +49,14 @@ impl Parse for HtmlList { impl ToTokens for HtmlList { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - let html_trees = &self.0; + let children = &self.0; tokens.extend(quote! { ::yew::virtual_dom::VNode::VList( - ::yew::virtual_dom::vlist::VList { - childs: vec![#(#html_trees,)*], - } + ::yew::virtual_dom::vlist::VList::new_with_children({ + let mut v = ::std::vec::Vec::new(); + #(v.extend(::yew::utils::NodeSeq::from(#children));)* + v + }) ) }); } diff --git a/crates/macro/src/html_tree/html_node.rs b/crates/macro/src/html_tree/html_node.rs index 41fce73ea89..f469092034b 100644 --- a/crates/macro/src/html_tree/html_node.rs +++ b/crates/macro/src/html_tree/html_node.rs @@ -4,6 +4,7 @@ use quote::{quote, quote_spanned, ToTokens}; use syn::buffer::Cursor; use syn::parse::{Parse, ParseStream, Result}; use syn::spanned::Spanned; +use syn::Expr; use syn::Lit; pub struct HtmlNode(Node); @@ -18,7 +19,7 @@ impl Parse for HtmlNode { } Node::Literal(lit) } else { - Node::Raw(input.parse()?) + Node::Expression(input.parse()?) }; Ok(HtmlNode(node)) @@ -46,12 +47,8 @@ impl ToTokens for HtmlNode { impl ToTokens for Node { fn to_tokens(&self, tokens: &mut TokenStream) { let node_token = match &self { - Node::Literal(lit) => quote! { - ::yew::virtual_dom::VNode::from(#lit) - }, - Node::Raw(stream) => quote_spanned! {stream.span()=> - ::yew::virtual_dom::VNode::from({#stream}) - }, + Node::Literal(lit) => quote! {#lit}, + Node::Expression(expr) => quote_spanned! {expr.span()=> {#expr} }, }; tokens.extend(node_token); @@ -60,5 +57,5 @@ impl ToTokens for Node { enum Node { Literal(Lit), - Raw(TokenStream), + Expression(Expr), } diff --git a/crates/macro/src/html_tree/html_prop.rs b/crates/macro/src/html_tree/html_prop.rs index 07cb906678d..8a47abdd0f7 100644 --- a/crates/macro/src/html_tree/html_prop.rs +++ b/crates/macro/src/html_tree/html_prop.rs @@ -49,18 +49,12 @@ impl Parse for HtmlPropSuffix { if let TokenTree::Punct(punct) = &next { match punct.as_char() { '>' => { - let possible_tag_end = input.peek(Token![<]) - || input.peek(syn::token::Brace) - || input.is_empty(); - - if angle_count > 1 || possible_tag_end { - angle_count -= 1; - if angle_count == 0 { - gt = Some(syn::token::Gt { - spans: [punct.span()], - }); - break; - } + angle_count -= 1; + if angle_count == 0 { + gt = Some(syn::token::Gt { + spans: [punct.span()], + }); + break; } } '<' => angle_count += 1, @@ -73,14 +67,6 @@ impl Parse for HtmlPropSuffix { break; } } - '-' => { - if input.peek(Token![>]) { - // Handle explicit return types in callbacks (#560) - // We increase angle_count here in order to ignore - // the following >. - angle_count += 1; - } - } _ => {} }; } diff --git a/crates/macro/src/html_tree/html_tag/mod.rs b/crates/macro/src/html_tree/html_tag/mod.rs index f1e020813a3..a92ebe216cd 100644 --- a/crates/macro/src/html_tree/html_tag/mod.rs +++ b/crates/macro/src/html_tree/html_tag/mod.rs @@ -91,18 +91,28 @@ impl ToTokens for HtmlTag { let TagAttributes { classes, attributes, + booleans, kind, value, checked, - disabled, - selected, + node_ref, href, listeners, } = &attributes; let vtag = Ident::new("__yew_vtag", tag_name.span()); - let attr_labels = attributes.iter().map(|attr| attr.label.to_string()); - let attr_values = attributes.iter().map(|attr| &attr.value); + let attr_pairs = attributes.iter().map(|TagAttribute { label, value }| { + let label_str = label.to_string(); + quote_spanned! {value.span() => (#label_str.to_owned(), (#value).to_string()) } + }); + let set_booleans = booleans.iter().map(|TagAttribute { label, value }| { + let label_str = label.to_string(); + quote_spanned! {value.span() => + if #value { + #vtag.add_attribute(&#label_str, &#label_str); + } + } + }); let set_kind = kind.iter().map(|kind| { quote_spanned! {kind.span()=> #vtag.set_kind(&(#kind)); } }); @@ -118,20 +128,6 @@ impl ToTokens for HtmlTag { let set_checked = checked.iter().map(|checked| { quote_spanned! {checked.span()=> #vtag.set_checked(#checked); } }); - let add_disabled = disabled.iter().map(|disabled| { - quote_spanned! {disabled.span()=> - if #disabled { - #vtag.add_attribute("disabled", &"true"); - } - } - }); - let add_selected = selected.iter().map(|selected| { - quote_spanned! {selected.span()=> - if #selected { - #vtag.add_attribute("selected", &"selected"); - } - } - }); let set_classes = classes.iter().map(|classes_form| match classes_form { ClassesForm::Tuple(classes) => quote! { #vtag.add_classes(vec![#(&(#classes)),*]); @@ -140,6 +136,23 @@ impl ToTokens for HtmlTag { #vtag.set_classes(#classes); }, }); + let set_node_ref = node_ref.iter().map(|node_ref| { + quote! { + #vtag.node_ref = #node_ref; + } + }); + let listeners = listeners.iter().map(|listener| { + let name = &listener.label.name; + let callback = &listener.value; + + quote_spanned! {name.span()=> { + ::yew::html::#name::Wrapper::new( + <::yew::virtual_dom::vtag::VTag as ::yew::virtual_dom::Transformer<_, _>>::transform( + #callback + ) + ) + }} + }); tokens.extend(quote! {{ let mut #vtag = ::yew::virtual_dom::vtag::VTag::new(#name); @@ -147,13 +160,13 @@ impl ToTokens for HtmlTag { #(#set_value)* #(#add_href)* #(#set_checked)* - #(#add_disabled)* - #(#add_selected)* + #(#set_booleans)* #(#set_classes)* - #vtag.add_attributes(vec![#((#attr_labels.to_owned(), (#attr_values).to_string())),*]); - #vtag.add_listeners(vec![#(::std::boxed::Box::new(#listeners)),*]); + #(#set_node_ref)* + #vtag.add_attributes(vec![#(#attr_pairs),*]); + #vtag.add_listeners(vec![#(::std::rc::Rc::new(#listeners)),*]); #vtag.add_children(vec![#(#children),*]); - ::yew::virtual_dom::VNode::VTag(#vtag) + ::yew::virtual_dom::VNode::from(#vtag) }}); } } diff --git a/crates/macro/src/html_tree/html_tag/tag_attributes.rs b/crates/macro/src/html_tree/html_tag/tag_attributes.rs index efce0733899..61ae5581e17 100644 --- a/crates/macro/src/html_tree/html_tag/tag_attributes.rs +++ b/crates/macro/src/html_tree/html_tag/tag_attributes.rs @@ -1,21 +1,20 @@ use crate::html_tree::HtmlProp as TagAttribute; use crate::PeekValue; use lazy_static::lazy_static; -use proc_macro2::TokenStream; -use quote::{quote, quote_spanned}; -use std::collections::HashMap; +use std::collections::HashSet; +use std::iter::FromIterator; use syn::parse::{Parse, ParseStream, Result as ParseResult}; -use syn::{Expr, ExprClosure, ExprTuple, Ident}; +use syn::{Expr, ExprTuple}; pub struct TagAttributes { pub attributes: Vec, - pub listeners: Vec, + pub listeners: Vec, pub classes: Option, + pub booleans: Vec, pub value: Option, pub kind: Option, pub checked: Option, - pub disabled: Option, - pub selected: Option, + pub node_ref: Option, pub href: Option, } @@ -24,75 +23,107 @@ pub enum ClassesForm { Single(Expr), } -pub struct TagListener { - name: Ident, - handler: Expr, - event_name: String, +lazy_static! { + static ref BOOLEAN_SET: HashSet<&'static str> = { + HashSet::from_iter( + vec![ + "async", + "autofocus", + "controls", + "default", + "defer", + "disabled", + "hidden", + "ismap", + "loop", + "multiple", + "muted", + "novalidate", + "open", + "readonly", + "required", + "selected", + ] + .into_iter(), + ) + }; } lazy_static! { - static ref LISTENER_MAP: HashMap<&'static str, &'static str> = { - let mut m = HashMap::new(); - m.insert("onclick", "ClickEvent"); - m.insert("ondoubleclick", "DoubleClickEvent"); - m.insert("onkeypress", "KeyPressEvent"); - m.insert("onkeydown", "KeyDownEvent"); - m.insert("onkeyup", "KeyUpEvent"); - m.insert("onmousedown", "MouseDownEvent"); - m.insert("onmousemove", "MouseMoveEvent"); - m.insert("onmouseout", "MouseOutEvent"); - m.insert("onmouseenter", "MouseEnterEvent"); - m.insert("onmouseleave", "MouseLeaveEvent"); - m.insert("onmousewheel", "MouseWheelEvent"); - m.insert("onmouseover", "MouseOverEvent"); - m.insert("onmouseup", "MouseUpEvent"); - m.insert("touchcancel", "TouchCancel"); - m.insert("touchend", "TouchEnd"); - m.insert("touchenter", "TouchEnter"); - m.insert("touchmove", "TouchMove"); - m.insert("touchstart", "TouchStart"); - m.insert("ongotpointercapture", "GotPointerCaptureEvent"); - m.insert("onlostpointercapture", "LostPointerCaptureEvent"); - m.insert("onpointercancel", "PointerCancelEvent"); - m.insert("onpointerdown", "PointerDownEvent"); - m.insert("onpointerenter", "PointerEnterEvent"); - m.insert("onpointerleave", "PointerLeaveEvent"); - m.insert("onpointermove", "PointerMoveEvent"); - m.insert("onpointerout", "PointerOutEvent"); - m.insert("onpointerover", "PointerOverEvent"); - m.insert("onpointerup", "PointerUpEvent"); - m.insert("onscroll", "ScrollEvent"); - m.insert("onblur", "BlurEvent"); - m.insert("onfocus", "FocusEvent"); - m.insert("onsubmit", "SubmitEvent"); - m.insert("oninput", "InputData"); - m.insert("onchange", "ChangeData"); - m.insert("ondrag", "DragEvent"); - m.insert("ondragstart", "DragStartEvent"); - m.insert("ondragend", "DragEndEvent"); - m.insert("ondragenter", "DragEnterEvent"); - m.insert("ondragleave", "DragLeaveEvent"); - m.insert("ondragover", "DragOverEvent"); - m.insert("ondragexit", "DragExitEvent"); - m.insert("ondrop", "DragDropEvent"); - m.insert("oncontextmenu", "ContextMenuEvent"); - m + static ref LISTENER_SET: HashSet<&'static str> = { + HashSet::from_iter( + vec![ + "onclick", + "ondoubleclick", + "onkeypress", + "onkeydown", + "onkeyup", + "onmousedown", + "onmousemove", + "onmouseout", + "onmouseenter", + "onmouseleave", + "onmousewheel", + "onmouseover", + "onmouseup", + "ontouchcancel", + "ontouchend", + "ontouchenter", + "ontouchmove", + "ontouchstart", + "ongotpointercapture", + "onlostpointercapture", + "onpointercancel", + "onpointerdown", + "onpointerenter", + "onpointerleave", + "onpointermove", + "onpointerout", + "onpointerover", + "onpointerup", + "onscroll", + "onblur", + "onfocus", + "onsubmit", + "oninput", + "onchange", + "ondrag", + "ondragstart", + "ondragend", + "ondragenter", + "ondragleave", + "ondragover", + "ondragexit", + "ondrop", + "oncontextmenu", + ] + .into_iter(), + ) }; } impl TagAttributes { - fn drain_listeners(attrs: &mut Vec) -> Vec { + fn drain_listeners(attrs: &mut Vec) -> Vec { let mut i = 0; let mut drained = Vec::new(); while i < attrs.len() { let name_str = attrs[i].label.to_string(); - if let Some(event_type) = LISTENER_MAP.get(&name_str.as_str()) { - let TagAttribute { label, value } = attrs.remove(i); - drained.push(TagListener { - name: label.name, - handler: value, - event_name: event_type.to_owned().to_string(), - }); + if LISTENER_SET.contains(&name_str.as_str()) { + drained.push(attrs.remove(i)); + } else { + i += 1; + } + } + drained + } + + fn drain_boolean(attrs: &mut Vec) -> Vec { + let mut i = 0; + let mut drained = Vec::new(); + while i < attrs.len() { + let name_str = attrs[i].label.to_string(); + if BOOLEAN_SET.contains(&name_str.as_str()) { + drained.push(attrs.remove(i)); } else { i += 1; } @@ -118,60 +149,6 @@ impl TagAttributes { expr => ClassesForm::Single(expr), } } - - fn map_listener(listener: TagListener) -> ParseResult { - let TagListener { - name, - event_name, - handler, - } = listener; - - match handler { - Expr::Closure(closure) => { - let ExprClosure { - inputs, - body, - or1_token, - or2_token, - .. - } = closure; - - let or_span = quote! {#or1_token#or2_token}; - if inputs.len() != 1 { - return Err(syn::Error::new_spanned( - or_span, - "there must be one closure argument", - )); - } - - let var = match inputs.first().unwrap().into_value() { - syn::FnArg::Inferred(pat) => pat, - _ => return Err(syn::Error::new_spanned(or_span, "invalid closure argument")), - }; - let handler = - Ident::new(&format!("__yew_{}_handler", name.to_string()), name.span()); - let listener = - Ident::new(&format!("__yew_{}_listener", name.to_string()), name.span()); - let segment = syn::PathSegment { - ident: Ident::new(&event_name, name.span()), - arguments: syn::PathArguments::None, - }; - let var_type = quote! { ::yew::events::#segment }; - let wrapper_type = quote! { ::yew::html::#name::Wrapper }; - let listener_stream = quote_spanned! {name.span()=> { - let #handler = move | #var: #var_type | #body; - let #listener = #wrapper_type::from(#handler); - #listener - }}; - - Ok(listener_stream) - } - _ => Err(syn::Error::new_spanned( - &name, - format!("`{}` attribute value should be a closure", name), - )), - } - } } impl Parse for TagAttributes { @@ -183,7 +160,7 @@ impl Parse for TagAttributes { let mut listeners = Vec::new(); for listener in TagAttributes::drain_listeners(&mut attributes) { - listeners.push(TagAttributes::map_listener(listener)?); + listeners.push(listener); } // Multiple listener attributes are allowed, but no others @@ -204,25 +181,25 @@ impl Parse for TagAttributes { } i += 1; } + let booleans = TagAttributes::drain_boolean(&mut attributes); let classes = TagAttributes::remove_attr(&mut attributes, "class").map(TagAttributes::map_classes); let value = TagAttributes::remove_attr(&mut attributes, "value"); let kind = TagAttributes::remove_attr(&mut attributes, "type"); let checked = TagAttributes::remove_attr(&mut attributes, "checked"); - let disabled = TagAttributes::remove_attr(&mut attributes, "disabled"); - let selected = TagAttributes::remove_attr(&mut attributes, "selected"); + let node_ref = TagAttributes::remove_attr(&mut attributes, "ref"); let href = TagAttributes::remove_attr(&mut attributes, "href"); Ok(TagAttributes { attributes, classes, listeners, + checked, + booleans, value, kind, - checked, - disabled, - selected, + node_ref, href, }) } diff --git a/crates/macro/src/html_tree/mod.rs b/crates/macro/src/html_tree/mod.rs index ebf5e0ed2f4..20102dd53a1 100644 --- a/crates/macro/src/html_tree/mod.rs +++ b/crates/macro/src/html_tree/mod.rs @@ -18,7 +18,7 @@ use html_prop::HtmlProp; use html_prop::HtmlPropSuffix; use html_tag::HtmlTag; use proc_macro2::TokenStream; -use quote::ToTokens; +use quote::{quote, ToTokens}; use syn::buffer::Cursor; use syn::parse::{Parse, ParseStream, Result}; @@ -105,17 +105,49 @@ impl PeekValue for HtmlTree { impl ToTokens for HtmlTree { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - let empty_html_el = HtmlList(Vec::new()); - let html_tree_el: &dyn ToTokens = match self { - HtmlTree::Empty => &empty_html_el, - HtmlTree::Component(comp) => comp, - HtmlTree::Tag(tag) => tag, - HtmlTree::List(list) => list, - HtmlTree::Node(node) => node, - HtmlTree::Iterable(iterable) => iterable, - HtmlTree::Block(block) => block, - }; + let node = self.token_stream(); + tokens.extend(quote! { + ::yew::virtual_dom::VNode::from(#node) + }); + } +} + +impl HtmlTree { + fn token_stream(&self) -> proc_macro2::TokenStream { + match self { + HtmlTree::Empty => HtmlList(Vec::new()).into_token_stream(), + HtmlTree::Component(comp) => comp.into_token_stream(), + HtmlTree::Tag(tag) => tag.into_token_stream(), + HtmlTree::List(list) => list.into_token_stream(), + HtmlTree::Node(node) => node.into_token_stream(), + HtmlTree::Iterable(iterable) => iterable.into_token_stream(), + HtmlTree::Block(block) => block.into_token_stream(), + } + } +} + +pub struct HtmlRootNested(HtmlTreeNested); +impl Parse for HtmlRootNested { + fn parse(input: ParseStream) -> Result { + Ok(HtmlRootNested(HtmlTreeNested::parse(input)?)) + } +} + +impl ToTokens for HtmlRootNested { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + self.0.to_tokens(tokens); + } +} - html_tree_el.to_tokens(tokens); +pub struct HtmlTreeNested(HtmlTree); +impl Parse for HtmlTreeNested { + fn parse(input: ParseStream) -> Result { + Ok(HtmlTreeNested(HtmlTree::parse(input)?)) + } +} + +impl ToTokens for HtmlTreeNested { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + tokens.extend(self.0.token_stream()); } } diff --git a/crates/macro/src/lib.rs b/crates/macro/src/lib.rs index 012f53c5763..19aaaf61032 100644 --- a/crates/macro/src/lib.rs +++ b/crates/macro/src/lib.rs @@ -9,8 +9,11 @@ //! # #[macro_use] extern crate yew; //! use yew::prelude::*; //! -//! # struct Component; -//! #[derive(Properties)] +//! struct Component { +//! link: ComponentLink, +//! } +//! +//! #[derive(Clone, Properties)] //! struct Props { //! #[props(required)] //! prop: String, @@ -28,16 +31,16 @@ //! # fn update(&mut self, msg: Self::Message) -> ShouldRender { //! # unimplemented!() //! # } -//! # } //! # -//! # impl Renderable for Component { -//! # fn view(&self) -> Html { +//! # fn view(&self) -> Html { //! # //! // ... //! //! html! { //!
-//! +//! //! <> //! //! @@ -51,7 +54,7 @@ //! # fn main() {} //! ``` //! -//! Please refer to [https://github.com/DenisKolodin/yew](https://github.com/DenisKolodin/yew) for how to set this up. +//! Please refer to [https://github.com/yewstack/yew](https://github.com/yewstack/yew) for how to set this up. #![recursion_limit = "128"] extern crate proc_macro; @@ -60,7 +63,7 @@ mod derive_props; mod html_tree; use derive_props::DerivePropsInput; -use html_tree::HtmlRoot; +use html_tree::{HtmlRoot, HtmlRootNested}; use proc_macro::TokenStream; use proc_macro_hack::proc_macro_hack; use quote::{quote, ToTokens}; @@ -91,6 +94,12 @@ pub fn derive_props(input: TokenStream) -> TokenStream { TokenStream::from(input.into_token_stream()) } +#[proc_macro_hack] +pub fn html_nested(input: TokenStream) -> TokenStream { + let root = parse_macro_input!(input as HtmlRootNested); + TokenStream::from(quote! {#root}) +} + #[proc_macro_hack] pub fn html(input: TokenStream) -> TokenStream { let root = parse_macro_input!(input as HtmlRoot); diff --git a/examples/Cargo.toml b/examples/Cargo.toml new file mode 100644 index 00000000000..2492b0e90b8 --- /dev/null +++ b/examples/Cargo.toml @@ -0,0 +1,26 @@ +[workspace] +members = [ + "counter", + "crm", + "custom_components", + "dashboard", + "node_refs", + "file_upload", + "fragments", + "futures", + "game_of_life", + "inner_html", + "js_callback", + "large_table", + "minimal", + "mount_point", + "multi_thread", + "nested_list", + "npm_and_rest", + "server", + "showcase", + "textarea", + "timer", + "todomvc", + "two_apps", +] diff --git a/examples/counter/Cargo.toml b/examples/counter/Cargo.toml index 1ad06a5cb88..22272744d51 100644 --- a/examples/counter/Cargo.toml +++ b/examples/counter/Cargo.toml @@ -5,5 +5,5 @@ authors = ["Denis Kolodin "] edition = "2018" [dependencies] -stdweb = "0.4.2" +stdweb = "0.4.20" yew = { path = "../.." } diff --git a/examples/counter/src/lib.rs b/examples/counter/src/lib.rs index 4710546cdaf..fa9d33de841 100644 --- a/examples/counter/src/lib.rs +++ b/examples/counter/src/lib.rs @@ -2,9 +2,10 @@ use stdweb::web::Date; use yew::services::ConsoleService; -use yew::{html, Component, ComponentLink, Html, Renderable, ShouldRender}; +use yew::{html, Component, ComponentLink, Html, ShouldRender}; pub struct Model { + link: ComponentLink, console: ConsoleService, value: i64, } @@ -19,8 +20,9 @@ impl Component for Model { type Message = Msg; type Properties = (); - fn create(_: Self::Properties, _: ComponentLink) -> Self { + fn create(_: Self::Properties, link: ComponentLink) -> Self { Model { + link, console: ConsoleService::new(), value: 0, } @@ -45,16 +47,20 @@ impl Component for Model { } true } -} -impl Renderable for Model { - fn view(&self) -> Html { + fn view(&self) -> Html { html! {

{ self.value }

{ Date::new().to_string() }

diff --git a/examples/crm/README.md b/examples/crm/README.md index 41cc08a1486..4be7a113408 100644 --- a/examples/crm/README.md +++ b/examples/crm/README.md @@ -4,3 +4,4 @@ The main goals of this demo example to show you how to: * Add multiple screens with Yew (scenes) * Use storage service +* Generate VNodes without the html! macro diff --git a/examples/crm/src/lib.rs b/examples/crm/src/lib.rs index 4010fd66f70..227feef5bbe 100644 --- a/examples/crm/src/lib.rs +++ b/examples/crm/src/lib.rs @@ -8,7 +8,7 @@ mod markdown; use yew::format::Json; use yew::services::storage::Area; use yew::services::{DialogService, StorageService}; -use yew::{html, Component, ComponentLink, Html, Renderable, ShouldRender}; +use yew::{html, Component, ComponentLink, Html, InputData, Renderable, ShouldRender}; const KEY: &'static str = "yew.crm.database"; @@ -42,6 +42,7 @@ pub enum Scene { } pub struct Model { + link: ComponentLink, storage: StorageService, dialog: DialogService, database: Database, @@ -62,13 +63,14 @@ impl Component for Model { type Message = Msg; type Properties = (); - fn create(_: Self::Properties, _: ComponentLink) -> Self { + fn create(_: Self::Properties, link: ComponentLink) -> Self { let storage = StorageService::new(Area::Local); let Json(database) = storage.restore(KEY); let database = database.unwrap_or_else(|_| Database { clients: Vec::new(), }); Model { + link, storage, dialog: DialogService::new(), database, @@ -143,44 +145,42 @@ impl Component for Model { } true } -} -impl Renderable for Model { - fn view(&self) -> Html { + fn view(&self) -> Html { match self.scene { Scene::ClientsList => html! {
- { for self.database.clients.iter().map(Renderable::view) } + { for self.database.clients.iter().map(Renderable::render) }
- - + +
}, Scene::NewClientForm(ref client) => html! {
- { client.view_first_name_input() } - { client.view_last_name_input() } - { client.view_description_textarea() } + { client.view_first_name_input(&self.link) } + { client.view_last_name_input(&self.link) } + { client.view_description_textarea(&self.link) }
- + onclick=self.link.callback(|_| Msg::AddNew)>{ "Add New" } +
}, Scene::Settings => html! {
- - + +
}, } } } -impl Renderable for Client { - fn view(&self) -> Html { +impl Renderable for Client { + fn render(&self) -> Html { html! {

{ format!("First Name: {}", self.first_name) }

@@ -193,29 +193,29 @@ impl Renderable for Client { } impl Client { - fn view_first_name_input(&self) -> Html { + fn view_first_name_input(&self, link: &ComponentLink) -> Html { html! { + oninput=link.callback(|e: InputData| Msg::UpdateFirstName(e.value)) /> } } - fn view_last_name_input(&self) -> Html { + fn view_last_name_input(&self, link: &ComponentLink) -> Html { html! { + oninput=link.callback(|e: InputData| Msg::UpdateLastName(e.value)) /> } } - fn view_description_textarea(&self) -> Html { + fn view_description_textarea(&self, link: &ComponentLink) -> Html { html! { - -

diff --git a/examples/large_table/src/lib.rs b/examples/large_table/src/lib.rs index 7c4766413a5..828c8acebae 100644 --- a/examples/large_table/src/lib.rs +++ b/examples/large_table/src/lib.rs @@ -1,9 +1,10 @@ //! This demo originally created by https://github.com/qthree //! Source: https://github.com/qthree/yew_table100x100_test -use yew::{html, Component, ComponentLink, Html, Renderable, ShouldRender}; +use yew::{html, Component, ComponentLink, Html, ShouldRender}; pub struct Model { + link: ComponentLink, selected: Option<(u32, u32)>, } @@ -15,8 +16,11 @@ impl Component for Model { type Message = Msg; type Properties = (); - fn create(_: (), _: ComponentLink) -> Self { - Model { selected: None } + fn create(_: (), link: ComponentLink) -> Self { + Model { + link, + selected: None, + } } // Some details omitted. Explore the examples to get more. @@ -28,41 +32,39 @@ impl Component for Model { } true } -} -fn square_class(this: (u32, u32), selected: Option<(u32, u32)>) -> &'static str { - match selected { - Some(xy) if xy == this => "square_green", - _ => "square_red", - } -} - -fn view_square(selected: Option<(u32, u32)>, row: u32, column: u32) -> Html { - html! { - - + fn view(&self) -> Html { + html! { + + { (0..99).map(|row| self.view_row(row)).collect::() } +
+ } } } -fn view_row(selected: Option<(u32, u32)>, row: u32) -> Html { - html! { - - {for (0..99).map(|column| { - view_square(selected, row, column) - })} - +impl Model { + fn view_square(&self, row: u32, column: u32) -> Html { + html! { + + + } } -} -impl Renderable for Model { - fn view(&self) -> Html { + fn view_row(&self, row: u32) -> Html { html! { - - {for (0..99).map(|row| { - view_row(self.selected, row) + + {for (0..99).map(|column| { + self.view_square(row, column) })} -
+ } } } + +fn square_class(this: (u32, u32), selected: Option<(u32, u32)>) -> &'static str { + match selected { + Some(xy) if xy == this => "square_green", + _ => "square_red", + } +} diff --git a/examples/large_table/static/index.html b/examples/large_table/static/index.html index 648c78a8c18..f8b3189590a 100644 --- a/examples/large_table/static/index.html +++ b/examples/large_table/static/index.html @@ -6,6 +6,6 @@ - + diff --git a/examples/minimal/src/lib.rs b/examples/minimal/src/lib.rs index 00420682847..535fa75cf52 100644 --- a/examples/minimal/src/lib.rs +++ b/examples/minimal/src/lib.rs @@ -1,6 +1,8 @@ -use yew::{html, Component, ComponentLink, Html, Renderable, ShouldRender}; +use yew::{html, Component, ComponentLink, Html, ShouldRender}; -pub struct Model {} +pub struct Model { + link: ComponentLink, +} pub enum Msg { Click, @@ -10,8 +12,8 @@ impl Component for Model { type Message = Msg; type Properties = (); - fn create(_: Self::Properties, _: ComponentLink) -> Self { - Model {} + fn create(_: Self::Properties, link: ComponentLink) -> Self { + Model { link } } fn update(&mut self, msg: Self::Message) -> ShouldRender { @@ -20,13 +22,11 @@ impl Component for Model { } true } -} -impl Renderable for Model { - fn view(&self) -> Html { + fn view(&self) -> Html { html! {

- +
} } diff --git a/examples/mount_point/Cargo.toml b/examples/mount_point/Cargo.toml index 7fdf92c19db..675b6281a5b 100644 --- a/examples/mount_point/Cargo.toml +++ b/examples/mount_point/Cargo.toml @@ -5,5 +5,5 @@ authors = ["Ben Berman "] edition = "2018" [dependencies] -stdweb = "0.4" +stdweb = "0.4.20" yew = { path = "../.." } diff --git a/examples/mount_point/src/lib.rs b/examples/mount_point/src/lib.rs index c8a6d6e6a4f..29c728b1ba3 100644 --- a/examples/mount_point/src/lib.rs +++ b/examples/mount_point/src/lib.rs @@ -1,6 +1,7 @@ -use yew::{html, Component, ComponentLink, Html, Renderable, ShouldRender}; +use yew::{html, Component, ComponentLink, Html, InputData, ShouldRender}; pub struct Model { + link: ComponentLink, name: String, } @@ -12,8 +13,9 @@ impl Component for Model { type Message = Msg; type Properties = (); - fn create(_: Self::Properties, _: ComponentLink) -> Self { + fn create(_: Self::Properties, link: ComponentLink) -> Self { Model { + link, name: "Reversed".to_owned(), } } @@ -26,13 +28,13 @@ impl Component for Model { } true } -} -impl Renderable for Model { - fn view(&self) -> Html { + fn view(&self) -> Html { html! {
- +

{ self.name.chars().rev().collect::() }

} diff --git a/examples/multi_thread/Cargo.toml b/examples/multi_thread/Cargo.toml index 50c8ba4d187..1841175d9b6 100644 --- a/examples/multi_thread/Cargo.toml +++ b/examples/multi_thread/Cargo.toml @@ -4,6 +4,14 @@ version = "0.1.0" authors = ["Denis Kolodin "] edition = "2018" +[[bin]] +name = "main" +path = "src/bin/main.rs" + +[[bin]] +name = "native_worker" +path = "src/bin/native_worker.rs" + [dependencies] log = "0.4" web_logger = "0.1" diff --git a/examples/multi_thread/README.md b/examples/multi_thread/README.md index f64c7475e6b..32da696e6c6 100644 --- a/examples/multi_thread/README.md +++ b/examples/multi_thread/README.md @@ -3,5 +3,5 @@ You should compile a worker which have to be spawned in a separate thread: ```sh -cargo web build --bin native_worker --target wasm32-unknown-unknown +cargo web build --bin native_worker --release ``` diff --git a/examples/multi_thread/Web.toml b/examples/multi_thread/Web.toml new file mode 100644 index 00000000000..813e27393a9 --- /dev/null +++ b/examples/multi_thread/Web.toml @@ -0,0 +1 @@ +default-target = "wasm32-unknown-unknown" diff --git a/examples/multi_thread/src/context.rs b/examples/multi_thread/src/context.rs index 532333f6ad9..c93a9461cb8 100644 --- a/examples/multi_thread/src/context.rs +++ b/examples/multi_thread/src/context.rs @@ -12,15 +12,11 @@ pub enum Request { GetDataFromServer, } -impl Transferable for Request {} - #[derive(Serialize, Deserialize, Debug)] pub enum Response { DataFetched, } -impl Transferable for Response {} - pub enum Msg { Updating, } @@ -28,7 +24,7 @@ pub enum Msg { pub struct Worker { link: AgentLink, interval: IntervalService, - task: Box, + task: Box, fetch: FetchService, } @@ -41,7 +37,7 @@ impl Agent for Worker { fn create(link: AgentLink) -> Self { let mut interval = IntervalService::new(); let duration = Duration::from_secs(3); - let callback = link.send_back(|_| Msg::Updating); + let callback = link.callback(|_| Msg::Updating); let task = interval.spawn(duration, callback); Worker { link, @@ -59,11 +55,11 @@ impl Agent for Worker { } } - fn handle(&mut self, msg: Self::Input, who: HandlerId) { + fn handle_input(&mut self, msg: Self::Input, who: HandlerId) { info!("Request: {:?}", msg); match msg { Request::GetDataFromServer => { - self.link.response(who, Response::DataFetched); + self.link.respond(who, Response::DataFetched); } } } diff --git a/examples/multi_thread/src/job.rs b/examples/multi_thread/src/job.rs index 97abefba7e8..a9d21aa4144 100644 --- a/examples/multi_thread/src/job.rs +++ b/examples/multi_thread/src/job.rs @@ -12,15 +12,11 @@ pub enum Request { GetDataFromServer, } -impl Transferable for Request {} - #[derive(Serialize, Deserialize, Debug)] pub enum Response { DataFetched, } -impl Transferable for Response {} - pub enum Msg { Updating, } @@ -28,7 +24,7 @@ pub enum Msg { pub struct Worker { link: AgentLink, interval: IntervalService, - task: Box, + task: Box, fetch: FetchService, } @@ -41,7 +37,7 @@ impl Agent for Worker { fn create(link: AgentLink) -> Self { let mut interval = IntervalService::new(); let duration = Duration::from_secs(3); - let callback = link.send_back(|_| Msg::Updating); + let callback = link.callback(|_| Msg::Updating); let task = interval.spawn(duration, callback); Worker { link, @@ -59,11 +55,11 @@ impl Agent for Worker { } } - fn handle(&mut self, msg: Self::Input, who: HandlerId) { + fn handle_input(&mut self, msg: Self::Input, who: HandlerId) { info!("Request: {:?}", msg); match msg { Request::GetDataFromServer => { - self.link.response(who, Response::DataFetched); + self.link.respond(who, Response::DataFetched); } } } diff --git a/examples/multi_thread/src/lib.rs b/examples/multi_thread/src/lib.rs index 1652d9c9d8e..e86645d0332 100644 --- a/examples/multi_thread/src/lib.rs +++ b/examples/multi_thread/src/lib.rs @@ -6,13 +6,14 @@ pub mod native_worker; use log::info; use yew::worker::*; -use yew::{html, Component, ComponentLink, Html, Renderable, ShouldRender}; +use yew::{html, Component, ComponentLink, Html, ShouldRender}; pub struct Model { - worker: Box>, - job: Box>, - context: Box>, - context_2: Box>, + link: ComponentLink, + worker: Box>, + job: Box>, + context: Box>, + context_2: Box>, } pub enum Msg { @@ -26,20 +27,21 @@ impl Component for Model { type Message = Msg; type Properties = (); - fn create(_: Self::Properties, mut link: ComponentLink) -> Self { - let callback = link.send_back(|_| Msg::DataReceived); + fn create(_: Self::Properties, link: ComponentLink) -> Self { + let callback = link.callback(|_| Msg::DataReceived); let worker = native_worker::Worker::bridge(callback); - let callback = link.send_back(|_| Msg::DataReceived); + let callback = link.callback(|_| Msg::DataReceived); let job = job::Worker::bridge(callback); - let callback = link.send_back(|_| Msg::DataReceived); + let callback = link.callback(|_| Msg::DataReceived); let context = context::Worker::bridge(callback); - let callback = link.send_back(|_| Msg::DataReceived); + let callback = link.callback(|_| Msg::DataReceived); let context_2 = context::Worker::bridge(callback); Model { + link, worker, job, context, @@ -65,16 +67,14 @@ impl Component for Model { } true } -} -impl Renderable for Model { - fn view(&self) -> Html { + fn view(&self) -> Html { html! {
} diff --git a/examples/multi_thread/src/native_worker.rs b/examples/multi_thread/src/native_worker.rs index c9e2f1b7661..723d54cec46 100644 --- a/examples/multi_thread/src/native_worker.rs +++ b/examples/multi_thread/src/native_worker.rs @@ -12,15 +12,11 @@ pub enum Request { GetDataFromServer, } -impl Transferable for Request {} - #[derive(Serialize, Deserialize, Debug)] pub enum Response { DataFetched, } -impl Transferable for Response {} - pub enum Msg { Updating, } @@ -28,7 +24,7 @@ pub enum Msg { pub struct Worker { link: AgentLink, interval: IntervalService, - task: Box, + task: Box, fetch: FetchService, } @@ -41,7 +37,7 @@ impl Agent for Worker { fn create(link: AgentLink) -> Self { let mut interval = IntervalService::new(); let duration = Duration::from_secs(3); - let callback = link.send_back(|_| Msg::Updating); + let callback = link.callback(|_| Msg::Updating); let task = interval.spawn(duration, callback); Worker { link, @@ -59,11 +55,11 @@ impl Agent for Worker { } } - fn handle(&mut self, msg: Self::Input, who: HandlerId) { + fn handle_input(&mut self, msg: Self::Input, who: HandlerId) { info!("Request: {:?}", msg); match msg { Request::GetDataFromServer => { - self.link.response(who, Response::DataFetched); + self.link.respond(who, Response::DataFetched); } } } diff --git a/examples/multi_thread/static/bin b/examples/multi_thread/static/bin index 3750e73937b..4a2105e009b 120000 --- a/examples/multi_thread/static/bin +++ b/examples/multi_thread/static/bin @@ -1 +1 @@ -../target/wasm32-unknown-unknown/release/ \ No newline at end of file +../../../target/wasm32-unknown-unknown/release \ No newline at end of file diff --git a/examples/nested_list/Cargo.toml b/examples/nested_list/Cargo.toml new file mode 100644 index 00000000000..744148fd54b --- /dev/null +++ b/examples/nested_list/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "nested_list" +version = "0.1.0" +authors = ["Justin Starry "] +edition = "2018" + +[dependencies] +log = "0.4" +web_logger = "0.2" +yew = { path = "../.." } diff --git a/examples/nested_list/src/app.rs b/examples/nested_list/src/app.rs new file mode 100644 index 00000000000..8e1d022a39c --- /dev/null +++ b/examples/nested_list/src/app.rs @@ -0,0 +1,77 @@ +use super::header::ListHeader; +use super::item::ListItem; +use super::list::List; +use super::{Hovered, WeakComponentLink}; +use yew::prelude::*; + +pub struct App { + link: ComponentLink, + hovered: Hovered, +} + +pub enum Msg { + Hover(Hovered), +} + +impl Component for App { + type Message = Msg; + type Properties = (); + + fn create(_: Self::Properties, link: ComponentLink) -> Self { + App { + link, + hovered: Hovered::None, + } + } + + fn update(&mut self, msg: Self::Message) -> ShouldRender { + match msg { + Msg::Hover(hovered) => self.hovered = hovered, + } + true + } + + fn view(&self) -> Html { + let on_hover = &self.link.callback(Msg::Hover); + let onmouseenter = &self.link.callback(|_| Msg::Hover(Hovered::None)); + let list_link = &WeakComponentLink::::default(); + let sub_list_link = &WeakComponentLink::::default(); + html! { +
+

{ "Nested List Demo" }

+ + + + + +
{"Sublist!"}
+ { + html! { + + + + + + + } + } +
+
+ {self.view_last_hovered()} +
+ } + } +} + +impl App { + fn view_last_hovered(&self) -> Html { + html! { +
+ { "Last hovered:"} + + { &self.hovered } + +
+ } + } +} diff --git a/examples/nested_list/src/header.rs b/examples/nested_list/src/header.rs new file mode 100644 index 00000000000..995d235f22c --- /dev/null +++ b/examples/nested_list/src/header.rs @@ -0,0 +1,41 @@ +use super::list::{List, Msg as ListMsg}; +use super::{Hovered, WeakComponentLink}; +use yew::prelude::*; + +pub struct ListHeader { + props: Props, +} + +#[derive(Clone, Properties)] +pub struct Props { + #[props(required)] + pub on_hover: Callback, + #[props(required)] + pub text: String, + #[props(required)] + pub list_link: WeakComponentLink, +} + +impl Component for ListHeader { + type Message = (); + type Properties = Props; + + fn create(props: Self::Properties, _: ComponentLink) -> Self { + ListHeader { props } + } + + fn update(&mut self, _: Self::Message) -> ShouldRender { + false + } + + fn view(&self) -> Html { + let list_link = self.props.list_link.borrow().clone().unwrap(); + let onclick = list_link.callback(|_| ListMsg::HeaderClick); + let onmouseover = self.props.on_hover.reform(|_| Hovered::Header); + html! { +
+ { &self.props.text } +
+ } + } +} diff --git a/examples/nested_list/src/item.rs b/examples/nested_list/src/item.rs new file mode 100644 index 00000000000..e8299474671 --- /dev/null +++ b/examples/nested_list/src/item.rs @@ -0,0 +1,58 @@ +use crate::Hovered; +use yew::html::Children; +use yew::prelude::*; + +pub struct ListItem { + props: Props, +} + +#[derive(Clone, Properties)] +pub struct Props { + pub hide: bool, + #[props(required)] + pub on_hover: Callback, + #[props(required)] + pub name: String, + pub children: Children, +} + +impl Component for ListItem { + type Message = (); + type Properties = Props; + + fn create(props: Self::Properties, _: ComponentLink) -> Self { + ListItem { props } + } + + fn update(&mut self, _msg: Self::Message) -> ShouldRender { + false + } + + fn view(&self) -> Html { + let name = self.props.name.clone(); + let onmouseover = self + .props + .on_hover + .reform(move |_| Hovered::Item(name.clone())); + html! { +
+ { &self.props.name } + { self.view_details() } +
+ } + } +} + +impl ListItem { + fn view_details(&self) -> Html { + if self.props.children.is_empty() { + return html! {}; + } + + html! { +
+ { self.props.children.render() } +
+ } + } +} diff --git a/examples/nested_list/src/lib.rs b/examples/nested_list/src/lib.rs new file mode 100644 index 00000000000..71c48682393 --- /dev/null +++ b/examples/nested_list/src/lib.rs @@ -0,0 +1,36 @@ +#![recursion_limit = "512"] + +mod app; +mod header; +mod item; +mod list; + +pub use app::App; +use std::cell::RefCell; +use std::fmt; +use std::rc::Rc; +use yew::html::ComponentLink; +pub type WeakComponentLink = Rc>>>; + +#[derive(Debug)] +pub enum Hovered { + Header, + Item(String), + List, + None, +} + +impl fmt::Display for Hovered { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + Hovered::Header => "Header", + Hovered::Item(name) => name, + Hovered::List => "List container", + Hovered::None => "Nothing", + } + ) + } +} diff --git a/examples/nested_list/src/list.rs b/examples/nested_list/src/list.rs new file mode 100644 index 00000000000..1b0faa3ef3a --- /dev/null +++ b/examples/nested_list/src/list.rs @@ -0,0 +1,132 @@ +use super::{Hovered, WeakComponentLink}; +use crate::{header::ListHeader, header::Props as HeaderProps}; +use crate::{item::ListItem, item::Props as ItemProps}; +use yew::html::{ChildrenRenderer, NodeRef}; +use yew::prelude::*; +use yew::virtual_dom::{VChild, VComp, VNode}; + +#[derive(Clone)] +pub enum Variants { + Item(::Properties), + Header(::Properties), +} + +impl From for Variants { + fn from(props: ItemProps) -> Self { + Variants::Item(props) + } +} + +impl From for Variants { + fn from(props: HeaderProps) -> Self { + Variants::Header(props) + } +} + +#[derive(Clone)] +pub struct ListVariant { + props: Variants, +} + +#[derive(Clone, Properties)] +pub struct Props { + #[props(required)] + pub children: ChildrenRenderer, + #[props(required)] + pub on_hover: Callback, + #[props(required)] + pub weak_link: WeakComponentLink, +} + +pub struct List { + props: Props, + inactive: bool, +} + +pub enum Msg { + HeaderClick, +} + +impl Component for List { + type Message = Msg; + type Properties = Props; + + fn create(props: Self::Properties, link: ComponentLink) -> Self { + *props.weak_link.borrow_mut() = Some(link); + List { + props, + inactive: false, + } + } + + fn update(&mut self, msg: Self::Message) -> ShouldRender { + match msg { + Msg::HeaderClick => { + self.inactive = !self.inactive; + true + } + } + } + + fn view(&self) -> Html { + let inactive = if self.inactive { "inactive" } else { "" }; + let onmouseover = self.props.on_hover.reform(|_| Hovered::List); + let onmouseout = self.props.on_hover.reform(|_| Hovered::None); + html! { +
+
+ {self.view_header()} +
+ {self.view_items()} +
+
+
+ } + } +} + +impl List { + fn view_header(&self) -> Html { + html! {{ + for self.props.children.iter().filter(|c| match c.props { + Variants::Header(_) => true, + _ => false + }) + }} + } + + fn view_items(&self) -> Html { + html! {{ + for self.props.children.iter().filter(|c| match &c.props { + Variants::Item(props) => !props.hide, + _ => false, + }).enumerate().map(|(i, mut c)| { + if let Variants::Item(ref mut props) = c.props { + props.name = format!("#{} - {}", i + 1, props.name); + } + c + }) + }} + } +} + +impl From> for ListVariant +where + CHILD: Component, + CHILD::Properties: Into, +{ + fn from(vchild: VChild) -> Self { + ListVariant { + props: vchild.props.into(), + } + } +} + +impl Into for ListVariant { + fn into(self) -> VNode { + match self.props { + Variants::Header(props) => VComp::new::(props, NodeRef::default()).into(), + Variants::Item(props) => VComp::new::(props, NodeRef::default()).into(), + } + } +} diff --git a/examples/nested_list/src/main.rs b/examples/nested_list/src/main.rs new file mode 100644 index 00000000000..130defbc4cb --- /dev/null +++ b/examples/nested_list/src/main.rs @@ -0,0 +1,4 @@ +fn main() { + web_logger::init(); + yew::start_app::(); +} diff --git a/examples/nested_list/static/index.html b/examples/nested_list/static/index.html new file mode 100644 index 00000000000..0d34f497c81 --- /dev/null +++ b/examples/nested_list/static/index.html @@ -0,0 +1,11 @@ + + + + + Yew • Nested List + + + + + + diff --git a/examples/nested_list/static/styles.css b/examples/nested_list/static/styles.css new file mode 100644 index 00000000000..79b3ca90544 --- /dev/null +++ b/examples/nested_list/static/styles.css @@ -0,0 +1,97 @@ +html, body { + width: 100%; + background: #FAFAFA; + font-family: monospace; +} + +.main { + display: flex; + flex-direction: column; + margin-top: 40px; + width: 100%; + align-items: center; +} + +.list-container { + margin-top: 20px; + display: flex; + flex-direction: column; + align-items: center; + padding: 30px; + border-radius: 4px; + background: #EEE; +} + +.list-container:hover { + background: #EAEAEA; +} + +.list { + display: flex; + flex-direction: column; + overflow: hidden; + border-radius: 3px; + border: 1px solid #666; + min-width: 30vw; +} + +.list.inactive { + opacity: 0.5; +} + +.list-header { + background: #FEECAA; + border-bottom: 1px solid #666; + padding: 10px; + cursor: pointer; +} + +.list-header:hover { + background: #FEE3A0; +} + +.list-item { + background: white; + border-bottom: 1px solid #666; + padding: 10px; +} + +.list-item:hover { + background: #FAFAFA; +} + +.list-item:last-child { + border-bottom: 0px; +} + +.list-item-details { + background: #EEE; + border: 1px solid #666; + border-radius: 3px; + margin-top: 10px; + padding: 10px; + text-align: center; +} + +.last-hovered { + margin-top: 20px; +} + +.last-hovered-text { + color: #666; + margin-left: 5px; +} + +.list-item-details .list-container { + margin-top: 0px; + padding: 15px; +} + +.sublist { + display: inline-block; + background: #FFF; + border: 1px solid #666; + border-radius: 3px; + margin: 5px; + padding: 5px 20px; +} diff --git a/examples/node_refs/Cargo.toml b/examples/node_refs/Cargo.toml new file mode 100644 index 00000000000..2cb0ce4acfd --- /dev/null +++ b/examples/node_refs/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "node_refs" +version = "0.1.0" +authors = ["Justin Starry "] +edition = "2018" + +[dependencies] +yew = { path = "../.." } +stdweb = "0.4.20" diff --git a/examples/node_refs/src/input.rs b/examples/node_refs/src/input.rs new file mode 100644 index 00000000000..e26f27c4ad4 --- /dev/null +++ b/examples/node_refs/src/input.rs @@ -0,0 +1,43 @@ +use yew::prelude::*; + +pub struct InputComponent { + props: Props, + link: ComponentLink, +} + +#[derive(Clone, Properties)] +pub struct Props { + #[props(required)] + pub on_hover: Callback<()>, +} + +pub enum Msg { + Hover, +} + +impl Component for InputComponent { + type Message = Msg; + type Properties = Props; + + fn create(props: Self::Properties, link: ComponentLink) -> Self { + InputComponent { props, link } + } + + fn update(&mut self, msg: Self::Message) -> ShouldRender { + match msg { + Msg::Hover => { + self.props.on_hover.emit(()); + } + } + false + } + + fn view(&self) -> Html { + html! { + + } + } +} diff --git a/examples/node_refs/src/lib.rs b/examples/node_refs/src/lib.rs new file mode 100644 index 00000000000..a40b5e09a65 --- /dev/null +++ b/examples/node_refs/src/lib.rs @@ -0,0 +1,75 @@ +#![recursion_limit = "256"] + +mod input; + +use input::InputComponent; +use stdweb::web::html_element::InputElement; +use stdweb::web::IHtmlElement; +use yew::prelude::*; + +pub struct Model { + link: ComponentLink, + refs: Vec, + focus_index: usize, +} + +pub enum Msg { + HoverIndex(usize), +} + +impl Component for Model { + type Message = Msg; + type Properties = (); + + fn create(_: Self::Properties, link: ComponentLink) -> Self { + Model { + link, + focus_index: 0, + refs: vec![NodeRef::default(), NodeRef::default()], + } + } + + fn mounted(&mut self) -> ShouldRender { + if let Some(input) = self.refs[self.focus_index].try_into::() { + input.focus(); + } + false + } + + fn update(&mut self, msg: Self::Message) -> ShouldRender { + match msg { + Msg::HoverIndex(index) => self.focus_index = index, + } + if let Some(input) = self.refs[self.focus_index].try_into::() { + input.focus(); + } + true + } + + fn view(&self) -> Html { + html! { +
+

{ "Node Refs Demo" }

+

{ "Refs can be used to access and manipulate DOM elements directly" }

+
    +
  • { "First input will focus on mount" }
  • +
  • { "Each input will focus on hover" }
  • +
+
+ + +
+
+ + +
+
+ } + } +} diff --git a/examples/node_refs/src/main.rs b/examples/node_refs/src/main.rs new file mode 100644 index 00000000000..63360e2c5d9 --- /dev/null +++ b/examples/node_refs/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + yew::start_app::(); +} diff --git a/examples/npm_and_rest/Cargo.toml b/examples/npm_and_rest/Cargo.toml index 71aa4b911f0..146b3b7a753 100644 --- a/examples/npm_and_rest/Cargo.toml +++ b/examples/npm_and_rest/Cargo.toml @@ -5,8 +5,8 @@ authors = ["Denis Kolodin "] edition = "2018" [dependencies] -failure = "0.1" +anyhow = "1" serde = "1" serde_derive = "1" -stdweb = "0.4" +stdweb = "0.4.20" yew = { path = "../.." } diff --git a/examples/npm_and_rest/src/gravatar.rs b/examples/npm_and_rest/src/gravatar.rs index 6b8b0401b78..2dafae95aa6 100644 --- a/examples/npm_and_rest/src/gravatar.rs +++ b/examples/npm_and_rest/src/gravatar.rs @@ -1,4 +1,4 @@ -use failure::{format_err, Error}; +use anyhow::{anyhow, Error}; use serde_derive::Deserialize; use yew::callback::Callback; use yew::format::{Json, Nothing}; @@ -38,8 +38,7 @@ impl GravatarService { if meta.status.is_success() { callback.emit(data) } else { - // format_err! is a macro in crate `failure` - callback.emit(Err(format_err!( + callback.emit(Err(anyhow!( "{}: error getting profile https://gravatar.com/", meta.status ))) diff --git a/examples/npm_and_rest/src/lib.rs b/examples/npm_and_rest/src/lib.rs index 5ff89a8a370..27d646ae315 100644 --- a/examples/npm_and_rest/src/lib.rs +++ b/examples/npm_and_rest/src/lib.rs @@ -7,14 +7,15 @@ extern crate stdweb; pub mod ccxt; pub mod gravatar; -use failure::Error; +use anyhow::Error; use yew::services::fetch::FetchTask; -use yew::{html, Callback, Component, ComponentLink, Html, Renderable, ShouldRender}; +use yew::{html, Callback, Component, ComponentLink, Html, ShouldRender}; use ccxt::CcxtService; use gravatar::{GravatarService, Profile}; pub struct Model { + link: ComponentLink, gravatar: GravatarService, ccxt: CcxtService, callback: Callback>, @@ -33,11 +34,12 @@ impl Component for Model { type Message = Msg; type Properties = (); - fn create(_: Self::Properties, mut link: ComponentLink) -> Self { + fn create(_: Self::Properties, link: ComponentLink) -> Self { Model { + link: link.clone(), gravatar: GravatarService::new(), ccxt: CcxtService::new(), - callback: link.send_back(Msg::GravatarReady), + callback: link.callback(Msg::GravatarReady), profile: None, exchanges: Vec::new(), task: None, @@ -64,10 +66,8 @@ impl Component for Model { } true } -} -impl Renderable for Model { - fn view(&self) -> Html { + fn view(&self) -> Html { let view_exchange = |exchange| { html! {
  • { exchange }
  • @@ -75,8 +75,8 @@ impl Renderable for Model { }; html! {
    - - + +
      { for self.exchanges.iter().map(view_exchange) }
    diff --git a/examples/routing/Cargo.toml b/examples/routing/Cargo.toml deleted file mode 100644 index 02962ac583c..00000000000 --- a/examples/routing/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "routing" -version = "0.1.0" -authors = ["Henry Zimmerman "] -edition = "2018" - -[dependencies] -log = "0.4" -web_logger = "0.1" -serde = { version = "1.0", features = ["derive"] } -yew = { path = "../.." } -stdweb = "0.4" diff --git a/examples/routing/src/b_component.rs b/examples/routing/src/b_component.rs deleted file mode 100644 index 7a429f2f1fb..00000000000 --- a/examples/routing/src/b_component.rs +++ /dev/null @@ -1,142 +0,0 @@ -use crate::router::{Request, Route, Router}; -use log::info; -use yew::agent::Bridged; -use yew::{html, Bridge, Component, ComponentLink, Html, Renderable, ShouldRender}; - -pub struct BModel { - number: Option, - sub_path: Option, - router: Box>>, -} - -pub enum Msg { - Navigate(Vec), // Navigate after performing other actions - Increment, - Decrement, - UpdateSubpath(String), - HandleRoute(Route<()>), -} - -impl Component for BModel { - type Message = Msg; - type Properties = (); - - fn create(_: Self::Properties, mut link: ComponentLink) -> Self { - let callback = link.send_back(|route: Route<()>| Msg::HandleRoute(route)); - let mut router = Router::bridge(callback); - - router.send(Request::GetCurrentRoute); - - BModel { - number: None, - sub_path: None, - router, - } - } - - fn update(&mut self, msg: Self::Message) -> ShouldRender { - match msg { - Msg::Navigate(msgs) => { - // Perform the wrapped action first - for msg in msgs { - self.update(msg); - } - - // The path dictating that this component be instantiated must be provided - let mut path_segments = vec!["b".into()]; - if let Some(ref sub_path) = self.sub_path { - path_segments.push(sub_path.clone()) - } - - let fragment: Option = self.number.map(|x: usize| x.to_string()); - - let route = Route { - path_segments, - query: None, - fragment, - state: (), - }; - - self.router.send(Request::ChangeRoute(route)); - false - } - Msg::HandleRoute(route) => { - info!("Routing: {}", route.to_route_string()); - // Instead of each component selecting which parts of the path are important to it, - // it is also possible to match on the `route.to_route_string().as_str()` once - // and create enum variants representing the different children and pass them as props. - self.sub_path = route.path_segments.get(1).map(String::clone); - self.number = route - .fragment - .and_then(|x| usize::from_str_radix(&x, 10).ok()); - - true - } - Msg::Increment => { - let n = if let Some(number) = self.number { - number + 1 - } else { - 1 - }; - self.number = Some(n); - true - } - Msg::Decrement => { - let n: usize = if let Some(number) = self.number { - if number > 0 { - number - 1 - } else { - number - } - } else { - 0 - }; - self.number = Some(n); - true - } - Msg::UpdateSubpath(path) => { - self.sub_path = Some(path); - true - } - } - } - - fn change(&mut self, _props: Self::Properties) -> ShouldRender { - // Apparently change MUST be implemented in this case, even though no props were changed - true - } -} -impl Renderable for BModel { - fn view(&self) -> Html { - html! { -
    -
    - { self.display_number() } - - -
    - - { self.display_subpath_input() } - -
    - } - } -} - -impl BModel { - fn display_number(&self) -> String { - if let Some(number) = self.number { - format!("Number: {}", number) - } else { - format!("Number: None") - } - } - fn display_subpath_input(&self) -> Html { - let sub_path = self.sub_path.clone(); - html! { - - } - } -} diff --git a/examples/routing/src/lib.rs b/examples/routing/src/lib.rs deleted file mode 100644 index 67a13e38d35..00000000000 --- a/examples/routing/src/lib.rs +++ /dev/null @@ -1,127 +0,0 @@ -#![recursion_limit = "128"] - -mod b_component; -mod router; -mod routing; -use b_component::BModel; - -use log::info; -use router::Route; -use yew::agent::Bridged; -use yew::{html, Bridge, Component, ComponentLink, Html, Renderable, ShouldRender}; - -pub enum Child { - A, - B, - PathNotFound(String), -} - -pub struct Model { - child: Child, - router: Box>>, -} - -pub enum Msg { - NavigateTo(Child), - HandleRoute(Route<()>), -} - -impl Component for Model { - type Message = Msg; - type Properties = (); - - fn create(_: Self::Properties, mut link: ComponentLink) -> Self { - let callback = link.send_back(|route: Route<()>| Msg::HandleRoute(route)); - let mut router = router::Router::bridge(callback); - - // TODO Not sure if this is technically correct. This should be sent _after_ the component has been created. - // I think the `Component` trait should have a hook called `on_mount()` - // that is called after the component has been attached to the vdom. - // It seems like this only works because the JS engine decides to activate the - // router worker logic after the mounting has finished. - router.send(router::Request::GetCurrentRoute); - - Model { - child: Child::A, // This should be quickly overwritten by the actual route. - router, - } - } - - fn update(&mut self, msg: Self::Message) -> ShouldRender { - match msg { - Msg::NavigateTo(child) => { - let path_segments = match child { - Child::A => vec!["a".into()], - Child::B => vec!["b".into()], - Child::PathNotFound(_) => vec!["path_not_found".into()], - }; - - let route = router::Route { - path_segments, - query: None, - fragment: None, - state: (), - }; - - self.router.send(router::Request::ChangeRoute(route)); - false - } - Msg::HandleRoute(route) => { - info!("Routing: {}", route.to_route_string()); - // Instead of each component selecting which parts of the path are important to it, - // it is also possible to match on the `route.to_route_string().as_str()` once - // and create enum variants representing the different children and pass them as props. - self.child = if let Some(first_segment) = route.path_segments.get(0) { - match first_segment.as_str() { - "a" => Child::A, - "b" => Child::B, - other => Child::PathNotFound(other.into()), - } - } else { - Child::PathNotFound("path_not_found".into()) - }; - - true - } - } - } -} - -impl Renderable for Model { - fn view(&self) -> Html { - html! { -
    - -
    - {self.child.view()} -
    -
    - } - } -} - -impl Renderable for Child { - fn view(&self) -> Html { - match *self { - Child::A => html! { - <> - {"This corresponds to route 'a'"} - - }, - Child::B => html! { - <> - {"This corresponds to route 'b'"} - - - }, - Child::PathNotFound(ref path) => html! { - <> - {format!("Invalid path: '{}'", path)} - - }, - } - } -} diff --git a/examples/routing/src/main.rs b/examples/routing/src/main.rs deleted file mode 100644 index 4f1fec842b4..00000000000 --- a/examples/routing/src/main.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - yew::start_app::(); -} diff --git a/examples/routing/src/router.rs b/examples/routing/src/router.rs deleted file mode 100644 index 8df891c372e..00000000000 --- a/examples/routing/src/router.rs +++ /dev/null @@ -1,181 +0,0 @@ -//! Agent that exposes a usable routing interface to components. - -use crate::routing::RouteService; -use log::info; -use serde::{Deserialize, Serialize}; -use std::collections::HashSet; -use std::fmt::Debug; -use stdweb::unstable::TryFrom; -use stdweb::JsSerialize; -use stdweb::Value; -use yew::worker::*; - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct Route { - pub path_segments: Vec, - pub query: Option, - pub fragment: Option, - pub state: T, -} - -impl Route -where - T: JsSerialize + Clone + TryFrom + Default + 'static, -{ - pub fn to_route_string(&self) -> String { - let path = self.path_segments.join("/"); - let mut path = format!("/{}", path); // add the leading '/' - if let Some(ref query) = self.query { - path = format!("{}?{}", path, query); - } - if let Some(ref fragment) = self.fragment { - path = format!("{}#{}", path, fragment) - } - path - } - - pub fn current_route(route_service: &RouteService) -> Self { - let path = route_service.get_path(); // guaranteed to always start with a '/' - let mut path_segments: Vec = path.split("/").map(String::from).collect(); - path_segments.remove(0); // remove empty string that is split from the first '/' - - let mut query: String = route_service.get_query(); // The first character will be a '?' - let query: Option = if query.len() > 1 { - query.remove(0); - Some(query) - } else { - None - }; - - let mut fragment: String = route_service.get_fragment(); // The first character will be a '#' - let fragment: Option = if fragment.len() > 1 { - fragment.remove(0); - Some(fragment) - } else { - None - }; - - Route { - path_segments, - query, - fragment, - state: T::default(), - } - } -} - -pub enum Msg -where - T: JsSerialize + Clone + Debug + TryFrom + 'static, -{ - BrowserNavigationRouteChanged((String, T)), -} - -impl Transferable for Route where for<'de> T: Serialize + Deserialize<'de> {} - -#[derive(Serialize, Deserialize, Debug)] -pub enum Request { - /// Changes the route using a RouteInfo struct and alerts connected components to the route change. - ChangeRoute(Route), - /// Changes the route using a RouteInfo struct, but does not alert connected components to the route change. - ChangeRouteNoBroadcast(Route), - GetCurrentRoute, -} - -impl Transferable for Request where for<'de> T: Serialize + Deserialize<'de> {} - -/// The Router worker holds on to the RouteService singleton and mediates access to it. -pub struct Router -where - for<'de> T: JsSerialize - + Clone - + Debug - + TryFrom - + Default - + Serialize - + Deserialize<'de> - + 'static, -{ - link: AgentLink>, - route_service: RouteService, - /// A list of all entities connected to the router. - /// When a route changes, either initiated by the browser or by the app, - /// the route change will be broadcast to all listening entities. - subscribers: HashSet, -} - -impl Agent for Router -where - for<'de> T: JsSerialize - + Clone - + Debug - + TryFrom - + Default - + Serialize - + Deserialize<'de> - + 'static, -{ - type Reach = Context; - type Message = Msg; - type Input = Request; - type Output = Route; - - fn create(link: AgentLink) -> Self { - let callback = link.send_back(|route_changed: (String, T)| { - Msg::BrowserNavigationRouteChanged(route_changed) - }); - let mut route_service = RouteService::new(); - route_service.register_callback(callback); - - Router { - link, - route_service, - subscribers: HashSet::new(), - } - } - - fn update(&mut self, msg: Self::Message) { - match msg { - Msg::BrowserNavigationRouteChanged((_route_string, state)) => { - info!("Browser navigated"); - let mut route = Route::current_route(&self.route_service); - route.state = state; - for sub in self.subscribers.iter() { - self.link.response(*sub, route.clone()); - } - } - } - } - - fn handle(&mut self, msg: Self::Input, who: HandlerId) { - info!("Request: {:?}", msg); - match msg { - Request::ChangeRoute(route) => { - let route_string: String = route.to_route_string(); - // set the route - self.route_service.set_route(&route_string, route.state); - // get the new route. This will contain a default state object - let route = Route::current_route(&self.route_service); - // broadcast it to all listening components - for sub in self.subscribers.iter() { - self.link.response(*sub, route.clone()); - } - } - Request::ChangeRouteNoBroadcast(route) => { - let route_string: String = route.to_route_string(); - self.route_service.set_route(&route_string, route.state); - } - Request::GetCurrentRoute => { - let route = Route::current_route(&self.route_service); - self.link.response(who, route.clone()); - } - } - } - - fn connected(&mut self, id: HandlerId) { - self.subscribers.insert(id); - } - fn disconnected(&mut self, id: HandlerId) { - self.subscribers.remove(&id); - } -} diff --git a/examples/routing/src/routing.rs b/examples/routing/src/routing.rs deleted file mode 100644 index b4bf75b58a4..00000000000 --- a/examples/routing/src/routing.rs +++ /dev/null @@ -1,101 +0,0 @@ -//! Service to handle routing. - -use stdweb::unstable::TryFrom; -use stdweb::web::event::PopStateEvent; -use stdweb::web::window; -use stdweb::web::EventListenerHandle; -use stdweb::web::History; -use stdweb::web::IEventTarget; -use stdweb::web::Location; -use stdweb::JsSerialize; -use stdweb::Value; -use yew::callback::Callback; - -use std::marker::PhantomData; - -/// A service that facilitates manipulation of the browser's URL bar and responding to browser -/// 'forward' and 'back' events. -/// -/// The `T` determines what route state can be stored in the route service. -pub struct RouteService { - history: History, - location: Location, - event_listener: Option, - phantom_data: PhantomData, -} - -impl RouteService -where - T: JsSerialize + Clone + TryFrom + 'static, -{ - /// Creates the route service. - pub fn new() -> RouteService { - let location = window() - .location() - .expect("browser does not support location API"); - RouteService { - history: window().history(), - location, - event_listener: None, - phantom_data: PhantomData, - } - } - - /// Registers a callback to the route service. - /// Callbacks will be called when the History API experiences a change such as - /// popping a state off of its stack when the forward or back buttons are pressed. - pub fn register_callback(&mut self, callback: Callback<(String, T)>) { - self.event_listener = Some(window().add_event_listener(move |event: PopStateEvent| { - let state_value: Value = event.state(); - - if let Ok(state) = T::try_from(state_value) { - let location: Location = window().location().unwrap(); - let route: String = Self::get_route_from_location(&location); - - callback.emit((route.clone(), state.clone())) - } else { - eprintln!("Nothing farther back in history, not calling routing callback."); - } - })); - } - - /// Sets the browser's url bar to contain the provided route, - /// and creates a history entry that can be navigated via the forward and back buttons. - /// The route should be a relative path that starts with a '/'. - /// A state object be stored with the url. - pub fn set_route(&mut self, route: &str, state: T) { - self.history.push_state(state, "", Some(route)); - } - - fn get_route_from_location(location: &Location) -> String { - let path = location.pathname().unwrap(); - let query = location.search().unwrap(); - let fragment = location.hash().unwrap(); - format!( - "{path}{query}{fragment}", - path = path, - query = query, - fragment = fragment - ) - } - - /// Gets the concatenated path, query, and fragment strings - pub fn get_route(&self) -> String { - Self::get_route_from_location(&self.location) - } - - /// Gets the path name of the current url. - pub fn get_path(&self) -> String { - self.location.pathname().unwrap() - } - - /// Gets the query string of the current url. - pub fn get_query(&self) -> String { - self.location.search().unwrap() - } - - /// Gets the fragment of the current url. - pub fn get_fragment(&self) -> String { - self.location.hash().unwrap() - } -} diff --git a/examples/showcase/Cargo.toml b/examples/showcase/Cargo.toml index a309cc0e6a8..b9acad62070 100644 --- a/examples/showcase/Cargo.toml +++ b/examples/showcase/Cargo.toml @@ -14,13 +14,13 @@ counter = { path = "../counter" } crm = { path = "../crm" } custom_components = { path = "../custom_components" } dashboard = { path = "../dashboard" } +node_refs = { path = "../node_refs" } fragments = { path = "../fragments" } game_of_life = { path = "../game_of_life" } inner_html = { path = "../inner_html" } large_table = { path = "../large_table" } mount_point = { path = "../mount_point" } npm_and_rest = { path = "../npm_and_rest" } -routing = { path = "../routing" } textarea = { path = "../textarea" } timer = { path = "../timer" } todomvc = { path = "../todomvc" } diff --git a/examples/showcase/src/main.rs b/examples/showcase/src/main.rs index 55d6ab9f6b1..d2b0329fd7b 100644 --- a/examples/showcase/src/main.rs +++ b/examples/showcase/src/main.rs @@ -10,8 +10,8 @@ use inner_html::Model as InnerHtml; use large_table::Model as LargeTable; use log::trace; use mount_point::Model as MountPoint; +use node_refs::Model as NodeRefs; use npm_and_rest::Model as NpmAndRest; -use routing::Model as Routing; use strum::IntoEnumIterator; use strum_macros::{Display, EnumIter, EnumString}; use textarea::Model as Textarea; @@ -19,7 +19,7 @@ use timer::Model as Timer; use todomvc::Model as Todomvc; use two_apps::Model as TwoApps; use yew::components::Select; -use yew::{html, App, Component, ComponentLink, Html, Renderable, ShouldRender}; +use yew::{html, App, Component, ComponentLink, Html, ShouldRender}; #[derive(Clone, Debug, Display, EnumString, EnumIter, PartialEq)] enum Scene { @@ -27,13 +27,13 @@ enum Scene { Crm, CustomComponents, Dashboard, + NodeRefs, Fragments, GameOfLife, InnerHtml, LargeTable, MountPoint, NpmAndRest, - Routing, Textarea, Timer, Todomvc, @@ -42,6 +42,7 @@ enum Scene { struct Model { scene: Option, + link: ComponentLink, } enum Msg { @@ -52,8 +53,8 @@ impl Component for Model { type Message = Msg; type Properties = (); - fn create(_: Self::Properties, _: ComponentLink) -> Self { - Self { scene: None } + fn create(_: Self::Properties, link: ComponentLink) -> Self { + Self { scene: None, link } } fn update(&mut self, msg: Self::Message) -> ShouldRender { @@ -64,10 +65,8 @@ impl Component for Model { } } } -} -impl Renderable for Model { - fn view(&self) -> Html { + fn view(&self) -> Html { html! {
    @@ -75,7 +74,7 @@ impl Renderable for Model { selected=self.scene.clone() options=Scene::iter().collect::>() - onchange=Msg::SwitchTo /> + onchange=self.link.callback(Msg::SwitchTo) />
    { self.view_scene() } @@ -86,20 +85,20 @@ impl Renderable for Model { } impl Model { - fn view_scene(&self) -> Html { + fn view_scene(&self) -> Html { if let Some(scene) = self.scene.as_ref() { match scene { Scene::Counter => html! { }, Scene::Crm => html! { }, Scene::CustomComponents => html! { }, Scene::Dashboard => html! { }, + Scene::NodeRefs => html! { }, Scene::Fragments => html! { }, Scene::GameOfLife => html! { }, Scene::InnerHtml => html! { }, Scene::LargeTable => html! { }, Scene::MountPoint => html! { }, Scene::NpmAndRest => html! { }, - Scene::Routing => html! { }, Scene::Textarea => html! { - +
    {&self.value} diff --git a/examples/timer/src/lib.rs b/examples/timer/src/lib.rs index 623c76453de..8a6f23c8379 100644 --- a/examples/timer/src/lib.rs +++ b/examples/timer/src/lib.rs @@ -2,17 +2,18 @@ use std::time::Duration; use yew::services::{ConsoleService, IntervalService, Task, TimeoutService}; -use yew::{html, Callback, Component, ComponentLink, Html, Renderable, ShouldRender}; +use yew::{html, Callback, Component, ComponentLink, Html, ShouldRender}; pub struct Model { + link: ComponentLink, timeout: TimeoutService, interval: IntervalService, console: ConsoleService, callback_tick: Callback<()>, callback_done: Callback<()>, - job: Option>, + job: Option>, messages: Vec<&'static str>, - _standalone: Box, + _standalone: Box, } pub enum Msg { @@ -27,7 +28,7 @@ impl Component for Model { type Message = Msg; type Properties = (); - fn create(_: Self::Properties, mut link: ComponentLink) -> Self { + fn create(_: Self::Properties, link: ComponentLink) -> Self { // This callback doesn't send any message to a scope let callback = |_| { println!("Example of a standalone callback."); @@ -36,11 +37,12 @@ impl Component for Model { let handle = interval.spawn(Duration::from_secs(10), callback.into()); Model { + link: link.clone(), timeout: TimeoutService::new(), interval, console: ConsoleService::new(), - callback_tick: link.send_back(|_| Msg::Tick), - callback_done: link.send_back(|_| Msg::Done), + callback_tick: link.callback(|_| Msg::Tick), + callback_done: link.callback(|_| Msg::Done), job: None, messages: Vec::new(), _standalone: Box::new(handle), @@ -96,19 +98,20 @@ impl Component for Model { } true } -} -impl Renderable for Model { - fn view(&self) -> Html { + fn view(&self) -> Html { let view_message = |message| { html! {

    { message }

    } }; let has_job = self.job.is_some(); html! {
    - - - + + +
    { for self.messages.iter().map(view_message) }
    diff --git a/examples/todomvc/src/lib.rs b/examples/todomvc/src/lib.rs index 0b80dad47b8..71113feef31 100644 --- a/examples/todomvc/src/lib.rs +++ b/examples/todomvc/src/lib.rs @@ -6,11 +6,12 @@ use strum_macros::{EnumIter, ToString}; use yew::events::IKeyboardEvent; use yew::format::Json; use yew::services::storage::{Area, StorageService}; -use yew::{html, Component, ComponentLink, Href, Html, Renderable, ShouldRender}; +use yew::{html, Component, ComponentLink, Href, Html, InputData, KeyPressEvent, ShouldRender}; const KEY: &'static str = "yew.todomvc.self"; pub struct Model { + link: ComponentLink, storage: StorageService, state: State, } @@ -48,7 +49,7 @@ impl Component for Model { type Message = Msg; type Properties = (); - fn create(_: Self::Properties, _: ComponentLink) -> Self { + fn create(_: Self::Properties, link: ComponentLink) -> Self { let storage = StorageService::new(Area::Local); let entries = { if let Json(Ok(restored_model)) = storage.restore(KEY) { @@ -63,7 +64,11 @@ impl Component for Model { value: "".into(), edit_value: "".into(), }; - Model { storage, state } + Model { + link, + storage, + state, + } } fn update(&mut self, msg: Self::Message) -> ShouldRender { @@ -115,10 +120,8 @@ impl Component for Model { self.storage.store(KEY, Json(&self.state.entries)); true } -} -impl Renderable for Model { - fn view(&self) -> Html { + fn view(&self) -> Html { html! {
    @@ -127,9 +130,13 @@ impl Renderable for Model { { self.view_input() }
    - +
      - { for self.state.entries.iter().filter(|e| self.state.filter.fit(e)).enumerate().map(view_entry) } + { for self.state.entries.iter().filter(|e| self.state.filter.fit(e)).enumerate().map(|e| self.view_entry(e)) }
    @@ -140,7 +147,7 @@ impl Renderable for Model {
      { for Filter::iter().map(|flt| self.view_filter(flt)) }
    -
    @@ -156,30 +163,30 @@ impl Renderable for Model { } impl Model { - fn view_filter(&self, filter: Filter) -> Html { + fn view_filter(&self, filter: Filter) -> Html { let flt = filter.clone(); html! {
  • + onclick=self.link.callback(move |_| Msg::SetFilter(flt.clone()))> { filter }
  • } } - fn view_input(&self) -> Html { + fn view_input(&self) -> Html { html! { // You can use standard Rust comments. One line: //
  • + }) /> /* Or multiline: