Merge branch 'leptos_dom_v2' of https://github.com/jquesada2016/leptos into leptos_dom_v2

This commit is contained in:
Jose Quesada 2022-12-28 10:52:06 -06:00
commit 4340fbfc78
31 changed files with 240 additions and 243 deletions

View file

@ -1,7 +1,7 @@
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
use leptos::{wasm_bindgen::JsValue, *};
use leptos::*;
use web_sys::HtmlElement;
use counters::{Counters, CountersProps};

View file

@ -1,5 +1,3 @@
use std::time::Duration;
use leptos::*;
use serde::{Deserialize, Serialize};

View file

@ -7,28 +7,31 @@ edition = "2021"
crate-type = ["cdylib", "rlib"]
[dependencies]
anyhow = "1"
console_log = "0.2"
console_error_panic_hook = "0.1"
futures = "0.3"
cfg-if = "1"
anyhow = "1.0.66"
console_log = "0.2.0"
console_error_panic_hook = "0.1.7"
futures = "0.3.25"
cfg-if = "1.0.0"
leptos = { path = "../../leptos", default-features = false, features = [
"serde",
] }
leptos_axum = { path = "../../integrations/axum", optional = true }
leptos_meta = { path = "../../meta", default-features = false }
leptos_router = { path = "../../router", default-features = false }
log = "0.4"
simple_logger = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
gloo-net = { version = "0.2", features = ["http"] }
reqwest = { version = "0.11", features = ["json"] }
axum = { version = "0.5.17", optional = true }
log = "0.4.17"
simple_logger = "4.0.0"
serde = { version = "1.0.148", features = ["derive"] }
serde_json = "1.0.89"
gloo-net = { version = "0.2.5", features = ["http"] }
reqwest = { version = "0.11.13", features = ["json"] }
axum = { version = "0.6.1", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.3.4", features = ["fs"], optional = true }
tokio = { version = "1.0", features = ["full"], optional = true }
tokio = { version = "1.22.0", features = ["full"], optional = true }
http = { version = "0.2.8", optional = true }
web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] }
wasm-bindgen = "0.2"
tracing = "0.1"
[features]
default = ["csr"]

View file

@ -9,7 +9,7 @@ app into one CRS bundle
## Server Side Rendering With Hydration
To run it as a server side app with hydration, first you should run
```bash
wasm-pack build --target=web --no-default-features --features=hydrate
wasm-pack build --target=web --debug --no-default-features --features=hydrate
```
to generate the Webassembly to provide hydration features for the server.
Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration.

View file

