This commit is contained in:
Greg Johnston 2024-02-10 14:18:56 -05:00
parent c8441f0f00
commit 17732a6e6a
11 changed files with 471 additions and 111 deletions

View File

@ -18,6 +18,8 @@ leptos_server = { workspace = true }
leptos_config = { workspace = true }
leptos-spin-macro = { version = "0.2", optional = true }
tracing = "0.1"
reactive_graph = { workspace = true }
tachys = { workspace = true, features = ["reactive_graph"] }
typed-builder = "0.18"
typed-builder-macro = "0.18"
serde = { version = "1", optional = true }
@ -66,6 +68,7 @@ nightly = [
"leptos_macro/nightly",
"leptos_reactive/nightly",
"leptos_server/nightly",
"tachys/nightly",
]
serde = ["leptos_reactive/serde"]
serde-lite = ["leptos_reactive/serde-lite"]

View File

@ -1,20 +1,26 @@
use leptos_dom::Fragment;
use std::rc::Rc;
use std::sync::Arc;
use tachys::{
renderer::dom::Dom,
view::{
any_view::{AnyView, IntoAny},
RenderHtml,
},
};
/// The most common type for the `children` property on components,
/// which can only be called once.
pub type Children = Box<dyn FnOnce() -> Fragment>;
pub type Children = Box<dyn FnOnce() -> AnyView<Dom>>;
/// A type for the `children` property on components that can be called
/// more than once.
pub type ChildrenFn = Rc<dyn Fn() -> Fragment>;
pub type ChildrenFn = Arc<dyn Fn() -> AnyView<Dom>>;
/// 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() -> Fragment>;
pub type ChildrenFnMut = Box<dyn FnMut() -> AnyView<Dom>>;
// This is to still support components that accept `Box<dyn Fn() -> Fragment>` as a children.
type BoxedChildrenFn = Box<dyn Fn() -> Fragment>;
// This is to still support components that accept `Box<dyn Fn() -> AnyView>` as a children.
type BoxedChildrenFn = Box<dyn Fn() -> AnyView<Dom>>;
/// This trait can be used when constructing a component that takes children without needing
/// to know exactly what children type the component expects. This is used internally by the
@ -93,42 +99,74 @@ pub trait ToChildren<F> {
fn to_children(f: F) -> Self;
}
impl<F> ToChildren<F> for Children
impl<F, C> ToChildren<F> for Children
where
F: FnOnce() -> Fragment + 'static,
F: FnOnce() -> C + 'static,
C: RenderHtml<Dom> + 'static,
{
#[inline]
fn to_children(f: F) -> Self {
Box::new(f)
Box::new(move || f().into_any())
}
}
impl<F> ToChildren<F> for ChildrenFn
impl<F, C> ToChildren<F> for ChildrenFn
where
F: Fn() -> Fragment + 'static,
F: Fn() -> C + 'static,
C: RenderHtml<Dom> + 'static,
{
#[inline]
fn to_children(f: F) -> Self {
Rc::new(f)
Arc::new(move || f().into_any())
}
}
impl<F> ToChildren<F> for ChildrenFnMut
impl<F, C> ToChildren<F> for ChildrenFnMut
where
F: FnMut() -> Fragment + 'static,
F: Fn() -> C + 'static,
C: RenderHtml<Dom> + 'static,
{
#[inline]
fn to_children(f: F) -> Self {
Box::new(f)
Box::new(move || f().into_any())
}
}
impl<F> ToChildren<F> for BoxedChildrenFn
impl<F, C> ToChildren<F> for BoxedChildrenFn
where
F: Fn() -> Fragment + 'static,
F: Fn() -> C + 'static,
C: RenderHtml<Dom> + 'static,
{
#[inline]
fn to_children(f: F) -> Self {
Box::new(f)
Box::new(move || f().into_any())
}
}
/// New-type wrapper for the a function that returns a view with `From` and `Default` traits implemented
/// to enable optional props in for example `<Show>` and `<Suspense>`.
#[derive(Clone)]
pub struct ViewFn(Arc<dyn Fn() -> AnyView<Dom>>);
impl Default for ViewFn {
fn default() -> Self {
Self(Arc::new(|| ().into_any()))
}
}
impl<F, C> From<F> for ViewFn
where
F: Fn() -> C + 'static,
C: RenderHtml<Dom> + 'static,
{
fn from(value: F) -> Self {
Self(Arc::new(move || value().into_any()))
}
}
impl ViewFn {
/// Execute the wrapped function
pub fn run(&self) -> AnyView<Dom> {
(self.0)()
}
}

