mirror of
https://github.com/leptos-rs/leptos
synced 2024-09-20 06:21:57 +00:00
perf: further reduce WASM binary size by ~5-7% (#459)
* Update `leptos_router` docs * Further reducing WASM bundle sizes
This commit is contained in:
parent
c4e693e01e
commit
6d0d70cd17
10 changed files with 349 additions and 299 deletions
|
@ -154,167 +154,182 @@ where
|
||||||
instrument(level = "trace", name = "<DynChild />", skip_all)
|
instrument(level = "trace", name = "<DynChild />", skip_all)
|
||||||
)]
|
)]
|
||||||
fn into_view(self, cx: Scope) -> View {
|
fn into_view(self, cx: Scope) -> View {
|
||||||
let Self { id, child_fn } = self;
|
// concrete inner function
|
||||||
|
fn create_dyn_view(
|
||||||
|
cx: Scope,
|
||||||
|
component: DynChildRepr,
|
||||||
|
child_fn: Box<dyn Fn() -> View>,
|
||||||
|
) -> DynChildRepr {
|
||||||
|
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||||
|
let closing = component.closing.node.clone();
|
||||||
|
|
||||||
let component = DynChildRepr::new_with_id(id);
|
let child = component.child.clone();
|
||||||
|
|
||||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
#[cfg(all(debug_assertions, target_arch = "wasm32", feature = "web"))]
|
||||||
let closing = component.closing.node.clone();
|
let span = tracing::Span::current();
|
||||||
|
|
||||||
let child = component.child.clone();
|
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||||
|
create_effect(
|
||||||
|
cx,
|
||||||
|
move |prev_run: Option<(Option<web_sys::Node>, ScopeDisposer)>| {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
let _guard = span.enter();
|
||||||
|
|
||||||
#[cfg(all(debug_assertions, target_arch = "wasm32", feature = "web"))]
|
let (new_child, disposer) =
|
||||||
let span = tracing::Span::current();
|
cx.run_child_scope(|cx| child_fn().into_view(cx));
|
||||||
|
|
||||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
let mut child_borrow = child.borrow_mut();
|
||||||
create_effect(
|
|
||||||
cx,
|
|
||||||
move |prev_run: Option<(Option<web_sys::Node>, ScopeDisposer)>| {
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
let _guard = span.enter();
|
|
||||||
|
|
||||||
let (new_child, disposer) =
|
// Is this at least the second time we are loading a child?
|
||||||
cx.run_child_scope(|cx| child_fn().into_view(cx));
|
if let Some((prev_t, prev_disposer)) = prev_run {
|
||||||
|
let child = child_borrow.take().unwrap();
|
||||||
|
|
||||||
let mut child_borrow = child.borrow_mut();
|
// Dispose of the scope
|
||||||
|
prev_disposer.dispose();
|
||||||
|
|
||||||
// Is this at least the second time we are loading a child?
|
// We need to know if our child wasn't moved elsewhere.
|
||||||
if let Some((prev_t, prev_disposer)) = prev_run {
|
// If it was, `DynChild` no longer "owns" that child, and
|
||||||
let child = child_borrow.take().unwrap();
|
// is therefore no longer sound to unmount it from the DOM
|
||||||
|
// or to reuse it in the case of a text node
|
||||||
|
|
||||||
// Dispose of the scope
|
// TODO check does this still detect moves correctly?
|
||||||
prev_disposer.dispose();
|
let was_child_moved = prev_t.is_none()
|
||||||
|
&& child.get_closing_node().next_sibling().as_ref()
|
||||||
|
!= Some(&closing);
|
||||||
|
|
||||||
// We need to know if our child wasn't moved elsewhere.
|
// If the previous child was a text node, we would like to
|
||||||
// If it was, `DynChild` no longer "owns" that child, and
|
// make use of it again if our current child is also a text
|
||||||
// is therefore no longer sound to unmount it from the DOM
|
// node
|
||||||
// or to reuse it in the case of a text node
|
let ret = if let Some(prev_t) = prev_t {
|
||||||
|
// Here, our child is also a text node
|
||||||
|
if let Some(new_t) = new_child.get_text() {
|
||||||
|
if !was_child_moved && child != new_child {
|
||||||
|
prev_t
|
||||||
|
.unchecked_ref::<web_sys::Text>()
|
||||||
|
.set_data(&new_t.content);
|
||||||
|
|
||||||
// TODO check does this still detect moves correctly?
|
**child_borrow = Some(new_child);
|
||||||
let was_child_moved = prev_t.is_none()
|
|
||||||
&& child.get_closing_node().next_sibling().as_ref()
|
|
||||||
!= Some(&closing);
|
|
||||||
|
|
||||||
// If the previous child was a text node, we would like to
|
(Some(prev_t), disposer)
|
||||||
// make use of it again if our current child is also a text
|
} else {
|
||||||
// node
|
mount_child(MountKind::Before(&closing), &new_child);
|
||||||
let ret = if let Some(prev_t) = prev_t {
|
|
||||||
// Here, our child is also a text node
|
**child_borrow = Some(new_child.clone());
|
||||||
if let Some(new_t) = new_child.get_text() {
|
|
||||||
if !was_child_moved && child != new_child {
|
(Some(new_t.node.clone()), disposer)
|
||||||
prev_t
|
}
|
||||||
.unchecked_ref::<web_sys::Text>()
|
}
|
||||||
.set_data(&new_t.content);
|
// Child is not a text node, so we can remove the previous
|
||||||
|
// text node
|
||||||
|
else {
|
||||||
|
if !was_child_moved && child != new_child {
|
||||||
|
// Remove the text
|
||||||
|
closing
|
||||||
|
.previous_sibling()
|
||||||
|
.unwrap()
|
||||||
|
.unchecked_into::<web_sys::Element>()
|
||||||
|
.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mount the new child, and we're done
|
||||||
|
mount_child(MountKind::Before(&closing), &new_child);
|
||||||
|
|
||||||
**child_borrow = Some(new_child);
|
**child_borrow = Some(new_child);
|
||||||
|
|
||||||
(Some(prev_t), disposer)
|
(None, disposer)
|
||||||
} else {
|
|
||||||
mount_child(MountKind::Before(&closing), &new_child);
|
|
||||||
|
|
||||||
**child_borrow = Some(new_child.clone());
|
|
||||||
|
|
||||||
(Some(new_t.node.clone()), disposer)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Child is not a text node, so we can remove the previous
|
// Otherwise, the new child can still be a text node,
|
||||||
// text node
|
// but we know the previous child was not, so no special
|
||||||
|
// treatment here
|
||||||
else {
|
else {
|
||||||
if !was_child_moved && child != new_child {
|
// Technically, I think this check shouldn't be necessary, but
|
||||||
// Remove the text
|
// I can imagine some edge case that the child changes while
|
||||||
closing
|
// hydration is ongoing
|
||||||
.previous_sibling()
|
if !HydrationCtx::is_hydrating() {
|
||||||
|
if !was_child_moved && child != new_child {
|
||||||
|
// Remove the child
|
||||||
|
let start = child.get_opening_node();
|
||||||
|
let end = &closing;
|
||||||
|
|
||||||
|
unmount_child(&start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mount the new child
|
||||||
|
mount_child(MountKind::Before(&closing), &new_child);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We want to reuse text nodes, so hold onto it if
|
||||||
|
// our child is one
|
||||||
|
let t = new_child.get_text().map(|t| t.node.clone());
|
||||||
|
|
||||||
|
**child_borrow = Some(new_child);
|
||||||
|
|
||||||
|
(t, disposer)
|
||||||
|
};
|
||||||
|
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
// Otherwise, we know for sure this is our first time
|
||||||
|
else {
|
||||||
|
// We need to remove the text created from SSR
|
||||||
|
if HydrationCtx::is_hydrating() && new_child.get_text().is_some() {
|
||||||
|
let t = closing
|
||||||
|
.previous_sibling()
|
||||||
|
.unwrap()
|
||||||
|
.unchecked_into::<web_sys::Element>();
|
||||||
|
|
||||||
|
// See note on ssr.rs when matching on `DynChild`
|
||||||
|
// for more details on why we need to do this for
|
||||||
|
// release
|
||||||
|
if !cfg!(debug_assertions) {
|
||||||
|
t.previous_sibling()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unchecked_into::<web_sys::Element>()
|
.unchecked_into::<web_sys::Element>()
|
||||||
.remove();
|
.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mount the new child, and we're done
|
t.remove();
|
||||||
|
|
||||||
mount_child(MountKind::Before(&closing), &new_child);
|
mount_child(MountKind::Before(&closing), &new_child);
|
||||||
|
|
||||||
**child_borrow = Some(new_child);
|
|
||||||
|
|
||||||
(None, disposer)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// Otherwise, the new child can still be a text node,
|
// If we are not hydrating, we simply mount the child
|
||||||
// but we know the previous child was not, so no special
|
|
||||||
// treatment here
|
|
||||||
else {
|
|
||||||
// Technically, I think this check shouldn't be necessary, but
|
|
||||||
// I can imagine some edge case that the child changes while
|
|
||||||
// hydration is ongoing
|
|
||||||
if !HydrationCtx::is_hydrating() {
|
if !HydrationCtx::is_hydrating() {
|
||||||
if !was_child_moved && child != new_child {
|
|
||||||
// Remove the child
|
|
||||||
let start = child.get_opening_node();
|
|
||||||
let end = &closing;
|
|
||||||
|
|
||||||
unmount_child(&start, end);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mount the new child
|
|
||||||
mount_child(MountKind::Before(&closing), &new_child);
|
mount_child(MountKind::Before(&closing), &new_child);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We want to reuse text nodes, so hold onto it if
|
// We want to update text nodes, rather than replace them, so
|
||||||
// our child is one
|
// make sure to hold onto the text node
|
||||||
let t = new_child.get_text().map(|t| t.node.clone());
|
let t = new_child.get_text().map(|t| t.node.clone());
|
||||||
|
|
||||||
**child_borrow = Some(new_child);
|
**child_borrow = Some(new_child);
|
||||||
|
|
||||||
(t, disposer)
|
(t, disposer)
|
||||||
};
|
|
||||||
|
|
||||||
ret
|
|
||||||
}
|
|
||||||
// Otherwise, we know for sure this is our first time
|
|
||||||
else {
|
|
||||||
// We need to remove the text created from SSR
|
|
||||||
if HydrationCtx::is_hydrating() && new_child.get_text().is_some() {
|
|
||||||
let t = closing
|
|
||||||
.previous_sibling()
|
|
||||||
.unwrap()
|
|
||||||
.unchecked_into::<web_sys::Element>();
|
|
||||||
|
|
||||||
// See note on ssr.rs when matching on `DynChild`
|
|
||||||
// for more details on why we need to do this for
|
|
||||||
// release
|
|
||||||
if !cfg!(debug_assertions) {
|
|
||||||
t.previous_sibling()
|
|
||||||
.unwrap()
|
|
||||||
.unchecked_into::<web_sys::Element>()
|
|
||||||
.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
t.remove();
|
|
||||||
|
|
||||||
mount_child(MountKind::Before(&closing), &new_child);
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// If we are not hydrating, we simply mount the child
|
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||||
if !HydrationCtx::is_hydrating() {
|
{
|
||||||
mount_child(MountKind::Before(&closing), &new_child);
|
let new_child = child_fn().into_view(cx);
|
||||||
}
|
|
||||||
|
|
||||||
// We want to update text nodes, rather than replace them, so
|
**child.borrow_mut() = Some(new_child);
|
||||||
// make sure to hold onto the text node
|
}
|
||||||
let t = new_child.get_text().map(|t| t.node.clone());
|
|
||||||
|
|
||||||
**child_borrow = Some(new_child);
|
component
|
||||||
|
|
||||||
(t, disposer)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
|
||||||
{
|
|
||||||
let new_child = child_fn().into_view(cx);
|
|
||||||
|
|
||||||
**child.borrow_mut() = Some(new_child);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// monomorphized outer function
|
||||||
|
let Self { id, child_fn } = self;
|
||||||
|
|
||||||
|
let component = DynChildRepr::new_with_id(id);
|
||||||
|
let component = create_dyn_view(
|
||||||
|
cx,
|
||||||
|
component,
|
||||||
|
Box::new(move || child_fn().into_view(cx)),
|
||||||
|
);
|
||||||
|
|
||||||
View::CoreComponent(crate::CoreComponent::DynChild(component))
|
View::CoreComponent(crate::CoreComponent::DynChild(component))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
|
|
|
@ -16,7 +16,8 @@ thread_local! {
|
||||||
pub fn add_event_listener<E>(
|
pub fn add_event_listener<E>(
|
||||||
target: &web_sys::Element,
|
target: &web_sys::Element,
|
||||||
event_name: Cow<'static, str>,
|
event_name: Cow<'static, str>,
|
||||||
mut cb: impl FnMut(E) + 'static,
|
#[cfg(debug_assertions)] mut cb: impl FnMut(E) + 'static,
|
||||||
|
#[cfg(not(debug_assertions))] cb: impl FnMut(E) + 'static,
|
||||||
) where
|
) where
|
||||||
E: FromWasmAbi + 'static,
|
E: FromWasmAbi + 'static,
|
||||||
{
|
{
|
||||||
|
|
|
@ -200,7 +200,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EffectId {
|
impl EffectId {
|
||||||
pub(crate) fn run<T>(&self, runtime_id: RuntimeId) {
|
pub(crate) fn run(&self, runtime_id: RuntimeId) {
|
||||||
_ = with_runtime(runtime_id, |runtime| {
|
_ = with_runtime(runtime_id, |runtime| {
|
||||||
let effect = {
|
let effect = {
|
||||||
let effects = runtime.effects.borrow();
|
let effects = runtime.effects.borrow();
|
||||||
|
|
|
@ -130,7 +130,8 @@ where
|
||||||
});
|
});
|
||||||
|
|
||||||
let id = with_runtime(cx.runtime, |runtime| {
|
let id = with_runtime(cx.runtime, |runtime| {
|
||||||
runtime.create_serializable_resource(Rc::clone(&r))
|
let r = Rc::clone(&r) as Rc<dyn SerializableResource>;
|
||||||
|
runtime.create_serializable_resource(r)
|
||||||
})
|
})
|
||||||
.expect("tried to create a Resource in a Runtime that has been disposed.");
|
.expect("tried to create a Resource in a Runtime that has been disposed.");
|
||||||
|
|
||||||
|
@ -250,7 +251,8 @@ where
|
||||||
});
|
});
|
||||||
|
|
||||||
let id = with_runtime(cx.runtime, |runtime| {
|
let id = with_runtime(cx.runtime, |runtime| {
|
||||||
runtime.create_unserializable_resource(Rc::clone(&r))
|
let r = Rc::clone(&r) as Rc<dyn UnserializableResource>;
|
||||||
|
runtime.create_unserializable_resource(r)
|
||||||
})
|
})
|
||||||
.expect("tried to create a Resource in a runtime that has been disposed.");
|
.expect("tried to create a Resource in a runtime that has been disposed.");
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
use crate::{
|
use crate::{
|
||||||
hydration::SharedContext, serialization::Serializable, AnyEffect, AnyResource, Effect,
|
hydration::SharedContext, AnyEffect, AnyResource, Effect, EffectId, Memo, ReadSignal,
|
||||||
EffectId, Memo, ReadSignal, ResourceId, ResourceState, RwSignal, Scope, ScopeDisposer, ScopeId,
|
ResourceId, ResourceState, RwSignal, Scope, ScopeDisposer, ScopeId, ScopeProperty,
|
||||||
ScopeProperty, SignalId, WriteSignal,
|
SerializableResource, SignalId, UnserializableResource, WriteSignal,
|
||||||
};
|
};
|
||||||
use cfg_if::cfg_if;
|
use cfg_if::cfg_if;
|
||||||
use futures::stream::FuturesUnordered;
|
use futures::stream::FuturesUnordered;
|
||||||
|
@ -115,18 +115,19 @@ impl RuntimeId {
|
||||||
ret
|
ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
pub(crate) fn create_concrete_signal(self, value: Rc<RefCell<dyn Any>>) -> SignalId {
|
||||||
|
with_runtime(self, |runtime| runtime.signals.borrow_mut().insert(value))
|
||||||
|
.expect("tried to create a signal in a runtime that has been disposed")
|
||||||
|
}
|
||||||
|
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub(crate) fn create_signal<T>(self, value: T) -> (ReadSignal<T>, WriteSignal<T>)
|
pub(crate) fn create_signal<T>(self, value: T) -> (ReadSignal<T>, WriteSignal<T>)
|
||||||
where
|
where
|
||||||
T: Any + 'static,
|
T: Any + 'static,
|
||||||
{
|
{
|
||||||
let id = with_runtime(self, |runtime| {
|
let id = self.create_concrete_signal(Rc::new(RefCell::new(value)) as Rc<RefCell<dyn Any>>);
|
||||||
runtime
|
|
||||||
.signals
|
|
||||||
.borrow_mut()
|
|
||||||
.insert(Rc::new(RefCell::new(value)))
|
|
||||||
})
|
|
||||||
.expect("tried to create a signal in a runtime that has been disposed");
|
|
||||||
(
|
(
|
||||||
ReadSignal {
|
ReadSignal {
|
||||||
runtime: self,
|
runtime: self,
|
||||||
|
@ -149,13 +150,7 @@ impl RuntimeId {
|
||||||
where
|
where
|
||||||
T: Any + 'static,
|
T: Any + 'static,
|
||||||
{
|
{
|
||||||
let id = with_runtime(self, |runtime| {
|
let id = self.create_concrete_signal(Rc::new(RefCell::new(value)) as Rc<RefCell<dyn Any>>);
|
||||||
runtime
|
|
||||||
.signals
|
|
||||||
.borrow_mut()
|
|
||||||
.insert(Rc::new(RefCell::new(value)))
|
|
||||||
})
|
|
||||||
.expect("tried to create a signal in a runtime that has been disposed");
|
|
||||||
RwSignal {
|
RwSignal {
|
||||||
runtime: self,
|
runtime: self,
|
||||||
id,
|
id,
|
||||||
|
@ -165,6 +160,12 @@ impl RuntimeId {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
pub(crate) fn create_concrete_effect(self, effect: Rc<dyn AnyEffect>) -> EffectId {
|
||||||
|
with_runtime(self, |runtime| runtime.effects.borrow_mut().insert(effect))
|
||||||
|
.expect("tried to create an effect in a runtime that has been disposed")
|
||||||
|
}
|
||||||
|
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub(crate) fn create_effect<T>(self, f: impl Fn(Option<T>) -> T + 'static) -> EffectId
|
pub(crate) fn create_effect<T>(self, f: impl Fn(Option<T>) -> T + 'static) -> EffectId
|
||||||
where
|
where
|
||||||
|
@ -173,18 +174,16 @@ impl RuntimeId {
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
let defined_at = std::panic::Location::caller();
|
let defined_at = std::panic::Location::caller();
|
||||||
|
|
||||||
with_runtime(self, |runtime| {
|
let effect = Effect {
|
||||||
let effect = Effect {
|
f,
|
||||||
f,
|
value: RefCell::new(None),
|
||||||
value: RefCell::new(None),
|
#[cfg(debug_assertions)]
|
||||||
#[cfg(debug_assertions)]
|
defined_at,
|
||||||
defined_at,
|
};
|
||||||
};
|
|
||||||
let id = { runtime.effects.borrow_mut().insert(Rc::new(effect)) };
|
let id = self.create_concrete_effect(Rc::new(effect));
|
||||||
id.run::<T>(self);
|
id.run(self);
|
||||||
id
|
id
|
||||||
})
|
|
||||||
.expect("tried to create an effect in a runtime that has been disposed")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
|
@ -256,27 +255,19 @@ impl Runtime {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn create_unserializable_resource<S, T>(
|
pub(crate) fn create_unserializable_resource(
|
||||||
&self,
|
&self,
|
||||||
state: Rc<ResourceState<S, T>>,
|
state: Rc<dyn UnserializableResource>,
|
||||||
) -> ResourceId
|
) -> ResourceId {
|
||||||
where
|
|
||||||
S: Clone + 'static,
|
|
||||||
T: 'static,
|
|
||||||
{
|
|
||||||
self.resources
|
self.resources
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
.insert(AnyResource::Unserializable(state))
|
.insert(AnyResource::Unserializable(state))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn create_serializable_resource<S, T>(
|
pub(crate) fn create_serializable_resource(
|
||||||
&self,
|
&self,
|
||||||
state: Rc<ResourceState<S, T>>,
|
state: Rc<dyn SerializableResource>,
|
||||||
) -> ResourceId
|
) -> ResourceId {
|
||||||
where
|
|
||||||
S: Clone + 'static,
|
|
||||||
T: Serializable + 'static,
|
|
||||||
{
|
|
||||||
self.resources
|
self.resources
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
.insert(AnyResource::Serializable(state))
|
.insert(AnyResource::Serializable(state))
|
||||||
|
|
|
@ -43,86 +43,109 @@ pub fn Form<A>(
|
||||||
where
|
where
|
||||||
A: ToHref + 'static,
|
A: ToHref + 'static,
|
||||||
{
|
{
|
||||||
let action_version = version;
|
fn inner(
|
||||||
let action = use_resolved_path(cx, move || action.to_href()());
|
cx: Scope,
|
||||||
|
method: Option<&'static str>,
|
||||||
|
action: Memo<Option<String>>,
|
||||||
|
enctype: Option<String>,
|
||||||
|
version: Option<RwSignal<usize>>,
|
||||||
|
error: Option<RwSignal<Option<Box<dyn Error>>>>,
|
||||||
|
#[allow(clippy::type_complexity)] on_form_data: Option<Rc<dyn Fn(&web_sys::FormData)>>,
|
||||||
|
#[allow(clippy::type_complexity)] on_response: Option<Rc<dyn Fn(&web_sys::Response)>>,
|
||||||
|
children: Children,
|
||||||
|
) -> HtmlElement<Form> {
|
||||||
|
let action_version = version;
|
||||||
|
let on_submit = move |ev: web_sys::SubmitEvent| {
|
||||||
|
if ev.default_prevented() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let navigate = use_navigate(cx);
|
||||||
|
|
||||||
let on_submit = move |ev: web_sys::SubmitEvent| {
|
let (form, method, action, enctype) = extract_form_attributes(&ev);
|
||||||
if ev.default_prevented() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let navigate = use_navigate(cx);
|
|
||||||
|
|
||||||
let (form, method, action, enctype) = extract_form_attributes(&ev);
|
let form_data = web_sys::FormData::new_with_form(&form).unwrap_throw();
|
||||||
|
if let Some(on_form_data) = on_form_data.clone() {
|
||||||
|
on_form_data(&form_data);
|
||||||
|
}
|
||||||
|
let params =
|
||||||
|
web_sys::UrlSearchParams::new_with_str_sequence_sequence(&form_data).unwrap_throw();
|
||||||
|
let action = use_resolved_path(cx, move || action.clone())
|
||||||
|
.get()
|
||||||
|
.unwrap_or_default();
|
||||||
|
// POST
|
||||||
|
if method == "post" {
|
||||||
|
ev.prevent_default();
|
||||||
|
|
||||||
let form_data = web_sys::FormData::new_with_form(&form).unwrap_throw();
|
let on_response = on_response.clone();
|
||||||
if let Some(on_form_data) = on_form_data.clone() {
|
spawn_local(async move {
|
||||||
on_form_data(&form_data);
|
let res = gloo_net::http::Request::post(&action)
|
||||||
}
|
.header("Accept", "application/json")
|
||||||
let params =
|
.header("Content-Type", &enctype)
|
||||||
web_sys::UrlSearchParams::new_with_str_sequence_sequence(&form_data).unwrap_throw();
|
.body(params)
|
||||||
let action = use_resolved_path(cx, move || action.clone())
|
.send()
|
||||||
.get()
|
.await;
|
||||||
.unwrap_or_default();
|
match res {
|
||||||
// POST
|
Err(e) => {
|
||||||
if method == "post" {
|
log::error!("<Form/> error while POSTing: {e:#?}");
|
||||||
ev.prevent_default();
|
if let Some(error) = error {
|
||||||
|
error.set(Some(Box::new(e)));
|
||||||
let on_response = on_response.clone();
|
}
|
||||||
spawn_local(async move {
|
|
||||||
let res = gloo_net::http::Request::post(&action)
|
|
||||||
.header("Accept", "application/json")
|
|
||||||
.header("Content-Type", &enctype)
|
|
||||||
.body(params)
|
|
||||||
.send()
|
|
||||||
.await;
|
|
||||||
match res {
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("<Form/> error while POSTing: {e:#?}");
|
|
||||||
if let Some(error) = error {
|
|
||||||
error.set(Some(Box::new(e)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(resp) => {
|
|
||||||
if let Some(version) = action_version {
|
|
||||||
version.update(|n| *n += 1);
|
|
||||||
}
|
|
||||||
if let Some(error) = error {
|
|
||||||
error.set(None);
|
|
||||||
}
|
|
||||||
if let Some(on_response) = on_response.clone() {
|
|
||||||
on_response(resp.as_raw());
|
|
||||||
}
|
}
|
||||||
|
Ok(resp) => {
|
||||||
|
if let Some(version) = action_version {
|
||||||
|
version.update(|n| *n += 1);
|
||||||
|
}
|
||||||
|
if let Some(error) = error {
|
||||||
|
error.set(None);
|
||||||
|
}
|
||||||
|
if let Some(on_response) = on_response.clone() {
|
||||||
|
on_response(resp.as_raw());
|
||||||
|
}
|
||||||
|
|
||||||
if resp.status() == 303 {
|
if resp.status() == 303 {
|
||||||
if let Some(redirect_url) = resp.headers().get("Location") {
|
if let Some(redirect_url) = resp.headers().get("Location") {
|
||||||
_ = navigate(&redirect_url, Default::default());
|
_ = navigate(&redirect_url, Default::default());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
// otherwise, GET
|
|
||||||
else {
|
|
||||||
let params = params.to_string().as_string().unwrap_or_default();
|
|
||||||
if navigate(&format!("{action}?{params}"), Default::default()).is_ok() {
|
|
||||||
ev.prevent_default();
|
|
||||||
}
|
}
|
||||||
|
// otherwise, GET
|
||||||
|
else {
|
||||||
|
let params = params.to_string().as_string().unwrap_or_default();
|
||||||
|
if navigate(&format!("{action}?{params}"), Default::default()).is_ok() {
|
||||||
|
ev.prevent_default();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let method = method.unwrap_or("get");
|
||||||
|
|
||||||
|
view! { cx,
|
||||||
|
<form
|
||||||
|
method=method
|
||||||
|
action=move || action.get()
|
||||||
|
enctype=enctype
|
||||||
|
on:submit=on_submit
|
||||||
|
>
|
||||||
|
{children(cx)}
|
||||||
|
</form>
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
let method = method.unwrap_or("get");
|
|
||||||
|
|
||||||
view! { cx,
|
|
||||||
<form
|
|
||||||
method=method
|
|
||||||
action=move || action.get()
|
|
||||||
enctype=enctype
|
|
||||||
on:submit=on_submit
|
|
||||||
>
|
|
||||||
{children(cx)}
|
|
||||||
</form>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let action = use_resolved_path(cx, move || action.to_href()());
|
||||||
|
inner(
|
||||||
|
cx,
|
||||||
|
method,
|
||||||
|
action,
|
||||||
|
enctype,
|
||||||
|
version,
|
||||||
|
error,
|
||||||
|
on_form_data,
|
||||||
|
on_response,
|
||||||
|
children,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Automatically turns a server [Action](leptos_server::Action) into an HTML
|
/// Automatically turns a server [Action](leptos_server::Action) into an HTML
|
||||||
|
|
|
@ -70,35 +70,47 @@ pub fn A<H>(
|
||||||
where
|
where
|
||||||
H: ToHref + 'static,
|
H: ToHref + 'static,
|
||||||
{
|
{
|
||||||
let location = use_location(cx);
|
fn inner(
|
||||||
let href = use_resolved_path(cx, move || href.to_href()());
|
cx: Scope,
|
||||||
let is_active = create_memo(cx, move |_| match href.get() {
|
href: Memo<Option<String>>,
|
||||||
None => false,
|
exact: bool,
|
||||||
|
state: Option<State>,
|
||||||
|
replace: bool,
|
||||||
|
class: Option<MaybeSignal<String>>,
|
||||||
|
children: Children,
|
||||||
|
) -> HtmlElement<A> {
|
||||||
|
let location = use_location(cx);
|
||||||
|
let is_active = create_memo(cx, move |_| match href.get() {
|
||||||
|
None => false,
|
||||||
|
|
||||||
Some(to) => {
|
Some(to) => {
|
||||||
let path = to
|
let path = to
|
||||||
.split(['?', '#'])
|
.split(['?', '#'])
|
||||||
.next()
|
.next()
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.to_lowercase();
|
.to_lowercase();
|
||||||
let loc = location.pathname.get().to_lowercase();
|
let loc = location.pathname.get().to_lowercase();
|
||||||
if exact {
|
if exact {
|
||||||
loc == path
|
loc == path
|
||||||
} else {
|
} else {
|
||||||
loc.starts_with(&path)
|
loc.starts_with(&path)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
|
||||||
view! { cx,
|
view! { cx,
|
||||||
<a
|
<a
|
||||||
href=move || href.get().unwrap_or_default()
|
href=move || href.get().unwrap_or_default()
|
||||||
prop:state={state.map(|s| s.to_js_value())}
|
prop:state={state.map(|s| s.to_js_value())}
|
||||||
prop:replace={replace}
|
prop:replace={replace}
|
||||||
aria-current=move || if is_active.get() { Some("page") } else { None }
|
aria-current=move || if is_active.get() { Some("page") } else { None }
|
||||||
class=move || class.as_ref().map(|class| class.get())
|
class=move || class.as_ref().map(|class| class.get())
|
||||||
>
|
>
|
||||||
{children(cx)}
|
{children(cx)}
|
||||||
</a>
|
</a>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let href = use_resolved_path(cx, move || href.to_href()());
|
||||||
|
inner(cx, href, exact, state, replace, class, children)
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,31 +36,47 @@ where
|
||||||
F: Fn(Scope) -> E + 'static,
|
F: Fn(Scope) -> E + 'static,
|
||||||
P: std::fmt::Display,
|
P: std::fmt::Display,
|
||||||
{
|
{
|
||||||
let children = children
|
fn inner(
|
||||||
.map(|children| {
|
cx: Scope,
|
||||||
children(cx)
|
children: Option<Children>,
|
||||||
.as_children()
|
path: String,
|
||||||
.iter()
|
view: Rc<dyn Fn(Scope) -> View>,
|
||||||
.filter_map(|child| {
|
) -> RouteDefinition {
|
||||||
child
|
let children = children
|
||||||
.as_transparent()
|
.map(|children| {
|
||||||
.and_then(|t| t.downcast_ref::<RouteDefinition>())
|
children(cx)
|
||||||
})
|
.as_children()
|
||||||
.cloned()
|
.iter()
|
||||||
.collect::<Vec<_>>()
|
.filter_map(|child| {
|
||||||
})
|
child
|
||||||
.unwrap_or_default();
|
.as_transparent()
|
||||||
let id = ROUTE_ID.with(|id| {
|
.and_then(|t| t.downcast_ref::<RouteDefinition>())
|
||||||
let next = id.get() + 1;
|
})
|
||||||
id.set(next);
|
.cloned()
|
||||||
next
|
.collect::<Vec<_>>()
|
||||||
});
|
})
|
||||||
RouteDefinition {
|
.unwrap_or_default();
|
||||||
id,
|
|
||||||
path: path.to_string(),
|
let id = ROUTE_ID.with(|id| {
|
||||||
children,
|
let next = id.get() + 1;
|
||||||
view: Rc::new(move |cx| view(cx).into_view(cx)),
|
id.set(next);
|
||||||
|
next
|
||||||
|
});
|
||||||
|
|
||||||
|
RouteDefinition {
|
||||||
|
id,
|
||||||
|
path,
|
||||||
|
children,
|
||||||
|
view,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inner(
|
||||||
|
cx,
|
||||||
|
children,
|
||||||
|
path.to_string(),
|
||||||
|
Rc::new(move |cx| view(cx).into_view(cx)),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoView for RouteDefinition {
|
impl IntoView for RouteDefinition {
|
||||||
|
|
|
@ -8,10 +8,6 @@
|
||||||
//! apps (SPAs), server-side rendering/multi-page apps (MPAs), or to synchronize
|
//! apps (SPAs), server-side rendering/multi-page apps (MPAs), or to synchronize
|
||||||
//! state between the two.
|
//! state between the two.
|
||||||
//!
|
//!
|
||||||
//! **Note:** This is a work in progress. The feature to pass client-side route [State] in
|
|
||||||
//! [History.state](https://developer.mozilla.org/en-US/docs/Web/API/History/state), in particular,
|
|
||||||
//! is incomplete.
|
|
||||||
//!
|
|
||||||
//! ## Philosophy
|
//! ## Philosophy
|
||||||
//!
|
//!
|
||||||
//! Leptos Router is built on a few simple principles:
|
//! Leptos Router is built on a few simple principles:
|
||||||
|
@ -23,12 +19,7 @@
|
||||||
//! and are rendered by different components. This means you can navigate between siblings
|
//! and are rendered by different components. This means you can navigate between siblings
|
||||||
//! in this tree without re-rendering or triggering any change in the parent routes.
|
//! in this tree without re-rendering or triggering any change in the parent routes.
|
||||||
//!
|
//!
|
||||||
//! 3. **Route-based data loading.** Each route should know exactly which data it needs
|
//! 3. **Progressive enhancement.** The [A] and [Form] components resolve any relative
|
||||||
//! to render itself when the route is defined. This allows each route’s data to be
|
|
||||||
//! reloaded independently, and allows data from nested routes to be loaded in parallel,
|
|
||||||
//! avoiding waterfalls.
|
|
||||||
//!
|
|
||||||
//! 4. **Progressive enhancement.** The [A] and [Form] components resolve any relative
|
|
||||||
//! nested routes, render actual `<a>` and `<form>` elements, and (when possible)
|
//! nested routes, render actual `<a>` and `<form>` elements, and (when possible)
|
||||||
//! upgrading them to handle those navigations with client-side routing. If you’re using
|
//! upgrading them to handle those navigations with client-side routing. If you’re using
|
||||||
//! them with server-side rendering (with or without hydration), they just work,
|
//! them with server-side rendering (with or without hydration), they just work,
|
||||||
|
|
Loading…
Reference in a new issue