Merge branch 'main' into jk/fix-form-inputs

This commit is contained in:
Jonathan Kelley 2024-03-04 17:46:38 -08:00
commit 16b38e339d
No known key found for this signature in database
GPG key ID: 1FBB50F7EB0A08BE
28 changed files with 1007 additions and 86 deletions

17
Cargo.lock generated
View file

@ -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"

View file

@ -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;

View file

@ -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}" }
/// }

View file

@ -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"

View file

@ -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;
}

View file

@ -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.
///

View file

@ -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();

View 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}" } }
}

View file

@ -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

View file

@ -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?)
}
}

View file

@ -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();

View file

@ -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> {

View file

@ -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);

View file

@ -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,

View file

@ -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

View file

@ -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)

View file

@ -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;
}

View file

@ -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);}"

View file

@ -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 {

View file

@ -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")
}
}
}

View file

@ -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()

View file

@ -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];

View file

@ -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 {

View file

@ -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();

View file

@ -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>,
}

View file

@ -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()

View file

@ -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();
}
}

View file

@ -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);
}