83
leptos/src/component.rs Normal file
View File

@ -0,0 +1,83 @@
//! Utility traits and functions that allow building components,
//! as either functions of their props or functions with no arguments,
//! without knowing the name of the props struct.
pub trait Component<P> {}
pub trait Props {
type Builder;
fn builder() -> Self::Builder;
}
#[doc(hidden)]
pub trait PropsOrNoPropsBuilder {
type Builder;
fn builder_or_not() -> Self::Builder;
}
#[doc(hidden)]
#[derive(Copy, Clone, Debug, Default)]
pub struct EmptyPropsBuilder {}
impl EmptyPropsBuilder {
pub fn build(self) {}
}
impl<P: Props> PropsOrNoPropsBuilder for P {
type Builder = <P as Props>::Builder;
fn builder_or_not() -> Self::Builder {
Self::builder()
}
}
impl PropsOrNoPropsBuilder for EmptyPropsBuilder {
type Builder = EmptyPropsBuilder;
fn builder_or_not() -> Self::Builder {
EmptyPropsBuilder {}
}
}
impl<F, R> Component<EmptyPropsBuilder> for F where F: FnOnce() -> R {}
impl<P, F, R> Component<P> for F
where
F: FnOnce(P) -> R,
P: Props,
{
}
pub fn component_props_builder<P: PropsOrNoPropsBuilder>(
_f: &impl Component<P>,
) -> <P as PropsOrNoPropsBuilder>::Builder {
<P as PropsOrNoPropsBuilder>::builder_or_not()
}
pub fn component_view<P, T>(f: impl ComponentConstructor<P, T>, props: P) -> T {
f.construct(props)
}
pub trait ComponentConstructor<P, T> {
fn construct(self, props: P) -> T;
}
impl<Func, T> ComponentConstructor<(), T> for Func
where
Func: FnOnce() -> T,
{
fn construct(self, (): ()) -> T {
(self)()
}
}
impl<Func, T, P> ComponentConstructor<P, T> for Func
where
Func: FnOnce(P) -> T,
P: PropsOrNoPropsBuilder,
{
fn construct(self, props: P) -> T {
(self)(props)
}
}

View File

@ -1,6 +1,9 @@
use leptos_dom::IntoView;
use leptos_macro::component;
use std::hash::Hash;
use std::{hash::Hash, marker::PhantomData};
use tachys::{
renderer::Renderer,
view::{keyed::keyed, RenderHtml},
};
/// Iterates over children and displays them, keyed by the `key` function given.
///
@ -38,64 +41,60 @@ use std::hash::Hash;
/// }
/// }
/// ```
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all)
)]
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
#[component(transparent)]
pub fn For<IF, I, T, EF, N, KF, K>(
pub fn For<Rndr, IF, I, T, EF, N, KF, K>(
/// Items over which the component should iterate.
each: IF,
/// A key function that will be applied to each item.
key: KF,
/// A function that takes the item, and returns the view that will be displayed for each item.
///
/// ## Syntax
/// This can be passed directly in the `view` children of the `<For/>` by using the
/// `let:` syntax to specify the name for the data variable passed in the argument.
///
/// ```rust
/// # use leptos::*;
/// # if false {
/// let (data, set_data) = create_signal(vec![0, 1, 2]);
/// view! {
/// <For
/// each=move || data.get()
/// key=|n| *n
/// // stores the item in each row in a variable named `data`
/// let:data
/// >
/// <p>{data}</p>
/// </For>
/// }
/// # ;
/// # }
/// ```
/// is the same as
/// ```rust
/// # use leptos::*;
/// # if false {
/// let (data, set_data) = create_signal(vec![0, 1, 2]);
/// view! {
/// <For
/// each=move || data.get()
/// key=|n| *n
/// children=|data| view! { <p>{data}</p> }
/// />
/// }
/// # ;
/// # }
/// ```
children: EF,
) -> impl IntoView
#[prop(optional)] _rndr: PhantomData<Rndr>,
) -> impl RenderHtml<Rndr>
where
IF: Fn() -> I + 'static,
I: IntoIterator<Item = T>,
EF: Fn(T) -> N + 'static,
N: IntoView + 'static,
KF: Fn(&T) -> K + 'static,
EF: Fn(T) -> N + Clone + 'static,
N: RenderHtml<Rndr> + 'static,
KF: Fn(&T) -> K + Clone + 'static,
K: Eq + Hash + 'static,
T: 'static,
Rndr: Renderer + 'static,
Rndr::Node: Clone,
Rndr::Element: Clone,
{
leptos_dom::Each::new(each, key, children).into_view()
move || keyed(each(), key.clone(), children.clone())
}
#[cfg(test)]
mod tests {
use crate::For;
use leptos_macro::view;
use reactive_graph::{signal::RwSignal, signal_traits::SignalGet};
use tachys::{
html::element::HtmlElement, prelude::ElementChild,
renderer::mock_dom::MockDom, view::Render,
};
#[test]
fn creates_list() {
let values = RwSignal::new(vec![1, 2, 3, 4, 5]);
let list: HtmlElement<_, _, _, MockDom> = view! {
<ol>
<For
each=move || values.get()
key=|i| *i
let:i
>
<li>{i}</li>
</For>
</ol>
};
let list = list.build();
assert_eq!(
list.el.to_debug_html(),
"<ol><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li></ol>"
);
}
}

View File

@ -0,0 +1,8 @@
(function (pkg_path, output_name, wasm_output_name) {
import(`/${pkg_path}/${output_name}.js`)
.then(mod => {
mod.default(`/${pkg_path}/${wasm_output_name}.wasm`).then(() => {
mod.hydrate();
});
})
})

View File

@ -0,0 +1,26 @@
(function (pkg_path, output_name, wasm_output_name) {
function idle(c) {
if ("requestIdleCallback" in window) {
window.requestIdleCallback(c);
} else {
c();
}
}
idle(() => {
import(`/${pkg_path}/${output_name}.js`)
.then(mod => {
mod.default(`/${pkg_path}/${wasm_output_name}.wasm`).then(() => {
mod.hydrate();
for (let e of document.querySelectorAll("leptos-island")) {
const l = e.dataset.component;
const islandFn = mod["_island_" + l];
if (islandFn) {
islandFn(e);
} else {
console.warn(`Could not find WASM function for the island ${l}.`);
}
}
});
})
});
})

View File

@ -0,0 +1,58 @@
#![allow(clippy::needless_lifetimes)]
use crate::prelude::*;
use leptos_config::LeptosOptions;
use leptos_macro::{component, view};
use tachys::view::RenderHtml;
#[component]
pub fn AutoReload<'a>(
#[prop(optional)] disable_watch: bool,
#[prop(optional)] nonce: Option<&'a str>,
options: LeptosOptions,
) -> impl RenderHtml<Dom> + 'a {
(!disable_watch && std::env::var("LEPTOS_WATCH").is_ok()).then(|| {
let reload_port = match options.reload_external_port {
Some(val) => val,
None => options.reload_port,
};
let protocol = match options.reload_ws_protocol {
leptos_config::ReloadWSProtocol::WS => "'ws://'",
leptos_config::ReloadWSProtocol::WSS => "'wss://'",
};
let script = include_str!("reload_script.js");
view! {
<script crossorigin=nonce>
{format!("{script}({reload_port:?}, {protocol})")}
</script>
}
})
}
#[component]
pub fn HydrationScripts(
options: LeptosOptions,
#[prop(optional)] islands: bool,
) -> impl RenderHtml<Dom> {
let pkg_path = &options.site_pkg_dir;
let output_name = &options.output_name;
let mut wasm_output_name = output_name.clone();
if std::option_env!("LEPTOS_OUTPUT_NAME").is_none() {
wasm_output_name.push_str("_bg");
}
let nonce = None::<String>; // use_nonce(); // TODO
let script = if islands {
include_str!("./island_script.js")
} else {
include_str!("./hydration_script.js")
};
view! {
<link rel="modulepreload" href=format!("/{pkg_path}/{output_name}.js") nonce=nonce.clone()/>
<link rel="preload" href=format!("/{pkg_path}/{wasm_output_name}.wasm") r#as="fetch" r#type="application/wasm" crossorigin=nonce.clone().unwrap_or_default()/>
<script type="module" nonce=nonce>
{format!("{script}({pkg_path:?}, {output_name:?}, {wasm_output_name:?})")}
</script>
}
}

View File

@ -0,0 +1,23 @@
(function (reload_port, protocol) {
let host = window.location.hostname;
let ws = new WebSocket(`${protocol}${host}:${reload_port}/live_reload`);
ws.onmessage = (ev) => {
let msg = JSON.parse(ev.data);
if (msg.all) window.location.reload();
if (msg.css) {
let found = false;
document.querySelectorAll("link").forEach((link) => {
if (link.getAttribute('href').includes(msg.css)) {
let newHref = '/' + msg.css + '?version=' + new Date().getMilliseconds();
link.setAttribute('href', newHref);
found = true;
}
});
if (!found) console.warn(`CSS hot-reload: Could not find a <link href=/\"${msg.css}\"> element`);
};
if(msg.view) {
patch(msg.view);
}
};
ws.onclose = () => console.warn('Live-reload stopped. Manual reload necessary.');
})

120
leptos/src/into_view.rs Normal file
View File

@ -0,0 +1,120 @@
use tachys::{
hydration::Cursor,
renderer::dom::Dom,
ssr::StreamBuilder,
view::{Mountable, Position, PositionState, Render, RenderHtml},
};
pub struct View<T>(T);
pub trait IntoView: Sized {
fn into_view(self) -> View<Self>;
}
impl<T: Render<Dom> + RenderHtml<Dom>> IntoView for T {
fn into_view(self) -> View<Self> {
View(self)
}
}
impl<T: Render<Dom>> Render<Dom> for View<T> {
type State = T::State;
fn build(self) -> Self::State {
self.0.build()
}
fn rebuild(self, state: &mut Self::State) {
self.0.rebuild(state)
}
}
impl<T: RenderHtml<Dom>> RenderHtml<Dom> for View<T> {
const MIN_LENGTH: usize = <T as RenderHtml<Dom>>::MIN_LENGTH;
fn to_html_with_buf(self, buf: &mut String, position: &mut Position) {
self.0.to_html_with_buf(buf, position);
}
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
self,
buf: &mut StreamBuilder,
position: &mut Position,
) where
Self: Sized,
{
self.0.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position)
}
fn hydrate<const FROM_SERVER: bool>(
self,
cursor: &Cursor<Dom>,
position: &PositionState,
) -> Self::State {
self.0.hydrate::<FROM_SERVER>(cursor, position)
}
}
/*pub trait IntoView {
const MIN_HTML_LENGTH: usize;
type State: Mountable<Dom>;
fn build(self) -> Self::State;
fn rebuild(self, state: &mut Self::State);
fn to_html_with_buf(self, buf: &mut String, position: &mut Position);
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
self,
buf: &mut StreamBuilder,
position: &mut Position,
);
fn hydrate<const FROM_SERVER: bool>(
self,
cursor: &Cursor<Dom>,
position: &PositionState,
) -> Self::State;
}
impl<T: RenderHtml<Dom>> IntoView for T {}
impl<T: IntoView> Render<Dom> for T {
type State = <Self as IntoView>::State;
fn build(self) -> Self::State {
IntoView::build(self)
}
fn rebuild(self, state: &mut Self::State) {
IntoView::rebuild(self, state);
}
}
impl<T: IntoView> RenderHtml<Dom> for T {
const MIN_LENGTH: usize = T::MIN_HTML_LENGTH;
fn to_html_with_buf(self, buf: &mut String, position: &mut Position) {
IntoView::to_html_with_buf(self, buf, position);
}
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
self,
buf: &mut StreamBuilder,
position: &mut Position,
) where
Self: Sized,
{
IntoView::to_html_async_with_buf::<OUT_OF_ORDER>(self, buf, position);
}
fn hydrate<const FROM_SERVER: bool>(
self,
cursor: &Cursor<Dom>,
position: &PositionState,
) -> Self::State {
IntoView::hydrate::<FROM_SERVER>(self, cursor, position)
}
}*/

View File

@ -1,4 +1,4 @@
#![deny(missing_docs)]
//#![deny(missing_docs)] // TODO restore
#![forbid(unsafe_code)]
//! # About Leptos
//!
@ -139,7 +139,34 @@
//! # }
//! ```
mod additional_attributes;
extern crate self as leptos;
pub mod prelude {
pub use reactive_graph::prelude::*;
pub use tachys::prelude::*;
}
pub mod children;
pub mod component;
mod for_loop;
mod hydration_scripts;
mod show;
pub use for_loop::*;
pub use hydration_scripts::*;
pub use leptos_macro::*;
pub use reactive_graph::{
self,
signal::{arc_signal, create_signal, signal},
};
pub use show::*;
#[doc(hidden)]
pub use typed_builder;
#[doc(hidden)]
pub use typed_builder_macro;
mod into_view;
pub use into_view::IntoView;
/*mod additional_attributes;
pub use additional_attributes::*;
mod await_;
pub use await_::*;
@ -209,10 +236,9 @@ pub use serde;
#[cfg(feature = "experimental-islands")]
pub use serde_json;
pub use show::*;
pub use suspense_component::*;
mod suspense_component;
mod transition;
//pub use suspense_component::*;
//mod suspense_component;
//mod transition;
#[cfg(any(debug_assertions, feature = "ssr"))]
#[doc(hidden)]
pub use tracing;
@ -373,4 +399,4 @@ where
fn construct(self, props: P) -> View {
(self)(props).into_view()
}
}
}*/

View File

@ -1,35 +1,11 @@
use leptos::{component, ChildrenFn, ViewFn};
use leptos_dom::IntoView;
use leptos_reactive::signal_prelude::*;
use crate::children::{ChildrenFn, ViewFn};
use leptos_macro::component;
use reactive_graph::{computed::ArcMemo, traits::Get};
use tachys::{
renderer::dom::Dom,
view::{either::Either, RenderHtml},
};
/// A component that will show its children when the `when` condition is `true`,
/// and show the fallback when it is `false`, without rerendering every time
/// the condition changes.
///
/// The fallback prop is optional and defaults to rendering nothing.
///
/// ```rust
/// # use leptos_reactive::*;
/// # use leptos_macro::*;
/// # use leptos_dom::*; use leptos::*;
/// # let runtime = create_runtime();
/// let (value, set_value) = create_signal(0);
///
/// view! {
/// <Show
/// when=move || value.get() < 5
/// fallback=|| view! { "Big number!" }
/// >
/// "Small number!"
/// </Show>
/// }
/// # ;
/// # runtime.dispose();
/// ```
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all)
)]
#[component]
pub fn Show<W>(
/// The children will be shown whenever the condition in the `when` closure returns `true`.
@ -39,14 +15,14 @@ pub fn Show<W>(
/// A closure that returns what gets rendered if the when statement is false. By default this is the empty view.
#[prop(optional, into)]
fallback: ViewFn,
) -> impl IntoView
) -> impl RenderHtml<Dom>
where
W: Fn() -> bool + 'static,
W: Fn() -> bool + Send + Sync + 'static,
{
let memoized_when = create_memo(move |_| when());
let memoized_when = ArcMemo::new(move |_| when());
move || match memoized_when.get() {
true => children().into_view(),
false => fallback.run(),
true => Either::Left(children()),
false => Either::Right(fallback.run()),
}
}