mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 16:54:41 -05:00
Compare commits
13 Commits
update-par
...
fix-node-r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55194c6c2e | ||
|
|
5d612d9740 | ||
|
|
eacff684ef | ||
|
|
4034aa9c11 | ||
|
|
45275ff8d4 | ||
|
|
3ff5089bf4 | ||
|
|
c28297fe93 | ||
|
|
6d0d70cd17 | ||
|
|
c4e693e01e | ||
|
|
2be4e8d959 | ||
|
|
fec4ff4381 | ||
|
|
25c313aeb5 | ||
|
|
0dbcc323ba |
@@ -1,4 +1,4 @@
|
||||
use leptos::{component, Scope, IntoView, view};
|
||||
use leptos::{component, view, IntoView, Scope};
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
@@ -6,7 +6,7 @@ pub fn Nav(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
<header class="header">
|
||||
<nav class="inner">
|
||||
<A href="/">
|
||||
<A href="/home">
|
||||
<strong>"HN"</strong>
|
||||
</A>
|
||||
<A href="/new">
|
||||
|
||||
@@ -20,6 +20,7 @@ pub fn RouterExample(cx: Scope) -> impl IntoView {
|
||||
<A exact=true href="/">"Contacts"</A>
|
||||
<A href="about">"About"</A>
|
||||
<A href="settings">"Settings"</A>
|
||||
<A href="redirect-home">"Redirect to Home"</A>
|
||||
</nav>
|
||||
<main>
|
||||
<Routes>
|
||||
@@ -44,6 +45,10 @@ pub fn RouterExample(cx: Scope) -> impl IntoView {
|
||||
path="settings"
|
||||
view=move |cx| view! { cx, <Settings/> }
|
||||
/>
|
||||
<Route
|
||||
path="redirect-home"
|
||||
view=move |cx| view! { cx, <Redirect path="/"/> }
|
||||
/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
|
||||
@@ -438,6 +438,7 @@ fn provide_contexts(cx: leptos::Scope, req: &HttpRequest, res_options: ResponseO
|
||||
provide_context(cx, MetaContext::new());
|
||||
provide_context(cx, res_options);
|
||||
provide_context(cx, req.clone());
|
||||
provide_server_redirect(cx, move |path| redirect(cx, path));
|
||||
}
|
||||
|
||||
fn leptos_corrected_path(req: &HttpRequest) -> String {
|
||||
|
||||
@@ -447,6 +447,7 @@ where
|
||||
provide_context(cx, MetaContext::new());
|
||||
provide_context(cx, req_parts);
|
||||
provide_context(cx, default_res_options);
|
||||
provide_server_redirect(cx, move |path| redirect(cx, path));
|
||||
app_fn(cx).into_view(cx)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -172,3 +172,7 @@ pub type Children = Box<dyn FnOnce(Scope) -> Fragment>;
|
||||
/// A type for the `children` property on components that can be called
|
||||
/// more than once.
|
||||
pub type ChildrenFn = Box<dyn Fn(Scope) -> Fragment>;
|
||||
|
||||
/// A type for the `children` property on components that can be called
|
||||
/// more than once, but may mutate the children.
|
||||
pub type ChildrenFnMut = Box<dyn FnMut(Scope) -> Fragment>;
|
||||
|
||||
@@ -6,6 +6,11 @@ use leptos_reactive::{create_memo, Scope};
|
||||
/// and show the fallback when it is `false`, without rerendering every time
|
||||
/// the condition changes.
|
||||
///
|
||||
/// *Note*: Because of the nature of generic arguments, it’s not really possible
|
||||
/// to make the `fallback` optional. If you want an empty fallback state—in other
|
||||
/// words, if you want to show the children if `when` is true and noting otherwise—use
|
||||
/// `fallback=|_| ()` (i.e., a fallback function that returns the unit type `()`).
|
||||
///
|
||||
/// ```rust
|
||||
/// # use leptos_reactive::*;
|
||||
/// # use leptos_macro::*;
|
||||
|
||||
@@ -5,7 +5,7 @@ edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Configuraiton for the Leptos web framework."
|
||||
description = "Configuration for the Leptos web framework."
|
||||
readme = "../README.md"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -93,8 +93,7 @@ fn from_str(input: &str) -> Result<Env, String> {
|
||||
"dev" | "development" => Ok(Env::DEV),
|
||||
"prod" | "production" => Ok(Env::PROD),
|
||||
_ => Err(format!(
|
||||
"{} is not a supported environment. Use either `dev` or `production`.",
|
||||
input
|
||||
"{input} is not a supported environment. Use either `dev` or `production`.",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,167 +154,182 @@ where
|
||||
instrument(level = "trace", name = "<DynChild />", skip_all)
|
||||
)]
|
||||
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"))]
|
||||
let closing = component.closing.node.clone();
|
||||
#[cfg(all(debug_assertions, target_arch = "wasm32", feature = "web"))]
|
||||
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 span = tracing::Span::current();
|
||||
let (new_child, disposer) =
|
||||
cx.run_child_scope(|cx| child_fn().into_view(cx));
|
||||
|
||||
#[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();
|
||||
let mut child_borrow = child.borrow_mut();
|
||||
|
||||
let (new_child, disposer) =
|
||||
cx.run_child_scope(|cx| child_fn().into_view(cx));
|
||||
// Is this at least the second time we are loading a child?
|
||||
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?
|
||||
if let Some((prev_t, prev_disposer)) = prev_run {
|
||||
let child = child_borrow.take().unwrap();
|
||||
// We need to know if our child wasn't moved elsewhere.
|
||||
// If it was, `DynChild` no longer "owns" that child, and
|
||||
// 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
|
||||
prev_disposer.dispose();
|
||||
// TODO check does this still detect moves correctly?
|
||||
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 it was, `DynChild` no longer "owns" that child, and
|
||||
// is therefore no longer sound to unmount it from the DOM
|
||||
// or to reuse it in the case of a text node
|
||||
// If the previous child was a text node, we would like to
|
||||
// make use of it again if our current child is also 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?
|
||||
let was_child_moved = prev_t.is_none()
|
||||
&& child.get_closing_node().next_sibling().as_ref()
|
||||
!= Some(&closing);
|
||||
**child_borrow = Some(new_child);
|
||||
|
||||
// If the previous child was a text node, we would like to
|
||||
// make use of it again if our current child is also 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);
|
||||
(Some(prev_t), 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
|
||||
// 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);
|
||||
|
||||
(Some(prev_t), disposer)
|
||||
} else {
|
||||
mount_child(MountKind::Before(&closing), &new_child);
|
||||
|
||||
**child_borrow = Some(new_child.clone());
|
||||
|
||||
(Some(new_t.node.clone()), disposer)
|
||||
(None, disposer)
|
||||
}
|
||||
}
|
||||
// Child is not a text node, so we can remove the previous
|
||||
// text node
|
||||
// Otherwise, the new child can still be a text node,
|
||||
// but we know the previous child was not, so no special
|
||||
// treatment here
|
||||
else {
|
||||
if !was_child_moved && child != new_child {
|
||||
// Remove the text
|
||||
closing
|
||||
.previous_sibling()
|
||||
// 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 !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()
|
||||
.unchecked_into::<web_sys::Element>()
|
||||
.remove();
|
||||
}
|
||||
|
||||
// Mount the new child, and we're done
|
||||
t.remove();
|
||||
|
||||
mount_child(MountKind::Before(&closing), &new_child);
|
||||
|
||||
**child_borrow = Some(new_child);
|
||||
|
||||
(None, disposer)
|
||||
}
|
||||
}
|
||||
// Otherwise, the new child can still be a text node,
|
||||
// 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 we are not hydrating, we simply mount the child
|
||||
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
|
||||
// We want to update text nodes, rather than replace them, so
|
||||
// make sure to hold onto the text node
|
||||
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()
|
||||
.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
|
||||
if !HydrationCtx::is_hydrating() {
|
||||
mount_child(MountKind::Before(&closing), &new_child);
|
||||
}
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
{
|
||||
let new_child = child_fn().into_view(cx);
|
||||
|
||||
// We want to update text nodes, rather than replace them, so
|
||||
// make sure to hold onto the text node
|
||||
let t = new_child.get_text().map(|t| t.node.clone());
|
||||
**child.borrow_mut() = Some(new_child);
|
||||
}
|
||||
|
||||
**child_borrow = Some(new_child);
|
||||
|
||||
(t, disposer)
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
{
|
||||
let new_child = child_fn().into_view(cx);
|
||||
|
||||
**child.borrow_mut() = Some(new_child);
|
||||
component
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -16,7 +16,8 @@ thread_local! {
|
||||
pub fn add_event_listener<E>(
|
||||
target: &web_sys::Element,
|
||||
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
|
||||
E: FromWasmAbi + 'static,
|
||||
{
|
||||
|
||||
@@ -150,6 +150,7 @@ generate_event_types! {
|
||||
canplaythrough: Event,
|
||||
change: Event,
|
||||
click: MouseEvent,
|
||||
#[does_not_bubble]
|
||||
close: Event,
|
||||
compositionend: CompositionEvent,
|
||||
compositionstart: CompositionEvent,
|
||||
|
||||
@@ -4,6 +4,7 @@ use convert_case::{
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use proc_macro2::{Ident, TokenStream};
|
||||
use proc_macro_error::ResultExt;
|
||||
use quote::{format_ident, ToTokens, TokenStreamExt};
|
||||
use std::collections::HashSet;
|
||||
use syn::{
|
||||
@@ -410,7 +411,7 @@ impl PropOpt {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Meta::List(MetaList { nested, .. }) = attr.parse_meta().ok()? {
|
||||
if let Meta::List(MetaList { nested, .. }) = attr.parse_meta().unwrap_or_abort() {
|
||||
Some(
|
||||
nested
|
||||
.iter()
|
||||
|
||||
@@ -457,12 +457,14 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
/// ```
|
||||
///
|
||||
/// 5. You can access the children passed into the component with the `children` property, which takes
|
||||
/// an argument of the form `Box<dyn FnOnce(Scope) -> Fragment>`.
|
||||
/// an argument of the type `Children`. This is an alias for `Box<dyn FnOnce(Scope) -> Fragment>`.
|
||||
/// If you need `children` to be a `Fn` or `FnMut`, you can use the `ChildrenFn` or `ChildrenFnMut`
|
||||
/// type aliases.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos::*;
|
||||
/// #[component]
|
||||
/// fn ComponentWithChildren(cx: Scope, children: Box<dyn FnOnce(Scope) -> Fragment>) -> impl IntoView {
|
||||
/// fn ComponentWithChildren(cx: Scope, children: Children) -> impl IntoView {
|
||||
/// view! {
|
||||
/// cx,
|
||||
/// <ul>
|
||||
|
||||
@@ -137,7 +137,7 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
|
||||
};
|
||||
|
||||
Ok(quote::quote! {
|
||||
#[derive(Clone, ::serde::Serialize, ::serde::Deserialize)]
|
||||
#[derive(Clone, Debug, ::serde::Serialize, ::serde::Deserialize)]
|
||||
pub struct #struct_name {
|
||||
#(#fields),*
|
||||
}
|
||||
|
||||
@@ -406,7 +406,7 @@ fn attribute_to_tokens_ssr(
|
||||
exprs_for_compiler: &mut Vec<TokenStream>,
|
||||
) {
|
||||
let name = node.key.to_string();
|
||||
if name == "ref" || name == "_ref" {
|
||||
if name == "ref" || name == "_ref" || name == "node_ref" {
|
||||
// ignore refs on SSR
|
||||
} else if name.strip_prefix("on:").is_some() {
|
||||
let (event_type, handler) = event_from_attribute_node(node);
|
||||
|
||||
@@ -200,7 +200,7 @@ where
|
||||
}
|
||||
|
||||
impl EffectId {
|
||||
pub(crate) fn run<T>(&self, runtime_id: RuntimeId) {
|
||||
pub(crate) fn run(&self, runtime_id: RuntimeId) {
|
||||
_ = with_runtime(runtime_id, |runtime| {
|
||||
let effect = {
|
||||
let effects = runtime.effects.borrow();
|
||||
|
||||
@@ -130,7 +130,8 @@ where
|
||||
});
|
||||
|
||||
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.");
|
||||
|
||||
@@ -250,7 +251,8 @@ where
|
||||
});
|
||||
|
||||
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.");
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#![forbid(unsafe_code)]
|
||||
use crate::{
|
||||
hydration::SharedContext, serialization::Serializable, AnyEffect, AnyResource, Effect,
|
||||
EffectId, Memo, ReadSignal, ResourceId, ResourceState, RwSignal, Scope, ScopeDisposer, ScopeId,
|
||||
ScopeProperty, SignalId, WriteSignal,
|
||||
hydration::SharedContext, AnyEffect, AnyResource, Effect, EffectId, Memo, ReadSignal,
|
||||
ResourceId, ResourceState, RwSignal, Scope, ScopeDisposer, ScopeId, ScopeProperty,
|
||||
SerializableResource, SignalId, UnserializableResource, WriteSignal,
|
||||
};
|
||||
use cfg_if::cfg_if;
|
||||
use futures::stream::FuturesUnordered;
|
||||
@@ -115,18 +115,19 @@ impl RuntimeId {
|
||||
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]
|
||||
pub(crate) fn create_signal<T>(self, value: T) -> (ReadSignal<T>, WriteSignal<T>)
|
||||
where
|
||||
T: Any + 'static,
|
||||
{
|
||||
let id = with_runtime(self, |runtime| {
|
||||
runtime
|
||||
.signals
|
||||
.borrow_mut()
|
||||
.insert(Rc::new(RefCell::new(value)))
|
||||
})
|
||||
.expect("tried to create a signal in a runtime that has been disposed");
|
||||
let id = self.create_concrete_signal(Rc::new(RefCell::new(value)) as Rc<RefCell<dyn Any>>);
|
||||
|
||||
(
|
||||
ReadSignal {
|
||||
runtime: self,
|
||||
@@ -149,13 +150,7 @@ impl RuntimeId {
|
||||
where
|
||||
T: Any + 'static,
|
||||
{
|
||||
let id = with_runtime(self, |runtime| {
|
||||
runtime
|
||||
.signals
|
||||
.borrow_mut()
|
||||
.insert(Rc::new(RefCell::new(value)))
|
||||
})
|
||||
.expect("tried to create a signal in a runtime that has been disposed");
|
||||
let id = self.create_concrete_signal(Rc::new(RefCell::new(value)) as Rc<RefCell<dyn Any>>);
|
||||
RwSignal {
|
||||
runtime: self,
|
||||
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]
|
||||
pub(crate) fn create_effect<T>(self, f: impl Fn(Option<T>) -> T + 'static) -> EffectId
|
||||
where
|
||||
@@ -173,18 +174,16 @@ impl RuntimeId {
|
||||
#[cfg(debug_assertions)]
|
||||
let defined_at = std::panic::Location::caller();
|
||||
|
||||
with_runtime(self, |runtime| {
|
||||
let effect = Effect {
|
||||
f,
|
||||
value: RefCell::new(None),
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at,
|
||||
};
|
||||
let id = { runtime.effects.borrow_mut().insert(Rc::new(effect)) };
|
||||
id.run::<T>(self);
|
||||
id
|
||||
})
|
||||
.expect("tried to create an effect in a runtime that has been disposed")
|
||||
let effect = Effect {
|
||||
f,
|
||||
value: RefCell::new(None),
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at,
|
||||
};
|
||||
|
||||
let id = self.create_concrete_effect(Rc::new(effect));
|
||||
id.run(self);
|
||||
id
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
@@ -256,27 +255,19 @@ impl Runtime {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub(crate) fn create_unserializable_resource<S, T>(
|
||||
pub(crate) fn create_unserializable_resource(
|
||||
&self,
|
||||
state: Rc<ResourceState<S, T>>,
|
||||
) -> ResourceId
|
||||
where
|
||||
S: Clone + 'static,
|
||||
T: 'static,
|
||||
{
|
||||
state: Rc<dyn UnserializableResource>,
|
||||
) -> ResourceId {
|
||||
self.resources
|
||||
.borrow_mut()
|
||||
.insert(AnyResource::Unserializable(state))
|
||||
}
|
||||
|
||||
pub(crate) fn create_serializable_resource<S, T>(
|
||||
pub(crate) fn create_serializable_resource(
|
||||
&self,
|
||||
state: Rc<ResourceState<S, T>>,
|
||||
) -> ResourceId
|
||||
where
|
||||
S: Clone + 'static,
|
||||
T: Serializable + 'static,
|
||||
{
|
||||
state: Rc<dyn SerializableResource>,
|
||||
) -> ResourceId {
|
||||
self.resources
|
||||
.borrow_mut()
|
||||
.insert(AnyResource::Serializable(state))
|
||||
|
||||
@@ -399,6 +399,12 @@ where
|
||||
Dynamic(Signal<T>),
|
||||
}
|
||||
|
||||
impl<T: Default> Default for MaybeSignal<T> {
|
||||
fn default() -> Self {
|
||||
Self::Static(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> UntrackedGettableSignal<T> for MaybeSignal<T>
|
||||
where
|
||||
T: 'static,
|
||||
|
||||
14
logos/Simple_Icon.svg
Normal file
14
logos/Simple_Icon.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
|
||||
<g>
|
||||
<path d="M10.1,17.8C7.6,17,5.9,14.6,5.9,12c0-3.4,2.7-6.1,6.1-6.1c0.3,0,0.5,0,0.8,0
|
||||
c0.4-0.9,0.9-1.7,1.5-2.5c-0.5-0.6-0.8-1.4-0.8-2.3c0-0.3,0-0.7,0.1-1c-0.5-0.1-1-0.1-1.5-0.1C5.4,0.1,0.1,5.4,0.1,12
|
||||
c0,4.3,2.3,8.1,5.8,10.2L6,22.1C7.9,21.2,9.3,19.7,10.1,17.8z"/>
|
||||
<path d="M16.9,3c0.7,0,1.3-0.4,1.6-0.9l0,0c-1-0.6-2.1-1.1-3.2-1.5l0,0c-0.1,0.2-0.1,0.4-0.1,0.6
|
||||
C15.1,2.2,15.9,3,16.9,3z"/>
|
||||
<path d="M19.9,3.1c-0.7,1-1.8,1.6-2.9,1.6c-0.3,0-0.5,0-0.8-0.1c-0.2,0-0.3-0.1-0.5-0.1
|
||||
c-0.5,0.6-0.9,1.2-1.2,1.9c2.2,1,3.7,3.1,3.7,5.6c0,3.4-2.7,6.1-6.1,6.1c0,0-0.1,0-0.1,0c-0.8,2.1-2.2,3.8-4.1,5
|
||||
c1.3,0.5,2.7,0.8,4.2,0.8c6.6,0,11.9-5.3,11.9-11.9C23.9,8.4,22.3,5.3,19.9,3.1z"/>
|
||||
<circle cx="12" cy="12" r="4.4"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 907 B |
@@ -22,6 +22,7 @@ percent-encoding = "2"
|
||||
thiserror = "1"
|
||||
serde_urlencoded = "0.7"
|
||||
serde = "1"
|
||||
tracing = "0.1"
|
||||
js-sys = { version = "0.3" }
|
||||
wasm-bindgen = { version = "0.2" }
|
||||
wasm-bindgen-futures = { version = "0.4" }
|
||||
|
||||
@@ -43,86 +43,109 @@ pub fn Form<A>(
|
||||
where
|
||||
A: ToHref + 'static,
|
||||
{
|
||||
let action_version = version;
|
||||
let action = use_resolved_path(cx, move || action.to_href()());
|
||||
fn inner(
|
||||
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| {
|
||||
if ev.default_prevented() {
|
||||
return;
|
||||
}
|
||||
let navigate = use_navigate(cx);
|
||||
let (form, method, action, enctype) = extract_form_attributes(&ev);
|
||||
|
||||
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();
|
||||
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 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());
|
||||
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());
|
||||
}
|
||||
|
||||
if resp.status() == 303 {
|
||||
if let Some(redirect_url) = resp.headers().get("Location") {
|
||||
_ = navigate(&redirect_url, Default::default());
|
||||
if resp.status() == 303 {
|
||||
if let Some(redirect_url) = resp.headers().get("Location") {
|
||||
_ = 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
|
||||
|
||||
@@ -70,35 +70,47 @@ pub fn A<H>(
|
||||
where
|
||||
H: ToHref + 'static,
|
||||
{
|
||||
let location = use_location(cx);
|
||||
let href = use_resolved_path(cx, move || href.to_href()());
|
||||
let is_active = create_memo(cx, move |_| match href.get() {
|
||||
None => false,
|
||||
fn inner(
|
||||
cx: Scope,
|
||||
href: Memo<Option<String>>,
|
||||
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) => {
|
||||
let path = to
|
||||
.split(['?', '#'])
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.to_lowercase();
|
||||
let loc = location.pathname.get().to_lowercase();
|
||||
if exact {
|
||||
loc == path
|
||||
} else {
|
||||
loc.starts_with(&path)
|
||||
Some(to) => {
|
||||
let path = to
|
||||
.split(['?', '#'])
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.to_lowercase();
|
||||
let loc = location.pathname.get().to_lowercase();
|
||||
if exact {
|
||||
loc == path
|
||||
} else {
|
||||
loc.starts_with(&path)
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
view! { cx,
|
||||
<a
|
||||
href=move || href.get().unwrap_or_default()
|
||||
prop:state={state.map(|s| s.to_js_value())}
|
||||
prop:replace={replace}
|
||||
aria-current=move || if is_active.get() { Some("page") } else { None }
|
||||
class=move || class.as_ref().map(|class| class.get())
|
||||
>
|
||||
{children(cx)}
|
||||
</a>
|
||||
view! { cx,
|
||||
<a
|
||||
href=move || href.get().unwrap_or_default()
|
||||
prop:state={state.map(|s| s.to_js_value())}
|
||||
prop:replace={replace}
|
||||
aria-current=move || if is_active.get() { Some("page") } else { None }
|
||||
class=move || class.as_ref().map(|class| class.get())
|
||||
>
|
||||
{children(cx)}
|
||||
</a>
|
||||
}
|
||||
}
|
||||
|
||||
let href = use_resolved_path(cx, move || href.to_href()());
|
||||
inner(cx, href, exact, state, replace, class, children)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
mod form;
|
||||
mod link;
|
||||
mod outlet;
|
||||
mod redirect;
|
||||
mod route;
|
||||
mod router;
|
||||
mod routes;
|
||||
@@ -8,6 +9,7 @@ mod routes;
|
||||
pub use form::*;
|
||||
pub use link::*;
|
||||
pub use outlet::*;
|
||||
pub use redirect::*;
|
||||
pub use route::*;
|
||||
pub use router::*;
|
||||
pub use routes::*;
|
||||
|
||||
@@ -27,7 +27,7 @@ pub fn Outlet(cx: Scope) -> impl IntoView {
|
||||
prev_scope.dispose();
|
||||
}
|
||||
is_showing.set(Some((child.id(), child.cx())));
|
||||
provide_context(child.cx(), child.clone());
|
||||
provide_context(cx, child.clone());
|
||||
set_outlet.set(Some(child.outlet(cx).into_view(cx)))
|
||||
}
|
||||
}
|
||||
|
||||
65
router/src/components/redirect.rs
Normal file
65
router/src/components/redirect.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
use crate::{use_navigate, use_resolved_path, NavigateOptions};
|
||||
use leptos::{component, provide_context, use_context, IntoView, Scope};
|
||||
use std::rc::Rc;
|
||||
|
||||
/// Redirects the user to a new URL, whether on the client side or on the server
|
||||
/// side. If rendered on the server, this sets a `302` status code and sets a `Location`
|
||||
/// header. If rendered in the browser, it uses client-side navigation to redirect.
|
||||
/// In either case, it resolves the route relative to the current route. (To use
|
||||
/// an absolute path, prefix it with `/`).
|
||||
///
|
||||
/// **Note**: Support for server-side redirects is provided by the server framework
|
||||
/// integrations (`leptos_actix` and `leptos_axum`). If you’re not using one of those
|
||||
/// integrations, you should manually provide a way of redirecting on the server
|
||||
/// using [provide_server_redirect].
|
||||
#[component]
|
||||
pub fn Redirect<P>(
|
||||
cx: Scope,
|
||||
/// The relative path to which the user should be redirected.
|
||||
path: P,
|
||||
/// Navigation options to be used on the client side.
|
||||
#[prop(optional)]
|
||||
options: Option<NavigateOptions>,
|
||||
) -> impl IntoView
|
||||
where
|
||||
P: std::fmt::Display + 'static,
|
||||
{
|
||||
// resolve relative path
|
||||
let path = use_resolved_path(cx, move || path.to_string());
|
||||
let path = path.get().unwrap_or_else(|| "/".to_string());
|
||||
|
||||
// redirect on the server
|
||||
if let Some(redirect_fn) = use_context::<ServerRedirectFunction>(cx) {
|
||||
(redirect_fn.f)(&path);
|
||||
}
|
||||
|
||||
// redirect on the client
|
||||
let navigate = use_navigate(cx);
|
||||
navigate(&path, options.unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Wrapping type for a function provided as context to allow for
|
||||
/// server-side redirects. See [provide_server_redirect]
|
||||
/// and [Redirect].
|
||||
#[derive(Clone)]
|
||||
pub struct ServerRedirectFunction {
|
||||
f: Rc<dyn Fn(&str)>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ServerRedirectFunction {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ServerRedirectFunction").finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides a function that can be used to redirect the user to another
|
||||
/// absolute path, on the server. This should set a `302` status code and an
|
||||
/// appropriate `Location` header.
|
||||
pub fn provide_server_redirect(cx: Scope, handler: impl Fn(&str) + 'static) {
|
||||
provide_context(
|
||||
cx,
|
||||
ServerRedirectFunction {
|
||||
f: Rc::new(handler),
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -36,31 +36,47 @@ where
|
||||
F: Fn(Scope) -> E + 'static,
|
||||
P: std::fmt::Display,
|
||||
{
|
||||
let children = children
|
||||
.map(|children| {
|
||||
children(cx)
|
||||
.as_children()
|
||||
.iter()
|
||||
.filter_map(|child| {
|
||||
child
|
||||
.as_transparent()
|
||||
.and_then(|t| t.downcast_ref::<RouteDefinition>())
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let id = ROUTE_ID.with(|id| {
|
||||
let next = id.get() + 1;
|
||||
id.set(next);
|
||||
next
|
||||
});
|
||||
RouteDefinition {
|
||||
id,
|
||||
path: path.to_string(),
|
||||
children,
|
||||
view: Rc::new(move |cx| view(cx).into_view(cx)),
|
||||
fn inner(
|
||||
cx: Scope,
|
||||
children: Option<Children>,
|
||||
path: String,
|
||||
view: Rc<dyn Fn(Scope) -> View>,
|
||||
) -> RouteDefinition {
|
||||
let children = children
|
||||
.map(|children| {
|
||||
children(cx)
|
||||
.as_children()
|
||||
.iter()
|
||||
.filter_map(|child| {
|
||||
child
|
||||
.as_transparent()
|
||||
.and_then(|t| t.downcast_ref::<RouteDefinition>())
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let id = ROUTE_ID.with(|id| {
|
||||
let next = id.get() + 1;
|
||||
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 {
|
||||
|
||||
@@ -8,10 +8,6 @@
|
||||
//! apps (SPAs), server-side rendering/multi-page apps (MPAs), or to synchronize
|
||||
//! 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
|
||||
//!
|
||||
//! 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
|
||||
//! 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
|
||||
//! 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
|
||||
//! 3. **Progressive enhancement.** The [A] and [Form] components resolve any relative
|
||||
//! 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
|
||||
//! them with server-side rendering (with or without hydration), they just work,
|
||||
|
||||
Reference in New Issue
Block a user