awesome: arbitrary expressions excepted without braces

This commit is contained in:
Jonathan Kelley committed 2021-12-30 03:14:47 -05:00
1 parent 14961023f9
commit 4c85bcfdc8
40 files changed
+825 -767

No files matched your search

+7 -1
View File
@@ -79,7 +79,9 @@ If you know React, then you already know Dioxus.
<tr> <tr>
</table> </table>
## Examples:
## Examples Projects:
| File Navigator (Desktop) | WiFi scanner (Desktop) | TodoMVC (All platforms) | Ecommerce w/ Tailwind (Liveview) | | File Navigator (Desktop) | WiFi scanner (Desktop) | TodoMVC (All platforms) | Ecommerce w/ Tailwind (Liveview) |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
@@ -88,6 +90,10 @@ If you know React, then you already know Dioxus.
See the awesome-dioxus page for a curated list of content in the Dioxus Ecosystem. See the awesome-dioxus page for a curated list of content in the Dioxus Ecosystem.
## Running examples locally
All local examples are built for the desktop renderer. This means you can simply clone this repo and call `cargo run --example EXAMPLE_NAME`. To run non-desktop examples, checkout the example projects shown above.
## Why Dioxus and why Rust? ## Why Dioxus and why Rust?
TypeScript is a fantastic addition to JavaScript, but it's still fundamentally JavaScript. TS code runs slightly slower, has tons of configuration options, and not every package is properly typed. TypeScript is a fantastic addition to JavaScript, but it's still fundamentally JavaScript. TS code runs slightly slower, has tons of configuration options, and not every package is properly typed.
+8 -16
View File
@@ -42,14 +42,6 @@ These examples are not necessarily meant to be run, but rather serve as a refere
| [Complete rsx reference](./rsx_usage.rs) | A complete reference for all rsx! usage | ✅ | | [Complete rsx reference](./rsx_usage.rs) | A complete reference for all rsx! usage | ✅ |
| [Event Listeners](./listener.rs) | Attach closures to events on elements | ✅ | | [Event Listeners](./listener.rs) | Attach closures to events on elements | ✅ |
These web-specific examples must be run with `dioxus-cli` using `dioxus develop --example XYZ`
| Example | What it does |
| ------- | ------------ |
| asd | this does |
| asd | this does |
## Show me some examples! ## Show me some examples!
@@ -75,9 +67,9 @@ fn Toggle(cx: Scope<ToggleProps>) -> Element {
let mut toggled = use_state(&cx, || false); let mut toggled = use_state(&cx, || false);
cx.render(rsx!{ cx.render(rsx!{
div { div {
{&cx.props.children} &cx.props.children
button { onclick: move |_| toggled.set(true), button { onclick: move |_| toggled.set(true),
{toggled.and_then(|| "On").or_else(|| "Off")} toggled.and_then(|| "On").or_else(|| "Off")
} }
} }
}) })
@@ -112,8 +104,8 @@ fn App(cx: Scope) -> Element {
if should_show { if should_show {
cx.render(rsx!( cx.render(rsx!(
{title} title,
ul { {list} } ul { list }
)) ))
} else { } else {
None None
@@ -169,10 +161,10 @@ enum Route {
fn App(cx: Scope) -> Element { fn App(cx: Scope) -> Element {
let route = use_router(cx, Route::parse); let route = use_router(cx, Route::parse);
cx.render(rsx!(div { cx.render(rsx!(div {
{match route { match route {
Route::Home => rsx!( Home {} ), Route::Home => rsx!( Home {} ),
Route::Post(id) => rsx!( Post { id: id }) Route::Post(id) => rsx!( Post { id: id })
}} }
})) }))
} }
``` ```
@@ -187,8 +179,8 @@ fn App(cx: Scope) -> Element {
cx.render(rsx!{ cx.render(rsx!{
div { div {
"One doggo coming right up:" "One doggo coming right up:",
{doggo} doggo
} }
}) })
} }
+4 -6
View File
@@ -26,20 +26,18 @@ fn app(cx: Scope) -> Element {
rsx!(cx, div { rsx!(cx, div {
h1 {"count is {count}"} h1 {"count is {count}"}
button { button { onclick: move |_| task.stop(),
"Stop counting" "Stop counting"
onclick: move |_| task.stop()
} }
button { button { onclick: move |_| task.resume(),
"Start counting" "Start counting"
onclick: move |_| task.resume()
} }
button { button {
"Switch counting direcion"
onclick: move |_| { onclick: move |_| {
*direction.modify() *= -1; *direction.modify() *= -1;
task.restart(); task.restart();
} },
"Switch counting direcion"
} }
}) })
} }
+1 -1
View File
@@ -21,7 +21,7 @@ fn main() {
} }
fn App(cx: Scope) -> Element { fn App(cx: Scope) -> Element {
let text: &mut Vec<String> = cx.use_hook(|_| vec![String::from("abc=def")], |f| f); let text = cx.use_hook(|_| vec![String::from("abc=def")], |f| f);
let first = text.get_mut(0).unwrap(); let first = text.get_mut(0).unwrap();
+53 -26
View File
@@ -24,7 +24,9 @@ fn app(cx: Scope) -> Element {
let input_digit = move |num: u8| display_value.modify().push_str(num.to_string().as_str()); let input_digit = move |num: u8| display_value.modify().push_str(num.to_string().as_str());
cx.render(rsx!( cx.render(rsx!(
div { class: "calculator", style { [include_str!("./assets/calculator.css")] }
div {
class: "calculator",
onkeydown: move |evt| match evt.key_code { onkeydown: move |evt| match evt.key_code {
KeyCode::Add => operator.set(Some("+")), KeyCode::Add => operator.set(Some("+")),
KeyCode::Subtract => operator.set(Some("-")), KeyCode::Subtract => operator.set(Some("-")),
@@ -46,12 +48,11 @@ fn app(cx: Scope) -> Element {
} }
} }
_ => {} _ => {}
} },
div { class: "calculator-display", {[cur_val.separated_string()]} } div { class: "calculator-display", [cur_val.separated_string()] }
div { class: "input-keys" div { class: "input-keys",
div { class: "function-keys" div { class: "function-keys",
CalculatorKey { CalculatorKey {
{[if display_value == "0" { "C" } else { "AC" }]}
name: "key-clear", name: "key-clear",
onclick: move |_| { onclick: move |_| {
display_value.set("0".to_string()); display_value.set("0".to_string());
@@ -59,10 +60,10 @@ fn app(cx: Scope) -> Element {
operator.set(None); operator.set(None);
cur_val.set(0.0); cur_val.set(0.0);
} }
} },
[if display_value == "0" { "C" } else { "AC" }]
} }
CalculatorKey { CalculatorKey {
"±"
name: "key-sign", name: "key-sign",
onclick: move |_| { onclick: move |_| {
if display_value.starts_with("-") { if display_value.starts_with("-") {
@@ -71,28 +72,53 @@ fn app(cx: Scope) -> Element {
display_value.set(format!("-{}", *display_value)) display_value.set(format!("-{}", *display_value))
} }
}, },
"±"
} }
CalculatorKey { CalculatorKey {
"%" onclick: toggle_percent,
onclick: {toggle_percent}
name: "key-percent", name: "key-percent",
"%"
} }
} }
div { class: "digit-keys" div { class: "digit-keys",
CalculatorKey { name: "key-0", onclick: move |_| input_digit(0), "0" } CalculatorKey { name: "key-0", onclick: move |_| input_digit(0),
CalculatorKey { name: "key-dot", onclick: move |_| display_value.modify().push_str("."), "●" } "0"
}
{(1..9).map(|k| rsx!{ CalculatorKey { name: "key-dot", onclick: move |_| display_value.modify().push_str("."),
CalculatorKey { key: "{k}", name: "key-{k}", onclick: move |_| input_digit(k), "{k}" } "●"
})} }
(1..9).map(|k| rsx!{
CalculatorKey {
key: "{k}",
name: "key-{k}",
onclick: move |_| input_digit(k),
"{k}"
}
}),
} }
div { class: "operator-keys"
CalculatorKey { name: "key-divide", onclick: move |_| operator.set(Some("/")) "÷" } div { class: "operator-keys",
CalculatorKey { name: "key-multiply", onclick: move |_| operator.set(Some("*")) "×" } CalculatorKey {
CalculatorKey { name: "key-subtract", onclick: move |_| operator.set(Some("-")) "−" } name: "key-divide",
CalculatorKey { name: "key-add", onclick: move |_| operator.set(Some("+")) "+" } onclick: move |_| operator.set(Some("/")),
"÷"
}
CalculatorKey {
name: "key-multiply",
onclick: move |_| operator.set(Some("*")),
"×"
}
CalculatorKey {
name: "key-subtract",
onclick: move |_| operator.set(Some("-")),
"−"
}
CalculatorKey {
name: "key-add",
onclick: move |_| operator.set(Some("+")),
"+"
}
CalculatorKey { CalculatorKey {
"="
name: "key-equals", name: "key-equals",
onclick: move |_| { onclick: move |_| {
if let Some(op) = operator.as_ref() { if let Some(op) = operator.as_ref() {
@@ -109,6 +135,7 @@ fn app(cx: Scope) -> Element {
operator.set(None); operator.set(None);
} }
}, },
"="
} }
} }
} }
@@ -125,9 +152,9 @@ fn CalculatorKey<'a>(
) -> Element { ) -> Element {
cx.render(rsx! { cx.render(rsx! {
button { button {
class: "calculator-key {name}" class: "calculator-key {name}",
onclick: {onclick} onclick: onclick,
{children} children
} }
}) })
} }
+2 -2
View File
@@ -20,11 +20,11 @@ pub static Example: Component = |cx| {
li { onclick: move |_| example_data.set(f) li { onclick: move |_| example_data.set(f)
"ID: {f}" "ID: {f}"
ul { ul {
{(0..10).map(|k| rsx!{ (0..10).map(|k| rsx!{
li { li {
"Sub iterator: {f}.{k}" "Sub iterator: {f}.{k}"
} }
})} })
} }
} }
} }
+95 -82
View File
@@ -4,7 +4,7 @@ Tiny CRM: A port of the Yew CRM example to Dioxus.
use dioxus::prelude::*; use dioxus::prelude::*;
fn main() { fn main() {
dioxus::web::launch(App); dioxus::desktop::launch(app);
} }
enum Scene { enum Scene {
ClientsList, ClientsList,
@@ -19,88 +19,101 @@ pub struct Client {
pub description: String, pub description: String,
} }
static App: Component = |cx| { fn app(cx: Scope) -> Element {
let mut clients = use_ref(&cx, || vec![] as Vec<Client>); let clients = use_ref(&cx, || vec![] as Vec<Client>);
let mut scene = use_state(&cx, || Scene::ClientsList);
let mut firstname = use_state(&cx, || String::new()); let scene = use_state(&cx, || Scene::ClientsList);
let mut lastname = use_state(&cx, || String::new()); let firstname = use_state(&cx, String::new);
let mut description = use_state(&cx, || String::new()); let lastname = use_state(&cx, String::new);
let description = use_state(&cx, String::new);
let scene = match *scene {
Scene::ClientsList => {
rsx!(cx, div { class: "crm"
h2 { "List of clients" margin_bottom: "10px" }
div { class: "clients" margin_left: "10px"
{clients.read().iter().map(|client| rsx!(
div { class: "client" style: "margin-bottom: 50px"
p { "First Name: {client.first_name}" }
p { "Last Name: {client.last_name}" }
p {"Description: {client.description}"}
})
)}
}
button { class: "pure-button pure-button-primary" onclick: move |_| scene.set(Scene::NewClientForm), "Add New" }
button { class: "pure-button" onclick: move |_| scene.set(Scene::Settings), "Settings" }
})
}
Scene::NewClientForm => {
let add_new = move |_| {
clients.write().push(Client {
description: (*description).clone(),
first_name: (*firstname).clone(),
last_name: (*lastname).clone(),
});
description.set(String::new());
firstname.set(String::new());
lastname.set(String::new());
};
rsx!(cx, div { class: "crm"
h2 {"Add new client" margin_bottom: "10px" }
form { class: "pure-form"
input { class: "new-client firstname" placeholder: "First name" value: "{firstname}"
oninput: move |e| firstname.set(e.value.clone())
}
input { class: "new-client lastname" placeholder: "Last name" value: "{lastname}"
oninput: move |e| lastname.set(e.value.clone())
}
textarea { class: "new-client description" placeholder: "Description" value: "{description}"
oninput: move |e| description.set(e.value.clone())
}
}
button { class: "pure-button pure-button-primary", onclick: {add_new}, "Add New" }
button { class: "pure-button", onclick: move |_| scene.set(Scene::ClientsList), "Go Back" }
})
}
Scene::Settings => {
rsx!(cx, div {
h2 {"Settings" margin_bottom: "10px" }
button {
background: "rgb(202, 60, 60)"
class: "pure-button pure-button-primary"
onclick: move |_| clients.write().clear(),
"Remove all clients"
}
button {
class: "pure-button pure-button-primary"
onclick: move |_| scene.set(Scene::ClientsList),
"Go Back"
}
})
}
};
cx.render(rsx!( cx.render(rsx!(
body { body { margin_left: "35%",
link { link {
rel: "stylesheet" rel: "stylesheet",
href: "https://unpkg.com/purecss@2.0.6/build/pure-min.css" href: "https://unpkg.com/purecss@2.0.6/build/pure-min.css",
integrity: "sha384-Uu6IeWbM+gzNVXJcM9XV3SohHtmWE+3VGi496jvgX1jyvDTXfdK+rfZc8C1Aehk5" integrity: "sha384-Uu6IeWbM+gzNVXJcM9XV3SohHtmWE+3VGi496jvgX1jyvDTXfdK+rfZc8C1Aehk5",
crossorigin: "anonymous" crossorigin: "anonymous",
} }
margin_left: "35%"
h1 {"Dioxus CRM Example"} h1 {"Dioxus CRM Example"}
{scene}
} match *scene {
Scene::ClientsList => rsx!(
div { class: "crm",
h2 { margin_bottom: "10px", "List of clients" }
div { class: "clients", margin_left: "10px",
clients.read().iter().map(|client| rsx!(
div { class: "client", style: "margin-bottom: 50px",
p { "First Name: {client.first_name}" }
p { "Last Name: {client.last_name}" }
p {"Description: {client.description}"}
})
)
}
button { class: "pure-button pure-button-primary", onclick: move |_| scene.set(Scene::NewClientForm), "Add New" }
button { class: "pure-button", onclick: move |_| scene.set(Scene::Settings), "Settings" }
}
),
Scene::NewClientForm => rsx!(
div { class: "crm",
h2 { margin_bottom: "10px", "Add new client" }
form { class: "pure-form",
input {
class: "new-client firstname",
placeholder: "First name",
value: "{firstname}",
oninput: move |e| firstname.set(e.value.clone())
}
input {
class: "new-client lastname",
placeholder: "Last name",
value: "{lastname}",
oninput: move |e| lastname.set(e.value.clone())
}
textarea {
class: "new-client description",
placeholder: "Description",
value: "{description}",
oninput: move |e| description.set(e.value.clone())
}
}
button {
class: "pure-button pure-button-primary",
onclick: move |_| {
clients.write().push(Client {
description: (*description).clone(),
first_name: (*firstname).clone(),
last_name: (*lastname).clone(),
});
description.set(String::new());
firstname.set(String::new());
lastname.set(String::new());
},
"Add New"
}
button { class: "pure-button", onclick: move |_| scene.set(Scene::ClientsList),
"Go Back"
}
}
),
Scene::Settings => rsx!(
div {
h2 { margin_bottom: "10px", "Settings" }
button {
background: "rgb(202, 60, 60)",
class: "pure-button pure-button-primary",
onclick: move |_| clients.write().clear(),
"Remove all clients"
}
button {
class: "pure-button pure-button-primary",
onclick: move |_| scene.set(Scene::ClientsList),
"Go Back"
}
}
)
}
}
)) ))
}; }
+1 -1
View File
@@ -13,6 +13,6 @@ static App: Component = |cx| {
div { div {
"hello world!" "hello world!"
} }
{(0..10).map(|f| rsx!( div {"abc {f}"}))} (0..10).map(|f| rsx!( div {"abc {f}"}))
)) ))
}; };
-8
View File
@@ -1,8 +0,0 @@
use dioxus_core as dioxus;
use dioxus_core::prelude::*;
use dioxus_core_macro::*;
use dioxus_hooks::*;
use dioxus_html as dioxus_elements;
fn main() {}
-1
View File
@@ -1 +0,0 @@
fn main() {}
+7 -7
View File
@@ -98,24 +98,24 @@ pub static App: Component = |cx| {
} }
} }
} }
ul { class: "todo-list" ul { class: "todo-list",
{filtered_todos.iter().map(|id| rsx!(TodoEntry { key: "{id}", id: *id }))} filtered_todos.iter().map(|id| rsx!(TodoEntry { key: "{id}", id: *id }))
} }
{(!todos.read().is_empty()).then(|| rsx!( (!todos.read().is_empty()).then(|| rsx!(
footer { class: "footer" footer { class: "footer",
span { class: "todo-count" strong {"{items_left} "} span {"{item_text} left"} } span { class: "todo-count" strong {"{items_left} "} span {"{item_text} left"} }
ul { class: "filters" ul { class: "filters"
li { class: "All", a { onclick: move |_| filter.set(FilterState::All), "All" }} li { class: "All", a { onclick: move |_| filter.set(FilterState::All), "All" }}
li { class: "Active", a { onclick: move |_| filter.set(FilterState::Active), "Active" }} li { class: "Active", a { onclick: move |_| filter.set(FilterState::Active), "Active" }}
li { class: "Completed", a { onclick: move |_| filter.set(FilterState::Completed), "Completed" }} li { class: "Completed", a { onclick: move |_| filter.set(FilterState::Completed), "Completed" }}
} }
{(show_clear_completed).then(|| rsx!( (show_clear_completed).then(|| rsx!(
button { class: "clear-completed", onclick: move |_| clear_completed(), button { class: "clear-completed", onclick: move |_| clear_completed(),
"Clear completed" "Clear completed"
} }
))} ))
} }
))} ))
} }
} }
footer { class: "info" footer { class: "info"
+30 -43
View File
@@ -4,50 +4,45 @@
//! This is a fun little desktop application that lets you explore the file system. //! This is a fun little desktop application that lets you explore the file system.
//! //!
//! This example is interesting because it's mixing filesystem operations and GUI, which is typically hard for UI to do. //! This example is interesting because it's mixing filesystem operations and GUI, which is typically hard for UI to do.
//!
//! It also uses `use_ref` to maintain a model, rather than `use_state`. That way,
//! we dont need to clutter our code with `read` commands.
use dioxus::prelude::*; use dioxus::prelude::*;
fn main() { fn main() {
dioxus::desktop::launch_cfg(App, |c| { dioxus::desktop::launch_cfg(app, |c| {
c.with_window(|w| { c.with_window(|w| {
w.with_resizable(true).with_inner_size( w.with_resizable(true)
dioxus::desktop::wry::application::dpi::LogicalSize::new(400.0, 800.0), .with_inner_size(dioxus::desktop::tao::dpi::LogicalSize::new(400.0, 800.0))
)
}) })
}); });
} }
static App: Component = |cx| { fn app(cx: Scope) -> Element {
let file_manager = use_ref(&cx, Files::new); let files = use_ref(&cx, Files::new);
let files = file_manager.read();
let file_list = files.path_names.iter().enumerate().map(|(dir_id, path)| {
rsx! (
li { a {"{path}", onclick: move |_| file_manager.write().enter_dir(dir_id), href: "#"} }
)
});
let err_disp = files.err.as_ref().map(|err| {
rsx! (
div {
code {"{err}"}
button {"x", onclick: move |_| file_manager.write().clear_err() }
}
)
});
let current_dir = files.current();
cx.render(rsx!( cx.render(rsx!(
div { h1 { "Files: " }
h1 {"Files: "} h3 { "Cur dir: " [files.read().current()] }
h3 {"Cur dir: {current_dir}"} button { onclick: move |_| files.write().go_up(), "go up" }
button { "go up", onclick: move |_| file_manager.write().go_up() } ol {
ol { {file_list} } files.read().path_names.iter().enumerate().map(|(dir_id, path)| rsx!(
{err_disp} li { key: "{path}",
a { href: "#", onclick: move |_| files.write().enter_dir(dir_id),
"{path}",
}
}
))
} }
files.read().err.as_ref().map(|err| rsx!(
div {
code { "{err}" }
button { onclick: move |_| files.write().clear_err(), "x" }
}
))
)) ))
}; }
struct Files { struct Files {
path_stack: Vec<String>, path_stack: Vec<String>,
@@ -69,29 +64,21 @@ impl Files {
} }
fn reload_path_list(&mut self) { fn reload_path_list(&mut self) {
let cur_path = self.path_stack.last().unwrap(); let paths = match std::fs::read_dir(self.path_stack.last().unwrap()) {
log::info!("Reloading path list for {:?}", cur_path);
let paths = match std::fs::read_dir(cur_path) {
Ok(e) => e, Ok(e) => e,
Err(err) => { Err(err) => {
let err = format!("An error occured: {:?}", err); self.err = Some(format!("An error occured: {:?}", err));
self.err = Some(err);
self.path_stack.pop(); self.path_stack.pop();
return; return;
} }
}; };
let collected = paths.collect::<Vec<_>>();
log::info!("Path list reloaded {:#?}", collected);
// clear the current state // clear the current state
self.clear_err(); self.clear_err();
self.path_names.clear(); self.path_names.clear();
for path in collected { self.path_names
self.path_names .extend(paths.map(|path| path.unwrap().path().display().to_string()));
.push(path.unwrap().path().display().to_string());
}
log::info!("path namees are {:#?}", self.path_names);
} }
fn go_up(&mut self) { fn go_up(&mut self) {
+15 -15
View File
@@ -2,8 +2,8 @@ use dioxus::prelude::*;
use rand::prelude::*; use rand::prelude::*;
fn main() { fn main() {
dioxus::web::launch(App); // dioxus::web::launch(App);
// dioxus::desktop::launch(App); dioxus::desktop::launch(App);
} }
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
@@ -31,16 +31,16 @@ impl Label {
} }
static App: Component = |cx| { static App: Component = |cx| {
let mut items = use_ref(&cx, || vec![]); let items = use_ref(&cx, || vec![]);
let mut selected = use_state(&cx, || None); let selected = use_state(&cx, || None);
cx.render(rsx! { cx.render(rsx! {
div { class: "container" div { class: "container",
div { class: "jumbotron" div { class: "jumbotron",
div { class: "row" div { class: "row",
div { class: "col-md-6", h1 { "Dioxus" } } div { class: "col-md-6", h1 { "Dioxus" } }
div { class: "col-md-6" div { class: "col-md-6",
div { class: "row" div { class: "row",
ActionButton { name: "Create 1,000 rows", id: "run", ActionButton { name: "Create 1,000 rows", id: "run",
onclick: move || items.set(Label::new_list(1_000)), onclick: move || items.set(Label::new_list(1_000)),
} }
@@ -67,15 +67,15 @@ static App: Component = |cx| {
tbody { tbody {
{items.read().iter().enumerate().map(|(id, item)| { {items.read().iter().enumerate().map(|(id, item)| {
let is_in_danger = if (*selected).map(|s| s == id).unwrap_or(false) {"danger"} else {""}; let is_in_danger = if (*selected).map(|s| s == id).unwrap_or(false) {"danger"} else {""};
rsx!(tr { class: "{is_in_danger}" rsx!(tr { class: "{is_in_danger}",
td { class:"col-md-1" } td { class:"col-md-1" }
td { class:"col-md-1", "{item.key}" } td { class:"col-md-1", "{item.key}" }
td { class:"col-md-1", onclick: move |_| selected.set(Some(id)), td { class:"col-md-1", onclick: move |_| selected.set(Some(id)),
a { class: "lbl", {item.labels} } a { class: "lbl", item.labels }
} }
td { class: "col-md-1" td { class: "col-md-1",
a { class: "remove", onclick: move |_| { items.write().remove(id); }, a { class: "remove", onclick: move |_| { items.write().remove(id); },
span { class: "glyphicon glyphicon-remove remove" aria_hidden: "true" } span { class: "glyphicon glyphicon-remove remove", aria_hidden: "true" }
} }
} }
td { class: "col-md-6" } td { class: "col-md-6" }
@@ -83,7 +83,7 @@ static App: Component = |cx| {
})} })}
} }
} }
// span { class: "preloadicon glyphicon glyphicon-remove" aria_hidden: "true" } span { class: "preloadicon glyphicon glyphicon-remove", aria_hidden: "true" }
} }
}) })
}; };
@@ -96,7 +96,7 @@ struct ActionButtonProps<'a> {
} }
fn ActionButton<'a>(cx: Scope<'a, ActionButtonProps<'a>>) -> Element { fn ActionButton<'a>(cx: Scope<'a, ActionButtonProps<'a>>) -> Element {
rsx!(cx, div { class: "col-sm-6 smallpad" rsx!(cx, div { class: "col-sm-6 smallpad",
button { class:"btn btn-primary btn-block", r#type: "button", id: "{cx.props.id}", onclick: move |_| (cx.props.onclick)(), button { class:"btn btn-primary btn-block", r#type: "button", id: "{cx.props.id}", onclick: move |_| (cx.props.onclick)(),
"{cx.props.name}" "{cx.props.name}"
} }
+4 -4
View File
@@ -16,18 +16,18 @@ fn main() {
let vdom = VirtualDom::new(App); let vdom = VirtualDom::new(App);
let content = ssr::render_vdom_cfg(&vdom, |f| f.pre_render(true)); let content = ssr::render_vdom_cfg(&vdom, |f| f.pre_render(true));
dioxus::desktop::launch_cfg(App, |c| c.with_prerendered(content)); // dioxus::desktop::launch_cfg(App, |c| c.with_prerendered(content));
} }
static App: Component = |cx| { static App: Component = |cx| {
let mut val = use_state(&cx, || 0); let val = use_state(&cx, || 0);
cx.render(rsx! { cx.render(rsx! {
div { div {
h1 {"hello world. Count: {val}"} h1 { "hello world. Count: {val}" }
button { button {
onclick: move |_| *val.modify() += 1,
"click to increment" "click to increment"
onclick: move |_| val += 1
} }
} }
}) })
+20 -20
View File
@@ -24,42 +24,42 @@ use dioxus::prelude::*;
const STYLE: &str = include_str!("./assets/calculator.css"); const STYLE: &str = include_str!("./assets/calculator.css");
fn main() { fn main() {
env_logger::init(); env_logger::init();
dioxus::desktop::launch_cfg(App, |cfg| { // dioxus::desktop::launch_cfg(App, |cfg| {
cfg.with_window(|w| { // cfg.with_window(|w| {
w.with_title("Calculator Demo") // w.with_title("Calculator Demo")
.with_resizable(false) // .with_resizable(false)
.with_inner_size(LogicalSize::new(320.0, 530.0)) // .with_inner_size(LogicalSize::new(320.0, 530.0))
}) // })
}); // });
} }
static App: Component = |cx| { fn app(cx: Scope) -> Element {
let state = use_ref(&cx, || Calculator::new()); let state = use_ref(&cx, || Calculator::new());
let clear_display = state.read().display_value.eq("0"); let clear_display = state.read().display_value.eq("0");
let clear_text = if clear_display { "C" } else { "AC" }; let clear_text = if clear_display { "C" } else { "AC" };
let formatted = state.read().formatted_display(); let formatted = state.read().formatted_display();
rsx!(cx, div { id: "wrapper" rsx!(cx, div { id: "wrapper",
div { class: "app", style { "{STYLE}" } div { class: "app", style { "{STYLE}" }
div { class: "calculator", onkeypress: move |evt| state.write().handle_keydown(evt), div { class: "calculator", onkeypress: move |evt| state.write().handle_keydown(evt),
div { class: "calculator-display", "{formatted}"} div { class: "calculator-display", "{formatted}"}
div { class: "calculator-keypad" div { class: "calculator-keypad",
div { class: "input-keys" div { class: "input-keys",
div { class: "function-keys" div { class: "function-keys",
CalculatorKey { name: "key-clear", onclick: move |_| state.write().clear_display(), "{clear_text}" } CalculatorKey { name: "key-clear", onclick: move |_| state.write().clear_display(), "{clear_text}" }
CalculatorKey { name: "key-sign", onclick: move |_| state.write().toggle_sign(), "±"} CalculatorKey { name: "key-sign", onclick: move |_| state.write().toggle_sign(), "±"}
CalculatorKey { name: "key-percent", onclick: move |_| state.write().toggle_percent(), "%"} CalculatorKey { name: "key-percent", onclick: move |_| state.write().toggle_percent(), "%"}
} }
div { class: "digit-keys" div { class: "digit-keys",
CalculatorKey { name: "key-0", onclick: move |_| state.write().input_digit(0), "0" } CalculatorKey { name: "key-0", onclick: move |_| state.write().input_digit(0), "0" }
CalculatorKey { name: "key-dot", onclick: move |_| state.write().input_dot(), "●" } CalculatorKey { name: "key-dot", onclick: move |_| state.write().input_dot(), "●" }
{(1..10).map(move |k| rsx!{ (1..10).map(move |k| rsx!{
CalculatorKey { key: "{k}", name: "key-{k}", onclick: move |_| state.write().input_digit(k), "{k}" } CalculatorKey { key: "{k}", name: "key-{k}", onclick: move |_| state.write().input_digit(k), "{k}" }
})} })
} }
} }
div { class: "operator-keys" div { class: "operator-keys",
CalculatorKey { name:"key-divide", onclick: move |_| state.write().set_operator(Operator::Div), "÷" } CalculatorKey { name:"key-divide", onclick: move |_| state.write().set_operator(Operator::Div), "÷" }
CalculatorKey { name:"key-multiply", onclick: move |_| state.write().set_operator(Operator::Mul), "×" } CalculatorKey { name:"key-multiply", onclick: move |_| state.write().set_operator(Operator::Mul), "×" }
CalculatorKey { name:"key-subtract", onclick: move |_| state.write().set_operator(Operator::Sub), "−" } CalculatorKey { name:"key-subtract", onclick: move |_| state.write().set_operator(Operator::Sub), "−" }
@@ -70,7 +70,7 @@ static App: Component = |cx| {
} }
} }
}) })
}; }
#[derive(Props)] #[derive(Props)]
struct CalculatorKeyProps<'a> { struct CalculatorKeyProps<'a> {
@@ -82,9 +82,9 @@ struct CalculatorKeyProps<'a> {
fn CalculatorKey<'a>(cx: Scope<'a, CalculatorKeyProps<'a>>) -> Element { fn CalculatorKey<'a>(cx: Scope<'a, CalculatorKeyProps<'a>>) -> Element {
cx.render(rsx! { cx.render(rsx! {
button { button {
class: "calculator-key {cx.props.name}" class: "calculator-key {cx.props.name}",
onclick: move |e| (cx.props.onclick)(e) onclick: move |e| (cx.props.onclick)(e),
{&cx.props.children} &cx.props.children
} }
}) })
} }
+11 -13
View File
@@ -14,20 +14,18 @@ fn main() {
pub static App: Component = |cx| { pub static App: Component = |cx| {
let state = use_state(&cx, PlayerState::new); let state = use_state(&cx, PlayerState::new);
let is_playing = state.is_playing(); cx.render(rsx!(
div {
rsx!(cx, div { h1 {"Select an option"}
h1 {"Select an option"} h3 { "The radio is... " [state.is_playing()], "!" }
h3 {"The radio is... {is_playing}!"} button { onclick: move |_| state.modify().reduce(PlayerAction::Pause),
button { "Pause"
"Pause" }
onclick: move |_| state.modify().reduce(PlayerAction::Pause) button { onclick: move |_| state.modify().reduce(PlayerAction::Play),
"Play"
}
} }
button { ))
"Play"
onclick: move |_| state.modify().reduce(PlayerAction::Play)
}
})
}; };
enum PlayerAction { enum PlayerAction {
+3 -3
View File
@@ -9,13 +9,13 @@ fn main() {
} }
static App: Component = |cx| { static App: Component = |cx| {
let count = use_state(&cx, || 0); let mut count = use_state(&cx, || 0);
cx.render(rsx! { cx.render(rsx! {
div { div {
h1 { "High-Five counter: {count}" } h1 { "High-Five counter: {count}" }
button { onclick: move |_| *count.modify() += 1, "Up high!" } button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| *count.modify() -= 1, "Down low!" } button { onclick: move |_| count -= 1, "Down low!" }
} }
}) })
}; };
+61
View File
@@ -0,0 +1,61 @@
use dioxus::prelude::*;
fn main() {
let mut vdom = VirtualDom::new(example);
vdom.rebuild();
let out = dioxus::ssr::render_vdom_cfg(&vdom, |c| c.newline(true).indent(true));
println!("{}", out);
}
fn example(cx: Scope) -> Element {
let items = use_state(&cx, || {
vec![Thing {
a: "asd".to_string(),
b: 10,
}]
});
let things = use_ref(&cx, || {
vec![Thing {
a: "asd".to_string(),
b: 10,
}]
});
let things_list = things.read();
let mything = use_ref(&cx, || Some(String::from("asd")));
let mything_read = mything.read();
cx.render(rsx!(
div {
div {
id: "asd",
"your neighborhood spiderman"
items.iter().cycle().take(5).map(|f| rsx!{
div {
"{f.a}"
}
})
things_list.iter().map(|f| rsx!{
div {
"{f.a}"
}
})
mything_read.as_ref().map(|f| rsx!{
div {
"{f}"
}
})
}
}
))
}
struct Thing {
a: String,
b: u32,
}
+54 -29
View File
@@ -60,6 +60,7 @@ pub static EXAMPLE: Component = |cx| {
h1 {"Some text"} h1 {"Some text"}
h1 {"Some text with {formatting}"} h1 {"Some text with {formatting}"}
h1 {"Formatting basic expressions {formatting_tuple.0} and {formatting_tuple.1}"} h1 {"Formatting basic expressions {formatting_tuple.0} and {formatting_tuple.1}"}
h1 {"Formatting without interpolation " [formatting_tuple.0] "and" [formatting_tuple.1] }
h2 { h2 {
"Multiple" "Multiple"
"Text" "Text"
@@ -72,7 +73,7 @@ pub static EXAMPLE: Component = |cx| {
h3 {"elements"} h3 {"elements"}
} }
div { div {
class: "my special div" class: "my special div",
h1 {"Headers and attributes!"} h1 {"Headers and attributes!"}
} }
div { div {
@@ -88,42 +89,51 @@ pub static EXAMPLE: Component = |cx| {
} }
// Expressions can be used in element position too: // Expressions can be used in element position too:
{rsx!(p { "More templating!" })} rsx!(p { "More templating!" }),
// {html!(<p>"Even HTML templating!!"</p>)}
// Iterators // Iterators
{(0..10).map(|i| rsx!(li { "{i}" }))} (0..10).map(|i| rsx!(li { "{i}" })),
{{
// Iterators within expressions
{
let data = std::collections::HashMap::<&'static str, &'static str>::new(); let data = std::collections::HashMap::<&'static str, &'static str>::new();
// Iterators *should* have keys when you can provide them. // Iterators *should* have keys when you can provide them.
// Keys make your app run faster. Make sure your keys are stable, unique, and predictable. // Keys make your app run faster. Make sure your keys are stable, unique, and predictable.
// Using an "ID" associated with your data is a good idea. // Using an "ID" associated with your data is a good idea.
data.into_iter().map(|(k, v)| rsx!(li { key: "{k}" "{v}" })) data.into_iter().map(|(k, v)| rsx!(li { key: "{k}", "{v}" }))
}} }
// Matching // Matching
{match true { match true {
true => rsx!( h1 {"Top text"}), true => rsx!( h1 {"Top text"}),
false => rsx!( h1 {"Bottom text"}) false => rsx!( h1 {"Bottom text"})
}} }
// Conditional rendering // Conditional rendering
// Dioxus conditional rendering is based around None/Some. We have no special syntax for conditionals. // Dioxus conditional rendering is based around None/Some. We have no special syntax for conditionals.
// You can convert a bool condition to rsx! with .then and .or // You can convert a bool condition to rsx! with .then and .or
{true.then(|| rsx!(div {}))} true.then(|| rsx!(div {})),
// True conditions need to be rendered (same reasons as matching) // Alternatively, you can use the "if" syntax - but both branches must be resolve to Element
{if true { if false {
rsx!(cx, h1 {"Top text"}) rsx!(h1 {"Top text"})
} else { } else {
rsx!(cx, h1 {"Bottom text"}) rsx!(h1 {"Bottom text"})
}} }
// returning "None" is a bit noisy... but rare in practice // Using optionals for diverging branches
{None as Option<()>} if true {
Some(rsx!(h1 {"Top text"}))
} else {
None
}
// returning "None" without a diverging branch is a bit noisy... but rare in practice
None as Option<()>,
// Use the Dioxus type-alias for less noise // Use the Dioxus type-alias for less noise
{NONE_ELEMENT} NONE_ELEMENT,
// can also just use empty fragments // can also just use empty fragments
Fragment {} Fragment {}
@@ -137,9 +147,8 @@ pub static EXAMPLE: Component = |cx| {
Fragment { Fragment {
"D" "D"
Fragment { Fragment {
"heavily nested fragments is an antipattern" "E"
"they cause Dioxus to do unnecessary work" "F"
"don't use them carelessly if you can help it"
} }
} }
} }
@@ -158,22 +167,29 @@ pub static EXAMPLE: Component = |cx| {
Taller { a: "asd" } Taller { a: "asd" }
// Can pass in props directly as an expression // Can pass in props directly as an expression
{{ {
let props = TallerProps {a: "hello", children: Default::default()}; let props = TallerProps {a: "hello", children: Default::default()};
rsx!(Taller { ..props }) rsx!(Taller { ..props })
}} }
// Spreading can also be overridden manually // Spreading can also be overridden manually
Taller { Taller {
..TallerProps { a: "ballin!", children: Default::default() } ..TallerProps { a: "ballin!", children: Default::default() },
a: "not ballin!" a: "not ballin!"
} }
// Can take children too! // Can take children too!
Taller { a: "asd", div {"hello world!"} } Taller { a: "asd", div {"hello world!"} }
// Components can be used with the `call` syntax
// This component's props are defined *inline* with the `inline_props` macro
with_inline(
text: "using functionc all syntax"
)
// helper functions // helper functions
{helper(&cx, "hello world!")} // Single values must be wrapped in braces or `Some` to satisfy `IntoIterator`
[helper(&cx, "hello world!")]
} }
}) })
}; };
@@ -187,6 +203,7 @@ mod baller {
#[derive(Props, PartialEq)] #[derive(Props, PartialEq)]
pub struct BallerProps {} pub struct BallerProps {}
#[allow(non_snake_case)]
/// This component totally balls /// This component totally balls
pub fn Baller(_: Scope<BallerProps>) -> Element { pub fn Baller(_: Scope<BallerProps>) -> Element {
todo!() todo!()
@@ -195,12 +212,20 @@ mod baller {
#[derive(Props)] #[derive(Props)]
pub struct TallerProps<'a> { pub struct TallerProps<'a> {
/// Fields are documented and accessible in rsx!
a: &'static str, a: &'static str,
children: Element<'a>, children: Element<'a>,
} }
/// This component is taller than most :) /// Documention for this component is visible within the rsx macro
pub fn Taller<'a>(_: Scope<'a, TallerProps<'a>>) -> Element { #[allow(non_snake_case)]
let b = true; pub fn Taller<'a>(cx: Scope<'a, TallerProps<'a>>) -> Element {
todo!() cx.render(rsx! {
&cx.props.children
})
}
#[inline_props]
fn with_inline<'a>(cx: Scope<'a>, text: &'a str) -> Element {
rsx!(cx, p { "{text}" })
} }
+38 -38
View File
@@ -16,9 +16,9 @@ const STYLE: &str = "body {overflow:hidden;}";
pub static App: Component = |cx| { pub static App: Component = |cx| {
cx.render(rsx!( cx.render(rsx!(
div { class: "overflow-hidden" div { class: "overflow-hidden",
style { "{STYLE}" } style { "{STYLE}" }
link { href:"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel:"stylesheet" } link { href:"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", rel:"stylesheet" }
Header {} Header {}
Entry {} Entry {}
Hero {} Hero {}
@@ -33,20 +33,20 @@ pub static App: Component = |cx| {
pub static Header: Component = |cx| { pub static Header: Component = |cx| {
cx.render(rsx! { cx.render(rsx! {
div { div {
header { class: "text-gray-400 bg-gray-900 body-font" header { class: "text-gray-400 bg-gray-900 body-font",
div { class: "container mx-auto flex flex-wrap p-5 flex-col md:flex-row items-center" div { class: "container mx-auto flex flex-wrap p-5 flex-col md:flex-row items-center",
a { class: "flex title-font font-medium items-center text-white mb-4 md:mb-0" a { class: "flex title-font font-medium items-center text-white mb-4 md:mb-0",
StacksIcon {} StacksIcon {}
span { class: "ml-3 text-xl" "Hello Dioxus!"} span { class: "ml-3 text-xl", "Hello Dioxus!"}
} }
nav { class: "md:ml-auto flex flex-wrap items-center text-base justify-center" nav { class: "md:ml-auto flex flex-wrap items-center text-base justify-center",
a { class: "mr-5 hover:text-white" "First Link"} a { class: "mr-5 hover:text-white", "First Link"}
a { class: "mr-5 hover:text-white" "Second Link"} a { class: "mr-5 hover:text-white", "Second Link"}
a { class: "mr-5 hover:text-white" "Third Link"} a { class: "mr-5 hover:text-white", "Third Link"}
a { class: "mr-5 hover:text-white" "Fourth Link"} a { class: "mr-5 hover:text-white", "Fourth Link"}
} }
button { button {
class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0" class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0",
"Button" "Button"
RightArrowIcon {} RightArrowIcon {}
} }
@@ -59,34 +59,34 @@ pub static Header: Component = |cx| {
pub static Hero: Component = |cx| { pub static Hero: Component = |cx| {
// //
cx.render(rsx! { cx.render(rsx! {
section{ class: "text-gray-400 bg-gray-900 body-font" section{ class: "text-gray-400 bg-gray-900 body-font",
div { class: "container mx-auto flex px-5 py-24 md:flex-row flex-col items-center" div { class: "container mx-auto flex px-5 py-24 md:flex-row flex-col items-center",
div { class: "lg:flex-grow md:w-1/2 lg:pr-24 md:pr-16 flex flex-col md:items-start md:text-left mb-16 md:mb-0 items-center text-center" div { class: "lg:flex-grow md:w-1/2 lg:pr-24 md:pr-16 flex flex-col md:items-start md:text-left mb-16 md:mb-0 items-center text-center",
h1 { class: "title-font sm:text-4xl text-3xl mb-4 font-medium text-white" h1 { class: "title-font sm:text-4xl text-3xl mb-4 font-medium text-white",
br { class: "hidden lg:inline-block" } br { class: "hidden lg:inline-block" }
"Dioxus Sneak Peek" "Dioxus Sneak Peek"
} }
p { p {
class: "mb-8 leading-relaxed" class: "mb-8 leading-relaxed",
"Dioxus is a new UI framework that makes it easy and simple to write cross-platform apps using web "Dioxus is a new UI framework that makes it easy and simple to write cross-platform apps using web
technologies! It is functional, fast, and portable. Dioxus can run on the web, on the desktop, and technologies! It is functional, fast, and portable. Dioxus can run on the web, on the desktop, and
on mobile and embedded platforms." on mobile and embedded platforms."
} }
div { class: "flex justify-center" div { class: "flex justify-center",
button { button {
class: "inline-flex text-white bg-indigo-500 border-0 py-2 px-6 focus:outline-none hover:bg-indigo-600 rounded text-lg" class: "inline-flex text-white bg-indigo-500 border-0 py-2 px-6 focus:outline-none hover:bg-indigo-600 rounded text-lg",
"Learn more" "Learn more"
} }
button { button {
class: "ml-4 inline-flex text-gray-400 bg-gray-800 border-0 py-2 px-6 focus:outline-none hover:bg-gray-700 hover:text-white rounded text-lg" class: "ml-4 inline-flex text-gray-400 bg-gray-800 border-0 py-2 px-6 focus:outline-none hover:bg-gray-700 hover:text-white rounded text-lg",
"Build an app" "Build an app"
} }
} }
} }
div { class: "lg:max-w-lg lg:w-full md:w-1/2 w-5/6" div { class: "lg:max-w-lg lg:w-full md:w-1/2 w-5/6",
img { class: "object-cover object-center rounded" alt: "hero" src: "https://i.imgur.com/oK6BLtw.png" img { class: "object-cover object-center rounded", alt: "hero", src: "https://i.imgur.com/oK6BLtw.png",
referrerpolicy:"no-referrer" referrerpolicy:"no-referrer"
} }
} }
@@ -97,8 +97,8 @@ pub static Hero: Component = |cx| {
pub static Entry: Component = |cx| { pub static Entry: Component = |cx| {
// //
cx.render(rsx! { cx.render(rsx! {
section{ class: "text-gray-400 bg-gray-900 body-font" section{ class: "text-gray-400 bg-gray-900 body-font",
div { class: "container mx-auto flex px-5 py-24 md:flex-row flex-col items-center" div { class: "container mx-auto flex px-5 py-24 md:flex-row flex-col items-center",
textarea { textarea {
} }
@@ -111,13 +111,13 @@ pub static StacksIcon: Component = |cx| {
cx.render(rsx!( cx.render(rsx!(
svg { svg {
// xmlns: "http://www.w3.org/2000/svg" // xmlns: "http://www.w3.org/2000/svg"
fill: "none" fill: "none",
stroke: "currentColor" stroke: "currentColor",
stroke_linecap: "round" stroke_linecap: "round",
stroke_linejoin: "round" stroke_linejoin: "round",
stroke_width: "2" stroke_width: "2",
class: "w-10 h-10 text-white p-2 bg-indigo-500 rounded-full" class: "w-10 h-10 text-white p-2 bg-indigo-500 rounded-full",
view_box: "0 0 24 24" view_box: "0 0 24 24",
path { d: "M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"} path { d: "M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"}
} }
)) ))
@@ -125,13 +125,13 @@ pub static StacksIcon: Component = |cx| {
pub static RightArrowIcon: Component = |cx| { pub static RightArrowIcon: Component = |cx| {
cx.render(rsx!( cx.render(rsx!(
svg { svg {
fill: "none" fill: "none",
stroke: "currentColor" stroke: "currentColor",
stroke_linecap: "round" stroke_linecap: "round",
stroke_linejoin: "round" stroke_linejoin: "round",
stroke_width: "2" stroke_width: "2",
class: "w-4 h-4 ml-1" class: "w-4 h-4 ml-1",
view_box: "0 0 24 24" view_box: "0 0 24 24",
path { d: "M5 12h14M12 5l7 7-7 7"} path { d: "M5 12h14M12 5l7 7-7 7"}
} }
)) ))
+3 -2
View File
@@ -15,8 +15,9 @@ fn app(cx: Scope) -> Element {
use_future(&cx, || { use_future(&cx, || {
for_async![count]; for_async![count];
async move { async move {
while let _ = tokio::time::sleep(Duration::from_millis(1000)).await { loop {
*count.modify() += 1; tokio::time::sleep(Duration::from_millis(1000)).await;
count += 1;
} }
} }
}); });
+21 -18
View File
@@ -29,7 +29,7 @@ const App: Component = |cx| {
let todolist = todos let todolist = todos
.iter() .iter()
.filter(|(id, item)| match *filter { .filter(|(_id, item)| match *filter {
FilterState::All => true, FilterState::All => true,
FilterState::Active => !item.checked, FilterState::Active => !item.checked,
FilterState::Completed => item.checked, FilterState::Completed => item.checked,
@@ -48,34 +48,37 @@ const App: Component = |cx| {
_ => "items", _ => "items",
}; };
rsx!(cx, div { id: "app" rsx!(cx, div { id: "app",
style {"{STYLE}"} style {"{STYLE}"}
div { div {
header { class: "header" header { class: "header",
h1 {"todos"} h1 {"todos"}
input { input {
class: "new-todo" class: "new-todo",
placeholder: "What needs to be done?" placeholder: "What needs to be done?",
value: "{draft}" value: "{draft}",
oninput: move |evt| draft.set(evt.value.clone()) oninput: move |evt| draft.set(evt.value.clone()),
} }
} }
{todolist} todolist,
{(!todos.is_empty()).then(|| rsx!( (!todos.is_empty()).then(|| rsx!(
footer { footer {
span { strong {"{items_left}"} span {"{item_text} left"} } span {
ul { class: "filters" strong {"{items_left}"}
span {"{item_text} left"}
}
ul { class: "filters",
li { class: "All", a { href: "", onclick: move |_| filter.set(FilterState::All), "All" }} li { class: "All", a { href: "", onclick: move |_| filter.set(FilterState::All), "All" }}
li { class: "Active", a { href: "active", onclick: move |_| filter.set(FilterState::Active), "Active" }} li { class: "Active", a { href: "active", onclick: move |_| filter.set(FilterState::Active), "Active" }}
li { class: "Completed", a { href: "completed", onclick: move |_| filter.set(FilterState::Completed), "Completed" }} li { class: "Completed", a { href: "completed", onclick: move |_| filter.set(FilterState::Completed), "Completed" }}
} }
} }
))} ))
} }
footer { class: "info" footer { class: "info",
p {"Double-click to edit a todo"} p {"Double-click to edit a todo"}
p { "Created by ", a { "jkelleyrtp", href: "http://github.com/jkelleyrtp/" }} p { "Created by ", a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" }}
p { "Part of ", a { "TodoMVC", href: "http://todomvc.com" }} p { "Part of ", a { href: "http://todomvc.com", "TodoMVC" }}
} }
}) })
}; };
@@ -93,13 +96,13 @@ pub fn TodoEntry(cx: Scope<TodoEntryProps>) -> Element {
rsx!(cx, li { rsx!(cx, li {
"{todo.id}" "{todo.id}"
input { input {
class: "toggle" class: "toggle",
r#type: "checkbox" r#type: "checkbox",
"{todo.checked}" "{todo.checked}"
} }
{is_editing.then(|| rsx!{ {is_editing.then(|| rsx!{
input { input {
value: "{contents}" value: "{contents}",
oninput: move |evt| contents.set(evt.value.clone()) oninput: move |evt| contents.set(evt.value.clone())
} }
})} })}
+9 -18
View File
@@ -13,28 +13,19 @@
use dioxus::prelude::*; use dioxus::prelude::*;
fn main() { fn main() {
#[cfg(target_arch = "wasm32")] dioxus::desktop::launch(App);
intern_strings();
dioxus::web::launch(App);
} }
static App: Component = |cx| { static App: Component = |cx| {
let mut rng = SmallRng::from_entropy(); let mut rng = SmallRng::from_entropy();
let rows = (0..1_000).map(|f| {
let label = Label::new(&mut rng);
rsx! {
Row {
row_id: f,
label: label
}
}
});
cx.render(rsx! { cx.render(rsx! {
table { table {
tbody { tbody {
{rows} (0..1_000).map(|f| {
let label = Label::new(&mut rng);
rsx! (Row { row_id: f, label: label })
})
} }
} }
}) })
@@ -50,12 +41,12 @@ fn Row(cx: Scope<RowProps>) -> Element {
cx.render(rsx! { cx.render(rsx! {
tr { tr {
td { class:"col-md-1", "{cx.props.row_id}" } td { class:"col-md-1", "{cx.props.row_id}" }
td { class:"col-md-1", onclick: move |_| { /* run onselect */ } td { class:"col-md-1", onclick: move |_| { /* run onselect */ },
a { class: "lbl", "{adj}" "{col}" "{noun}" } a { class: "lbl", "{adj}" "{col}" "{noun}" }
} }
td { class: "col-md-1" td { class: "col-md-1",
a { class: "remove", onclick: move |_| {/* remove */} a { class: "remove", onclick: move |_| {/* remove */},
span { class: "glyphicon glyphicon-remove remove" aria_hidden: "true" } span { class: "glyphicon glyphicon-remove remove", aria_hidden: "true" }
} }
} }
td { class: "col-md-6" } td { class: "col-md-6" }
+3 -3
View File
@@ -16,13 +16,13 @@ fn main() {
} }
fn app(cx: Scope) -> Element { fn app(cx: Scope) -> Element {
let count = use_state(&cx, || 0); let mut count = use_state(&cx, || 0);
cx.render(rsx! { cx.render(rsx! {
div { div {
h1 { "Hifive counter: {count}" } h1 { "Hifive counter: {count}" }
button { onclick: move |_| *count.modify() += 1, "Up high!" } button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| *count.modify() -= 1, "Down low!" } button { onclick: move |_| count -= 1, "Down low!" }
} }
}) })
} }
+31
View File
@@ -0,0 +1,31 @@
use dioxus::prelude::*;
fn main() {
dioxus::desktop::launch(app);
}
fn app(cx: Scope) -> Element {
let contents = use_state(&cx, || String::from("<script>alert(123)</script>"));
cx.render(rsx! {
div {
"hello world!"
h1 {
"{contents}"
}
h3 {
[contents.as_str()]
}
input {
value: "{contents}",
oninput: move |e| {
contents.set(e.value.clone());
eprintln!("asd");
},
"type": "text",
}
}
})
}
+1
View File
@@ -16,6 +16,7 @@ proc-macro = true
[dependencies] [dependencies]
once_cell = "1.8" once_cell = "1.8"
proc-macro-error = "1.0.4"
proc-macro2 = { version = "1.0.6" } proc-macro2 = { version = "1.0.6" }
quote = "1.0" quote = "1.0"
syn = { version = "1.0.11", features = ["full", "extra-traits"] } syn = { version = "1.0.11", features = ["full", "extra-traits"] }
+1
View File
@@ -99,6 +99,7 @@ impl ToTokens for InlinePropsBody {
}; };
out_tokens.append_all(quote! { out_tokens.append_all(quote! {
#[allow(non_camel_case)]
#modifiers #modifiers
#vis struct #struct_name #generics { #vis struct #struct_name #generics {
#(#fields),* #(#fields),*
+3 -2
View File
@@ -177,11 +177,12 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token
/// todo!() /// todo!()
/// } /// }
/// ``` /// ```
#[proc_macro_error::proc_macro_error]
#[proc_macro] #[proc_macro]
pub fn rsx(s: TokenStream) -> TokenStream { pub fn rsx(s: TokenStream) -> TokenStream {
match syn::parse::<rsx::CallBody>(s) { match syn::parse::<rsx::CallBody>(s) {
Err(e) => e.to_compile_error().into(), Err(err) => err.to_compile_error().into(),
Ok(s) => s.to_token_stream().into(), Ok(stream) => stream.to_token_stream().into(),
} }
} }
-64
View File
@@ -1,64 +0,0 @@
//! Parse anything that has a pattern of < Ident, Bracket >
//! ========================================================
//!
//! Whenever a `name {}` pattern emerges, we need to parse it into an element, a component, or a fragment.
//! This feature must support:
//! - Namepsaced/pathed components
//! - Differentiating between built-in and custom elements
use super::*;
use proc_macro2::TokenStream as TokenStream2;
use quote::ToTokens;
use syn::{
parse::{Parse, ParseStream},
Error, Ident, Result, Token,
};
#[allow(clippy::large_enum_variant)]
pub enum AmbiguousElement {
Element(Element),
Component(Component),
}
impl Parse for AmbiguousElement {
fn parse(input: ParseStream) -> Result<Self> {
// Try to parse as an absolute path and immediately defer to the componetn
if input.peek(Token![::]) {
return input.parse::<Component>().map(AmbiguousElement::Component);
}
// If not an absolute path, then parse the ident and check if it's a valid tag
if let Ok(pat) = input.fork().parse::<syn::Path>() {
if pat.segments.len() > 1 {
return input.parse::<Component>().map(AmbiguousElement::Component);
}
}
use syn::ext::IdentExt;
if let Ok(name) = input.fork().call(Ident::parse_any) {
let name_str = name.to_string();
let first_char = name_str.chars().next().unwrap();
if first_char.is_ascii_uppercase() {
input.parse::<Component>().map(AmbiguousElement::Component)
} else {
if input.peek2(syn::token::Paren) {
input.parse::<Component>().map(AmbiguousElement::Component)
} else {
input.parse::<Element>().map(AmbiguousElement::Element)
}
}
} else {
Err(Error::new(input.span(), "Not a valid Html tag"))
}
}
}
impl ToTokens for AmbiguousElement {
fn to_tokens(&self, tokens: &mut TokenStream2) {
match self {
AmbiguousElement::Element(el) => el.to_tokens(tokens),
AmbiguousElement::Component(comp) => comp.to_tokens(tokens),
}
}
}
-66
View File
@@ -1,66 +0,0 @@
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens, TokenStreamExt};
use syn::{
parse::{Parse, ParseStream},
Ident, Result, Token,
};
use super::*;
pub struct CallBody {
custom_context: Option<Ident>,
roots: Vec<BodyNode>,
}
/// The custom rusty variant of parsing rsx!
impl Parse for CallBody {
fn parse(input: ParseStream) -> Result<Self> {
let custom_context = try_parse_custom_context(input)?;
let (_, roots, _) = BodyConfig::new_call_body().parse_component_body(input)?;
Ok(Self {
custom_context,
roots,
})
}
}
fn try_parse_custom_context(input: ParseStream) -> Result<Option<Ident>> {
let res = if input.peek(Ident) && input.peek2(Token![,]) {
let name = input.parse::<Ident>()?;
input.parse::<Token![,]>()?;
Some(name)
} else {
None
};
Ok(res)
}
/// Serialize the same way, regardless of flavor
impl ToTokens for CallBody {
fn to_tokens(&self, out_tokens: &mut TokenStream2) {
let inner = if self.roots.len() == 1 {
let inner = &self.roots[0];
quote! {#inner}
} else {
let childs = &self.roots;
quote! { __cx.fragment_root([ #(#childs),* ]) }
};
match &self.custom_context {
// The `in cx` pattern allows directly rendering
Some(ident) => out_tokens.append_all(quote! {
#ident.render(LazyNodes::new_some(move |__cx: NodeFactory| -> VNode {
use dioxus_elements::{GlobalAttributes, SvgAttributes};
#inner
}))
}),
// Otherwise we just build the LazyNode wrapper
None => out_tokens.append_all(quote! {
LazyNodes::new_some(move |__cx: NodeFactory| -> VNode {
use dioxus_elements::{GlobalAttributes, SvgAttributes};
#inner
})
}),
};
}
}
+20 -95
View File
@@ -19,11 +19,10 @@ use quote::{quote, ToTokens, TokenStreamExt};
use syn::{ use syn::{
ext::IdentExt, ext::IdentExt,
parse::{Parse, ParseBuffer, ParseStream}, parse::{Parse, ParseBuffer, ParseStream},
token, Error, Expr, ExprClosure, Ident, Result, Token, token, Expr, ExprClosure, Ident, Result, Token,
}; };
pub struct Component { pub struct Component {
// accept any path-like argument
name: syn::Path, name: syn::Path,
body: Vec<ComponentField>, body: Vec<ComponentField>,
children: Vec<BodyNode>, children: Vec<BodyNode>,
@@ -32,12 +31,8 @@ pub struct Component {
impl Parse for Component { impl Parse for Component {
fn parse(stream: ParseStream) -> Result<Self> { fn parse(stream: ParseStream) -> Result<Self> {
// let name = s.parse::<syn::ExprPath>()?;
// todo: look into somehow getting the crate/super/etc
let name = syn::Path::parse_mod_style(stream)?; let name = syn::Path::parse_mod_style(stream)?;
// parse the guts
let content: ParseBuffer; let content: ParseBuffer;
// if we see a `{` then we have a block // if we see a `{` then we have a block
@@ -48,13 +43,25 @@ impl Parse for Component {
syn::parenthesized!(content in stream); syn::parenthesized!(content in stream);
} }
let cfg: BodyConfig = BodyConfig { let mut body = Vec::new();
allow_children: true, let mut children = Vec::new();
allow_fields: true, let mut manual_props = None;
allow_manual_props: true,
};
let (body, children, manual_props) = cfg.parse_component_body(&content)?; while !content.is_empty() {
// if we splat into a component then we're merging properties
if content.peek(Token![..]) {
content.parse::<Token![..]>()?;
manual_props = Some(content.parse::<Expr>()?);
} else if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) {
body.push(content.parse::<ComponentField>()?);
} else {
children.push(content.parse::<BodyNode>()?);
}
if content.peek(Token![,]) {
let _ = content.parse::<Token![,]>();
}
}
Ok(Self { Ok(Self {
name, name,
@@ -65,77 +72,6 @@ impl Parse for Component {
} }
} }
pub struct BodyConfig {
pub allow_fields: bool,
pub allow_children: bool,
pub allow_manual_props: bool,
}
impl BodyConfig {
/// The configuration to parse the root
pub fn new_call_body() -> Self {
Self {
allow_children: true,
allow_fields: false,
allow_manual_props: false,
}
}
}
impl BodyConfig {
// todo: unify this body parsing for both elements and components
// both are style rather ad-hoc, though components are currently more configured
pub fn parse_component_body(
&self,
content: &ParseBuffer,
) -> Result<(Vec<ComponentField>, Vec<BodyNode>, Option<Expr>)> {
let mut body = Vec::new();
let mut children = Vec::new();
let mut manual_props = None;
'parsing: loop {
// [1] Break if empty
if content.is_empty() {
break 'parsing;
}
if content.peek(Token![..]) {
if !self.allow_manual_props {
return Err(Error::new(
content.span(),
"Props spread syntax is not allowed in this context. \nMake to only use the elipsis `..` in Components.",
));
}
content.parse::<Token![..]>()?;
manual_props = Some(content.parse::<Expr>()?);
} else if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) {
if !self.allow_fields {
return Err(Error::new(
content.span(),
"Property fields is not allowed in this context. \nMake to only use fields in Components or Elements.",
));
}
body.push(content.parse::<ComponentField>()?);
} else {
if !self.allow_children {
return Err(Error::new(
content.span(),
"This item is not allowed to accept children.",
));
}
children.push(content.parse::<BodyNode>()?);
}
// consume comma if it exists
// we don't actually care if there *are* commas between attrs
if content.peek(Token![,]) {
let _ = content.parse::<Token![,]>();
}
}
Ok((body, children, manual_props))
}
}
impl ToTokens for Component { impl ToTokens for Component {
fn to_tokens(&self, tokens: &mut TokenStream2) { fn to_tokens(&self, tokens: &mut TokenStream2) {
let name = &self.name; let name = &self.name;
@@ -219,9 +155,7 @@ pub struct ComponentField {
enum ContentField { enum ContentField {
ManExpr(Expr), ManExpr(Expr),
OnHandler(ExprClosure),
// A handler was provided in {} tokens
OnHandlerRaw(Expr), OnHandlerRaw(Expr),
} }
@@ -229,9 +163,6 @@ impl ToTokens for ContentField {
fn to_tokens(&self, tokens: &mut TokenStream2) { fn to_tokens(&self, tokens: &mut TokenStream2) {
match self { match self {
ContentField::ManExpr(e) => e.to_tokens(tokens), ContentField::ManExpr(e) => e.to_tokens(tokens),
ContentField::OnHandler(e) => tokens.append_all(quote! {
__cx.bump().alloc(#e)
}),
ContentField::OnHandlerRaw(e) => tokens.append_all(quote! { ContentField::OnHandlerRaw(e) => tokens.append_all(quote! {
__cx.bump().alloc(#e) __cx.bump().alloc(#e)
}), }),
@@ -246,13 +177,7 @@ impl Parse for ComponentField {
let name_str = name.to_string(); let name_str = name.to_string();
let content = if name_str.starts_with("on") { let content = if name_str.starts_with("on") {
if input.peek(token::Brace) { ContentField::OnHandlerRaw(input.parse()?)
let content;
syn::braced!(content in input);
ContentField::OnHandlerRaw(content.parse()?)
} else {
ContentField::OnHandler(input.parse()?)
}
} else { } else {
ContentField::ManExpr(input.parse::<Expr>()?) ContentField::ManExpr(input.parse::<Expr>()?)
}; };
+116 -63
View File
@@ -4,7 +4,7 @@ use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens, TokenStreamExt}; use quote::{quote, ToTokens, TokenStreamExt};
use syn::{ use syn::{
parse::{Parse, ParseBuffer, ParseStream}, parse::{Parse, ParseBuffer, ParseStream},
token, Expr, ExprClosure, Ident, LitStr, Result, Token, Expr, Ident, LitStr, Result, Token,
}; };
// ======================================= // =======================================
@@ -33,45 +33,69 @@ impl Parse for Element {
let mut key = None; let mut key = None;
let mut _el_ref = None; let mut _el_ref = None;
// todo: more descriptive error handling // parse fields with commas
while !content.is_empty() { // break when we don't get this pattern anymore
// start parsing bodynodes
// "def": 456,
// abc: 123,
loop {
// Parse the raw literal fields
if content.peek(LitStr) && content.peek2(Token![:]) && !content.peek3(Token![:]) {
let name = content.parse::<LitStr>()?;
let ident = name.clone();
content.parse::<Token![:]>()?;
if content.peek(LitStr) {
let value = content.parse::<LitStr>()?;
attributes.push(ElementAttrNamed {
el_name: el_name.clone(),
attr: ElementAttr::CustomAttrText { name, value },
});
} else {
let value = content.parse::<Expr>()?;
attributes.push(ElementAttrNamed {
el_name: el_name.clone(),
attr: ElementAttr::CustomAttrExpression { name, value },
});
}
if content.is_empty() {
break;
}
// todo: add a message saying you need to include commas between fields
if content.parse::<Token![,]>().is_err() {
proc_macro_error::emit_error!(
ident,
"This attribute is misisng a trailing comma"
)
}
continue;
}
if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) { if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) {
let name = content.parse::<Ident>()?; let name = content.parse::<Ident>()?;
let ident = name.clone();
let name_str = name.to_string(); let name_str = name.to_string();
content.parse::<Token![:]>()?; content.parse::<Token![:]>()?;
if name_str.starts_with("on") { if name_str.starts_with("on") {
if content.peek(token::Brace) { listeners.push(ElementAttrNamed {
let mycontent; el_name: el_name.clone(),
syn::braced!(mycontent in content); attr: ElementAttr::EventTokens {
name,
listeners.push(ElementAttrNamed { tokens: content.parse()?,
el_name: el_name.clone(), },
attr: ElementAttr::EventTokens { });
name,
tokens: mycontent.parse()?,
},
});
} else {
listeners.push(ElementAttrNamed {
el_name: el_name.clone(),
attr: ElementAttr::EventClosure {
name,
closure: content.parse()?,
},
});
};
} else { } else {
match name_str.as_str() { match name_str.as_str() {
"key" => { "key" => {
key = Some(content.parse()?); key = Some(content.parse()?);
} }
"classes" => { "classes" => todo!("custom class list not supported yet"),
todo!("custom class list not supported") // "namespace" => todo!("custom namespace not supported yet"),
}
"namespace" => {
todo!("custom namespace not supported")
}
"node_ref" => { "node_ref" => {
_el_ref = Some(content.parse::<Expr>()?); _el_ref = Some(content.parse::<Expr>()?);
} }
@@ -96,27 +120,56 @@ impl Parse for Element {
} }
} }
} }
} else if content.peek(LitStr) && content.peek2(Token![:]) {
let name = content.parse::<LitStr>()?;
content.parse::<Token![:]>()?;
if content.peek(LitStr) { if content.is_empty() {
let value = content.parse::<LitStr>()?; break;
attributes.push(ElementAttrNamed {
el_name: el_name.clone(),
attr: ElementAttr::CustomAttrText { name, value },
});
} else {
let value = content.parse::<Expr>()?;
attributes.push(ElementAttrNamed {
el_name: el_name.clone(),
attr: ElementAttr::CustomAttrExpression { name, value },
});
} }
} else {
children.push(content.parse::<BodyNode>()?); // todo: add a message saying you need to include commas between fields
if content.parse::<Token![,]>().is_err() {
proc_macro_error::emit_error!(
ident,
"This attribute is misisng a trailing comma"
)
}
continue;
} }
break;
}
while !content.is_empty() {
if (content.peek(LitStr) && content.peek2(Token![:])) && !content.peek3(Token![:]) {
let ident = content.parse::<LitStr>().unwrap();
let name = ident.value();
proc_macro_error::emit_error!(
ident, "This attribute `{}` is in the wrong place.", name;
help =
"All attribute fields must be placed above children elements.
div {
attr: \"...\", <---- attribute is above children
div { } <---- children are below attributes
}";
)
}
if (content.peek(Ident) && content.peek2(Token![:])) && !content.peek3(Token![:]) {
let ident = content.parse::<Ident>().unwrap();
let name = ident.to_string();
proc_macro_error::emit_error!(
ident, "This attribute `{}` is in the wrong place.", name;
help =
"All attribute fields must be placed above children elements.
div {
attr: \"...\", <---- attribute is above children
div { } <---- children are below attributes
}";
)
}
children.push(content.parse::<BodyNode>()?);
// consume comma if it exists // consume comma if it exists
// we don't actually care if there *are* commas after elements/text // we don't actually care if there *are* commas after elements/text
if content.peek(Token![,]) { if content.peek(Token![,]) {
@@ -138,7 +191,7 @@ impl Parse for Element {
impl ToTokens for Element { impl ToTokens for Element {
fn to_tokens(&self, tokens: &mut TokenStream2) { fn to_tokens(&self, tokens: &mut TokenStream2) {
let name = &self.name; let name = &self.name;
let childs = &self.children; let children = &self.children;
let listeners = &self.listeners; let listeners = &self.listeners;
let attr = &self.attributes; let attr = &self.attributes;
@@ -153,7 +206,7 @@ impl ToTokens for Element {
dioxus_elements::#name, dioxus_elements::#name,
[ #(#listeners),* ], [ #(#listeners),* ],
[ #(#attr),* ], [ #(#attr),* ],
[ #(#childs),* ], [ #(#children),* ],
#key, #key,
) )
}); });
@@ -173,8 +226,8 @@ enum ElementAttr {
// "attribute": true, // "attribute": true,
CustomAttrExpression { name: LitStr, value: Expr }, CustomAttrExpression { name: LitStr, value: Expr },
// onclick: move |_| {} // // onclick: move |_| {}
EventClosure { name: Ident, closure: ExprClosure }, // EventClosure { name: Ident, closure: ExprClosure },
// onclick: {} // onclick: {}
EventTokens { name: Ident, tokens: Expr }, EventTokens { name: Ident, tokens: Expr },
@@ -189,7 +242,7 @@ impl ToTokens for ElementAttrNamed {
fn to_tokens(&self, tokens: &mut TokenStream2) { fn to_tokens(&self, tokens: &mut TokenStream2) {
let ElementAttrNamed { el_name, attr } = self; let ElementAttrNamed { el_name, attr } = self;
let toks = match attr { tokens.append_all(match attr {
ElementAttr::AttrText { name, value } => { ElementAttr::AttrText { name, value } => {
quote! { quote! {
dioxus_elements::#el_name.#name(__cx, format_args_f!(#value)) dioxus_elements::#el_name.#name(__cx, format_args_f!(#value))
@@ -200,26 +253,26 @@ impl ToTokens for ElementAttrNamed {
dioxus_elements::#el_name.#name(__cx, #value) dioxus_elements::#el_name.#name(__cx, #value)
} }
} }
ElementAttr::CustomAttrText { name, value } => { ElementAttr::CustomAttrText { name, value } => {
quote! { __cx.attr( #name, format_args_f!(#value), None, false ) }
}
ElementAttr::CustomAttrExpression { name, value } => {
quote! { __cx.attr( #name, format_args_f!(#value), None, false ) }
}
ElementAttr::EventClosure { name, closure } => {
quote! { quote! {
dioxus_elements::on::#name(__cx, #closure) __cx.attr( #name, format_args_f!(#value), None, false )
} }
} }
ElementAttr::CustomAttrExpression { name, value } => {
quote! {
__cx.attr( #name, format_args_f!(#value), None, false )
}
}
// ElementAttr::EventClosure { name, closure } => {
// quote! {
// dioxus_elements::on::#name(__cx, #closure)
// }
// }
ElementAttr::EventTokens { name, tokens } => { ElementAttr::EventTokens { name, tokens } => {
quote! { quote! {
dioxus_elements::on::#name(__cx, #tokens) dioxus_elements::on::#name(__cx, #tokens)
} }
} }
}; });
tokens.append_all(toks);
} }
} }
-63
View File
@@ -1,63 +0,0 @@
//! Parse `Fragments` into the Fragment VNode
//! ==========================================
//!
//! This parsing path emerges from [`AmbiguousElement`] which supports validation of the Fragment format.
//! We can be reasonably sure that whatever enters this parsing path is in the right format.
//! This feature must support:
//! - [x] Optional commas
//! - [ ] Children
//! - [ ] Keys
use super::AmbiguousElement;
use syn::parse::ParseBuffer;
use {
proc_macro2::TokenStream as TokenStream2,
quote::{quote, ToTokens, TokenStreamExt},
syn::{
parse::{Parse, ParseStream},
Ident, Result, Token,
},
};
pub struct Fragment {
children: Vec<AmbiguousElement>,
}
impl Parse for Fragment {
fn parse(input: ParseStream) -> Result<Self> {
input.parse::<Ident>()?;
let children = Vec::new();
// parse the guts
let content: ParseBuffer;
syn::braced!(content in input);
while !content.is_empty() {
content.parse::<AmbiguousElement>()?;
if content.peek(Token![,]) {
let _ = content.parse::<Token![,]>();
}
}
Ok(Self { children })
}
}
impl ToTokens for Fragment {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let childs = &self.children;
let children = quote! {
ChildrenList::new(__cx)
#( .add_child(#childs) )*
.finish()
};
tokens.append_all(quote! {
// #key_token,
dioxus::builder::vfragment(
__cx,
None,
#children
)
})
}
}
+74 -6
View File
@@ -11,17 +11,85 @@
//! //!
//! Any errors in using rsx! will likely occur when people start using it, so the first errors must be really helpful. //! Any errors in using rsx! will likely occur when people start using it, so the first errors must be really helpful.
mod ambiguous;
mod body;
mod component; mod component;
mod element; mod element;
mod fragment;
mod node; mod node;
// Re-export the namespaces into each other // Re-export the namespaces into each other
pub use ambiguous::*;
pub use body::*;
pub use component::*; pub use component::*;
pub use element::*; pub use element::*;
pub use fragment::*;
pub use node::*; pub use node::*;
// imports
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens, TokenStreamExt};
use syn::{
parse::{Parse, ParseStream},
Ident, Result, Token,
};
pub struct CallBody {
custom_context: Option<Ident>,
roots: Vec<BodyNode>,
}
impl Parse for CallBody {
fn parse(input: ParseStream) -> Result<Self> {
let custom_context = if input.peek(Ident) && input.peek2(Token![,]) {
let name = input.parse::<Ident>()?;
input.parse::<Token![,]>()?;
Some(name)
} else {
None
};
let mut roots = Vec::new();
while !input.is_empty() {
let node = input.parse::<BodyNode>()?;
if input.peek(Token![,]) {
let _ = input.parse::<Token![,]>();
}
roots.push(node);
}
Ok(Self {
custom_context,
roots,
})
}
}
/// Serialize the same way, regardless of flavor
impl ToTokens for CallBody {
fn to_tokens(&self, out_tokens: &mut TokenStream2) {
let inner = if self.roots.len() == 1 {
let inner = &self.roots[0];
quote! { #inner }
} else {
let childs = &self.roots;
quote! { __cx.fragment_root([ #(#childs),* ]) }
};
match &self.custom_context {
// The `in cx` pattern allows directly rendering
Some(ident) => out_tokens.append_all(quote! {
#ident.render(LazyNodes::new_some(move |__cx: NodeFactory| -> VNode {
use dioxus_elements::{GlobalAttributes, SvgAttributes};
#inner
}))
}),
// Otherwise we just build the LazyNode wrapper
None => out_tokens.append_all(quote! {
LazyNodes::new_some(move |__cx: NodeFactory| -> VNode {
use dioxus_elements::{GlobalAttributes, SvgAttributes};
#inner
})
}),
};
}
}
+54 -39
View File
@@ -4,34 +4,67 @@ use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens, TokenStreamExt}; use quote::{quote, ToTokens, TokenStreamExt};
use syn::{ use syn::{
parse::{Parse, ParseStream}, parse::{Parse, ParseStream},
token, Expr, LitStr, Result, token, Expr, LitStr, Result, Token,
}; };
// ============================================== /*
// Parse any div {} as a VElement Parse
// ============================================== -> div {}
-> Component {}
-> component()
-> "text {with_args}"
-> (0..10).map(|f| rsx!("asd")), // <--- notice the comma - must be a complete expr
*/
pub enum BodyNode { pub enum BodyNode {
Element(AmbiguousElement), Element(Element),
Text(TextNode), Component(Component),
Text(LitStr),
RawExpr(Expr), RawExpr(Expr),
} }
impl Parse for BodyNode { impl Parse for BodyNode {
fn parse(stream: ParseStream) -> Result<Self> { fn parse(stream: ParseStream) -> Result<Self> {
// Supposedly this approach is discouraged due to inability to return proper errors
// TODO: Rework this to provide more informative errors
if stream.peek(token::Brace) {
let content;
syn::braced!(content in stream);
return Ok(BodyNode::RawExpr(content.parse::<Expr>()?));
}
if stream.peek(LitStr) { if stream.peek(LitStr) {
return Ok(BodyNode::Text(stream.parse::<TextNode>()?)); return Ok(BodyNode::Text(stream.parse()?));
} }
Ok(BodyNode::Element(stream.parse::<AmbiguousElement>()?)) // div {} -> el
// Div {} -> comp
if stream.peek(syn::Ident) && stream.peek2(token::Brace) {
if stream
.fork()
.parse::<Ident>()?
.to_string()
.chars()
.next()
.unwrap()
.is_ascii_uppercase()
{
return Ok(BodyNode::Component(stream.parse()?));
} else {
return Ok(BodyNode::Element(stream.parse::<Element>()?));
}
}
// component() -> comp
// ::component {} -> comp
// ::component () -> comp
if (stream.peek(syn::Ident) && stream.peek2(token::Paren))
|| (stream.peek(Token![::]))
|| (stream.peek(Token![:]) && stream.peek2(Token![:]))
{
return Ok(BodyNode::Component(stream.parse::<Component>()?));
}
// crate::component{} -> comp
// crate::component() -> comp
if let Ok(pat) = stream.fork().parse::<syn::Path>() {
if pat.segments.len() > 1 {
return Ok(BodyNode::Component(stream.parse::<Component>()?));
}
}
Ok(BodyNode::RawExpr(stream.parse::<Expr>()?))
} }
} }
@@ -39,31 +72,13 @@ impl ToTokens for BodyNode {
fn to_tokens(&self, tokens: &mut TokenStream2) { fn to_tokens(&self, tokens: &mut TokenStream2) {
match &self { match &self {
BodyNode::Element(el) => el.to_tokens(tokens), BodyNode::Element(el) => el.to_tokens(tokens),
BodyNode::Text(txt) => txt.to_tokens(tokens), BodyNode::Component(comp) => comp.to_tokens(tokens),
BodyNode::Text(txt) => tokens.append_all(quote! {
__cx.text(format_args_f!(#txt))
}),
BodyNode::RawExpr(exp) => tokens.append_all(quote! { BodyNode::RawExpr(exp) => tokens.append_all(quote! {
__cx.fragment_from_iter(#exp) __cx.fragment_from_iter(#exp)
}), }),
} }
} }
} }
// =======================================
// Parse just plain text
// =======================================
pub struct TextNode(LitStr);
impl Parse for TextNode {
fn parse(s: ParseStream) -> Result<Self> {
Ok(Self(s.parse()?))
}
}
impl ToTokens for TextNode {
fn to_tokens(&self, tokens: &mut TokenStream2) {
// todo: use heuristics to see if we can promote to &static str
let token_stream = &self.0.to_token_stream();
tokens.append_all(quote! {
__cx.text(format_args_f!(#token_stream))
});
}
}
+2 -2
View File
@@ -734,9 +734,9 @@ impl<'a, 'b> IntoVNode<'a> for LazyNodes<'a, 'b> {
} }
} }
impl IntoVNode<'_> for &'static str { impl<'b> IntoVNode<'_> for &'b str {
fn into_vnode(self, cx: NodeFactory) -> VNode { fn into_vnode(self, cx: NodeFactory) -> VNode {
cx.static_text(self) cx.text(format_args!("{}", self))
} }
} }
+1 -1
View File
@@ -44,7 +44,7 @@ fn lists_work() {
static App: Component = |cx| { static App: Component = |cx| {
cx.render(rsx!( cx.render(rsx!(
h1 {"hello"} h1 {"hello"}
{(0..6).map(|f| rsx!(span{ "{f}" }))} (0..6).map(|f| rsx!(span{ "{f}" }))
)) ))
}; };
let mut vdom = VirtualDom::new(App); let mut vdom = VirtualDom::new(App);
+64
View File
@@ -215,3 +215,67 @@ impl<'a, O, T: std::ops::Not<Output = O> + Copy> std::ops::Not for UseState<'a,
!*self.get() !*self.get()
} }
} }
/*
Convenience methods for UseState.
Note!
This is not comprehensive.
This is *just* meant to make common operations easier.
*/
use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign};
impl<'a, T: Copy + Add<T, Output = T>> Add<T> for UseState<'a, T> {
type Output = T;
fn add(self, rhs: T) -> Self::Output {
self.inner.current_val.add(rhs)
}
}
impl<'a, T: Copy + Add<T, Output = T>> AddAssign<T> for UseState<'a, T> {
fn add_assign(&mut self, rhs: T) {
self.set(self.inner.current_val.add(rhs));
}
}
impl<'a, T: Copy + Sub<T, Output = T>> Sub<T> for UseState<'a, T> {
type Output = T;
fn sub(self, rhs: T) -> Self::Output {
self.inner.current_val.sub(rhs)
}
}
impl<'a, T: Copy + Sub<T, Output = T>> SubAssign<T> for UseState<'a, T> {
fn sub_assign(&mut self, rhs: T) {
self.set(self.inner.current_val.sub(rhs));
}
}
/// MUL
impl<'a, T: Copy + Mul<T, Output = T>> Mul<T> for UseState<'a, T> {
type Output = T;
fn mul(self, rhs: T) -> Self::Output {
self.inner.current_val.mul(rhs)
}
}
impl<'a, T: Copy + Mul<T, Output = T>> MulAssign<T> for UseState<'a, T> {
fn mul_assign(&mut self, rhs: T) {
self.set(self.inner.current_val.mul(rhs));
}
}
/// DIV
impl<'a, T: Copy + Div<T, Output = T>> Div<T> for UseState<'a, T> {
type Output = T;
fn div(self, rhs: T) -> Self::Output {
self.inner.current_val.div(rhs)
}
}
impl<'a, T: Copy + Div<T, Output = T>> DivAssign<T> for UseState<'a, T> {
fn div_assign(&mut self, rhs: T) {
self.set(self.inner.current_val.div(rhs));
}
}
+1 -1
View File
@@ -41,8 +41,8 @@ pub fn Link<'a, R: Routable>(cx: Scope<'a, LinkProps<'a, R>>) -> Element {
a { a {
href: "#", href: "#",
class: format_args!("{}", cx.props.class.unwrap_or("")), class: format_args!("{}", cx.props.class.unwrap_or("")),
{&cx.props.children}
onclick: move |_| service.push_route(cx.props.to.clone()), onclick: move |_| service.push_route(cx.props.to.clone()),
{&cx.props.children}
} }
}) })
} }
+7 -8
View File
@@ -309,19 +309,18 @@ mod tests {
static SLIGHTLY_MORE_COMPLEX: Component = |cx| { static SLIGHTLY_MORE_COMPLEX: Component = |cx| {
cx.render(rsx! { cx.render(rsx! {
div { div { title: "About W3Schools",
title: "About W3Schools" (0..20).map(|f| rsx!{
{(0..20).map(|f| rsx!{
div { div {
title: "About W3Schools" title: "About W3Schools",
style: "color:blue;text-align:center" style: "color:blue;text-align:center",
class: "About W3Schools" class: "About W3Schools",
p { p {
title: "About W3Schools" title: "About W3Schools",
"Hello world!: {f}" "Hello world!: {f}"
} }
} }
})} })
} }
}) })
}; };