mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-22 20:23:09 +00:00
Merge branch 'main' into jk/fix-form-inputs
This commit is contained in:
commit
16b38e339d
28 changed files with 1007 additions and 86 deletions
17
Cargo.lock
generated
17
Cargo.lock
generated
|
@ -2361,6 +2361,8 @@ dependencies = [
|
|||
"slab",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-fluent-assertions",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -5924,9 +5926,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.10"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09"
|
||||
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
|
@ -9705,6 +9707,17 @@ dependencies = [
|
|||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-fluent-assertions"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12de1a8c6bcfee614305e836308b596bbac831137a04c61f7e5b0b0bf2cfeaf6"
|
||||
dependencies = [
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-futures"
|
||||
version = "0.2.5"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
//! A simple example that shows how to use the use_future hook to run a background task.
|
||||
//!
|
||||
//! use_future assumes your future will never complete - it won't return a value.
|
||||
//! If you want to return a value, use use_resource instead.
|
||||
//! use_future won't return a value, analagous to use_effect.
|
||||
//! If you want to return a value from a future, use use_resource instead.
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use std::time::Duration;
|
||||
|
|
|
@ -167,7 +167,7 @@ pub(crate) const COMPONENT_ARG_CASE_CHECK_OFF: &str = "no_case_check";
|
|||
/// #[warn(non_snake_case)]
|
||||
/// #[inline(always)]
|
||||
/// fn __dx_inner_comp(props: GreetPersonProps>e) -> Element {
|
||||
/// let GreetPersonProps { person } = &cx.props;
|
||||
/// let GreetPersonProps { person } = props;
|
||||
/// {
|
||||
/// rsx! { "hello, {person}" }
|
||||
/// }
|
||||
|
|
|
@ -20,9 +20,11 @@ slab = { workspace = true }
|
|||
futures-channel = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
serde = { version = "1", features = ["derive"], optional = true }
|
||||
tracing-subscriber = "0.3.18"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
tracing-fluent-assertions = "0.3.0"
|
||||
dioxus = { workspace = true }
|
||||
pretty_assertions = "1.3.0"
|
||||
rand = "0.8.5"
|
||||
|
|
|
@ -79,11 +79,7 @@ impl VNode {
|
|||
// The target ScopeState still has the reference to the old props, so there's no need to update anything
|
||||
// This also implicitly drops the new props since they're not used
|
||||
if old_props.memoize(new_props.props()) {
|
||||
tracing::trace!(
|
||||
"Memoized props for component {:#?} ({})",
|
||||
scope_id,
|
||||
old_scope.state().name
|
||||
);
|
||||
tracing::trace!("Memoized props for component {:#?}", scope_id,);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::{
|
||||
any_props::BoxedAnyProps, nodes::RenderReturn, runtime::Runtime, scope_context::Scope,
|
||||
};
|
||||
use std::{cell::Ref, fmt::Debug, rc::Rc};
|
||||
use std::{cell::Ref, rc::Rc};
|
||||
|
||||
/// A component's unique identifier.
|
||||
///
|
||||
|
@ -9,9 +9,26 @@ use std::{cell::Ref, fmt::Debug, rc::Rc};
|
|||
/// time. We do try and guarantee that between calls to `wait_for_work`, no ScopeIds will be recycled in order to give
|
||||
/// time for any logic that relies on these IDs to properly update.
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct ScopeId(pub usize);
|
||||
|
||||
impl std::fmt::Debug for ScopeId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut builder = f.debug_tuple("ScopeId");
|
||||
let mut builder = builder.field(&self.0);
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
if let Some(name) = Runtime::current()
|
||||
.as_ref()
|
||||
.and_then(|rt| rt.get_state(*self))
|
||||
{
|
||||
builder = builder.field(&name.name);
|
||||
}
|
||||
}
|
||||
builder.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl ScopeId {
|
||||
/// The root ScopeId.
|
||||
///
|
||||
|
|
|
@ -19,6 +19,7 @@ use futures_util::StreamExt;
|
|||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use slab::Slab;
|
||||
use std::{any::Any, collections::BTreeSet, rc::Rc};
|
||||
use tracing::instrument;
|
||||
|
||||
/// A virtual node system that progresses user events and diffs UI trees.
|
||||
///
|
||||
|
@ -303,6 +304,7 @@ impl VirtualDom {
|
|||
/// let mut dom = VirtualDom::new_from_root(VComponent::new(Example, SomeProps { name: "jane" }, "Example"));
|
||||
/// let mutations = dom.rebuild();
|
||||
/// ```
|
||||
#[instrument(skip(root), level = "trace", name = "VirtualDom::new")]
|
||||
pub(crate) fn new_with_component(root: impl AnyProps + 'static) -> Self {
|
||||
let (tx, rx) = futures_channel::mpsc::unbounded();
|
||||
|
||||
|
@ -345,6 +347,7 @@ impl VirtualDom {
|
|||
}
|
||||
|
||||
/// Run a closure inside the dioxus runtime
|
||||
#[instrument(skip(self, f), level = "trace", name = "VirtualDom::in_runtime")]
|
||||
pub fn in_runtime<O>(&self, f: impl FnOnce() -> O) -> O {
|
||||
let _runtime = RuntimeGuard::new(self.runtime.clone());
|
||||
f()
|
||||
|
@ -373,7 +376,13 @@ impl VirtualDom {
|
|||
return;
|
||||
};
|
||||
|
||||
tracing::trace!("Marking scope {:?} ({}) as dirty", id, scope.name);
|
||||
tracing::event!(
|
||||
tracing::Level::TRACE,
|
||||
"Marking scope {:?} ({}) as dirty",
|
||||
id,
|
||||
scope.name
|
||||
);
|
||||
|
||||
self.dirty_scopes.insert(DirtyScope {
|
||||
height: scope.height(),
|
||||
id,
|
||||
|
@ -389,6 +398,7 @@ impl VirtualDom {
|
|||
/// It is up to the listeners themselves to mark nodes as dirty.
|
||||
///
|
||||
/// If you have multiple events, you can call this method multiple times before calling "render_with_deadline"
|
||||
#[instrument(skip(self), level = "trace", name = "VirtualDom::handle_event")]
|
||||
pub fn handle_event(
|
||||
&mut self,
|
||||
name: &str,
|
||||
|
@ -422,12 +432,14 @@ impl VirtualDom {
|
|||
/// ```rust, ignore
|
||||
/// let dom = VirtualDom::new(app);
|
||||
/// ```
|
||||
#[instrument(skip(self), level = "trace", name = "VirtualDom::wait_for_work")]
|
||||
pub async fn wait_for_work(&mut self) {
|
||||
// And then poll the futures
|
||||
self.poll_tasks().await;
|
||||
}
|
||||
|
||||
///
|
||||
#[instrument(skip(self), level = "trace", name = "VirtualDom::poll_tasks")]
|
||||
async fn poll_tasks(&mut self) {
|
||||
// Release the flush lock
|
||||
// This will cause all the flush wakers to immediately spring to life, which we will off with process_events
|
||||
|
@ -461,6 +473,7 @@ impl VirtualDom {
|
|||
}
|
||||
|
||||
/// Process all events in the queue until there are no more left
|
||||
#[instrument(skip(self), level = "trace", name = "VirtualDom::process_events")]
|
||||
pub fn process_events(&mut self) {
|
||||
let _runtime = RuntimeGuard::new(self.runtime.clone());
|
||||
|
||||
|
@ -478,7 +491,8 @@ impl VirtualDom {
|
|||
///
|
||||
/// The caller must ensure that the template references the same dynamic attributes and nodes as the original template.
|
||||
///
|
||||
/// This will only replace the the parent template, not any nested templates.
|
||||
/// This will only replace the parent template, not any nested templates.
|
||||
#[instrument(skip(self), level = "trace", name = "VirtualDom::replace_template")]
|
||||
pub fn replace_template(&mut self, template: Template) {
|
||||
self.register_template_first_byte_index(template);
|
||||
// iterating a slab is very inefficient, but this is a rare operation that will only happen during development so it's fine
|
||||
|
@ -518,7 +532,7 @@ impl VirtualDom {
|
|||
/// The mutations item expects the RealDom's stack to be the root of the application.
|
||||
///
|
||||
/// Tasks will not be polled with this method, nor will any events be processed from the event queue. Instead, the
|
||||
/// root component will be ran once and then diffed. All updates will flow out as mutations.
|
||||
/// root component will be run once and then diffed. All updates will flow out as mutations.
|
||||
///
|
||||
/// All state stored in components will be completely wiped away.
|
||||
///
|
||||
|
@ -533,6 +547,7 @@ impl VirtualDom {
|
|||
///
|
||||
/// apply_edits(edits);
|
||||
/// ```
|
||||
#[instrument(skip(self, to), level = "trace", name = "VirtualDom::rebuild")]
|
||||
pub fn rebuild(&mut self, to: &mut impl WriteMutations) {
|
||||
self.flush_templates(to);
|
||||
let _runtime = RuntimeGuard::new(self.runtime.clone());
|
||||
|
@ -546,6 +561,7 @@ impl VirtualDom {
|
|||
|
||||
/// Render whatever the VirtualDom has ready as fast as possible without requiring an executor to progress
|
||||
/// suspended subtrees.
|
||||
#[instrument(skip(self, to), level = "trace", name = "VirtualDom::render_immediate")]
|
||||
pub fn render_immediate(&mut self, to: &mut impl WriteMutations) {
|
||||
self.flush_templates(to);
|
||||
|
||||
|
@ -584,7 +600,8 @@ impl VirtualDom {
|
|||
/// The mutations will be thrown out, so it's best to use this method for things like SSR that have async content
|
||||
///
|
||||
/// We don't call "flush_sync" here since there's no sync work to be done. Futures will be progressed like usual,
|
||||
/// however any futures wating on flush_sync will remain pending
|
||||
/// however any futures waiting on flush_sync will remain pending
|
||||
#[instrument(skip(self), level = "trace", name = "VirtualDom::wait_for_suspense")]
|
||||
pub async fn wait_for_suspense(&mut self) {
|
||||
loop {
|
||||
if self.suspended_scopes.is_empty() {
|
||||
|
@ -605,6 +622,7 @@ impl VirtualDom {
|
|||
}
|
||||
|
||||
/// Flush any queued template changes
|
||||
#[instrument(skip(self, to), level = "trace", name = "VirtualDom::flush_templates")]
|
||||
fn flush_templates(&mut self, to: &mut impl WriteMutations) {
|
||||
for template in self.queued_templates.drain(..) {
|
||||
to.register_template(template);
|
||||
|
@ -632,6 +650,11 @@ impl VirtualDom {
|
|||
| | | <-- no, broke early
|
||||
| <-- no, broke early
|
||||
*/
|
||||
#[instrument(
|
||||
skip(self, uievent),
|
||||
level = "trace",
|
||||
name = "VirtualDom::handle_bubbling_event"
|
||||
)]
|
||||
fn handle_bubbling_event(
|
||||
&mut self,
|
||||
mut parent: Option<ElementRef>,
|
||||
|
@ -670,6 +693,11 @@ impl VirtualDom {
|
|||
|
||||
// Now that we've accumulated all the parent attributes for the target element, call them in reverse order
|
||||
// We check the bubble state between each call to see if the event has been stopped from bubbling
|
||||
tracing::event!(
|
||||
tracing::Level::TRACE,
|
||||
"Calling {} listeners",
|
||||
listeners.len()
|
||||
);
|
||||
for listener in listeners.into_iter().rev() {
|
||||
if let AttributeValue::Listener(listener) = listener {
|
||||
self.runtime.rendering.set(false);
|
||||
|
@ -688,6 +716,11 @@ impl VirtualDom {
|
|||
}
|
||||
|
||||
/// Call an event listener in the simplest way possible without bubbling upwards
|
||||
#[instrument(
|
||||
skip(self, uievent),
|
||||
level = "trace",
|
||||
name = "VirtualDom::handle_non_bubbling_event"
|
||||
)]
|
||||
fn handle_non_bubbling_event(&mut self, node: ElementRef, name: &str, uievent: Event<dyn Any>) {
|
||||
let el_ref = &self.mounts[node.mount.0].node;
|
||||
let node_template = el_ref.template.get();
|
||||
|
|
82
packages/core/tests/tracing.rs
Normal file
82
packages/core/tests/tracing.rs
Normal file
|
@ -0,0 +1,82 @@
|
|||
use dioxus::html::SerializedHtmlEventConverter;
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_core::ElementId;
|
||||
use std::rc::Rc;
|
||||
use tracing_fluent_assertions::{AssertionRegistry, AssertionsLayer};
|
||||
use tracing_subscriber::{layer::SubscriberExt, Registry};
|
||||
|
||||
#[test]
|
||||
fn basic_tracing() {
|
||||
// setup tracing
|
||||
let assertion_registry = AssertionRegistry::default();
|
||||
let base_subscriber = Registry::default();
|
||||
// log to standard out for testing
|
||||
let std_out_log = tracing_subscriber::fmt::layer().pretty();
|
||||
let subscriber = base_subscriber
|
||||
.with(std_out_log)
|
||||
.with(AssertionsLayer::new(&assertion_registry));
|
||||
tracing::subscriber::set_global_default(subscriber).unwrap();
|
||||
|
||||
let new_virtual_dom = assertion_registry
|
||||
.build()
|
||||
.with_name("VirtualDom::new")
|
||||
.was_created()
|
||||
.was_entered_exactly(1)
|
||||
.was_closed()
|
||||
.finalize();
|
||||
|
||||
let edited_virtual_dom = assertion_registry
|
||||
.build()
|
||||
.with_name("VirtualDom::rebuild")
|
||||
.was_created()
|
||||
.was_entered_exactly(1)
|
||||
.was_closed()
|
||||
.finalize();
|
||||
|
||||
set_event_converter(Box::new(SerializedHtmlEventConverter));
|
||||
let mut dom = VirtualDom::new(app);
|
||||
|
||||
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
||||
|
||||
new_virtual_dom.assert();
|
||||
edited_virtual_dom.assert();
|
||||
|
||||
for _ in 0..3 {
|
||||
dom.handle_event(
|
||||
"click",
|
||||
Rc::new(PlatformEventData::new(Box::<SerializedMouseData>::default())),
|
||||
ElementId(2),
|
||||
true,
|
||||
);
|
||||
dom.process_events();
|
||||
_ = dom.render_immediate_to_vec();
|
||||
}
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
let mut idx = use_signal(|| 0);
|
||||
let onhover = |_| println!("go!");
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
button {
|
||||
onclick: move |_| {
|
||||
idx += 1;
|
||||
println!("Clicked");
|
||||
},
|
||||
"+"
|
||||
}
|
||||
button { onclick: move |_| idx -= 1, "-" }
|
||||
ul {
|
||||
{(0..idx()).map(|i| rsx! {
|
||||
ChildExample { i: i, onhover: onhover }
|
||||
})}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ChildExample(i: i32, onhover: EventHandler<MouseEvent>) -> Element {
|
||||
rsx! { li { onmouseover: move |e| onhover.call(e), "{i}" } }
|
||||
}
|
|
@ -9,6 +9,7 @@ use dioxus_ssr::{
|
|||
use std::future::Future;
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
use tokio::task::block_in_place;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
@ -64,7 +65,7 @@ impl SsrRendererPool {
|
|||
let prev_context = SERVER_CONTEXT.with(|ctx| ctx.replace(server_context));
|
||||
// poll the future, which may call server_context()
|
||||
tracing::info!("Rebuilding vdom");
|
||||
vdom.rebuild(&mut NoOpMutations);
|
||||
block_in_place(|| vdom.rebuild(&mut NoOpMutations));
|
||||
vdom.wait_for_suspense().await;
|
||||
tracing::info!("Suspense resolved");
|
||||
// after polling the future, we need to restore the context
|
||||
|
@ -124,7 +125,7 @@ impl SsrRendererPool {
|
|||
.with(|ctx| ctx.replace(Box::new(server_context)));
|
||||
// poll the future, which may call server_context()
|
||||
tracing::info!("Rebuilding vdom");
|
||||
vdom.rebuild(&mut NoOpMutations);
|
||||
block_in_place(|| vdom.rebuild(&mut NoOpMutations));
|
||||
vdom.wait_for_suspense().await;
|
||||
tracing::info!("Suspense resolved");
|
||||
// after polling the future, we need to restore the context
|
||||
|
|
|
@ -84,14 +84,36 @@ mod server_fn_impl {
|
|||
/// Get the request that triggered:
|
||||
/// - The initial SSR render if called from a ScopeState or ServerFn
|
||||
/// - The server function to be called if called from a server function after the initial render
|
||||
pub fn request_parts(&self) -> tokio::sync::RwLockReadGuard<'_, http::request::Parts> {
|
||||
pub async fn request_parts(
|
||||
&self,
|
||||
) -> tokio::sync::RwLockReadGuard<'_, http::request::Parts> {
|
||||
self.parts.read().await
|
||||
}
|
||||
|
||||
/// Get the request that triggered:
|
||||
/// - The initial SSR render if called from a ScopeState or ServerFn
|
||||
/// - The server function to be called if called from a server function after the initial render
|
||||
pub fn request_parts_blocking(
|
||||
&self,
|
||||
) -> tokio::sync::RwLockReadGuard<'_, http::request::Parts> {
|
||||
self.parts.blocking_read()
|
||||
}
|
||||
|
||||
/// Get the request that triggered:
|
||||
/// - The initial SSR render if called from a ScopeState or ServerFn
|
||||
/// - The server function to be called if called from a server function after the initial render
|
||||
pub fn request_parts_mut(&self) -> tokio::sync::RwLockWriteGuard<'_, http::request::Parts> {
|
||||
pub async fn request_parts_mut(
|
||||
&self,
|
||||
) -> tokio::sync::RwLockWriteGuard<'_, http::request::Parts> {
|
||||
self.parts.write().await
|
||||
}
|
||||
|
||||
/// Get the request that triggered:
|
||||
/// - The initial SSR render if called from a ScopeState or ServerFn
|
||||
/// - The server function to be called if called from a server function after the initial render
|
||||
pub fn request_parts_mut_blocking(
|
||||
&self,
|
||||
) -> tokio::sync::RwLockWriteGuard<'_, http::request::Parts> {
|
||||
self.parts.blocking_write()
|
||||
}
|
||||
|
||||
|
@ -239,6 +261,6 @@ impl<
|
|||
type Rejection = R;
|
||||
|
||||
async fn from_request(req: &DioxusServerContext) -> Result<Self, Self::Rejection> {
|
||||
Ok(I::from_request_parts(&mut req.request_parts_mut(), &()).await?)
|
||||
Ok(I::from_request_parts(&mut *req.request_parts_mut().await, &()).await?)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,18 @@ pub fn try_use_context<T: 'static + Clone>() -> Option<T> {
|
|||
/// Consume some context in the tree, providing a sharable handle to the value
|
||||
///
|
||||
/// Does not regenerate the value if the value is changed at the parent.
|
||||
/// ```rust
|
||||
/// fn Parent() -> Element {
|
||||
/// use_context_provider(|| Theme::Dark);
|
||||
/// rsx! { Child {} }
|
||||
/// }
|
||||
/// #[component]
|
||||
/// fn Child() -> Element {
|
||||
/// //gets context provided by parent element with use_context_provider
|
||||
/// let user_theme = use_context::<Theme>();
|
||||
/// rsx! { "user using dark mode: {user_theme == Theme::Dark}" }
|
||||
/// }
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn use_context<T: 'static + Clone>() -> T {
|
||||
use_hook(|| consume_context::<T>())
|
||||
|
@ -22,6 +34,24 @@ pub fn use_context<T: 'static + Clone>() -> T {
|
|||
/// Provide some context via the tree and return a reference to it
|
||||
///
|
||||
/// Once the context has been provided, it is immutable. Mutations should be done via interior mutability.
|
||||
/// Context can be read by any child components of the context provider, and is a solution to prop
|
||||
/// drilling, using a context provider with a Signal inside is a good way to provide global/shared
|
||||
/// state in your app:
|
||||
/// ```rust
|
||||
///fn app() -> Element {
|
||||
/// use_context_provider(|| Signal::new(0));
|
||||
/// rsx! { Child {} }
|
||||
///}
|
||||
/// // This component does read from the signal, so when the signal changes it will rerun
|
||||
///#[component]
|
||||
///fn Child() -> Element {
|
||||
/// let signal: Signal<i32> = use_context();
|
||||
/// rsx! {
|
||||
/// button { onclick: move |_| signal += 1, "increment context" }
|
||||
/// p {"{signal}"}
|
||||
/// }
|
||||
///}
|
||||
/// ```
|
||||
pub fn use_context_provider<T: 'static + Clone>(f: impl FnOnce() -> T) -> T {
|
||||
use_hook(|| {
|
||||
let val = f();
|
||||
|
|
|
@ -93,7 +93,8 @@ where
|
|||
}
|
||||
|
||||
/// Get a handle to a coroutine higher in the tree
|
||||
///
|
||||
/// Analagous to use_context_provider and use_context,
|
||||
/// but used for coroutines specifically
|
||||
/// See the docs for [`use_coroutine`] for more details.
|
||||
#[must_use]
|
||||
pub fn use_coroutine_handle<M: 'static>() -> Coroutine<M> {
|
||||
|
|
|
@ -1,21 +1,35 @@
|
|||
use dioxus_core::prelude::*;
|
||||
use dioxus_signals::ReactiveContext;
|
||||
|
||||
/// Create a new effect. The effect will be run immediately and whenever any signal it reads changes.
|
||||
/// The signal will be owned by the current component and will be dropped when the component is dropped.
|
||||
///
|
||||
/// `use_effect` will subscribe to any changes in the signal values it captures
|
||||
/// effects will always run after first mount and then whenever the signal values change
|
||||
/// If the use_effect call was skipped due to an early return, the effect will no longer activate.
|
||||
/// ```rust
|
||||
/// fn app() -> Element {
|
||||
/// let mut count = use_signal(|| 0);
|
||||
/// //the effect runs again each time count changes
|
||||
/// use_effect(move || println!("Count changed to {count}"));
|
||||
///
|
||||
/// rsx! {
|
||||
/// h1 { "High-Five counter: {count}" }
|
||||
/// button { onclick: move |_| count += 1, "Up high!" }
|
||||
/// button { onclick: move |_| count -= 1, "Down low!" }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[track_caller]
|
||||
pub fn use_effect(mut callback: impl FnMut() + 'static) {
|
||||
// let mut run_effect = use_hook(|| CopyValue::new(true));
|
||||
// use_hook_did_run(move |did_run| run_effect.set(did_run));
|
||||
|
||||
let location = std::panic::Location::caller();
|
||||
|
||||
use_hook(|| {
|
||||
spawn(async move {
|
||||
let rc = ReactiveContext::new();
|
||||
|
||||
let rc = ReactiveContext::new_with_origin(location);
|
||||
loop {
|
||||
// Wait for the dom the be finished with sync work
|
||||
flush_sync().await;
|
||||
// flush_sync().await;
|
||||
|
||||
// Run the effect
|
||||
rc.run_in(&mut callback);
|
||||
|
|
|
@ -8,10 +8,34 @@ use dioxus_signals::*;
|
|||
use dioxus_signals::{Readable, Writable};
|
||||
use std::future::Future;
|
||||
|
||||
/// A hook that allows you to spawn a future
|
||||
///
|
||||
/// A hook that allows you to spawn a future.
|
||||
/// This future will **not** run on the server
|
||||
/// The future is spawned on the next call to `flush_sync` which means that it will not run on the server.
|
||||
/// To run a future on the server, you should use `spawn` directly.
|
||||
/// `use_future` **won't return a value**.
|
||||
/// If you want to return a value from a future, use `use_resource` instead.
|
||||
/// ```rust
|
||||
/// fn app() -> Element {
|
||||
/// let mut count = use_signal(|| 0);
|
||||
/// let mut running = use_signal(|| true);
|
||||
/// // `use_future` will spawn an infinitely running future that can be started and stopped
|
||||
/// use_future(move || async move {
|
||||
/// loop {
|
||||
/// if running() {
|
||||
/// count += 1;
|
||||
/// }
|
||||
/// tokio::time::sleep(Duration::from_millis(400)).await;
|
||||
/// }
|
||||
/// });
|
||||
/// rsx! {
|
||||
/// div {
|
||||
/// h1 { "Current count: {count}" }
|
||||
/// button { onclick: move |_| running.toggle(), "Start/Stop the count"}
|
||||
/// button { onclick: move |_| count.set(0), "Reset the count" }
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn use_future<F>(mut future: impl FnMut() -> F + 'static) -> UseFuture
|
||||
where
|
||||
F: Future + 'static,
|
||||
|
|
|
@ -10,8 +10,32 @@ use futures_util::{future, pin_mut, FutureExt};
|
|||
use std::future::Future;
|
||||
|
||||
/// A memo that resolve to a value asynchronously.
|
||||
/// Unlike `use_future`, `use_resource` runs on the **server**
|
||||
/// See [`Resource`] for more details.
|
||||
/// ```rust
|
||||
///fn app() -> Element {
|
||||
/// let country = use_signal(|| WeatherLocation {
|
||||
/// city: "Berlin".to_string(),
|
||||
/// country: "Germany".to_string(),
|
||||
/// coordinates: (52.5244, 13.4105)
|
||||
/// });
|
||||
///
|
||||
/// This runs on the server
|
||||
/// let current_weather = //run a future inside the use_resource hook
|
||||
/// use_resource(move || async move { get_weather(&country.read().clone()).await });
|
||||
///
|
||||
/// rsx! {
|
||||
/// //the value of the future can be polled to
|
||||
/// //conditionally render elements based off if the future
|
||||
/// //finished (Some(Ok(_)), errored Some(Err(_)),
|
||||
/// //or is still finishing (None)
|
||||
/// match current_weather.value() {
|
||||
/// Some(Ok(weather)) => WeatherElement { weather },
|
||||
/// Some(Err(e)) => p { "Loading weather failed, {e}" }
|
||||
/// None => p { "Loading..." }
|
||||
/// }
|
||||
/// }
|
||||
///}
|
||||
/// ```
|
||||
#[must_use = "Consider using `cx.spawn` to run a future without reading its value"]
|
||||
pub fn use_resource<T, F>(future: impl Fn() -> F + 'static) -> Resource<T>
|
||||
where
|
||||
|
|
|
@ -1613,10 +1613,10 @@ builder_constructors! {
|
|||
/// element.
|
||||
hatchpath "http://www.w3.org/2000/svg" {};
|
||||
|
||||
// /// Build a
|
||||
// /// [`<image>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/image)
|
||||
// /// element.
|
||||
// image "http://www.w3.org/2000/svg" {};
|
||||
/// Build a
|
||||
/// [`<image>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/image)
|
||||
/// element.
|
||||
image "http://www.w3.org/2000/svg" {};
|
||||
|
||||
/// Build a
|
||||
/// [`<line>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/line)
|
||||
|
|
|
@ -145,3 +145,607 @@ this.handler = async function (event, name, bubbles) {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
function find_real_id(target) {
|
||||
let realId = null;
|
||||
if (target instanceof Element) {
|
||||
realId = target.getAttribute(`data-dioxus-id`);
|
||||
}
|
||||
// walk the tree to find the real element
|
||||
while (realId == null) {
|
||||
// we've reached the root we don't want to send an event
|
||||
if (target.parentElement === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
target = target.parentElement;
|
||||
if (target instanceof Element) {
|
||||
realId = target.getAttribute(`data-dioxus-id`);
|
||||
}
|
||||
}
|
||||
return realId;
|
||||
}
|
||||
|
||||
class ListenerMap {
|
||||
constructor(root) {
|
||||
// bubbling events can listen at the root element
|
||||
this.global = {};
|
||||
// non bubbling events listen at the element the listener was created at
|
||||
this.local = {};
|
||||
this.root = null;
|
||||
}
|
||||
|
||||
create(event_name, element, bubbles, handler) {
|
||||
if (bubbles) {
|
||||
if (this.global[event_name] === undefined) {
|
||||
this.global[event_name] = {};
|
||||
this.global[event_name].active = 1;
|
||||
this.root.addEventListener(event_name, handler);
|
||||
} else {
|
||||
this.global[event_name].active++;
|
||||
}
|
||||
}
|
||||
else {
|
||||
const id = element.getAttribute("data-dioxus-id");
|
||||
if (!this.local[id]) {
|
||||
this.local[id] = {};
|
||||
}
|
||||
element.addEventListener(event_name, handler);
|
||||
}
|
||||
}
|
||||
|
||||
remove(element, event_name, bubbles) {
|
||||
if (bubbles) {
|
||||
this.global[event_name].active--;
|
||||
if (this.global[event_name].active === 0) {
|
||||
this.root.removeEventListener(event_name, this.global[event_name].callback);
|
||||
delete this.global[event_name];
|
||||
}
|
||||
}
|
||||
else {
|
||||
const id = element.getAttribute("data-dioxus-id");
|
||||
delete this.local[id][event_name];
|
||||
if (this.local[id].length === 0) {
|
||||
delete this.local[id];
|
||||
}
|
||||
element.removeEventListener(event_name, this.global[event_name].callback);
|
||||
}
|
||||
}
|
||||
|
||||
removeAllNonBubbling(element) {
|
||||
const id = element.getAttribute("data-dioxus-id");
|
||||
delete this.local[id];
|
||||
}
|
||||
}
|
||||
this.LoadChild = function (array) {
|
||||
// iterate through each number and get that child
|
||||
let node = this.stack[this.stack.length - 1];
|
||||
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
this.end = array[i];
|
||||
for (node = node.firstChild; this.end > 0; this.end--) {
|
||||
node = node.nextSibling;
|
||||
}
|
||||
}
|
||||
return node;
|
||||
}
|
||||
this.listeners = new ListenerMap();
|
||||
this.nodes = [];
|
||||
this.stack = [];
|
||||
this.templates = {};
|
||||
this.end = null;
|
||||
|
||||
this.AppendChildren = function (id, many) {
|
||||
let root = this.nodes[id];
|
||||
let els = this.stack.splice(this.stack.length - many);
|
||||
for (let k = 0; k < many; k++) {
|
||||
root.appendChild(els[k]);
|
||||
}
|
||||
}
|
||||
|
||||
this.initialize = function (root) {
|
||||
this.nodes = [root];
|
||||
this.stack = [root];
|
||||
this.listeners.root = root;
|
||||
}
|
||||
|
||||
this.getClientRect = function (id) {
|
||||
const node = this.nodes[id];
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
const rect = node.getBoundingClientRect();
|
||||
return {
|
||||
type: "GetClientRect",
|
||||
origin: [rect.x, rect.y],
|
||||
size: [rect.width, rect.height],
|
||||
};
|
||||
}
|
||||
|
||||
this.scrollTo = function (id, behavior) {
|
||||
const node = this.nodes[id];
|
||||
if (!node) {
|
||||
return false;
|
||||
}
|
||||
node.scrollIntoView({
|
||||
behavior: behavior,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Set the focus on the element
|
||||
this.setFocus = function (id, focus) {
|
||||
const node = this.nodes[id];
|
||||
if (!node) {
|
||||
return false;
|
||||
}
|
||||
if (focus) {
|
||||
node.focus();
|
||||
} else {
|
||||
node.blur();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function get_mouse_data(event) {
|
||||
const {
|
||||
altKey,
|
||||
button,
|
||||
buttons,
|
||||
clientX,
|
||||
clientY,
|
||||
ctrlKey,
|
||||
metaKey,
|
||||
offsetX,
|
||||
offsetY,
|
||||
pageX,
|
||||
pageY,
|
||||
screenX,
|
||||
screenY,
|
||||
shiftKey,
|
||||
} = event;
|
||||
return {
|
||||
alt_key: altKey,
|
||||
button: button,
|
||||
buttons: buttons,
|
||||
client_x: clientX,
|
||||
client_y: clientY,
|
||||
ctrl_key: ctrlKey,
|
||||
meta_key: metaKey,
|
||||
offset_x: offsetX,
|
||||
offset_y: offsetY,
|
||||
page_x: pageX,
|
||||
page_y: pageY,
|
||||
screen_x: screenX,
|
||||
screen_y: screenY,
|
||||
shift_key: shiftKey,
|
||||
};
|
||||
}
|
||||
|
||||
async function serialize_event(event) {
|
||||
switch (event.type) {
|
||||
case "copy":
|
||||
case "cut":
|
||||
case "past": {
|
||||
return {};
|
||||
}
|
||||
case "compositionend":
|
||||
case "compositionstart":
|
||||
case "compositionupdate": {
|
||||
let { data } = event;
|
||||
return {
|
||||
data,
|
||||
};
|
||||
}
|
||||
case "keydown":
|
||||
case "keypress":
|
||||
case "keyup": {
|
||||
let {
|
||||
charCode,
|
||||
isComposing,
|
||||
key,
|
||||
altKey,
|
||||
ctrlKey,
|
||||
metaKey,
|
||||
keyCode,
|
||||
shiftKey,
|
||||
location,
|
||||
repeat,
|
||||
which,
|
||||
code,
|
||||
} = event;
|
||||
return {
|
||||
char_code: charCode,
|
||||
is_composing: isComposing,
|
||||
key: key,
|
||||
alt_key: altKey,
|
||||
ctrl_key: ctrlKey,
|
||||
meta_key: metaKey,
|
||||
key_code: keyCode,
|
||||
shift_key: shiftKey,
|
||||
location: location,
|
||||
repeat: repeat,
|
||||
which: which,
|
||||
code,
|
||||
};
|
||||
}
|
||||
case "focus":
|
||||
case "blur": {
|
||||
return {};
|
||||
}
|
||||
case "change": {
|
||||
let target = event.target;
|
||||
let value;
|
||||
if (target.type === "checkbox" || target.type === "radio") {
|
||||
value = target.checked ? "true" : "false";
|
||||
} else {
|
||||
value = target.value ?? target.textContent;
|
||||
}
|
||||
return {
|
||||
value: value,
|
||||
values: {},
|
||||
};
|
||||
}
|
||||
case "input":
|
||||
case "invalid":
|
||||
case "reset":
|
||||
case "submit": {
|
||||
let target = event.target;
|
||||
let value = target.value ?? target.textContent;
|
||||
|
||||
if (target.type === "checkbox") {
|
||||
value = target.checked ? "true" : "false";
|
||||
}
|
||||
|
||||
return {
|
||||
value: value,
|
||||
values: {},
|
||||
};
|
||||
}
|
||||
case "drag":
|
||||
case "dragend":
|
||||
case "dragenter":
|
||||
case "dragexit":
|
||||
case "dragleave":
|
||||
case "dragover":
|
||||
case "dragstart":
|
||||
case "drop": {
|
||||
let files = null;
|
||||
if (event.dataTransfer && event.dataTransfer.files) {
|
||||
files = await serializeFileList(event.dataTransfer.files);
|
||||
}
|
||||
|
||||
return { mouse: get_mouse_data(event), files };
|
||||
}
|
||||
case "click":
|
||||
case "contextmenu":
|
||||
case "doubleclick":
|
||||
case "dblclick":
|
||||
case "mousedown":
|
||||
case "mouseenter":
|
||||
case "mouseleave":
|
||||
case "mousemove":
|
||||
case "mouseout":
|
||||
case "mouseover":
|
||||
case "mouseup": {
|
||||
return get_mouse_data(event);
|
||||
}
|
||||
case "pointerdown":
|
||||
case "pointermove":
|
||||
case "pointerup":
|
||||
case "pointercancel":
|
||||
case "gotpointercapture":
|
||||
case "lostpointercapture":
|
||||
case "pointerenter":
|
||||
case "pointerleave":
|
||||
case "pointerover":
|
||||
case "pointerout": {
|
||||
const {
|
||||
altKey,
|
||||
button,
|
||||
buttons,
|
||||
clientX,
|
||||
clientY,
|
||||
ctrlKey,
|
||||
metaKey,
|
||||
pageX,
|
||||
pageY,
|
||||
screenX,
|
||||
screenY,
|
||||
shiftKey,
|
||||
pointerId,
|
||||
width,
|
||||
height,
|
||||
pressure,
|
||||
tangentialPressure,
|
||||
tiltX,
|
||||
tiltY,
|
||||
twist,
|
||||
pointerType,
|
||||
isPrimary,
|
||||
} = event;
|
||||
return {
|
||||
alt_key: altKey,
|
||||
button: button,
|
||||
buttons: buttons,
|
||||
client_x: clientX,
|
||||
client_y: clientY,
|
||||
ctrl_key: ctrlKey,
|
||||
meta_key: metaKey,
|
||||
page_x: pageX,
|
||||
page_y: pageY,
|
||||
screen_x: screenX,
|
||||
screen_y: screenY,
|
||||
shift_key: shiftKey,
|
||||
pointer_id: pointerId,
|
||||
width: width,
|
||||
height: height,
|
||||
pressure: pressure,
|
||||
tangential_pressure: tangentialPressure,
|
||||
tilt_x: tiltX,
|
||||
tilt_y: tiltY,
|
||||
twist: twist,
|
||||
pointer_type: pointerType,
|
||||
is_primary: isPrimary,
|
||||
};
|
||||
}
|
||||
case "select": {
|
||||
return {};
|
||||
}
|
||||
case "touchcancel":
|
||||
case "touchend":
|
||||
case "touchmove":
|
||||
case "touchstart": {
|
||||
const { altKey, ctrlKey, metaKey, shiftKey } = event;
|
||||
return {
|
||||
// changed_touches: event.changedTouches,
|
||||
// target_touches: event.targetTouches,
|
||||
// touches: event.touches,
|
||||
alt_key: altKey,
|
||||
ctrl_key: ctrlKey,
|
||||
meta_key: metaKey,
|
||||
shift_key: shiftKey,
|
||||
};
|
||||
}
|
||||
case "scroll": {
|
||||
return {};
|
||||
}
|
||||
case "wheel": {
|
||||
const { deltaX, deltaY, deltaZ, deltaMode } = event;
|
||||
return {
|
||||
delta_x: deltaX,
|
||||
delta_y: deltaY,
|
||||
delta_z: deltaZ,
|
||||
delta_mode: deltaMode,
|
||||
};
|
||||
}
|
||||
case "animationstart":
|
||||
case "animationend":
|
||||
case "animationiteration": {
|
||||
const { animationName, elapsedTime, pseudoElement } = event;
|
||||
return {
|
||||
animation_name: animationName,
|
||||
elapsed_time: elapsedTime,
|
||||
pseudo_element: pseudoElement,
|
||||
};
|
||||
}
|
||||
case "transitionend": {
|
||||
const { propertyName, elapsedTime, pseudoElement } = event;
|
||||
return {
|
||||
property_name: propertyName,
|
||||
elapsed_time: elapsedTime,
|
||||
pseudo_element: pseudoElement,
|
||||
};
|
||||
}
|
||||
case "abort":
|
||||
case "canplay":
|
||||
case "canplaythrough":
|
||||
case "durationchange":
|
||||
case "emptied":
|
||||
case "encrypted":
|
||||
case "ended":
|
||||
case "error":
|
||||
case "loadeddata":
|
||||
case "loadedmetadata":
|
||||
case "loadstart":
|
||||
case "pause":
|
||||
case "play":
|
||||
case "playing":
|
||||
case "progress":
|
||||
case "ratechange":
|
||||
case "seeked":
|
||||
case "seeking":
|
||||
case "stalled":
|
||||
case "suspend":
|
||||
case "timeupdate":
|
||||
case "volumechange":
|
||||
case "waiting": {
|
||||
return {};
|
||||
}
|
||||
case "toggle": {
|
||||
return {};
|
||||
}
|
||||
default: {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
this.serializeIpcMessage = function (method, params = {}) {
|
||||
return JSON.stringify({ method, params });
|
||||
}
|
||||
|
||||
function is_element_node(node) {
|
||||
return node.nodeType == 1;
|
||||
}
|
||||
|
||||
function event_bubbles(event) {
|
||||
switch (event) {
|
||||
case "copy":
|
||||
return true;
|
||||
case "cut":
|
||||
return true;
|
||||
case "paste":
|
||||
return true;
|
||||
case "compositionend":
|
||||
return true;
|
||||
case "compositionstart":
|
||||
return true;
|
||||
case "compositionupdate":
|
||||
return true;
|
||||
case "keydown":
|
||||
return true;
|
||||
case "keypress":
|
||||
return true;
|
||||
case "keyup":
|
||||
return true;
|
||||
case "focus":
|
||||
return false;
|
||||
case "focusout":
|
||||
return true;
|
||||
case "focusin":
|
||||
return true;
|
||||
case "blur":
|
||||
return false;
|
||||
case "change":
|
||||
return true;
|
||||
case "input":
|
||||
return true;
|
||||
case "invalid":
|
||||
return true;
|
||||
case "reset":
|
||||
return true;
|
||||
case "submit":
|
||||
return true;
|
||||
case "click":
|
||||
return true;
|
||||
case "contextmenu":
|
||||
return true;
|
||||
case "doubleclick":
|
||||
return true;
|
||||
case "dblclick":
|
||||
return true;
|
||||
case "drag":
|
||||
return true;
|
||||
case "dragend":
|
||||
return true;
|
||||
case "dragenter":
|
||||
return false;
|
||||
case "dragexit":
|
||||
return false;
|
||||
case "dragleave":
|
||||
return true;
|
||||
case "dragover":
|
||||
return true;
|
||||
case "dragstart":
|
||||
return true;
|
||||
case "drop":
|
||||
return true;
|
||||
case "mousedown":
|
||||
return true;
|
||||
case "mouseenter":
|
||||
return false;
|
||||
case "mouseleave":
|
||||
return false;
|
||||
case "mousemove":
|
||||
return true;
|
||||
case "mouseout":
|
||||
return true;
|
||||
case "scroll":
|
||||
return false;
|
||||
case "mouseover":
|
||||
return true;
|
||||
case "mouseup":
|
||||
return true;
|
||||
case "pointerdown":
|
||||
return true;
|
||||
case "pointermove":
|
||||
return true;
|
||||
case "pointerup":
|
||||
return true;
|
||||
case "pointercancel":
|
||||
return true;
|
||||
case "gotpointercapture":
|
||||
return true;
|
||||
case "lostpointercapture":
|
||||
return true;
|
||||
case "pointerenter":
|
||||
return false;
|
||||
case "pointerleave":
|
||||
return false;
|
||||
case "pointerover":
|
||||
return true;
|
||||
case "pointerout":
|
||||
return true;
|
||||
case "select":
|
||||
return true;
|
||||
case "touchcancel":
|
||||
return true;
|
||||
case "touchend":
|
||||
return true;
|
||||
case "touchmove":
|
||||
return true;
|
||||
case "touchstart":
|
||||
return true;
|
||||
case "wheel":
|
||||
return true;
|
||||
case "abort":
|
||||
return false;
|
||||
case "canplay":
|
||||
return false;
|
||||
case "canplaythrough":
|
||||
return false;
|
||||
case "durationchange":
|
||||
return false;
|
||||
case "emptied":
|
||||
return false;
|
||||
case "encrypted":
|
||||
return true;
|
||||
case "ended":
|
||||
return false;
|
||||
case "error":
|
||||
return false;
|
||||
case "loadeddata":
|
||||
case "loadedmetadata":
|
||||
case "loadstart":
|
||||
case "load":
|
||||
return false;
|
||||
case "pause":
|
||||
return false;
|
||||
case "play":
|
||||
return false;
|
||||
case "playing":
|
||||
return false;
|
||||
case "progress":
|
||||
return false;
|
||||
case "ratechange":
|
||||
return false;
|
||||
case "seeked":
|
||||
return false;
|
||||
case "seeking":
|
||||
return false;
|
||||
case "stalled":
|
||||
return false;
|
||||
case "suspend":
|
||||
return false;
|
||||
case "timeupdate":
|
||||
return false;
|
||||
case "volumechange":
|
||||
return false;
|
||||
case "waiting":
|
||||
return false;
|
||||
case "animationstart":
|
||||
return true;
|
||||
case "animationend":
|
||||
return true;
|
||||
case "animationiteration":
|
||||
return true;
|
||||
case "transitionend":
|
||||
return true;
|
||||
case "toggle":
|
||||
return true;
|
||||
case "mounted":
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -80,9 +80,7 @@ mod js {
|
|||
this.listeners = new ListenerMap();
|
||||
this.nodes = [];
|
||||
this.stack = [];
|
||||
this.root = null;
|
||||
this.templates = {};
|
||||
this.els = null;
|
||||
this.save_template = function(nodes, tmpl_id) {
|
||||
this.templates[tmpl_id] = nodes;
|
||||
}
|
||||
|
@ -131,15 +129,15 @@ mod js {
|
|||
}
|
||||
this.AppendChildren = function (id, many){
|
||||
let root = this.nodes[id];
|
||||
this.els = this.stack.splice(this.stack.length-many);
|
||||
let els = this.stack.splice(this.stack.length-many);
|
||||
for (let k = 0; k < many; k++) {
|
||||
root.appendChild(this.els[k]);
|
||||
root.appendChild(els[k]);
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
fn mount_to_root() {
|
||||
"{this.AppendChildren(this.root, this.stack.length-1);}"
|
||||
"{this.AppendChildren(this.listeners.root, this.stack.length-1);}"
|
||||
}
|
||||
fn push_root(root: u32) {
|
||||
"{this.stack.push(this.nodes[$root$]);}"
|
||||
|
@ -151,7 +149,7 @@ mod js {
|
|||
"{this.stack.pop();}"
|
||||
}
|
||||
fn replace_with(id: u32, n: u16) {
|
||||
"{const root = this.nodes[$id$]; this.els = this.stack.splice(this.stack.length-$n$); if (root.listening) { this.listeners.removeAllNonBubbling(root); } root.replaceWith(...this.els);}"
|
||||
"{const root = this.nodes[$id$]; let els = this.stack.splice(this.stack.length-$n$); if (root.listening) { this.listeners.removeAllNonBubbling(root); } root.replaceWith(...els);}"
|
||||
}
|
||||
fn insert_after(id: u32, n: u16) {
|
||||
"{this.nodes[$id$].after(...this.stack.splice(this.stack.length-$n$));}"
|
||||
|
@ -228,7 +226,7 @@ mod js {
|
|||
}"#
|
||||
}
|
||||
fn replace_placeholder(ptr: u32, len: u8, n: u16) {
|
||||
"{this.els = this.stack.splice(this.stack.length - $n$); let node = this.LoadChild($ptr$, $len$); node.replaceWith(...this.els);}"
|
||||
"{els = this.stack.splice(this.stack.length - $n$); let node = this.LoadChild($ptr$, $len$); node.replaceWith(...els);}"
|
||||
}
|
||||
fn load_template(tmpl_id: u16, index: u16, id: u32) {
|
||||
"{let node = this.templates[$tmpl_id$][$index$].cloneNode(true); this.nodes[$id$] = node; this.stack.push(node);}"
|
||||
|
@ -270,9 +268,6 @@ pub mod binary_protocol {
|
|||
const JS_FILE: &str = "./src/interpreter.js";
|
||||
const JS_FILE: &str = "./src/common.js";
|
||||
|
||||
fn mount_to_root() {
|
||||
"{this.AppendChildren(this.root, this.stack.length-1);}"
|
||||
}
|
||||
fn push_root(root: u32) {
|
||||
"{this.stack.push(this.nodes[$root$]);}"
|
||||
}
|
||||
|
@ -282,9 +277,9 @@ pub mod binary_protocol {
|
|||
fn append_children_to_top(many: u16) {
|
||||
"{
|
||||
let root = this.stack[this.stack.length-many-1];
|
||||
this.els = this.stack.splice(this.stack.length-many);
|
||||
let els = this.stack.splice(this.stack.length-many);
|
||||
for (let k = 0; k < many; k++) {
|
||||
root.appendChild(this.els[k]);
|
||||
root.appendChild(els[k]);
|
||||
}
|
||||
}"
|
||||
}
|
||||
|
@ -292,7 +287,7 @@ pub mod binary_protocol {
|
|||
"{this.stack.pop();}"
|
||||
}
|
||||
fn replace_with(id: u32, n: u16) {
|
||||
"{let root = this.nodes[$id$]; this.els = this.stack.splice(this.stack.length-$n$); if (root.listening) { this.listeners.removeAllNonBubbling(root); } root.replaceWith(...this.els);}"
|
||||
"{let root = this.nodes[$id$]; let els = this.stack.splice(this.stack.length-$n$); if (root.listening) { this.listeners.removeAllNonBubbling(root); } root.replaceWith(...els);}"
|
||||
}
|
||||
fn insert_after(id: u32, n: u16) {
|
||||
"{this.nodes[$id$].after(...this.stack.splice(this.stack.length-$n$));}"
|
||||
|
@ -406,7 +401,7 @@ pub mod binary_protocol {
|
|||
}"#
|
||||
}
|
||||
fn replace_placeholder(array: &[u8], n: u16) {
|
||||
"{this.els = this.stack.splice(this.stack.length - $n$); let node = this.LoadChild($array$); node.replaceWith(...this.els);}"
|
||||
"{let els = this.stack.splice(this.stack.length - $n$); let node = this.LoadChild($array$); node.replaceWith(...els);}"
|
||||
}
|
||||
fn load_template(tmpl_id: u16, index: u16, id: u32) {
|
||||
"{let node = this.templates[$tmpl_id$][$index$].cloneNode(true); this.nodes[$id$] = node; this.stack.push(node);}"
|
||||
|
|
|
@ -2,7 +2,7 @@ use dioxus_lib::prelude::{try_consume_context, use_hook};
|
|||
|
||||
use crate::prelude::{Navigator, RouterContext};
|
||||
|
||||
/// A hook that provides access to the navigator to change the router history. Unlike [`use_router`], this hook will not cause a rerender when the current route changes
|
||||
/// A hook that provides access to the navigator to change the router history.
|
||||
///
|
||||
/// > The Routable macro will define a version of this hook with an explicit type.
|
||||
///
|
||||
|
@ -26,7 +26,7 @@ use crate::prelude::{Navigator, RouterContext};
|
|||
///
|
||||
/// #[component]
|
||||
/// fn Index() -> Element {
|
||||
/// let navigator = use_navigator(&cx);
|
||||
/// let navigator = use_navigator();
|
||||
///
|
||||
/// rsx! {
|
||||
/// button {
|
||||
|
|
|
@ -5,12 +5,8 @@ use crate::utils::use_router_internal::use_router_internal;
|
|||
///
|
||||
/// > The Routable macro will define a version of this hook with an explicit type.
|
||||
///
|
||||
/// # Return values
|
||||
/// - None, when not called inside a [`Link`] component.
|
||||
/// - Otherwise the current route.
|
||||
///
|
||||
/// # Panic
|
||||
/// - When the calling component is not nested within a [`Link`] component during a debug build.
|
||||
/// - When the calling component is not nested within a [`Router`] component.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
|
@ -49,7 +45,7 @@ pub fn use_route<R: Routable + Clone>() -> R {
|
|||
match use_router_internal() {
|
||||
Some(r) => r.current(),
|
||||
None => {
|
||||
panic!("`use_route` must have access to a parent router")
|
||||
panic!("`use_route` must be called in a descendant of a Router component")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -134,7 +134,7 @@ where
|
|||
return Box::new(AnyHistoryProviderImplWrapper::new(
|
||||
MemoryHistory::<R>::with_initial_path(
|
||||
dioxus_fullstack::prelude::server_context()
|
||||
.request_parts()
|
||||
.request_parts_blocking()
|
||||
.uri
|
||||
.to_string()
|
||||
.parse()
|
||||
|
|
|
@ -8,10 +8,10 @@ use crate::prelude::*;
|
|||
/// single component, but not recommended. Multiple subscriptions will be discarded.
|
||||
///
|
||||
/// # Return values
|
||||
/// - [`None`], when the current component isn't a descendant of a [`Link`] component.
|
||||
/// - [`None`], when the current component isn't a descendant of a [`Router`] component.
|
||||
/// - Otherwise [`Some`].
|
||||
pub(crate) fn use_router_internal() -> Option<RouterContext> {
|
||||
let router = use_hook(consume_context::<RouterContext>);
|
||||
let router = try_consume_context::<RouterContext>()?;
|
||||
let id = current_scope_id().expect("use_router_internal called outside of a component");
|
||||
use_drop({
|
||||
to_owned![router];
|
||||
|
|
|
@ -36,18 +36,18 @@ use dioxus_signals::*;
|
|||
#[component]
|
||||
fn App() -> Element {
|
||||
// Because signal is never read in this component, this component will not rerun when the signal changes
|
||||
let signal = use_signal(|| 0);
|
||||
let mut signal = use_signal(|| 0);
|
||||
|
||||
rsx! {
|
||||
button {
|
||||
onclick: move |_| {
|
||||
*signal.write() += 1;
|
||||
signal += 1;
|
||||
},
|
||||
"Increase"
|
||||
}
|
||||
for id in 0..10 {
|
||||
Child {
|
||||
signal: signal,
|
||||
signal,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -58,11 +58,10 @@ struct ChildProps {
|
|||
signal: Signal<usize>,
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Child(cx: Scope<ChildProps>) -> Element {
|
||||
fn Child(props: ChildProps) -> Element {
|
||||
// This component does read from the signal, so when the signal changes it will rerun
|
||||
rsx! {
|
||||
"{cx.props.signal}"
|
||||
"{props.signal}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
@ -85,7 +84,7 @@ fn App() -> Element {
|
|||
|
||||
#[component]
|
||||
fn Child() -> Element {
|
||||
let signal: Signal<i32> = *use_context(cx).unwrap();
|
||||
let signal: Signal<i32> = use_context();
|
||||
// This component does read from the signal, so when the signal changes it will rerun
|
||||
rsx! {
|
||||
"{signal}"
|
||||
|
@ -105,12 +104,12 @@ use dioxus_signals::*;
|
|||
|
||||
#[component]
|
||||
fn App() -> Element {
|
||||
let signal = use_signal(|| 0);
|
||||
let doubled = use_memo(|| signal * 2);
|
||||
let mut signal = use_signal(|| 0);
|
||||
let doubled = use_memo(move || signal * 2);
|
||||
|
||||
rsx! {
|
||||
button {
|
||||
onclick: move |_| *signal.write() += 1,
|
||||
onclick: move |_| signal += 1,
|
||||
"Increase"
|
||||
}
|
||||
Child {
|
||||
|
|
|
@ -93,7 +93,7 @@ fn current_owner<S: Storage<T>, T>() -> Owner<S> {
|
|||
}
|
||||
|
||||
fn owner_in_scope<S: Storage<T>, T>(scope: ScopeId) -> Owner<S> {
|
||||
match consume_context_from_scope(scope) {
|
||||
match scope.has_context() {
|
||||
Some(rt) => rt,
|
||||
None => {
|
||||
let owner = S::owner();
|
||||
|
|
|
@ -22,20 +22,47 @@ thread_local! {
|
|||
static CURRENT: RefCell<Vec<ReactiveContext>> = const { RefCell::new(vec![]) };
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ReactiveContext {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let read = self.inner.read();
|
||||
match read.scope_subscriber {
|
||||
Some(scope) => write!(f, "ReactiveContext for scope {:?}", scope),
|
||||
None => {
|
||||
#[cfg(debug_assertions)]
|
||||
return write!(f, "ReactiveContext created at {}", read.origin);
|
||||
#[cfg(not(debug_assertions))]
|
||||
write!(f, "ReactiveContext")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ReactiveContext {
|
||||
#[track_caller]
|
||||
fn default() -> Self {
|
||||
Self::new_for_scope(None)
|
||||
Self::new_for_scope(None, std::panic::Location::caller())
|
||||
}
|
||||
}
|
||||
|
||||
impl ReactiveContext {
|
||||
/// Create a new reactive context
|
||||
#[track_caller]
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Create a new reactive context with a location for debugging purposes
|
||||
/// This is useful for reactive contexts created within closures
|
||||
pub fn new_with_origin(origin: &'static std::panic::Location<'static>) -> Self {
|
||||
Self::new_for_scope(None, origin)
|
||||
}
|
||||
|
||||
/// Create a new reactive context that may update a scope
|
||||
pub(crate) fn new_for_scope(scope: Option<ScopeId>) -> Self {
|
||||
#[allow(unused)]
|
||||
pub(crate) fn new_for_scope(
|
||||
scope: Option<ScopeId>,
|
||||
origin: &'static std::panic::Location<'static>,
|
||||
) -> Self {
|
||||
let (tx, rx) = flume::unbounded();
|
||||
|
||||
let mut scope_subscribers = FxHashSet::default();
|
||||
|
@ -49,6 +76,8 @@ impl ReactiveContext {
|
|||
self_: None,
|
||||
update_any: schedule_update_any(),
|
||||
receiver: rx,
|
||||
#[cfg(debug_assertions)]
|
||||
origin,
|
||||
};
|
||||
|
||||
let mut self_ = Self {
|
||||
|
@ -87,6 +116,7 @@ impl ReactiveContext {
|
|||
// Otherwise, create a new context at the current scope
|
||||
Some(provide_context(ReactiveContext::new_for_scope(
|
||||
current_scope_id(),
|
||||
std::panic::Location::caller(),
|
||||
)))
|
||||
}
|
||||
|
||||
|
@ -108,6 +138,17 @@ impl ReactiveContext {
|
|||
/// Returns true if the context was marked as dirty, or false if the context has been dropped
|
||||
pub fn mark_dirty(&self) -> bool {
|
||||
if let Ok(self_read) = self.inner.try_read() {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
if let Some(scope) = self_read.scope_subscriber {
|
||||
tracing::trace!("Marking reactive context for scope {:?} as dirty", scope);
|
||||
} else {
|
||||
tracing::trace!(
|
||||
"Marking reactive context created at {} as dirty",
|
||||
self_read.origin
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(scope) = self_read.scope_subscriber {
|
||||
(self_read.update_any)(scope);
|
||||
}
|
||||
|
@ -148,4 +189,8 @@ struct Inner {
|
|||
// Futures will call .changed().await
|
||||
sender: flume::Sender<()>,
|
||||
receiver: flume::Receiver<()>,
|
||||
|
||||
// Debug information for signal subscriptions
|
||||
#[cfg(debug_assertions)]
|
||||
origin: &'static std::panic::Location<'static>,
|
||||
}
|
||||
|
|
|
@ -39,7 +39,9 @@ pub trait Readable {
|
|||
MappedSignal::new(try_read, peek)
|
||||
}
|
||||
|
||||
/// Get the current value of the state. If this is a signal, this will subscribe the current scope to the signal. If the value has been dropped, this will panic.
|
||||
/// Get the current value of the state. If this is a signal, this will subscribe the current scope to the signal.
|
||||
/// If the value has been dropped, this will panic. Calling this on a Signal is the same as
|
||||
/// using the signal() syntax to read and subscribe to its value
|
||||
#[track_caller]
|
||||
fn read(&self) -> ReadableRef<Self> {
|
||||
self.try_read().unwrap()
|
||||
|
|
|
@ -30,17 +30,15 @@ use std::{
|
|||
/// }
|
||||
///
|
||||
/// #[component]
|
||||
/// fn Child(state: Signal<u32>) -> Element {
|
||||
/// let state = *state;
|
||||
///
|
||||
/// use_future( |()| async move {
|
||||
/// fn Child(mut state: Signal<u32>) -> Element {
|
||||
/// use_future(move || async move {
|
||||
/// // Because the signal is a Copy type, we can use it in an async block without cloning it.
|
||||
/// *state.write() += 1;
|
||||
/// state += 1;
|
||||
/// });
|
||||
///
|
||||
/// rsx! {
|
||||
/// button {
|
||||
/// onclick: move |_| *state.write() += 1,
|
||||
/// onclick: move |_| state += 1,
|
||||
/// "{state}"
|
||||
/// }
|
||||
/// }
|
||||
|
@ -202,6 +200,7 @@ impl<T, S: Storage<SignalData<T>>> Readable for Signal<T, S> {
|
|||
let inner = self.inner.try_read()?;
|
||||
|
||||
if let Some(reactive_context) = ReactiveContext::current() {
|
||||
tracing::trace!("Subscribing to the reactive context {}", reactive_context);
|
||||
inner.subscribers.lock().unwrap().insert(reactive_context);
|
||||
}
|
||||
|
||||
|
@ -244,7 +243,11 @@ impl<T: 'static, S: Storage<SignalData<T>>> Writable for Signal<T, S> {
|
|||
let borrow = S::map_mut(inner, |v| &mut v.value);
|
||||
Write {
|
||||
write: borrow,
|
||||
drop_signal: Box::new(SignalSubscriberDrop { signal: *self }),
|
||||
drop_signal: Box::new(SignalSubscriberDrop {
|
||||
signal: *self,
|
||||
#[cfg(debug_assertions)]
|
||||
origin: std::panic::Location::caller(),
|
||||
}),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -344,10 +347,17 @@ impl<T: ?Sized, S: AnyStorage> DerefMut for Write<T, S> {
|
|||
|
||||
struct SignalSubscriberDrop<T: 'static, S: Storage<SignalData<T>>> {
|
||||
signal: Signal<T, S>,
|
||||
#[cfg(debug_assertions)]
|
||||
origin: &'static std::panic::Location<'static>,
|
||||
}
|
||||
|
||||
impl<T: 'static, S: Storage<SignalData<T>>> Drop for SignalSubscriberDrop<T, S> {
|
||||
fn drop(&mut self) {
|
||||
#[cfg(debug_assertions)]
|
||||
tracing::trace!(
|
||||
"Write on signal at {:?} finished, updating subscribers",
|
||||
self.origin
|
||||
);
|
||||
self.signal.update_subscribers();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,6 +56,11 @@ fn deref_signal() {
|
|||
|
||||
#[test]
|
||||
fn drop_signals() {
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
static SIGNAL_DROP_COUNT: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
let mut dom = VirtualDom::new(|| {
|
||||
let generation = generation();
|
||||
|
||||
|
@ -68,10 +73,18 @@ fn drop_signals() {
|
|||
});
|
||||
|
||||
fn Child() -> Element {
|
||||
let signal = create_without_cx();
|
||||
struct TracksDrops;
|
||||
|
||||
impl Drop for TracksDrops {
|
||||
fn drop(&mut self) {
|
||||
SIGNAL_DROP_COUNT.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
use_signal(|| TracksDrops);
|
||||
|
||||
rsx! {
|
||||
"{signal}"
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -79,7 +92,5 @@ fn drop_signals() {
|
|||
dom.mark_dirty(ScopeId::ROOT);
|
||||
dom.render_immediate(&mut NoOpMutations);
|
||||
|
||||
fn create_without_cx() -> Signal<String> {
|
||||
Signal::new("hello world".to_string())
|
||||
}
|
||||
assert_eq!(SIGNAL_DROP_COUNT.load(Ordering::Relaxed), 10);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue