mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-23 12:43:08 +00:00
Merge pull request #52 from DioxusLabs/jk/polish
polish: stricter, more flexible rsx macro, docs
This commit is contained in:
commit
8d26173345
140 changed files with 2346 additions and 2076 deletions
1
.vscode/spellright.dict
vendored
1
.vscode/spellright.dict
vendored
|
@ -67,3 +67,4 @@ SegVec
|
|||
contentful
|
||||
Jank
|
||||
noderef
|
||||
reborrow
|
||||
|
|
22
Cargo.toml
22
Cargo.toml
|
@ -11,18 +11,19 @@ documentation = "https://dioxuslabs.com"
|
|||
keywords = ["dom", "ui", "gui", "react", "wasm"]
|
||||
|
||||
[dependencies]
|
||||
dioxus-core = { path = "./packages/core", version = "^0.1.3" }
|
||||
dioxus-html = { path = "./packages/html", optional = true }
|
||||
dioxus-core-macro = { path = "./packages/core-macro", optional = true }
|
||||
dioxus-core = { path = "./packages/core", version = "^0.1.4" }
|
||||
dioxus-html = { path = "./packages/html", version = "^0.1.1", optional = true }
|
||||
dioxus-core-macro = { path = "./packages/core-macro", version = "^0.1.3", optional = true }
|
||||
dioxus-hooks = { path = "./packages/hooks", optional = true }
|
||||
|
||||
dioxus-ssr = { path = "./packages/ssr", optional = true }
|
||||
dioxus-web = { path = "./packages/web", optional = true }
|
||||
dioxus-web = { path = "./packages/web", version = "^0.0.1", optional = true }
|
||||
dioxus-desktop = { path = "./packages/desktop", optional = true }
|
||||
dioxus-ssr = { path = "./packages/ssr", optional = true }
|
||||
|
||||
dioxus-router = { path = "./packages/router", optional = true }
|
||||
|
||||
dioxus-mobile = { path = "./packages/mobile", optional = true }
|
||||
dioxus-liveview = { path = "./packages/liveview", optional = true }
|
||||
# dioxus-mobile = { path = "./packages/mobile", optional = true }
|
||||
# dioxus-liveview = { path = "./packages/liveview", optional = true }
|
||||
|
||||
[features]
|
||||
default = ["macro", "hooks", "html"]
|
||||
|
@ -31,11 +32,13 @@ macro = ["dioxus-core-macro"]
|
|||
hooks = ["dioxus-hooks"]
|
||||
html = ["dioxus-html"]
|
||||
router = ["dioxus-router"]
|
||||
liveview = ["dioxus-liveview"]
|
||||
ssr = ["dioxus-ssr"]
|
||||
web = ["dioxus-web", "dioxus-router/web"]
|
||||
desktop = ["dioxus-desktop", "dioxus-router/desktop"]
|
||||
mobile = ["dioxus-mobile"]
|
||||
|
||||
|
||||
# mobile = ["dioxus-mobile"]
|
||||
# liveview = ["dioxus-liveview"]
|
||||
|
||||
|
||||
[workspace]
|
||||
|
@ -60,7 +63,6 @@ im-rc = "15.0.0"
|
|||
fxhash = "0.2.1"
|
||||
anyhow = "1.0.51"
|
||||
serde_json = "1.0.73"
|
||||
simple_logger = "1.16.0"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
|
||||
argh = "0.1.7"
|
||||
|
|
18
README.md
18
README.md
|
@ -51,7 +51,7 @@ fn app(cx: Scope) -> Element {
|
|||
button { onclick: move |_| count += 1, "Up high!" }
|
||||
button { onclick: move |_| count -= 1, "Down low!" }
|
||||
))
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Dioxus can be used to deliver webapps, desktop apps, static sites, liveview apps, mobile apps (WIP), and more. At its core, Dioxus is entirely renderer agnostic and has great documentation for creating new renderers for any platform.
|
||||
|
@ -73,21 +73,27 @@ If you know React, then you already know Dioxus.
|
|||
<th><a href="https://dioxuslabs.com/guide/">Web</a></th>
|
||||
<th><a href="https://dioxuslabs.com/guide/">Desktop</a></th>
|
||||
<th><a href="https://dioxuslabs.com/guide/">Mobile</a></th>
|
||||
<th><a href="https://dioxuslabs.com/guide/">TUI</a></th>
|
||||
<th><a href="https://dioxuslabs.com/guide/">State</a></th>
|
||||
<th><a href="https://dioxuslabs.com/guide/">Docs</a></th>
|
||||
<th><a href="https://dioxuslabs.com/guide/">Tools</a></th>
|
||||
<tr>
|
||||
</table>
|
||||
|
||||
## Examples:
|
||||
|
||||
| File Navigator (Desktop) | WiFi scanner (Desktop) | TodoMVC (All platforms) | Ecommerce w/ Tailwind (Liveview) |
|
||||
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| [![File Explorer](https://github.com/DioxusLabs/example-projects/raw/master/file-explorer/image.png)](https://github.com/DioxusLabs/example-projects/blob/master/file-explorer) | [![Wifi Scanner Demo](https://github.com/DioxusLabs/example-projects/raw/master/wifi-scanner/demo_small.png)](https://github.com/DioxusLabs/example-projects/blob/master/wifi-scanner) | [![TodoMVC example](https://github.com/DioxusLabs/example-projects/raw/master/todomvc/example.png)](https://github.com/DioxusLabs/example-projects/blob/master/todomvc) | [![Ecommerce Example](https://github.com/DioxusLabs/example-projects/raw/master/ecommerce-site/demo.png)](https://github.com/DioxusLabs/example-projects/blob/master/ecommerce-site) |
|
||||
|
||||
## Examples Projects:
|
||||
|
||||
| File Navigator (Desktop) | WiFi scanner (Desktop) | TodoMVC (All platforms) | E-commerce w/ Tailwind (Liveview) |
|
||||
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [![File Explorer](https://github.com/DioxusLabs/example-projects/raw/master/file-explorer/image.png)](https://github.com/DioxusLabs/example-projects/blob/master/file-explorer) | [![Wifi Scanner Demo](https://github.com/DioxusLabs/example-projects/raw/master/wifi-scanner/demo_small.png)](https://github.com/DioxusLabs/example-projects/blob/master/wifi-scanner) | [![TodoMVC example](https://github.com/DioxusLabs/example-projects/raw/master/todomvc/example.png)](https://github.com/DioxusLabs/example-projects/blob/master/todomvc) | [![E-commerce Example](https://github.com/DioxusLabs/example-projects/raw/master/ecommerce-site/demo.png)](https://github.com/DioxusLabs/example-projects/blob/master/ecommerce-site) |
|
||||
|
||||
|
||||
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?
|
||||
|
||||
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.
|
||||
|
|
|
@ -18,7 +18,7 @@ fn App(cx: Scope) -> Element {
|
|||
|
||||
In general, Dioxus and React share many functional similarities. If this guide is lacking in any general concept or an error message is confusing, React's documentation might be more helpful. We are dedicated to providing a *familiar* toolkit for UI in Rust, so we've chosen to follow in the footsteps of popular UI frameworks (React, Redux, etc). If you know React, then you already know Dioxus. If you don't know either, this guide will still help you!
|
||||
|
||||
> This is introduction book! For advanced topics, check out the [Reference](https://dioxuslabs.com/reference) instead.
|
||||
> This is an introduction book! For advanced topics, check out the [Reference](https://dioxuslabs.com/reference) instead.
|
||||
|
||||
## Multiplatform
|
||||
|
||||
|
@ -29,6 +29,7 @@ Right now, we have several 1st-party renderers:
|
|||
- Tao/Tokio (for Desktop apps)
|
||||
- Tao/Tokio (for Mobile apps)
|
||||
- SSR (for generating static markup)
|
||||
- TUI/Rink (for terminal-based apps)
|
||||
|
||||
### Web Support
|
||||
---
|
||||
|
@ -40,12 +41,10 @@ Because the web is a fairly mature platform, we expect there to be very little A
|
|||
[Jump to the getting started guide for the web.]()
|
||||
|
||||
Examples:
|
||||
- [TodoMVC](https://github.com/dioxusLabs/todomvc/)
|
||||
- [ECommerce]()
|
||||
- [Photo Editor]()
|
||||
|
||||
[![TODOMVC](https://github.com/DioxusLabs/todomvc/raw/master/example.png)](https://github.com/dioxusLabs/todomvc/)
|
||||
- [TodoMVC](https://github.com/DioxusLabs/example-projects/tree/master/todomvc)
|
||||
- [ECommerce](https://github.com/DioxusLabs/example-projects/tree/master/ecommerce-site)
|
||||
|
||||
[![TodoMVC example](https://github.com/DioxusLabs/example-projects/raw/master/todomvc/example.png)](https://github.com/DioxusLabs/example-projects/blob/master/todomvc)
|
||||
### SSR Support
|
||||
---
|
||||
Dioxus supports server-side rendering!
|
||||
|
@ -59,7 +58,7 @@ let contents = dioxus::ssr::render_vdom(&dom);
|
|||
[Jump to the getting started guide for SSR.]()
|
||||
|
||||
Examples:
|
||||
- [Example DocSite]()
|
||||
- [Example DocSite](https://github.com/dioxusLabs/docsite)
|
||||
- [Tide WebServer]()
|
||||
- [Markdown to fancy HTML generator]()
|
||||
|
||||
|
@ -72,9 +71,8 @@ Desktop APIs will likely be in flux as we figure out better patterns than our El
|
|||
[Jump to the getting started guide for Desktop.]()
|
||||
|
||||
Examples:
|
||||
- [File explorer]()
|
||||
- [Bluetooth scanner]()
|
||||
- [Device Viewer]()
|
||||
- [File explorer](https://github.com/dioxusLabs/file-explorer/)
|
||||
- [WiFi scanner](https://github.com/DioxusLabs/example-projects/blob/master/wifi-scanner)
|
||||
|
||||
[![File ExplorerExample](https://github.com/DioxusLabs/file-explorer-example/raw/master/image.png)](https://github.com/dioxusLabs/file-explorer/)
|
||||
|
||||
|
@ -87,8 +85,7 @@ Mobile support is currently best suited for CRUD-style apps, ideally for interna
|
|||
[Jump to the getting started guide for Mobile.]()
|
||||
|
||||
Examples:
|
||||
- [Todo App]()
|
||||
- [Chat App]()
|
||||
- [Todo App](https://github.com/DioxusLabs/example-projects/blob/master/ios_demo)
|
||||
|
||||
### LiveView / Server Component Support
|
||||
---
|
||||
|
|
|
@ -3,96 +3,39 @@
|
|||
- [Introduction](README.md)
|
||||
- [Getting Setup](setup.md)
|
||||
- [Hello, World!](hello_world.md)
|
||||
- [Describing the UI](concepts/00-index.md)
|
||||
- [Intro to Elements](concepts/vnodes.md)
|
||||
- [Intro to Components](concepts/components.md)
|
||||
- [Reusing, Importing, and Exporting Components](concepts/exporting_components.md)
|
||||
- [Passing children and attributes](concepts/component_children.md)
|
||||
- [Conditional Rendering](concepts/conditional_rendering.md)
|
||||
- [Lists](concepts/lists.md)
|
||||
- [Adding Interactivity](concepts/interactivity.md)
|
||||
- [Hooks and Internal State](concepts/hooks.md)
|
||||
- [Event handlers](concepts/event_handlers.md)
|
||||
- [User Input and Controlled Components](concepts/user_input.md)
|
||||
- [Lifecycle, updates, and effects](concepts/lifecycles.md)
|
||||
|
||||
<!-- Responding to Events
|
||||
State: A Component's Memory
|
||||
Render and Commit
|
||||
State as a Snapshot
|
||||
Queueing a Series of State Updates
|
||||
Updating Objects in State
|
||||
Updating Arrays in State -->
|
||||
|
||||
<!-- Reacting to Input with State
|
||||
Choosing the State Structure
|
||||
Sharing State Between Components
|
||||
Preserving and Resetting State
|
||||
Extracting State Logic into a Reducer
|
||||
Passing Data Deeply with Context
|
||||
Scaling Up with Reducer and Context -->
|
||||
- [Managing State](concepts/managing_state.md)
|
||||
- [Global State](concepts/sharedstate.md)
|
||||
- [Error handling](concepts/errorhandling.md)
|
||||
- [Effects](concepts/effects.md)
|
||||
- [Working with Async](concepts/async.md)
|
||||
- [Tasks](concepts/asynctasks.md)
|
||||
- [Suspense](concepts/suspense.md)
|
||||
- [Async Callbacks](concepts/asynccallbacks.md)
|
||||
- [Putting it all together](tutorial/index.md)
|
||||
- [Describing the UI](elements/index.md)
|
||||
- [Intro to Elements](elements/vnodes.md)
|
||||
- [Intro to Components](elements/components.md)
|
||||
- [Reusing, Importing, and Exporting Components](elements/exporting_components.md)
|
||||
- [Passing children and attributes](elements/component_children.md)
|
||||
- [Conditional Rendering](elements/conditional_rendering.md)
|
||||
- [Lists](elements/lists.md)
|
||||
- [Adding Interactivity](interactivity/index.md)
|
||||
- [Hooks and Internal State](interactivity/hooks.md)
|
||||
- [Event handlers](interactivity/event_handlers.md)
|
||||
- [User Input and Controlled Components](interactivity/user_input.md)
|
||||
- [Lifecycle, updates, and effects](interactivity/lifecycles.md)
|
||||
- [Managing State](state/index.md)
|
||||
- [Local State](state/localstate.md)
|
||||
- [Lifting State](state/liftingstate.md)
|
||||
- [Global State](state/sharedstate.md)
|
||||
- [Error handling](state/errorhandling.md)
|
||||
- [Working with Async](async/index.md)
|
||||
- [Tasks](async/asynctasks.md)
|
||||
- [Putting it all together: Dog Search Engine](tutorial/index.md)
|
||||
- [New app](tutorial/new_app.md)
|
||||
- [Structuring our app](tutorial/structure.md)
|
||||
- [Defining State](tutorial/state.md)
|
||||
- [Defining Components](tutorial/components.md)
|
||||
- [Styling](tutorial/styling.md)
|
||||
- [Publishing](tutorial/publishing.md)
|
||||
- [Next Steps and Advanced Topics](final/index.md)
|
||||
<!-- - [Topics in Depth](depth/topics.md)
|
||||
- [RSX](depth/rsx.md)
|
||||
- [Components](depth/components.md)
|
||||
- [Props](depth/props.md)
|
||||
- [Memoization](depth/memoization.md)
|
||||
- [Performance](depth/performance.md)
|
||||
- [Testing](depth/testing.md)
|
||||
- [Advanced Guides](tutorial/advanced_guides.md)
|
||||
- [Memoization](concepts/memoization.md)
|
||||
- [RSX in Depth](concepts/rsx_in_depth.md)
|
||||
- [Building Elements with NodeFactory](concepts/rsx.md)
|
||||
- [Custom Elements](concepts/custom_elements.md)
|
||||
- [Custom Renderer](concepts/custom_renderer.md)
|
||||
- [Server-side components](concepts/server_side_components.md)
|
||||
- [Bundling and Distributing](concepts/bundline.md)
|
||||
- [Web]()
|
||||
- [Getting Started]()
|
||||
- [Down-casting Nodes]()
|
||||
- [Wrapping Web APIs]()
|
||||
- [SSR]()
|
||||
- [Wrapping Web APIs]()
|
||||
- [Desktop]()
|
||||
- [Wrapping Web APIs]()
|
||||
- [Mobile]()
|
||||
- [Wrapping Web APIs]()
|
||||
- [Reference Guide]()
|
||||
- [Anti-patterns]()
|
||||
- [Children]()
|
||||
- [Conditional Rendering]()
|
||||
- [Controlled Inputs]()
|
||||
- [Custom Elements]()
|
||||
- [Empty Components]()
|
||||
- [Error Handling]()
|
||||
- [Fragments]()
|
||||
- [Global CSS]()
|
||||
- [Inline Styles]()
|
||||
- [Iterators]()
|
||||
- [Listeners]()
|
||||
- [Memoization]()
|
||||
- [Node Refs]()
|
||||
- [Spread Pattern]()
|
||||
- [State Management]()
|
||||
- [Suspense]()
|
||||
- [task]()
|
||||
- [Testing]() -->
|
||||
- [Bundling](tutorial/publishing.md)
|
||||
- [Next Steps and Advanced Topics](final.md)
|
||||
|
||||
|
||||
-----------
|
||||
|
||||
[Contributors](misc/contributors.md)
|
||||
|
||||
|
||||
<!-- - [Suspense](concepts/suspense.md) -->
|
||||
<!-- - [Async Callbacks](concepts/asynccallbacks.md) -->
|
||||
|
|
|
@ -20,7 +20,7 @@ fn runs_in_browser() {
|
|||
|
||||
Then, when you run
|
||||
|
||||
```
|
||||
```console
|
||||
$ dioxus test --chrome
|
||||
```
|
||||
|
90
docs/guide/src/async/asynctasks.md
Normal file
90
docs/guide/src/async/asynctasks.md
Normal file
|
@ -0,0 +1,90 @@
|
|||
# Tasks
|
||||
|
||||
All async code in Dioxus must be explicit and handled through Dioxus' task system.
|
||||
|
||||
In this chapter, we'll learn how to spawn new tasks through our `Scope`.
|
||||
|
||||
## Spawning a task
|
||||
|
||||
You can push any `'static` future into the Dioxus future queue by simply calling `cx.spawn` to spawn a task. Pushing a future returns a `TaskId` which can then be used to cancel the
|
||||
|
||||
```rust
|
||||
fn App(cx: Scope) -> Element {
|
||||
cx.spawn(async {
|
||||
let mut count = 0;
|
||||
loop {
|
||||
tokio::time::delay(std::instant::Duration::from_millis(500)).await;
|
||||
count += 1;
|
||||
println!("Current count is {}", count);
|
||||
}
|
||||
});
|
||||
|
||||
None
|
||||
}
|
||||
```
|
||||
|
||||
The future must be `'static` - so any values captured by the task must not carry any references to `cx`. All the Dioxus hooks have a method called `for_async` which will create a slightly more limited handle to the hook for you to use in your async code.
|
||||
|
||||
```rust
|
||||
fn App(cx: Scope) -> Element {
|
||||
let mut count = use_state(&cx, || 0);
|
||||
|
||||
let taskid = cx.spawn({
|
||||
let mut count = count.for_async();
|
||||
async {
|
||||
loop {
|
||||
tokio::time::delay(std::instant::Duration::from_millis(500)).await;
|
||||
count += 1;
|
||||
println!("Current count is {}", count);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
The task will run in the background until it is completed.
|
||||
|
||||
> Note: `spawn` will always spawn a *new* future. You probably want to call it from a hook initializer instead of the main body of your component.
|
||||
|
||||
When bringing lots of values into your task, we provide the `for_async!` macro which will can `for_async` on all values passed in. For types that implement `ToOwned`, `for_async!` will simply call `ToOwned` for that value.
|
||||
|
||||
```rust
|
||||
fn App(cx: Scope) -> Element {
|
||||
let mut age = use_state(&cx, || 0);
|
||||
let mut name = use_state(&cx, || "Bob");
|
||||
let mut description = use_state(&cx, || "asd".to_string());
|
||||
|
||||
let taskid = cx.spawn({
|
||||
for_async![count, name, description]
|
||||
async { /* code that uses count/name/description */ }
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Details of Tasks
|
||||
|
||||
Calling `spawn` is *not* a hook and will *always* generate a new task. Make sure to only spawn tasks when you want to. You should *probably* not call `spawn` in the main body of your component, since a new task will be spawned on every render.
|
||||
|
||||
## Spawning Tokio Tasks (for multithreaded use cases)
|
||||
|
||||
Sometimes, you might want to spawn a background task that needs multiple threads or talk to hardware that might block your app code. In these cases, we can can directly spawn a `tokio task` from our future. For Dioxus-Desktop, your task will be spawned onto Tokio's Multithreaded runtime:
|
||||
|
||||
```rust
|
||||
cx.spawn({
|
||||
tokio::spawn(async {
|
||||
// some multithreaded work
|
||||
}).await;
|
||||
|
||||
tokio::spawn_blocking(|| {
|
||||
// some extremely blocking work
|
||||
}).await;
|
||||
|
||||
tokio::spawn_local(|| {
|
||||
// some !Send work
|
||||
}).await;
|
||||
})
|
||||
```
|
||||
|
||||
> Note: Tokio tasks must be `Send`. Most hooks are `Send` compatible, but if they aren't, then you can use `spawn_local` to spawn onto Dioxus-Desktop's `localset`.
|
||||
|
||||
|
19
docs/guide/src/async/index.md
Normal file
19
docs/guide/src/async/index.md
Normal file
|
@ -0,0 +1,19 @@
|
|||
# Working with Async
|
||||
|
||||
Not all apps you'll build can be self-contained with synchronous code. You'll often need to interact with file systems, network interfaces, hardware, or timers.
|
||||
|
||||
So far, we've only talked about building apps with synchronous code, so this chapter will focus integrating asynchronous code into your app.
|
||||
|
||||
|
||||
## The Runtime
|
||||
|
||||
By default, Dioxus-Desktop ships with the `Tokio` runtime and automatically sets everything up for you.
|
||||
|
||||
|
||||
|
||||
## Send/Sync
|
||||
Writing apps that deal with Send/Sync can be frustrating at times. Under the hood, Dioxus is not currently thread-safe, so any async code you write does *not* need to be `Send/Sync`. That means Cell/Rc/RefCell are all fair game.
|
||||
|
||||
|
||||
|
||||
All async code in your app is polled on a `LocalSet`, so any async code we w
|
|
@ -1 +0,0 @@
|
|||
# Working with Async
|
|
@ -1 +0,0 @@
|
|||
# Async Callbacks
|
|
@ -1 +0,0 @@
|
|||
# Tasks
|
|
@ -1 +0,0 @@
|
|||
# Bundling and Distributing
|
|
@ -1 +0,0 @@
|
|||
# Custom Elements
|
|
@ -1 +0,0 @@
|
|||
# Custom Renderer
|
|
@ -1 +0,0 @@
|
|||
# Effects
|
|
@ -1,11 +0,0 @@
|
|||
# Handling Events
|
||||
|
||||
In the overview for this section, we mentioned how we can modify the state of our component by responding to user-generated events inside of event listeners.
|
||||
|
||||
In this section, we'll talk more about event listeners:
|
||||
- What events are available
|
||||
- Handling event data
|
||||
- Mutability in event listeners
|
||||
|
||||
## Event Listeners
|
||||
|
|
@ -1 +0,0 @@
|
|||
# Managing State
|
|
@ -1 +0,0 @@
|
|||
# Memoization
|
|
@ -1 +0,0 @@
|
|||
# Server-side components
|
|
@ -1,54 +0,0 @@
|
|||
# Suspense
|
||||
|
||||
Suspense in Dioxus is enabled through placeholder nodes.
|
||||
|
||||
just a component that renders nodes into the placeholder after the future is finished?
|
||||
|
||||
in react, suspense is just completely pausing diffing while a
|
||||
|
||||
```rust
|
||||
let n = use_suspense(cx || {
|
||||
cx.render(rsx!{
|
||||
Suspense {
|
||||
prom: fut,
|
||||
callback: || {}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
suspense () {
|
||||
let value = use_state();
|
||||
if first_render {
|
||||
push_task({
|
||||
value.set(fut.await);
|
||||
});
|
||||
} else {
|
||||
callback(value)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let name = fetch_name().await;
|
||||
|
||||
|
||||
function ProfileDetails() {
|
||||
// Try to read user info, although it might not have loaded yet
|
||||
const user = resource.read();
|
||||
return <h1>{user.name}</h1>;
|
||||
}
|
||||
|
||||
|
||||
fn ProfileDteails() {
|
||||
let user = resource.suspend(cx, |l| rsx!("{l}"));
|
||||
|
||||
// waits for the resource to be ready and updates the placeholder with the tree
|
||||
let name = resource.suspend_with(cx, |val| rsx!( div { "hello" "{user}" } ));
|
||||
|
||||
cx.render(rsx!(
|
||||
div {
|
||||
{user}
|
||||
{name}
|
||||
}
|
||||
))
|
||||
}
|
||||
```
|
|
@ -1 +0,0 @@
|
|||
# Fundamental Hooks and use_hook
|
|
@ -30,7 +30,7 @@ struct ClickableProps<'a> {
|
|||
title: &'a str
|
||||
}
|
||||
|
||||
fn clickable(cx: Scope<ClickableProps>) -> Element {
|
||||
fn Clickable(cx: Scope<ClickableProps>) -> Element {
|
||||
cx.render(rsx!(
|
||||
a {
|
||||
href: "{cx.props.href}"
|
||||
|
@ -44,10 +44,10 @@ And then use it in our code like so:
|
|||
|
||||
```rust
|
||||
rsx!(
|
||||
clickable(
|
||||
Clickable {
|
||||
href: "https://google.com"
|
||||
title: "Link to Google"
|
||||
)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
|
@ -64,11 +64,11 @@ struct ClickableProps<'a> {
|
|||
body: Element<'a>
|
||||
}
|
||||
|
||||
fn clickable(cx: Scope<ClickableProps>) -> Element {
|
||||
fn Clickable(cx: Scope<ClickableProps>) -> Element {
|
||||
cx.render(rsx!(
|
||||
a {
|
||||
href: "{cx.props.href}"
|
||||
{&cx.props.body}
|
||||
href: "{cx.props.href}",
|
||||
&cx.props.body
|
||||
}
|
||||
))
|
||||
}
|
||||
|
@ -78,15 +78,17 @@ Then, at the call site, we can render some nodes and pass them in:
|
|||
|
||||
```rust
|
||||
rsx!(
|
||||
clickable(
|
||||
Clickable {
|
||||
href: "https://google.com"
|
||||
body: cx.render(rsx!(
|
||||
img { src: "https://www.google.com/logos/doodles/2021/seasonal-holidays-2021-6753651837109324-6752733080595603-cst.gif" }
|
||||
img { src: "https://www.google.com/logos/doodles/..." }
|
||||
))
|
||||
)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## Auto Conversion of the `Children` field
|
||||
|
||||
This pattern can become tedious in some instances, so Dioxus actually performs an implicit conversion of any `rsx` calls inside components into `Elements` at the `children` field. This means you must explicitly declare if a component can take children.
|
||||
|
||||
```rust
|
||||
|
@ -99,26 +101,26 @@ struct ClickableProps<'a> {
|
|||
fn clickable(cx: Scope<ClickableProps>) -> Element {
|
||||
cx.render(rsx!(
|
||||
a {
|
||||
href: "{cx.props.href}"
|
||||
{&cx.props.children}
|
||||
href: "{cx.props.href}",
|
||||
&cx.props.children
|
||||
}
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
And to call `clickable`:
|
||||
Now, whenever we use `Clickable` in another component, we don't need to call `render` on child nodes - it will happen automatically!
|
||||
```rust
|
||||
rsx!(
|
||||
clickable(
|
||||
Clickable {
|
||||
href: "https://google.com"
|
||||
img { src: "https://www.google.com/logos/doodles/2021/seasonal-holidays-2021-6753651837109324-6752733080595603-cst.gif" }
|
||||
)
|
||||
img { src: "https://www.google.com/logos/doodles/...." }
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
> Note: Passing children into components will break any memoization due to the associated lifetime.
|
||||
|
||||
While technically allowed, it's an antipattern to pass children more than once in a component and will probably break your app significantly.
|
||||
While technically allowed, it's an antipattern to pass children more than once in a component and will probably cause your app to crash.
|
||||
|
||||
However, because the `Element` is transparently a `VNode`, we can actually match on it to extract the nodes themselves, in case we are expecting a specific format:
|
||||
|
||||
|
@ -142,10 +144,10 @@ In the cases where you need to pass arbitrary element properties into a componen
|
|||
```rust
|
||||
|
||||
rsx!(
|
||||
clickable(
|
||||
Clickable {
|
||||
"class": "blue-button",
|
||||
"style": "background: red;"
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
```
|
||||
|
@ -161,7 +163,7 @@ struct ClickableProps<'a> {
|
|||
fn clickable(cx: Scope<ClickableProps>) -> Element {
|
||||
cx.render(rsx!(
|
||||
a {
|
||||
..{cx.props.attributes},
|
||||
..cx.props.attributes,
|
||||
"Any link, anywhere"
|
||||
}
|
||||
))
|
||||
|
@ -195,9 +197,9 @@ Then, we can attach a listener at the call site:
|
|||
|
||||
```rust
|
||||
rsx!(
|
||||
clickable(
|
||||
Clickable {
|
||||
onclick: move |_| log::info!("Clicked"),
|
||||
)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
|
@ -110,7 +110,7 @@ When declaring a component in `rsx!`, we can pass in properties using the tradit
|
|||
|
||||
Let's take a look at the `VoteButton` component. For now, we won't include any interactivity - just the rendering the score and buttons to the screen.
|
||||
|
||||
Most of your Components will look exactly like this: a Props struct and a render function. Every component must take a tuple of `Scope` and `&Props` and return an `Element`.
|
||||
Most of your Components will look exactly like this: a Props struct and a render function. Every component must take a `Scope` generic over some `Props` and return an `Element`.
|
||||
|
||||
As covered before, we'll build our User Interface with the `rsx!` macro and HTML tags. However, with components, we must actually "render" our HTML markup. Calling `cx.render` converts our "lazy" `rsx!` structure into an `Element`.
|
||||
|
||||
|
@ -145,7 +145,7 @@ struct TitleCardProps<'a> {
|
|||
title: &'a str,
|
||||
}
|
||||
|
||||
fn TitleCard(cx: Scope<TitleCardProps>) -> Element {
|
||||
fn TitleCard<'a>(cx: Scope<'a, TitleCardProps<'a>>) -> Element {
|
||||
cx.render(rsx!{
|
||||
h1 { "{cx.props.title}" }
|
||||
})
|
||||
|
@ -156,6 +156,42 @@ For users of React: Dioxus knows *not* to memoize components that borrow propert
|
|||
|
||||
This means that during the render process, a newer version of `TitleCardProps` will never be compared with a previous version, saving some clock cycles.
|
||||
|
||||
## The inline_props macro
|
||||
|
||||
Yes - *another* macro! However, this one is entirely optional.
|
||||
|
||||
For internal components, we provide the `inline_props` macro, which will let you embed your `Props` definition right into the function arguments of your component.
|
||||
|
||||
Our title card above would be transformed from:
|
||||
|
||||
```rust
|
||||
#[derive(Props, PartialEq)]
|
||||
struct TitleCardProps {
|
||||
title: String,
|
||||
}
|
||||
|
||||
fn TitleCard(cx: Scope<TitleCardProps>) -> Element {
|
||||
cx.render(rsx!{
|
||||
h1 { "{cx.props.title}" }
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```rust
|
||||
#[inline_props]
|
||||
fn TitleCard(cx: Scope, title: String) -> Element {
|
||||
cx.render(rsx!{
|
||||
h1 { "{title}" }
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Again, this macro is optional and should not be used by library authors since you have less fine-grained control over documentation and optionality.
|
||||
|
||||
However, it's great for quickly throwing together an app without dealing with *any* extra boilerplate.
|
||||
|
||||
## The `Scope` object
|
||||
|
||||
Though very similar to React, Dioxus is different in a few ways. Most notably, React components will not have a `Scope` parameter in the component declaration.
|
|
@ -101,14 +101,14 @@ fn App(cx: Scope)-> Element {
|
|||
|
||||
## Nesting RSX
|
||||
|
||||
By looking at other examples, you might have noticed that it's possible to include `rsx!` calls inside other `rsx!` calls. With the curly-brace syntax, we can include anything in our `rsx!` that implements `IntoVnodeList`: a marker trait for iterators that produce Elements. `rsx!` itself implements this trait, so we can include it directly:
|
||||
By looking at other examples, you might have noticed that it's possible to include `rsx!` calls inside other `rsx!` calls. We can include anything in our `rsx!` that implements `IntoVnodeList`: a marker trait for iterators that produce Elements. `rsx!` itself implements this trait, so we can include it directly:
|
||||
|
||||
```rust
|
||||
rsx!(
|
||||
div {
|
||||
{rsx!(
|
||||
rsx!(
|
||||
"more rsx!"
|
||||
)}
|
||||
)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
@ -120,7 +120,7 @@ let title = rsx!( "more rsx!" );
|
|||
|
||||
rsx!(
|
||||
div {
|
||||
{title}
|
||||
title
|
||||
}
|
||||
)
|
||||
```
|
||||
|
@ -136,7 +136,7 @@ let screen = match logged_in {
|
|||
|
||||
cx.render(rsx!{
|
||||
Navbar {}
|
||||
{screen}
|
||||
screen,
|
||||
Footer {}
|
||||
})
|
||||
```
|
||||
|
@ -152,9 +152,9 @@ By default, Rust lets you convert any `boolean` into any other type by calling `
|
|||
let show_title = true;
|
||||
rsx!(
|
||||
div {
|
||||
{show_title.and_then(|| rsx!{
|
||||
show_title.and_then(|| rsx!{
|
||||
"This is the title"
|
||||
})}
|
||||
})
|
||||
}
|
||||
)
|
||||
```
|
||||
|
@ -164,20 +164,23 @@ We can use this pattern for many things, including options:
|
|||
let user_name = Some("bob");
|
||||
rsx!(
|
||||
div {
|
||||
{user_name.map(|name| rsx!("Hello {name}"))}
|
||||
user_name.map(|name| rsx!("Hello {name}"))
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## Rendering Nothing
|
||||
|
||||
Sometimes, you don't want your component to return anything at all. In these cases, we can pass "None" into our bracket. However, Rust is not able to infer that our `None` corresponds to `Element` so we need to cast it appropriately:
|
||||
Sometimes, you don't want your component to return anything at all. Under the hood, the `Element` type is just an alias for `Option<VNode>`, so you can simply return `None`.
|
||||
|
||||
This can be helpful in certain patterns where you need to perform some logical side-effects but don't want to render anything.
|
||||
|
||||
```rust
|
||||
rsx!(cx, {None as Element} )
|
||||
fn demo(cx: Scope) -> Element {
|
||||
None
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Moving Forward:
|
||||
|
||||
In this chapter, we learned how to render different Elements from a Component depending on a condition. This is a very powerful building block to assemble complex User Interfaces!
|
|
@ -25,27 +25,27 @@ fn main() {
|
|||
dioxus::desktop::launch(App);
|
||||
}
|
||||
|
||||
fn App((cx, props): Component) -> Element {}
|
||||
fn App(Scope) -> Element {}
|
||||
|
||||
#[derive(PartialEq, Props)]
|
||||
struct PostProps{}
|
||||
fn Post((cx, props): Component<PostProps>) -> Element {}
|
||||
fn Post(Scope<PostProps>) -> Element {}
|
||||
|
||||
#[derive(PartialEq, Props)]
|
||||
struct VoteButtonsProps {}
|
||||
fn VoteButtons((cx, props): Component<VoteButtonsProps>) -> Element {}
|
||||
fn VoteButtons(Scope<VoteButtonsProps>) -> Element {}
|
||||
|
||||
#[derive(PartialEq, Props)]
|
||||
struct TitleCardProps {}
|
||||
fn TitleCard((cx, props): Component<TitleCardProps>) -> Element {}
|
||||
fn TitleCard(Scope<TitleCardProps>) -> Element {}
|
||||
|
||||
#[derive(PartialEq, Props)]
|
||||
struct MetaCardProps {}
|
||||
fn MetaCard((cx, props): Component<MetaCardProps>) -> Element {}
|
||||
fn MetaCard(Scope<MetaCardProps>) -> Element {}
|
||||
|
||||
#[derive(PartialEq, Props)]
|
||||
struct ActionCardProps {}
|
||||
fn ActionCard((cx, props): Component<ActionCardProps>) -> Element {}
|
||||
fn ActionCard(Scope<ActionCardProps>) -> Element {}
|
||||
```
|
||||
|
||||
That's a lot of components for one file! We've successfully refactored our app into components, but we should probably start breaking it up into a file for each component.
|
||||
|
@ -61,7 +61,7 @@ use dioxus::prelude::*;
|
|||
|
||||
#[derive(PartialEq, Props)]
|
||||
struct ActionCardProps {}
|
||||
fn ActionCard((cx, props): Component<ActionCardProps>) -> Element {}
|
||||
fn ActionCard(Scope<ActionCardProps>) -> Element {}
|
||||
```
|
||||
|
||||
We should also create a `mod.rs` file in the `post` folder so we can use it from our `main.rs`. Our `Post` component and its props will go into this file.
|
||||
|
@ -92,7 +92,7 @@ fn main() {
|
|||
|
||||
mod post;
|
||||
|
||||
fn App((cx, props): Component) -> Element {
|
||||
fn App(Scope) -> Element {
|
||||
cx.render(rsx!{
|
||||
post::Post {
|
||||
id: Uuid::new_v4(),
|
||||
|
@ -116,7 +116,7 @@ use dioxus::prelude::*;
|
|||
|
||||
#[derive(PartialEq, Props)]
|
||||
pub struct PostProps {}
|
||||
pub fn Post((cx, props): Component<PostProps>) -> Element {}
|
||||
pub fn Post(Scope<PostProps>) -> Element {}
|
||||
```
|
||||
|
||||
While we're here, we also need to make sure each of our subcomponents are included as modules and exported.
|
||||
|
@ -142,7 +142,7 @@ pub struct PostProps {
|
|||
original_poster: String
|
||||
}
|
||||
|
||||
pub fn Post((cx, props): Component<PostProps>) -> Element {
|
||||
pub fn Post(Scope<PostProps>) -> Element {
|
||||
cx.render(rsx!{
|
||||
div { class: "post-container"
|
||||
vote::VoteButtons {
|
||||
|
@ -191,7 +191,7 @@ fn main() {
|
|||
|
||||
mod post;
|
||||
|
||||
fn App((cx, props): Component) -> Element {
|
||||
fn App(Scope) -> Element {
|
||||
cx.render(rsx!{
|
||||
post::Post {
|
||||
id: Uuid::new_v4(),
|
||||
|
@ -227,7 +227,7 @@ pub struct PostProps {
|
|||
original_poster: String
|
||||
}
|
||||
|
||||
pub fn Post((cx, props): Component<PostProps>) -> Element {
|
||||
pub fn Post(Scope<PostProps>) -> Element {
|
||||
cx.render(rsx!{
|
||||
div { class: "post-container"
|
||||
vote::VoteButtons {
|
||||
|
@ -255,7 +255,7 @@ use dioxus::prelude::*;
|
|||
|
||||
#[derive(PartialEq, Props)]
|
||||
pub struct VoteButtonsProps {}
|
||||
pub fn VoteButtons((cx, props): Component<VoteButtonsProps>) -> Element {}
|
||||
pub fn VoteButtons(Scope<VoteButtonsProps>) -> Element {}
|
||||
```
|
||||
|
||||
```rust
|
||||
|
@ -264,7 +264,7 @@ use dioxus::prelude::*;
|
|||
|
||||
#[derive(PartialEq, Props)]
|
||||
pub struct TitleCardProps {}
|
||||
pub fn TitleCard((cx, props): Component<TitleCardProps>) -> Element {}
|
||||
pub fn TitleCard(Scope<TitleCardProps>) -> Element {}
|
||||
```
|
||||
|
||||
```rust
|
||||
|
@ -273,7 +273,7 @@ use dioxus::prelude::*;
|
|||
|
||||
#[derive(PartialEq, Props)]
|
||||
pub struct MetaCardProps {}
|
||||
pub fn MetaCard((cx, props): Component<MetaCardProps>) -> Element {}
|
||||
pub fn MetaCard(Scope<MetaCardProps>) -> Element {}
|
||||
```
|
||||
|
||||
```rust
|
||||
|
@ -282,7 +282,7 @@ use dioxus::prelude::*;
|
|||
|
||||
#[derive(PartialEq, Props)]
|
||||
pub struct ActionCardProps {}
|
||||
pub fn ActionCard((cx, props): Component<ActionCardProps>) -> Element {}
|
||||
pub fn ActionCard(Scope<ActionCardProps>) -> Element {}
|
||||
```
|
||||
|
||||
## Moving forward
|
73
docs/guide/src/elements/index.md
Normal file
73
docs/guide/src/elements/index.md
Normal file
|
@ -0,0 +1,73 @@
|
|||
# Core Topics
|
||||
|
||||
In this chapter, we'll cover some core topics on how Dioxus works and how to best leverage the features to build a beautiful, reactive app.
|
||||
|
||||
At a very high level, Dioxus is simply a Rust framework for _declaring_ user interfaces and _reacting_ to changes.
|
||||
|
||||
1) We declare what we want our user interface to look like given a state using Rust-based logic and control flow.
|
||||
2) We declare how we want our state to change when the user triggers an event.
|
||||
|
||||
## Declarative UI
|
||||
|
||||
Dioxus is a *declarative* framework. This means that instead of manually writing calls to "create element" and "set element background to red," we simply *declare* what we want the element to look like and let Dioxus handle the differences.
|
||||
|
||||
Let's pretend that we have a stoplight we need to control - it has a color state with red, yellow, and green as options.
|
||||
|
||||
|
||||
Using an imperative approach, we would have to manually declare each element and then handlers for advancing the stoplight.
|
||||
|
||||
```rust
|
||||
let container = Container::new();
|
||||
|
||||
let green_light = Light::new().color("green").enabled(true);
|
||||
let yellow_light = Light::new().color("yellow").enabled(false);
|
||||
let red_light = Light::new().color("red").enabled(false);
|
||||
container.push(green_light);
|
||||
container.push(yellow_light);
|
||||
container.push(red_light);
|
||||
|
||||
container.set_onclick(move |_| {
|
||||
if red_light.enabled() {
|
||||
red_light.set_enabled(false);
|
||||
green_light.set_enabled(true);
|
||||
} else if yellow_light.enabled() {
|
||||
yellow_light.set_enabled(false);
|
||||
red_light.set_enabled(true);
|
||||
} else if green_light.enabled() {
|
||||
green_light.set_enabled(false);
|
||||
yellow_light.set_enabled(true);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
As the UI grows in scale, our logic to keep each element in the proper state would grow exponentially. This can become very unwieldy and lead to out-of-sync UIs that harm user experience.
|
||||
|
||||
Instead, with Dioxus, we *declare* what we want our UI to look like:
|
||||
|
||||
```rust
|
||||
let mut state = use_state(&cx, || "red");
|
||||
|
||||
cx.render(rsx!(
|
||||
Container {
|
||||
Light { color: "red", enabled: state == "red", }
|
||||
Light { color: "yellow", enabled: state == "yellow", }
|
||||
Light { color: "green", enabled: state == "green", }
|
||||
|
||||
onclick: move |_| {
|
||||
state.set(match *state {
|
||||
"green" => "yellow",
|
||||
"yellow" => "red",
|
||||
"red" => "green",
|
||||
})
|
||||
}
|
||||
}
|
||||
))
|
||||
```
|
||||
|
||||
Remember: this concept is not new! Many frameworks are declarative - with React being the most popular. Declarative frameworks tend to be much more enjoyable to work with than imperative frameworks.
|
||||
|
||||
Here's some reading about declaring UI in React:
|
||||
|
||||
- [https://stackoverflow.com/questions/33655534/difference-between-declarative-and-imperative-in-react-js](https://stackoverflow.com/questions/33655534/difference-between-declarative-and-imperative-in-react-js)
|
||||
|
||||
- [https://medium.com/@myung.kim287/declarative-vs-imperative-251ce99c6c44](https://medium.com/@myung.kim287/declarative-vs-imperative-251ce99c6c44)
|
|
@ -87,9 +87,15 @@ Unfortunately, you cannot drop in arbitrary expressions directly into the string
|
|||
rsx!( {[format_args!("Hello {}", if enabled { "Jack" } else { "Bob" } )]} )
|
||||
```
|
||||
|
||||
Alternatively, `&str` can be included directly, though it must be inside of an array:
|
||||
|
||||
```rust
|
||||
rsx!( "Hello ", [if enabled { "Jack" } else { "Bob" }] )
|
||||
```
|
||||
|
||||
This is different from React's way of generating arbitrary markup but fits within idiomatic Rust.
|
||||
|
||||
Typically, with Dioxus, you'll just want to compute your substrings outside of the `rsx!` call:
|
||||
Typically, with Dioxus, you'll just want to compute your substrings outside of the `rsx!` call and leverage the f-string formatting:
|
||||
|
||||
```rust
|
||||
let name = if enabled { "Jack" } else { "Bob" };
|
||||
|
@ -100,7 +106,7 @@ rsx! ( "hello {name}" )
|
|||
|
||||
Every Element in your User Interface will have some sort of properties that the renderer will use when drawing to the screen. These might inform the renderer if the component should be hidden, what its background color should be, or to give it a specific name or ID.
|
||||
|
||||
To do this, we use the familiar struct-style syntax that Rust provides. Commas are optional:
|
||||
To do this, we use the familiar struct-style syntax that Rust provides:
|
||||
|
||||
```rust
|
||||
rsx!(
|
||||
|
@ -127,19 +133,29 @@ rsx!(
|
|||
)
|
||||
```
|
||||
|
||||
Note: the name of the custom attribute must match exactly what you want the renderer to output. All attributes defined as methods in `dioxus-html` follow the snake_case naming convention. However, they internally translate their snake_case convention to HTML's camelCase convention. When using custom attributes, make sure the name of the attribute exactly matches what the renderer is expecting.
|
||||
> Note: the name of the custom attribute must match exactly what you want the renderer to output. All attributes defined as methods in `dioxus-html` follow the snake_case naming convention. However, they internally translate their snake_case convention to HTML's camelCase convention. When using custom attributes, make sure the name of the attribute **exactly** matches what the renderer is expecting.
|
||||
|
||||
All element attributes must occur *before* child elements. The `rsx!` macro will throw an error if your child elements come before any of your attributes. If you don't see the error, try editing your Rust-Analyzer IDE setting to ignore macro-errors. This is a temporary workaround because Rust-Analyzer currently throws *two* errors instead of just the one we care about.
|
||||
|
||||
```rust
|
||||
// settings.json
|
||||
{
|
||||
"rust-analyzer.diagnostics.disabled": [
|
||||
"macro-error"
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
## Listeners
|
||||
|
||||
Listeners are a special type of Attribute that only accept functions. Listeners let us attach functionality to our Elements by running a provided closure whenever the specified Listener is triggered.
|
||||
|
||||
We'll cover listeners in more depth in the Listeners chapter, but for now, just know that every listener must start with the `on` keyword and can accept either a closure or an expression wrapped in curly braces.
|
||||
We'll cover listeners in more depth in the Listeners chapter, but for now, just know that every listener must start with the `on` keyword and accepts closures.
|
||||
|
||||
```rust
|
||||
rsx!(
|
||||
div {
|
||||
onclick: move |_| {}
|
||||
onmouseover: {handler},
|
||||
onclick: move |_| log::debug!("div clicked!"),
|
||||
}
|
||||
)
|
||||
```
|
49
docs/guide/src/final.md
Normal file
49
docs/guide/src/final.md
Normal file
|
@ -0,0 +1,49 @@
|
|||
# Congrats!
|
||||
|
||||
Congrats! You've made it through the `learning Dioxus` book. Throughout this tutorial, you've learned a ton:
|
||||
|
||||
- How to build User Interfaces with Elements
|
||||
- How to compose Element groups together as Components
|
||||
- How to handle user input with event listeners
|
||||
- How to manage local and global state
|
||||
- How to work with async using tasks, coroutines, and suspense
|
||||
- How to build custom hooks and handlers
|
||||
|
||||
With any luck, you followed through the "Putting it All Together" mini guide and have your very own dog search engine app!
|
||||
|
||||
# Next Steps and Advanced Topics
|
||||
|
||||
Continuing on your journey with Dioxus, you can try a number of things:
|
||||
|
||||
- Build a simple TUI app
|
||||
- Publish your search engine app
|
||||
- Deploy a WASM app to GitHub
|
||||
- Design a custom hook
|
||||
- Contribute to the ecosystem!
|
||||
|
||||
There are a number of advanced topics we glossed over:
|
||||
|
||||
- The underlying NodeFactory API
|
||||
- Static elements and templates
|
||||
- Anti-patterns
|
||||
- Bundling/distribution
|
||||
- Working with wasm apps
|
||||
|
||||
# Contributing to the ecosystem
|
||||
|
||||
Dioxus is still quite young and could use your help!
|
||||
|
||||
The core team is actively working on:
|
||||
|
||||
- Declarative window management (via Tauri) for Desktop apps
|
||||
- Portals for Dioxus Core
|
||||
- Mobile support
|
||||
- Integration with 3D renderers
|
||||
- Better async story (suspense, error handling)
|
||||
- Global state management
|
||||
- Web development server
|
||||
- LiveView
|
||||
- Broader platform support (iOS/Android/TV/embedded)
|
||||
|
||||
If there's something specifically interesting to you, don't be afraid to jump in!
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
# Next Steps and Advanced Topics
|
||||
|
||||
|
||||
Congrats! You've made it through the `learning Dioxus` book. Throughout this tutorial, you've learned a ton:
|
||||
|
||||
- How to build User Interfaces with Elements
|
||||
- How to compose Element groups together as Components
|
||||
- How to handle user input with event listeners
|
||||
- How to manage global and local state
|
||||
- How to work with async using tasks, coroutines, and suspense
|
|
@ -1,4 +1,4 @@
|
|||
# Hello, World desktop app
|
||||
# "Hello, World" desktop app
|
||||
|
||||
Let's put together a simple "hello world" desktop application to get acquainted with Dioxus.
|
||||
|
||||
|
@ -138,11 +138,13 @@ Writing `fn App(cx: Scope) -> Element {` might become tedious. Rust will also le
|
|||
static App: Component = |cx| cx.render(rsx!(div { "Hello, world!" }));
|
||||
```
|
||||
|
||||
### What is this `Context` object?
|
||||
### What is this `Scope` object?
|
||||
|
||||
Coming from React, the `Context` object might be confusing. In React, you'll want to store data between renders with hooks. However, hooks rely on global variables which make them difficult to integrate in multi-tenant systems like server-rendering.
|
||||
Coming from React, the `Scope` object might be confusing. In React, you'll want to store data between renders with hooks. However, hooks rely on global variables which make them difficult to integrate in multi-tenant systems like server-rendering.
|
||||
|
||||
In Dioxus, you are given an explicit `Context` object to control how the component renders and stores data. The `Context` object provides a handful of useful APIs for features like suspense, rendering, and more.
|
||||
In Dioxus, you are given an explicit `Scope` object to control how the component renders and stores data. The `Scope` object provides a handful of useful APIs for features like suspense, rendering, and more.
|
||||
|
||||
For now, just know that `Scope` lets you store state with hooks and render elements with `cx.render`.
|
||||
|
||||
## Moving on
|
||||
|
||||
|
|
BIN
docs/guide/src/images/publish.png
Normal file
BIN
docs/guide/src/images/publish.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 272 KiB |
1
docs/guide/src/interactivity/event_handlers.md
Normal file
1
docs/guide/src/interactivity/event_handlers.md
Normal file
|
@ -0,0 +1 @@
|
|||
# Event handlers
|
|
@ -19,16 +19,16 @@ Broadly, there are two types of GUI structures:
|
|||
|
||||
Typically, immediate-mode GUIs are simpler to write but can slow down as more features, like styling, are added.
|
||||
|
||||
Many GUIs today - including Dioxus - are written in *Retained mode* - your code changes the data of the user interface but the renderer is responsible for actually drawing to the screen. In these cases, our GUI's state sticks around as the UI is rendered.
|
||||
Many GUIs today are written in *Retained mode* - your code changes the data of the user interface but the renderer is responsible for actually drawing to the screen. In these cases, our GUI's state sticks around as the UI is rendered. To help accommodate retained mode GUIs, like the web browser, Dioxus provides a mechanism to keep state around.
|
||||
|
||||
Dioxus, following in the footsteps of React, provides a "Reactive" model for you to design your UI. This model emphasizes one-way data flow and encapsulation. Essentially, your UI code should be as predictable as possible.
|
||||
> Note: Even though hooks are accessible, you should still prefer to one-way data flow and encapsulation. Your UI code should be as predictable as possible. Dioxus is plenty fast, even for the largest apps.
|
||||
|
||||
## Mechanics of Hooks
|
||||
In order to have state stick around between renders, Dioxus provides the `hook` through the `use_hook` API. This gives us a mutable reference to data returned from the initialization function.
|
||||
|
||||
```rust
|
||||
fn example(cx: Scope) -> Element {
|
||||
let name: &mut String = cx.use_hook(|| "John Doe".to_string(), |hook| hook);
|
||||
let name: &mut String = cx.use_hook(|| "John Doe".to_string());
|
||||
|
||||
//
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ We can even modify this value directly from an event handler:
|
|||
|
||||
```rust
|
||||
fn example(cx: Scope) -> Element {
|
||||
let name: &mut String = cx.use_hook(|| "John Doe".to_string(), |hook| hook);
|
||||
let name: &mut String = cx.use_hook(|| "John Doe".to_string());
|
||||
|
||||
cx.render(rsx!(
|
||||
button {
|
||||
|
@ -52,9 +52,9 @@ Mechanically, each call to `use_hook` provides us with `&mut T` for a new value.
|
|||
|
||||
```rust
|
||||
fn example(cx: Scope) -> Element {
|
||||
let name: &mut String = cx.use_hook(|| "John Doe".to_string(), |hook| hook);
|
||||
let age: &mut u32 = cx.use_hook(|| 10, |hook| hook);
|
||||
let friends: &mut Vec<String> = cx.use_hook(|| vec!["Jane Doe".to_string()], |hook| hook);
|
||||
let name: &mut String = cx.use_hook(|| "John Doe".to_string());
|
||||
let age: &mut u32 = cx.use_hook(|| 10);
|
||||
let friends: &mut Vec<String> = cx.use_hook(|| vec!["Jane Doe".to_string()]);
|
||||
|
||||
//
|
||||
}
|
||||
|
@ -84,7 +84,7 @@ Consider when we try to pass our `&mut String` into two different handlers:
|
|||
|
||||
```rust
|
||||
fn example(cx: Scope) -> Element {
|
||||
let name: &mut String = cx.use_hook(|| "John Doe".to_string(), |hook| hook);
|
||||
let name: &mut String = cx.use_hook(|| "John Doe".to_string());
|
||||
|
||||
cx.render(rsx!(
|
||||
button { onclick: move |_| name.push_str("yes"), }
|
||||
|
@ -112,7 +112,7 @@ This example uses the `Cell` type to let us replace the value through interior m
|
|||
|
||||
```rust
|
||||
fn example(cx: Scope) -> Element {
|
||||
let name: &Cell<&'static str> = cx.use_hook(|| "John Doe", |hook| hook);
|
||||
let name: &Cell<&'static str> = cx.use_hook(|| Cell::new("John Doe"));
|
||||
|
||||
cx.render(rsx!(
|
||||
button { onclick: move |_| name.set("John"), }
|
||||
|
@ -180,6 +180,8 @@ By default, we bundle a handful of hooks in the Dioxus-Hooks package. Feel free
|
|||
- [use_callback](https://docs.rs/dioxus_hooks/use_callback) - store a callback that implements PartialEq for memoization
|
||||
- [use_provide_context](https://docs.rs/dioxus_hooks/use_provide_context) - expose state to descendent components
|
||||
- [use_context](https://docs.rs/dioxus_hooks/use_context) - consume state provided by `use_provide_context`
|
||||
|
||||
For a more in-depth guide to building new hooks, checkout out the advanced hook building guide in the reference.
|
||||
|
||||
## Wrapping up
|
||||
|
|
@ -139,7 +139,7 @@ fn App(cx: Scope)-> Element {
|
|||
let mut sec_elapsed = use_state(&cx, || 0);
|
||||
|
||||
use_future(&cx, || {
|
||||
let mut sec_elapsed = sec_elapsed.to_owned();
|
||||
let mut sec_elapsed = sec_elapsed.for_async();
|
||||
async move {
|
||||
loop {
|
||||
TimeoutFuture::from_ms(1000).await;
|
|
@ -58,7 +58,7 @@ That's it! We won't need to touch NPM/WebPack/Babel/Parcel, etc. However, you _c
|
|||
|
||||
## Rust Knowledge
|
||||
|
||||
With Rust, things like benchmarking, testing, and documentation are included in the language. We strongly recommend going through the official Rust book _completely_. However, our hope is that a Dioxus app can serve as a great first project. With Dioxus you'll learn about:
|
||||
With Rust, things like benchmarking, testing, and documentation are included in the language. We strongly recommend going through the official Rust book _completely_. However, our hope is that a Dioxus app can serve as a great first Rust project. With Dioxus you'll learn about:
|
||||
|
||||
- Error handling
|
||||
- Structs, Functions, Enums
|
||||
|
|
8
docs/guide/src/state/index.md
Normal file
8
docs/guide/src/state/index.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
# Managing State
|
||||
|
||||
Every app you'll build with Dioxus will have some sort of state that needs to be maintained and updated as your users interact with it. However, managing state can be particular challenging at times, and is frequently the source of bugs in many GUI frameworks.
|
||||
|
||||
In this chapter, we'll cover the various ways to manage state, the appropriate terminology, various patterns, and then take an overview
|
||||
|
||||
|
||||
## Terminology
|
1
docs/guide/src/state/liftingstate.md
Normal file
1
docs/guide/src/state/liftingstate.md
Normal file
|
@ -0,0 +1 @@
|
|||
# Lifting State
|
1
docs/guide/src/state/localstate.md
Normal file
1
docs/guide/src/state/localstate.md
Normal file
|
@ -0,0 +1 @@
|
|||
# Local State
|
|
@ -1 +1,65 @@
|
|||
# Publishing
|
||||
|
||||
Congrats! You've made your first Dioxus app that actually does some pretty cool stuff. This app uses your operating system's WebView library, so it's portable to be distributed for other platforms.
|
||||
|
||||
In this section, we'll cover how to bundle your app for macOS, Windows, and Linux.
|
||||
|
||||
|
||||
|
||||
## Install `cargo-bundle`
|
||||
|
||||
|
||||
The first thing we'll do is install [`cargo-bundle`](https://github.com/burtonageo/cargo-bundle). This extension to cargo will make it very easy to package our app for the various platforms.
|
||||
|
||||
According to the `cargo-bundle` github page,
|
||||
|
||||
|
||||
|
||||
*"cargo-bundle is a tool used to generate installers or app bundles for GUI executables built with cargo. It can create .app bundles for Mac OS X and iOS, .deb packages for Linux, and .msi installers for Windows (note however that iOS and Windows support is still experimental). Support for creating .rpm packages (for Linux) and .apk packages (for Android) is still pending."*
|
||||
|
||||
|
||||
To install, simply run
|
||||
|
||||
|
||||
`cargo install cargo-bundle`
|
||||
|
||||
## Setting up your project
|
||||
|
||||
|
||||
To get a project setup for bundling, we need to add some flags to our `Cargo.toml` file.
|
||||
|
||||
|
||||
```toml
|
||||
[package]
|
||||
name = "example"
|
||||
# ...other fields...
|
||||
|
||||
[package.metadata.bundle]
|
||||
name = "DogSearch"
|
||||
identifier = "com.dogs.dogsearch"
|
||||
version = "1.0.0"
|
||||
copyright = "Copyright (c) Jane Doe 2016. All rights reserved."
|
||||
category = "Developer Tool"
|
||||
short_description = "Easily search for Dog photos"
|
||||
long_description = """
|
||||
This app makes it quick and easy to browse photos of dogs from over 200 bree
|
||||
"""
|
||||
```
|
||||
|
||||
|
||||
## Building
|
||||
|
||||
Following cargo-bundle's instructions, we simply `cargo-bundle --release` to produce a final app with all the optimizations and assets builtin.
|
||||
|
||||
Once you've ran `cargo-bundle --release`, your app should be accessible in
|
||||
|
||||
`target/release/bundle/<platform>/`.
|
||||
|
||||
For example, a macOS app would look like this:
|
||||
|
||||
![Published App](../images/publish.png)
|
||||
|
||||
Nice! And it's only 4.8 Mb - extremely lean!! Because Dioxus leverages your platform's native WebView, Dioxus apps are extremely memory efficient and won't waste your battery.
|
||||
|
||||
> Note: not all CSS works the same on all platforms. Make sure to view your app's CSS on each platform - or web browser (Firefox, Chrome, Safari) before publishing.
|
||||
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
# Summary
|
||||
|
||||
- [Introduction](README.md)
|
||||
|
||||
- [Web](web/intro.md)
|
||||
- [Getting Started](web/setup.md)
|
||||
- [Events](concepts/errorhandling.md)
|
||||
|
||||
- [Desktop](desktop/intro.md)
|
||||
- [Getting Started](desktop/setup.md)
|
||||
- [events](desktop/.md)
|
||||
|
||||
- [Mobile](concepts/managing_state.md)
|
||||
- [Getting Started](concepts/sharedstate.md)
|
||||
- [Specifics](concepts/errorhandling.md)
|
||||
|
||||
- [Working with Async](concepts/async.md)
|
||||
|
||||
- [Code Reference]()
|
||||
- [Code Reference]()
|
||||
- [Code Reference]()
|
||||
- [Code Reference]()
|
||||
- [Code Reference]()
|
||||
|
||||
- [Topics in Depth](depth/topics.md)
|
||||
- [RSX](depth/rsx.md)
|
||||
- [Components](depth/components.md)
|
||||
- [Props](depth/props.md)
|
||||
- [Memoization](depth/memoization.md)
|
||||
- [Performance](depth/performance.md)
|
||||
- [Testing](depth/testing.md)
|
||||
|
||||
- [Advanced Guides](tutorial/advanced_guides.md)
|
||||
- [Memoization](concepts/memoization.md)
|
||||
- [RSX in Depth](concepts/rsx_in_depth.md)
|
||||
- [Building Elements with NodeFactory](concepts/rsx.md)
|
||||
- [Custom Elements](concepts/custom_elements.md)
|
||||
- [Custom Renderer](concepts/custom_renderer.md)
|
||||
- [Server-side components](concepts/server_side_components.md)
|
||||
- [Bundling and Distributing](concepts/bundline.md)
|
||||
|
||||
- [Reference Guide]()
|
||||
- [Anti-patterns]()
|
||||
- [Children]()
|
||||
- [Conditional Rendering]()
|
||||
- [Controlled Inputs]()
|
||||
- [Custom Elements]()
|
||||
- [Empty Components]()
|
||||
- [Error Handling]()
|
||||
- [Fragments]()
|
||||
- [Global CSS]()
|
||||
- [Inline Styles]()
|
||||
- [Iterators]()
|
||||
- [Listeners]()
|
||||
- [Memoization]()
|
||||
- [Node Refs]()
|
||||
- [Spread Pattern]()
|
||||
- [State Management]()
|
||||
- [Suspense]()
|
||||
- [task]()
|
||||
- [Testing]()
|
|
@ -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 | ✅ |
|
||||
| [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!
|
||||
|
||||
|
@ -64,7 +56,7 @@ Here's what a few common tasks look like in Dioxus:
|
|||
|
||||
Nested components with children and internal state:
|
||||
```rust
|
||||
fn App(cx: Scope<()>) -> Element {
|
||||
fn App(cx: Scope) -> Element {
|
||||
cx.render(rsx!( Toggle { "Toggle me" } ))
|
||||
}
|
||||
|
||||
|
@ -75,9 +67,9 @@ fn Toggle(cx: Scope<ToggleProps>) -> Element {
|
|||
let mut toggled = use_state(&cx, || false);
|
||||
cx.render(rsx!{
|
||||
div {
|
||||
{&cx.props.children}
|
||||
&cx.props.children
|
||||
button { onclick: move |_| toggled.set(true),
|
||||
{toggled.and_then(|| "On").or_else(|| "Off")}
|
||||
toggled.and_then(|| "On").or_else(|| "Off")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -86,7 +78,7 @@ fn Toggle(cx: Scope<ToggleProps>) -> Element {
|
|||
|
||||
Controlled inputs:
|
||||
```rust
|
||||
fn App(cx: Scope<()>) -> Element {
|
||||
fn App(cx: Scope) -> Element {
|
||||
let value = use_state(&cx, String::new);
|
||||
cx.render(rsx!(
|
||||
input {
|
||||
|
@ -100,7 +92,7 @@ fn App(cx: Scope<()>) -> Element {
|
|||
|
||||
Lists and Conditional rendering:
|
||||
```rust
|
||||
fn App(cx: Scope<()>) -> Element {
|
||||
fn App(cx: Scope) -> Element {
|
||||
let list = (0..10).map(|i| {
|
||||
rsx!(li { key: "{i}", "Value: {i}" })
|
||||
});
|
||||
|
@ -112,8 +104,8 @@ fn App(cx: Scope<()>) -> Element {
|
|||
|
||||
if should_show {
|
||||
cx.render(rsx!(
|
||||
{title}
|
||||
ul { {list} }
|
||||
title,
|
||||
ul { list }
|
||||
))
|
||||
} else {
|
||||
None
|
||||
|
@ -128,7 +120,7 @@ static App: Component = |cx, _| rsx!(cx, div {"hello world!"});
|
|||
|
||||
Borrowed prop contents:
|
||||
```rust
|
||||
fn App(cx: Scope<()>) -> Element {
|
||||
fn App(cx: Scope) -> Element {
|
||||
let name = use_state(&cx, || String::from("example"));
|
||||
rsx!(cx, Child { title: name.as_str() })
|
||||
}
|
||||
|
@ -145,12 +137,12 @@ Global State
|
|||
```rust
|
||||
struct GlobalState { name: String }
|
||||
|
||||
fn App(cx: Scope<()>) -> Element {
|
||||
fn App(cx: Scope) -> Element {
|
||||
use_provide_shared_state(cx, || GlobalState { name: String::from("Toby") })
|
||||
rsx!(cx, Leaf {})
|
||||
}
|
||||
|
||||
fn Leaf(cx: Scope<()>) -> Element {
|
||||
fn Leaf(cx: Scope) -> Element {
|
||||
let state = use_consume_shared_state::<GlobalState>(cx)?;
|
||||
rsx!(cx, "Hello {state.name}")
|
||||
}
|
||||
|
@ -166,20 +158,20 @@ enum Route {
|
|||
Post(id)
|
||||
}
|
||||
|
||||
fn App(cx: Scope<()>) -> Element {
|
||||
fn App(cx: Scope) -> Element {
|
||||
let route = use_router(cx, Route::parse);
|
||||
cx.render(rsx!(div {
|
||||
{match route {
|
||||
match route {
|
||||
Route::Home => rsx!( Home {} ),
|
||||
Route::Post(id) => rsx!( Post { id: id })
|
||||
}}
|
||||
}
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
Suspense
|
||||
```rust
|
||||
fn App(cx: Scope<()>) -> Element {
|
||||
fn App(cx: Scope) -> Element {
|
||||
let doggo = use_suspense(cx,
|
||||
|| async { reqwest::get("https://dog.ceo/api/breeds/image/random").await.unwrap().json::<Response>().await.unwrap() },
|
||||
|response| cx.render(rsx!( img { src: "{response.message}" }))
|
||||
|
@ -187,8 +179,8 @@ fn App(cx: Scope<()>) -> Element {
|
|||
|
||||
cx.render(rsx!{
|
||||
div {
|
||||
"One doggo coming right up:"
|
||||
{doggo}
|
||||
"One doggo coming right up:",
|
||||
doggo
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -8,38 +8,36 @@ use gloo_timers::future::TimeoutFuture;
|
|||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
dioxus::desktop::launch(App);
|
||||
dioxus::desktop::launch(app);
|
||||
}
|
||||
|
||||
pub static App: Component = |cx| {
|
||||
fn app(cx: Scope) -> Element {
|
||||
let count = use_state(&cx, || 0);
|
||||
let mut direction = use_state(&cx, || 1);
|
||||
let direction = use_state(&cx, || 1);
|
||||
|
||||
let (async_count, dir) = (count.for_async(), *direction);
|
||||
|
||||
let task = use_coroutine(&cx, move || async move {
|
||||
loop {
|
||||
TimeoutFuture::new(250).await;
|
||||
*async_count.get_mut() += dir;
|
||||
*async_count.modify() += dir;
|
||||
}
|
||||
});
|
||||
|
||||
rsx!(cx, div {
|
||||
h1 {"count is {count}"}
|
||||
button {
|
||||
button { onclick: move |_| task.stop(),
|
||||
"Stop counting"
|
||||
onclick: move |_| task.stop()
|
||||
}
|
||||
button {
|
||||
button { onclick: move |_| task.resume(),
|
||||
"Start counting"
|
||||
onclick: move |_| task.resume()
|
||||
}
|
||||
button {
|
||||
"Switch counting direcion"
|
||||
onclick: move |_| {
|
||||
direction *= -1;
|
||||
*direction.modify() *= -1;
|
||||
task.restart();
|
||||
}
|
||||
},
|
||||
"Switch counting direcion"
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
|
|
|
@ -20,8 +20,8 @@ fn main() {
|
|||
dioxus::desktop::launch(App);
|
||||
}
|
||||
|
||||
fn App(cx: Scope<()>) -> Element {
|
||||
let text: &mut Vec<String> = cx.use_hook(|_| vec![String::from("abc=def")], |f| f);
|
||||
fn App(cx: Scope) -> Element {
|
||||
let text = cx.use_hook(|_| vec![String::from("abc=def")]);
|
||||
|
||||
let first = text.get_mut(0).unwrap();
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#![allow(non_snake_case)]
|
||||
|
||||
/*
|
||||
This example is a simple iOS-style calculator. This particular example can run any platform - Web, Mobile, Desktop.
|
||||
This calculator version uses React-style state management. All state is held as individual use_states.
|
||||
|
@ -10,10 +12,10 @@ use dioxus::prelude::*;
|
|||
use separator::Separatable;
|
||||
|
||||
fn main() {
|
||||
dioxus::desktop::launch(APP);
|
||||
// dioxus::desktop::launch(app);
|
||||
}
|
||||
|
||||
static APP: Component = |cx| {
|
||||
fn app(cx: Scope) -> Element {
|
||||
let cur_val = use_state(&cx, || 0.0_f64);
|
||||
let operator = use_state(&cx, || None as Option<&'static str>);
|
||||
let display_value = use_state(&cx, || String::from(""));
|
||||
|
@ -21,109 +23,138 @@ static APP: Component = |cx| {
|
|||
let toggle_percent = move |_| todo!();
|
||||
let input_digit = move |num: u8| display_value.modify().push_str(num.to_string().as_str());
|
||||
|
||||
rsx!(cx, div {
|
||||
class: "calculator",
|
||||
onkeydown: move |evt| match evt.key_code {
|
||||
KeyCode::Add => operator.set(Some("+")),
|
||||
KeyCode::Subtract => operator.set(Some("-")),
|
||||
KeyCode::Divide => operator.set(Some("/")),
|
||||
KeyCode::Multiply => operator.set(Some("*")),
|
||||
KeyCode::Num0 => input_digit(0),
|
||||
KeyCode::Num1 => input_digit(1),
|
||||
KeyCode::Num2 => input_digit(2),
|
||||
KeyCode::Num3 => input_digit(3),
|
||||
KeyCode::Num4 => input_digit(4),
|
||||
KeyCode::Num5 => input_digit(5),
|
||||
KeyCode::Num6 => input_digit(6),
|
||||
KeyCode::Num7 => input_digit(7),
|
||||
KeyCode::Num8 => input_digit(8),
|
||||
KeyCode::Num9 => input_digit(9),
|
||||
KeyCode::Backspace => {
|
||||
if !display_value.as_str().eq("0") {
|
||||
display_value.modify().pop();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
div { class: "calculator-display", {[format_args!("{}", cur_val.separated_string())]} }
|
||||
div { class: "input-keys"
|
||||
div { class: "function-keys"
|
||||
CalculatorKey {
|
||||
{[if display_value == "0" { "C" } else { "AC" }]}
|
||||
name: "key-clear",
|
||||
onclick: move |_| {
|
||||
display_value.set("0".to_string());
|
||||
if display_value != "0" {
|
||||
operator.set(None);
|
||||
cur_val.set(0.0);
|
||||
}
|
||||
cx.render(rsx!(
|
||||
style { [include_str!("./assets/calculator.css")] }
|
||||
div {
|
||||
class: "calculator",
|
||||
onkeydown: move |evt| match evt.key_code {
|
||||
KeyCode::Add => operator.set(Some("+")),
|
||||
KeyCode::Subtract => operator.set(Some("-")),
|
||||
KeyCode::Divide => operator.set(Some("/")),
|
||||
KeyCode::Multiply => operator.set(Some("*")),
|
||||
KeyCode::Num0 => input_digit(0),
|
||||
KeyCode::Num1 => input_digit(1),
|
||||
KeyCode::Num2 => input_digit(2),
|
||||
KeyCode::Num3 => input_digit(3),
|
||||
KeyCode::Num4 => input_digit(4),
|
||||
KeyCode::Num5 => input_digit(5),
|
||||
KeyCode::Num6 => input_digit(6),
|
||||
KeyCode::Num7 => input_digit(7),
|
||||
KeyCode::Num8 => input_digit(8),
|
||||
KeyCode::Num9 => input_digit(9),
|
||||
KeyCode::Backspace => {
|
||||
if !display_value.as_str().eq("0") {
|
||||
display_value.modify().pop();
|
||||
}
|
||||
}
|
||||
CalculatorKey {
|
||||
"±"
|
||||
name: "key-sign",
|
||||
onclick: move |_| {
|
||||
if display_value.starts_with("-") {
|
||||
display_value.set(display_value.trim_start_matches("-").to_string())
|
||||
} else {
|
||||
display_value.set(format!("-{}", *display_value))
|
||||
_ => {}
|
||||
},
|
||||
div { class: "calculator-display", [cur_val.separated_string()] }
|
||||
div { class: "input-keys",
|
||||
div { class: "function-keys",
|
||||
CalculatorKey {
|
||||
name: "key-clear",
|
||||
onclick: move |_| {
|
||||
display_value.set("0".to_string());
|
||||
if display_value != "0" {
|
||||
operator.set(None);
|
||||
cur_val.set(0.0);
|
||||
}
|
||||
},
|
||||
[if display_value == "0" { "C" } else { "AC" }]
|
||||
}
|
||||
CalculatorKey {
|
||||
name: "key-sign",
|
||||
onclick: move |_| {
|
||||
if display_value.starts_with("-") {
|
||||
display_value.set(display_value.trim_start_matches("-").to_string())
|
||||
} else {
|
||||
display_value.set(format!("-{}", *display_value))
|
||||
}
|
||||
},
|
||||
"±"
|
||||
}
|
||||
CalculatorKey {
|
||||
onclick: toggle_percent,
|
||||
name: "key-percent",
|
||||
"%"
|
||||
}
|
||||
}
|
||||
div { class: "digit-keys",
|
||||
CalculatorKey { name: "key-0", onclick: move |_| input_digit(0),
|
||||
"0"
|
||||
}
|
||||
CalculatorKey { name: "key-dot", onclick: move |_| display_value.modify().push_str("."),
|
||||
"●"
|
||||
}
|
||||
(1..9).map(|k| rsx!{
|
||||
CalculatorKey {
|
||||
key: "{k}",
|
||||
name: "key-{k}",
|
||||
onclick: move |_| input_digit(k),
|
||||
"{k}"
|
||||
}
|
||||
},
|
||||
}),
|
||||
}
|
||||
CalculatorKey {
|
||||
"%"
|
||||
onclick: {toggle_percent}
|
||||
name: "key-percent",
|
||||
}
|
||||
}
|
||||
div { class: "digit-keys"
|
||||
CalculatorKey { name: "key-0", onclick: move |_| input_digit(0), "0" }
|
||||
CalculatorKey { name: "key-dot", onclick: move |_| display_value.modify().push_str("."), "●" }
|
||||
|
||||
{(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("/")) "÷" }
|
||||
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 {
|
||||
"="
|
||||
name: "key-equals",
|
||||
onclick: move |_| {
|
||||
if let Some(op) = operator.as_ref() {
|
||||
let rhs = display_value.parse::<f64>().unwrap();
|
||||
let new_val = match *op {
|
||||
"+" => *cur_val + rhs,
|
||||
"-" => *cur_val - rhs,
|
||||
"*" => *cur_val * rhs,
|
||||
"/" => *cur_val / rhs,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
cur_val.set(new_val);
|
||||
display_value.set(new_val.to_string());
|
||||
operator.set(None);
|
||||
}
|
||||
},
|
||||
div { class: "operator-keys",
|
||||
CalculatorKey {
|
||||
name: "key-divide",
|
||||
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 {
|
||||
name: "key-equals",
|
||||
onclick: move |_| {
|
||||
if let Some(op) = operator.as_ref() {
|
||||
let rhs = display_value.parse::<f64>().unwrap();
|
||||
let new_val = match *op {
|
||||
"+" => *cur_val + rhs,
|
||||
"-" => *cur_val - rhs,
|
||||
"*" => *cur_val * rhs,
|
||||
"/" => *cur_val / rhs,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
cur_val.set(new_val);
|
||||
display_value.set(new_val.to_string());
|
||||
operator.set(None);
|
||||
}
|
||||
},
|
||||
"="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Props)]
|
||||
struct CalculatorKeyProps<'a> {
|
||||
#[inline_props]
|
||||
fn CalculatorKey<'a>(
|
||||
cx: Scope,
|
||||
name: &'static str,
|
||||
onclick: &'a dyn Fn(Arc<MouseEvent>),
|
||||
children: Element<'a>,
|
||||
}
|
||||
|
||||
fn CalculatorKey<'a>(cx: Scope<'a, CalculatorKeyProps<'a>>) -> Element {
|
||||
rsx!(cx, button {
|
||||
class: "calculator-key {cx.props.name}"
|
||||
onclick: {cx.props.onclick}
|
||||
{&cx.props.children}
|
||||
) -> Element {
|
||||
cx.render(rsx! {
|
||||
button {
|
||||
class: "calculator-key {name}",
|
||||
onclick: onclick,
|
||||
children
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ fn main() {
|
|||
assert!(g.edits.len() > 1);
|
||||
}
|
||||
|
||||
fn App((cx, props): Scope<()>) -> Element {
|
||||
fn App((cx, props): Scope) -> Element {
|
||||
let mut rng = SmallRng::from_entropy();
|
||||
let rows = (0..10_000_usize).map(|f| {
|
||||
let label = Label::new(&mut rng);
|
||||
|
|
|
@ -20,11 +20,11 @@ pub static Example: Component = |cx| {
|
|||
li { onclick: move |_| example_data.set(f)
|
||||
"ID: {f}"
|
||||
ul {
|
||||
{(0..10).map(|k| rsx!{
|
||||
(0..10).map(|k| rsx!{
|
||||
li {
|
||||
"Sub iterator: {f}.{k}"
|
||||
}
|
||||
})}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
178
examples/crm.rs
178
examples/crm.rs
|
@ -4,7 +4,7 @@ Tiny CRM: A port of the Yew CRM example to Dioxus.
|
|||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
dioxus::web::launch(App);
|
||||
dioxus::desktop::launch(app);
|
||||
}
|
||||
enum Scene {
|
||||
ClientsList,
|
||||
|
@ -19,88 +19,102 @@ pub struct Client {
|
|||
pub description: String,
|
||||
}
|
||||
|
||||
static App: Component = |cx| {
|
||||
let mut clients = use_ref(&cx, || vec![] as Vec<Client>);
|
||||
let mut scene = use_state(&cx, || Scene::ClientsList);
|
||||
fn app(cx: Scope) -> Element {
|
||||
let clients = use_ref(&cx, || vec![] as Vec<Client>);
|
||||
|
||||
let mut firstname = use_state(&cx, || String::new());
|
||||
let mut lastname = use_state(&cx, || String::new());
|
||||
let mut 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"
|
||||
}
|
||||
})
|
||||
}
|
||||
};
|
||||
let scene = use_state(&cx, || Scene::ClientsList);
|
||||
let firstname = use_state(&cx, String::new);
|
||||
let lastname = use_state(&cx, String::new);
|
||||
let description = use_state(&cx, String::new);
|
||||
|
||||
cx.render(rsx!(
|
||||
body {
|
||||
link {
|
||||
rel: "stylesheet"
|
||||
href: "https://unpkg.com/purecss@2.0.6/build/pure-min.css"
|
||||
integrity: "sha384-Uu6IeWbM+gzNVXJcM9XV3SohHtmWE+3VGi496jvgX1jyvDTXfdK+rfZc8C1Aehk5"
|
||||
crossorigin: "anonymous"
|
||||
}
|
||||
margin_left: "35%"
|
||||
h1 {"Dioxus CRM Example"}
|
||||
{scene}
|
||||
}
|
||||
body {
|
||||
margin_left: "35%",
|
||||
link {
|
||||
rel: "stylesheet",
|
||||
href: "https://unpkg.com/purecss@2.0.6/build/pure-min.css",
|
||||
integrity: "sha384-Uu6IeWbM+gzNVXJcM9XV3SohHtmWE+3VGi496jvgX1jyvDTXfdK+rfZc8C1Aehk5",
|
||||
crossorigin: "anonymous",
|
||||
}
|
||||
|
||||
h1 {"Dioxus CRM Example"}
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
))
|
||||
};
|
||||
}
|
||||
|
|
|
@ -13,6 +13,6 @@ static App: Component = |cx| {
|
|||
div {
|
||||
"hello world!"
|
||||
}
|
||||
{(0..10).map(|f| rsx!( div {"abc {f}"}))}
|
||||
(0..10).map(|f| rsx!( div {"abc {f}"}))
|
||||
))
|
||||
};
|
||||
|
|
|
@ -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 +0,0 @@
|
|||
fn main() {}
|
|
@ -5,15 +5,10 @@ use dioxus_core::prelude::*;
|
|||
use dioxus_core_macro::*;
|
||||
use dioxus_hooks::*;
|
||||
use dioxus_html as dioxus_elements;
|
||||
use simple_logger::SimpleLogger;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn main() {
|
||||
if cfg!(debug_assertions) {
|
||||
SimpleLogger::new().init().unwrap();
|
||||
}
|
||||
|
||||
dioxus_desktop::launch(App)
|
||||
}
|
||||
|
||||
|
@ -103,24 +98,24 @@ pub static App: Component = |cx| {
|
|||
}
|
||||
}
|
||||
}
|
||||
ul { class: "todo-list"
|
||||
{filtered_todos.iter().map(|id| rsx!(TodoEntry { key: "{id}", id: *id }))}
|
||||
ul { class: "todo-list",
|
||||
filtered_todos.iter().map(|id| rsx!(TodoEntry { key: "{id}", id: *id }))
|
||||
}
|
||||
{(!todos.read().is_empty()).then(|| rsx!(
|
||||
footer { class: "footer"
|
||||
(!todos.read().is_empty()).then(|| rsx!(
|
||||
footer { class: "footer",
|
||||
span { class: "todo-count" strong {"{items_left} "} span {"{item_text} left"} }
|
||||
ul { class: "filters"
|
||||
li { class: "All", a { onclick: move |_| filter.set(FilterState::All), "All" }}
|
||||
li { class: "Active", a { onclick: move |_| filter.set(FilterState::Active), "Active" }}
|
||||
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(),
|
||||
"Clear completed"
|
||||
}
|
||||
))}
|
||||
))
|
||||
}
|
||||
))}
|
||||
))
|
||||
}
|
||||
}
|
||||
footer { class: "info"
|
||||
|
|
|
@ -4,51 +4,45 @@
|
|||
//! 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.
|
||||
//!
|
||||
//! 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::*;
|
||||
|
||||
fn main() {
|
||||
// simple_logger::init_with_level(log::Level::Debug);
|
||||
dioxus::desktop::launch_cfg(App, |c| {
|
||||
dioxus::desktop::launch_cfg(app, |c| {
|
||||
c.with_window(|w| {
|
||||
w.with_resizable(true).with_inner_size(
|
||||
dioxus::desktop::wry::application::dpi::LogicalSize::new(400.0, 800.0),
|
||||
)
|
||||
w.with_resizable(true)
|
||||
.with_inner_size(dioxus::desktop::tao::dpi::LogicalSize::new(400.0, 800.0))
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
static App: Component = |cx| {
|
||||
let file_manager = 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();
|
||||
fn app(cx: Scope) -> Element {
|
||||
let files = use_ref(&cx, Files::new);
|
||||
|
||||
cx.render(rsx!(
|
||||
div {
|
||||
h1 {"Files: "}
|
||||
h3 {"Cur dir: {current_dir}"}
|
||||
button { "go up", onclick: move |_| file_manager.write().go_up() }
|
||||
ol { {file_list} }
|
||||
{err_disp}
|
||||
h1 { "Files: " }
|
||||
h3 { "Cur dir: " [files.read().current()] }
|
||||
button { onclick: move |_| files.write().go_up(), "go up" }
|
||||
ol {
|
||||
files.read().path_names.iter().enumerate().map(|(dir_id, path)| rsx!(
|
||||
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 {
|
||||
path_stack: Vec<String>,
|
||||
|
@ -70,29 +64,21 @@ impl Files {
|
|||
}
|
||||
|
||||
fn reload_path_list(&mut self) {
|
||||
let cur_path = self.path_stack.last().unwrap();
|
||||
log::info!("Reloading path list for {:?}", cur_path);
|
||||
let paths = match std::fs::read_dir(cur_path) {
|
||||
let paths = match std::fs::read_dir(self.path_stack.last().unwrap()) {
|
||||
Ok(e) => e,
|
||||
Err(err) => {
|
||||
let err = format!("An error occured: {:?}", err);
|
||||
self.err = Some(err);
|
||||
self.err = Some(format!("An error occured: {:?}", err));
|
||||
self.path_stack.pop();
|
||||
return;
|
||||
}
|
||||
};
|
||||
let collected = paths.collect::<Vec<_>>();
|
||||
log::info!("Path list reloaded {:#?}", collected);
|
||||
|
||||
// clear the current state
|
||||
self.clear_err();
|
||||
self.path_names.clear();
|
||||
|
||||
for path in collected {
|
||||
self.path_names
|
||||
.push(path.unwrap().path().display().to_string());
|
||||
}
|
||||
log::info!("path namees are {:#?}", self.path_names);
|
||||
self.path_names
|
||||
.extend(paths.map(|path| path.unwrap().path().display().to_string()));
|
||||
}
|
||||
|
||||
fn go_up(&mut self) {
|
||||
|
|
|
@ -2,8 +2,7 @@ use dioxus::prelude::*;
|
|||
use rand::prelude::*;
|
||||
|
||||
fn main() {
|
||||
dioxus::web::launch(App);
|
||||
// dioxus::desktop::launch(App);
|
||||
dioxus::desktop::launch(App);
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
|
@ -31,16 +30,16 @@ impl Label {
|
|||
}
|
||||
|
||||
static App: Component = |cx| {
|
||||
let mut items = use_ref(&cx, || vec![]);
|
||||
let mut selected = use_state(&cx, || None);
|
||||
let items = use_ref(&cx, || vec![]);
|
||||
let selected = use_state(&cx, || None);
|
||||
|
||||
cx.render(rsx! {
|
||||
div { class: "container"
|
||||
div { class: "jumbotron"
|
||||
div { class: "row"
|
||||
div { class: "container",
|
||||
div { class: "jumbotron",
|
||||
div { class: "row",
|
||||
div { class: "col-md-6", h1 { "Dioxus" } }
|
||||
div { class: "col-md-6"
|
||||
div { class: "row"
|
||||
div { class: "col-md-6",
|
||||
div { class: "row",
|
||||
ActionButton { name: "Create 1,000 rows", id: "run",
|
||||
onclick: move || items.set(Label::new_list(1_000)),
|
||||
}
|
||||
|
@ -67,15 +66,15 @@ static App: Component = |cx| {
|
|||
tbody {
|
||||
{items.read().iter().enumerate().map(|(id, item)| {
|
||||
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", "{item.key}" }
|
||||
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); },
|
||||
span { class: "glyphicon glyphicon-remove remove" aria_hidden: "true" }
|
||||
span { class: "glyphicon glyphicon-remove remove", aria_hidden: "true" }
|
||||
}
|
||||
}
|
||||
td { class: "col-md-6" }
|
||||
|
@ -83,7 +82,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 +95,7 @@ struct ActionButtonProps<'a> {
|
|||
}
|
||||
|
||||
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)(),
|
||||
"{cx.props.name}"
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ fn main() {
|
|||
dioxus::desktop::launch(app);
|
||||
}
|
||||
|
||||
fn app(cx: Scope<()>) -> Element {
|
||||
fn app(cx: Scope) -> Element {
|
||||
cx.render(rsx! (
|
||||
div { "Hello, world!" }
|
||||
))
|
||||
|
|
|
@ -15,19 +15,18 @@ use dioxus::ssr;
|
|||
fn main() {
|
||||
let vdom = VirtualDom::new(App);
|
||||
let content = ssr::render_vdom_cfg(&vdom, |f| f.pre_render(true));
|
||||
|
||||
dioxus::desktop::launch_cfg(App, |c| c.with_prerendered(content));
|
||||
}
|
||||
|
||||
static App: Component = |cx| {
|
||||
let mut val = use_state(&cx, || 0);
|
||||
let val = use_state(&cx, || 0);
|
||||
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
h1 {"hello world. Count: {val}"}
|
||||
h1 { "hello world. Count: {val}" }
|
||||
button {
|
||||
onclick: move |_| *val.modify() += 1,
|
||||
"click to increment"
|
||||
onclick: move |_| val += 1
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
150
examples/inputs.rs
Normal file
150
examples/inputs.rs
Normal file
|
@ -0,0 +1,150 @@
|
|||
//! This example roughly shows how events are serialized into Rust from JavaScript.
|
||||
//!
|
||||
//! There is some conversion happening when input types are checkbox/radio/select/textarea etc.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use dioxus::{events::FormEvent, prelude::*};
|
||||
|
||||
fn main() {
|
||||
dioxus::desktop::launch(App);
|
||||
}
|
||||
|
||||
const FIELDS: &[(&str, &str)] = &[
|
||||
("button", "Click me!"),
|
||||
("checkbox", "CHECKBOX"),
|
||||
("color", ""),
|
||||
("date", ""),
|
||||
("datetime-local", ""),
|
||||
("email", ""),
|
||||
("file", ""),
|
||||
("image", ""),
|
||||
("number", ""),
|
||||
("password", ""),
|
||||
("radio", ""),
|
||||
("range", ""),
|
||||
("reset", ""),
|
||||
("search", ""),
|
||||
("submit", ""),
|
||||
("tel", ""),
|
||||
("text", ""),
|
||||
("time", ""),
|
||||
("url", ""),
|
||||
//
|
||||
// less supported things
|
||||
("hidden", ""),
|
||||
("month", ""), // degrades to text most of the time, but works properly as "value'"
|
||||
("week", ""), // degrades to text most of the time
|
||||
];
|
||||
|
||||
static App: Component = |cx| {
|
||||
cx.render(rsx! {
|
||||
div { margin_left: "30px",
|
||||
|
||||
// radio group
|
||||
|
||||
|
||||
|
||||
|
||||
div {
|
||||
// handling inputs on divs will catch all input events below
|
||||
// so the value of our input event will be either huey, dewey, louie, or true/false (because of the checkboxe)
|
||||
// be mindful in grouping inputs together, as they will all be handled by the same event handler
|
||||
oninput: move |evt| {
|
||||
println!("{:?}", evt);
|
||||
},
|
||||
div {
|
||||
input {
|
||||
id: "huey",
|
||||
r#type: "radio",
|
||||
value: "huey",
|
||||
checked: "",
|
||||
name: "drone",
|
||||
}
|
||||
label {
|
||||
r#for: "huey",
|
||||
"Huey"
|
||||
}
|
||||
}
|
||||
div {
|
||||
input {
|
||||
id: "dewey",
|
||||
r#type: "radio",
|
||||
value: "dewey",
|
||||
name: "drone",
|
||||
}
|
||||
label {
|
||||
r#for: "dewey",
|
||||
"Dewey"
|
||||
}
|
||||
}
|
||||
div {
|
||||
input {
|
||||
id: "louie",
|
||||
value: "louie",
|
||||
r#type: "radio",
|
||||
name: "drone",
|
||||
}
|
||||
label {
|
||||
r#for: "louie",
|
||||
"Louie"
|
||||
}
|
||||
}
|
||||
div {
|
||||
input {
|
||||
id: "groovy",
|
||||
value: "groovy",
|
||||
r#type: "checkbox",
|
||||
name: "drone",
|
||||
}
|
||||
label {
|
||||
r#for: "groovy",
|
||||
"groovy"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// elements with driven values will preventdefault automatically.
|
||||
// you can disable this override with preventdefault: false
|
||||
|
||||
div {
|
||||
input {
|
||||
id: "pdf",
|
||||
value: "pdf",
|
||||
name: "pdf",
|
||||
r#type: "checkbox",
|
||||
oninput: move |evt| {
|
||||
println!("{:?}", evt);
|
||||
},
|
||||
}
|
||||
label {
|
||||
r#for: "pdf",
|
||||
"pdf"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
FIELDS.iter().map(|(field, value)| rsx!(
|
||||
div {
|
||||
input {
|
||||
id: "{field}",
|
||||
name: "{field}",
|
||||
r#type: "{field}",
|
||||
value: "{value}",
|
||||
// checked: "false",
|
||||
oninput: move |evt: Arc<FormEvent>| {
|
||||
println!("{:?}", evt);
|
||||
},
|
||||
}
|
||||
label {
|
||||
r#for: "{field}",
|
||||
"{field} element"
|
||||
}
|
||||
br {}
|
||||
}
|
||||
))
|
||||
|
||||
}
|
||||
})
|
||||
};
|
|
@ -24,7 +24,7 @@ use dioxus::prelude::*;
|
|||
const STYLE: &str = include_str!("./assets/calculator.css");
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
dioxus::desktop::launch_cfg(App, |cfg| {
|
||||
dioxus::desktop::launch_cfg(app, |cfg| {
|
||||
cfg.with_window(|w| {
|
||||
w.with_title("Calculator Demo")
|
||||
.with_resizable(false)
|
||||
|
@ -33,33 +33,33 @@ fn main() {
|
|||
});
|
||||
}
|
||||
|
||||
static App: Component = |cx| {
|
||||
fn app(cx: Scope) -> Element {
|
||||
let state = use_ref(&cx, || Calculator::new());
|
||||
|
||||
let clear_display = state.read().display_value.eq("0");
|
||||
let clear_text = if clear_display { "C" } else { "AC" };
|
||||
let formatted = state.read().formatted_display();
|
||||
|
||||
rsx!(cx, div { id: "wrapper"
|
||||
rsx!(cx, div { id: "wrapper",
|
||||
div { class: "app", style { "{STYLE}" }
|
||||
div { class: "calculator", onkeypress: move |evt| state.write().handle_keydown(evt),
|
||||
div { class: "calculator-display", "{formatted}"}
|
||||
div { class: "calculator-keypad"
|
||||
div { class: "input-keys"
|
||||
div { class: "function-keys"
|
||||
div { class: "calculator-keypad",
|
||||
div { class: "input-keys",
|
||||
div { class: "function-keys",
|
||||
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-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-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}" }
|
||||
})}
|
||||
})
|
||||
}
|
||||
}
|
||||
div { class: "operator-keys"
|
||||
div { class: "operator-keys",
|
||||
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-subtract", onclick: move |_| state.write().set_operator(Operator::Sub), "−" }
|
||||
|
@ -70,7 +70,7 @@ static App: Component = |cx| {
|
|||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Props)]
|
||||
struct CalculatorKeyProps<'a> {
|
||||
|
@ -82,9 +82,9 @@ struct CalculatorKeyProps<'a> {
|
|||
fn CalculatorKey<'a>(cx: Scope<'a, CalculatorKeyProps<'a>>) -> Element {
|
||||
cx.render(rsx! {
|
||||
button {
|
||||
class: "calculator-key {cx.props.name}"
|
||||
onclick: move |e| (cx.props.onclick)(e)
|
||||
{&cx.props.children}
|
||||
class: "calculator-key {cx.props.name}",
|
||||
onclick: move |e| (cx.props.onclick)(e),
|
||||
&cx.props.children
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -14,20 +14,18 @@ fn main() {
|
|||
pub static App: Component = |cx| {
|
||||
let state = use_state(&cx, PlayerState::new);
|
||||
|
||||
let is_playing = state.is_playing();
|
||||
|
||||
rsx!(cx, div {
|
||||
h1 {"Select an option"}
|
||||
h3 {"The radio is... {is_playing}!"}
|
||||
button {
|
||||
"Pause"
|
||||
onclick: move |_| state.modify().reduce(PlayerAction::Pause)
|
||||
cx.render(rsx!(
|
||||
div {
|
||||
h1 {"Select an option"}
|
||||
h3 { "The radio is... " [state.is_playing()], "!" }
|
||||
button { onclick: move |_| state.modify().reduce(PlayerAction::Pause),
|
||||
"Pause"
|
||||
}
|
||||
button { onclick: move |_| state.modify().reduce(PlayerAction::Play),
|
||||
"Play"
|
||||
}
|
||||
}
|
||||
button {
|
||||
"Play"
|
||||
onclick: move |_| state.modify().reduce(PlayerAction::Play)
|
||||
}
|
||||
})
|
||||
))
|
||||
};
|
||||
|
||||
enum PlayerAction {
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
//! The example from the README.md.
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
dioxus::desktop::launch(App);
|
||||
}
|
||||
|
|
|
@ -32,14 +32,14 @@ static App: Component = |cx| {
|
|||
Link { to: Route::AllUsers { page: 0 }, "List all users" }
|
||||
Link { to: Route::BlogList { page: 0 }, "Blog posts" }
|
||||
}
|
||||
{match route {
|
||||
match route {
|
||||
Route::Home => rsx!("Home"),
|
||||
Route::AllUsers { page } => rsx!("All users - page {page}"),
|
||||
Route::User { id } => rsx!("User - id: {id}"),
|
||||
Route::BlogList { page } => rsx!("Blog posts - page {page}"),
|
||||
Route::BlogPost { post_id } => rsx!("Blog post - post {post_id}"),
|
||||
Route::NotFound => rsx!("Not found"),
|
||||
}}
|
||||
}
|
||||
footer {}
|
||||
})
|
||||
};
|
||||
|
|
62
examples/rsx_compile_fail.rs
Normal file
62
examples/rsx_compile_fail.rs
Normal file
|
@ -0,0 +1,62 @@
|
|||
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}"
|
||||
"{f.b}"
|
||||
}
|
||||
})
|
||||
|
||||
mything_read.as_ref().map(|f| rsx!{
|
||||
div {
|
||||
"{f}"
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
struct Thing {
|
||||
a: String,
|
||||
b: u32,
|
||||
}
|
|
@ -60,6 +60,7 @@ pub static EXAMPLE: Component = |cx| {
|
|||
h1 {"Some text"}
|
||||
h1 {"Some text with {formatting}"}
|
||||
h1 {"Formatting basic expressions {formatting_tuple.0} and {formatting_tuple.1}"}
|
||||
h1 {"Formatting without interpolation " [formatting_tuple.0] "and" [formatting_tuple.1] }
|
||||
h2 {
|
||||
"Multiple"
|
||||
"Text"
|
||||
|
@ -72,7 +73,7 @@ pub static EXAMPLE: Component = |cx| {
|
|||
h3 {"elements"}
|
||||
}
|
||||
div {
|
||||
class: "my special div"
|
||||
class: "my special div",
|
||||
h1 {"Headers and attributes!"}
|
||||
}
|
||||
div {
|
||||
|
@ -88,42 +89,51 @@ pub static EXAMPLE: Component = |cx| {
|
|||
}
|
||||
|
||||
// Expressions can be used in element position too:
|
||||
{rsx!(p { "More templating!" })}
|
||||
// {html!(<p>"Even HTML templating!!"</p>)}
|
||||
rsx!(p { "More templating!" }),
|
||||
|
||||
// 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();
|
||||
// Iterators *should* have keys when you can provide them.
|
||||
// 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.
|
||||
data.into_iter().map(|(k, v)| rsx!(li { key: "{k}" "{v}" }))
|
||||
}}
|
||||
data.into_iter().map(|(k, v)| rsx!(li { key: "{k}", "{v}" }))
|
||||
}
|
||||
|
||||
// Matching
|
||||
{match true {
|
||||
match true {
|
||||
true => rsx!( h1 {"Top text"}),
|
||||
false => rsx!( h1 {"Bottom text"})
|
||||
}}
|
||||
}
|
||||
|
||||
// Conditional rendering
|
||||
// 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
|
||||
{true.then(|| rsx!(div {}))}
|
||||
true.then(|| rsx!(div {})),
|
||||
|
||||
// True conditions need to be rendered (same reasons as matching)
|
||||
{if true {
|
||||
rsx!(cx, h1 {"Top text"})
|
||||
// Alternatively, you can use the "if" syntax - but both branches must be resolve to Element
|
||||
if false {
|
||||
rsx!(h1 {"Top text"})
|
||||
} else {
|
||||
rsx!(cx, h1 {"Bottom text"})
|
||||
}}
|
||||
rsx!(h1 {"Bottom text"})
|
||||
}
|
||||
|
||||
// returning "None" is a bit noisy... but rare in practice
|
||||
{None as Option<()>}
|
||||
// Using optionals for diverging branches
|
||||
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
|
||||
{NONE_ELEMENT}
|
||||
NONE_ELEMENT,
|
||||
|
||||
// can also just use empty fragments
|
||||
Fragment {}
|
||||
|
@ -137,9 +147,8 @@ pub static EXAMPLE: Component = |cx| {
|
|||
Fragment {
|
||||
"D"
|
||||
Fragment {
|
||||
"heavily nested fragments is an antipattern"
|
||||
"they cause Dioxus to do unnecessary work"
|
||||
"don't use them carelessly if you can help it"
|
||||
"E"
|
||||
"F"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -158,22 +167,29 @@ pub static EXAMPLE: Component = |cx| {
|
|||
Taller { a: "asd" }
|
||||
|
||||
// Can pass in props directly as an expression
|
||||
{{
|
||||
{
|
||||
let props = TallerProps {a: "hello", children: Default::default()};
|
||||
rsx!(Taller { ..props })
|
||||
}}
|
||||
}
|
||||
|
||||
// Spreading can also be overridden manually
|
||||
Taller {
|
||||
..TallerProps { a: "ballin!", children: Default::default() }
|
||||
..TallerProps { a: "ballin!", children: Default::default() },
|
||||
a: "not ballin!"
|
||||
}
|
||||
|
||||
// Can take children too!
|
||||
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(&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)]
|
||||
pub struct BallerProps {}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
/// This component totally balls
|
||||
pub fn Baller(_: Scope<BallerProps>) -> Element {
|
||||
todo!()
|
||||
|
@ -195,12 +212,20 @@ mod baller {
|
|||
|
||||
#[derive(Props)]
|
||||
pub struct TallerProps<'a> {
|
||||
/// Fields are documented and accessible in rsx!
|
||||
a: &'static str,
|
||||
children: Element<'a>,
|
||||
}
|
||||
|
||||
/// This component is taller than most :)
|
||||
pub fn Taller<'a>(_: Scope<'a, TallerProps<'a>>) -> Element {
|
||||
let b = true;
|
||||
todo!()
|
||||
/// Documention for this component is visible within the rsx macro
|
||||
#[allow(non_snake_case)]
|
||||
pub fn Taller<'a>(cx: Scope<'a, TallerProps<'a>>) -> Element {
|
||||
cx.render(rsx! {
|
||||
&cx.props.children
|
||||
})
|
||||
}
|
||||
|
||||
#[inline_props]
|
||||
fn with_inline<'a>(cx: Scope<'a>, text: &'a str) -> Element {
|
||||
rsx!(cx, p { "{text}" })
|
||||
}
|
||||
|
|
|
@ -16,9 +16,9 @@ const STYLE: &str = "body {overflow:hidden;}";
|
|||
|
||||
pub static App: Component = |cx| {
|
||||
cx.render(rsx!(
|
||||
div { class: "overflow-hidden"
|
||||
div { class: "overflow-hidden",
|
||||
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 {}
|
||||
Entry {}
|
||||
Hero {}
|
||||
|
@ -33,20 +33,20 @@ pub static App: Component = |cx| {
|
|||
pub static Header: Component = |cx| {
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
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"
|
||||
a { class: "flex title-font font-medium items-center text-white mb-4 md:mb-0"
|
||||
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",
|
||||
a { class: "flex title-font font-medium items-center text-white mb-4 md:mb-0",
|
||||
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"
|
||||
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" "Third Link"}
|
||||
a { class: "mr-5 hover:text-white" "Fourth Link"}
|
||||
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", "Second Link"}
|
||||
a { class: "mr-5 hover:text-white", "Third Link"}
|
||||
a { class: "mr-5 hover:text-white", "Fourth Link"}
|
||||
}
|
||||
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"
|
||||
RightArrowIcon {}
|
||||
}
|
||||
|
@ -59,34 +59,34 @@ pub static Header: Component = |cx| {
|
|||
pub static Hero: Component = |cx| {
|
||||
//
|
||||
cx.render(rsx! {
|
||||
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: "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"
|
||||
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: "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",
|
||||
br { class: "hidden lg:inline-block" }
|
||||
"Dioxus Sneak Peek"
|
||||
}
|
||||
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
|
||||
technologies! It is functional, fast, and portable. Dioxus can run on the web, on the desktop, and
|
||||
on mobile and embedded platforms."
|
||||
|
||||
}
|
||||
div { class: "flex justify-center"
|
||||
div { class: "flex justify-center",
|
||||
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"
|
||||
}
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
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"
|
||||
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",
|
||||
referrerpolicy:"no-referrer"
|
||||
}
|
||||
}
|
||||
|
@ -97,8 +97,8 @@ pub static Hero: Component = |cx| {
|
|||
pub static Entry: Component = |cx| {
|
||||
//
|
||||
cx.render(rsx! {
|
||||
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"
|
||||
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",
|
||||
textarea {
|
||||
|
||||
}
|
||||
|
@ -111,13 +111,13 @@ pub static StacksIcon: Component = |cx| {
|
|||
cx.render(rsx!(
|
||||
svg {
|
||||
// xmlns: "http://www.w3.org/2000/svg"
|
||||
fill: "none"
|
||||
stroke: "currentColor"
|
||||
stroke_linecap: "round"
|
||||
stroke_linejoin: "round"
|
||||
stroke_width: "2"
|
||||
class: "w-10 h-10 text-white p-2 bg-indigo-500 rounded-full"
|
||||
view_box: "0 0 24 24"
|
||||
fill: "none",
|
||||
stroke: "currentColor",
|
||||
stroke_linecap: "round",
|
||||
stroke_linejoin: "round",
|
||||
stroke_width: "2",
|
||||
class: "w-10 h-10 text-white p-2 bg-indigo-500 rounded-full",
|
||||
view_box: "0 0 24 24",
|
||||
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| {
|
||||
cx.render(rsx!(
|
||||
svg {
|
||||
fill: "none"
|
||||
stroke: "currentColor"
|
||||
stroke_linecap: "round"
|
||||
stroke_linejoin: "round"
|
||||
stroke_width: "2"
|
||||
class: "w-4 h-4 ml-1"
|
||||
view_box: "0 0 24 24"
|
||||
fill: "none",
|
||||
stroke: "currentColor",
|
||||
stroke_linecap: "round",
|
||||
stroke_linejoin: "round",
|
||||
stroke_width: "2",
|
||||
class: "w-4 h-4 ml-1",
|
||||
view_box: "0 0 24 24",
|
||||
path { d: "M5 12h14M12 5l7 7-7 7"}
|
||||
}
|
||||
))
|
||||
|
|
|
@ -2,26 +2,31 @@
|
|||
//!
|
||||
//! The example from the README.md.
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use std::time::Duration;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
fn main() {
|
||||
dioxus::desktop::launch(app);
|
||||
}
|
||||
|
||||
fn app(cx: Scope<()>) -> Element {
|
||||
let mut count = use_state(&cx, || 0);
|
||||
fn app(cx: Scope) -> Element {
|
||||
let count = use_state(&cx, || 0);
|
||||
|
||||
cx.push_future(|| async move {
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
count += 1;
|
||||
use_future(&cx, || {
|
||||
for_async![count];
|
||||
async move {
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_millis(1000)).await;
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
h1 { "High-Five counter: {count}" }
|
||||
button {
|
||||
onclick: move |_| count +=1 ,
|
||||
onclick: move |_| *count.modify() += 1,
|
||||
"Click me!"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ const App: Component = |cx| {
|
|||
|
||||
let todolist = todos
|
||||
.iter()
|
||||
.filter(|(id, item)| match *filter {
|
||||
.filter(|(_id, item)| match *filter {
|
||||
FilterState::All => true,
|
||||
FilterState::Active => !item.checked,
|
||||
FilterState::Completed => item.checked,
|
||||
|
@ -48,34 +48,37 @@ const App: Component = |cx| {
|
|||
_ => "items",
|
||||
};
|
||||
|
||||
rsx!(cx, div { id: "app"
|
||||
rsx!(cx, div { id: "app",
|
||||
style {"{STYLE}"}
|
||||
div {
|
||||
header { class: "header"
|
||||
header { class: "header",
|
||||
h1 {"todos"}
|
||||
input {
|
||||
class: "new-todo"
|
||||
placeholder: "What needs to be done?"
|
||||
value: "{draft}"
|
||||
oninput: move |evt| draft.set(evt.value.clone())
|
||||
class: "new-todo",
|
||||
placeholder: "What needs to be done?",
|
||||
value: "{draft}",
|
||||
oninput: move |evt| draft.set(evt.value.clone()),
|
||||
}
|
||||
}
|
||||
{todolist}
|
||||
{(!todos.is_empty()).then(|| rsx!(
|
||||
todolist,
|
||||
(!todos.is_empty()).then(|| rsx!(
|
||||
footer {
|
||||
span { strong {"{items_left}"} span {"{item_text} left"} }
|
||||
ul { class: "filters"
|
||||
span {
|
||||
strong {"{items_left}"}
|
||||
span {"{item_text} left"}
|
||||
}
|
||||
ul { class: "filters",
|
||||
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: "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 { "Created by ", a { "jkelleyrtp", href: "http://github.com/jkelleyrtp/" }}
|
||||
p { "Part of ", a { "TodoMVC", href: "http://todomvc.com" }}
|
||||
p { "Created by ", a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" }}
|
||||
p { "Part of ", a { href: "http://todomvc.com", "TodoMVC" }}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
@ -93,13 +96,13 @@ pub fn TodoEntry(cx: Scope<TodoEntryProps>) -> Element {
|
|||
rsx!(cx, li {
|
||||
"{todo.id}"
|
||||
input {
|
||||
class: "toggle"
|
||||
r#type: "checkbox"
|
||||
class: "toggle",
|
||||
r#type: "checkbox",
|
||||
"{todo.checked}"
|
||||
}
|
||||
{is_editing.then(|| rsx!{
|
||||
input {
|
||||
value: "{contents}"
|
||||
value: "{contents}",
|
||||
oninput: move |evt| contents.set(evt.value.clone())
|
||||
}
|
||||
})}
|
||||
|
|
|
@ -43,21 +43,21 @@ struct WeatherProps {}
|
|||
static WeatherDisplay: Component<WeatherProps> = |cx| {
|
||||
//
|
||||
cx.render(rsx!(
|
||||
div { class: "flex items-center justify-center flex-col"
|
||||
div { class: "flex items-center justify-center"
|
||||
div { class: "flex flex-col bg-white rounded p-4 w-full max-w-xs"
|
||||
div{ class: "font-bold text-xl"
|
||||
div { class: "flex items-center justify-center flex-col",
|
||||
div { class: "flex items-center justify-center",
|
||||
div { class: "flex flex-col bg-white rounded p-4 w-full max-w-xs",
|
||||
div{ class: "font-bold text-xl",
|
||||
"Jon's awesome site!!"
|
||||
}
|
||||
div{ class: "text-sm text-gray-500"
|
||||
div{ class: "text-sm text-gray-500",
|
||||
"He worked so hard on it :)"
|
||||
}
|
||||
div { class: "flex flex-row items-center justify-center mt-6"
|
||||
div { class: "font-medium text-6xl"
|
||||
div { class: "flex flex-row items-center justify-center mt-6",
|
||||
div { class: "font-medium text-6xl",
|
||||
"1337"
|
||||
}
|
||||
}
|
||||
div { class: "flex flex-row justify-between mt-6"
|
||||
div { class: "flex flex-row justify-between mt-6",
|
||||
"Legit made my own React"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,28 +13,19 @@
|
|||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
intern_strings();
|
||||
|
||||
dioxus::web::launch(App);
|
||||
dioxus::desktop::launch(App);
|
||||
}
|
||||
|
||||
static App: Component = |cx| {
|
||||
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! {
|
||||
table {
|
||||
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! {
|
||||
tr {
|
||||
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}" }
|
||||
}
|
||||
td { class: "col-md-1"
|
||||
a { class: "remove", onclick: move |_| {/* remove */}
|
||||
span { class: "glyphicon glyphicon-remove remove" aria_hidden: "true" }
|
||||
td { class: "col-md-1",
|
||||
a { class: "remove", onclick: move |_| {/* remove */},
|
||||
span { class: "glyphicon glyphicon-remove remove", aria_hidden: "true" }
|
||||
}
|
||||
}
|
||||
td { class: "col-md-6" }
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
#![allow(non_upper_case_globals, non_snake_case)]
|
||||
//! Example: Webview Renderer
|
||||
//! -------------------------
|
||||
//!
|
||||
|
@ -13,10 +12,10 @@
|
|||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
dioxus::web::launch(App);
|
||||
dioxus::desktop::launch(app);
|
||||
}
|
||||
|
||||
static App: Component = |cx| {
|
||||
fn app(cx: Scope) -> Element {
|
||||
let mut count = use_state(&cx, || 0);
|
||||
|
||||
cx.render(rsx! {
|
||||
|
@ -26,4 +25,4 @@ static App: Component = |cx| {
|
|||
button { onclick: move |_| count -= 1, "Down low!" }
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
|
|
28
examples/xss_safety.rs
Normal file
28
examples/xss_safety.rs
Normal file
|
@ -0,0 +1,28 @@
|
|||
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",
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
|
@ -17,11 +17,7 @@ for more general tasks, we need some way of submitting a future or task into som
|
|||
|
||||
```rust
|
||||
|
||||
let task = use_hook(
|
||||
|| { /* */ },
|
||||
|| { /* update the future if it needs to be updated */ },
|
||||
|| {}
|
||||
);
|
||||
let task = use_hook(|| { /* */ });
|
||||
cx.poll_future()
|
||||
// let recoil_event_loop = cx.use_task(move |_| async move {
|
||||
// loop {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "dioxus-core-macro"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
authors = ["Jonathan Kelley"]
|
||||
edition = "2021"
|
||||
description = "Core macro for Dioxus Virtual DOM"
|
||||
|
@ -16,6 +16,7 @@ proc-macro = true
|
|||
|
||||
[dependencies]
|
||||
once_cell = "1.8"
|
||||
proc-macro-error = "1.0.4"
|
||||
proc-macro2 = { version = "1.0.6" }
|
||||
quote = "1.0"
|
||||
syn = { version = "1.0.11", features = ["full", "extra-traits"] }
|
||||
|
|
|
@ -99,6 +99,7 @@ impl ToTokens for InlinePropsBody {
|
|||
};
|
||||
|
||||
out_tokens.append_all(quote! {
|
||||
#[allow(non_camel_case)]
|
||||
#modifiers
|
||||
#vis struct #struct_name #generics {
|
||||
#(#fields),*
|
||||
|
|
|
@ -161,7 +161,7 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token
|
|||
/// pub struct BallerProps {}
|
||||
///
|
||||
/// /// This component totally balls
|
||||
/// pub fn Baller(cx: Scope<()>) -> DomTree {
|
||||
/// pub fn Baller(cx: Scope) -> DomTree {
|
||||
/// todo!()
|
||||
/// }
|
||||
/// }
|
||||
|
@ -177,11 +177,12 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token
|
|||
/// todo!()
|
||||
/// }
|
||||
/// ```
|
||||
#[proc_macro_error::proc_macro_error]
|
||||
#[proc_macro]
|
||||
pub fn rsx(s: TokenStream) -> TokenStream {
|
||||
match syn::parse::<rsx::CallBody>(s) {
|
||||
Err(e) => e.to_compile_error().into(),
|
||||
Ok(s) => s.to_token_stream().into(),
|
||||
Err(err) => err.to_compile_error().into(),
|
||||
Ok(stream) => stream.to_token_stream().into(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
})
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
|
@ -19,11 +19,10 @@ use quote::{quote, ToTokens, TokenStreamExt};
|
|||
use syn::{
|
||||
ext::IdentExt,
|
||||
parse::{Parse, ParseBuffer, ParseStream},
|
||||
token, Error, Expr, ExprClosure, Ident, Result, Token,
|
||||
token, Expr, Ident, Result, Token,
|
||||
};
|
||||
|
||||
pub struct Component {
|
||||
// accept any path-like argument
|
||||
name: syn::Path,
|
||||
body: Vec<ComponentField>,
|
||||
children: Vec<BodyNode>,
|
||||
|
@ -32,12 +31,8 @@ pub struct Component {
|
|||
|
||||
impl Parse for Component {
|
||||
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)?;
|
||||
|
||||
// parse the guts
|
||||
let content: ParseBuffer;
|
||||
|
||||
// if we see a `{` then we have a block
|
||||
|
@ -48,13 +43,25 @@ impl Parse for Component {
|
|||
syn::parenthesized!(content in stream);
|
||||
}
|
||||
|
||||
let cfg: BodyConfig = BodyConfig {
|
||||
allow_children: true,
|
||||
allow_fields: true,
|
||||
allow_manual_props: true,
|
||||
};
|
||||
let mut body = Vec::new();
|
||||
let mut children = Vec::new();
|
||||
let mut manual_props = None;
|
||||
|
||||
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 {
|
||||
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 {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
let name = &self.name;
|
||||
|
@ -219,9 +155,7 @@ pub struct ComponentField {
|
|||
|
||||
enum ContentField {
|
||||
ManExpr(Expr),
|
||||
OnHandler(ExprClosure),
|
||||
|
||||
// A handler was provided in {} tokens
|
||||
OnHandlerRaw(Expr),
|
||||
}
|
||||
|
||||
|
@ -229,9 +163,6 @@ impl ToTokens for ContentField {
|
|||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
match self {
|
||||
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! {
|
||||
__cx.bump().alloc(#e)
|
||||
}),
|
||||
|
@ -246,13 +177,7 @@ impl Parse for ComponentField {
|
|||
|
||||
let name_str = name.to_string();
|
||||
let content = if name_str.starts_with("on") {
|
||||
if input.peek(token::Brace) {
|
||||
let content;
|
||||
syn::braced!(content in input);
|
||||
ContentField::OnHandlerRaw(content.parse()?)
|
||||
} else {
|
||||
ContentField::OnHandler(input.parse()?)
|
||||
}
|
||||
ContentField::OnHandlerRaw(input.parse()?)
|
||||
} else {
|
||||
ContentField::ManExpr(input.parse::<Expr>()?)
|
||||
};
|
||||
|
|
|
@ -4,7 +4,7 @@ use proc_macro2::TokenStream as TokenStream2;
|
|||
use quote::{quote, ToTokens, TokenStreamExt};
|
||||
use syn::{
|
||||
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 _el_ref = None;
|
||||
|
||||
// todo: more descriptive error handling
|
||||
while !content.is_empty() {
|
||||
// parse fields with commas
|
||||
// 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![:]) {
|
||||
let name = content.parse::<Ident>()?;
|
||||
let ident = name.clone();
|
||||
|
||||
let name_str = name.to_string();
|
||||
content.parse::<Token![:]>()?;
|
||||
|
||||
if name_str.starts_with("on") {
|
||||
if content.peek(token::Brace) {
|
||||
let mycontent;
|
||||
syn::braced!(mycontent in content);
|
||||
|
||||
listeners.push(ElementAttrNamed {
|
||||
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()?,
|
||||
},
|
||||
});
|
||||
};
|
||||
listeners.push(ElementAttrNamed {
|
||||
el_name: el_name.clone(),
|
||||
attr: ElementAttr::EventTokens {
|
||||
name,
|
||||
tokens: content.parse()?,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
match name_str.as_str() {
|
||||
"key" => {
|
||||
key = Some(content.parse()?);
|
||||
}
|
||||
"classes" => {
|
||||
todo!("custom class list not supported")
|
||||
}
|
||||
"namespace" => {
|
||||
todo!("custom namespace not supported")
|
||||
}
|
||||
"classes" => todo!("custom class list not supported yet"),
|
||||
// "namespace" => todo!("custom namespace not supported yet"),
|
||||
"node_ref" => {
|
||||
_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) {
|
||||
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;
|
||||
}
|
||||
} 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
|
||||
// we don't actually care if there *are* commas after elements/text
|
||||
if content.peek(Token![,]) {
|
||||
|
@ -138,7 +191,7 @@ impl Parse for Element {
|
|||
impl ToTokens for Element {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
let name = &self.name;
|
||||
let childs = &self.children;
|
||||
let children = &self.children;
|
||||
|
||||
let listeners = &self.listeners;
|
||||
let attr = &self.attributes;
|
||||
|
@ -153,7 +206,7 @@ impl ToTokens for Element {
|
|||
dioxus_elements::#name,
|
||||
[ #(#listeners),* ],
|
||||
[ #(#attr),* ],
|
||||
[ #(#childs),* ],
|
||||
[ #(#children),* ],
|
||||
#key,
|
||||
)
|
||||
});
|
||||
|
@ -173,8 +226,8 @@ enum ElementAttr {
|
|||
// "attribute": true,
|
||||
CustomAttrExpression { name: LitStr, value: Expr },
|
||||
|
||||
// onclick: move |_| {}
|
||||
EventClosure { name: Ident, closure: ExprClosure },
|
||||
// // onclick: move |_| {}
|
||||
// EventClosure { name: Ident, closure: ExprClosure },
|
||||
|
||||
// onclick: {}
|
||||
EventTokens { name: Ident, tokens: Expr },
|
||||
|
@ -189,7 +242,7 @@ impl ToTokens for ElementAttrNamed {
|
|||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
let ElementAttrNamed { el_name, attr } = self;
|
||||
|
||||
let toks = match attr {
|
||||
tokens.append_all(match attr {
|
||||
ElementAttr::AttrText { name, value } => {
|
||||
quote! {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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! {
|
||||
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 } => {
|
||||
quote! {
|
||||
dioxus_elements::on::#name(__cx, #tokens)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
tokens.append_all(toks);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
||||
mod ambiguous;
|
||||
mod body;
|
||||
mod component;
|
||||
mod element;
|
||||
mod fragment;
|
||||
mod node;
|
||||
|
||||
// Re-export the namespaces into each other
|
||||
pub use ambiguous::*;
|
||||
pub use body::*;
|
||||
pub use component::*;
|
||||
pub use element::*;
|
||||
pub use fragment::*;
|
||||
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
|
||||
})
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,34 +4,67 @@ use proc_macro2::TokenStream as TokenStream2;
|
|||
use quote::{quote, ToTokens, TokenStreamExt};
|
||||
use syn::{
|
||||
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 {
|
||||
Element(AmbiguousElement),
|
||||
Text(TextNode),
|
||||
Element(Element),
|
||||
Component(Component),
|
||||
Text(LitStr),
|
||||
RawExpr(Expr),
|
||||
}
|
||||
|
||||
impl Parse for BodyNode {
|
||||
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) {
|
||||
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) {
|
||||
match &self {
|
||||
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! {
|
||||
__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))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "dioxus-core"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
authors = ["Jonathan Kelley"]
|
||||
edition = "2018"
|
||||
description = "Core functionality for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences"
|
||||
|
@ -50,7 +50,6 @@ anyhow = "1.0.42"
|
|||
dioxus-html = { path = "../html" }
|
||||
fern = { version = "0.6.0", features = ["colored"] }
|
||||
rand = { version = "0.8.4", features = ["small_rng"] }
|
||||
simple_logger = "1.13.0"
|
||||
dioxus-core-macro = { path = "../core-macro", version = "^0.1.2" }
|
||||
criterion = "0.3.5"
|
||||
thiserror = "1.0.30"
|
||||
|
|
|
@ -6,7 +6,7 @@ To build new apps with Dioxus or to extend the ecosystem with new hooks or compo
|
|||
|
||||
|
||||
```rust
|
||||
fn app(cx: Scope<()>) -> Element {
|
||||
fn app(cx: Scope) -> Element {
|
||||
rsx!(cx, div { "hello world" })
|
||||
}
|
||||
|
||||
|
|
|
@ -25,16 +25,16 @@ criterion_main!(mbenches);
|
|||
fn create_rows(c: &mut Criterion) {
|
||||
static App: Component = |cx| {
|
||||
let mut rng = SmallRng::from_entropy();
|
||||
let rows = (0..10_000_usize).map(|f| {
|
||||
let label = Label::new(&mut rng);
|
||||
rsx!(Row {
|
||||
row_id: f,
|
||||
label: label
|
||||
})
|
||||
});
|
||||
|
||||
rsx!(cx, table {
|
||||
tbody {
|
||||
{rows}
|
||||
(0..10_000_usize).map(|f| {
|
||||
let label = Label::new(&mut rng);
|
||||
rsx!(Row {
|
||||
row_id: f,
|
||||
label: label
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
|
@ -58,12 +58,12 @@ fn Row(cx: Scope<RowProps>) -> Element {
|
|||
cx.render(rsx! {
|
||||
tr {
|
||||
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}" }
|
||||
}
|
||||
td { class: "col-md-1"
|
||||
a { class: "remove", onclick: move |_| {/* remove */}
|
||||
span { class: "glyphicon glyphicon-remove remove" aria_hidden: "true" }
|
||||
td { class: "col-md-1",
|
||||
a { class: "remove", onclick: move |_| {/* remove */},
|
||||
span { class: "glyphicon glyphicon-remove remove", aria_hidden: "true" }
|
||||
}
|
||||
}
|
||||
td { class: "col-md-6" }
|
||||
|
|
|
@ -11,8 +11,8 @@ fn main() {
|
|||
dbg!(edits);
|
||||
}
|
||||
|
||||
fn parent(cx: Scope<()>) -> Element {
|
||||
let value = cx.use_hook(|_| String::new(), |f| f);
|
||||
fn parent(cx: Scope) -> Element {
|
||||
let value = cx.use_hook(|_| String::new());
|
||||
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
|
@ -33,8 +33,8 @@ struct ChildProps<'a> {
|
|||
fn child<'a>(cx: Scope<'a, ChildProps<'a>>) -> Element {
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
"it's nested {cx.props.name}"
|
||||
{&cx.props.children}
|
||||
"it's nested {cx.props.name}",
|
||||
&cx.props.children
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -9,8 +9,8 @@ fn main() {
|
|||
let _ = VirtualDom::new(parent);
|
||||
}
|
||||
|
||||
fn parent(cx: Scope<()>) -> Element {
|
||||
let value = cx.use_hook(|_| String::new(), |f| f);
|
||||
fn parent(cx: Scope) -> Element {
|
||||
let value = cx.use_hook(|_| String::new());
|
||||
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
|
|
|
@ -473,9 +473,16 @@ impl<'bump> DiffState<'bump> {
|
|||
// Check the most common cases first
|
||||
// these are *actual* elements, not wrappers around lists
|
||||
(Text(old), Text(new)) => {
|
||||
self.diff_text_nodes(old, new, old_node, new_node);
|
||||
if let Some(root) = old.id.get() {
|
||||
if old.text != new.text {
|
||||
self.mutations.set_text(new.text, root.as_u64());
|
||||
}
|
||||
self.scopes.update_node(new_node, root);
|
||||
|
||||
new.id.set(Some(root));
|
||||
}
|
||||
}
|
||||
(Element(old), Element(new)) => self.diff_element_nodes(old, new, old_node, new_node),
|
||||
|
||||
(Placeholder(old), Placeholder(new)) => {
|
||||
if let Some(root) = old.id.get() {
|
||||
self.scopes.update_node(new_node, root);
|
||||
|
@ -483,10 +490,13 @@ impl<'bump> DiffState<'bump> {
|
|||
}
|
||||
}
|
||||
|
||||
(Element(old), Element(new)) => self.diff_element_nodes(old, new, old_node, new_node),
|
||||
|
||||
// These two sets are pointers to nodes but are not actually nodes themselves
|
||||
(Component(old), Component(new)) => {
|
||||
self.diff_component_nodes(old_node, new_node, *old, *new)
|
||||
}
|
||||
|
||||
(Fragment(old), Fragment(new)) => self.diff_fragment_nodes(old, new),
|
||||
|
||||
// The normal pathway still works, but generates slightly weird instructions
|
||||
|
@ -506,23 +516,6 @@ impl<'bump> DiffState<'bump> {
|
|||
}
|
||||
}
|
||||
|
||||
fn diff_text_nodes(
|
||||
&mut self,
|
||||
old: &'bump VText<'bump>,
|
||||
new: &'bump VText<'bump>,
|
||||
_old_node: &'bump VNode<'bump>,
|
||||
new_node: &'bump VNode<'bump>,
|
||||
) {
|
||||
if let Some(root) = old.id.get() {
|
||||
if old.text != new.text {
|
||||
self.mutations.set_text(new.text, root.as_u64());
|
||||
}
|
||||
self.scopes.update_node(new_node, root);
|
||||
|
||||
new.id.set(Some(root));
|
||||
}
|
||||
}
|
||||
|
||||
fn diff_element_nodes(
|
||||
&mut self,
|
||||
old: &'bump VElement<'bump>,
|
||||
|
|
|
@ -65,10 +65,10 @@ pub(crate) mod innerlude {
|
|||
}
|
||||
|
||||
pub use crate::innerlude::{
|
||||
Attribute, Component, DioxusElement, DomEdit, Element, ElementId, EventHandler, EventPriority,
|
||||
IntoVNode, LazyNodes, Listener, Mutations, NodeFactory, Properties, SchedulerMsg, Scope,
|
||||
ScopeId, ScopeState, TaskId, UserEvent, VComponent, VElement, VFragment, VNode, VPlaceholder,
|
||||
VText, VirtualDom,
|
||||
Attribute, Component, DioxusElement, DomEdit, Element, ElementId, ElementIdIterator, Event,
|
||||
EventHandler, EventPriority, IntoVNode, LazyNodes, Listener, Mutations, NodeFactory,
|
||||
Properties, SchedulerMsg, Scope, ScopeId, ScopeState, TaskId, UserEvent, VComponent, VElement,
|
||||
VFragment, VNode, VPlaceholder, VText, VirtualDom,
|
||||
};
|
||||
|
||||
pub mod prelude {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue