feat: add <A>
This commit is contained in:
parent
8642c563d8
commit
782cb93743
|
@ -17,6 +17,7 @@ use log::{debug, info};
|
|||
use routing::{
|
||||
components::{ParentRoute, Redirect, Route, Router, Routes},
|
||||
hooks::{use_location, use_navigate, use_params},
|
||||
link::A,
|
||||
location::{BrowserUrl, Location},
|
||||
params::Params,
|
||||
MatchNestedRoutes, NestedRoute, Outlet, ParamSegment, StaticSegment,
|
||||
|
@ -35,17 +36,15 @@ pub fn RouterExample() -> impl IntoView {
|
|||
view! {
|
||||
<Router>
|
||||
<nav>
|
||||
// TODO <A>
|
||||
// ordinary <a> elements can be used for client-side navigation
|
||||
// using <A> has two effects:
|
||||
// 1) ensuring that relative routing works properly for nested routes
|
||||
// 2) setting the `aria-current` attribute on the current link,
|
||||
// for a11y and styling purposes
|
||||
|
||||
<a href="/contacts">"Contacts"</a>
|
||||
<a href="/about">"About"</a>
|
||||
<a href="/settings">"Settings"</a>
|
||||
<a href="/redirect-home">"Redirect to Home"</a>
|
||||
<A href="/contacts">"Contacts"</A>
|
||||
<A href="/about">"About"</A>
|
||||
<A href="/settings">"Settings"</A>
|
||||
<A href="/redirect-home">"Redirect to Home"</A>
|
||||
</nav>
|
||||
<main>
|
||||
<Routes fallback=|| "This page could not be found.">
|
||||
|
@ -67,7 +66,7 @@ pub fn RouterExample() -> impl IntoView {
|
|||
#[component]
|
||||
pub fn ContactRoutes() -> impl MatchNestedRoutes<Dom> + Clone {
|
||||
view! {
|
||||
<ParentRoute path=StaticSegment("") view=ContactList>
|
||||
<ParentRoute path=StaticSegment("contacts") view=ContactList>
|
||||
<Route path=StaticSegment("") view=|| "Select a contact."/>
|
||||
<Route path=ParamSegment("id") view=Contact/>
|
||||
</ParentRoute>
|
||||
|
@ -93,9 +92,8 @@ pub fn ContactList() -> impl IntoView {
|
|||
contacts.await
|
||||
.into_iter()
|
||||
.map(|contact| {
|
||||
// TODO <A>
|
||||
view! {
|
||||
<li><a href=contact.id.to_string()><span>{contact.first_name} " " {contact.last_name}</span></a></li>
|
||||
<li><A href=contact.id.to_string()><span>{contact.first_name} " " {contact.last_name}</span></A></li>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
|
|
|
@ -272,6 +272,24 @@ where
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts the value into its cheaply-clonable form in place.
|
||||
/// In other words, if it is currently [`Oco::Owned`], converts into [`Oco::Counted`]
|
||||
/// in an `O(n)` operation, so that all future clones are `O(1)`.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use leptos_reactive::oco::Oco;
|
||||
/// let mut oco = Oco::<str>::Owned("Hello".to_string());
|
||||
/// oco.upgrade_inplace();
|
||||
/// assert!(oco1.is_counted());
|
||||
/// ```
|
||||
pub fn upgrade_inplace(&mut self) {
|
||||
if let Self::Owned(v) = &*self {
|
||||
let rc = Arc::from(v.borrow());
|
||||
*self = Self::Counted(rc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized> Default for Oco<'_, T>
|
||||
|
|
|
@ -9,7 +9,9 @@ use crate::{
|
|||
};
|
||||
use leptos::{
|
||||
children::{ToChildren, TypedChildren},
|
||||
component, IntoView,
|
||||
component,
|
||||
oco::Oco,
|
||||
IntoView,
|
||||
};
|
||||
use reactive_graph::{
|
||||
computed::ArcMemo,
|
||||
|
@ -141,6 +143,15 @@ impl RouterContext {
|
|||
state: options.state,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn resolve_path<'a>(
|
||||
&'a self,
|
||||
path: &'a str,
|
||||
from: Option<&'a str>,
|
||||
) -> Option<Cow<'a, str>> {
|
||||
let base = self.base.as_deref().unwrap_or_default();
|
||||
resolve_path(base, path, from)
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for RouterContext {
|
||||
|
@ -173,7 +184,6 @@ where
|
|||
|
||||
#[component]
|
||||
pub fn Routes<Defs, FallbackFn, Fallback>(
|
||||
#[prop(optional, into)] base: Option<Cow<'static, str>>,
|
||||
fallback: FallbackFn,
|
||||
children: RouteChildren<Defs>,
|
||||
) -> impl IntoView
|
||||
|
@ -182,8 +192,15 @@ where
|
|||
FallbackFn: Fn() -> Fallback + Send + 'static,
|
||||
Fallback: IntoView + 'static,
|
||||
{
|
||||
let RouterContext { current_url, .. } = use_context()
|
||||
let RouterContext {
|
||||
current_url, base, ..
|
||||
} = use_context()
|
||||
.expect("<Routes> should be used inside a <Router> component");
|
||||
let base = base.map(|base| {
|
||||
let mut base = Oco::from(base);
|
||||
base.upgrade_inplace();
|
||||
base
|
||||
});
|
||||
let routes = Routes::new(children.into_inner());
|
||||
let path = ArcMemo::new({
|
||||
let url = current_url.clone();
|
||||
|
@ -201,7 +218,7 @@ where
|
|||
url: current_url.clone(),
|
||||
path: path.clone(),
|
||||
search_params: search_params.clone(),
|
||||
base: base.clone(), // TODO is this necessary?
|
||||
base: base.clone(),
|
||||
fallback: fallback(),
|
||||
rndr: PhantomData,
|
||||
}
|
||||
|
|
|
@ -3,15 +3,17 @@ use crate::{
|
|||
location::{Location, Url},
|
||||
navigate::{NavigateOptions, UseNavigate},
|
||||
params::{Params, ParamsError, ParamsMap},
|
||||
RouteContext,
|
||||
};
|
||||
use leptos::{leptos_dom::helpers::window, oco::Oco};
|
||||
use reactive_graph::{
|
||||
computed::Memo,
|
||||
computed::{ArcMemo, Memo},
|
||||
owner::use_context,
|
||||
signal::{ArcReadSignal, ArcRwSignal, ReadSignal},
|
||||
traits::{Get, With},
|
||||
traits::{Get, Read, With},
|
||||
};
|
||||
use std::{rc::Rc, str::FromStr};
|
||||
use tachys::renderer::Renderer;
|
||||
/*
|
||||
/// Constructs a signal synchronized with a specific URL query parameter.
|
||||
///
|
||||
|
@ -182,23 +184,28 @@ where
|
|||
Memo::new(move |_| url.with(|url| T::from_map(url.search_params())))
|
||||
}
|
||||
|
||||
/*
|
||||
/// Resolves the given path relative to the current route.
|
||||
#[track_caller]
|
||||
pub fn use_resolved_path(
|
||||
path: impl Fn() -> String + 'static,
|
||||
) -> Memo<Option<String>> {
|
||||
let route = use_route();
|
||||
|
||||
create_memo(move |_| {
|
||||
pub(crate) fn use_resolved_path<R: Renderer + 'static>(
|
||||
path: impl Fn() -> String + Send + Sync + 'static,
|
||||
) -> ArcMemo<Option<String>> {
|
||||
let router = use_context::<RouterContext>()
|
||||
.expect("called use_resolved_path outside a <Router>");
|
||||
let matched = use_context::<RouteContext<R>>().map(|route| route.matched);
|
||||
ArcMemo::new(move |_| {
|
||||
let path = path();
|
||||
if path.starts_with('/') {
|
||||
Some(path)
|
||||
} else {
|
||||
route.resolve_path_tracked(&path)
|
||||
router
|
||||
.resolve_path(
|
||||
&path,
|
||||
matched.as_ref().map(|n| n.get()).as_deref(),
|
||||
)
|
||||
.map(|n| n.to_string())
|
||||
}
|
||||
})
|
||||
}*/
|
||||
}
|
||||
|
||||
/// Returns a function that can be used to navigate to a new route.
|
||||
///
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
pub mod components;
|
||||
mod generate_route_list;
|
||||
pub mod hooks;
|
||||
pub mod link;
|
||||
pub mod location;
|
||||
mod matching;
|
||||
mod method;
|
||||
|
|
|
@ -0,0 +1,186 @@
|
|||
use crate::{
|
||||
components::RouterContext,
|
||||
hooks::{use_location, use_resolved_path},
|
||||
location::State,
|
||||
};
|
||||
use either_of::Either;
|
||||
use leptos::{
|
||||
children::{Children, TypedChildren},
|
||||
oco::Oco,
|
||||
prelude::*,
|
||||
*,
|
||||
};
|
||||
use reactive_graph::{computed::ArcMemo, effect::Effect, owner::use_context};
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// Describes a value that is either a static or a reactive URL, i.e.,
|
||||
/// a [`String`], a [`&str`], or a reactive `Fn() -> String`.
|
||||
pub trait ToHref {
|
||||
/// Converts the (static or reactive) URL into a function that can be called to
|
||||
/// return the URL.
|
||||
fn to_href(&self) -> Box<dyn Fn() -> String + '_>;
|
||||
}
|
||||
|
||||
impl ToHref for &str {
|
||||
fn to_href(&self) -> Box<dyn Fn() -> String> {
|
||||
let s = self.to_string();
|
||||
Box::new(move || s.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl ToHref for String {
|
||||
fn to_href(&self) -> Box<dyn Fn() -> String> {
|
||||
let s = self.clone();
|
||||
Box::new(move || s.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl ToHref for Cow<'_, str> {
|
||||
fn to_href(&self) -> Box<dyn Fn() -> String + '_> {
|
||||
let s = self.to_string();
|
||||
Box::new(move || s.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl ToHref for Oco<'_, str> {
|
||||
fn to_href(&self) -> Box<dyn Fn() -> String + '_> {
|
||||
let s = self.to_string();
|
||||
Box::new(move || s.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> ToHref for F
|
||||
where
|
||||
F: Fn() -> String + 'static,
|
||||
{
|
||||
fn to_href(&self) -> Box<dyn Fn() -> String + '_> {
|
||||
Box::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// An HTML [`a`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a)
|
||||
/// progressively enhanced to use client-side routing.
|
||||
///
|
||||
/// Client-side routing also works with ordinary HTML `<a>` tags, but `<A>` does two additional things:
|
||||
/// 1) Correctly resolves relative nested routes. Relative routing with ordinary `<a>` tags can be tricky.
|
||||
/// For example, if you have a route like `/post/:id`, `<A href="1">` will generate the correct relative
|
||||
/// route, but `<a href="1">` likely will not (depending on where it appears in your view.)
|
||||
/// 2) Sets the `aria-current` attribute if this link is the active link (i.e., it’s a link to the page you’re on).
|
||||
/// This is helpful for accessibility and for styling. For example, maybe you want to set the link a
|
||||
/// different color if it’s a link to the page you’re currently on.
|
||||
#[component]
|
||||
pub fn A<H>(
|
||||
/// Used to calculate the link's `href` attribute. Will be resolved relative
|
||||
/// to the current route.
|
||||
href: H,
|
||||
/// Where to display the linked URL, as the name for a browsing context (a tab, window, or `<iframe>`).
|
||||
#[prop(optional, into)]
|
||||
target: Option<Oco<'static, str>>,
|
||||
/// If `true`, the link is marked active when the location matches exactly;
|
||||
/// if false, link is marked active if the current route starts with it.
|
||||
#[prop(optional)]
|
||||
exact: bool,
|
||||
/// Provides a class to be added when the link is active. If provided, it will
|
||||
/// be added at the same time that the `aria-current` attribute is set.
|
||||
///
|
||||
/// This supports multiple space-separated class names.
|
||||
///
|
||||
/// **Performance**: If it’s possible to style the link using the CSS with the
|
||||
/// `[aria-current=page]` selector, you should prefer that, as it enables significant
|
||||
/// SSR optimizations.
|
||||
#[prop(optional, into)]
|
||||
active_class: Option<Oco<'static, str>>,
|
||||
/// An object of any type that will be pushed to router state
|
||||
#[prop(optional)]
|
||||
state: Option<State>,
|
||||
/// If `true`, the link will not add to the browser's history (so, pressing `Back`
|
||||
/// will skip this page.)
|
||||
#[prop(optional)]
|
||||
replace: bool,
|
||||
// TODO arbitrary attributes
|
||||
/*/// Sets the `class` attribute on the underlying `<a>` tag, making it easier to style.
|
||||
#[prop(optional, into)]
|
||||
class: Option<AttributeValue>,
|
||||
/// Sets the `id` attribute on the underlying `<a>` tag, making it easier to target.
|
||||
#[prop(optional, into)]
|
||||
id: Option<Oco<'static, str>>,
|
||||
/// Arbitrary attributes to add to the `<a>`. Attributes can be added with the
|
||||
/// `attr:` syntax in the `view` macro.
|
||||
#[prop(attrs)]
|
||||
attributes: Vec<(&'static str, Attribute)>,*/
|
||||
/// The nodes or elements to be shown inside the link.
|
||||
children: Children,
|
||||
) -> impl IntoView
|
||||
where
|
||||
H: ToHref + Send + Sync + 'static,
|
||||
{
|
||||
fn inner(
|
||||
href: ArcMemo<Option<String>>,
|
||||
target: Option<Oco<'static, str>>,
|
||||
exact: bool,
|
||||
#[allow(unused)] state: Option<State>,
|
||||
#[allow(unused)] replace: bool,
|
||||
#[allow(unused)] active_class: Option<Oco<'static, str>>,
|
||||
children: Children,
|
||||
) -> impl IntoView {
|
||||
let RouterContext { current_url, .. } =
|
||||
use_context().expect("tried to use <A/> outside a <Router/>.");
|
||||
let is_active = ArcMemo::new({
|
||||
let href = href.clone();
|
||||
move |_| {
|
||||
href.read().as_deref().is_some_and(|to| {
|
||||
let path = to.split(['?', '#']).next().unwrap_or_default();
|
||||
current_url.with(|loc| {
|
||||
let loc = loc.path();
|
||||
if exact {
|
||||
loc == path
|
||||
} else {
|
||||
std::iter::zip(loc.split('/'), path.split('/'))
|
||||
.all(|(loc_p, path_p)| loc_p == path_p)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
let mut a = view! {
|
||||
<a
|
||||
href=move || href.get().unwrap_or_default()
|
||||
target=target
|
||||
prop:state=state.map(|s| s.to_js_value())
|
||||
prop:replace=replace
|
||||
aria-current={
|
||||
let is_active = is_active.clone();
|
||||
move || if is_active.get() { Some("page") } else { None }
|
||||
}
|
||||
>
|
||||
|
||||
// TODO attributes
|
||||
// class=class
|
||||
// id=id
|
||||
{children()}
|
||||
</a>
|
||||
};
|
||||
|
||||
/*if let Some(active_class) = active_class {
|
||||
let classes = active_class
|
||||
.split_ascii_whitespace()
|
||||
.map(|class| Cow::Owned(class.to_string()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Either::Left(a.class((classes, move || is_active.get())))
|
||||
} else {
|
||||
Either::Right(a)
|
||||
}*/
|
||||
a
|
||||
|
||||
// TODO attributes
|
||||
/*for (attr_name, attr_value) in attributes {
|
||||
a = a.attr(attr_name, attr_value);
|
||||
}*/
|
||||
}
|
||||
|
||||
let href = use_resolved_path::<Dom>(move || href.to_href()());
|
||||
inner(href, target, exact, state, replace, active_class, children)
|
||||
}
|
||||
|
|
@ -2,16 +2,17 @@ use crate::{
|
|||
location::{Location, Url},
|
||||
matching::Routes,
|
||||
params::ParamsMap,
|
||||
resolve_path::resolve_path,
|
||||
ChooseView, MatchInterface, MatchNestedRoutes, MatchParams, RouteMatchId,
|
||||
};
|
||||
use either_of::Either;
|
||||
use leptos::{component, IntoView};
|
||||
use leptos::{component, oco::Oco, IntoView};
|
||||
use or_poisoned::OrPoisoned;
|
||||
use reactive_graph::{
|
||||
computed::{ArcMemo, Memo},
|
||||
owner::{provide_context, use_context, Owner},
|
||||
signal::{ArcRwSignal, ArcTrigger},
|
||||
traits::{Get, Read, Set, Track, Trigger},
|
||||
traits::{Get, Read, ReadUntracked, Set, Track, Trigger},
|
||||
};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
|
@ -41,7 +42,7 @@ pub(crate) struct NestedRoutesView<Defs, Fal, R> {
|
|||
pub url: ArcRwSignal<Url>,
|
||||
pub path: ArcMemo<String>,
|
||||
pub search_params: ArcMemo<ParamsMap>,
|
||||
pub base: Option<Cow<'static, str>>,
|
||||
pub base: Option<Oco<'static, str>>,
|
||||
pub fallback: Fal,
|
||||
pub rndr: PhantomData<R>,
|
||||
}
|
||||
|
@ -84,7 +85,7 @@ where
|
|||
let view = match new_match {
|
||||
None => Either::Left(fallback),
|
||||
Some(route) => {
|
||||
route.build_nested_route(&mut outlets, &outer_owner);
|
||||
route.build_nested_route(base, &mut outlets, &outer_owner);
|
||||
outer_owner.with(|| {
|
||||
Either::Right(
|
||||
Outlet(OutletProps::builder().build()).into_any(),
|
||||
|
@ -115,6 +116,7 @@ where
|
|||
}
|
||||
Some(route) => {
|
||||
route.rebuild_nested_route(
|
||||
self.base,
|
||||
&mut 0,
|
||||
&mut state.outlets,
|
||||
&self.outer_owner,
|
||||
|
@ -168,7 +170,7 @@ where
|
|||
type OutletViewFn<R> = Box<dyn FnOnce() -> AnyView<R> + Send>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RouteContext<R>
|
||||
pub(crate) struct RouteContext<R>
|
||||
where
|
||||
R: Renderer,
|
||||
{
|
||||
|
@ -176,6 +178,8 @@ where
|
|||
trigger: ArcTrigger,
|
||||
params: ArcRwSignal<ParamsMap>,
|
||||
owner: Owner,
|
||||
pub matched: ArcRwSignal<String>,
|
||||
base: Option<Oco<'static, str>>,
|
||||
tx: Sender<OutletViewFn<R>>,
|
||||
rx: Arc<Mutex<Option<Receiver<OutletViewFn<R>>>>>,
|
||||
}
|
||||
|
@ -200,6 +204,8 @@ where
|
|||
trigger: self.trigger.clone(),
|
||||
params: self.params.clone(),
|
||||
owner: self.owner.clone(),
|
||||
matched: self.matched.clone(),
|
||||
base: self.base.clone(),
|
||||
tx: self.tx.clone(),
|
||||
rx: self.rx.clone(),
|
||||
}
|
||||
|
@ -212,12 +218,14 @@ where
|
|||
{
|
||||
fn build_nested_route(
|
||||
self,
|
||||
base: Option<Oco<'static, str>>,
|
||||
outlets: &mut Vec<RouteContext<R>>,
|
||||
parent: &Owner,
|
||||
);
|
||||
|
||||
fn rebuild_nested_route(
|
||||
self,
|
||||
base: Option<Oco<'static, str>>,
|
||||
items: &mut usize,
|
||||
outlets: &mut Vec<RouteContext<R>>,
|
||||
parent: &Owner,
|
||||
|
@ -231,6 +239,7 @@ where
|
|||
{
|
||||
fn build_nested_route(
|
||||
self,
|
||||
base: Option<Oco<'static, str>>,
|
||||
outlets: &mut Vec<RouteContext<R>>,
|
||||
parent: &Owner,
|
||||
) {
|
||||
|
@ -243,6 +252,10 @@ where
|
|||
// params, even if there's not a route match change
|
||||
let params = ArcRwSignal::new(self.to_params().into_iter().collect());
|
||||
|
||||
// the matched signal will also be updated on every match
|
||||
// it's used for relative route resolution
|
||||
let matched = ArcRwSignal::new(self.as_matched().to_string());
|
||||
|
||||
// the trigger and channel will be used to send new boxed AnyViews to the Outlet;
|
||||
// whenever we match a different route, the trigger will be triggered and a new view will
|
||||
// be sent through the channel to be rendered by the Outlet
|
||||
|
@ -259,8 +272,10 @@ where
|
|||
trigger,
|
||||
params,
|
||||
owner: owner.clone(),
|
||||
matched: ArcRwSignal::new(self.as_matched().to_string()),
|
||||
tx: tx.clone(),
|
||||
rx: Arc::new(Mutex::new(Some(rx))),
|
||||
base: base.clone(),
|
||||
};
|
||||
outlets.push(outlet.clone());
|
||||
|
||||
|
@ -281,12 +296,13 @@ where
|
|||
// this is important because to build the view, we need access to the outlet
|
||||
// and the outlet will be returned from building this child
|
||||
if let Some(child) = child {
|
||||
child.build_nested_route(outlets, &owner);
|
||||
child.build_nested_route(base, outlets, &owner);
|
||||
}
|
||||
}
|
||||
|
||||
fn rebuild_nested_route(
|
||||
self,
|
||||
base: Option<Oco<'static, str>>,
|
||||
items: &mut usize,
|
||||
outlets: &mut Vec<RouteContext<R>>,
|
||||
parent: &Owner,
|
||||
|
@ -295,7 +311,7 @@ where
|
|||
match current {
|
||||
// if there's nothing currently in the routes at this point, build from here
|
||||
None => {
|
||||
self.build_nested_route(outlets, parent);
|
||||
self.build_nested_route(base, outlets, parent);
|
||||
}
|
||||
Some(current) => {
|
||||
// a unique ID for each route, which allows us to compare when we get new matches
|
||||
|
@ -304,11 +320,20 @@ where
|
|||
let id = self.as_id();
|
||||
|
||||
// whether the route is the same or different, we always need to
|
||||
// 1) update the params, and
|
||||
// 1) update the params (if they've changed),
|
||||
// 2) update the matched path (if it's changed),
|
||||
// 2) access the view and children
|
||||
current
|
||||
.params
|
||||
.set(self.to_params().into_iter().collect::<ParamsMap>());
|
||||
|
||||
let new_params =
|
||||
self.to_params().into_iter().collect::<ParamsMap>();
|
||||
if current.params.read() != new_params {
|
||||
current.params.set(new_params);
|
||||
}
|
||||
let new_match = self.as_matched();
|
||||
if &*current.matched.read() != new_match {
|
||||
current.matched.set(new_match);
|
||||
}
|
||||
|
||||
let (view, child) = self.into_view_and_child();
|
||||
|
||||
// if the IDs don't match, everything below in the tree needs to be swapped:
|
||||
|
@ -345,7 +370,11 @@ where
|
|||
// if this children has matches, then rebuild the lower section of the tree
|
||||
if let Some(child) = child {
|
||||
let mut new_outlets = Vec::new();
|
||||
child.build_nested_route(&mut new_outlets, &owner);
|
||||
child.build_nested_route(
|
||||
base,
|
||||
&mut new_outlets,
|
||||
&owner,
|
||||
);
|
||||
outlets.extend(new_outlets);
|
||||
}
|
||||
|
||||
|
@ -357,7 +386,7 @@ where
|
|||
if let Some(child) = child {
|
||||
let owner = current.owner.clone();
|
||||
*items += 1;
|
||||
child.rebuild_nested_route(items, outlets, &owner);
|
||||
child.rebuild_nested_route(base, items, outlets, &owner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -401,6 +430,7 @@ where
|
|||
owner,
|
||||
tx,
|
||||
rx,
|
||||
..
|
||||
} = ctx;
|
||||
let rx = rx.lock().or_poisoned().take().expect(
|
||||
"Tried to render <Outlet/> but could not find the view receiver. Are \
|
||||
|
|
|
@ -323,11 +323,16 @@ where
|
|||
let (el, prev) = state;
|
||||
match (self, prev.as_mut()) {
|
||||
(None, None) => {}
|
||||
(None, Some(_)) => R::remove_attribute(el, key),
|
||||
(None, Some(_)) => {
|
||||
R::remove_attribute(el, key);
|
||||
*prev = None;
|
||||
}
|
||||
(Some(value), None) => {
|
||||
*prev = Some(value.build(el, key));
|
||||
}
|
||||
(Some(new), Some(old)) => new.rebuild(key, old),
|
||||
(Some(new), Some(old)) => {
|
||||
new.rebuild(key, old);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -150,6 +150,44 @@ macro_rules! prop_type {
|
|||
*prev = value;
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> IntoProperty<R> for Option<$prop_type>
|
||||
where
|
||||
R: DomRenderer,
|
||||
{
|
||||
type State = (R::Element, JsValue);
|
||||
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
self,
|
||||
el: &R::Element,
|
||||
key: &str,
|
||||
) -> Self::State {
|
||||
let was_some = self.is_some();
|
||||
let value = self.into();
|
||||
if was_some {
|
||||
R::set_property(el, key, &value);
|
||||
}
|
||||
(el.clone(), value)
|
||||
}
|
||||
|
||||
fn build(self, el: &R::Element, key: &str) -> Self::State {
|
||||
let was_some = self.is_some();
|
||||
let value = self.into();
|
||||
if was_some {
|
||||
R::set_property(el, key, &value);
|
||||
}
|
||||
(el.clone(), value)
|
||||
}
|
||||
|
||||
fn rebuild(self, state: &mut Self::State, key: &str) {
|
||||
let (el, prev) = state;
|
||||
let value = self.into();
|
||||
if value != *prev {
|
||||
R::set_property(el, key, &value);
|
||||
}
|
||||
*prev = value;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
use super::RenderEffectState;
|
||||
use crate::{html::class::IntoClass, renderer::DomRenderer};
|
||||
use reactive_graph::{effect::RenderEffect, signal::guards::ReadGuard};
|
||||
use std::{borrow::Borrow, ops::Deref};
|
||||
use std::{
|
||||
borrow::{Borrow, Cow},
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
impl<F, C, R> IntoClass<R> for F
|
||||
where
|
||||
|
@ -148,6 +151,92 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<F, T, R> IntoClass<R> for (Vec<Cow<'static, str>>, F)
|
||||
where
|
||||
F: FnMut() -> T + Send + 'static,
|
||||
T: Borrow<bool>,
|
||||
R: DomRenderer,
|
||||
{
|
||||
type State = RenderEffectState<(R::ClassList, bool)>;
|
||||
|
||||
fn html_len(&self) -> usize {
|
||||
self.0.iter().map(|n| n.len()).sum()
|
||||
}
|
||||
|
||||
fn to_html(self, class: &mut String) {
|
||||
let (names, mut f) = self;
|
||||
let include = *f().borrow();
|
||||
if include {
|
||||
for name in names {
|
||||
<&str as IntoClass<R>>::to_html(&name, class);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn hydrate<const FROM_SERVER: bool>(self, el: &R::Element) -> Self::State {
|
||||
// TODO FROM_SERVER vs template
|
||||
let (names, mut f) = self;
|
||||
let class_list = R::class_list(el);
|
||||
|
||||
RenderEffect::new(move |prev: Option<(R::ClassList, bool)>| {
|
||||
let include = *f().borrow();
|
||||
if let Some((class_list, prev)) = prev {
|
||||
if include {
|
||||
if !prev {
|
||||
for name in &names {
|
||||
// TODO multi-class optimizations here
|
||||
R::add_class(&class_list, name);
|
||||
}
|
||||
}
|
||||
} else if prev {
|
||||
for name in &names {
|
||||
R::remove_class(&class_list, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
(class_list.clone(), include)
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
fn build(self, el: &R::Element) -> Self::State {
|
||||
let (names, mut f) = self;
|
||||
let class_list = R::class_list(el);
|
||||
|
||||
RenderEffect::new(move |prev: Option<(R::ClassList, bool)>| {
|
||||
let include = *f().borrow();
|
||||
match prev {
|
||||
Some((class_list, prev)) => {
|
||||
if include {
|
||||
for name in &names {
|
||||
if !prev {
|
||||
R::add_class(&class_list, name);
|
||||
}
|
||||
}
|
||||
} else if prev {
|
||||
for name in &names {
|
||||
R::remove_class(&class_list, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if include {
|
||||
for name in &names {
|
||||
R::add_class(&class_list, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(class_list.clone(), include)
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
fn rebuild(self, _state: &mut Self::State) {
|
||||
// TODO rebuild?
|
||||
}
|
||||
}
|
||||
|
||||
impl<G, R> IntoClass<R> for ReadGuard<String, G>
|
||||
where
|
||||
G: Deref<Target = String> + Send,
|
||||
|
|
Loading…
Reference in New Issue