Rework Guide (#486)

* Move getting started to Guide

* Clean up summary and getting started

* Shorten intro by moving stuff to individual platform docs

* Make intro even shorter, summarize features

* Further cleanup; move development-related docs to separate section

* Make `guide` a crate. Turn code samples into examples so that we can check if they compile

* Rewrite "Describing the UI":

* Focus on RSX syntax (interactivity covered in later chapters); make sure samples are tested; concise language

* Move some "special attribute" samples to the `examples` directory

* Simplify introduction to components

* Simplify introduction to component props

* Document Prop features; add code samples

* Simplify component children docs

* Interactivity: better introduction to events

* Hooks: better introduction

* Remove outdated doc

* Introducs use_ref

* Simplify User Input chapter

* Document event handler props

* Meme editor example

* Meme editor walkthrough

* Add dark mode example

* Guide for context; dark mode example

* Guide: custom hooks

* Guide: conditional rendering

* Guide: rendering lists

* Guide: rendering lists + keys

* Move remaining infor from Reference to guide

* Delete reference book
This commit is contained in:
Reinis Mazeiks authored and GitHub committed 2022-07-07 08:50:36 +00:00
1 parent 92a8b7c75f
commit 6c323e9fc5
152 files changed
+2827 -5052

No files matched your search

+1
View File
@@ -0,0 +1 @@
Some of these examples (e.g. web) cannot be run. The code samples are here mostly so that we can easily check that they compile using `cargo test`.
+71
View File
@@ -0,0 +1,71 @@
#![allow(non_snake_case, unused)]
//! This example shows what *not* to do
use std::collections::HashMap;
use dioxus::prelude::*;
fn main() {}
fn AntipatternNestedFragments(cx: Scope<()>) -> Element {
// ANCHOR: nested_fragments
// ❌ Don't unnecessarily nest fragments
let _ = cx.render(rsx!(
Fragment {
Fragment {
Fragment {
Fragment {
Fragment {
div { "Finally have a real node!" }
}
}
}
}
}
));
// ✅ Render shallow structures
cx.render(rsx!(
div { "Finally have a real node!" }
))
// ANCHOR_END: nested_fragments
}
#[derive(PartialEq, Props)]
struct NoKeysProps {
data: HashMap<u32, String>,
}
fn AntipatternNoKeys(cx: Scope<NoKeysProps>) -> Element {
// ANCHOR: iter_keys
let data: &HashMap<_, _> = &cx.props.data;
// ❌ No keys
cx.render(rsx! {
ul {
data.values().map(|value| rsx!(
li { "List item: {value}" }
))
}
});
// ❌ Using index as keys
cx.render(rsx! {
ul {
cx.props.data.values().enumerate().map(|(index, value)| rsx!(
li { key: "{index}", "List item: {value}" }
))
}
});
// ✅ Using unique IDs as keys:
cx.render(rsx! {
ul {
cx.props.data.iter().map(|(key, value)| rsx!(
li { key: "{key}", "List item: {value}" }
))
}
})
// ANCHOR_END: iter_keys
}
+17
View File
@@ -0,0 +1,17 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
fn main() {
dioxus::desktop::launch(App);
}
fn App(cx: Scope) -> Element {
// ANCHOR: boolean_attribute
cx.render(rsx! {
div {
hidden: "false",
"hello"
}
})
// ANCHOR_END: boolean_attribute
}
@@ -0,0 +1,27 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
fn main() {
dioxus::desktop::launch(App);
}
// ANCHOR: App
fn App(cx: Scope) -> Element {
let hello = "Hello Dioxus!";
cx.render(rsx!(TitleCard { title: hello }))
}
// ANCHOR_END: App
// ANCHOR: TitleCard
#[derive(Props)]
struct TitleCardProps<'a> {
title: &'a str,
}
fn TitleCard<'a>(cx: Scope<'a, TitleCardProps<'a>>) -> Element {
cx.render(rsx! {
h1 { "{cx.props.title}" }
})
}
// ANCHOR_END: TitleCard
+36
View File
@@ -0,0 +1,36 @@
// ANCHOR: all
#![allow(non_snake_case)]
use dioxus::prelude::*;
fn main() {
dioxus::desktop::launch(App);
}
fn App(cx: Scope) -> Element {
// ANCHOR: Clickable_usage
cx.render(rsx! {
Clickable {
href: "https://www.youtube.com/watch?v=C-M2hs3sXGo",
"How to " i {"not"} " be seen"
}
})
// ANCHOR_END: Clickable_usage
}
// ANCHOR: Clickable
#[derive(Props)]
struct ClickableProps<'a> {
href: &'a str,
children: Element<'a>,
}
fn Clickable<'a>(cx: Scope<'a, ClickableProps<'a>>) -> Element {
cx.render(rsx!(
a {
href: "{cx.props.href}",
class: "fancy-button",
&cx.props.children
}
))
}
// ANCHOR_END: Clickable
@@ -0,0 +1,37 @@
// ANCHOR: all
#![allow(non_snake_case, unused)]
use dioxus::prelude::*;
fn main() {
dioxus::desktop::launch(App);
}
fn App(cx: Scope) -> Element {
// ANCHOR: Clickable_usage
cx.render(rsx! {
Clickable {
href: "https://www.youtube.com/watch?v=C-M2hs3sXGo",
"How to " i {"not"} " be seen"
}
})
// ANCHOR_END: Clickable_usage
}
#[derive(Props)]
struct ClickableProps<'a> {
href: &'a str,
children: Element<'a>,
}
// ANCHOR: Clickable
fn Clickable<'a>(cx: Scope<'a, ClickableProps<'a>>) -> Element {
match cx.props.children {
Some(VNode::Text(_)) => {
todo!("render some stuff")
}
_ => {
todo!("render some other stuff")
}
}
}
// ANCHOR_END: Clickable
@@ -0,0 +1,36 @@
// ANCHOR: all
#![allow(non_snake_case)]
use dioxus::prelude::*;
fn main() {
dioxus::desktop::launch(App);
}
fn App(cx: Scope) -> Element {
// ANCHOR: Clickable_usage
cx.render(rsx! {
Clickable {
href: "https://www.youtube.com/watch?v=C-M2hs3sXGo",
body: cx.render(rsx!("How to " i {"not"} " be seen")),
}
})
// ANCHOR_END: Clickable_usage
}
// ANCHOR: Clickable
#[derive(Props)]
struct ClickableProps<'a> {
href: &'a str,
body: Element<'a>,
}
fn Clickable<'a>(cx: Scope<'a, ClickableProps<'a>>) -> Element {
cx.render(rsx!(
a {
href: "{cx.props.href}",
class: "fancy-button",
&cx.props.body
}
))
}
// ANCHOR_END: Clickable
@@ -0,0 +1,34 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
fn main() {
dioxus::desktop::launch(App);
}
// ANCHOR: App
fn App(cx: Scope) -> Element {
cx.render(rsx! {
Likes {
score: 42,
},
})
}
// ANCHOR_END: App
// ANCHOR: Likes
// Remember: Owned props must implement `PartialEq`!
#[derive(PartialEq, Props)]
struct LikesProps {
score: i32,
}
fn Likes(cx: Scope<LikesProps>) -> Element {
cx.render(rsx! {
div {
"This post has ",
b { "{cx.props.score}" },
" likes"
}
})
}
// ANCHOR_END: Likes
@@ -0,0 +1,113 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
fn main() {
dioxus::desktop::launch(App);
}
fn App(cx: Scope) -> Element {
cx.render(rsx! {
// ANCHOR: OptionalProps_usage
Title {
title: "Some Title",
},
Title {
title: "Some Title",
subtitle: "Some Subtitle",
},
// Providing an Option explicitly won't compile though:
// Title {
// title: "Some Title",
// subtitle: None,
// },
// ANCHOR_END: OptionalProps_usage
// ANCHOR: ExplicitOption_usage
ExplicitOption {
title: "Some Title",
subtitle: None,
},
ExplicitOption {
title: "Some Title",
subtitle: Some("Some Title"),
},
// This won't compile:
// ExplicitOption {
// title: "Some Title",
// },
// ANCHOR_END: ExplicitOption_usage
// ANCHOR: DefaultComponent_usage
DefaultComponent {
number: 5,
},
DefaultComponent {},
// ANCHOR_END: DefaultComponent_usage
// ANCHOR: IntoComponent_usage
IntoComponent {
string: "some &str",
},
// ANCHOR_END: IntoComponent_usage
})
}
// ANCHOR: OptionalProps
#[derive(Props)]
struct OptionalProps<'a> {
title: &'a str,
subtitle: Option<&'a str>,
}
fn Title<'a>(cx: Scope<'a, OptionalProps>) -> Element<'a> {
return cx.render(rsx!(h1{
"{cx.props.title}: ",
[cx.props.subtitle.unwrap_or("No subtitle provided")],
}));
}
// ANCHOR_END: OptionalProps
// ANCHOR: ExplicitOption
#[derive(Props)]
struct ExplicitOptionProps<'a> {
title: &'a str,
#[props(!optional)]
subtitle: Option<&'a str>,
}
fn ExplicitOption<'a>(cx: Scope<'a, ExplicitOptionProps>) -> Element<'a> {
return cx.render(rsx!(h1{
"{cx.props.title}: ",
[cx.props.subtitle.unwrap_or("No subtitle provided")],
}));
}
// ANCHOR_END: ExplicitOption
// ANCHOR: DefaultComponent
#[derive(PartialEq, Props)]
struct DefaultProps {
// default to 42 when not provided
#[props(default = 42)]
number: i64,
}
fn DefaultComponent(cx: Scope<DefaultProps>) -> Element {
return cx.render(rsx!(h1{
"{cx.props.number}",
}));
}
// ANCHOR_END: DefaultComponent
// ANCHOR: IntoComponent
#[derive(PartialEq, Props)]
struct IntoProps {
#[props(into)]
string: String,
}
fn IntoComponent(cx: Scope<IntoProps>) -> Element {
return cx.render(rsx!(h1{
"{cx.props.string}",
}));
}
// ANCHOR_END: IntoComponent
+24
View File
@@ -0,0 +1,24 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
fn main() {
dioxus::desktop::launch(App);
}
// ANCHOR: App
fn App(cx: Scope) -> Element {
cx.render(rsx! {
About {},
About {},
})
}
// ANCHOR_END: App
// ANCHOR: About
pub fn About(cx: Scope) -> Element {
cx.render(rsx!(p {
b {"Dioxus Labs"}
" An Open Source project dedicated to making Rust UI wonderful."
}))
}
// ANCHOR_END: About
@@ -0,0 +1,60 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
fn main() {
dioxus::desktop::launch(App);
}
pub fn App(cx: Scope) -> Element {
let is_logged_in = use_state(&cx, || false);
cx.render(rsx!(LogIn {
is_logged_in: **is_logged_in,
on_log_in: |_| is_logged_in.set(true),
on_log_out: |_| is_logged_in.set(false),
}))
}
#[inline_props]
fn LogIn<'a>(
cx: Scope<'a>,
is_logged_in: bool,
on_log_in: EventHandler<'a>,
on_log_out: EventHandler<'a>,
) -> Element<'a> {
// ANCHOR: if_else
if *is_logged_in {
cx.render(rsx! {
div {
"Welcome!",
button {
onclick: move |_| on_log_out.call(()),
"Log Out",
}
}
})
} else {
cx.render(rsx! {
button {
onclick: move |_| on_log_in.call(()),
"Log In",
}
})
}
// ANCHOR_END: if_else
}
#[inline_props]
fn LogInWarning(cx: Scope, is_logged_in: bool) -> Element {
// ANCHOR: conditional_none
if *is_logged_in {
return None;
}
cx.render(rsx! {
a {
"You must be logged in to comment"
}
})
// ANCHOR_END: conditional_none
}
@@ -0,0 +1,19 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
fn main() {
dioxus::desktop::launch(App);
}
fn App(cx: Scope) -> Element {
// ANCHOR: dangerous_inner_html
// this should come from a trusted source
let contents = "live <b>dangerously</b>";
cx.render(rsx! {
div {
dangerous_inner_html: "{contents}",
}
})
// ANCHOR_END: dangerous_inner_html
}
+17
View File
@@ -0,0 +1,17 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
fn main() {
dioxus::desktop::launch(App);
}
fn App(cx: Scope) -> Element {
// ANCHOR: rsx
cx.render(rsx! {
button {
onclick: move |event| println!("Clicked! Event: {event:?}"),
"click me!"
}
})
// ANCHOR_END: rsx
}
+33
View File
@@ -0,0 +1,33 @@
#![allow(non_snake_case)]
use dioxus::events::MouseEvent;
use dioxus::prelude::*;
fn main() {
dioxus::desktop::launch(App);
}
fn App(cx: Scope) -> Element {
// ANCHOR: usage
cx.render(rsx! {
FancyButton {
on_click: move |event| println!("Clicked! {event:?}")
}
})
// ANCHOR_END: usage
}
// ANCHOR: component_with_handler
#[derive(Props)]
pub struct FancyButtonProps<'a> {
on_click: EventHandler<'a, MouseEvent>,
}
pub fn FancyButton<'a>(cx: Scope<'a, FancyButtonProps<'a>>) -> Element<'a> {
cx.render(rsx!(button {
class: "fancy-button",
onclick: move |evt| cx.props.on_click.call(evt),
"click me pls."
}))
}
// ANCHOR_END: component_with_handler
+24
View File
@@ -0,0 +1,24 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
fn main() {
dioxus::desktop::launch(App);
}
fn App(cx: Scope) -> Element {
// ANCHOR: rsx
cx.render(rsx! {
div {
onclick: move |_event| {},
"outer",
button {
onclick: move |event| {
// now, outer won't be triggered
event.cancel_bubble();
},
"inner"
}
}
})
// ANCHOR_END: rsx
}
@@ -0,0 +1,17 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
fn main() {
dioxus::desktop::launch(App);
}
fn App(cx: Scope) -> Element {
// ANCHOR: prevent_default
cx.render(rsx! {
input {
prevent_default: "oninput",
prevent_default: "onclick",
}
})
// ANCHOR_END: prevent_default
}
@@ -0,0 +1,18 @@
// ANCHOR: all
#![allow(non_snake_case)]
use dioxus::prelude::*;
fn main() {
dioxus::desktop::launch(App);
}
// ANCHOR: component
fn App(cx: Scope) -> Element {
cx.render(rsx! {
div {
"Hello, world!"
}
})
}
// ANCHOR_END: component
// ANCHOR_END: all
+20
View File
@@ -0,0 +1,20 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
fn main() {
dioxus::tui::launch(App);
}
fn App(cx: Scope) -> Element {
cx.render(rsx! {
div {
width: "100%",
height: "10px",
background_color: "red",
justify_content: "center",
align_items: "center",
"Hello world!"
}
})
}
@@ -0,0 +1,36 @@
// todo remove deprecated
#![allow(non_snake_case, deprecated)]
use dioxus::events::{KeyCode, KeyboardEvent};
use dioxus::prelude::*;
use dioxus::tui::TuiContext;
fn main() {
dioxus::tui::launch_cfg(
App,
dioxus::tui::Config::new()
.without_ctrl_c_quit()
// Some older terminals only support 16 colors or ANSI colors
// If your terminal is one of these, change this to BaseColors or ANSI
.with_rendering_mode(dioxus::tui::RenderingMode::Rgb),
);
}
fn App(cx: Scope) -> Element {
let tui_ctx: TuiContext = cx.consume_context().unwrap();
cx.render(rsx! {
div {
width: "100%",
height: "10px",
background_color: "red",
justify_content: "center",
align_items: "center",
onkeydown: move |k: KeyboardEvent| if let KeyCode::Q = k.data.key_code {
tui_ctx.quit();
},
"Hello world!"
}
})
}
+14
View File
@@ -0,0 +1,14 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
fn main() {
dioxus::web::launch(App);
}
fn App(cx: Scope) -> Element {
cx.render(rsx! {
div {
"Hello, world!"
}
})
}
+67
View File
@@ -0,0 +1,67 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
use std::collections::HashMap;
fn main() {
dioxus::desktop::launch(App);
}
fn App(cx: Scope) -> Element {
let you_are_happy = true;
let you_know_it = false;
// ANCHOR: conditional
// ❌ don't call hooks in conditionals!
// We must ensure that the same hooks will be called every time
// But `if` statements only run if the conditional is true!
// So we might violate rule 2.
if you_are_happy && you_know_it {
let something = use_state(&cx, || "hands");
println!("clap your {something}")
}
// ✅ instead, *always* call use_state
// You can put other stuff in the conditional though
let something = use_state(&cx, || "hands");
if you_are_happy && you_know_it {
println!("clap your {something}")
}
// ANCHOR_END: conditional
// ANCHOR: closure
// ❌ don't call hooks inside closures!
// We can't guarantee that the closure, if used, will be called at the same time every time
let _a = || {
let b = use_state(&cx, || 0);
b.get()
};
// ✅ instead, move hook `b` outside
let b = use_state(&cx, || 0);
let _a = || b.get();
// ANCHOR_END: closure
let names: Vec<&str> = vec![];
// ANCHOR: loop
// `names` is a Vec<&str>
// ❌ Do not use hooks in loops!
// In this case, if the length of the Vec changes, we break rule 2
for _name in &names {
let is_selected = use_state(&cx, || false);
println!("selected: {is_selected}");
}
// ✅ Instead, use a hashmap with use_ref
let selection_map = use_ref(&cx, || HashMap::<&str, bool>::new());
for name in &names {
let is_selected = selection_map.read()[name];
println!("selected: {is_selected}");
}
// ANCHOR_END: loop
None
}
+13
View File
@@ -0,0 +1,13 @@
#![allow(unused)]
use dioxus::prelude::*;
fn main() {}
struct AppSettings {}
// ANCHOR: wrap_context
fn use_settings(cx: &ScopeState) -> UseSharedState<AppSettings> {
use_context::<AppSettings>(&cx).expect("App settings not provided")
}
// ANCHOR_END: wrap_context
+18
View File
@@ -0,0 +1,18 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
fn main() {
dioxus::desktop::launch(App);
}
// ANCHOR: component
fn App(cx: Scope) -> Element {
let mut count = use_state(&cx, || 0);
cx.render(rsx!(
h1 { "High-Five counter: {count}" }
button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
))
}
// ANCHOR_END: component
@@ -0,0 +1,24 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
fn main() {
dioxus::desktop::launch(App);
}
// ANCHOR: component
fn App(cx: Scope) -> Element {
// ANCHOR: use_state_calls
let mut count_a = use_state(&cx, || 0);
let mut count_b = use_state(&cx, || 0);
// ANCHOR_END: use_state_calls
cx.render(rsx!(
h1 { "Counter_a: {count_a}" }
button { onclick: move |_| count_a += 1, "a++" }
button { onclick: move |_| count_a -= 1, "a--" }
h1 { "Counter_b: {count_b}" }
button { onclick: move |_| count_b += 1, "b++" }
button { onclick: move |_| count_b -= 1, "b--" }
))
}
// ANCHOR_END: component
+23
View File
@@ -0,0 +1,23 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
fn main() {
dioxus::desktop::launch(App);
}
// ANCHOR: component
fn App(cx: Scope) -> Element {
let list = use_ref(&cx, || Vec::new());
let list_formatted = format!("{:?}", *list.read());
cx.render(rsx!(
p { "Current list: {list_formatted}" }
button {
onclick: move |event| {
list.write().push(event)
},
"Click me!"
}
))
}
// ANCHOR_END: component
+21
View File
@@ -0,0 +1,21 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
fn main() {
dioxus::desktop::launch(App);
}
// ANCHOR: component
fn App(cx: Scope) -> Element {
let name = use_state(&cx, || "bob".to_string());
cx.render(rsx! {
input {
// we tell the component what to render
value: "{name}",
// and what to do when the value changes
oninput: move |evt| name.set(evt.value.clone()),
}
})
}
// ANCHOR_END: component
+22
View File
@@ -0,0 +1,22 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
fn main() {
dioxus::desktop::launch(App);
}
// ANCHOR: component
fn App(cx: Scope) -> Element {
cx.render(rsx! {
form {
onsubmit: move |event| {
println!("Submitted! {event:?}")
},
input { name: "name", },
input { name: "age", },
input { name: "date", },
input { r#type: "submit", },
}
})
}
// ANCHOR_END: component
+105
View File
@@ -0,0 +1,105 @@
// ANCHOR: all
#![allow(non_snake_case)]
use dioxus::events::FormEvent;
use dioxus::prelude::*;
fn main() {
dioxus::desktop::launch(MemeEditor);
}
// ANCHOR: meme_editor
fn MemeEditor(cx: Scope) -> Element {
let container_style = r"
display: flex;
flex-direction: column;
gap: 16px;
margin: 0 auto;
width: fit-content;
";
let caption = use_state(&cx, || "me waiting for my rust code to compile".to_string());
cx.render(rsx! {
div {
style: "{container_style}",
h1 { "Meme Editor" },
Meme {
caption: caption,
},
CaptionEditor {
caption: caption,
on_input: move |event: FormEvent| {caption.set(event.value.clone());},
},
}
})
}
// ANCHOR_END: meme_editor
// ANCHOR: meme_component
#[inline_props]
fn Meme<'a>(cx: Scope<'a>, caption: &'a str) -> Element<'a> {
let container_style = r#"
position: relative;
width: fit-content;
"#;
let caption_container_style = r#"
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 16px 8px;
"#;
let caption_style = r"
font-size: 32px;
margin: 0;
color: white;
text-align: center;
";
cx.render(rsx!(
div {
style: "{container_style}",
img {
src: "https://i.imgflip.com/2zh47r.jpg",
height: "500px",
},
div {
style: "{caption_container_style}",
p {
style: "{caption_style}",
"{caption}"
}
}
}
))
}
// ANCHOR_END: meme_component
// ANCHOR: caption_editor
#[inline_props]
fn CaptionEditor<'a>(
cx: Scope<'a>,
caption: &'a str,
on_input: EventHandler<'a, FormEvent>,
) -> Element<'a> {
let input_style = r"
border: none;
background: cornflowerblue;
padding: 8px 16px;
margin: 0;
border-radius: 4px;
color: white;
";
cx.render(rsx!(input {
style: "{input_style}",
value: "{caption}",
oninput: move |event| on_input.call(event),
}))
}
// ANCHOR_END: caption_editor
// ANCHOR_END: all
@@ -0,0 +1,184 @@
// ANCHOR: all
#![allow(non_snake_case)]
use dioxus::events::FormEvent;
use dioxus::prelude::*;
fn main() {
dioxus::desktop::launch(App);
}
// ANCHOR: DarkMode_struct
struct DarkMode(bool);
// ANCHOR_END: DarkMode_struct
pub fn App(cx: Scope) -> Element {
// ANCHOR: context_provider
use_context_provider(&cx, || DarkMode(false));
// ANCHOR_END: context_provider
let is_dark_mode = use_is_dark_mode(&cx);
let wrapper_style = if is_dark_mode {
r"
background: #222;
min-height: 100vh;
"
} else {
r""
};
cx.render(rsx!(div {
style: "{wrapper_style}",
DarkModeToggle {},
MemeEditor {},
}))
}
pub fn use_is_dark_mode(cx: &ScopeState) -> bool {
// ANCHOR: use_context
let dark_mode_context = use_context::<DarkMode>(&cx);
// ANCHOR_END: use_context
dark_mode_context
.map(|context| context.read().0)
.unwrap_or(false)
}
// ANCHOR: toggle
pub fn DarkModeToggle(cx: Scope) -> Element {
let dark_mode = use_context::<DarkMode>(&cx)?;
let style = if dark_mode.read().0 {
"color:white"
} else {
""
};
cx.render(rsx!(label {
style: "{style}",
"Dark Mode",
input {
r#type: "checkbox",
oninput: move |event| {
let is_enabled = event.value == "true";
dark_mode.write().0 = is_enabled;
},
},
}))
}
// ANCHOR_END: toggle
// ANCHOR: meme_editor
fn MemeEditor(cx: Scope) -> Element {
let is_dark_mode = use_is_dark_mode(&cx);
let heading_style = if is_dark_mode { "color: white" } else { "" };
let container_style = r"
display: flex;
flex-direction: column;
gap: 16px;
margin: 0 auto;
width: fit-content;
";
let caption = use_state(&cx, || "me waiting for my rust code to compile".to_string());
cx.render(rsx! {
div {
style: "{container_style}",
h1 {
style: "{heading_style}",
"Meme Editor"
},
Meme {
caption: caption,
},
CaptionEditor {
caption: caption,
on_input: move |event: FormEvent| {caption.set(event.value.clone());},
},
}
})
}
// ANCHOR_END: meme_editor
// ANCHOR: meme_component
#[inline_props]
fn Meme<'a>(cx: Scope<'a>, caption: &'a str) -> Element<'a> {
let container_style = r"
position: relative;
width: fit-content;
";
let caption_container_style = r"
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 16px 8px;
";
let caption_style = r"
font-size: 32px;
margin: 0;
color: white;
text-align: center;
";
cx.render(rsx!(
div {
style: "{container_style}",
img {
src: "https://i.imgflip.com/2zh47r.jpg",
height: "500px",
},
div {
style: "{caption_container_style}",
p {
style: "{caption_style}",
"{caption}"
}
}
}
))
}
// ANCHOR_END: meme_component
// ANCHOR: caption_editor
#[inline_props]
fn CaptionEditor<'a>(
cx: Scope<'a>,
caption: &'a str,
on_input: EventHandler<'a, FormEvent>,
) -> Element<'a> {
let is_dark_mode = use_is_dark_mode(&cx);
let colors = if is_dark_mode {
r"
background: cornflowerblue;
color: white;
"
} else {
r"
background: #def;
color: black;
"
};
let input_style = r"
border: none;
padding: 8px 16px;
margin: 0;
border-radius: 4px;
";
cx.render(rsx!(input {
style: "{input_style}{colors}",
value: "{caption}",
oninput: move |event| on_input.call(event),
}))
}
// ANCHOR_END: caption_editor
// ANCHOR_END: all
+60
View File
@@ -0,0 +1,60 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
fn main() {
dioxus::desktop::launch(App);
}
#[derive(PartialEq, Clone)]
struct Comment {
content: String,
id: usize,
}
pub fn App(cx: Scope) -> Element {
// ANCHOR: render_list
let comment_field = use_state(&cx, || String::new());
let mut next_id = use_state(&cx, || 0);
let comments = use_ref(&cx, || Vec::<Comment>::new());
let comments_lock = comments.read();
let comments_rendered = comments_lock.iter().map(|comment| {
cx.render(rsx!(CommentComponent {
key: "{comment.id}",
comment: comment.clone(),
}))
});
cx.render(rsx!(
form {
onsubmit: move |_| {
comments.write().push(Comment {
content: comment_field.get().clone(),
id: *next_id.get(),
});
next_id += 1;
comment_field.set(String::new());
},
input {
value: "{comment_field}",
oninput: |event| comment_field.set(event.value.clone()),
}
input {
r#type: "submit",
}
},
comments_rendered,
))
// ANCHOR_END: render_list
}
#[inline_props]
fn CommentComponent(cx: Scope, comment: Comment) -> Element {
cx.render(rsx!(div {
"Comment by anon:",
p { "{comment.content}" }
button { "Reply" },
}))
}
+109
View File
@@ -0,0 +1,109 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
fn main() {
dioxus::desktop::launch(App);
}
pub fn App(cx: Scope) -> Element {
cx.render(rsx!(
Empty {},
Children {},
Fragments {},
Attributes {},
VariableAttributes {},
CustomAttributes {},
Formatting {},
Expression {},
))
}
pub fn Empty(cx: Scope) -> Element {
// ANCHOR: empty
cx.render(rsx!(div {}))
// ANCHOR_END: empty
}
pub fn Children(cx: Scope) -> Element {
// ANCHOR: children
cx.render(rsx!(ol {
li {"First Item"}
li {"Second Item"}
li {"Third Item"}
}))
// ANCHOR_END: children
}
pub fn Fragments(cx: Scope) -> Element {
// ANCHOR: fragments
cx.render(rsx!(
p {"First Item"},
p {"Second Item"},
Fragment {
span { "a group" },
span { "of three" },
span { "items" },
}
))
// ANCHOR_END: fragments
}
pub fn Attributes(cx: Scope) -> Element {
// ANCHOR: attributes
cx.render(rsx!(a {
href: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
class: "primary_button",
"Log In"
}))
// ANCHOR_END: attributes
}
pub fn VariableAttributes(cx: Scope) -> Element {
// ANCHOR: variable_attributes
let written_in_rust = true;
let button_type = "button";
cx.render(rsx!(button {
disabled: "{written_in_rust}",
class: "{button_type}",
"Rewrite it in rust"
}))
// ANCHOR_END: variable_attributes
}
pub fn CustomAttributes(cx: Scope) -> Element {
// ANCHOR: custom_attributes
cx.render(rsx!(b {
"customAttribute": "value",
"Rust is Cool"
}))
// ANCHOR_END: custom_attributes
}
pub fn Formatting(cx: Scope) -> Element {
// ANCHOR: formatting
let coordinates = (42, 0);
let country = "es";
cx.render(rsx!(div {
class: "country-{country}",
"Coordinates: {coordinates:?}",
// arbitrary expressions are allowed,
// as long as they don't contain `{}`
div {
"{country.to_uppercase()}"
},
div {
"{7*6}"
},
}))
// ANCHOR_END: formatting
}
pub fn Expression(cx: Scope) -> Element {
// ANCHOR: expression
let text = "Dioxus";
cx.render(rsx!(span {
[text.to_uppercase()]
}))
// ANCHOR_END: expression
}
+84
View File
@@ -0,0 +1,84 @@
#![allow(non_snake_case, unused)]
use dioxus::prelude::*;
fn main() {
dioxus::desktop::launch(App);
}
fn App(cx: Scope) -> Element {
// ANCHOR: spawn
let logged_in = use_state(&cx, || false);
let log_in = move |_| {
cx.spawn({
let logged_in = logged_in.to_owned();
async move {
let resp = reqwest::Client::new()
.post("http://example.com/login")
.send()
.await;
match resp {
Ok(_data) => {
println!("Login successful!");
logged_in.set(true);
}
Err(_err) => {
println!(
"Login failed - you need a login server running on localhost:8080."
)
}
}
}
});
};
cx.render(rsx! {
button {
onclick: log_in,
"Login",
}
})
// ANCHOR_END: spawn
}
pub fn Tokio(cx: Scope) -> Element {
let _ = || {
// ANCHOR: tokio
cx.spawn(async {
let _ = tokio::spawn(async {}).await;
let _ = tokio::task::spawn_local(async {
// some !Send work
})
.await;
});
// ANCHOR_END: tokio
};
None
}
pub fn ToOwnedMacro(cx: Scope) -> Element {
let count = use_state(&cx, || 0);
let age = use_state(&cx, || 0);
let name = use_state(&cx, || 0);
let description = use_state(&cx, || 0);
let _ = || {
// ANCHOR: to_owned_macro
use dioxus::core::to_owned;
cx.spawn({
to_owned![count, age, name, description];
async move {
// ...
}
});
// ANCHOR_END: to_owned_macro
};
None
}
+60
View File
@@ -0,0 +1,60 @@
#![allow(non_snake_case, unused)]
use dioxus::prelude::*;
fn main() {
dioxus::desktop::launch(App);
}
#[derive(serde::Deserialize, Debug)]
struct ApiResponse {
#[serde(rename = "message")]
image_url: String,
}
fn App(cx: Scope) -> Element {
// ANCHOR: use_future
let future = use_future(&cx, (), |_| async move {
reqwest::get("https://dog.ceo/api/breeds/image/random")
.await
.unwrap()
.json::<ApiResponse>()
.await
});
// ANCHOR_END: use_future
// ANCHOR: render
cx.render(match future.value() {
Some(Ok(response)) => rsx! {
button {
onclick: move |_| future.restart(),
"Click to fetch another doggo"
}
div {
img {
max_width: "500px",
max_height: "500px",
src: "{response.image_url}",
}
}
},
Some(Err(_)) => rsx! { div { "Loading dogs failed" } },
None => rsx! { div { "Loading dogs..." } },
})
// ANCHOR_END: render
}
#[inline_props]
fn RandomDog(cx: Scope, breed: String) -> Element {
// ANCHOR: dependency
let future = use_future(&cx, (breed,), |(breed,)| async move {
reqwest::get(format!("https://dog.ceo/api/breed/{breed}/images/random"))
.await
.unwrap()
.json::<ApiResponse>()
.await
});
// ANCHOR_END: dependency
None
}