From bb529b99732501977a0447fe6ae1ad081dc7ecf8 Mon Sep 17 00:00:00 2001 From: despiegk Date: Fri, 8 Aug 2025 08:14:18 +0200 Subject: [PATCH] ... --- .gitignore | 2 + Cargo.toml | 18 ++ LICENSE-APACHE | 177 ++++++++++++++++++ LICENSE-MIT | 25 +++ README.md | 75 +++++++- ai_instructions.md | 452 +++++++++++++++++++++++++++++++++++++++++++++ env.sh | 0 index.html | 11 ++ index.scss | 49 +++++ src/app.rs | 95 ++++++++++ src/main.rs | 6 + start.sh | 6 + 12 files changed, 915 insertions(+), 1 deletion(-) create mode 100644 Cargo.toml create mode 100644 LICENSE-APACHE create mode 100644 LICENSE-MIT create mode 100644 ai_instructions.md create mode 100644 env.sh create mode 100644 index.html create mode 100644 index.scss create mode 100644 src/app.rs create mode 100644 src/main.rs create mode 100755 start.sh diff --git a/.gitignore b/.gitignore index 3ca43ae..8f77cef 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ Cargo.lock # MSVC Windows builds of rustc generate these, which store debugging information *.pdb + +dist/ diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a6388cd --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "web1" +version = "0.1.0" +edition = "2021" +description = "Template for starting a Yew project using Trunk" +readme = "README.md" +repository = "https://github.com/yewstack/yew-trunk-minimal-template" +license = "MIT OR Apache-2.0" +keywords = ["yew", "trunk"] +categories = ["gui", "wasm", "web-programming"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[dependencies] +yew = { version="0.21", features=["csr"] } +web-sys = { version = "0.3", features = ["Document", "HtmlElement", "Window"] } +gloo-utils = "0.1" +gloo-storage = "0.2" +serde = { version = "1.0", features = ["derive"] } \ No newline at end of file diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..08017da --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..b6c65b2 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) despiegk + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index bd39e9e..7f0fff9 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,75 @@ -# webtest +# Yew Trunk Template +This is a fairly minimal template for a Yew app that's built with [Trunk]. + +## Usage + +For a more thorough explanation of Trunk and its features, please head over to the [repository][trunk]. + +### Installation + +If you don't already have it installed, it's time to install Rust: . +The rest of this guide assumes a typical Rust installation which contains both `rustup` and Cargo. + +To compile Rust to WASM, we need to have the `wasm32-unknown-unknown` target installed. +If you don't already have it, install it with the following command: + +```bash +rustup target add wasm32-unknown-unknown +``` + +Now that we have our basics covered, it's time to install the star of the show: [Trunk]. +Simply run the following command to install it: + +```bash +cargo install trunk wasm-bindgen-cli +``` + +That's it, we're done! + +### Running + +```bash +trunk serve +``` + +Rebuilds the app whenever a change is detected and runs a local server to host it. + +There's also the `trunk watch` command which does the same thing but without hosting it. + +### Release + +```bash +trunk build --release +``` + +This builds the app in release mode similar to `cargo build --release`. +You can also pass the `--release` flag to `trunk serve` if you need to get every last drop of performance. + +Unless overwritten, the output will be located in the `dist` directory. + +## Using this template + +There are a few things you have to adjust when adopting this template. + +### Remove example code + +The code in [src/main.rs](src/main.rs) specific to the example is limited to only the `view` method. +There is, however, a fair bit of Sass in [index.scss](index.scss) you can remove. + +### Update metadata + +Update the `version`, `description` and `repository` fields in the [Cargo.toml](Cargo.toml) file. +The [index.html](index.html) file also contains a `` tag that needs updating. + + +Finally, you should update this very `README` file to be about your app. + +### License + +The template ships with both the Apache and MIT license. +If you don't want to have your app dual licensed, just remove one (or both) of the files and update the `license` field in `Cargo.toml`. + +There are two empty spaces in the MIT license you need to fill out: `` and `despiegk <kristof@incubaid.com>`. + +[trunk]: https://github.com/thedodd/trunk \ No newline at end of file diff --git a/ai_instructions.md b/ai_instructions.md new file mode 100644 index 0000000..fac047a --- /dev/null +++ b/ai_instructions.md @@ -0,0 +1,452 @@ + +# Yew Framework Documentation for AI Coders: Core Concepts & Coding + +This document provides a comprehensive overview of the Yew framework's core coding concepts, designed to equip AI robot coders with the knowledge necessary to build web applications using Rust and WebAssembly. + +Yew is a modern Rust framework for building client-side web applications using WebAssembly (Wasm). It enables the development of highly performant web UIs by leveraging Rust's strong type system and rich ecosystem. Yew promotes a component-based architecture for building reusable and maintainable UI elements. + +## Fundamental Building Blocks + +### The `html!` Macro for UI Composition + +Yew employs the `html!` procedural macro for declarative UI construction, drawing inspiration from JSX. This macro is the primary way to define the structure of your component's output. + +**Syntax and Features:** + +* **Single Root Node:** The `html!` macro always expects a single root HTML node. To render multiple top-level elements without a wrapping container, use the fragment syntax: `<> ... </>`. +* **Embedding Rust Expressions:** Any valid Rust expression can be embedded within the markup using curly braces (`{ expression }`). The expression **must evaluate to a type that implements `Into<Html>`**. + ```rust + let header_text = "Welcome to Yew".to_string(); + let count = 5; + html! { + <> + <h1>{ header_text }</h1> + <p>{"Current count: "}{ count }</p> + </> + } + ``` + * **Literals:** String literals are typically enclosed in quotes and then a `{}`, e.g., `{"Hello"}`. They are treated as `Text` nodes, inherently mitigating common HTML injection (XSS) risks. +* **Element / Component Definition:** + * **HTML Elements:** Standard HTML elements are written as `<tagname property="value">child</tagname>` or self-closing `<tagname property="value" />`. + * **Dynamic Tag Names:** For situations where the HTML tag name is determined at runtime, use the `@{expression}` syntax. The expression must be a string. + ```rust + let level = 3; + html! { <@{format!("h{}", level)} class="subtitle">{"Dynamic Heading"}</@> } + ``` + * **Yew Components:** Yew components are instantiated like custom HTML tags, using PascalCase for their names: `<MyComponent property={value} />`. +* **Attributes and Properties:** + * **HTML Attributes:** Set on elements directly: `<div attribute={rust_value} />`. + * **Boolean Attributes:** Set with `true` or `false`. `false` is equivalent to omitting the attribute entirely. + ```rust + html! { <input type="checkbox" checked={some_boolean_var} /> } + ``` + * **String-like Attributes:** Can accept `&str`, `String`, or Yew's optimized `AttrValue` (a cheaply cloneable `Rc<str>` or `&'static str`). `AttrValue` is generally recommended for performance-sensitive scenarios, especially when passing values as properties to other components. + * **Optional Attributes:** Use `Option<T>` for attribute values. If the `Option` is `None`, the attribute will not be rendered in the DOM. + ```rust + let maybe_id: Option<&str> = Some("unique-element"); + html! { <div id={maybe_id}></div> } // Renders with id or not + ``` + * **Yew-Specific Properties (Special Props):** These are not directly reflected in the DOM but serve as instructions to Yew's Virtual DOM. + * `ref={node_ref_handle}`: Connects a `NodeRef` to a DOM element, allowing direct programmatic access to the underlying DOM node (e.g., for canvas manipulation, scrolling, form input values). + * `key={unique_key}`: Provides a unique identifier for elements within a list. Crucial for performance optimization, as Yew uses keys to efficiently reconcile list items during updates, preventing unnecessary re-renders or DOM manipulations. Keys must be unique *within their immediate siblings* (the list itself) and should be stable/deterministic, not based on item position. +* **Conditional Rendering:** Use standard Rust `if` and `if let` control flow structures directly within the `html!` macro to conditionally render content. + ```rust + let show_message = true; + html! { + if show_message { + <p>{"This message is visible."}</p> + } else { + <p>{"Message hidden."}</p> + } + } + ``` +* **List Rendering / Iteration:** + * Use `for` syntax directly in `html!`: `{ for collection.iter() }`. This expects the iterator items to be renderable `Html`. + * Alternatively, map the collection to `Html` elements and use `.collect::<Html>()`. + * **Always use `key` for list items** where the order or presence of items can change, as this drastically improves reconciliation performance. + ```rust + let items = vec!["Apple", "Banana", "Cherry"]; + html! { + <ul> + { for items.iter().map(|item| html! { <li key={item.to_string()}>{item}</li> }) } + </ul> + } + ``` + +### Components: Function Components + +Yew applications are built from components, which encapsulate UI logic and presentation. Function components are the recommended and most common way to define components in modern Yew. + +* **Definition:** Declare a function component using the `#[function_component]` attribute on a `fn` that returns `Html`. By convention, component names are PascalCase. + ```rust + use yew::prelude::*; + + #[function_component] + fn MySimpleComponent() -> Html { + html! { <p>{"Hello from a component!"}</p> } + } + ``` +* **Properties (Props):** Data is passed from parent to child components using "props". Props are defined by a struct that must implement `Properties` (usually via `#[derive(Properties)]`) and `PartialEq`. The function component accepts an `&Props` reference as its single argument. + ```rust + #[derive(Properties, PartialEq)] + pub struct GreetProps { + pub name: String, + #[prop_or_default] // Field attribute for optional props with default value + pub greeting_text: String, + } + + #[function_component] + fn Greeter(props: &GreetProps) -> Html { + let greeting = if props.greeting_text.is_empty() { + "Hello".to_string() + } else { + props.greeting_text.clone() + }; + html! { <p>{ format!("{}, {}!", greeting, props.name) }</p> } + } + // Usage: html! { <Greeter name="Alice" /> } or html! { <Greeter name="Bob" greeting_text="Hi" /> } + ``` + * **Reactive Nature of Props:** Yew automatically re-renders a component when its props change (detected via `PartialEq`). + * **`props!` Macro:** Allows building `Properties` structs programmatically. + ```rust + use yew::props; + let my_props = props! { GreetProps { name: "Charlie".to_string() } }; + html! { <Greeter ..my_props /> } + ``` +* **Children (Special Prop):** If a component's `Props` struct includes a `pub children: Html` field, it can accept nested `html!` content. + ```rust + #[derive(Properties, PartialEq)] + pub struct CardProps { + pub title: String, + pub children: Html, // This field name is special + } + + #[function_component] + fn Card(props: &CardProps) -> Html { + html! { + <div class="card"> + <h2>{ &props.title }</h2> + <div class="card-content"> + { props.children.clone() } + </div> + </div> + } + } + // Usage: html! { <Card title="My Card"> <p>Some content here.</p> </Card> } + ``` +* **Generic Components:** Function components can be generic over types, provided the generic type parameters meet the necessary trait bounds (e.g., `PartialEq`). + ```rust + #[derive(Properties, PartialEq)] + pub struct ItemDisplayProps<T: PartialEq + ToHtml> { + pub item: T, + } + + #[function_component] + pub fn ItemDisplay<T>(props: &ItemDisplayProps<T>) -> Html + where + T: PartialEq + ToHtml + 'static, // 'static for use in VDOM + { + html! { <p>{ &props.item }</p> } + } + // Usage: html! { <ItemDisplay<i32> item=123 /> } + ``` +* **Pure Components:** A function component is "pure" if its output `Html` is solely determined by its props, and it has no side effects or internal mutable state. Yew's reconciliation benefits from pure components. + * Simple pure components with no hooks can sometimes be implemented as regular functions returning `Html` to reduce overhead. +* **Communication Patterns:** + * **Parent to Child:** Via [Props](#properties-props). + * **Child to Parent:** Via [Callbacks](#callbacks). The parent passes a `Callback` to the child via props, and the child calls `emit()` on it to send data back up. + +## State Management and Interactivity + +Yew provides "hooks" to manage mutable state and side effects within function components. + +### Hooks + +Hooks are functions that allow "hooking into" the lifecycle and state management capabilities of function components. + +**Rules of Hooks:** + +1. **Naming Convention:** Hook function names **must start with `use_`**. +2. **Top-Level Calls Only:** Hooks can **only be called at the top level of a function component or another hook**, and not inside loops, conditionals without `if let`, or nested functions *unless* within the scrutinee of a top-level `if` or `match` expression. +3. **Consistent Call Order:** Hooks must be called in the exact same order on every render. This enables Yew to correctly associate state with hook calls. +4. **No Early Return:** A component using hooks cannot `return` early before all hooks are called unless using [Suspense](#suspense). + +**Common Pre-defined Hooks:** + +* **`use_state<T>() -> UseStateHandle<T>`:** Manages local component state. Returns a handle that can be dereferenced to get the current value and has a `.set(new_value)` method to update the state. Updating state triggers a re-render. + ```rust + use yew::prelude::*; + + #[function_component] + fn ClickCounter() -> Html { + let count = use_state(|| 0); // Initializes with 0 + let onclick = { + let count = count.clone(); // Clone the handle for the closure + move |_| { + count.set(*count + 1); // Update the state + } + }; + html! { <button {onclick}>{"Clicked "}{*count}{" times"}</button> } + } + ``` +* **`use_state_eq<T: PartialEq>() -> UseStateHandle<T>`:** Similar to `use_state`, but only triggers a re-render if the new value is *not* equal to the current value (using `PartialEq`). +* **`use_memo<T, D>(f: impl FnOnce(D) -> T, deps: D) -> Rc<T>`:** Memoizes an expensive computation. The `f` closure is only re-executed if `deps` (dependencies) change. +* **`use_callback<IN, OUT, F>(f: F) -> Callback<IN, OUT>`:** Memoizes a `Callback` instance. The `Callback` is only recreated if its dependencies (captured variables) change. +* **`use_mut_ref<T>() -> Rc<RefCell<T>>`:** Provides a mutable reference (`RefCell`) that persists across re-renders without triggering them. Useful for mutable data that doesn't directly affect rendering or for interop with mutable JS APIs. +* **`use_node_ref() -> NodeRef`:** Creates a `NodeRef` handle, used to get a direct reference to a rendered DOM element. + ```rust + use yew::prelude::*; + use web_sys::HtmlInputElement; + + #[function_component] + fn MyInput() -> Html { + let input_ref = use_node_ref(); + let current_value = use_state(|| String::new()); + + let on_input_change = { + let input_ref = input_ref.clone(); + let current_value = current_value.clone(); + move |_| { + if let Some(input) = input_ref.cast::<HtmlInputElement>() { + current_value.set(input.value()); + } + } + }; + + html! { + <div> + <input ref={input_ref} type="text" oninput={on_input_change} /> + <p>{"Input Value: "}{&*current_value}</p> + </div> + } + } + ``` +* **`use_reducer<R>() -> UseReducerHandle<R>`:** Manages more complex state using a reducer pattern (similar to Redux), allowing state updates based on dispatched "actions". The state must implement the `Reducible` trait. +* **`use_reducer_eq<R: PartialEq>() -> UseReducerHandle<R>`:** Similar to `use_reducer`, but dispatches only if the new state differs from the old. +* **`use_effect(f: impl FnOnce() -> impl FnOnce())`:** Runs a side effect after every render. Can return a cleanup closure that runs before the next effect or when the component is unmounted. +* **`use_effect_with<D>(deps: D, f: impl FnOnce(D) -> impl FnOnce())`:** Runs a side effect only when `deps` (dependencies) change. +* **`use_context<T: Clone + PartialEq>() -> Option<Rc<T>>`:** Accesses a context value provided by an ancestor `ContextProvider` component. +* **`use_force_update()`:** Returns a callback that, when emitted, forces a re-render of the component. Use sparingly. + +**Custom Hooks:** Components can extract reusable stateful logic into custom hooks by defining functions starting with `use_` and marking them with `#[hook]`. Custom hooks compose existing hooks. + +### Callbacks + +`Callback` is a crucial type for event handling and child-to-parent communication. It wraps an `Fn` closure in an `Rc`, making it cheaply clonable. + +* **`Callback::from(closure)`:** Creates a `Callback` from a closure. +* **`callback.emit(value)`:** Invokes the wrapped closure with the given value. +* **DOM Events:** Event handlers in `html!` (e.g., `onclick`, `oninput`) expect a `Callback` that takes the corresponding `web_sys` event type as an argument. + ```rust + use yew::prelude::*; + use web_sys::MouseEvent; + + #[function_component] + fn MyButton() -> Html { + let onclick_handler = Callback::from(move |e: MouseEvent| { + // Access event properties: + log::info!("Click event at: ({}, {})", e.client_x(), e.client_y()); + // More complex logic... + }); + html! { <button onclick={onclick_handler}>{"Click Me"}</button> } + } + ``` +* **`TargetCast` Trait:** Provided by Yew (within `yew::prelude::*`), this trait extends `web_sys::Event` to safely cast the event target to a specific HTML element type (e.g., `HtmlInputElement`). + * `event.target_dyn_into::<HtmlElementType>() -> Option<HtmlElementType>` (safe, checked) + * `event.target_unchecked_into::<HtmlElementType>() -> HtmlElementType` (unchecked, use with caution when type is guaranteed) + +### Contexts + +Contexts provide a way to pass data deeply through the component tree without manually "prop drilling" at every level. + +* **Provider (`ContextProvider`):** An ancestor component wraps its children with `ContextProvider<T>`, providing a value of type `T`. `T` must implement `Clone` and `PartialEq`. + ```rust + // Define your context data + #[derive(Clone, Debug, PartialEq)] + struct Theme { + foreground: String, + background: String, + } + + #[function_component] + fn ThemeProvider(props: &ChildrenProps) -> Html { // ChildrenProps from yew::html + let theme = use_state(|| Theme { + foreground: "#000".to_string(), + background: "#eee".to_string(), + }); + html! { + <yew::ContextProvider<Theme> context={(*theme).clone()}> + { props.children.clone() } + </yew::ContextProvider<Theme>> + } + } + // Usage: html! { <ThemeProvider> <ArbitraryDeepComponent /> </ThemeProvider> } + ``` +* **Consumer (`use_context` hook):** Descendant function components use `use_context::<T>()` to retrieve the provided value. + ```rust + #[function_component] + fn ThemedText() -> Html { + let theme = use_context::<Theme>() // Retrieve the Theme context + .expect("Theme context not provided!"); + html! { + <p style={format!("color: {}; background: {};", theme.foreground, theme.background)}> + {"This text is themed."} + </p> + } + } + ``` +* **Mutable Contexts:** To allow children to modify a context value, combine `ContextProvider` with `use_reducer` for a predictable state update mechanism. + +### Event Handling and Delegation + +Yew integrates with `web-sys` for DOM events. Yew's event system employs event delegation: listeners are not directly attached to individual elements but are handled by a single delegate at the application's root. Events then "bubble up" through Yew's Virtual DOM hierarchy. + +* **Event Listener Names:** In `html!`, event listeners start with `on` followed by the event name (e.g., `onclick`, `oninput`). +* **Manual Event Listeners:** For events not directly supported by `html!` or for fine-grained control, use `use_effect_with` and `gloo-events` (`EventListener`) to manually attach/detach event listeners to `NodeRef`-obtained DOM elements. + ```rust + use gloo::events::EventListener; + use yew::prelude::*; + use web_sys::HtmlElement; + + #[function_component] + fn CustomEventHandler() -> Html { + let div_ref = use_node_ref(); + use_effect_with(div_ref.clone(), { + let div_ref_clone = div_ref.clone(); + move |_| { + let mut listener_obj: Option<EventListener> = None; + if let Some(div_element) = div_ref_clone.cast::<HtmlElement>() { + let listener = EventListener::new(&div_element, "custom-event", move |event| { + log::info!("Custom event received!"); + // event.dyn_into::<web_sys::CustomEvent>() for richer data + }); + listener_obj = Some(listener); + } + move || drop(listener_obj) // Cleanup when effect re-runs or component unmounts + } + }); + html! { <div ref={div_ref}>{"Div with custom event listener"}</div> } + } + ``` + +## Advanced Topics + +### Interacting with JavaScript (JS) and Web APIs + +Yew compiles to Wasm, but direct interaction with browser APIs often involves `wasm-bindgen` and related crates. + +* **`wasm-bindgen`:** Bridges calls between Rust and JavaScript. Used for defining FFI (Foreign Function Interface) to import/export functions. +* **`web-sys`:** Provides Rust bindings for all Web APIs (DOM, Fetch, etc.). This is the primary way to interact with the browser from Rust. + * **Features:** `web-sys` is heavily feature-gated; enable only the necessary features in `Cargo.toml` to avoid bloat (e.g., `features = ["Document", "HtmlElement", "Window"]`). + * **Inheritance:** `web-sys` types simulate JavaScript inheritance using Rust's `Deref` and `AsRef` traits. For instance, an `HtmlElement` can deref to `Element`, then `Node`, then `EventTarget`, finally `JsValue`. This allows calling methods from ancestor types. + * **`JsCast` Trait:** Crucial for downcasting `JsValue` or generic `EventTarget` (from `web_sys`) to specific, more concrete types (e.g., `HtmlInputElement`). Provides `dyn_into` (checked, returns `Result`) and `unchecked_into` (unchecked, faster). +* **`js-sys`:** Provides Rust bindings for JavaScript's standard, built-in objects (e.g., `Date`, `Object`). +* **`wasm-bindgen-futures`:** Bridges Rust `Future`s with JavaScript `Promise`s. + * **`spawn_local(future)`:** Spawns a Rust `Future` to run on the current thread's event loop, enabling asynchronous operations (e.g., `async fetch()` calls). + +### Asynchronous Operations and Suspense + +Yew's `Suspense` component allows suspending component rendering while waiting for an asynchronous task (e.g., data fetching) to complete, showing a fallback UI in the interim. This enables a "Render-as-You-Fetch" pattern. + +* **`Suspense` Component:** Wraps children components that might "suspend". Requires a `fallback` prop, which is the `Html` to render during suspension. +* **Suspending Hooks:** A hook can signal suspension by returning `Err(Suspension)`. +* **`Suspension::new()`:** Creates a `Suspension` and a `SuspensionHandle`. When `handle.resume()` is called (or `handle` is dropped), the suspended component re-renders. + ```rust + use yew::prelude::*; + use yew::suspense::{Suspension, SuspensionResult}; + + // A hypothetical async data loading function + async fn fetch_user_data() -> String { "AI Robot Coder".to_string() } + + // This hook will suspend rendering until user data is fetched + #[hook] + fn use_current_user() -> SuspensionResult<String> { + let (suspension, handle) = Suspension::new(); // Create suspension + let user_data_state = use_state(|| None ); // Local state for fetched data + + // If data is already there, return Ok. + if let Some(data) = &*user_data_state { + return Ok(data.clone()); + } + + // If not, spawn an async task and signal suspension. + let user_data_state_clone = user_data_state.clone(); + wasm_bindgen_futures::spawn_local(async move { + let data = fetch_user_data().await; + user_data_state_clone.set(Some(data)); // Update state + handle.resume(); // Signal the suspension to resume + }); + + Err(suspension) // Signal that the component should suspend + } + + #[function_component] + fn UserDisplay() -> HtmlResult { // HtmlResult instead of Html for suspending components + let user_name = use_current_user()?; // The '?' operator propagates the suspension + Ok(html! { <p>{ format!("Hello, {}!", user_name) }</p> }) + } + + #[function_component] + fn App() -> Html { + html! { + <Suspense fallback={html!{<p>{"Loading user..."}</p>}}> + <UserDisplay /> + </Suspense> + } + } + ``` + +### Routing (`yew-router` crate) + +`yew-router` provides client-side routing for Single Page Applications (SPAs). + +* **`Routable` Enum:** Define routes as an enum deriving `Routable`. Each variant maps to a URL path using `#[at("/path")]`. `#[not_found]` designates the fallback route. + * **Path Segments:** Define dynamic segments with `:segment_name` (for single segments) or `*wildcard` (for multi-segment wildcards). These become fields in the enum variant. +* **`BrowserRouter`:** The main router component that manages browser history. All `<Switch />` and `<Link />` components must be descendants of `BrowserRouter`. +* **`Switch` Component:** Takes an `Routable` enum and a `render` function. The `render` function receives the matched route variant and returns the `Html` to display. +* **`Link` Component:** A component that renders an `<a>` tag but performs client-side navigation (`pushState`) instead of a full page reload. +* **Navigation API (`use_navigator()`):** Provides programmatic navigation via `navigator.push(&Route)` or `navigator.replace(&Route)`. +* **Query Parameters:** Can be included in navigation by passing a serializable struct/map to `push_with_query()` or retrieved from `location.query()`. +* **Nested Routers:** Allows for modular routing within sub-sections of the application, often using a `*` wildcard in the parent router to delegate to a child router. +* **Basename:** Configure a common prefix for all routes, useful when the application is served from a sub-path. + +### Web Workers and Agents + +Yew `Agents` are a mechanism for offloading tasks to Web Workers, enabling concurrent processing and preventing UI unresponsiveness. + +* **Types of Agents:** + * `Public`: A single instance shared across all bridges in a web worker. + * `Private`: A new instance spawned for each bridge connection. +* **Communication:** + * `Bridges`: Bi-directional communication between a component and an agent, or between agents. + * `Dispatchers`: Uni-directional communication from a component to an agent. +* **Overhead:** Agents introduce serialization/deserialization overhead for messages between threads (using `bincode`), making them suitable for chunky computations rather than frequent, small messages. + +## Deployment Considerations + +* **Release Build:** Use `trunk build --release` for optimized, production-ready builds. +* **Server Configuration:** + * **SPA Fallback:** Configure the HTTP server to serve `index.html` as a fallback for any unmatched URL paths, allowing the `yew-router` to handle client-side routing. + * **MIME-type:** Ensure `.wasm` files are served with the `application/wasm` MIME-type. +* **Relative Paths:** Use `<base data-trunk-public-url />` in `index.html` and `trunk build --public-url /your/path/` to deploy the app under a sub-path. +* **Environment Variables:** Use `std::env!("VAR_NAME")` to embed environment variables at **compile time** (runtime access in browser is not direct). + +## Debugging and Testing + +### Debugging + +* **Panics:** Yew automatically logs Rust panics to the browser's developer console. +* **Console Logging:** + * `wasm-logger`: Integrates Rust's `log` crate with the browser console. + * `gloo-console`: Provides `log!` macro for direct `JsValue` logging to console. + * `tracing-web`: Integrates `tracing` framework with browser console and performance API. +* **Component Lifecycles:** Use `tracing` to gain insights into component re-renders and hook execution. +* **Source Maps:** Limited support available; requires specific configuration. + +### Testing + +* **`wasm_bindgen_test`:** Enables running Rust tests directly in a browser environment. +* **Snapshot Testing:** Yew provides utilities (`yew::tests::layout_tests`) for snapshot testing component output. +* **Shallow Rendering:** Work in progress for testing components in isolation without rendering their full subtree. + +--- \ No newline at end of file diff --git a/env.sh b/env.sh new file mode 100644 index 0000000..e69de29 diff --git a/index.html b/index.html new file mode 100644 index 0000000..a3a7f5d --- /dev/null +++ b/index.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8" /> + <title>Trunk Template + + + + + + diff --git a/index.scss b/index.scss new file mode 100644 index 0000000..840ef34 --- /dev/null +++ b/index.scss @@ -0,0 +1,49 @@ +:root { + --bs-body-bg: #fff; + --bs-body-color: #212529; + --bs-navbar-bg: #f8f9fa; + --bs-navbar-color: rgba(0, 0, 0, 0.55); + --bs-navbar-active-color: rgba(0, 0, 0, 0.9); + --bs-jumbotron-bg: #e9ecef; + --bs-jumbotron-color: #212529; +} + +.dark-theme { + --bs-body-bg: #212529; + --bs-body-color: #f8f9fa; + --bs-navbar-bg: #343a40; + --bs-navbar-color: rgba(255, 255, 255, 0.55); + --bs-navbar-active-color: rgba(255, 255, 255, 0.9); + --bs-jumbotron-bg: #343a40; + --bs-jumbotron-color: #f8f9fa; +} + +body { + background-color: var(--bs-body-bg); + color: var(--bs-body-color); +} + +.navbar { + background-color: var(--bs-navbar-bg) !important; +} + +.navbar-light .navbar-nav .nav-link { + color: var(--bs-navbar-color); +} + +.navbar-light .navbar-nav .nav-link.active { + color: var(--bs-navbar-active-color); +} + +.navbar-dark .navbar-nav .nav-link { + color: var(--bs-navbar-color); +} + +.navbar-dark .navbar-nav .nav-link.active { + color: var(--bs-navbar-active-color); +} + +.jumbotron { + background-color: var(--bs-jumbotron-bg) !important; + color: var(--bs-jumbotron-color); +} \ No newline at end of file diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..feb4378 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,95 @@ +use yew::prelude::*; +use gloo_utils::document; +use gloo_storage::{LocalStorage, Storage}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)] +enum Theme { + Light, + Dark, +} + +impl Theme { + fn as_str(&self) -> &'static str { + match self { + Theme::Light => "light-theme", + Theme::Dark => "dark-theme", + } + } +} + +#[function_component(App)] +pub fn app() -> Html { + let theme = use_state(|| { + LocalStorage::get("theme").unwrap_or(Theme::Light) + }); + + let onclick_theme_toggle = { + let theme = theme.clone(); + Callback::from(move |_| { + let new_theme = if *theme == Theme::Light { Theme::Dark } else { Theme::Light }; + theme.set(new_theme); + }) + }; + + use_effect_with(theme.clone(), move |theme| { + let body = document().body().expect("body element not found"); + body.set_class_name(theme.as_str()); + LocalStorage::set("theme", **theme).unwrap(); + }); + + html! { + <> + + +
+
+

{"Welcome to Yew Bootstrap!"}

+

{"This is a simple Yew application demonstrating Bootstrap 5 integration with a navigation bar and a full-width hero section."}

+ +
+
+ +
+
+
+

{"Section 1"}

+

{"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."}

+
+
+

{"Section 2"}

+

{"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."}

+
+
+

{"Section 3"}

+

{"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur."}

+
+
+
+ + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..c526784 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,6 @@ +mod app; +use app::App; + +fn main() { + yew::Renderer::::new().render(); +} \ No newline at end of file diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..0f3ca8d --- /dev/null +++ b/start.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -ex + +cd "$(dirname "$0")" + +trunk serve --open -p 9999 \ No newline at end of file