mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 14:54:16 +00:00
SSR/hydration example!
This commit is contained in:
parent
22b4c78804
commit
812c3a2045
20 changed files with 283 additions and 128 deletions
|
@ -3,6 +3,18 @@ name = "todomvc-ssr-client"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
todomvc = { path = "../../todomvc", features = ["hydrate"] }
|
console_log = "0.2"
|
||||||
leptos = { path = "../../../leptos", features = ["hydrate"] }
|
leptos = { path = "../../../leptos", features = ["hydrate"] }
|
||||||
|
todomvc = { path = "../../todomvc", features = ["hydrate"] }
|
||||||
|
log = "0.4"
|
||||||
|
wasm-bindgen = "0.2"
|
||||||
|
wee_alloc = "0.4"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
codegen-units = 1
|
||||||
|
lto = true
|
||||||
|
opt-level = 'z'
|
24
examples/todomvc-ssr/todomvc-ssr-client/src/lib.rs
Normal file
24
examples/todomvc-ssr/todomvc-ssr-client/src/lib.rs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
use leptos::*;
|
||||||
|
use todomvc::*;
|
||||||
|
use wasm_bindgen::prelude::wasm_bindgen;
|
||||||
|
|
||||||
|
extern crate wee_alloc;
|
||||||
|
#[global_allocator]
|
||||||
|
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn main() {
|
||||||
|
console_log::init_with_level(log::Level::Debug);
|
||||||
|
log::debug!("initialized logging");
|
||||||
|
|
||||||
|
leptos::hydrate(body().unwrap(), |cx| {
|
||||||
|
// initial state — identical to server
|
||||||
|
let todos = Todos(vec![
|
||||||
|
Todo::new(cx, 0, "Buy milk".to_string()),
|
||||||
|
Todo::new(cx, 1, "???".to_string()),
|
||||||
|
Todo::new(cx, 2, "Profit!".to_string()),
|
||||||
|
]);
|
||||||
|
|
||||||
|
view! { <TodoMVC todos=todos/> }
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,3 +0,0 @@
|
||||||
fn main() {
|
|
||||||
println!("Hello, world!");
|
|
||||||
}
|
|
|
@ -4,6 +4,7 @@ version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
actix-files = "0.6"
|
||||||
actix-web = "4"
|
actix-web = "4"
|
||||||
leptos = { path = "../../../leptos", features = ["ssr"] }
|
leptos = { path = "../../../leptos", features = ["ssr"] }
|
||||||
todomvc = { path = "../../todomvc", features = ["ssr"] }
|
todomvc = { path = "../../todomvc", default-features = false, features = ["ssr"] }
|
||||||
|
|
|
@ -1,20 +1,43 @@
|
||||||
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
|
use actix_files::{Directory, Files, NamedFile};
|
||||||
use actix_web::*;
|
use actix_web::*;
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
use todomvc::*;
|
use todomvc::*;
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
// - WASM/JS routes
|
||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
async fn render_todomvc() -> impl Responder {
|
async fn render_todomvc() -> impl Responder {
|
||||||
let mut buffer: String;
|
HttpResponse::Ok().content_type("text/html").body(format!(
|
||||||
_ = create_scope(|cx| {
|
"<!DOCTYPE html>{}",
|
||||||
let todos = Todos(vec![
|
run_scope({
|
||||||
Todo::new(cx, 0, "Buy milk".to_string()),
|
|cx| {
|
||||||
Todo::new(cx, 1, "???".to_string()),
|
let todos = Todos(vec![
|
||||||
Todo::new(cx, 2, "Profit!".to_string())
|
Todo::new(cx, 0, "Buy milk".to_string()),
|
||||||
]);
|
Todo::new(cx, 1, "???".to_string()),
|
||||||
|
Todo::new(cx, 2, "Profit!".to_string()),
|
||||||
|
]);
|
||||||
|
|
||||||
buffer = view! { <div id="root"><TodoMVC todos=todos/></div> };
|
view! {
|
||||||
});
|
<html lang="en">
|
||||||
buffer
|
<head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
|
<link rel="stylesheet" href="/static/todomvc-common/base.css"/>
|
||||||
|
<link rel="stylesheet" href="/static/todomvc-app-css/index.css"/>
|
||||||
|
<title>"Leptos • TodoMVC"</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<TodoMVC todos=todos/>
|
||||||
|
</body>
|
||||||
|
<script type="module">r#"import init, {{ main }} from './pkg/todomvc_ssr_client.js'; init().then(main);"#</script>
|
||||||
|
</html>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
|
@ -22,8 +45,10 @@ async fn main() -> std::io::Result<()> {
|
||||||
HttpServer::new(|| {
|
HttpServer::new(|| {
|
||||||
App::new()
|
App::new()
|
||||||
.service(render_todomvc)
|
.service(render_todomvc)
|
||||||
|
.service(Files::new("/static", "../../todomvc/node_modules"))
|
||||||
|
.service(Files::new("/pkg", "../todomvc-ssr-client/pkg"))
|
||||||
})
|
})
|
||||||
.bind(("127.0.0.1", 8080))?
|
.bind(("127.0.0.1", 8080))?
|
||||||
.run()
|
.run()
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ leptos = { path = "../../leptos" }
|
||||||
wee_alloc = "0.4"
|
wee_alloc = "0.4"
|
||||||
miniserde = "0.1"
|
miniserde = "0.1"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
console_log = "0.1"
|
console_log = "0.2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
wasm-bindgen-test = "0.3.0"
|
wasm-bindgen-test = "0.3.0"
|
||||||
|
@ -19,6 +19,7 @@ lto = true
|
||||||
opt-level = 'z'
|
opt-level = 'z'
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
default = ["csr"]
|
||||||
csr = ["leptos/csr"]
|
csr = ["leptos/csr"]
|
||||||
hydrate = ["leptos/hydrate"]
|
hydrate = ["leptos/hydrate"]
|
||||||
ssr = ["leptos/ssr"]
|
ssr = ["leptos/ssr"]
|
||||||
|
|
|
@ -13,23 +13,21 @@ impl Todos {
|
||||||
pub fn new(cx: Scope) -> Self {
|
pub fn new(cx: Scope) -> Self {
|
||||||
let starting_todos = if is_server!() {
|
let starting_todos = if is_server!() {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
|
} else if let Ok(Some(storage)) = window().local_storage() {
|
||||||
|
storage
|
||||||
|
.get_item(STORAGE_KEY)
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.and_then(|value| json::from_str::<Vec<TodoSerialized>>(&value).ok())
|
||||||
|
.map(|values| {
|
||||||
|
values
|
||||||
|
.into_iter()
|
||||||
|
.map(|stored| stored.into_todo(cx))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
} else {
|
} else {
|
||||||
if let Ok(Some(storage)) = window().local_storage() {
|
Vec::new()
|
||||||
storage
|
|
||||||
.get_item(STORAGE_KEY)
|
|
||||||
.ok()
|
|
||||||
.flatten()
|
|
||||||
.and_then(|value| json::from_str::<Vec<TodoSerialized>>(&value).ok())
|
|
||||||
.map(|values| {
|
|
||||||
values
|
|
||||||
.into_iter()
|
|
||||||
.map(|stored| stored.into_todo(cx))
|
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
.unwrap_or_default()
|
|
||||||
} else {
|
|
||||||
Vec::new()
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
Self(starting_todos)
|
Self(starting_todos)
|
||||||
}
|
}
|
||||||
|
@ -39,7 +37,7 @@ impl Todos {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add(&mut self, todo: Todo) {
|
pub fn add(&mut self, todo: Todo) {
|
||||||
self.0.push(todo);
|
self.0.push(todo);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove(&mut self, id: usize) {
|
pub fn remove(&mut self, id: usize) {
|
||||||
|
@ -47,17 +45,11 @@ impl Todos {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remaining(&self) -> usize {
|
pub fn remaining(&self) -> usize {
|
||||||
self.0
|
self.0.iter().filter(|todo| !(todo.completed)()).count()
|
||||||
.iter()
|
|
||||||
.filter(|todo| !(todo.completed)())
|
|
||||||
.count()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn completed(&self) -> usize {
|
pub fn completed(&self) -> usize {
|
||||||
self.0
|
self.0.iter().filter(|todo| (todo.completed)()).count()
|
||||||
.iter()
|
|
||||||
.filter(|todo| (todo.completed)())
|
|
||||||
.count()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toggle_all(&self) {
|
pub fn toggle_all(&self) {
|
||||||
|
@ -67,13 +59,13 @@ impl Todos {
|
||||||
if todo.completed.get() {
|
if todo.completed.get() {
|
||||||
(todo.set_completed)(|completed| *completed = false);
|
(todo.set_completed)(|completed| *completed = false);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
// otherwise, mark them all complete
|
// otherwise, mark them all complete
|
||||||
else {
|
else {
|
||||||
for todo in &self.0 {
|
for todo in &self.0 {
|
||||||
(todo.set_completed)(|completed| *completed = true);
|
(todo.set_completed)(|completed| *completed = true);
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,6 +110,14 @@ const ENTER_KEY: u32 = 13;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn TodoMVC(cx: Scope, todos: Todos) -> Vec<Element> {
|
pub fn TodoMVC(cx: Scope, todos: Todos) -> Vec<Element> {
|
||||||
|
let mut next_id = todos
|
||||||
|
.0
|
||||||
|
.iter()
|
||||||
|
.map(|todo| todo.id)
|
||||||
|
.max()
|
||||||
|
.map(|last| last + 1)
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
let (todos, set_todos) = create_signal(cx, todos);
|
let (todos, set_todos) = create_signal(cx, todos);
|
||||||
provide_context(cx, set_todos);
|
provide_context(cx, set_todos);
|
||||||
|
|
||||||
|
@ -127,7 +127,6 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> Vec<Element> {
|
||||||
set_mode(|mode| *mode = new_mode);
|
set_mode(|mode| *mode = new_mode);
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut next_id = 0;
|
|
||||||
let add_todo = move |ev: web_sys::Event| {
|
let add_todo = move |ev: web_sys::Event| {
|
||||||
let target = event_target::<HtmlInputElement>(&ev);
|
let target = event_target::<HtmlInputElement>(&ev);
|
||||||
ev.stop_propagation();
|
ev.stop_propagation();
|
||||||
|
@ -144,20 +143,20 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> Vec<Element> {
|
||||||
};
|
};
|
||||||
|
|
||||||
let filtered_todos = create_memo::<Vec<Todo>>(cx, move |_| {
|
let filtered_todos = create_memo::<Vec<Todo>>(cx, move |_| {
|
||||||
todos.with(|todos| {
|
todos.with(|todos| match mode.get() {
|
||||||
match mode.get() {
|
Mode::All => todos.0.to_vec(),
|
||||||
Mode::All => todos.0.to_vec(),
|
Mode::Active => todos
|
||||||
Mode::Active => todos.0
|
.0
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|todo| !todo.completed.get())
|
.filter(|todo| !todo.completed.get())
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect(),
|
.collect(),
|
||||||
Mode::Completed => todos.0
|
Mode::Completed => todos
|
||||||
.iter()
|
.0
|
||||||
.filter(|todo| todo.completed.get())
|
.iter()
|
||||||
.cloned()
|
.filter(|todo| todo.completed.get())
|
||||||
.collect(),
|
.cloned()
|
||||||
}
|
.collect(),
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -165,7 +164,9 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> Vec<Element> {
|
||||||
// this does reactive reads, so it will automatically serialize on any relevant change
|
// this does reactive reads, so it will automatically serialize on any relevant change
|
||||||
create_effect(cx, move |_| {
|
create_effect(cx, move |_| {
|
||||||
if let Ok(Some(storage)) = window().local_storage() {
|
if let Ok(Some(storage)) = window().local_storage() {
|
||||||
let objs = todos.get().0
|
let objs = todos
|
||||||
|
.get()
|
||||||
|
.0
|
||||||
.iter()
|
.iter()
|
||||||
.map(TodoSerialized::from)
|
.map(TodoSerialized::from)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
@ -230,11 +231,9 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> Vec<Element> {
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Todo(cx: Scope, todo: Todo) -> Element {
|
pub fn Todo(cx: Scope, todo: Todo) -> Element {
|
||||||
// creates a scope-bound reference to the Todo
|
|
||||||
// this allows us to move the reference into closures below without cloning it
|
|
||||||
let (editing, set_editing) = create_signal(cx, false);
|
let (editing, set_editing) = create_signal(cx, false);
|
||||||
let set_todos = use_context::<WriteSignal<Todos>>(cx).unwrap();
|
let set_todos = use_context::<WriteSignal<Todos>>(cx).unwrap();
|
||||||
let input: web_sys::Element;
|
let input: Element;
|
||||||
|
|
||||||
let save = move |value: &str| {
|
let save = move |value: &str| {
|
||||||
let value = value.trim();
|
let value = value.trim();
|
||||||
|
@ -288,6 +287,7 @@ pub fn Todo(cx: Scope, todo: Todo) -> Element {
|
||||||
</li>
|
</li>
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(not(feature = "ssr"))]
|
||||||
create_effect(cx, move |_| {
|
create_effect(cx, move |_| {
|
||||||
if editing() {
|
if editing() {
|
||||||
_ = input.unchecked_ref::<HtmlInputElement>().focus();
|
_ = input.unchecked_ref::<HtmlInputElement>().focus();
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
|
#[cfg(any(feature = "csr", feature = "hydrate", feature = "ssr"))]
|
||||||
mod for_component;
|
mod for_component;
|
||||||
mod map;
|
mod map;
|
||||||
|
#[cfg(any(feature = "csr", feature = "hydrate", feature = "ssr"))]
|
||||||
mod suspense;
|
mod suspense;
|
||||||
|
|
||||||
|
#[cfg(any(feature = "csr", feature = "hydrate", feature = "ssr"))]
|
||||||
pub use for_component::*;
|
pub use for_component::*;
|
||||||
|
#[cfg(any(feature = "csr", feature = "hydrate", feature = "ssr"))]
|
||||||
pub use suspense::*;
|
pub use suspense::*;
|
||||||
|
|
||||||
pub trait Prop {
|
pub trait Prop {
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
use std::rc::Rc;
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
use leptos_reactive::Scope;
|
use leptos_reactive::Scope;
|
||||||
|
|
||||||
|
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
|
|
||||||
use crate::Node;
|
use crate::Node;
|
||||||
|
@ -9,7 +11,7 @@ use crate::Node;
|
||||||
pub enum Child {
|
pub enum Child {
|
||||||
Null,
|
Null,
|
||||||
Text(String),
|
Text(String),
|
||||||
Fn(Rc<dyn Fn() -> Child>),
|
Fn(Rc<RefCell<dyn FnMut() -> Child>>),
|
||||||
Node(Node),
|
Node(Node),
|
||||||
Nodes(Vec<Node>),
|
Nodes(Vec<Node>),
|
||||||
}
|
}
|
||||||
|
@ -21,9 +23,9 @@ impl Child {
|
||||||
Child::Null => String::new(),
|
Child::Null => String::new(),
|
||||||
Child::Text(text) => text.to_string(),
|
Child::Text(text) => text.to_string(),
|
||||||
Child::Fn(f) => {
|
Child::Fn(f) => {
|
||||||
let mut value = f();
|
let mut value = (f.borrow_mut())();
|
||||||
while let Child::Fn(f) = value {
|
while let Child::Fn(f) = value {
|
||||||
value = f();
|
value = (f.borrow_mut())();
|
||||||
}
|
}
|
||||||
value.as_child_string()
|
value.as_child_string()
|
||||||
}
|
}
|
||||||
|
@ -106,8 +108,7 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
impl IntoChild for Vec<Node> {
|
||||||
impl IntoChild for Vec<web_sys::Node> {
|
|
||||||
fn into_child(self, _cx: Scope) -> Child {
|
fn into_child(self, _cx: Scope) -> Child {
|
||||||
Child::Nodes(self)
|
Child::Nodes(self)
|
||||||
}
|
}
|
||||||
|
@ -126,11 +127,11 @@ impl IntoChild for Vec<web_sys::Element> {
|
||||||
|
|
||||||
impl<T, U> IntoChild for T
|
impl<T, U> IntoChild for T
|
||||||
where
|
where
|
||||||
T: Fn() -> U + 'static,
|
T: FnMut() -> U + 'static,
|
||||||
U: IntoChild,
|
U: IntoChild,
|
||||||
{
|
{
|
||||||
fn into_child(self, cx: Scope) -> Child {
|
fn into_child(mut self, cx: Scope) -> Child {
|
||||||
let modified_fn = Rc::new(move || (self)().into_child(cx));
|
let modified_fn = Rc::new(RefCell::new(move || (self)().into_child(cx)));
|
||||||
Child::Fn(modified_fn)
|
Child::Fn(modified_fn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
mod attribute;
|
mod attribute;
|
||||||
|
#[cfg(any(feature = "csr", feature = "hydrate", feature = "ssr"))]
|
||||||
mod child;
|
mod child;
|
||||||
mod class;
|
mod class;
|
||||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
|
||||||
mod event_delegation;
|
mod event_delegation;
|
||||||
pub mod logging;
|
pub mod logging;
|
||||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
|
||||||
mod operations;
|
mod operations;
|
||||||
mod property;
|
mod property;
|
||||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||||
|
@ -13,10 +12,10 @@ mod reconcile;
|
||||||
mod render;
|
mod render;
|
||||||
|
|
||||||
pub use attribute::*;
|
pub use attribute::*;
|
||||||
|
#[cfg(any(feature = "csr", feature = "hydrate", feature = "ssr"))]
|
||||||
pub use child::*;
|
pub use child::*;
|
||||||
pub use class::*;
|
pub use class::*;
|
||||||
pub use logging::*;
|
pub use logging::*;
|
||||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
|
||||||
pub use operations::*;
|
pub use operations::*;
|
||||||
pub use property::*;
|
pub use property::*;
|
||||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||||
|
@ -82,7 +81,7 @@ where
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
#[cfg(feature = "hydrate")]
|
||||||
pub fn hydrate<T, F>(parent: web_sys::HtmlElement, f: F)
|
pub fn hydrate<T, F>(parent: web_sys::HtmlElement, f: F)
|
||||||
where
|
where
|
||||||
F: Fn(Scope) -> T + 'static,
|
F: Fn(Scope) -> T + 'static,
|
||||||
|
@ -99,7 +98,7 @@ where
|
||||||
|
|
||||||
pub fn create_component<F, T>(cx: Scope, f: F) -> T
|
pub fn create_component<F, T>(cx: Scope, f: F) -> T
|
||||||
where
|
where
|
||||||
F: Fn() -> T,
|
F: FnOnce() -> T,
|
||||||
{
|
{
|
||||||
// TODO hydration logic here
|
// TODO hydration logic here
|
||||||
cx.untrack(f)
|
cx.untrack(f)
|
||||||
|
@ -108,7 +107,7 @@ where
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! is_server {
|
macro_rules! is_server {
|
||||||
() => {
|
() => {
|
||||||
cfg!(feature = "server")
|
cfg!(feature = "ssr")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -239,15 +239,15 @@ pub fn add_event_listener(
|
||||||
.unwrap_throw(); */
|
.unwrap_throw(); */
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn window_event_listener(event_name: &str, cb: impl Fn(web_sys::Event)) {
|
pub fn window_event_listener(event_name: &str, cb: impl Fn(web_sys::Event) + 'static) {
|
||||||
let boxed: Box<dyn FnMut(web_sys::Event)> = Box::new(cb);
|
if !is_server!() {
|
||||||
// Safety: see add_event_listener above
|
let handler = Box::new(cb) as Box<dyn FnMut(web_sys::Event)>;
|
||||||
let handler: Box<dyn FnMut(web_sys::Event) + 'static> = unsafe { std::mem::transmute(boxed) };
|
|
||||||
|
|
||||||
let cb = Closure::wrap(handler).into_js_value();
|
let cb = Closure::wrap(handler).into_js_value();
|
||||||
window()
|
window()
|
||||||
.add_event_listener_with_callback(event_name, cb.unchecked_ref())
|
.add_event_listener_with_callback(event_name, cb.unchecked_ref())
|
||||||
.unwrap_throw();
|
.unwrap_throw();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hydration operations to find text and comment nodes
|
// Hydration operations to find text and comment nodes
|
||||||
|
|
|
@ -128,11 +128,11 @@ pub fn insert(
|
||||||
.unwrap_or_else(|| initial.clone())
|
.unwrap_or_else(|| initial.clone())
|
||||||
.unwrap_or(Child::Null);
|
.unwrap_or(Child::Null);
|
||||||
|
|
||||||
let mut value = f();
|
let mut value = (f.borrow_mut())();
|
||||||
|
|
||||||
if current != value {
|
if current != value {
|
||||||
while let Child::Fn(f) = value {
|
while let Child::Fn(f) = value {
|
||||||
value = f();
|
value = (f.borrow_mut())();
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(insert_expression(
|
Some(insert_expression(
|
||||||
|
@ -243,9 +243,9 @@ pub fn insert_expression(
|
||||||
|
|
||||||
// Nested Signals here simply won't do anything; they should be flattened so it's a single Signal
|
// Nested Signals here simply won't do anything; they should be flattened so it's a single Signal
|
||||||
Child::Fn(f) => {
|
Child::Fn(f) => {
|
||||||
let mut value = f();
|
let mut value = (f.borrow_mut())();
|
||||||
while let Child::Fn(f) = value {
|
while let Child::Fn(f) = value {
|
||||||
value = f();
|
value = (f.borrow_mut())();
|
||||||
}
|
}
|
||||||
value
|
value
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,12 +83,14 @@ fn root_element_to_tokens(template_uid: &Ident, node: &Node, mode: Mode) -> Toke
|
||||||
|
|
||||||
match mode {
|
match mode {
|
||||||
Mode::Ssr => {
|
Mode::Ssr => {
|
||||||
quote! {
|
quote! {{
|
||||||
|
#(#navigations);*;
|
||||||
|
|
||||||
format!(
|
format!(
|
||||||
#template,
|
#template,
|
||||||
#(#expressions),*
|
#(#expressions),*
|
||||||
)
|
)
|
||||||
}
|
}}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// create the root element from which navigations and expressions will begin
|
// create the root element from which navigations and expressions will begin
|
||||||
|
@ -165,7 +167,14 @@ fn element_to_tokens(
|
||||||
|
|
||||||
// attributes
|
// attributes
|
||||||
for attr in &node.attributes {
|
for attr in &node.attributes {
|
||||||
attr_to_tokens(attr, &this_el_ident, template, expressions, mode);
|
attr_to_tokens(
|
||||||
|
attr,
|
||||||
|
&this_el_ident,
|
||||||
|
template,
|
||||||
|
expressions,
|
||||||
|
navigations,
|
||||||
|
mode,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// navigation for this el
|
// navigation for this el
|
||||||
|
@ -187,7 +196,9 @@ fn element_to_tokens(
|
||||||
let #this_el_ident = #parent.first_child().unwrap_throw();
|
let #this_el_ident = #parent.first_child().unwrap_throw();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
navigations.push(this_nav);
|
if mode != Mode::Ssr {
|
||||||
|
navigations.push(this_nav);
|
||||||
|
}
|
||||||
|
|
||||||
// self-closing tags
|
// self-closing tags
|
||||||
// https://developer.mozilla.org/en-US/docs/Glossary/Empty_element
|
// https://developer.mozilla.org/en-US/docs/Glossary/Empty_element
|
||||||
|
@ -261,6 +272,7 @@ fn attr_to_tokens(
|
||||||
el_id: &Ident,
|
el_id: &Ident,
|
||||||
template: &mut String,
|
template: &mut String,
|
||||||
expressions: &mut Vec<TokenStream>,
|
expressions: &mut Vec<TokenStream>,
|
||||||
|
navigations: &mut Vec<TokenStream>,
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
) {
|
) {
|
||||||
let name = node
|
let name = node
|
||||||
|
@ -283,8 +295,26 @@ fn attr_to_tokens(
|
||||||
|
|
||||||
// refs
|
// refs
|
||||||
if name == "ref" {
|
if name == "ref" {
|
||||||
// refs mean nothing in SSR
|
let ident = match &node.value {
|
||||||
if mode != Mode::Ssr {
|
Some(expr) => {
|
||||||
|
if let Some(ident) = expr_to_ident(expr) {
|
||||||
|
quote_spanned! { span => #ident }
|
||||||
|
} else {
|
||||||
|
quote_spanned! { span => compile_error!("'ref' needs to be passed a variable name") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
quote_spanned! { span => compile_error!("'ref' needs to be passed a variable name") }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if mode == Mode::Ssr {
|
||||||
|
// fake the initialization; should only be used in effects or event handlers, which will never run on the server
|
||||||
|
// but if we don't initialize it, the compiler will complain
|
||||||
|
navigations.push(quote_spanned! {
|
||||||
|
span => #ident = String::new();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
expressions.push(match &node.value {
|
expressions.push(match &node.value {
|
||||||
Some(expr) => {
|
Some(expr) => {
|
||||||
if let Some(ident) = expr_to_ident(expr) {
|
if let Some(ident) = expr_to_ident(expr) {
|
||||||
|
@ -329,7 +359,7 @@ fn attr_to_tokens(
|
||||||
// Classes
|
// Classes
|
||||||
else if name.starts_with("class:") {
|
else if name.starts_with("class:") {
|
||||||
if mode == Mode::Ssr {
|
if mode == Mode::Ssr {
|
||||||
todo!()
|
// TODO class: in SSR
|
||||||
} else {
|
} else {
|
||||||
let name = name.replacen("class:", "", 1);
|
let name = name.replacen("class:", "", 1);
|
||||||
let value = node.value.as_ref().expect("class: attributes need values");
|
let value = node.value.as_ref().expect("class: attributes need values");
|
||||||
|
@ -468,11 +498,15 @@ fn child_to_tokens(
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(v) = str_value {
|
if let Some(v) = str_value {
|
||||||
navigations.push(location);
|
if mode != Mode::Ssr {
|
||||||
|
navigations.push(location);
|
||||||
|
}
|
||||||
template.push_str(&v);
|
template.push_str(&v);
|
||||||
|
|
||||||
PrevSibChange::Sib(name)
|
PrevSibChange::Sib(name)
|
||||||
} else /* if next_sib.is_some() */ {
|
} else
|
||||||
|
/* if next_sib.is_some() */
|
||||||
|
{
|
||||||
// these markers are one of the primary templating differences across modes
|
// these markers are one of the primary templating differences across modes
|
||||||
match mode {
|
match mode {
|
||||||
// in CSR, simply insert a comment node: it will be picked up and replaced with the value
|
// in CSR, simply insert a comment node: it will be picked up and replaced with the value
|
||||||
|
|
|
@ -2,14 +2,14 @@ use crate::{Runtime, Scope, ScopeId, Source, Subscriber};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{any::type_name, cell::RefCell, collections::HashSet, fmt::Debug, marker::PhantomData};
|
use std::{any::type_name, cell::RefCell, collections::HashSet, fmt::Debug, marker::PhantomData};
|
||||||
|
|
||||||
pub fn create_render_effect<T>(cx: Scope, f: impl FnMut(Option<T>) -> T + 'static) -> Effect<T>
|
pub fn create_render_effect<T>(cx: Scope, f: impl FnMut(Option<T>) -> T + 'static)
|
||||||
where
|
where
|
||||||
T: Debug + 'static,
|
T: Debug + 'static,
|
||||||
{
|
{
|
||||||
cx.create_eff(true, f)
|
cx.create_eff(true, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_effect<T>(cx: Scope, f: impl FnMut(Option<T>) -> T + 'static) -> Effect<T>
|
pub fn create_effect<T>(cx: Scope, f: impl FnMut(Option<T>) -> T + 'static)
|
||||||
where
|
where
|
||||||
T: Debug + 'static,
|
T: Debug + 'static,
|
||||||
{
|
{
|
||||||
|
@ -17,11 +17,8 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Scope {
|
impl Scope {
|
||||||
pub(crate) fn create_eff<T>(
|
#[cfg(not(feature = "ssr"))]
|
||||||
self,
|
pub(crate) fn create_eff<T>(self, render_effect: bool, f: impl FnMut(Option<T>) -> T + 'static)
|
||||||
render_effect: bool,
|
|
||||||
f: impl FnMut(Option<T>) -> T + 'static,
|
|
||||||
) -> Effect<T>
|
|
||||||
where
|
where
|
||||||
T: Debug + 'static,
|
T: Debug + 'static,
|
||||||
{
|
{
|
||||||
|
@ -29,16 +26,19 @@ impl Scope {
|
||||||
|
|
||||||
let id = self.push_effect(state);
|
let id = self.push_effect(state);
|
||||||
|
|
||||||
let eff = Effect {
|
|
||||||
scope: self.id,
|
|
||||||
id,
|
|
||||||
ty: PhantomData,
|
|
||||||
};
|
|
||||||
|
|
||||||
self.runtime
|
self.runtime
|
||||||
.any_effect((self.id, id), |effect| effect.run((self.id, id)));
|
.any_effect((self.id, id), |effect| effect.run((self.id, id)));
|
||||||
|
}
|
||||||
|
|
||||||
eff
|
// Simply don't run effects on the server at all
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
pub(crate) fn create_eff<T>(
|
||||||
|
self,
|
||||||
|
_render_effect: bool,
|
||||||
|
_f: impl FnMut(Option<T>) -> T + 'static,
|
||||||
|
) where
|
||||||
|
T: Debug + 'static,
|
||||||
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,13 +2,16 @@ use std::collections::HashMap;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Default)]
|
#[derive(Debug, PartialEq, Default)]
|
||||||
pub struct SharedContext {
|
pub struct SharedContext {
|
||||||
|
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||||
pub completed: Vec<web_sys::Element>,
|
pub completed: Vec<web_sys::Element>,
|
||||||
pub events: Vec<()>,
|
pub events: Vec<()>,
|
||||||
pub id: Option<usize>,
|
pub id: Option<usize>,
|
||||||
|
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||||
pub registry: HashMap<String, web_sys::Element>,
|
pub registry: HashMap<String, web_sys::Element>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SharedContext {
|
impl SharedContext {
|
||||||
|
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||||
pub fn new_with_registry(registry: HashMap<String, web_sys::Element>) -> Self {
|
pub fn new_with_registry(registry: HashMap<String, web_sys::Element>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
completed: Default::default(),
|
completed: Default::default(),
|
||||||
|
@ -28,8 +31,4 @@ impl SharedContext {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn gather(&self) {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
|
|
||||||
mod context;
|
mod context;
|
||||||
mod effect;
|
mod effect;
|
||||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
|
||||||
mod hydration;
|
mod hydration;
|
||||||
mod memo;
|
mod memo;
|
||||||
mod resource;
|
mod resource;
|
||||||
|
|
|
@ -6,6 +6,7 @@ pub struct Memo<T>(ReadSignal<Option<T>>)
|
||||||
where
|
where
|
||||||
T: 'static;
|
T: 'static;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "ssr"))]
|
||||||
pub fn create_memo<T>(cx: Scope, mut f: impl FnMut(Option<T>) -> T + 'static) -> Memo<T>
|
pub fn create_memo<T>(cx: Scope, mut f: impl FnMut(Option<T>) -> T + 'static) -> Memo<T>
|
||||||
where
|
where
|
||||||
T: PartialEq + Clone + Debug + 'static,
|
T: PartialEq + Clone + Debug + 'static,
|
||||||
|
@ -23,6 +24,18 @@ where
|
||||||
Memo(read)
|
Memo(read)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// On the server, Memo just carries its original value
|
||||||
|
// If we didn't provide this alternate version, it would panic because its inner effect wouldn't run
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
pub fn create_memo<T>(cx: Scope, mut f: impl FnMut(Option<T>) -> T + 'static) -> Memo<T>
|
||||||
|
where
|
||||||
|
T: PartialEq + Clone + Debug + 'static,
|
||||||
|
{
|
||||||
|
let (read, _) = create_signal(cx, Some(f(None)));
|
||||||
|
|
||||||
|
Memo(read)
|
||||||
|
}
|
||||||
|
|
||||||
impl<T> Clone for Memo<T>
|
impl<T> Clone for Memo<T>
|
||||||
where
|
where
|
||||||
T: 'static,
|
T: 'static,
|
||||||
|
|
|
@ -65,6 +65,7 @@ where
|
||||||
});
|
});
|
||||||
|
|
||||||
// initial load fires immediately
|
// initial load fires immediately
|
||||||
|
// TODO SSR — this won't run on server
|
||||||
create_effect(cx, {
|
create_effect(cx, {
|
||||||
let r = Rc::clone(&r);
|
let r = Rc::clone(&r);
|
||||||
move |_| r.load(false)
|
move |_| r.load(false)
|
||||||
|
@ -179,6 +180,8 @@ where
|
||||||
|
|
||||||
let suspense_contexts = self.suspense_contexts.clone();
|
let suspense_contexts = self.suspense_contexts.clone();
|
||||||
let has_value = v.is_some();
|
let has_value = v.is_some();
|
||||||
|
|
||||||
|
// TODO SSR check -- this won't run on server
|
||||||
create_effect(self.scope, move |_| {
|
create_effect(self.scope, move |_| {
|
||||||
if let Some(s) = &suspense_cx {
|
if let Some(s) = &suspense_cx {
|
||||||
let mut contexts = suspense_contexts.borrow_mut();
|
let mut contexts = suspense_contexts.borrow_mut();
|
||||||
|
|
|
@ -3,16 +3,14 @@ use crate::{
|
||||||
ScopeState, SignalId, SignalState, Subscriber, TransitionState,
|
ScopeState, SignalId, SignalState, Subscriber, TransitionState,
|
||||||
};
|
};
|
||||||
use slotmap::SlotMap;
|
use slotmap::SlotMap;
|
||||||
use std::cell::{Cell, RefCell};
|
use std::cell::RefCell;
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
|
||||||
use crate::hydration::SharedContext;
|
use crate::hydration::SharedContext;
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default, Debug)]
|
||||||
pub(crate) struct Runtime {
|
pub(crate) struct Runtime {
|
||||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
|
||||||
pub(crate) shared_context: RefCell<Option<SharedContext>>,
|
pub(crate) shared_context: RefCell<Option<SharedContext>>,
|
||||||
pub(crate) stack: RefCell<Vec<Subscriber>>,
|
pub(crate) stack: RefCell<Vec<Subscriber>>,
|
||||||
pub(crate) scopes: RefCell<SlotMap<ScopeId, Rc<ScopeState>>>,
|
pub(crate) scopes: RefCell<SlotMap<ScopeId, Rc<ScopeState>>>,
|
||||||
|
@ -133,6 +131,20 @@ impl Runtime {
|
||||||
ScopeDisposer(Box::new(move || scope.dispose()))
|
ScopeDisposer(Box::new(move || scope.dispose()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn run_scope<T>(&'static self, f: impl FnOnce(Scope) -> T, parent: Option<Scope>) -> T {
|
||||||
|
let id = {
|
||||||
|
self.scopes
|
||||||
|
.borrow_mut()
|
||||||
|
.insert(Rc::new(ScopeState::new(parent)))
|
||||||
|
};
|
||||||
|
let scope = Scope { runtime: self, id };
|
||||||
|
let ret = f(scope);
|
||||||
|
|
||||||
|
scope.dispose();
|
||||||
|
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
pub fn push_stack(&self, id: Subscriber) {
|
pub fn push_stack(&self, id: Subscriber) {
|
||||||
self.stack.borrow_mut().push(id);
|
self.stack.borrow_mut().push(id);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
AnyEffect, AnySignal, EffectId, EffectState, ReadSignal, ResourceId, ResourceState, Runtime,
|
hydration::SharedContext, AnyEffect, AnySignal, EffectId, EffectState, ReadSignal, ResourceId,
|
||||||
SignalId, SignalState, WriteSignal,
|
ResourceState, Runtime, SignalId, SignalState, WriteSignal,
|
||||||
};
|
};
|
||||||
use elsa::FrozenVec;
|
use elsa::FrozenVec;
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -12,11 +12,24 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
#[must_use = "Scope will leak memory if the disposer function is never called"]
|
#[must_use = "Scope will leak memory if the disposer function is never called"]
|
||||||
|
/// Creates a child reactive scope and runs the function within it. This is useful for applications
|
||||||
|
/// like a list or a router, which may want to create child scopes and dispose of them when
|
||||||
|
/// they are no longer needed (e.g., a list item has been destroyed or the user has navigated away
|
||||||
|
/// from the route.)
|
||||||
pub fn create_scope(f: impl FnOnce(Scope) + 'static) -> ScopeDisposer {
|
pub fn create_scope(f: impl FnOnce(Scope) + 'static) -> ScopeDisposer {
|
||||||
let runtime = Box::leak(Box::new(Runtime::new()));
|
let runtime = Box::leak(Box::new(Runtime::new()));
|
||||||
runtime.create_scope(f, None)
|
runtime.create_scope(f, None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a temporary scope, runs the given function, disposes of the scope,
|
||||||
|
/// and returns the value returned from the function. This is very useful for short-lived
|
||||||
|
/// applications like SSR, where actual reactivity is not required beyond the end
|
||||||
|
/// of the synchronous operation.
|
||||||
|
pub fn run_scope<T>(f: impl FnOnce(Scope) -> T + 'static) -> T {
|
||||||
|
let runtime = Box::leak(Box::new(Runtime::new()));
|
||||||
|
runtime.run_scope(f, None)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
pub struct Scope {
|
pub struct Scope {
|
||||||
pub(crate) runtime: &'static Runtime,
|
pub(crate) runtime: &'static Runtime,
|
||||||
|
@ -152,13 +165,13 @@ impl Scope {
|
||||||
let v = curr.node_value();
|
let v = curr.node_value();
|
||||||
if v == Some("#".to_string()) {
|
if v == Some("#".to_string()) {
|
||||||
count += 1;
|
count += 1;
|
||||||
} else if v == Some("/".to_string()) {
|
} else if v == Some("/".to_string()) {
|
||||||
count -= 1;
|
count -= 1;
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
current.push(curr.clone());
|
current.push(curr.clone());
|
||||||
return (curr, current);
|
return (curr, current);
|
||||||
}
|
}
|
||||||
|
|
||||||
log::debug!(">>> count is now {count}");
|
log::debug!(">>> count is now {count}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -168,10 +181,28 @@ impl Scope {
|
||||||
}
|
}
|
||||||
|
|
||||||
log::debug!("end = {end:?}");
|
log::debug!("end = {end:?}");
|
||||||
log::debug!("current = {:?}", current.iter().map(|n| (n.node_name(), n.node_value())).collect::<Vec<_>>());
|
log::debug!(
|
||||||
|
"current = {:?}",
|
||||||
|
current
|
||||||
|
.iter()
|
||||||
|
.map(|n| (n.node_name(), n.node_value()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
|
||||||
(start, current)
|
(start, current)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn next_hydration_key(&self) -> usize {
|
||||||
|
let mut sc = self.runtime.shared_context.borrow_mut();
|
||||||
|
if let Some(ref mut sc) = *sc {
|
||||||
|
sc.next_hydration_key()
|
||||||
|
} else {
|
||||||
|
let mut new_sc = SharedContext::default();
|
||||||
|
let id = new_sc.next_hydration_key();
|
||||||
|
*sc = Some(new_sc);
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ScopeDisposer(pub(crate) Box<dyn FnOnce()>);
|
pub struct ScopeDisposer(pub(crate) Box<dyn FnOnce()>);
|
||||||
|
|
Loading…
Reference in a new issue