Compare commits

..

2 Commits

Author SHA1 Message Date
Greg Johnston
9c4a6ba9ba fix sandboxed tests 2024-10-08 15:16:33 -04:00
Greg Johnston
d63c2fd507 chore: re-add regression tests from #2639 2024-10-07 21:15:31 -04:00
22 changed files with 368 additions and 614 deletions

View File

@@ -94,7 +94,7 @@ jobs:
fi
done
- name: Install Deno
uses: denoland/setup-deno@v2
uses: denoland/setup-deno@v1
with:
deno-version: v1.x
- name: Maybe install gtk-rs dependencies

View File

@@ -64,7 +64,7 @@ pub fn RouterExample() -> impl IntoView {
// You can define other routes in their own component.
// Routes implement the MatchNestedRoutes
#[component(transparent)]
#[component]
pub fn ContactRoutes() -> impl MatchNestedRoutes + Clone {
view! {
<ParentRoute path=path!("") view=ContactList>

View File

@@ -1381,41 +1381,39 @@ where
),
)
} else {
router
.route(path, web::head().to(HttpResponse::Ok))
.route(
path,
match mode {
SsrMode::OutOfOrder => {
render_app_to_stream_with_context(
additional_context_and_method.clone(),
app_fn.clone(),
method,
)
}
SsrMode::PartiallyBlocked => {
render_app_to_stream_with_context_and_replace_blocks(
additional_context_and_method.clone(),
app_fn.clone(),
method,
true,
)
}
SsrMode::InOrder => {
render_app_to_stream_in_order_with_context(
additional_context_and_method.clone(),
app_fn.clone(),
method,
)
}
SsrMode::Async => render_app_async_with_context(
additional_context_and_method.clone(),
app_fn.clone(),
method,
),
_ => unreachable!()
},
)
router.route(
path,
match mode {
SsrMode::OutOfOrder => {
render_app_to_stream_with_context(
additional_context_and_method.clone(),
app_fn.clone(),
method,
)
}
SsrMode::PartiallyBlocked => {
render_app_to_stream_with_context_and_replace_blocks(
additional_context_and_method.clone(),
app_fn.clone(),
method,
true,
)
}
SsrMode::InOrder => {
render_app_to_stream_in_order_with_context(
additional_context_and_method.clone(),
app_fn.clone(),
method,
)
}
SsrMode::Async => render_app_async_with_context(
additional_context_and_method.clone(),
app_fn.clone(),
method,
),
_ => unreachable!()
},
)
};
}
}

View File

@@ -87,7 +87,7 @@ where
T: Send + Sync + Serialize + DeserializeOwned + 'static,
Fut: std::future::Future<Output = T> + Send + 'static,
Chil: FnOnce(&T) -> V + Send + 'static,
V: IntoView + 'static,
V: IntoView,
{
let res = ArcOnceResource::<T>::new_with_options(future, blocking);
let ready = res.ready();

View File

@@ -35,7 +35,7 @@ pub fn Provider<T, Chil>(
) -> impl IntoView
where
T: Send + Sync + 'static,
Chil: IntoView + 'static,
Chil: IntoView,
{
let owner = Owner::current()
.expect("no current reactive Owner found")

View File

@@ -1,7 +1,5 @@
#[cfg(feature = "ssr")]
use leptos::html::HtmlElement;
#[cfg(feature = "ssr")]
#[test]
fn simple_ssr_test() {
use leptos::prelude::*;
@@ -22,7 +20,6 @@ fn simple_ssr_test() {
);
}
#[cfg(feature = "ssr")]
#[test]
fn ssr_test_with_components() {
use leptos::prelude::*;
@@ -54,7 +51,6 @@ fn ssr_test_with_components() {
);
}
#[cfg(feature = "ssr")]
#[test]
fn ssr_test_with_snake_case_components() {
use leptos::prelude::*;
@@ -85,7 +81,6 @@ fn ssr_test_with_snake_case_components() {
);
}
#[cfg(feature = "ssr")]
#[test]
fn test_classes() {
use leptos::prelude::*;
@@ -103,7 +98,6 @@ fn test_classes() {
assert_eq!(rendered.to_html(), "<div class=\"my big red car\"></div>");
}
#[cfg(feature = "ssr")]
#[test]
fn ssr_with_styles() {
use leptos::prelude::*;
@@ -125,7 +119,6 @@ fn ssr_with_styles() {
);
}
#[cfg(feature = "ssr")]
#[test]
fn ssr_option() {
use leptos::prelude::*;

View File

@@ -396,7 +396,8 @@ impl IntervalHandle {
}
}
/// Repeatedly calls the given function, with a delay of the given duration between calls.
/// Repeatedly calls the given function, with a delay of the given duration between calls,
/// returning a cancelable handle.
/// See [`setInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval).
#[cfg_attr(
feature = "tracing",

View File

@@ -51,15 +51,7 @@ axum = ["server_fn_macro/axum"]
[package.metadata.cargo-all-features]
denylist = ["nightly", "tracing", "trace-component-props"]
skip_feature_sets = [
["csr", "hydrate"],
["hydrate", "csr"],
["hydrate", "ssr"],
["actix", "axum"]
]
skip_feature_sets = [["csr", "hydrate"], ["hydrate", "csr"], ["hydrate", "ssr"]]
[package.metadata.docs.rs]
rustdoc-args = ["--generate-link-to-definition"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(erase_components)'] }

View File

@@ -18,7 +18,6 @@ use syn::{
};
pub struct Model {
is_transparent: bool,
island: Option<String>,
docs: Docs,
unknown_attrs: UnknownAttrs,
@@ -63,7 +62,6 @@ impl Parse for Model {
});
Ok(Self {
is_transparent: false,
island: None,
docs,
unknown_attrs,
@@ -104,7 +102,6 @@ pub fn convert_from_snake_case(name: &Ident) -> Ident {
impl ToTokens for Model {
fn to_tokens(&self, tokens: &mut TokenStream) {
let Self {
is_transparent,
island,
docs,
unknown_attrs,
@@ -119,22 +116,20 @@ impl ToTokens for Model {
let no_props = props.is_empty();
// check for components that end ;
if !is_transparent {
let ends_semi =
body.block.stmts.iter().last().and_then(|stmt| match stmt {
Stmt::Item(Item::Macro(mac)) => mac.semi_token.as_ref(),
_ => None,
});
if let Some(semi) = ends_semi {
proc_macro_error2::emit_error!(
semi.span(),
"A component that ends with a `view!` macro followed by a \
semicolon will return (), an empty view. This is usually \
an accident, not intentional, so we prevent it. If youd \
like to return (), you can do it it explicitly by \
returning () as the last item from the component."
);
}
let ends_semi =
body.block.stmts.iter().last().and_then(|stmt| match stmt {
Stmt::Item(Item::Macro(mac)) => mac.semi_token.as_ref(),
_ => None,
});
if let Some(semi) = ends_semi {
proc_macro_error2::emit_error!(
semi.span(),
"A component that ends with a `view!` macro followed by a \
semicolon will return (), an empty view. This is usually an \
accident, not intentional, so we prevent it. If youd like \
to return (), you can do it it explicitly by returning () as \
the last item from the component."
);
}
//body.sig.ident = format_ident!("__{}", body.sig.ident);
@@ -270,30 +265,14 @@ impl ToTokens for Model {
}
};
let component = if *is_transparent {
body_expr
} else if cfg!(erase_components) {
quote! {
::leptos::prelude::IntoAny::into_any(
::leptos::prelude::untrack(
move || {
#tracing_guard_expr
#tracing_props_expr
#body_expr
}
)
)
}
} else {
quote! {
::leptos::prelude::untrack(
move || {
#tracing_guard_expr
#tracing_props_expr
#body_expr
}
)
}
let component = quote! {
::leptos::prelude::untrack(
move || {
#tracing_guard_expr
#tracing_props_expr
#body_expr
}
)
};
// add island wrapper if island
@@ -541,13 +520,6 @@ impl ToTokens for Model {
}
impl Model {
#[allow(clippy::wrong_self_convention)]
pub fn is_transparent(mut self, is_transparent: bool) -> Self {
self.is_transparent = is_transparent;
self
}
#[allow(clippy::wrong_self_convention)]
pub fn with_island(mut self, island: Option<String>) -> Self {
self.island = island;

View File

@@ -535,24 +535,11 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
/// ```
#[proc_macro_error2::proc_macro_error]
#[proc_macro_attribute]
pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
let is_transparent = if !args.is_empty() {
let transparent = parse_macro_input!(args as syn::Ident);
if transparent != "transparent" {
abort!(
transparent,
"only `transparent` is supported";
help = "try `#[component(transparent)]` or `#[component]`"
);
}
true
} else {
false
};
component_macro(s, is_transparent, None)
pub fn component(
_args: proc_macro::TokenStream,
s: TokenStream,
) -> TokenStream {
component_macro(s, None)
}
/// Defines a component as an interactive island when you are using the
@@ -628,37 +615,17 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
/// ```
#[proc_macro_error2::proc_macro_error]
#[proc_macro_attribute]
pub fn island(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
let is_transparent = if !args.is_empty() {
let transparent = parse_macro_input!(args as syn::Ident);
if transparent != "transparent" {
abort!(
transparent,
"only `transparent` is supported";
help = "try `#[component(transparent)]` or `#[component]`"
);
}
true
} else {
false
};
pub fn island(_args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
let island_src = s.to_string();
component_macro(s, is_transparent, Some(island_src))
component_macro(s, Some(island_src))
}
fn component_macro(
s: TokenStream,
is_transparent: bool,
island: Option<String>,
) -> TokenStream {
fn component_macro(s: TokenStream, island: Option<String>) -> TokenStream {
let mut dummy = syn::parse::<DummyModel>(s.clone());
let parse_result = syn::parse::<component::Model>(s);
if let (Ok(ref mut unexpanded), Ok(model)) = (&mut dummy, parse_result) {
let expanded = model.is_transparent(is_transparent).with_island(island).into_token_stream();
let expanded = model.with_island(island).into_token_stream();
if !matches!(unexpanded.vis, Visibility::Public(_)) {
unexpanded.vis = Visibility::Public(Pub {
span: unexpanded.vis.span(),

View File

@@ -179,7 +179,7 @@ fn is_inert_element(orig_node: &Node<impl CustomNode>) -> bool {
}
enum Item<'a, T> {
Node(&'a Node<T>, bool),
Node(&'a Node<T>),
ClosingTag(String),
}
@@ -290,11 +290,10 @@ impl<'a> InertElementBuilder<'a> {
fn inert_element_to_tokens(
node: &Node<impl CustomNode>,
escape_text: bool,
global_class: Option<&TokenTree>,
) -> Option<TokenStream> {
let mut html = InertElementBuilder::new(global_class);
let mut nodes = VecDeque::from([Item::Node(node, escape_text)]);
let mut nodes = VecDeque::from([Item::Node(node)]);
while let Some(current) = nodes.pop_front() {
match current {
@@ -304,32 +303,21 @@ fn inert_element_to_tokens(
html.push_str(&tag);
html.push('>');
}
Item::Node(current, escape) => {
Item::Node(current) => {
match current {
Node::RawText(raw) => {
let text = raw.to_string_best();
let text = if escape {
html_escape::encode_text(&text)
} else {
text.into()
};
let text = html_escape::encode_text(&text);
html.push_str(&text);
}
Node::Text(text) => {
let text = text.value_string();
let text = if escape {
html_escape::encode_text(&text)
} else {
text.into()
};
let text = html_escape::encode_text(&text);
html.push_str(&text);
}
Node::Element(node) => {
let self_closing = is_self_closing(node);
let el_name = node.name().to_string();
let escape = el_name != "script"
&& el_name != "style"
&& el_name != "textarea";
// opening tag
html.push('<');
@@ -376,7 +364,7 @@ fn inert_element_to_tokens(
nodes.push_front(Item::ClosingTag(el_name));
let children = node.children.iter().rev();
for child in children {
nodes.push_front(Item::Node(child, escape));
nodes.push_front(Item::Node(child));
}
}
}
@@ -571,11 +559,7 @@ fn node_to_tokens(
}
Node::Element(el_node) => {
if !top_level && is_inert {
let el_name = el_node.name().to_string();
let escape = el_name != "script"
&& el_name != "style"
&& el_name != "textarea";
inert_element_to_tokens(node, escape, global_class)
inert_element_to_tokens(node, global_class)
} else {
element_to_tokens(
el_node,

View File

@@ -1,4 +1,3 @@
#[cfg(not(erase_components))]
#[test]
fn ui() {
let t = trybuild::TestCases::new();

View File

@@ -434,14 +434,6 @@ pub struct OnceResource<T, Ser = JsonSerdeCodec> {
defined_at: &'static Location<'static>,
}
impl<T, Ser> Clone for OnceResource<T, Ser> {
fn clone(&self) -> Self {
*self
}
}
impl<T, Ser> Copy for OnceResource<T, Ser> {}
impl<T, Ser> OnceResource<T, Ser>
where
T: Send + Sync + 'static,

View File

@@ -1,12 +1,9 @@
use crate::{
computed::{ArcMemo, Memo},
diagnostics::is_suppressing_resource_load,
owner::{
ArcStoredValue, ArenaItem, FromLocal, LocalStorage, Storage,
SyncStorage,
},
owner::{ArenaItem, FromLocal, LocalStorage, Storage, SyncStorage},
signal::{ArcRwSignal, RwSignal},
traits::{DefinedAt, Dispose, Get, GetUntracked, GetValue, Update},
traits::{DefinedAt, Dispose, Get, GetUntracked, Update},
unwrap_signal,
};
use any_spawner::Executor;
@@ -96,7 +93,6 @@ pub struct ArcAction<I, O> {
input: ArcRwSignal<Option<I>>,
value: ArcRwSignal<Option<O>>,
version: ArcRwSignal<usize>,
dispatched: ArcStoredValue<usize>,
#[allow(clippy::complexity)]
action_fn: Arc<
dyn Fn(&I) -> Pin<Box<dyn Future<Output = O> + Send>> + Send + Sync,
@@ -112,7 +108,6 @@ impl<I, O> Clone for ArcAction<I, O> {
input: self.input.clone(),
value: self.value.clone(),
version: self.version.clone(),
dispatched: self.dispatched.clone(),
action_fn: self.action_fn.clone(),
#[cfg(debug_assertions)]
defined_at: self.defined_at,
@@ -196,7 +191,6 @@ where
input: Default::default(),
value: ArcRwSignal::new(value),
version: Default::default(),
dispatched: Default::default(),
action_fn: Arc::new(move |input| Box::pin(action_fn(input))),
#[cfg(debug_assertions)]
defined_at: Location::caller(),
@@ -236,14 +230,14 @@ where
// Update the state before loading
self.in_flight.update(|n| *n += 1);
let current_version = self.dispatched.get_value();
let current_version =
self.version.try_get_untracked().unwrap_or_default();
self.input.try_update(|inp| *inp = Some(input));
// Spawn the task
crate::spawn({
let input = self.input.clone();
let version = self.version.clone();
let dispatched = self.dispatched.clone();
let value = self.value.clone();
let in_flight = self.in_flight.clone();
async move {
@@ -255,7 +249,7 @@ where
// otherwise, update the value
result = fut => {
in_flight.update(|n| *n = n.saturating_sub(1));
let is_latest = dispatched.get_value() <= current_version;
let is_latest = version.get_untracked() <= current_version;
if is_latest {
version.update(|n| *n += 1);
value.update(|n| *n = Some(result));
@@ -288,7 +282,8 @@ where
// Update the state before loading
self.in_flight.update(|n| *n += 1);
let current_version = self.dispatched.get_value();
let current_version =
self.version.try_get_untracked().unwrap_or_default();
self.input.try_update(|inp| *inp = Some(input));
// Spawn the task
@@ -296,7 +291,6 @@ where
let input = self.input.clone();
let version = self.version.clone();
let value = self.value.clone();
let dispatched = self.dispatched.clone();
let in_flight = self.in_flight.clone();
async move {
select! {
@@ -307,7 +301,7 @@ where
// otherwise, update the value
result = fut => {
in_flight.update(|n| *n = n.saturating_sub(1));
let is_latest = dispatched.get_value() <= current_version;
let is_latest = version.get_untracked() <= current_version;
if is_latest {
version.update(|n| *n += 1);
value.update(|n| *n = Some(result));
@@ -357,7 +351,6 @@ where
input: Default::default(),
value: ArcRwSignal::new(value),
version: Default::default(),
dispatched: Default::default(),
action_fn: Arc::new(move |input| {
Box::pin(SendWrapper::new(action_fn(input)))
}),

View File

@@ -171,26 +171,12 @@ impl KeyMap {
where
K: Debug + Hash + PartialEq + Eq + Send + Sync + 'static,
{
// this incredibly defensive mechanism takes the guard twice
// on initialization. unfortunately, this is because `initialize`, on
// a nested keyed field can, when being initialized), can in fact try
// to take the lock again, as we try to insert the keys of the parent
// while inserting the keys on this child.
//
// see here https://github.com/leptos-rs/leptos/issues/3086
let mut guard = self.0.write().or_poisoned();
if guard.contains_key(&path) {
let entry = guard.get_mut(&path)?;
let entry = entry.downcast_mut::<FieldKeys<K>>()?;
Some(fun(entry))
} else {
drop(guard);
let keys = Box::new(FieldKeys::new(initialize()));
let mut guard = self.0.write().or_poisoned();
let entry = guard.entry(path).or_insert(keys);
let entry = entry.downcast_mut::<FieldKeys<K>>()?;
Some(fun(entry))
}
let entry = guard
.entry(path)
.or_insert_with(|| Box::new(FieldKeys::new(initialize())));
let entry = entry.downcast_mut::<FieldKeys<K>>()?;
Some(fun(entry))
}
}

View File

@@ -24,7 +24,6 @@ use reactive_graph::{
use std::{
borrow::Cow,
fmt::{Debug, Display},
mem,
sync::Arc,
time::Duration,
};
@@ -48,7 +47,7 @@ where
}
}
#[component(transparent)]
#[component]
pub fn Router<Chil>(
/// The base URL for the router. Defaults to `""`.
#[prop(optional, into)]
@@ -102,7 +101,6 @@ where
location,
state,
set_is_routing,
query_mutations: Default::default(),
});
let children = children.into_inner();
@@ -116,8 +114,6 @@ pub(crate) struct RouterContext {
pub location: Location,
pub state: ArcRwSignal<State>,
pub set_is_routing: Option<SignalSetter<bool>>,
pub query_mutations:
ArcStoredValue<Vec<(Oco<'static, str>, Option<String>)>>,
}
impl RouterContext {
@@ -134,7 +130,7 @@ impl RouterContext {
resolve_path("", path, None)
};
let mut url = match resolved_to.map(|to| BrowserUrl::parse(&to)) {
let url = match resolved_to.map(|to| BrowserUrl::parse(&to)) {
Some(Ok(url)) => url,
Some(Err(e)) => {
leptos::logging::error!("Error parsing URL: {e:?}");
@@ -145,22 +141,6 @@ impl RouterContext {
return;
}
};
let query_mutations =
mem::take(&mut *self.query_mutations.write_value());
if !query_mutations.is_empty() {
for (key, value) in query_mutations {
if let Some(value) = value {
url.search_params_mut().replace(key, value);
} else {
url.search_params_mut().remove(&key);
}
}
*url.search_mut() = url
.search_params()
.to_query_string()
.trim_start_matches('?')
.into()
}
if url.origin() != current.origin() {
window().location().set_href(path).unwrap();
@@ -173,14 +153,13 @@ impl RouterContext {
}
// update URL signal, if necessary
let value = url.to_full_path();
if current != url {
drop(current);
self.current_url.set(url);
}
BrowserUrl::complete_navigation(&LocationChange {
value,
value: path.to_string(),
replace: options.replace,
scroll: options.scroll,
state: options.state,
@@ -225,7 +204,7 @@ where
}
}*/
#[component(transparent)]
#[component]
pub fn Routes<Defs, FallbackFn, Fallback>(
fallback: FallbackFn,
children: RouteChildren<Defs>,
@@ -268,7 +247,7 @@ where
}
}
#[component(transparent)]
#[component]
pub fn FlatRoutes<Defs, FallbackFn, Fallback>(
fallback: FallbackFn,
children: RouteChildren<Defs>,
@@ -315,7 +294,7 @@ where
}
}
#[component(transparent)]
#[component]
pub fn Route<Segments, View>(
path: Segments,
view: View,
@@ -327,7 +306,7 @@ where
NestedRoute::new(path, view).ssr_mode(ssr)
}
#[component(transparent)]
#[component]
pub fn ParentRoute<Segments, View, Children>(
path: Segments,
view: View,
@@ -341,7 +320,7 @@ where
NestedRoute::new(path, view).ssr_mode(ssr).child(children)
}
#[component(transparent)]
#[component]
pub fn ProtectedRoute<Segments, ViewFn, View, C, PathFn, P>(
path: Segments,
view: ViewFn,
@@ -383,7 +362,7 @@ where
NestedRoute::new(path, view).ssr_mode(ssr)
}
#[component(transparent)]
#[component]
pub fn ProtectedParentRoute<Segments, ViewFn, View, C, PathFn, P, Children>(
path: Segments,
view: ViewFn,
@@ -445,7 +424,7 @@ where
///
/// [`leptos_actix`]: <https://docs.rs/leptos_actix/>
/// [`leptos_axum`]: <https://docs.rs/leptos_axum/>
#[component(transparent)]
#[component]
pub fn Redirect<P>(
/// The relative path to which the user should be redirected.
path: P,

View File

@@ -4,18 +4,15 @@ use crate::{
navigate::NavigateOptions,
params::{Params, ParamsError, ParamsMap},
};
use leptos::{leptos_dom::helpers::request_animation_frame, oco::Oco};
use leptos::oco::Oco;
use reactive_graph::{
computed::{ArcMemo, Memo},
owner::{expect_context, use_context},
owner::use_context,
signal::{ArcRwSignal, ReadSignal},
traits::{Get, GetUntracked, ReadUntracked, With, WriteValue},
traits::{Get, GetUntracked, With},
wrappers::write::SignalSetter,
};
use std::{
str::FromStr,
sync::atomic::{AtomicBool, Ordering},
};
use std::str::FromStr;
#[track_caller]
#[deprecated = "This has been renamed to `query_signal` to match Rust naming \
@@ -96,15 +93,10 @@ pub fn query_signal_with_options<T>(
where
T: FromStr + ToString + PartialEq + Send + Sync,
{
static IS_NAVIGATING: AtomicBool = AtomicBool::new(false);
let mut key: Oco<'static, str> = key.into();
let query_map = use_query_map();
let navigate = use_navigate();
let location = use_location();
let RouterContext {
query_mutations, ..
} = expect_context();
let get = Memo::new({
let key = key.clone_inplace();
@@ -116,25 +108,20 @@ where
});
let set = SignalSetter::map(move |value: Option<T>| {
let mut new_query_map = query_map.get();
match value {
Some(value) => {
new_query_map.insert(key.to_string(), value.to_string());
}
None => {
new_query_map.remove(&key);
}
}
let qs = new_query_map.to_query_string();
let path = location.pathname.get_untracked();
let hash = location.hash.get_untracked();
let qs = location.query.read_untracked().to_query_string();
let new_url = format!("{path}{qs}{hash}");
query_mutations
.write_value()
.push((key.clone(), value.as_ref().map(ToString::to_string)));
if !IS_NAVIGATING.load(Ordering::Relaxed) {
IS_NAVIGATING.store(true, Ordering::Relaxed);
request_animation_frame({
let navigate = navigate.clone();
let nav_options = nav_options.clone();
move || {
navigate(&new_url, nav_options.clone());
IS_NAVIGATING.store(false, Ordering::Relaxed)
}
})
}
navigate(&new_url, nav_options.clone());
});
(get, set)

View File

@@ -121,7 +121,7 @@ impl LocationProvider for BrowserUrl {
&& curr.path() == new_url.path()
};
url.set(new_url.clone());
url.set(new_url);
if same_path {
Self::complete_navigation(&loc);
}
@@ -130,19 +130,12 @@ impl LocationProvider for BrowserUrl {
if !same_path {
*pending.lock().or_poisoned() = Some(tx);
}
let url = url.clone();
async move {
if !same_path {
// if it has been canceled, ignore
// otherwise, complete navigation -- i.e., set URL in address bar
if rx.await.is_ok() {
// only update the URL in the browser if this is still the current URL
// if we've navigated to another page in the meantime, don't update the
// browser URL
let curr = url.read_untracked();
if curr == new_url {
Self::complete_navigation(&loc);
}
Self::complete_navigation(&loc);
}
}
}

View File

@@ -36,42 +36,22 @@ impl Url {
&self.origin
}
pub fn origin_mut(&mut self) -> &mut String {
&mut self.origin
}
pub fn path(&self) -> &str {
&self.path
}
pub fn path_mut(&mut self) -> &mut str {
&mut self.path
}
pub fn search(&self) -> &str {
&self.search
}
pub fn search_mut(&mut self) -> &mut String {
&mut self.search
}
pub fn search_params(&self) -> &ParamsMap {
&self.search_params
}
pub fn search_params_mut(&mut self) -> &mut ParamsMap {
&mut self.search_params
}
pub fn hash(&self) -> &str {
&self.hash
}
pub fn hash_mut(&mut self) -> &mut String {
&mut self.hash
}
pub fn provide_server_action_error(&self) {
let search_params = self.search_params();
if let (Some(err), Some(path)) = (
@@ -82,19 +62,6 @@ impl Url {
}
}
pub(crate) fn to_full_path(&self) -> String {
let mut path = self.path.to_string();
if !self.search.is_empty() {
path.push('?');
path.push_str(&self.search);
}
if !self.hash.is_empty() {
path.push('#');
path.push_str(&self.hash);
}
path
}
pub fn escape(s: &str) -> String {
#[cfg(not(feature = "ssr"))]
{

View File

@@ -21,9 +21,6 @@ impl ParamsMap {
}
/// Inserts a value into the map.
///
/// If a value with that key already exists, the new value will be added to it.
/// To replace the value instead, see [`replace`].
pub fn insert(&mut self, key: impl Into<Cow<'static, str>>, value: String) {
let value = unescape(&value);
@@ -35,23 +32,6 @@ impl ParamsMap {
}
}
/// Inserts a value into the map, replacing any existing value for that key.
pub fn replace(
&mut self,
key: impl Into<Cow<'static, str>>,
value: String,
) {
let value = unescape(&value);
let key = key.into();
if let Some(prev) = self.0.iter_mut().find(|(k, _)| k == &key) {
prev.1.clear();
prev.1.push(value);
} else {
self.0.push((key, vec![value]));
}
}
/// Gets the most-recently-added value of this param from the map.
pub fn get(&self, key: &str) -> Option<String> {
self.get_str(key).map(ToOwned::to_owned)

View File

@@ -67,121 +67,107 @@ where
let value = Box::new(self) as Box<dyn Any + Send>;
match value.downcast::<AnyAttribute>() {
// if it's already an AnyAttribute, we don't need to double-wrap it
Ok(any_attribute) => *any_attribute,
Err(value) => {
#[cfg(feature = "ssr")]
let to_html =
|value: Box<dyn Any>,
buf: &mut String,
class: &mut String,
style: &mut String,
inner_html: &mut String| {
let value = value.downcast::<T>().expect(
"AnyAttribute::to_html could not be downcast",
);
value.to_html(buf, class, style, inner_html);
};
let build =
|value: Box<dyn Any>,
#[cfg(feature = "ssr")]
let to_html = |value: Box<dyn Any>,
buf: &mut String,
class: &mut String,
style: &mut String,
inner_html: &mut String| {
let value = value
.downcast::<T>()
.expect("AnyAttribute::to_html could not be downcast");
value.to_html(buf, class, style, inner_html);
};
let build = |value: Box<dyn Any>,
el: &crate::renderer::types::Element| {
let value = value
.downcast::<T>()
.expect("AnyAttribute::build couldn't downcast");
let state = Box::new(value.build(el));
let value = value
.downcast::<T>()
.expect("AnyAttribute::build couldn't downcast");
let state = Box::new(value.build(el));
AnyAttributeState {
type_id: TypeId::of::<T>(),
state,
el: el.clone(),
}
};
#[cfg(feature = "hydrate")]
let hydrate_from_server =
|value: Box<dyn Any>,
el: &crate::renderer::types::Element| {
let value = value.downcast::<T>().expect(
"AnyAttribute::hydrate_from_server couldn't \
downcast",
);
let state = Box::new(value.hydrate::<true>(el));
AnyAttributeState {
type_id: TypeId::of::<T>(),
state,
el: el.clone(),
}
};
#[cfg(feature = "hydrate")]
let hydrate_from_template =
|value: Box<dyn Any>,
el: &crate::renderer::types::Element| {
let value = value.downcast::<T>().expect(
"AnyAttribute::hydrate_from_server couldn't \
downcast",
);
let state = Box::new(value.hydrate::<true>(el));
AnyAttributeState {
type_id: TypeId::of::<T>(),
state,
el: el.clone(),
}
};
let rebuild =
|new_type_id: TypeId,
value: Box<dyn Any>,
state: &mut AnyAttributeState| {
let value = value.downcast::<T>().expect(
"AnyAttribute::rebuild couldn't downcast value",
);
if new_type_id == state.type_id {
let state = state.state.downcast_mut().expect(
"AnyAttribute::rebuild couldn't downcast state",
);
value.rebuild(state);
} else {
let new = value.into_any_attr().build(&state.el);
*state = new;
}
};
#[cfg(feature = "ssr")]
let dry_resolve = |value: &mut Box<dyn Any + Send>| {
let value = value
.downcast_mut::<T>()
.expect("AnyView::resolve could not be downcast");
value.dry_resolve();
};
#[cfg(feature = "ssr")]
let resolve = |value: Box<dyn Any>| {
let value = value
.downcast::<T>()
.expect("AnyView::resolve could not be downcast");
Box::pin(
async move { value.resolve().await.into_any_attr() },
)
as Pin<Box<dyn Future<Output = AnyAttribute> + Send>>
};
AnyAttribute {
type_id: TypeId::of::<T>(),
html_len,
value,
#[cfg(feature = "ssr")]
to_html,
build,
rebuild,
#[cfg(feature = "hydrate")]
hydrate_from_server,
#[cfg(feature = "hydrate")]
hydrate_from_template,
#[cfg(feature = "ssr")]
resolve,
#[cfg(feature = "ssr")]
dry_resolve,
}
AnyAttributeState {
type_id: TypeId::of::<T>(),
state,
el: el.clone(),
}
};
#[cfg(feature = "hydrate")]
let hydrate_from_server =
|value: Box<dyn Any>, el: &crate::renderer::types::Element| {
let value = value.downcast::<T>().expect(
"AnyAttribute::hydrate_from_server couldn't downcast",
);
let state = Box::new(value.hydrate::<true>(el));
AnyAttributeState {
type_id: TypeId::of::<T>(),
state,
el: el.clone(),
}
};
#[cfg(feature = "hydrate")]
let hydrate_from_template =
|value: Box<dyn Any>, el: &crate::renderer::types::Element| {
let value = value.downcast::<T>().expect(
"AnyAttribute::hydrate_from_server couldn't downcast",
);
let state = Box::new(value.hydrate::<true>(el));
AnyAttributeState {
type_id: TypeId::of::<T>(),
state,
el: el.clone(),
}
};
let rebuild = |new_type_id: TypeId,
value: Box<dyn Any>,
state: &mut AnyAttributeState| {
let value = value
.downcast::<T>()
.expect("AnyAttribute::rebuild couldn't downcast value");
if new_type_id == state.type_id {
let state = state
.state
.downcast_mut()
.expect("AnyAttribute::rebuild couldn't downcast state");
value.rebuild(state);
} else {
let new = value.into_any_attr().build(&state.el);
*state = new;
}
};
#[cfg(feature = "ssr")]
let dry_resolve = |value: &mut Box<dyn Any + Send>| {
let value = value
.downcast_mut::<T>()
.expect("AnyView::resolve could not be downcast");
value.dry_resolve();
};
#[cfg(feature = "ssr")]
let resolve = |value: Box<dyn Any>| {
let value = value
.downcast::<T>()
.expect("AnyView::resolve could not be downcast");
Box::pin(async move { value.resolve().await.into_any_attr() })
as Pin<Box<dyn Future<Output = AnyAttribute> + Send>>
};
AnyAttribute {
type_id: TypeId::of::<T>(),
html_len,
value,
#[cfg(feature = "ssr")]
to_html,
build,
rebuild,
#[cfg(feature = "hydrate")]
hydrate_from_server,
#[cfg(feature = "hydrate")]
hydrate_from_template,
#[cfg(feature = "ssr")]
resolve,
#[cfg(feature = "ssr")]
dry_resolve,
}
}
}

View File

@@ -27,8 +27,7 @@ use std::{future::Future, pin::Pin};
pub struct AnyView {
type_id: TypeId,
value: Box<dyn Any + Send>,
build: fn(Box<dyn Any>) -> AnyViewState,
rebuild: fn(TypeId, Box<dyn Any>, &mut AnyViewState),
// The fields below are cfg-gated so they will not be included in WASM bundles if not needed.
// Ordinarily, the compiler can simply omit this dead code because the methods are not called.
// With this type-erased wrapper, however, the compiler is not *always* able to correctly
@@ -43,6 +42,8 @@ pub struct AnyView {
#[cfg(feature = "ssr")]
to_html_async_ooo:
fn(Box<dyn Any>, &mut StreamBuilder, &mut Position, bool, bool),
build: fn(Box<dyn Any>) -> AnyViewState,
rebuild: fn(TypeId, Box<dyn Any>, &mut AnyViewState),
#[cfg(feature = "ssr")]
#[allow(clippy::type_complexity)]
resolve: fn(Box<dyn Any>) -> Pin<Box<dyn Future<Output = AnyView> + Send>>,
@@ -137,172 +138,156 @@ where
let value = Box::new(self) as Box<dyn Any + Send>;
match value.downcast::<AnyView>() {
// if it's already an AnyView, we don't need to double-wrap it
Ok(any_view) => *any_view,
Err(value) => {
#[cfg(feature = "ssr")]
let dry_resolve = |value: &mut Box<dyn Any + Send>| {
let value = value
.downcast_mut::<T>()
.expect("AnyView::resolve could not be downcast");
value.dry_resolve();
};
#[cfg(feature = "ssr")]
let dry_resolve = |value: &mut Box<dyn Any + Send>| {
let value = value
.downcast_mut::<T>()
.expect("AnyView::resolve could not be downcast");
value.dry_resolve();
};
#[cfg(feature = "ssr")]
let resolve = |value: Box<dyn Any>| {
let value = value
.downcast::<T>()
.expect("AnyView::resolve could not be downcast");
Box::pin(async move { value.resolve().await.into_any() })
as Pin<Box<dyn Future<Output = AnyView> + Send>>
};
#[cfg(feature = "ssr")]
let to_html =
|value: Box<dyn Any>,
buf: &mut String,
position: &mut Position,
escape: bool,
mark_branches: bool| {
let type_id = mark_branches
.then(|| format!("{:?}", TypeId::of::<T>()))
.unwrap_or_default();
let value = value
.downcast::<T>()
.expect("AnyView::to_html could not be downcast");
if mark_branches {
buf.open_branch(&type_id);
}
value.to_html_with_buf(
buf,
position,
escape,
mark_branches,
);
if mark_branches {
buf.close_branch(&type_id);
}
};
#[cfg(feature = "ssr")]
let to_html_async =
|value: Box<dyn Any>,
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool| {
let type_id = mark_branches
.then(|| format!("{:?}", TypeId::of::<T>()))
.unwrap_or_default();
let value = value
.downcast::<T>()
.expect("AnyView::to_html could not be downcast");
if mark_branches {
buf.open_branch(&type_id);
}
value.to_html_async_with_buf::<false>(
buf,
position,
escape,
mark_branches,
);
if mark_branches {
buf.close_branch(&type_id);
}
};
#[cfg(feature = "ssr")]
let to_html_async_ooo =
|value: Box<dyn Any>,
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool| {
let value = value
.downcast::<T>()
.expect("AnyView::to_html could not be downcast");
value.to_html_async_with_buf::<true>(
buf,
position,
escape,
mark_branches,
);
};
let build = |value: Box<dyn Any>| {
let value = value
.downcast::<T>()
.expect("AnyView::build couldn't downcast");
let state = Box::new(value.build());
AnyViewState {
type_id: TypeId::of::<T>(),
state,
mount: mount_any::<T>,
unmount: unmount_any::<T>,
insert_before_this: insert_before_this::<T>,
}
};
#[cfg(feature = "hydrate")]
let hydrate_from_server =
|value: Box<dyn Any>,
cursor: &Cursor,
position: &PositionState| {
let value = value.downcast::<T>().expect(
"AnyView::hydrate_from_server couldn't downcast",
);
let state =
Box::new(value.hydrate::<true>(cursor, position));
AnyViewState {
type_id: TypeId::of::<T>(),
state,
mount: mount_any::<T>,
unmount: unmount_any::<T>,
insert_before_this: insert_before_this::<T>,
}
};
let rebuild =
|new_type_id: TypeId,
value: Box<dyn Any>,
state: &mut AnyViewState| {
let value = value
.downcast::<T>()
.expect("AnyView::rebuild couldn't downcast value");
if new_type_id == state.type_id {
let state = state.state.downcast_mut().expect(
"AnyView::rebuild couldn't downcast state",
);
value.rebuild(state);
} else {
let mut new = value.into_any().build();
state.insert_before_this(&mut new);
state.unmount();
*state = new;
}
};
AnyView {
type_id: TypeId::of::<T>(),
value,
build,
rebuild,
#[cfg(feature = "ssr")]
resolve,
#[cfg(feature = "ssr")]
dry_resolve,
#[cfg(feature = "ssr")]
html_len,
#[cfg(feature = "ssr")]
to_html,
#[cfg(feature = "ssr")]
to_html_async,
#[cfg(feature = "ssr")]
to_html_async_ooo,
#[cfg(feature = "hydrate")]
hydrate_from_server,
}
#[cfg(feature = "ssr")]
let resolve = |value: Box<dyn Any>| {
let value = value
.downcast::<T>()
.expect("AnyView::resolve could not be downcast");
Box::pin(async move { value.resolve().await.into_any() })
as Pin<Box<dyn Future<Output = AnyView> + Send>>
};
#[cfg(feature = "ssr")]
let to_html = |value: Box<dyn Any>,
buf: &mut String,
position: &mut Position,
escape: bool,
mark_branches: bool| {
let type_id = mark_branches
.then(|| format!("{:?}", TypeId::of::<T>()))
.unwrap_or_default();
let value = value
.downcast::<T>()
.expect("AnyView::to_html could not be downcast");
if mark_branches {
buf.open_branch(&type_id);
}
value.to_html_with_buf(buf, position, escape, mark_branches);
if mark_branches {
buf.close_branch(&type_id);
}
};
#[cfg(feature = "ssr")]
let to_html_async = |value: Box<dyn Any>,
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool| {
let type_id = mark_branches
.then(|| format!("{:?}", TypeId::of::<T>()))
.unwrap_or_default();
let value = value
.downcast::<T>()
.expect("AnyView::to_html could not be downcast");
if mark_branches {
buf.open_branch(&type_id);
}
value.to_html_async_with_buf::<false>(
buf,
position,
escape,
mark_branches,
);
if mark_branches {
buf.close_branch(&type_id);
}
};
#[cfg(feature = "ssr")]
let to_html_async_ooo =
|value: Box<dyn Any>,
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool| {
let value = value
.downcast::<T>()
.expect("AnyView::to_html could not be downcast");
value.to_html_async_with_buf::<true>(
buf,
position,
escape,
mark_branches,
);
};
let build = |value: Box<dyn Any>| {
let value = value
.downcast::<T>()
.expect("AnyView::build couldn't downcast");
let state = Box::new(value.build());
AnyViewState {
type_id: TypeId::of::<T>(),
state,
mount: mount_any::<T>,
unmount: unmount_any::<T>,
insert_before_this: insert_before_this::<T>,
}
};
#[cfg(feature = "hydrate")]
let hydrate_from_server =
|value: Box<dyn Any>, cursor: &Cursor, position: &PositionState| {
let value = value
.downcast::<T>()
.expect("AnyView::hydrate_from_server couldn't downcast");
let state = Box::new(value.hydrate::<true>(cursor, position));
AnyViewState {
type_id: TypeId::of::<T>(),
state,
mount: mount_any::<T>,
unmount: unmount_any::<T>,
insert_before_this: insert_before_this::<T>,
}
};
let rebuild = |new_type_id: TypeId,
value: Box<dyn Any>,
state: &mut AnyViewState| {
let value = value
.downcast::<T>()
.expect("AnyView::rebuild couldn't downcast value");
if new_type_id == state.type_id {
let state = state
.state
.downcast_mut()
.expect("AnyView::rebuild couldn't downcast state");
value.rebuild(state);
} else {
let mut new = value.into_any().build();
state.insert_before_this(&mut new);
state.unmount();
*state = new;
}
};
AnyView {
type_id: TypeId::of::<T>(),
value,
build,
rebuild,
#[cfg(feature = "ssr")]
resolve,
#[cfg(feature = "ssr")]
dry_resolve,
#[cfg(feature = "ssr")]
html_len,
#[cfg(feature = "ssr")]
to_html,
#[cfg(feature = "ssr")]
to_html_async,
#[cfg(feature = "ssr")]
to_html_async_ooo,
#[cfg(feature = "hydrate")]
hydrate_from_server,
}
}
}
@@ -329,7 +314,7 @@ impl AddAnyAttr for AnyView {
where
Self::Output<NewAttr>: RenderHtml,
{
self
todo!()
}
}