@ -1,4 +1,4 @@
use leptos::Serializable;
use leptos::{on_cleanup, Scope, Serializable};
use serde::{Deserialize, Serialize};
pub fn story(path: &str) -> String {
@ -10,11 +10,15 @@ pub fn user(path: &str) -> String {
}
#[cfg(not(feature = "ssr"))]
pub async fn fetch_api<T>(path: &str) -> Option<T>
pub async fn fetch_api<T>(cx: Scope, path: &str) -> Option<T>
where
T: Serializable,
{
let abort_controller = web_sys::AbortController::new().ok();
let abort_signal = abort_controller.as_ref().map(|a| a.signal());
let json = gloo_net::http::Request::get(path)
.abort_signal(abort_signal.as_ref())
.send()
.await
.map_err(|e| log::error!("{e}"))
@ -22,11 +26,19 @@ where
.text()
.await
.ok()?;
// abort in-flight requests if the Scope is disposed
// i.e., if we've navigated away from this page
on_cleanup(cx, move || {
if let Some(abort_controller) = abort_controller {
abort_controller.abort()
}
});
T::from_json(&json).ok()
}
#[cfg(feature = "ssr")]
pub async fn fetch_api<T>(path: &str) -> Option<T>
pub async fn fetch_api<T>(cx: Scope, path: &str) -> Option<T>
where
T: Serializable,
{

View file

@ -1,5 +1,5 @@
use cfg_if::cfg_if;
use leptos::*;
use leptos::{component, Scope, IntoView, provide_context, view};
use leptos_meta::*;
use leptos_router::*;
mod api;
@ -11,13 +11,12 @@ use routes::story::*;
use routes::users::*;
#[component]
pub fn App(cx: Scope) -> Element {
provide_context(cx, MetaContext::default());
pub fn App(cx: Scope) -> impl IntoView {
view! {
cx,
<div>
<Stylesheet href="/static/style.css"/>
<>
<Stylesheet href="/style.css"/>
<Meta name="description" content="Leptos implementation of a HackerNews demo."/>
<Router>
<Nav />
<main>
@ -28,7 +27,7 @@ pub fn App(cx: Scope) -> Element {
</Routes>
</main>
</Router>
</div>
</>
}
}
@ -41,7 +40,7 @@ cfg_if! {
pub fn hydrate() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::hydrate(body().unwrap(), move |cx| {
leptos::mount_to_body(move |cx| {
view! { cx, <App/> }
});
}

View file

@ -4,32 +4,47 @@ use leptos::*;
// boilerplate to run in different modes
cfg_if! {
if #[cfg(feature = "ssr")] {
// use actix_files::{Files, NamedFile};
// use actix_web::*;
use axum::{
routing::{get},
Router,
handler::Handler,
error_handling::HandleError,
};
use http::StatusCode;
use std::net::SocketAddr;
use leptos_hackernews_axum::handlers::{file_handler, get_static_file_handler};
use tower_http::services::ServeDir;
use std::env;
#[tokio::main]
async fn main() {
use leptos_hackernews_axum::*;
let addr = SocketAddr::from(([127, 0, 0, 1], 8082));
let addr = SocketAddr::from(([127, 0, 0, 1], 3002));
log::debug!("serving at {addr}");
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
// These are Tower Services that will serve files from the static and pkg repos.
// HandleError is needed as Axum requires services to implement Infallible Errors
// because all Errors are converted into Responses
let static_service = HandleError::new( ServeDir::new("./static"), handle_file_error);
let pkg_service =HandleError::new( ServeDir::new("./pkg"), handle_file_error);
/// Convert the Errors from ServeDir to a type that implements IntoResponse
async fn handle_file_error(err: std::io::Error) -> (StatusCode, String) {
(
StatusCode::NOT_FOUND,
format!("File Not Found: {}", err),
)
}
let render_options: RenderOptions = RenderOptions::builder().pkg_path("/pkg/leptos_hackernews_axum").socket_address(addr).reload_port(3001).environment(&env::var("RUST_ENV")).build();
render_options.write_to_file();
// build our application with a route
let app = Router::new()
// `GET /` goes to `root`
.nest("/pkg", get(file_handler))
.nest("/static", get(get_static_file_handler))
.fallback(leptos_axum::render_app_to_stream("leptos_hackernews_axum", |cx| view! { cx, <App/> }).into_service());
.nest_service("/pkg", pkg_service)
.nest_service("/static", static_service)
.fallback(leptos_axum::render_app_to_stream(render_options, |cx| view! { cx, <App/> }));
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`

View file

@ -1,8 +1,8 @@
use leptos::*;
use leptos::{component, Scope, IntoView, view};
use leptos_router::*;
#[component]
pub fn Nav(cx: Scope) -> Element {
pub fn Nav(cx: Scope) -> impl IntoView {
view! { cx,
<header class="header">
<nav class="inner">

View file

@ -14,7 +14,7 @@ fn category(from: &str) -> &'static str {
}
#[component]
pub fn Stories(cx: Scope) -> Element {
pub fn Stories(cx: Scope) -> impl IntoView {
let query = use_query_map(cx);
let params = use_params_map(cx);
let page = move || {
@ -32,11 +32,13 @@ pub fn Stories(cx: Scope) -> Element {
move || (page(), story_type()),
move |(page, story_type)| async move {
let path = format!("{}?page={}", category(&story_type), page);
api::fetch_api::<Vec<api::Story>>(&api::story(&path)).await
api::fetch_api::<Vec<api::Story>>(cx, &api::story(&path)).await
},
);
let (pending, set_pending) = create_signal(cx, false);
let hide_more_link = move || stories.read().unwrap_or(None).unwrap_or_default().len() < 28;
let hide_more_link =
move || pending() || stories.read().unwrap_or(None).unwrap_or_default().len() < 28;
view! {
cx,
@ -52,14 +54,14 @@ pub fn Stories(cx: Scope) -> Element {
>
"< prev"
</a>
}
}.into_any()
} else {
view! {
cx,
<span class="page-link disabled" aria-hidden="true">
"< prev"
</span>
}
}.into_any()
}}
</span>
<span>"page " {page}</span>
@ -76,25 +78,30 @@ pub fn Stories(cx: Scope) -> Element {
</div>
<main class="news-list">
<div>
<Suspense fallback=view! { cx, <p>"Loading..."</p> }>
<Transition
fallback=move || view! { cx, <p>"Loading..."</p> }
set_pending=set_pending.into()
>
{move || match stories.read() {
None => None,
Some(None) => Some(view! { cx, <p>"Error loading stories."</p> }),
Some(None) => Some(view! { cx, <p>"Error loading stories."</p> }.into_any()),
Some(Some(stories)) => {
Some(view! { cx,
<ul>
<For each=move || stories.clone() key=|story| story.id>{
move |cx: Scope, story: &api::Story| {
<For
each=move || stories.clone()
key=|story| story.id
view=move |story: api::Story| {
view! { cx,
<Story story=story.clone() />
<Story story/>
}
}
}</For>
/>
</ul>
})
}.into_any())
}
}}
</Suspense>
</Transition>
</div>
</main>
</div>
@ -102,7 +109,7 @@ pub fn Stories(cx: Scope) -> Element {
}
#[component]
fn Story(cx: Scope, story: api::Story) -> Element {
fn Story(cx: Scope, story: api::Story) -> impl IntoView {
view! { cx,
<li class="news-item">
<span class="score">{story.points}</span>
@ -115,10 +122,10 @@ fn Story(cx: Scope, story: api::Story) -> Element {
</a>
<span class="host">"("{story.domain}")"</span>
</span>
}
}.into_view(cx)
} else {
let title = story.title.clone();
view! { cx, <A href=format!("/stories/{}", story.id)>{title.clone()}</A> }
view! { cx, <A href=format!("/stories/{}", story.id)>{title.clone()}</A> }.into_view(cx)
}}
</span>
<br />
@ -137,17 +144,15 @@ fn Story(cx: Scope, story: api::Story) -> Element {
}}
</A>
</span>
}
}.into_view(cx)
} else {
let title = story.title.clone();
view! { cx, <A href=format!("/item/{}", story.id)>{title.clone()}</A> }
view! { cx, <A href=format!("/item/{}", story.id)>{title.clone()}</A> }.into_view(cx)
}}
</span>
{(story.story_type != "link").then(|| view! { cx,
<span>
//{" "}
<span class="label">{story.story_type}</span>
</span>
" "
<span class="label">{story.story_type}</span>
})}
</li>
}

View file

@ -1,18 +1,27 @@
use crate::api;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
#[component]
pub fn Story(cx: Scope) -> Element {
pub fn Story(cx: Scope) -> impl IntoView {
let params = use_params_map(cx);
let story = create_resource(
cx,
move || params().get("id").cloned().unwrap_or_default(),
move |id| async move { api::fetch_api::<api::Story>(&api::story(&format!("item/{id}"))).await },
move |id| async move {
if id.is_empty() {
None
} else {
api::fetch_api::<api::Story>(cx, &api::story(&format!("item/{id}"))).await
}
},
);
let meta_description = move || story.read().and_then(|story| story.map(|story| story.title.clone())).unwrap_or_else(|| "Loading story...".to_string());
view! { cx,
<div>
<>
<Meta name="description" content=meta_description/>
{move || story.read().map(|story| match story {
None => view! { cx, <div class="item-view">"Error loading this story."</div> },
Some(story) => view! { cx,
@ -40,19 +49,21 @@ pub fn Story(cx: Scope) -> Element {
}}
</p>
<ul class="comment-children">
<For each=move || story.comments.clone().unwrap_or_default() key=|comment| comment.id>
{move |cx, comment: &api::Comment| view! { cx, <Comment comment=comment.clone() /> }}
</For>
<For
each=move || story.comments.clone().unwrap_or_default()
key=|comment| comment.id
view=move |comment| view! { cx, <Comment comment /> }
/>
</ul>
</div>
</div>
}})}
</div>
</>
}
}
#[component]
pub fn Comment(cx: Scope, comment: api::Comment) -> Element {
pub fn Comment(cx: Scope, comment: api::Comment) -> impl IntoView {
let (open, set_open) = create_signal(cx, true);
view! { cx,
@ -81,9 +92,11 @@ pub fn Comment(cx: Scope, comment: api::Comment) -> Element {
let comments = comment.comments.clone();
move || view! { cx,
<ul class="comment-children">
<For each=move || comments.clone() key=|comment| comment.id>
{|cx, comment: &api::Comment| view! { cx, <Comment comment=comment.clone() /> }}
</For>
<For
each=move || comments.clone()
key=|comment| comment.id
view=move |comment: api::Comment| view! { cx, <Comment comment /> }
/>
</ul>
}
})}

View file

@ -3,17 +3,23 @@ use leptos::*;
use leptos_router::*;
#[component]
pub fn User(cx: Scope) -> Element {
pub fn User(cx: Scope) -> impl IntoView {
let params = use_params_map(cx);
let user = create_resource(
cx,
move || params().get("id").cloned().unwrap_or_default(),
move |id| async move { api::fetch_api::<User>(&api::user(&id)).await },
move |id| async move {
if id.is_empty() {
None
} else {
api::fetch_api::<User>(cx, &api::user(&id)).await
}
},
);
view! { cx,
<div class="user-view">
{move || user.read().map(|user| match user {
None => view! { cx, <h1>"User not found."</h1> },
None => view! { cx, <h1>"User not found."</h1> }.into_any(),
Some(user) => view! { cx,
<div>
<h1>"User: " {&user.id}</h1>
@ -32,7 +38,7 @@ pub fn User(cx: Scope) -> Element {
<a href=format!("https://news.ycombinator.com/threads?id={}", user.id)>"comments"</a>
</p>
</div>
}
}.into_any()
})}
</div>
}

View file

@ -307,8 +307,8 @@ pub fn Todo(cx: Scope, todo: Todo) -> impl IntoView {
class="edit"
class:hidden={move || !(editing)()}
prop:value={move || todo.title.get()}
on:focusout=move |ev| save(&event_target_value(&ev))
on:keyup={move |ev| {
on:focusout=move |ev: web_sys::FocusEvent| save(&event_target_value(&ev))
on:keyup={move |ev: web_sys::KeyboardEvent| {
let key_code = ev.key_code();
if key_code == ENTER_KEY {
save(&event_target_value(&ev));

View file

@ -228,7 +228,7 @@ pub type PinnedHtmlStream = Pin<Box<dyn Stream<Item = io::Result<Bytes>> + Send>
/// use leptos::*;
///
/// #[component]
/// fn MyApp(cx: Scope) -> Element {
/// fn MyApp(cx: Scope) -> impl IntoView {
/// view! { cx, <main>"Hello, world!"</main> }
/// }
///

View file

@ -1,10 +1,10 @@
use cfg_if::cfg_if;
use leptos_macro::component;
use std::rc::Rc;
use leptos_dom::{DynChild, Fragment, IntoView, Component, HydrationCtx};
use leptos_dom::{DynChild, Fragment, IntoView, Component};
use leptos_reactive::{provide_context, Scope, SuspenseContext};
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
use leptos_dom::HydrationKey;
use leptos_dom::{HydrationCtx, HydrationKey};
/// If any [Resources](leptos_reactive::Resource) are read in the `children` of this
/// component, it will show the `fallback` while they are loading. Once all are resolved,

View file

@ -61,7 +61,7 @@ fn test_classes() {
use leptos::*;
_ = create_scope(create_runtime(), |cx| {
let (value, set_value) = create_signal(cx, 5);
let (value, _set_value) = create_signal(cx, 5);
let rendered = view! {
cx,
<div class="my big" class:a={move || value.get() > 10} class:red=true class:car={move || value.get() > 1}></div>

View file

@ -26,6 +26,9 @@ tracing = "0.1"
wasm-bindgen = { version = "0.2", features = ["enable-interning"] }
wasm-bindgen-futures = "0.4.31"
[dev-dependencies]
leptos = { path = "../leptos" }
[dependencies.web-sys]
version = "0.3"
features = [

View file

@ -542,7 +542,10 @@ impl<El: ElementDescriptor> HtmlElement<El> {
pub fn on<E: EventDescriptor + 'static>(
self,
event: E,
#[cfg(all(target_arch = "wasm32", feature = "web"))]
mut event_handler: impl EventHandler<E>,
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
event_handler: impl EventHandler<E>,
) -> Self {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
{

View file

@ -539,34 +539,40 @@ impl View {
event: E,
event_handler: impl FnMut(E::EventType) + 'static,
) -> Self {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
match &self {
Self::Element(el) => {
if event.bubbles() {
add_event_listener(&el.element, event.name(), event_handler);
} else {
add_event_listener_undelegated(
&el.element,
&event.name(),
event_handler,
);
cfg_if! {
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
match &self {
Self::Element(el) => {
if event.bubbles() {
add_event_listener(&el.element, event.name(), event_handler);
} else {
add_event_listener_undelegated(
&el.element,
&event.name(),
event_handler,
);
}
}
Self::Component(c) => {
let event_handler = Rc::new(RefCell::new(event_handler));
c.children.iter().cloned().for_each(|c| {
let event_handler = event_handler.clone();
c.on(event.clone(), move |e| event_handler.borrow_mut()(e));
});
}
Self::CoreComponent(c) => match c {
CoreComponent::DynChild(_) => {}
CoreComponent::Each(_) => {}
_ => {}
},
_ => {}
}
} else {
_ = event;
_ = event_handler;
}
Self::Component(c) => {
let event_handler = Rc::new(RefCell::new(event_handler));
c.children.iter().cloned().for_each(|c| {
let event_handler = event_handler.clone();
c.on(event.clone(), move |e| event_handler.borrow_mut()(e));
});
}
Self::CoreComponent(c) => match c {
CoreComponent::DynChild(_) => {}
CoreComponent::Each(_) => {}
_ => {}
},
_ => {}
}
self
@ -751,7 +757,7 @@ pub const fn is_browser() -> bool {
/// Returns true if `debug_assertions` are enabled.
/// ```
/// # use leptos_dom::is_dev;
/// if is_dev!() {
/// if is_dev() {
/// // log something or whatever
/// }
/// ```

View file

@ -6,14 +6,15 @@ use leptos_reactive::{create_rw_signal, RwSignal, Scope};
/// ```
/// # use leptos::*;
/// #[component]
/// pub fn MyComponent(cx: Scope) -> Element {
/// let input_ref = NodeRef::new(cx);
/// pub fn MyComponent(cx: Scope) -> impl IntoView {
/// let input_ref = NodeRef::<HtmlElement<Input>>::new(cx);
///
/// let on_click = move |_| {
/// let node = input_ref
/// .get()
/// .expect("input_ref should be loaded by now")
/// .unchecked_into::<web_sys::HtmlInputElement>();
/// .expect("input_ref should be loaded by now");
/// // `node` is strongly typed
/// // it is dereferenced to an `HtmlInputElement` automatically
/// log!("value is {:?}", node.value())
/// };
///

View file

@ -11,11 +11,12 @@ use std::borrow::Cow;
///
/// ```
/// # cfg_if::cfg_if! { if #[cfg(not(any(feature = "csr", feature = "hydrate")))] {
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view;
/// # use leptos::*;
/// let html = render_to_string(|cx| view! { cx,
/// <p>"Hello, world!"</p>
/// });
/// assert_eq!(html, r#"<p>Hello, world!</p>"#);
/// // static HTML includes some hydration info
/// assert_eq!(html, "<style>[leptos]{display:none;}</style><p id=\"_0-1\">Hello, world!</p>");
/// # }}
/// ```
pub fn render_to_string<F, N>(f: F) -> String
@ -151,6 +152,7 @@ pub fn render_to_stream_with_prefix_undisposed(
let fragments = fragments.map(|(fragment_id, id_before_suspense, html)| {
cfg_if! {
if #[cfg(debug_assertions)] {
_ = id_before_suspense;
// Debug-mode <Suspense/>-replacement code
format!(
r#"
@ -447,89 +449,4 @@ fn to_kebab_case(name: &str) -> String {
}
new_name
}
#[cfg(test)]
mod tests {
#[test]
fn simple_ssr_test() {
use leptos::*;
_ = create_scope(create_runtime(), |cx| {
let (value, set_value) = create_signal(cx, 0);
let rendered = view! {
cx,
<div>
<button on:click=move |_| set_value.update(|value| *value -= 1)>"-1"</button>
<span>"Value: " {move || value.get().to_string()} "!"</span>
<button on:click=move |_| set_value.update(|value| *value += 1)>"+1"</button>
</div>
}.render_to_string();
assert_eq!(
rendered,
"<div><button id=\"1-1-2\">-1</button><span>Value: <template \
id=\"2-4-6o\"/> <template id=\"2-4-6c\"/>!</span><button \
id=\"1-3-4\">+1</button></div>"
);
});
}
#[test]
fn ssr_test_with_components() {
use leptos::*;
#[component]
fn Counter(cx: Scope, initial_value: i32) -> View {
let (value, set_value) = create_signal(cx, initial_value);
view! {
cx,
<div>
<button on:click=move |_| set_value.update(|value| *value -= 1)>"-1"</button>
<span>"Value: " {move || value.get().to_string()} "!"</span>
<button on:click=move |_| set_value.update(|value| *value += 1)>"+1"</button>
</div>
}
}
_ = create_scope(create_runtime(), |cx| {
let rendered = view! {
cx,
<div class="counters">
<Counter initial_value=1/>
<Counter initial_value=2/>
</div>
}
.render_to_string();
assert_eq!(
rendered,
"<div class=\"counters\"><template id=\"1-1-2o\"/><div><button \
id=\"3-1-4\">-1</button><span>Value: <template id=\"4-4-8o\"/> \
<template id=\"4-4-8c\"/>!</span><button \
id=\"3-3-6\">+1</button></div><template id=\"1-1-2c\"/><template \
id=\"1-2-3o\"/><div><button id=\"3-1-4\">-1</button><span>Value: \
<template id=\"4-4-8o\"/> <template id=\"4-4-8c\"/>!</span><button \
id=\"3-3-6\">+1</button></div><template id=\"1-2-3c\"/></div>"
);
});
}
#[test]
fn test_classes() {
use leptos::*;
_ = create_scope(create_runtime(), |cx| {
let (value, set_value) = create_signal(cx, 5);
let rendered = view! {
cx,
<div class="my big" class:a={move || value.get() > 10} class:red=true class:car={move || value.get() > 1}></div>
}.render_to_string();
assert_eq!(
rendered,
"<div class=\"my big red car\" id=\"0-0-0\"></div>"
);
});
}
}
}

View file

@ -29,6 +29,7 @@ lazy_static = "1.4"
[dev-dependencies]
log = "0.4"
typed-builder = "0.10"
leptos = { path = "../leptos" }
[features]
default = ["ssr"]

View file

@ -38,7 +38,7 @@ mod server;
///
/// 1. Text content should be provided as a Rust string, i.e., double-quoted:
/// ```rust
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast;
/// # use leptos::*;
/// # run_scope(create_runtime(), |cx| {
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// view! { cx, <p>"Heres some text"</p> };
@ -48,7 +48,7 @@ mod server;
///
/// 2. Self-closing tags need an explicit `/` as in XML/XHTML
/// ```rust,compile_fail
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast;
/// # use leptos::*;
/// # run_scope(create_runtime(), |cx| {
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// // ❌ not like this
@ -58,7 +58,7 @@ mod server;
/// # });
/// ```
/// ```rust
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast;
/// # use leptos::*;
/// # run_scope(create_runtime(), |cx| {
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// // ✅ add that slash
@ -70,9 +70,9 @@ mod server;
///
/// 3. Components (functions annotated with `#[component]`) can be inserted as camel-cased tags
/// ```rust
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::*; use typed_builder::TypedBuilder; use leptos_dom::wasm_bindgen::JsCast; use leptos_dom as leptos; use leptos_dom::Marker;
/// # #[derive(TypedBuilder)] struct CounterProps { initial_value: i32 }
/// # fn Counter(cx: Scope, props: CounterProps) -> impl IntoView { view! { cx, <p></p>} }
/// # use leptos::*;
/// # #[component]
/// # fn Counter(cx: Scope, initial_value: i32) -> impl IntoView { view! { cx, <p></p>} }
/// # run_scope(create_runtime(), |cx| {
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// view! { cx, <div><Counter initial_value=3 /></div> }
@ -90,7 +90,7 @@ mod server;
/// take an `Option`, in which case `Some` sets the attribute and `None` removes the attribute.
///
/// ```rust
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast; use leptos_dom as leptos; use leptos_dom::Marker;
/// # use leptos::*;
/// # run_scope(create_runtime(), |cx| {
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// let (count, set_count) = create_signal(cx, 0);
@ -112,7 +112,7 @@ mod server;
/// 5. Event handlers can be added with `on:` attributes. In most cases, the events are given the correct type
/// based on the event name.
/// ```rust
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast;
/// # use leptos::*;
/// # run_scope(create_runtime(), |cx| {
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// view! {
@ -132,7 +132,7 @@ mod server;
/// that returns a primitive or JsValue). They can also take an `Option`, in which case `Some` sets the property
/// and `None` deletes the property.
/// ```rust
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast;
/// # use leptos::*;
/// # run_scope(create_runtime(), |cx| {
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// let (name, set_name) = create_signal(cx, "Alice".to_string());
@ -154,7 +154,7 @@ mod server;
///
/// 7. Classes can be toggled with `class:` attributes, which take a `bool` (or a signal that returns a `bool`).
/// ```rust
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast;
/// # use leptos::*;
/// # run_scope(create_runtime(), |cx| {
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// let (count, set_count) = create_signal(cx, 2);
@ -166,7 +166,7 @@ mod server;
///
/// Class names can include dashes, but cannot (at the moment) include a dash-separated segment of only numbers.
/// ```rust,compile_fail
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast;
/// # use leptos::*;
/// # run_scope(create_runtime(), |cx| {
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// let (count, set_count) = create_signal(cx, 2);
@ -180,7 +180,7 @@ mod server;
/// 8. You can use the `_ref` attribute to store a reference to its DOM element in a
/// [NodeRef](leptos_reactive::NodeRef) to use later.
/// ```rust
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast;
/// # use leptos::*;
/// # run_scope(create_runtime(), |cx| {
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// let (value, set_value) = create_signal(cx, 0);
@ -194,7 +194,7 @@ mod server;
///
/// Heres a simple example that shows off several of these features, put together
/// ```rust
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::*; use leptos_dom as leptos; use leptos_dom::Marker; use leptos_dom::wasm_bindgen::JsCast;
/// # use leptos::*;
///
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// pub fn SimpleCounter(cx: Scope) -> impl IntoView {
@ -342,7 +342,7 @@ pub fn view(tokens: TokenStream) -> TokenStream {
/// // ❌ This won't work.
/// # use leptos::*;
/// #[component]
/// fn MyComponent<T: Fn() -> impl IntoView>(cx: Scope, render_prop: T) -> impl IntoView {
/// fn MyComponent<T: Fn() -> HtmlElement<Div>>(cx: Scope, render_prop: T) -> impl IntoView {
/// todo!()
/// }
/// ```
@ -351,18 +351,19 @@ pub fn view(tokens: TokenStream) -> TokenStream {
/// // ✅ Do this instead
/// # use leptos::*;
/// #[component]
/// fn MyComponent<T>(cx: Scope, render_prop: T) -> impl IntoView where T: Fn() -> impl IntoView {
/// fn MyComponent<T>(cx: Scope, render_prop: T) -> impl IntoView
/// where T: Fn() -> HtmlElement<Div> {
/// todo!()
/// }
/// ```
///
/// 5. You can access the children passed into the component with the `children` property, which takes
/// an argument of the form `Box<dyn Fn(Scope) -> [Fragment](leptos_dom::Fragment)>`.
/// an argument of the form `Box<dyn Fn(Scope) -> Fragment>`.
///
/// ```
/// # use leptos::*;
/// #[component]
/// fn ComponentWithChildren(cx: Scope, children: Box<dyn Fn() -> Fragment>) -> impl IntoView {
/// fn ComponentWithChildren(cx: Scope, children: Box<dyn Fn(Scope) -> Fragment>) -> impl IntoView {
/// view! {
/// cx,
/// <ul>

View file

@ -133,7 +133,7 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
#(#fields),*
}
impl ServerFn for #struct_name {
impl leptos::ServerFn for #struct_name {
type Output = #output_ty;
fn prefix() -> &'static str {

View file

@ -33,6 +33,7 @@ cfg-if = "1.0.0"
[dev-dependencies]
tokio-test = "0.4"
leptos = { path = "../leptos" }
[features]
default = []

View file

@ -27,7 +27,7 @@ use crate::{runtime::with_runtime, Scope};
/// struct ValueSetter(WriteSignal<i32>);
///
/// #[component]
/// pub fn Provider(cx: Scope) -> Element {
/// pub fn Provider(cx: Scope) -> impl IntoView {
/// let (value, set_value) = create_signal(cx, 0);
///
/// // the newtype pattern isn't *necessary* here but is a good practice
@ -41,7 +41,7 @@ use crate::{runtime::with_runtime, Scope};
/// }
///
/// #[component]
/// pub fn Consumer(cx: Scope) -> Element {
/// pub fn Consumer(cx: Scope) -> impl IntoView {
/// // consume the provided context of type `ValueSetter` using `use_context`
/// // this traverses up the tree of `Scope`s and gets the nearest provided `ValueSetter`
/// let set_value = use_context::<ValueSetter>(cx).unwrap().0;
@ -85,7 +85,7 @@ where
/// struct ValueSetter(WriteSignal<i32>);
///
/// #[component]
/// pub fn Provider(cx: Scope) -> Element {
/// pub fn Provider(cx: Scope) -> impl IntoView {
/// let (value, set_value) = create_signal(cx, 0);
///
/// // the newtype pattern isn't *necessary* here but is a good practice
@ -99,7 +99,7 @@ where
/// }
///
/// #[component]
/// pub fn Consumer(cx: Scope) -> Element {
/// pub fn Consumer(cx: Scope) -> impl IntoView {
/// // consume the provided context of type `ValueSetter` using `use_context`
/// // this traverses up the tree of `Scope`s and gets the nearest provided `ValueSetter`
/// let set_value = use_context::<ValueSetter>(cx).unwrap().0;

View file

@ -27,11 +27,11 @@ impl PartialEq for SharedContext {
impl Eq for SharedContext {}
#[allow(clippy::derivable_impls)]
impl Default for SharedContext {
fn default() -> Self {
cfg_if! {
if #[cfg(feature = "hydrate")] {
if #[cfg(all(feature = "hydrate", target_arch = "wasm32"))] {
let pending_resources = js_sys::Reflect::get(
&web_sys::window().unwrap(),
&wasm_bindgen::JsValue::from_str("__LEPTOS_PENDING_RESOURCES"),

View file

@ -14,7 +14,7 @@
//! use leptos_meta::*;
//!
//! #[component]
//! fn MyApp(cx: Scope) -> Element {
//! fn MyApp(cx: Scope) -> impl IntoView {
//! let (name, set_name) = create_signal(cx, "Alice".to_string());
//!
//! view! { cx,
@ -58,6 +58,15 @@ pub struct MetaContext {
pub(crate) meta_tags: MetaTagsContext
}
/// Provides a [MetaContext], if there is not already one provided. This ensures that you can provide it
/// at the highest possible level, without overwriting a [MetaContext] that has already been provided
/// (for example, by a server-rendering integration.)
pub fn provide_meta_context(cx: Scope) {
if use_context::<MetaContext>(cx).is_none() {
provide_context(cx, MetaContext::new());
}
}
/// Returns the current [MetaContext].
///
/// If there is no [MetaContext] in this [Scope](leptos::Scope) or any parent scope, this will
@ -107,7 +116,10 @@ impl MetaContext {
/// };
///
/// // `app` contains only the body content w/ hydration stuff, not the meta tags
/// assert_eq!(app, r#"<main data-hk="0-0"><!--#--><!--/--><!--#--><!--/--><p>Some text</p></main>"#);
/// assert_eq!(
/// app.into_view(cx).render_to_string(cx),
/// "<main id=\"_0-1\"><leptos-unit leptos id=_0-2c></leptos-unit><leptos-unit leptos id=_0-3c></leptos-unit><p id=\"_0-4\">Some text</p></main>"
/// );
/// // `MetaContext::dehydrate()` gives you HTML that should be in the `<head>`
/// assert_eq!(use_head(cx).dehydrate(), r#"<title>my title</title><link rel="stylesheet" href="/style.css">"#)
/// });

View file

@ -90,8 +90,8 @@ pub struct MetaProps {
/// use leptos_meta::*;
///
/// #[component]
/// fn MyApp(cx: Scope) -> Element {
/// provide_context(cx, MetaContext::new());
/// fn MyApp(cx: Scope) -> impl IntoView {
/// provide_meta_context(cx);
///
/// view! { cx,
/// <main>
@ -121,7 +121,7 @@ pub fn Meta(cx: Scope, props: MetaProps) {
let meta_tags = meta.meta_tags;
let id = meta_tags.get_next_id();
let el = if let Ok(Some(el)) = document().query_selector(&format!("[data-leptos-meta={}]", id.0)) {
let el = if let Ok(Some(el)) = document().query_selector(&format!("[data-leptos-meta='{}']", id.0)) {
el
} else {
document().create_element("meta").unwrap_throw()

View file

@ -37,8 +37,8 @@ pub struct StylesheetProps {
/// use leptos_meta::*;
///
/// #[component]
/// fn MyApp(cx: Scope) -> Element {
/// provide_context(cx, MetaContext::new());
/// fn MyApp(cx: Scope) -> impl IntoView {
/// provide_meta_context(cx);
///
/// view! { cx,
/// <main>

View file

@ -66,7 +66,7 @@ pub struct TitleProps {
/// use leptos_meta::*;
///
/// #[component]
/// fn MyApp(cx: Scope) -> Element {
/// fn MyApp(cx: Scope) -> impl IntoView {
/// provide_context(cx, MetaContext::new());
/// let formatter = |text| format!("{text} — Leptos Online");
///
@ -79,7 +79,7 @@ pub struct TitleProps {
/// }
///
/// #[component]
/// fn PageA(cx: Scope) -> Element {
/// fn PageA(cx: Scope) -> impl IntoView {
/// view! { cx,
/// <main>
/// <Title text="Page A"/> // sets title to "Page A — Leptos Online"
@ -88,7 +88,7 @@ pub struct TitleProps {
/// }
///
/// #[component]
/// fn PageB(cx: Scope) -> Element {
/// fn PageB(cx: Scope) -> impl IntoView {
/// view! { cx,
/// <main>
/// <Title text="Page B"/> // sets title to "Page B — Leptos Online"

View file

@ -39,7 +39,8 @@
//! use leptos::*;
//! use leptos_router::*;
//!
//! pub fn router_example(cx: Scope) -> View {
//! #[component]
//! pub fn RouterExample(cx: Scope) -> impl IntoView {
//! view! {
//! cx,
//! <div id="root">
@ -60,23 +61,23 @@
//! // our root route: the contact list is always shown
//! <Route
//! path=""
//! element=move |cx| view! { cx, <ContactList/> }
//! view=move |cx| view! { cx, <ContactList/> }
//! >
//! // users like /gbj or /bob
//! <Route
//! path=":id"
//! element=move |cx| view! { cx, <Contact/> }
//! view=move |cx| view! { cx, <Contact/> }
//! />
//! // a fallback if the /:id segment is missing from the URL
//! <Route
//! path=""
//! element=move |_| view! { cx, <p class="contact">"Select a contact."</p> }
//! view=move |_| view! { cx, <p class="contact">"Select a contact."</p> }
//! />
//! </Route>
//! // LR will automatically use this for /about, not the /:id match above
//! <Route
//! path="about"
//! element=move |cx| view! { cx, <About/> }
//! view=move |cx| view! { cx, <About/> }
//! />
//! </Routes>
//! </main>
@ -100,7 +101,7 @@
//! }
//!
//! #[component]
//! fn ContactList(cx: Scope) -> View {
//! fn ContactList(cx: Scope) -> impl IntoView {
//! // loads the contact list data once; doesn't reload when nested routes change
//! let contacts = create_resource(cx, || (), |_| contact_list_data());
//! view! {
@ -118,7 +119,7 @@
//! }
//!
//! #[component]
//! fn Contact(cx: Scope) -> View {
//! fn Contact(cx: Scope) -> impl IntoView {
//! let params = use_params_map(cx);
//! let data = create_resource(
//! cx,
@ -129,7 +130,7 @@
//! }
//!
//! #[component]
//! fn About(cx: Scope) -> View {
//! fn About(cx: Scope) -> impl IntoView {
//! todo!()
//! }
//!