mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 09:54:41 -05:00
Create HackerNews app
This commit is contained in:
2
TODO.md
2
TODO.md
@@ -13,6 +13,8 @@
|
||||
- [x] Hydration
|
||||
- [ ] Streaming HTML from server
|
||||
- [ ] Streaming `Resource`s
|
||||
- [ ] Escaping HTML entities when running format!()
|
||||
- [ ] Isomorphic rendering benchmark
|
||||
- [ ] Docs (and clippy warning to insist on docs)
|
||||
- [ ] Read through + understand...
|
||||
- [ ] `Props` macro
|
||||
|
||||
25
examples/hackernews/hackernews-app/Cargo.toml
Normal file
25
examples/hackernews/hackernews-app/Cargo.toml
Normal file
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "hackernews-app"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
console_log = "0.2"
|
||||
leptos = { path = "../../../leptos" }
|
||||
log = "0.4"
|
||||
gloo-net = { version = "0.2", features = ["http"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
wee_alloc = "0.4"
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
opt-level = 'z'
|
||||
|
||||
[features]
|
||||
default = ["csr"]
|
||||
csr = ["leptos/csr"]
|
||||
hydrate = ["leptos/hydrate"]
|
||||
ssr = ["leptos/ssr"]
|
||||
8
examples/hackernews/hackernews-app/index.html
Normal file
8
examples/hackernews/hackernews-app/index.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z"/>
|
||||
<link data-trunk rel="css" href="./style.css"/>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
59
examples/hackernews/hackernews-app/src/api.rs
Normal file
59
examples/hackernews/hackernews-app/src/api.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use anyhow::Result;
|
||||
use serde::{de::DeserializeOwned, Deserialize};
|
||||
|
||||
pub fn story(path: &str) -> String {
|
||||
format!("https://node-hnapi.herokuapp.com/{path}")
|
||||
}
|
||||
|
||||
pub fn user(path: &str) -> String {
|
||||
format!("https://hacker-news.firebaseio.com/v0/user/{path}.json")
|
||||
}
|
||||
|
||||
pub async fn fetch_api<T>(path: &str) -> Result<T, ()>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
gloo_net::http::Request::get(path)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| log::error!("{e}"))?
|
||||
.json::<T>()
|
||||
.await
|
||||
.map_err(|e| log::error!("{e}"))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
|
||||
pub struct Story {
|
||||
pub id: usize,
|
||||
pub title: String,
|
||||
pub points: Option<i32>,
|
||||
pub user: Option<String>,
|
||||
pub time: usize,
|
||||
pub time_ago: String,
|
||||
#[serde(alias = "type")]
|
||||
pub story_type: String,
|
||||
pub url: String,
|
||||
#[serde(default)]
|
||||
pub domain: String,
|
||||
pub comments: Option<Vec<Comment>>,
|
||||
pub comments_count: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
|
||||
pub struct Comment {
|
||||
pub id: usize,
|
||||
pub level: usize,
|
||||
pub user: String,
|
||||
pub time: usize,
|
||||
pub time_ago: String,
|
||||
pub content: String,
|
||||
pub comments: Vec<Comment>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
|
||||
pub struct User {
|
||||
pub created: usize,
|
||||
pub id: String,
|
||||
pub karma: i32,
|
||||
pub about: Option<String>,
|
||||
}
|
||||
41
examples/hackernews/hackernews-app/src/lib.rs
Normal file
41
examples/hackernews/hackernews-app/src/lib.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
// This is essentially a port of Solid's Hacker News demo
|
||||
// https://github.com/solidjs/solid-hackernews
|
||||
|
||||
use leptos::*;
|
||||
|
||||
mod api;
|
||||
mod nav;
|
||||
mod stories;
|
||||
mod story;
|
||||
mod users;
|
||||
use nav::*;
|
||||
use stories::*;
|
||||
use story::*;
|
||||
use users::*;
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> Element {
|
||||
view! {
|
||||
<div>
|
||||
<Router mode=BrowserIntegration {}>
|
||||
<Routes>
|
||||
<Route path="" element=|cx| view! { <Main/> }>
|
||||
<Route path="users/:id" element=|cx| view! { <User/> } loader=user_data.into() />
|
||||
<Route path="stories/:id" element=|cx| view! { <Story/> } loader=story_data.into() />
|
||||
<Route path="*stories" element=|cx| view! { <Stories/> } loader=stories_data.into()/>
|
||||
</Route>
|
||||
</Routes>
|
||||
</Router>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Main(cx: Scope) -> Element {
|
||||
view! {
|
||||
<div>
|
||||
<Nav />
|
||||
<Outlet />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
10
examples/hackernews/hackernews-app/src/main.rs
Normal file
10
examples/hackernews/hackernews-app/src/main.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
use hackernews_app::*;
|
||||
use leptos::*;
|
||||
|
||||
#[global_allocator]
|
||||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
mount_to_body(|cx| view! { <App/> })
|
||||
}
|
||||
29
examples/hackernews/hackernews-app/src/nav.rs
Normal file
29
examples/hackernews/hackernews-app/src/nav.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use leptos::*;
|
||||
|
||||
#[component]
|
||||
pub fn Nav(cx: Scope) -> Element {
|
||||
view! {
|
||||
<header class="header">
|
||||
<nav class="inner">
|
||||
<Link to="/".into()>
|
||||
<strong>"HN"</strong>
|
||||
</Link>
|
||||
<Link to="/new".into()>
|
||||
<strong>"New"</strong>
|
||||
</Link>
|
||||
<Link to="/show".into()>
|
||||
<strong>"Show"</strong>
|
||||
</Link>
|
||||
<Link to="/ask".into()>
|
||||
<strong>"Ask"</strong>
|
||||
</Link>
|
||||
<Link to="/job".into()>
|
||||
<strong>"Jobs"</strong>
|
||||
</Link>
|
||||
<a class="github" href="http://github.com/gbj/leptos" target="_blank" rel="noreferrer">
|
||||
"Built with Leptos"
|
||||
</a>
|
||||
</nav>
|
||||
</header>
|
||||
}
|
||||
}
|
||||
177
examples/hackernews/hackernews-app/src/stories.rs
Normal file
177
examples/hackernews/hackernews-app/src/stories.rs
Normal file
@@ -0,0 +1,177 @@
|
||||
use leptos::*;
|
||||
|
||||
use crate::api;
|
||||
|
||||
fn category(from: &str) -> &'static str {
|
||||
match from {
|
||||
"new" => "newest",
|
||||
"show" => "show",
|
||||
"ask" => "ask",
|
||||
"job" => "jobs",
|
||||
_ => "news",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stories_data(cx: Scope, params: Memo<ParamsMap>, location: Location) -> StoriesData {
|
||||
log::debug!("(stories_data) loading data for stories");
|
||||
let page = create_memo(cx, move |_| {
|
||||
location
|
||||
.query
|
||||
.with(|q| q.get("page").and_then(|p| p.parse::<usize>().ok()))
|
||||
.unwrap_or(1)
|
||||
});
|
||||
let story_type = create_memo(cx, move |_| {
|
||||
params
|
||||
.with(|params| params.get("stories").cloned())
|
||||
.unwrap_or_else(|| "top".to_string())
|
||||
});
|
||||
let stories = create_resource(
|
||||
cx,
|
||||
move || format!("{}?page={}", category(&story_type()), page()),
|
||||
|path| async move {
|
||||
api::fetch_api::<Vec<api::Story>>(&api::story(&path))
|
||||
.await
|
||||
.map_err(|_| ())
|
||||
},
|
||||
);
|
||||
StoriesData {
|
||||
page,
|
||||
story_type,
|
||||
stories,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct StoriesData {
|
||||
pub page: Memo<usize>,
|
||||
pub story_type: Memo<String>,
|
||||
pub stories: Resource<String, Result<Vec<api::Story>, ()>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for StoriesData {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("StoriesData").finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Stories(cx: Scope) -> Element {
|
||||
let StoriesData {
|
||||
page,
|
||||
story_type,
|
||||
stories,
|
||||
} = use_loader::<StoriesData>(cx);
|
||||
|
||||
view! {
|
||||
<div class="news-view">
|
||||
<div class="news-list-nav">
|
||||
{move || if page() > 1 {
|
||||
view! {
|
||||
<Link
|
||||
attr:class="page-link"
|
||||
to={format!("/{}?page={}", story_type(), page() - 1)}
|
||||
attr:aria_label="Previous Page"
|
||||
>
|
||||
"< prev"
|
||||
</Link>
|
||||
}
|
||||
} else {
|
||||
view! {
|
||||
<span class="page-link disabled" aria-hidden="true">
|
||||
"< prev"
|
||||
</span>
|
||||
}
|
||||
}}
|
||||
<span>"page " {page}</span>
|
||||
{
|
||||
move || if stories.read().unwrap_or(Err(())).unwrap_or_default().len() >= 28 {
|
||||
view! {
|
||||
<Link
|
||||
attr:class="page-link"
|
||||
to={format!("/{}?page={}", story_type(), page() + 1)}
|
||||
attr:aria_label="Next Page"
|
||||
>
|
||||
"more >"
|
||||
</Link>
|
||||
}
|
||||
} else {
|
||||
view! {
|
||||
<span class="page-link disabled" aria-hidden="true">
|
||||
"more >"
|
||||
</span>
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<main class="news-list">
|
||||
{move || match stories.read() {
|
||||
None => None,
|
||||
Some(Err(_)) => Some(view! { <p>"Error loading stories."</p> }),
|
||||
Some(Ok(stories)) => {
|
||||
Some(view! {
|
||||
<ul>
|
||||
<For each={move || stories.clone()} key=|story| story.id>{
|
||||
|cx, story: &api::Story| view! {
|
||||
<Story story={story.clone()} />
|
||||
}
|
||||
}</For>
|
||||
</ul>
|
||||
})
|
||||
}
|
||||
}}
|
||||
</main>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Story(cx: Scope, story: api::Story) -> Element {
|
||||
view! {
|
||||
<li class="news-item">
|
||||
<span class="score">{story.points}</span>
|
||||
<span class="title">
|
||||
{if !story.url.starts_with("item?id=") {
|
||||
view! {
|
||||
<span>
|
||||
<a href={story.url} target="_blank" rel="noreferrer">
|
||||
{story.title.clone()}
|
||||
</a>
|
||||
<span class="host">"("{story.domain}")"</span>
|
||||
</span>
|
||||
}
|
||||
} else {
|
||||
let title = story.title.clone();
|
||||
view! { <Link to={format!("/stories/{}", story.id)}>{title}</Link> }
|
||||
}}
|
||||
</span>
|
||||
<br />
|
||||
<span class="meta">
|
||||
{if story.story_type != "job" {
|
||||
view! {
|
||||
<span>
|
||||
{"by "}
|
||||
{story.user.map(|user| view ! { <Link to={format!("/users/{}", user)}>{user}</Link>})}
|
||||
{format!(" {} | ", story.time_ago)}
|
||||
<Link to={format!("/stories/{}", story.id)}>
|
||||
{if story.comments_count.unwrap_or_default() > 0 {
|
||||
format!("{} comments", story.comments_count.unwrap_or_default())
|
||||
} else {
|
||||
"discuss".into()
|
||||
}}
|
||||
</Link>
|
||||
</span>
|
||||
}
|
||||
} else {
|
||||
let title = story.title.clone();
|
||||
view! { <Link to={format!("/item/{}", story.id)}>{title}</Link> }
|
||||
}}
|
||||
</span>
|
||||
{(story.story_type != "link").then(|| view! {
|
||||
<span>
|
||||
{" "}
|
||||
<span class="label">{story.story_type}</span>
|
||||
</span>
|
||||
})}
|
||||
</li>
|
||||
}
|
||||
}
|
||||
113
examples/hackernews/hackernews-app/src/story.rs
Normal file
113
examples/hackernews/hackernews-app/src/story.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
use crate::api;
|
||||
use leptos::*;
|
||||
|
||||
pub fn story_data(
|
||||
cx: Scope,
|
||||
params: Memo<ParamsMap>,
|
||||
_location: Location,
|
||||
) -> Resource<String, Result<api::Story, ()>> {
|
||||
log::debug!("(story_data) loading data for story");
|
||||
create_resource(
|
||||
cx,
|
||||
move || params().get("id").cloned().unwrap_or_default(),
|
||||
|id| async move { api::fetch_api(&api::story(&format!("item/{id}"))).await },
|
||||
)
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Story(cx: Scope) -> Element {
|
||||
let story = use_loader::<Resource<String, Result<api::Story, ()>>>(cx);
|
||||
|
||||
view! {
|
||||
<div>
|
||||
{move || story.read().map(|story| match story {
|
||||
Err(_) => view! { <div class="item-view">"Error loading this story."</div> },
|
||||
Ok(story) => view! {
|
||||
<div class="item-view">
|
||||
<div class="item-view-header">
|
||||
<a href={story.url} target="_blank">
|
||||
<h1>{story.title}</h1>
|
||||
</a>
|
||||
<span class="host">
|
||||
"("{story.domain}")"
|
||||
</span>
|
||||
{story.user.map(|user| view! { <p class="meta">
|
||||
// TODO issue here in renderer
|
||||
{story.points}
|
||||
" points | by "
|
||||
<Link to=format!("/users/{}", user)>{user}</Link>
|
||||
{format!(" {}", story.time_ago)}
|
||||
</p>})}
|
||||
</div>
|
||||
<div class="item-view-comments">
|
||||
<p class="item-view-comments-header">
|
||||
{if story.comments_count.unwrap_or_default() > 0 {
|
||||
format!("{} comments", story.comments_count.unwrap_or_default())
|
||||
} else {
|
||||
"No comments yet.".into()
|
||||
}}
|
||||
</p>
|
||||
<ul class="comment-children">
|
||||
<For each={move || story.comments.clone().unwrap_or_default()} key={|comment| comment.id}>
|
||||
{move |cx, comment: &api::Comment| view! { <Comment comment={comment.clone()} /> }}
|
||||
</For>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}})}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Comment(cx: Scope, comment: api::Comment) -> Element {
|
||||
let (open, set_open) = create_signal(cx, true);
|
||||
|
||||
view! {
|
||||
<li class="comment">
|
||||
<div class="by">
|
||||
<Link to={format!("/users/{}", comment.user)}>{comment.user}</Link>
|
||||
{format!(" {}", comment.time_ago)}
|
||||
</div>
|
||||
<div class="text" inner_html={comment.content}></div>
|
||||
{(!comment.comments.is_empty()).then(|| {
|
||||
view! {
|
||||
<div>
|
||||
<div class="toggle" class:open=open>
|
||||
<a on:click=move |_| set_open(|n| *n = !*n)>
|
||||
{
|
||||
let comments_len = comment.comments.len();
|
||||
move || if open() {
|
||||
"[-]".into()
|
||||
} else {
|
||||
format!("[+] {}{} collapsed", comments_len, pluralize(comments_len))
|
||||
}
|
||||
}
|
||||
</a>
|
||||
</div>
|
||||
{move || open().then({
|
||||
let comments = comment.comments.clone();
|
||||
move || view! {
|
||||
<ul class="comment-children">
|
||||
<For each={move || comments.clone()} key=|comment| comment.id>
|
||||
{|cx, comment: &api::Comment| view! {
|
||||
<Comment comment={comment.clone()} />
|
||||
}}
|
||||
</For>
|
||||
</ul>
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
})}
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
||||
fn pluralize(n: usize) -> &'static str {
|
||||
if n == 1 {
|
||||
" reply"
|
||||
} else {
|
||||
" replies"
|
||||
}
|
||||
}
|
||||
46
examples/hackernews/hackernews-app/src/users.rs
Normal file
46
examples/hackernews/hackernews-app/src/users.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use crate::api;
|
||||
use leptos::*;
|
||||
|
||||
pub fn user_data(
|
||||
cx: Scope,
|
||||
params: Memo<ParamsMap>,
|
||||
_location: Location,
|
||||
) -> Resource<String, Result<api::User, ()>> {
|
||||
log::debug!("(story_data) loading data for user");
|
||||
create_resource(
|
||||
cx,
|
||||
move || params().get("id").cloned().unwrap_or_default(),
|
||||
|id| async move { api::fetch_api(&api::user(&id)).await },
|
||||
)
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn User(cx: Scope) -> Element {
|
||||
let user = use_loader::<Resource<String, Result<api::User, ()>>>(cx);
|
||||
view! {
|
||||
<div class="user-view">
|
||||
{move || user.read().map(|user| match user {
|
||||
Err(_) => view! { <h1>"User not found."</h1> },
|
||||
Ok(user) => view! {
|
||||
<div>
|
||||
<h1>"User: " {user.id}</h1>
|
||||
<ul class="meta">
|
||||
<li>
|
||||
<span class="label">"Created: "</span> {user.created}
|
||||
</li>
|
||||
<li>
|
||||
<span class="label">"Karma: "</span> {user.karma}
|
||||
</li>
|
||||
//{user.about.map(|about| view! { <li inner_html={user.about} class="about"></li> })}
|
||||
</ul>
|
||||
/* <p class="links">
|
||||
<a href={format!("https://news.ycombinator.com/submitted?id={}", user.id)}>"submissions"</a>
|
||||
" | "
|
||||
<a href={format!("https://news.ycombinator.com/threads?id={}", user.id)}>"comments"</a>
|
||||
</p> */
|
||||
</div>
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
326
examples/hackernews/hackernews-app/style.css
Normal file
326
examples/hackernews/hackernews-app/style.css
Normal file
@@ -0,0 +1,326 @@
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
||||
font-size: 15px;
|
||||
background-color: #f2f3f5;
|
||||
margin: 0;
|
||||
padding-top: 55px;
|
||||
color: #34495e;
|
||||
overflow-y: scroll
|
||||
}
|
||||
|
||||
a {
|
||||
color: #34495e;
|
||||
text-decoration: none
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: #335d92;
|
||||
position: fixed;
|
||||
z-index: 999;
|
||||
height: 55px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0
|
||||
}
|
||||
|
||||
.header .inner {
|
||||
max-width: 800px;
|
||||
box-sizing: border-box;
|
||||
margin: 0 auto;
|
||||
padding: 15px 5px
|
||||
}
|
||||
|
||||
.header a {
|
||||
color: rgba(255, 255, 255, .8);
|
||||
line-height: 24px;
|
||||
transition: color .15s ease;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
font-weight: 300;
|
||||
letter-spacing: .075em;
|
||||
margin-right: 1.8em
|
||||
}
|
||||
|
||||
.header a:hover {
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.header a.active {
|
||||
color: #fff;
|
||||
font-weight: 400
|
||||
}
|
||||
|
||||
.header a:nth-child(6) {
|
||||
margin-right: 0
|
||||
}
|
||||
|
||||
.header .github {
|
||||
color: #fff;
|
||||
font-size: .9em;
|
||||
margin: 0;
|
||||
float: right
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 24px;
|
||||
margin-right: 10px;
|
||||
display: inline-block;
|
||||
vertical-align: middle
|
||||
}
|
||||
|
||||
.view {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
position: relative
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-exit-active {
|
||||
transition: all .2s ease
|
||||
}
|
||||
|
||||
.fade-enter,
|
||||
.fade-exit-active {
|
||||
opacity: 0
|
||||
}
|
||||
|
||||
@media (max-width:860px) {
|
||||
.header .inner {
|
||||
padding: 15px 30px
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width:600px) {
|
||||
.header .inner {
|
||||
padding: 15px
|
||||
}
|
||||
|
||||
.header a {
|
||||
margin-right: 1em
|
||||
}
|
||||
|
||||
.header .github {
|
||||
display: none
|
||||
}
|
||||
}
|
||||
|
||||
.news-view {
|
||||
padding-top: 45px
|
||||
}
|
||||
|
||||
.news-list,
|
||||
.news-list-nav {
|
||||
background-color: #fff;
|
||||
border-radius: 2px
|
||||
}
|
||||
|
||||
.news-list-nav {
|
||||
padding: 15px 30px;
|
||||
position: fixed;
|
||||
text-align: center;
|
||||
top: 55px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 998;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, .1)
|
||||
}
|
||||
|
||||
.news-list-nav .page-link {
|
||||
margin: 0 1em
|
||||
}
|
||||
|
||||
.news-list-nav .disabled {
|
||||
color: #aaa
|
||||
}
|
||||
|
||||
.news-list {
|
||||
position: absolute;
|
||||
margin: 30px 0;
|
||||
width: 100%;
|
||||
transition: all .5s cubic-bezier(.55, 0, .1, 1)
|
||||
}
|
||||
|
||||
.news-list ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0
|
||||
}
|
||||
|
||||
@media (max-width:600px) {
|
||||
.news-list {
|
||||
margin: 10px 0
|
||||
}
|
||||
}
|
||||
|
||||
.news-item {
|
||||
background-color: #fff;
|
||||
padding: 20px 30px 20px 80px;
|
||||
border-bottom: 1px solid #eee;
|
||||
position: relative;
|
||||
line-height: 20px
|
||||
}
|
||||
|
||||
.news-item .score {
|
||||
color: #335d92;
|
||||
font-size: 1.1em;
|
||||
font-weight: 700;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
margin-top: -10px
|
||||
}
|
||||
|
||||
.news-item .host,
|
||||
.news-item .meta {
|
||||
font-size: .85em;
|
||||
color: #626262
|
||||
}
|
||||
|
||||
.news-item .host a,
|
||||
.news-item .meta a {
|
||||
color: #626262;
|
||||
text-decoration: underline
|
||||
}
|
||||
|
||||
.news-item .host a:hover,
|
||||
.news-item .meta a:hover {
|
||||
color: #335d92
|
||||
}
|
||||
|
||||
.item-view-header {
|
||||
background-color: #fff;
|
||||
padding: 1.8em 2em 1em;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, .1)
|
||||
}
|
||||
|
||||
.item-view-header h1 {
|
||||
display: inline;
|
||||
font-size: 1.5em;
|
||||
margin: 0;
|
||||
margin-right: .5em
|
||||
}
|
||||
|
||||
.item-view-header .host,
|
||||
.item-view-header .meta,
|
||||
.item-view-header .meta a {
|
||||
color: #626262
|
||||
}
|
||||
|
||||
.item-view-header .meta a {
|
||||
text-decoration: underline
|
||||
}
|
||||
|
||||
.item-view-comments {
|
||||
background-color: #fff;
|
||||
margin-top: 10px;
|
||||
padding: 0 2em .5em
|
||||
}
|
||||
|
||||
.item-view-comments-header {
|
||||
margin: 0;
|
||||
font-size: 1.1em;
|
||||
padding: 1em 0;
|
||||
position: relative
|
||||
}
|
||||
|
||||
.item-view-comments-header .spinner {
|
||||
display: inline-block;
|
||||
margin: -15px 0
|
||||
}
|
||||
|
||||
.comment-children {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0
|
||||
}
|
||||
|
||||
@media (max-width:600px) {
|
||||
.item-view-header h1 {
|
||||
font-size: 1.25em
|
||||
}
|
||||
}
|
||||
|
||||
.comment-children .comment-children {
|
||||
margin-left: 1.5em
|
||||
}
|
||||
|
||||
.comment {
|
||||
border-top: 1px solid #eee;
|
||||
position: relative
|
||||
}
|
||||
|
||||
.comment .by,
|
||||
.comment .text,
|
||||
.comment .toggle {
|
||||
font-size: .9em;
|
||||
margin: 1em 0
|
||||
}
|
||||
|
||||
.comment .by {
|
||||
color: #626262
|
||||
}
|
||||
|
||||
.comment .by a {
|
||||
color: #626262;
|
||||
text-decoration: underline
|
||||
}
|
||||
|
||||
.comment .text {
|
||||
overflow-wrap: break-word
|
||||
}
|
||||
|
||||
.comment .text a:hover {
|
||||
color: #335d92
|
||||
}
|
||||
|
||||
.comment .text pre {
|
||||
white-space: pre-wrap
|
||||
}
|
||||
|
||||
.comment .toggle {
|
||||
background-color: #fffbf2;
|
||||
padding: .3em .5em;
|
||||
border-radius: 4px
|
||||
}
|
||||
|
||||
.comment .toggle a {
|
||||
color: #626262;
|
||||
cursor: pointer
|
||||
}
|
||||
|
||||
.comment .toggle.open {
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
margin-bottom: -.5em
|
||||
}
|
||||
|
||||
.user-view {
|
||||
background-color: #fff;
|
||||
box-sizing: border-box;
|
||||
padding: 2em 3em
|
||||
}
|
||||
|
||||
.user-view h1 {
|
||||
margin: 0;
|
||||
font-size: 1.5em
|
||||
}
|
||||
|
||||
.user-view .meta {
|
||||
list-style-type: none;
|
||||
padding: 0
|
||||
}
|
||||
|
||||
.user-view .label {
|
||||
display: inline-block;
|
||||
min-width: 4em
|
||||
}
|
||||
|
||||
.user-view .about {
|
||||
margin: 1em 0
|
||||
}
|
||||
|
||||
.user-view .links a {
|
||||
text-decoration: underline
|
||||
}
|
||||
@@ -54,7 +54,13 @@ pub fn attribute(cx: Scope, el: &web_sys::Element, attr_name: &'static str, valu
|
||||
|
||||
fn attribute_expression(el: &web_sys::Element, attr_name: &str, value: Attribute) {
|
||||
match value {
|
||||
Attribute::String(value) => set_attribute(el, attr_name, &value),
|
||||
Attribute::String(value) => {
|
||||
if attr_name == "inner_html" {
|
||||
el.set_inner_html(&value);
|
||||
} else {
|
||||
set_attribute(el, attr_name, &value)
|
||||
}
|
||||
}
|
||||
Attribute::Option(value) => match value {
|
||||
Some(value) => set_attribute(el, attr_name, &value),
|
||||
None => remove_attribute(el, attr_name),
|
||||
@@ -342,7 +348,7 @@ fn clean_children(
|
||||
marker: &Marker,
|
||||
replacement: Option<web_sys::Node>,
|
||||
) -> Child {
|
||||
log::debug!("clean_children on {} with current = {current:?} and marker = {marker:#?} and replacement = {replacement:#?}", parent.node_name());
|
||||
//log::debug!("clean_children on {} with current = {current:?} and marker = {marker:#?} and replacement = {replacement:#?}", parent.node_name());
|
||||
|
||||
if marker == &Marker::NoChildren {
|
||||
parent.set_text_content(Some(""));
|
||||
|
||||
@@ -504,9 +504,7 @@ fn child_to_tokens(
|
||||
template.push_str(&v);
|
||||
|
||||
PrevSibChange::Sib(name)
|
||||
} else
|
||||
/* if next_sib.is_some() */
|
||||
{
|
||||
} else {
|
||||
// these markers are one of the primary templating differences across modes
|
||||
match mode {
|
||||
// in CSR, simply insert a comment node: it will be picked up and replaced with the value
|
||||
@@ -631,7 +629,11 @@ fn create_component(node: &Node, mode: Mode) -> TokenStream {
|
||||
|
||||
let props = node.attributes.iter().filter_map(|attr| {
|
||||
let attr_name = attr.name_as_string().unwrap_or_default();
|
||||
if attr_name.strip_prefix("on:").is_some() {
|
||||
if attr_name.starts_with("on:")
|
||||
|| attr_name.starts_with("prop:")
|
||||
|| attr_name.starts_with("class:")
|
||||
|| attr_name.starts_with("attr:")
|
||||
{
|
||||
None
|
||||
} else {
|
||||
let name = ident_from_tag_name(attr.name.as_ref().unwrap());
|
||||
@@ -657,18 +659,26 @@ fn create_component(node: &Node, mode: Mode) -> TokenStream {
|
||||
}
|
||||
// Properties
|
||||
else if let Some(name) = attr_name.strip_prefix("prop:") {
|
||||
let value = node.value.as_ref().expect("prop: blocks need values");
|
||||
let value = attr.value.as_ref().expect("prop: attributes need values");
|
||||
Some(quote_spanned! {
|
||||
span => leptos_dom::property(cx, #component_name.unchecked_ref(), #name, #value.into_property(cx))
|
||||
})
|
||||
}
|
||||
// Classes
|
||||
else if let Some(name) = attr_name.strip_prefix("class:") {
|
||||
let value = node.value.as_ref().expect("class: attributes need values");
|
||||
let value = attr.value.as_ref().expect("class: attributes need values");
|
||||
Some(quote_spanned! {
|
||||
span => leptos_dom::class(cx, #component_name.unchecked_ref(), #name, #value.into_class(cx))
|
||||
})
|
||||
}
|
||||
// Attributes
|
||||
else if let Some(name) = attr_name.strip_prefix("attr:") {
|
||||
let value = attr.value.as_ref().expect("attr: attributes need values");
|
||||
let name = name.replace("_", "-");
|
||||
Some(quote_spanned! {
|
||||
span => leptos_dom::attribute(cx, #component_name.unchecked_ref(), #name, #value.into_attribute(cx))
|
||||
})
|
||||
}
|
||||
else {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -126,6 +126,8 @@ where
|
||||
pub(crate) trait AnyEffect: Debug {
|
||||
fn run(&self, id: (ScopeId, EffectId));
|
||||
|
||||
fn clear_dependencies(&self);
|
||||
|
||||
fn subscribe_to(&self, source: Source);
|
||||
}
|
||||
|
||||
@@ -156,6 +158,10 @@ where
|
||||
self.runtime.pop_stack();
|
||||
}
|
||||
|
||||
fn clear_dependencies(&self) {
|
||||
self.sources.borrow_mut().clear();
|
||||
}
|
||||
|
||||
fn subscribe_to(&self, source: Source) {
|
||||
self.add_source(source);
|
||||
}
|
||||
|
||||
@@ -88,13 +88,26 @@ impl Scope {
|
||||
}
|
||||
|
||||
pub fn dispose(self) {
|
||||
// first, drop child scopes
|
||||
self.runtime.scope(self.id, |scope| {
|
||||
for id in scope.children.borrow().iter() {
|
||||
self.runtime.remove_scope(id)
|
||||
if let Some(scope) = self.runtime.scopes.borrow_mut().remove(self.id) {
|
||||
for id in scope.children.take() {
|
||||
Scope {
|
||||
runtime: self.runtime,
|
||||
id,
|
||||
}
|
||||
.dispose();
|
||||
}
|
||||
})
|
||||
// removing from the runtime will drop this Scope, and all its Signals/Effects/Memos
|
||||
|
||||
for effect in &scope.effects {
|
||||
log::debug!("unloading dependencies from effect");
|
||||
effect.clear_dependencies();
|
||||
}
|
||||
|
||||
for cleanup in scope.cleanups.take() {
|
||||
(cleanup)();
|
||||
}
|
||||
|
||||
drop(scope);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
@@ -111,8 +124,6 @@ impl Scope {
|
||||
pub fn get_next_element(&self, template: &web_sys::Element) -> web_sys::Element {
|
||||
use wasm_bindgen::{JsCast, UnwrapThrowExt};
|
||||
|
||||
log::debug!("get_next_element");
|
||||
|
||||
let cloned_template = |t: &web_sys::Element| {
|
||||
t.unchecked_ref::<web_sys::HtmlTemplateElement>()
|
||||
.content()
|
||||
@@ -159,7 +170,6 @@ impl Scope {
|
||||
{
|
||||
while let Some(curr) = end {
|
||||
start = curr.clone();
|
||||
log::debug!("curr = {} => {:?}", curr.node_name(), curr.node_value());
|
||||
if curr.node_type() == 8 {
|
||||
// COMMENT
|
||||
let v = curr.node_value();
|
||||
@@ -171,8 +181,6 @@ impl Scope {
|
||||
current.push(curr.clone());
|
||||
return (curr, current);
|
||||
}
|
||||
|
||||
log::debug!(">>> count is now {count}");
|
||||
}
|
||||
}
|
||||
current.push(curr.clone());
|
||||
@@ -180,15 +188,6 @@ impl Scope {
|
||||
}
|
||||
}
|
||||
|
||||
log::debug!("end = {end:?}");
|
||||
log::debug!(
|
||||
"current = {:?}",
|
||||
current
|
||||
.iter()
|
||||
.map(|n| (n.node_name(), n.node_value()))
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
(start, current)
|
||||
}
|
||||
|
||||
@@ -228,6 +227,7 @@ pub(crate) struct ScopeState {
|
||||
pub(crate) signals: FrozenVec<Box<dyn AnySignal>>,
|
||||
pub(crate) effects: FrozenVec<Box<dyn AnyEffect>>,
|
||||
pub(crate) resources: FrozenVec<Rc<dyn Any>>,
|
||||
pub(crate) cleanups: RefCell<Vec<Box<dyn FnOnce()>>>,
|
||||
}
|
||||
|
||||
impl Debug for ScopeState {
|
||||
@@ -245,6 +245,7 @@ impl ScopeState {
|
||||
signals: Default::default(),
|
||||
effects: Default::default(),
|
||||
resources: Default::default(),
|
||||
cleanups: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ features = [
|
||||
]
|
||||
|
||||
[features]
|
||||
default = ["csr"]
|
||||
csr = ["leptos_core/csr", "leptos_dom/csr", "leptos_macro/csr", "leptos_reactive/csr", "dep:js-sys"]
|
||||
hydrate = ["leptos_core/hydrate", "leptos_dom/hydrate", "leptos_macro/hydrate", "leptos_reactive/hydrate", "dep:js-sys"]
|
||||
ssr = ["leptos_core/ssr", "leptos_dom/ssr", "leptos_macro/ssr", "leptos_reactive/ssr", "dep:url", "dep:regex"]
|
||||
|
||||
@@ -11,11 +11,7 @@ use leptos_reactive::Scope;
|
||||
#[component]
|
||||
pub fn Outlet(cx: Scope) -> Child {
|
||||
let route = use_route(cx);
|
||||
create_effect(cx, move |_| {
|
||||
log::debug!("<Outlet> RouteContext is {:#?}", use_route(cx).path())
|
||||
});
|
||||
if let Some(child) = route.child() {
|
||||
log::debug!("<Outlet> providing context {child:#?}");
|
||||
provide_context(child.cx(), child.clone());
|
||||
child.outlet().into_child(child.cx())
|
||||
} else {
|
||||
|
||||
@@ -119,10 +119,6 @@ impl RouteContext {
|
||||
}
|
||||
|
||||
pub fn resolve_path<'a>(&'a self, to: &'a str) -> Option<Cow<'a, str>> {
|
||||
log::debug!(
|
||||
"resolve_path to {to:?}: RouteContext.inner is {:#?}",
|
||||
self.inner.path
|
||||
);
|
||||
resolve_path(&self.inner.base_path, to, Some(&self.inner.path))
|
||||
}
|
||||
|
||||
|
||||
@@ -114,6 +114,7 @@ where
|
||||
|
||||
if disposers.len() > i + 1 {
|
||||
let old_route_disposer = std::mem::replace(&mut disposers[i], disposer);
|
||||
log::debug!("disposing");
|
||||
old_route_disposer.dispose();
|
||||
} else {
|
||||
disposers.push(disposer);
|
||||
@@ -168,6 +169,7 @@ pub struct RouterContext {
|
||||
pub(crate) struct RouterContextInner {
|
||||
pub location: Location,
|
||||
pub base: RouteContext,
|
||||
base_path: String,
|
||||
history: Box<dyn History>,
|
||||
cx: Scope,
|
||||
reference: ReadSignal<String>,
|
||||
@@ -253,6 +255,7 @@ impl RouterContext {
|
||||
});
|
||||
|
||||
let inner = Rc::new(RouterContextInner {
|
||||
base_path: base_path.into_owned(),
|
||||
location,
|
||||
base,
|
||||
history: Box::new(history),
|
||||
@@ -364,13 +367,11 @@ impl RouterContextInner {
|
||||
}
|
||||
|
||||
pub(crate) fn navigate_end(self: Rc<Self>, mut next: LocationChange) {
|
||||
log::debug!("navigate_end w/ referrers = {:#?}\n\nnext = {next:#?}", self.referrers.borrow());
|
||||
let first = self.referrers.borrow().get(0).cloned();
|
||||
if let Some(first) = first {
|
||||
if next.value != first.value || next.state != first.state {
|
||||
next.replace = first.replace;
|
||||
next.scroll = first.scroll;
|
||||
log::debug!("navigating in browser to {next:#?}");
|
||||
self.history.navigate(&next);
|
||||
}
|
||||
self.referrers.borrow_mut().clear();
|
||||
@@ -380,16 +381,15 @@ impl RouterContextInner {
|
||||
pub(crate) fn handle_anchor_click(self: Rc<Self>, ev: web_sys::Event) {
|
||||
use leptos_dom::wasm_bindgen::JsCast;
|
||||
let ev = ev.unchecked_into::<web_sys::MouseEvent>();
|
||||
/* if ev.default_prevented()
|
||||
if ev.default_prevented()
|
||||
|| ev.button() != 0
|
||||
|| ev.meta_key()
|
||||
|| ev.alt_key()
|
||||
|| ev.ctrl_key()
|
||||
|| ev.shift_key()
|
||||
{
|
||||
log::debug!("branch A prevent");
|
||||
return;
|
||||
} */
|
||||
}
|
||||
|
||||
let composed_path = ev.composed_path();
|
||||
let mut a: Option<web_sys::HtmlAnchorElement> = None;
|
||||
@@ -408,11 +408,9 @@ impl RouterContextInner {
|
||||
// let browser handle this event if link has target,
|
||||
// or if it doesn't have href or state
|
||||
// TODO "state" is set as a prop, not an attribute
|
||||
/* if !target.is_empty() || (href.is_empty() && !a.has_attribute("state")) {
|
||||
log::debug!("target or href empty");
|
||||
ev.prevent_default();
|
||||
if !target.is_empty() || (href.is_empty() && !a.has_attribute("state")) {
|
||||
return;
|
||||
} */
|
||||
}
|
||||
|
||||
let rel = a.get_attribute("rel").unwrap_or_default();
|
||||
let mut rel = rel.split([' ', '\t']);
|
||||
@@ -427,15 +425,15 @@ impl RouterContextInner {
|
||||
|
||||
// let browser handle this event if it leaves our domain
|
||||
// or our base path
|
||||
/* if url.origin != leptos_dom::location().origin().unwrap_or_default()
|
||||
|| (!base_path.is_empty()
|
||||
if url.origin != leptos_dom::location().origin().unwrap_or_default()
|
||||
|| (!self.base_path.is_empty()
|
||||
&& !path_name.is_empty()
|
||||
&& !path_name
|
||||
.to_lowercase()
|
||||
.starts_with(&base_path.to_lowercase()))
|
||||
.starts_with(&self.base_path.to_lowercase()))
|
||||
{
|
||||
return;
|
||||
} */
|
||||
}
|
||||
|
||||
let to = path_name + &unescape(&url.search) + &unescape(&url.hash);
|
||||
// TODO "state" is set as a prop, not an attribute
|
||||
|
||||
@@ -37,7 +37,7 @@ pub struct RouteData {
|
||||
}
|
||||
|
||||
impl RouteData {
|
||||
fn score(&self) -> usize {
|
||||
fn score(&self) -> i32 {
|
||||
let (pattern, splat) = match self.pattern.split_once("/*") {
|
||||
Some((p, s)) => (p, Some(s)),
|
||||
None => (self.pattern.as_str(), None),
|
||||
@@ -47,7 +47,7 @@ impl RouteData {
|
||||
.filter(|n| !n.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
segments.iter().fold(
|
||||
segments.len() - if splat.is_none() { 0 } else { 1 },
|
||||
(segments.len() as i32) - if splat.is_none() { 0 } else { 1 },
|
||||
|score, segment| score + if segment.starts_with(':') { 2 } else { 3 },
|
||||
)
|
||||
}
|
||||
@@ -83,7 +83,7 @@ fn create_branches(
|
||||
pub(crate) fn create_branch(routes: &[RouteData], index: usize) -> Branch {
|
||||
Branch {
|
||||
routes: routes.to_vec(),
|
||||
score: routes.last().unwrap().score() * 10000 - index,
|
||||
score: routes.last().unwrap().score() * 10000 - (index as i32),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::{any::Any, fmt::Debug, rc::Rc};
|
||||
|
||||
use leptos_reactive::{Memo, ReadSignal, Scope};
|
||||
use leptos_reactive::{Memo, Scope};
|
||||
|
||||
use crate::{use_route, Location, ParamsMap};
|
||||
|
||||
@@ -8,18 +8,9 @@ pub fn use_loader<T>(cx: Scope) -> T
|
||||
where
|
||||
T: Clone + Debug + 'static,
|
||||
{
|
||||
log::debug!("use_loader on cx {:?}\n\n{:#?}", cx.id(), cx);
|
||||
|
||||
let route = use_route(cx);
|
||||
|
||||
log::debug!("use_loader route = {route:#?}");
|
||||
|
||||
let data = route.data().as_ref().unwrap();
|
||||
|
||||
log::debug!("use_loader data = {data:?}");
|
||||
|
||||
let data = data.downcast_ref::<T>().unwrap();
|
||||
|
||||
data.clone()
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ impl PartialEq for RouterError {
|
||||
(Self::NoMatch(l0), Self::NoMatch(r0)) => l0 == r0,
|
||||
(Self::NotFound(l0), Self::NotFound(r0)) => l0 == r0,
|
||||
(Self::MissingParam(l0), Self::MissingParam(r0)) => l0 == r0,
|
||||
(Self::Params(l0), Self::Params(r0)) => false,
|
||||
(Self::Params(_), Self::Params(_)) => false,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,14 +6,11 @@ use super::params::ParamsMap;
|
||||
|
||||
pub fn create_location(cx: Scope, path: ReadSignal<String>, state: ReadSignal<State>) -> Location {
|
||||
let url = create_memo(cx, move |prev: Option<Url>| {
|
||||
path.with(|path| {
|
||||
log::debug!("create_location with path {path}");
|
||||
match Url::try_from(path.as_str()) {
|
||||
Ok(url) => url,
|
||||
Err(e) => {
|
||||
log::error!("[Leptos Router] Invalid path {path}\n\n{e:?}");
|
||||
prev.clone().unwrap()
|
||||
}
|
||||
path.with(|path| match Url::try_from(path.as_str()) {
|
||||
Ok(url) => url,
|
||||
Err(e) => {
|
||||
log::error!("[Leptos Router] Invalid path {path}\n\n{e:?}");
|
||||
prev.clone().unwrap()
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos_reactive::{create_signal, ReadSignal, Scope};
|
||||
use leptos_reactive::{create_signal, use_context, ReadSignal, Scope};
|
||||
use wasm_bindgen::UnwrapThrowExt;
|
||||
|
||||
mod location;
|
||||
@@ -6,6 +6,8 @@ mod params;
|
||||
mod state;
|
||||
mod url;
|
||||
|
||||
use crate::{NavigateOptions, RouterContext};
|
||||
|
||||
pub use self::url::*;
|
||||
pub use location::*;
|
||||
pub use params::*;
|
||||
@@ -38,21 +40,45 @@ impl History for BrowserIntegration {
|
||||
let (location, set_location) = create_signal(cx, Self::current());
|
||||
|
||||
leptos_dom::window_event_listener("popstate", move |_| {
|
||||
set_location(|change| *change = Self::current());
|
||||
log::debug!(
|
||||
"[BrowserIntegration::location] popstate fired {:#?}",
|
||||
Self::current()
|
||||
);
|
||||
let router = use_context::<RouterContext>(cx);
|
||||
if let Some(router) = router {
|
||||
let change = Self::current();
|
||||
match router.inner.navigate_from_route(
|
||||
&change.value,
|
||||
&NavigateOptions {
|
||||
resolve: false,
|
||||
replace: change.replace,
|
||||
scroll: change.scroll,
|
||||
state: change.state,
|
||||
},
|
||||
) {
|
||||
Ok(_) => log::debug!("navigated"),
|
||||
Err(e) => log::error!("{e:#?}"),
|
||||
};
|
||||
set_location(|change| *change = Self::current());
|
||||
} else {
|
||||
log::debug!("RouterContext not found");
|
||||
}
|
||||
|
||||
//Self::navigate(&Self {}, &Self::current());
|
||||
//set_location(|change| *change = Self::current());
|
||||
});
|
||||
|
||||
location
|
||||
}
|
||||
|
||||
fn navigate(&self, loc: &LocationChange) {
|
||||
log::debug!("[BrowserIntegration::navigate] {loc:#?}");
|
||||
let history = leptos_dom::window().history().unwrap();
|
||||
if loc.replace {
|
||||
log::debug!("replacing state");
|
||||
history
|
||||
.replace_state_with_url(&loc.state.to_js_value(), "", Some(&loc.value))
|
||||
.unwrap_throw();
|
||||
} else {
|
||||
log::debug!("pushing state");
|
||||
history
|
||||
.push_state_with_url(&loc.state.to_js_value(), "", Some(&loc.value))
|
||||
.unwrap_throw();
|
||||
|
||||
@@ -12,6 +12,8 @@ impl Url {
|
||||
pub fn search_params(&self) -> ParamsMap {
|
||||
let map = self
|
||||
.search
|
||||
.strip_prefix('?')
|
||||
.unwrap_or_default()
|
||||
.split('&')
|
||||
.filter_map(|piece| {
|
||||
let mut parts = piece.split('=');
|
||||
|
||||
@@ -28,7 +28,7 @@ pub(crate) fn get_route_matches(branches: Vec<Branch>, location: String) -> Vec<
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Branch {
|
||||
pub routes: Vec<RouteData>,
|
||||
pub score: usize,
|
||||
pub score: i32,
|
||||
}
|
||||
|
||||
impl Branch {
|
||||
|
||||
@@ -66,7 +66,7 @@ pub fn normalize(path: &str, omit_slash: bool) -> Cow<'_, str> {
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn join_paths<'a>(from: &'a str, to: &'a str) -> String {
|
||||
let from = replace_query(&normalize(from, false)).to_string();
|
||||
let from = replace_query(&normalize(from, false));
|
||||
from + &normalize(to, false)
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ const QUERY: &str = r#"/*(\*.*)?$"#;
|
||||
fn replace_trim_path<'a>(text: &'a str, replace: &str) -> Cow<'a, str> {
|
||||
let re = js_sys::RegExp::new(TRIM_PATH, "g");
|
||||
js_sys::JsString::from(text)
|
||||
.replace_by_pattern(&re, "")
|
||||
.replace_by_pattern(&re, replace)
|
||||
.as_string()
|
||||
.unwrap()
|
||||
.into()
|
||||
@@ -98,7 +98,6 @@ fn replace_query(text: &str) -> String {
|
||||
.replace_by_pattern(&re, "")
|
||||
.as_string()
|
||||
.unwrap()
|
||||
.into()
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
|
||||
@@ -38,7 +38,7 @@ impl Default for RouteDefinition {
|
||||
loader: Default::default(),
|
||||
action: Default::default(),
|
||||
children: Default::default(),
|
||||
element: Rc::new(|cx| Child::Null),
|
||||
element: Rc::new(|_| Child::Null),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user