Compare commits

..

127 Commits

Author SHA1 Message Date
Greg Johnston
9e8b559e51 handle optional props correctly 2023-08-19 07:21:56 -04:00
Greg Johnston
db4ce00f98 add MaybeSignal deserialization 2023-08-19 07:21:56 -04:00
Greg Johnston
9edcc5f43b add support for island props 2023-08-19 07:21:56 -04:00
Greg Johnston
ea2cda3d17 work on serializing props 2023-08-19 07:21:56 -04:00
Greg Johnston
1d5252b97e fix iOS Safari pleeeaaassseeee stop it, Apple 2023-08-19 07:21:56 -04:00
Greg Johnston
d175249d00 fix build 2023-08-19 07:21:56 -04:00
Greg Johnston
35a9d84734 ignore rIC if not supported 2023-08-19 07:21:56 -04:00
Greg Johnston
c8a5434c9b start work on serializing props in DOM 2023-08-19 07:21:56 -04:00
Greg Johnston
ac00ea4ffe fix merge issues 2023-08-19 07:21:56 -04:00
Greg Johnston
59b708db9f support suspense with islands 2023-08-19 07:21:56 -04:00
Greg Johnston
722f6913d1 make whole JS import idle-only 2023-08-19 07:21:56 -04:00
Greg Johnston
aabcfb7ac3 switch to data-hk for all use cases 2023-08-19 07:21:56 -04:00
Greg Johnston
e691ae391f fix re-layouts on hydration (WIP) 2023-08-19 07:21:56 -04:00
Greg Johnston
7c3fd1d67d use correct name for hkc 2023-08-19 07:21:56 -04:00
Greg Johnston
88ee228389 turn fragments back off 2023-08-19 07:21:56 -04:00
Greg Johnston
02b0ac15a3 make whole hydration process idle 2023-08-19 07:21:56 -04:00
Greg Johnston
83edcfe29d restore compression 2023-08-19 07:21:56 -04:00
Greg Johnston
b288b7d838 do set fragment 2023-08-19 07:21:56 -04:00
Greg Johnston
a1a78f165a axum islands example 2023-08-19 07:21:56 -04:00
Greg Johnston
1f3453e94b rIC per island and resuming based on keys both working 2023-08-19 07:21:56 -04:00
Greg Johnston
e8b35ba284 work on resuming HK 2023-08-19 07:21:56 -04:00
Greg Johnston
02275d4fc0 requestIdleCallback per island 2023-08-19 07:21:56 -04:00
Greg Johnston
18e44be19a playing with islands hackernews 2023-08-19 07:21:56 -04:00
Greg Johnston
0bd4e5269a request idle callback before hydration 2023-08-19 07:21:56 -04:00
Greg Johnston
88405bfd10 intern id removal 2023-08-19 07:21:56 -04:00
Greg Johnston
80bc9a4e10 islands 2023-08-19 07:21:56 -04:00
Greg Johnston
b4c3068ddd got children working! 2023-08-19 07:21:56 -04:00
Greg Johnston
ac5406b8a6 fix merge issues 2023-08-19 07:21:56 -04:00
Greg Johnston
8f38a8a5f5 do the island hydration directly in JS, save a kb 2023-08-19 07:21:56 -04:00
Greg Johnston
bd7ff6a3fc support multiple islands, oops 2023-08-19 07:21:56 -04:00
Greg Johnston
c2d9696494 working super-basic islands implementation 2023-08-19 07:21:56 -04:00
Greg Johnston
35893d4eb9 release mode benchmarks 2023-08-19 07:20:10 -04:00
Greg Johnston
c3959d1de8 make hydration keys optional (to allow NoHydrate areas) 2023-08-19 07:20:10 -04:00
Greg Johnston
76c9d8233a islands 2023-08-19 07:20:10 -04:00
Greg Johnston
2988fc1c36 initial islands work 2023-08-19 07:20:09 -04:00
Greg Johnston
e7662ae940 perf: don't include unused hydration code in CSR bundles 2023-08-19 07:20:09 -04:00
Greg Johnston
674fcd2ade stash 2023-08-19 07:20:09 -04:00
Greg Johnston
5beab9f3cc fix examples 2023-08-19 07:03:54 -04:00
Greg Johnston
49be4a219d .map_ref() => .map() 2023-08-18 14:14:45 -04:00
Greg Johnston
f92aebdf01 cargo fmt example 2023-08-18 14:14:07 -04:00
Greg Johnston
6ba7a8e235 nope 2023-08-18 14:07:42 -04:00
Greg Johnston
10e3106760 removing cx/Scope from some merged docs 2023-08-18 08:41:37 -04:00
Greg Johnston
63dee1c93f closes issue #1558 2023-08-18 08:36:56 -04:00
Greg Johnston
fca4215fdb Change resource API to .get(), .with(), .map_ref(), and .and_then() 2023-08-18 08:36:56 -04:00
Greg Johnston
72cde5c355 use tuple form of .child() 2023-08-18 08:36:56 -04:00
Greg Johnston
0bf1f11638 avoid panic in use_navigate during SSR 2023-08-18 08:36:56 -04:00
Greg Johnston
2ea4531313 0.5.0-beta 2023-08-18 08:36:56 -04:00
Greg Johnston
bd2acfc530 fix example 2023-08-18 08:36:56 -04:00
Greg Johnston
42ff663622 sometimes Rust's variable scoping rules baffle me (should fix panic with overlapping BorrowMut in on_cleanup) 2023-08-18 08:36:56 -04:00
Greg Johnston
8ffd5c69ab erroneous log 2023-08-18 08:36:56 -04:00
Greg Johnston
90ce31edf8 fix unsetting of title when navigating between multiple pages 2023-08-18 08:36:56 -04:00
Greg Johnston
e7160092f6 fix Suspense-For-Suspense panic 2023-08-18 08:36:56 -04:00
Greg Johnston
c6bd7a40e7 pull in changes to view macor from main 2023-08-18 08:36:56 -04:00
Greg Johnston
d544a678b1 remove removed API 2023-08-18 08:36:56 -04:00
Greg Johnston
4b96a71da5 on_cleanup untracked (fixes #1494) 2023-08-18 08:36:56 -04:00
Greg Johnston
6ab95d8a16 remove deprecated APIs 2023-08-18 08:36:56 -04:00
Greg Johnston
db51f6d2b4 embed requestAnimationFrame into navigate() 2023-08-18 08:36:56 -04:00
Greg Johnston
964a26b5d3 BAD rebase, NO rebase. 2023-08-18 08:36:56 -04:00
Greg Johnston
1871d2bc6d impl Serialize and Deserialize for signal types 2023-08-18 08:36:56 -04:00
Greg Johnston
fc82788bcc add some useful From implementations 2023-08-18 08:36:56 -04:00
Ben Wishovich
dba5c444ae fix: render_route() path matching (#1479) 2023-08-18 08:36:56 -04:00
Greg Johnston
95f285f126 fix <For/> in SSR 2023-08-18 08:36:56 -04:00
Greg Johnston
5865ea4048 fix: restore missing fixes in <For/> (closes #1473) 2023-08-18 08:36:56 -04:00
Greg Johnston
121c312ba3 alpha 2 2023-08-18 08:36:56 -04:00
Greg Johnston
19967d1dd5 remove duplicate from merge 2023-08-18 08:36:56 -04:00
Greg Johnston
42a1395ffd clippy 2023-08-18 08:36:56 -04:00
Greg Johnston
421715f923 clean up window click listener 2023-08-18 08:36:56 -04:00
Greg Johnston
0eba690993 cancelable handle for window_event_listener 2023-08-18 08:36:56 -04:00
Greg Johnston
2a971b5fd3 remove scope 2023-08-18 08:36:56 -04:00
Greg Johnston
8701bd88bd closes #1465 2023-08-18 08:36:56 -04:00
Greg Johnston
8ccc731e91 better error messages for expect_context 2023-08-18 08:36:56 -04:00
Greg Johnston
b9bb999b6a was breaking something 2023-08-18 08:36:56 -04:00
Greg Johnston
d7080bff96 don't double-create children in nested suspense in ssr 2023-08-18 08:36:56 -04:00
Greg Johnston
52eea6222b track caller for better error messages 2023-08-18 08:36:56 -04:00
Greg Johnston
c1c469fd08 better error messages 2023-08-18 08:36:56 -04:00
Greg Johnston
42d18d4b54 clippy 2023-08-18 08:36:56 -04:00
Greg Johnston
e69d55190e fix routing progress display 2023-08-18 08:36:56 -04:00
Ben Wishovich
431f5398f9 Add render_route functions to respect SsrMode on routes when using custom handlers (#1460) 2023-08-18 08:36:56 -04:00
Greg Johnston
3223182141 add <Route data/> loaders again 2023-08-18 08:36:56 -04:00
Greg Johnston
815acee19b even even better error message 2023-08-18 08:36:56 -04:00
Greg Johnston
270622ce86 typo in doctest 2023-08-18 08:36:56 -04:00
Greg Johnston
de97e2dc12 fix ssr tests 2023-08-18 08:36:56 -04:00
Greg Johnston
3dd12bf416 fix release build SSR 2023-08-18 08:36:56 -04:00
Greg Johnston
59d3450d5b gtfo 2023-08-18 08:36:56 -04:00
Greg Johnston
02d0849a34 don't need to render EachItem markers for View::Element nodes 2023-08-18 08:36:56 -04:00
Greg Johnston
41f8d66565 restore continuing from current id 2023-08-18 08:36:56 -04:00
Greg Johnston
8da6710e44 add separate error field in hydration keys 2023-08-18 08:36:56 -04:00
IcosaHedron
38a68926ca cleanups must be called before properties are disposed (#1449) 2023-08-18 08:36:56 -04:00
Greg Johnston
895c8765ed add MaybeProp 2023-08-18 08:36:56 -04:00
Greg Johnston
ddd797f853 clean up logs 2023-08-18 08:36:56 -04:00
Greg Johnston
39dd204cda fix spawn docs examples 2023-08-18 08:36:56 -04:00
Greg Johnston
90041a9e99 fix reactive tests 2023-08-18 08:36:56 -04:00
Greg Johnston
d3a6d59970 cx in tests 2023-08-18 08:36:56 -04:00
Greg Johnston
314c803e4d v0.5.0-alpha 2023-08-18 08:36:56 -04:00
Greg Johnston
f5028e200e add some panic docs 2023-08-18 08:36:56 -04:00
Greg Johnston
fd15859ee4 clean up docs 2023-08-18 08:36:56 -04:00
Greg Johnston
d4ede63b3a clippy 2023-08-18 08:36:42 -04:00
Greg Johnston
b7c4a9d5c7 fix errorboundary 2023-08-18 08:36:42 -04:00
Greg Johnston
c780924c3d fix views in component macro docs 2023-08-18 08:36:42 -04:00
Greg Johnston
d2e8981e94 component macro 2023-08-18 08:36:42 -04:00
Greg Johnston
449a14bf16 fix issues in examples 2023-08-18 08:36:42 -04:00
Greg Johnston
5cc4c977ad tests 2023-08-18 08:36:42 -04:00
Greg Johnston
67ee6d5dbf ssr tests 2023-08-18 08:36:42 -04:00
Greg Johnston
919ab91cb0 fix 2023-08-18 08:36:42 -04:00
Greg Johnston
3f0c908479 fixing tests 2023-08-18 08:36:42 -04:00
Greg Johnston
a7cf566700 clippy 2023-08-18 08:36:42 -04:00
Greg Johnston
a30356da12 remove _ = cx; 2023-08-18 08:36:42 -04:00
Greg Johnston
c612c2c0a3 clippy 2023-08-18 08:36:42 -04:00
Greg Johnston
370d8a951a fix merge issues 2023-08-18 08:36:42 -04:00
Greg Johnston
b3d75125d4 clean up 2023-08-18 08:36:42 -04:00
Greg Johnston
b12b397f15 clean up 2023-08-18 08:36:42 -04:00
Greg Johnston
39d9b806ff remove Scope from docs 2023-08-18 08:36:42 -04:00
Greg Johnston
02c4bb31bc remove cx from docs 2023-08-18 08:36:41 -04:00
Greg Johnston
52f0ce4b48 fix Viz 2023-08-18 08:35:57 -04:00
Greg Johnston
b8d30676f8 move forbid(unsafe) to crate level 2023-08-18 08:35:57 -04:00
Greg Johnston
e7b52c0076 fix watch 2023-08-18 08:35:57 -04:00
Greg Johnston
8cc3ece052 even better error msg 2023-08-18 08:35:57 -04:00
Greg Johnston
a19e83818d better error when failing to deserialize resource JSON 2023-08-18 08:35:57 -04:00
Greg Johnston
e92029d36c fix suspense ownership chain 2023-08-18 08:35:57 -04:00
Greg Johnston
4a9d959488 unused import 2023-08-18 08:35:57 -04:00
Greg Johnston
9fd9d94783 get AnimatedOutlet working 2023-08-18 08:35:57 -04:00
Greg Johnston
241ca5e4dd restore <AnimatedRoutes/> 2023-08-18 08:35:57 -04:00
Greg Johnston
39e81136c5 fix suspense 2023-08-18 08:35:57 -04:00
Greg Johnston
0ebf63f8af cleaning up tests 2023-08-18 08:35:57 -04:00
Greg Johnston
0b532735fc default impls 2023-08-18 08:35:57 -04:00
Greg Johnston
40998f62c3 don't store runtime ID in signals 2023-08-18 08:35:57 -04:00
Greg Johnston
4df6057393 squash work on reactive ownership 2023-08-18 08:35:57 -04:00
158 changed files with 4271 additions and 2348 deletions

View File

@@ -9,21 +9,66 @@ on:
- main
jobs:
get-leptos-changed:
uses: ./.github/workflows/get-leptos-changed.yml
setup:
name: Get Examples
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
source_changed: ${{ steps.set-source-changed.outputs.source_changed }}
steps:
- name: Checkout
uses: actions/checkout@v3
get-examples-matrix:
uses: ./.github/workflows/get-examples-matrix.yml
- name: Install JQ Tool
uses: mbround18/install-jq@v1
test:
- name: Set Matrix
id: set-matrix
run: |
examples=$(ls examples |
awk '{print "examples/" $0}' |
grep -v examples/README.md |
grep -v examples/Makefile.toml |
grep -v examples/cargo-make |
grep -v examples/gtk |
jq -R -s -c 'split("\n")[:-1]')
echo "Example Directories: $examples"
echo "matrix={\"directory\":$examples}" >> "$GITHUB_OUTPUT"
- name: Get source files that changed
id: changed-source
uses: tj-actions/changed-files@v36
with:
files: |
integrations
leptos
leptos_config
leptos_dom
leptos_hot_reload
leptos_macro
leptos_reactive
leptos_server
meta
router
server_fn
server_fn_macro
- name: List source files that changed
run: echo '${{ steps.changed-source.outputs.all_changed_files }}'
- name: Set source_changed
id: set-source-changed
run: |
echo "source_changed=${{ steps.changed-source.outputs.any_changed }}" >> "$GITHUB_OUTPUT"
matrix-job:
name: Check
needs: [get-leptos-changed, get-examples-matrix]
if: needs.get-leptos-changed.outputs.leptos_changed == 'true'
needs: [setup]
if: needs.setup.outputs.source_changed == 'true'
strategy:
matrix: ${{ fromJSON(needs.get-examples-matrix.outputs.matrix) }}
matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
fail-fast: false
uses: ./.github/workflows/run-cargo-make-task.yml
with:
directory: ${{ matrix.directory }}
cargo_make_task: "check"
toolchain: nightly

View File

@@ -2,25 +2,82 @@ name: Check stable
on:
push:
branches:
- main
branches: [main]
pull_request:
branches:
- main
branches: [main]
env:
CARGO_TERM_COLOR: always
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
jobs:
get-leptos-changed:
uses: ./.github/workflows/get-leptos-changed.yml
setup:
name: Detect Changes
runs-on: ubuntu-latest
outputs:
source_changed: ${{ steps.set-source-changed.outputs.source_changed }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Get source files that changed
id: changed-source
uses: tj-actions/changed-files@v36
with:
files: |
integrations
leptos
leptos_config
leptos_dom
leptos_hot_reload
leptos_macro
leptos_reactive
leptos_server
meta
router
server_fn
server_fn_macro
- name: List source files that changed
run: echo '${{ steps.changed-source.outputs.all_changed_files }}'
- name: Set source_changed
id: set-source-changed
run: |
echo "source_changed=${{ steps.changed-source.outputs.any_changed }}" >> "$GITHUB_OUTPUT"
test:
name: Check
needs: [get-leptos-changed]
if: needs.get-leptos-changed.outputs.leptos_changed == 'true'
name: Check examples ${{ matrix.os }} (using rustc ${{ matrix.rust }})
needs: [setup]
if: needs.setup.outputs.source_changed == 'true'
runs-on: ${{ matrix.os }}
strategy:
matrix:
directory: [examples/counters_stable, examples/counter_without_macros]
uses: ./.github/workflows/run-cargo-make-task.yml
with:
directory: ${{ matrix.directory }}
cargo_make_task: "check"
toolchain: stable
rust:
- stable
os:
- ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
override: true
components: rustfmt
- name: Add wasm32-unknown-unknown
run: rustup target add wasm32-unknown-unknown
- name: Setup cargo-make
uses: davidB/rust-cargo-make@v1
- name: Cargo generate-lockfile
run: cargo generate-lockfile
- uses: Swatinem/rust-cache@v2
- name: Run cargo check on all examples
run: cargo make --profile=github-actions check-stable

View File

@@ -1,32 +0,0 @@
name: CI Changed Examples
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
get-example-changed:
uses: ./.github/workflows/get-example-changed.yml
get-matrix:
needs: [get-example-changed]
uses: ./.github/workflows/get-changed-examples-matrix.yml
with:
example_changed: ${{ fromJSON(needs.get-example-changed.outputs.example_changed) }}
test:
name: CI
needs: [get-example-changed, get-matrix]
if: needs.get-example-changed.outputs.example_changed == 'true'
strategy:
matrix: ${{ fromJSON(needs.get-matrix.outputs.matrix) }}
fail-fast: false
uses: ./.github/workflows/run-cargo-make-task.yml
with:
directory: ${{ matrix.directory }}
cargo_make_task: "ci"
toolchain: nightly

View File

@@ -9,13 +9,45 @@ on:
- main
jobs:
get-leptos-changed:
uses: ./.github/workflows/get-leptos-changed.yml
setup:
name: Detect Changes
runs-on: ubuntu-latest
outputs:
source_changed: ${{ steps.set-source-changed.outputs.source_changed }}
steps:
- name: Checkout
uses: actions/checkout@v3
test:
- name: Get source files that changed
id: changed-source
uses: tj-actions/changed-files@v36
with:
files: |
integrations
leptos
leptos_config
leptos_dom
leptos_hot_reload
leptos_macro
leptos_reactive
leptos_server
meta
router
server_fn
server_fn_macro
- name: List source files that changed
run: echo '${{ steps.changed-source.outputs.all_changed_files }}'
- name: Set source_changed
id: set-source-changed
run: |
echo "source_changed=${{ steps.changed-source.outputs.any_changed }}" >> "$GITHUB_OUTPUT"
matrix-job:
name: CI
needs: [get-leptos-changed]
if: needs.get-leptos-changed.outputs.leptos_changed == 'true'
needs: [setup]
if: needs.setup.outputs.source_changed == 'true'
strategy:
matrix:
directory:
@@ -41,4 +73,3 @@ jobs:
with:
directory: ${{ matrix.directory }}
cargo_make_task: "ci"
toolchain: nightly

View File

@@ -1,39 +0,0 @@
name: Examples Changed Call
on:
workflow_call:
outputs:
example_changed:
description: "Example Changed"
value: ${{ jobs.get-example-changed.outputs.example_changed }}
jobs:
get-example-changed:
name: Get Example Changed
runs-on: ubuntu-latest
outputs:
example_changed: ${{ steps.set-example-changed.outputs.example_changed }}
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Get example files that changed
id: changed-files
uses: tj-actions/changed-files@v36
with:
files: |
examples
!examples/cargo-make
!examples/gtk
!examples/Makefile.toml
!examples/README.md
- name: List example files that changed
run: echo '${{ steps.changed-files.outputs.all_changed_files }}'
- name: Set example_changed
id: set-example-changed
run: |
echo "example_changed=${{ steps.changed-files.outputs.any_changed }}" >> "$GITHUB_OUTPUT"

View File

@@ -1,40 +0,0 @@
name: Get Examples Matrix Call
on:
workflow_call:
outputs:
matrix:
description: "Matrix"
value: ${{ jobs.create.outputs.matrix }}
jobs:
create:
name: Create Examples Matrix
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install JQ Tool
uses: mbround18/install-jq@v1
- name: Set Matrix
id: set-matrix
run: |
examples=$(ls examples |
awk '{print "examples/" $0}' |
grep -v examples/README.md |
grep -v examples/Makefile.toml |
grep -v examples/cargo-make |
grep -v examples/gtk |
jq -R -s -c 'split("\n")[:-1]')
echo "Example Directories: $examples"
echo "matrix={\"directory\":$examples}" >> "$GITHUB_OUTPUT"
- name: Print Location Info
run: |
echo "Workspace: ${{ github.workspace }}"
pwd
ls | sort -u

View File

@@ -1,44 +0,0 @@
name: Get Leptos Changed Call
on:
workflow_call:
outputs:
leptos_changed:
description: "Leptos Changed"
value: ${{ jobs.create.outputs.leptos_changed }}
jobs:
create:
name: Detect Source Change
runs-on: ubuntu-latest
outputs:
leptos_changed: ${{ steps.set-source-changed.outputs.leptos_changed }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Get source files that changed
id: changed-source
uses: tj-actions/changed-files@v36
with:
files: |
integrations
leptos
leptos_config
leptos_dom
leptos_hot_reload
leptos_macro
leptos_reactive
leptos_server
meta
router
server_fn
server_fn_macro
- name: List source files that changed
run: echo '${{ steps.changed-source.outputs.all_changed_files }}'
- name: Set leptos_changed
id: set-source-changed
run: |
echo "leptos_changed=${{ steps.changed-source.outputs.any_changed }}" >> "$GITHUB_OUTPUT"

View File

@@ -1,8 +1,9 @@
name: Deploy book
on:
push:
tags:
- '*-?v[0-9]+*'
paths: ['docs/book/**']
branches:
- main
jobs:
deploy:
@@ -33,4 +34,4 @@ jobs:
mv ../book/* .
git add .
git commit -m "Deploy book $GITHUB_SHA to gh-pages"
git push --force --set-upstream origin gh-pages
git push --force --set-upstream origin gh-pages

View File

@@ -9,27 +9,34 @@ on:
cargo_make_task:
required: true
type: string
toolchain:
required: true
type: string
env:
CARGO_TERM_COLOR: always
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
jobs:
test:
name: Run ${{ inputs.cargo_make_task }} (${{ inputs.toolchain }})
runs-on: ubuntu-latest
name: Run ${{ matrix.os }} (using rustc ${{ matrix.rust }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
rust:
- nightly
os:
- ubuntu-latest
steps:
# Setup environment
- name: Install playwright browser dependencies
run: |
sudo apt-get update
sudo apt-get install libegl1 libvpx7 libevent-2.1-7 libopus0 libopengl0 libwoff1 libharfbuzz-icu0 libgstreamer-plugins-base1.0-0 libgstreamer-gl1.0-0 libhyphen0 libmanette-0.2-0 libgles2 gstreamer1.0-libav
- uses: actions/checkout@v3
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ inputs.toolchain }}
toolchain: ${{ matrix.rust }}
override: true
components: rustfmt
@@ -77,35 +84,17 @@ jobs:
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Maybe install chromedriver
- name: Install Chrome Webriver
run: |
project_makefile=${{inputs.directory}}/Makefile.toml
webdriver_count=$(cat $project_makefile | grep "cargo-make/webdriver.toml" | wc -l)
if [ $webdriver_count -eq 1 ]; then
if ! command -v chromedriver &>/dev/null; then
echo chromedriver required
sudo apt-get update
sudo apt-get install chromium-chromedriver
else
echo chromedriver is already installed
fi
else
echo chromedriver is not required
fi
- name: Maybe install playwright browser dependencies
run: |
playwright_count=$(find ${{inputs.directory}} -name playwright.config.ts | wc -l)
if [ $playwright_count -eq 1 ]; then
echo playwright required
sudo apt-get update
sudo apt-get install libegl1 libvpx7 libevent-2.1-7 libopus0 libopengl0 libwoff1 libharfbuzz-icu0 libgstreamer-plugins-base1.0-0 libgstreamer-gl1.0-0 libhyphen0 libmanette-0.2-0 libgles2 gstreamer1.0-libav
else
echo playwright is not required
fi
sudo apt-get update
sudo apt-get install chromium-chromedriver
# Run Cargo Make Task
- name: ${{ inputs.cargo_make_task }}
run: |
cd ${{ inputs.directory }}
cargo make --profile=github-actions ${{ inputs.cargo_make_task }}
if [ "${{ inputs.directory }}" = "INTERNAL" ]; then
echo No verification required
else
cd ${{ inputs.directory }}
cargo make --profile=github-actions ${{ inputs.cargo_make_task }}
fi

View File

@@ -1,4 +1,4 @@
name: CI Examples
name: Verify All Examples
on:
workflow_dispatch:
@@ -10,17 +10,38 @@ on:
- cron: "0 8 * * *"
jobs:
get-examples-matrix:
uses: ./.github/workflows/get-examples-matrix.yml
setup:
name: Get Examples
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Checkout
uses: actions/checkout@v3
test:
name: CI
needs: [get-examples-matrix]
- name: Install JQ Tool
uses: mbround18/install-jq@v1
- name: Set Matrix
id: set-matrix
run: |
examples=$(ls examples |
awk '{print "examples/" $0}' |
grep -v examples/README.md |
grep -v examples/Makefile.toml |
grep -v examples/cargo-make |
grep -v examples/gtk |
jq -R -s -c 'split("\n")[:-1]')
echo "Example Directories: $examples"
echo "matrix={\"directory\":$examples}" >> "$GITHUB_OUTPUT"
matrix-job:
name: Verify
needs: [setup]
strategy:
matrix: ${{ fromJSON(needs.get-examples-matrix.outputs.matrix) }}
matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
fail-fast: false
uses: ./.github/workflows/run-cargo-make-task.yml
with:
directory: ${{ matrix.directory }}
cargo_make_task: "ci"
toolchain: nightly
cargo_make_task: "verify-flow"

View File

@@ -1,20 +1,16 @@
name: Changed Examples Matrix Call
name: Verify Changed Examples
on:
workflow_call:
inputs:
example_changed:
description: "Example Changed"
required: true
type: boolean
outputs:
matrix:
description: "Matrix"
value: ${{ jobs.get-example-changed.outputs.matrix }}
push:
branches:
- main
pull_request:
branches:
- main
jobs:
get-example-changed:
name: Get Changed Example Matrix
setup:
name: Get Changes
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
@@ -24,6 +20,16 @@ jobs:
with:
fetch-depth: 0
- name: Get all example files that changed
id: changed-files
uses: tj-actions/changed-files@v36
with:
files: |
examples
- name: List all example files that changed
run: echo '${{ steps.changed-files.outputs.all_changed_files }}'
- name: Get example project directories that changed
id: changed-dirs
uses: tj-actions/changed-files@v36
@@ -45,10 +51,21 @@ jobs:
- name: Set Matrix
id: set-matrix
run: |
if [ ${{ inputs.example_changed }} == 'true' ]; then
if [ ${{ steps.changed-files.outputs.any_changed }} == 'true' ]; then
# Create matrix with changed directories
echo "matrix={\"directory\":${{ steps.changed-dirs.outputs.all_changed_files }}}" >> "$GITHUB_OUTPUT"
else
# Create matrix with one item to prevent an empty vector error
echo "matrix={\"directory\":[\"NO_CHANGE\"]}" >> "$GITHUB_OUTPUT"
echo "matrix={\"directory\":[\"INTERNAL\"]}" >> "$GITHUB_OUTPUT"
fi
matrix-job:
name: Verify
needs: [setup]
strategy:
matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
fail-fast: false
uses: ./.github/workflows/run-cargo-make-task.yml
with:
directory: ${{ matrix.directory }}
cargo_make_task: "verify-flow"

View File

@@ -26,22 +26,22 @@ members = [
exclude = ["benchmarks", "examples"]
[workspace.package]
version = "0.5.0"
version = "0.5.0-beta"
[workspace.dependencies]
leptos = { path = "./leptos", version = "0.5.0" }
leptos_dom = { path = "./leptos_dom", version = "0.5.0" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.5.0" }
leptos_macro = { path = "./leptos_macro", version = "0.5.0" }
leptos_reactive = { path = "./leptos_reactive", version = "0.5.0" }
leptos_server = { path = "./leptos_server", version = "0.5.0" }
server_fn = { path = "./server_fn", version = "0.5.0" }
server_fn_macro = { path = "./server_fn_macro", version = "0.5.0" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.5.0" }
leptos_config = { path = "./leptos_config", version = "0.5.0" }
leptos_router = { path = "./router", version = "0.5.0" }
leptos_meta = { path = "./meta", version = "0.5.0" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.5.0" }
leptos = { path = "./leptos", version = "0.5.0-beta" }
leptos_dom = { path = "./leptos_dom", version = "0.5.0-beta" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.5.0-beta" }
leptos_macro = { path = "./leptos_macro", version = "0.5.0-beta" }
leptos_reactive = { path = "./leptos_reactive", version = "0.5.0-beta" }
leptos_server = { path = "./leptos_server", version = "0.5.0-beta" }
server_fn = { path = "./server_fn", version = "0.5.0-beta" }
server_fn_macro = { path = "./server_fn_macro", version = "0.5.0-beta" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.5.0-beta" }
leptos_config = { path = "./leptos_config", version = "0.5.0-beta" }
leptos_router = { path = "./router", version = "0.5.0-beta" }
leptos_meta = { path = "./meta", version = "0.5.0-beta" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.5.0-beta" }
[profile.release]
codegen-units = 1

View File

@@ -16,9 +16,9 @@
use leptos::*;
#[component]
pub fn SimpleCounter(initial_value: i32) -> impl IntoView {
pub fn SimpleCounter(cx: Scope, initial_value: i32) -> impl IntoView {
// create a reactive signal with the initial value
let (value, set_value) = create_signal(initial_value);
let (value, set_value) = create_signal(cx, initial_value);
// create event handlers for our buttons
// note that `value` and `set_value` are `Copy`, so it's super easy to move them into closures
@@ -27,29 +27,23 @@ pub fn SimpleCounter(initial_value: i32) -> impl IntoView {
let increment = move |_| set_value.update(|value| *value += 1);
// create user interfaces with the declarative `view!` macro
view! {
view! { cx,
<div>
<button on:click=clear>Clear</button>
<button on:click=decrement>-1</button>
// text nodes can be quoted or unquoted
<button on:click=clear>"Clear"</button>
<button on:click=decrement>"-1"</button>
<span>"Value: " {value} "!"</span>
<button on:click=increment>+1</button>
<button on:click=increment>"+1"</button>
</div>
}
}
// Easy to use with Trunk (trunkrs.dev) or with a simple wasm-bindgen setup
pub fn main() {
mount_to_body(|| view! {
<SimpleCounter initial_value=3 />
})
mount_to_body(|cx| view! { cx, <SimpleCounter initial_value=3 /> })
}
```
### Important Note
This example, and the entire `main` branch, now reflect the upcoming `0.5.0` release. You can use `0.5.0` with the `0.5.0-beta` release on crates.io or by a git dependency on the `main` branch of this repo. [Click here for the 0.4.9 `README`](https://crates.io/crates/leptos).
## About the Framework
Leptos is a full-stack, isomorphic Rust web framework leveraging fine-grained reactivity to build declarative user interfaces.
@@ -119,7 +113,7 @@ People usually mean one of three things by this question.
1. **Are the APIs stable?** i.e., will I have to rewrite my whole app from Leptos 0.1 to 0.2 to 0.3 to 0.4, or can I write it now and benefit from new features and updates as new versions come?
The APIs are basically settled. Were adding new features, but were very happy with where the type system and patterns have landed. I would not expect major breaking changes to your code to adapt to future releases, in terms of architecture.
The APIs are basically settled. Were adding new features, but were very happy with where the type system and patterns have landed. I would not expect major breaking changes to your code to adapt to future releases. The sorts of breaking changes that we discuss are things like “Oh yeah, that function should probably take `cx` as its argument...” not major changes to the way you write your application.
2. **Are there bugs?**
@@ -158,13 +152,13 @@ There are some practical differences that make a significant difference:
- **Templating:** Leptos uses a JSX-like template format (built on [syn-rsx](https://github.com/stoically/syn-rsx)) for its `view` macro. Sycamore offers the choice of its own templating DSL or a builder syntax.
- **Server integration:** Leptos provides primitives that encourage HTML streaming and allow for easy async integration and RPC calls, even without WASM enabled, making it easy to opt into integrations between your frontend and backend code without pushing you toward any particular metaframework patterns.
- **Read-write segregation:** Leptos, like Solid, encourages read-write segregation between signal getters and setters, so you end up accessing signals with tuples like `let (count, set_count) = create_signal(0);` _(If you prefer or if it's more convenient for your API, you can use [`create_rw_signal`](https://docs.rs/leptos/latest/leptos/fn.create_rw_signal.html) to give a unified read/write signal.)_
- **Read-write segregation:** Leptos, like Solid, encourages read-write segregation between signal getters and setters, so you end up accessing signals with tuples like `let (count, set_count) = create_signal(cx, 0);` _(If you prefer or if it's more convenient for your API, you can use [`create_rw_signal`](https://docs.rs/leptos/latest/leptos/fn.create_rw_signal.html) to give a unified read/write signal.)_
- **Signals are functions:** In Leptos, you can call a signal to access it rather than calling a specific method (so, `count()` instead of `count.get()`) This creates a more consistent mental model: accessing a reactive value is always a matter of calling a function. For example:
```rust
let (count, set_count) = create_signal(0); // a signal
let (count, set_count) = create_signal(cx, 0); // a signal
let double_count = move || count() * 2; // a derived signal
let memoized_count = create_memo(move |_| count() * 3); // a memo
let memoized_count = create_memo(cx, move |_| count() * 3); // a memo
// all are accessed by calling them
assert_eq!(count(), 0);
assert_eq!(double_count(), 0);

View File

@@ -5,9 +5,9 @@ edition = "2021"
[dependencies]
l021 = { package = "leptos", version = "0.2.1" }
leptos = { path = "../leptos", features = ["ssr"] }
leptos = { path = "../leptos", features = ["ssr", "nightly", "islands"] }
sycamore = { version = "0.8", features = ["ssr"] }
yew = { git = "https://github.com/yewstack/yew", features = ["ssr"] }
yew = { version = "0.20", features = ["ssr"] }
tokio-test = "0.4"
miniserde = "0.1"
gloo = "0.8"

View File

@@ -7,15 +7,15 @@ fn leptos_deep_creation(b: &mut Bencher) {
let runtime = create_runtime();
b.iter(|| {
create_scope(runtime, || {
let signal = create_rw_signal(0);
create_scope(runtime, |cx| {
let signal = create_rw_signal(cx, 0);
let mut memos = Vec::<Memo<usize>>::new();
for _ in 0..1000usize {
let prev = memos.last().copied();
if let Some(prev) = prev {
memos.push(create_memo(move |_| prev.get() + 1));
memos.push(create_memo(cx, move |_| prev.get() + 1));
} else {
memos.push(create_memo(move |_| signal.get() + 1));
memos.push(create_memo(cx, move |_| signal.get() + 1));
}
}
})
@@ -31,14 +31,14 @@ fn leptos_deep_update(b: &mut Bencher) {
let runtime = create_runtime();
b.iter(|| {
create_scope(runtime, || {
let signal = create_rw_signal(0);
create_scope(runtime, |cx| {
let signal = create_rw_signal(cx, 0);
let mut memos = Vec::<Memo<usize>>::new();
for _ in 0..1000usize {
if let Some(prev) = memos.last().copied() {
memos.push(create_memo(move |_| prev.get() + 1));
memos.push(create_memo(cx, move |_| prev.get() + 1));
} else {
memos.push(create_memo(move |_| signal.get() + 1));
memos.push(create_memo(cx, move |_| signal.get() + 1));
}
}
signal.set(1);
@@ -56,11 +56,12 @@ fn leptos_narrowing_down(b: &mut Bencher) {
let runtime = create_runtime();
b.iter(|| {
create_scope(runtime, || {
let sigs = (0..1000).map(|n| create_signal(n)).collect::<Vec<_>>();
create_scope(runtime, |cx| {
let sigs =
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
let memo = create_memo(move |_| {
let memo = create_memo(cx, move |_| {
reads.iter().map(|r| r.get()).sum::<i32>()
});
assert_eq!(memo(), 499500);
@@ -77,10 +78,10 @@ fn leptos_fanning_out(b: &mut Bencher) {
let runtime = create_runtime();
b.iter(|| {
create_scope(runtime, || {
let sig = create_rw_signal(0);
create_scope(runtime, |cx| {
let sig = create_rw_signal(cx, 0);
let memos = (0..1000)
.map(|_| create_memo(move |_| sig.get()))
.map(|_| create_memo(cx, move |_| sig.get()))
.collect::<Vec<_>>();
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
sig.set(1);
@@ -98,16 +99,17 @@ fn leptos_narrowing_update(b: &mut Bencher) {
let runtime = create_runtime();
b.iter(|| {
create_scope(runtime, || {
create_scope(runtime, |cx| {
let acc = Rc::new(Cell::new(0));
let sigs = (0..1000).map(|n| create_signal(n)).collect::<Vec<_>>();
let sigs =
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
let memo = create_memo(move |_| {
let memo = create_memo(cx, move |_| {
reads.iter().map(|r| r.get()).sum::<i32>()
});
assert_eq!(memo(), 499500);
create_isomorphic_effect({
create_isomorphic_effect(cx, {
let acc = Rc::clone(&acc);
move |_| {
acc.set(memo());
@@ -139,9 +141,9 @@ fn leptos_scope_creation_and_disposal(b: &mut Bencher) {
.map(|_| {
create_scope(runtime, {
let acc = Rc::clone(&acc);
move || {
let (r, w) = create_signal(0);
create_isomorphic_effect({
move |cx| {
let (r, w) = create_signal(cx, 0);
create_isomorphic_effect(cx, {
move |_| {
acc.set(r());
}
@@ -161,9 +163,7 @@ fn leptos_scope_creation_and_disposal(b: &mut Bencher) {
#[bench]
fn rs_deep_update(b: &mut Bencher) {
use reactive_signals::{
runtimes::ClientRuntime, signal, types::Func, Scope, Signal,
};
use reactive_signals::{Scope, Signal, signal, runtimes::ClientRuntime, types::Func};
let sc = ClientRuntime::new_root_scope();
b.iter(|| {
@@ -184,9 +184,7 @@ fn rs_deep_update(b: &mut Bencher) {
#[bench]
fn rs_fanning_out(b: &mut Bencher) {
use reactive_signals::{
runtimes::ClientRuntime, signal, types::Func, Scope, Signal,
};
use reactive_signals::{Scope, Signal, signal, runtimes::ClientRuntime, types::Func};
let cx = ClientRuntime::new_root_scope();
b.iter(|| {
@@ -202,17 +200,18 @@ fn rs_fanning_out(b: &mut Bencher) {
#[bench]
fn rs_narrowing_update(b: &mut Bencher) {
use reactive_signals::{
runtimes::ClientRuntime, signal, types::Func, Scope, Signal,
};
use reactive_signals::{Scope, Signal, signal, runtimes::ClientRuntime, types::Func};
let cx = ClientRuntime::new_root_scope();
b.iter(|| {
let acc = Rc::new(Cell::new(0));
let sigs = (0..1000).map(|n| signal!(cx, n)).collect::<Vec<_>>();
let sigs =
(0..1000).map(|n| signal!(cx, n)).collect::<Vec<_>>();
let memo = signal!(cx, {
let sigs = sigs.clone();
move || sigs.iter().map(|r| r.get()).sum::<i32>()
move || {
sigs.iter().map(|r| r.get()).sum::<i32>()
}
});
assert_eq!(memo.get(), 499500);
signal!(cx, {

View File

@@ -7,7 +7,7 @@ fn leptos_ssr_bench(b: &mut Bencher) {
leptos_dom::HydrationCtx::reset_id();
_ = create_scope(create_runtime(), |cx| {
#[component]
fn Counter(initial: i32) -> impl IntoView {
fn Counter(cx: Scope, initial: i32) -> impl IntoView {
let (value, set_value) = create_signal(cx, initial);
view! {
cx,

View File

@@ -1,7 +1,7 @@
pub use leptos::*;
use miniserde::*;
use wasm_bindgen::JsCast;
use web_sys::HtmlInputElement;
use wasm_bindgen::JsCast;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Todos(pub Vec<Todo>);
@@ -9,13 +9,13 @@ pub struct Todos(pub Vec<Todo>);
const STORAGE_KEY: &str = "todos-leptos";
impl Todos {
pub fn new() -> Self {
pub fn new(cx: Scope) -> Self {
Self(vec![])
}
pub fn new_with_1000() -> Self {
pub fn new_with_1000(cx: Scope) -> Self {
let todos = (0..1000)
.map(|id| Todo::new(id, format!("Todo #{id}")))
.map(|id| Todo::new(cx, id, format!("Todo #{id}")))
.collect();
Self(todos)
}
@@ -72,17 +72,13 @@ pub struct Todo {
}
impl Todo {
pub fn new(id: usize, title: String) -> Self {
Self::new_with_completed(id, title, false)
pub fn new(cx: Scope, id: usize, title: String) -> Self {
Self::new_with_completed(cx, id, title, false)
}
pub fn new_with_completed(
id: usize,
title: String,
completed: bool,
) -> Self {
let (title, set_title) = create_signal(title);
let (completed, set_completed) = create_signal(completed);
pub fn new_with_completed(cx: Scope, id: usize, title: String, completed: bool) -> Self {
let (title, set_title) = create_signal(cx, title);
let (completed, set_completed) = create_signal(cx, completed);
Self {
id,
title,
@@ -102,7 +98,7 @@ const ESCAPE_KEY: u32 = 27;
const ENTER_KEY: u32 = 13;
#[component]
pub fn TodoMVC(todos: Todos) -> impl IntoView {
pub fn TodoMVC(cx: Scope, todos: Todos) -> impl IntoView {
let mut next_id = todos
.0
.iter()
@@ -111,10 +107,10 @@ pub fn TodoMVC(todos: Todos) -> impl IntoView {
.map(|last| last + 1)
.unwrap_or(0);
let (todos, set_todos) = create_signal(todos);
provide_context(set_todos);
let (todos, set_todos) = create_signal(cx, todos);
provide_context(cx, set_todos);
let (mode, set_mode) = create_signal(Mode::All);
let (mode, set_mode) = create_signal(cx, Mode::All);
let add_todo = move |ev: web_sys::KeyboardEvent| {
let target = event_target::<HtmlInputElement>(&ev);
@@ -124,7 +120,7 @@ pub fn TodoMVC(todos: Todos) -> impl IntoView {
let title = event_target_value(&ev);
let title = title.trim();
if !title.is_empty() {
let new = Todo::new(next_id, title.to_string());
let new = Todo::new(cx, next_id, title.to_string());
set_todos.update(|t| t.add(new));
next_id += 1;
target.set_value("");
@@ -132,7 +128,7 @@ pub fn TodoMVC(todos: Todos) -> impl IntoView {
}
};
let filtered_todos = create_memo::<Vec<Todo>>(move |_| {
let filtered_todos = create_memo::<Vec<Todo>>(cx, move |_| {
todos.with(|todos| match mode.get() {
Mode::All => todos.0.to_vec(),
Mode::Active => todos
@@ -152,7 +148,7 @@ pub fn TodoMVC(todos: Todos) -> impl IntoView {
// effect to serialize to JSON
// this does reactive reads, so it will automatically serialize on any relevant change
create_effect(move |_| {
create_effect(cx, move |_| {
if let Ok(Some(storage)) = window().local_storage() {
let objs = todos
.get()
@@ -167,7 +163,7 @@ pub fn TodoMVC(todos: Todos) -> impl IntoView {
}
});
view! {
view! { cx,
<main>
<section class="todoapp">
<header class="header">
@@ -192,8 +188,8 @@ pub fn TodoMVC(todos: Todos) -> impl IntoView {
<For
each=filtered_todos
key=|todo| todo.id
view=move |todo: Todo| {
view! { <Todo todo=todo.clone()/> }
view=move |cx, todo: Todo| {
view! { cx, <Todo todo=todo.clone()/> }
}
/>
</ul>
@@ -240,14 +236,14 @@ pub fn TodoMVC(todos: Todos) -> impl IntoView {
<p>"Part of " <a href="http://todomvc.com">"TodoMVC"</a></p>
</footer>
</main>
}.into_view()
}.into_view(cx)
}
#[component]
pub fn Todo(todo: Todo) -> impl IntoView {
let (editing, set_editing) = create_signal(false);
let set_todos = use_context::<WriteSignal<Todos>>().unwrap();
//let input = NodeRef::new();
pub fn Todo(cx: Scope, todo: Todo) -> impl IntoView {
let (editing, set_editing) = create_signal(cx, false);
let set_todos = use_context::<WriteSignal<Todos>>(cx).unwrap();
//let input = NodeRef::new(cx);
let save = move |value: &str| {
let value = value.trim();
@@ -259,7 +255,7 @@ pub fn Todo(todo: Todo) -> impl IntoView {
set_editing(false);
};
view! {
view! { cx,
<li class="todo" class:editing=editing class:completed=move || (todo.completed)()>
<div class="view">
<input class="toggle" type="checkbox" prop:checked=move || (todo.completed)()/>
@@ -272,7 +268,7 @@ pub fn Todo(todo: Todo) -> impl IntoView {
{move || {
editing()
.then(|| {
view! {
view! { cx,
<input
class="edit"
class:hidden=move || !(editing)()
@@ -323,8 +319,8 @@ pub struct TodoSerialized {
}
impl TodoSerialized {
pub fn into_todo(self, ) -> Todo {
Todo::new_with_completed(self.id, self.title, self.completed)
pub fn into_todo(self, cx: Scope) -> Todo {
Todo::new_with_completed(cx, self.id, self.title, self.completed)
}
}

View File

@@ -12,8 +12,8 @@ fn leptos_todomvc_ssr(b: &mut Bencher) {
b.iter(|| {
use crate::todomvc::leptos::*;
let html = ::leptos::ssr::render_to_string(|| {
view! { <TodoMVC todos=Todos::new()/> }
let html = ::leptos::ssr::render_to_string(|cx| {
view! { cx, <TodoMVC todos=Todos::new(cx)/> }
});
assert!(html.len() > 1);
});

View File

@@ -15,12 +15,13 @@ cfg_if! {
}
}
#[server]
// "/api" is an optional prefix that allows you to locate server functions wherever you'd like on the server
#[server(GetServerCount, "/api")]
pub async fn get_server_count() -> Result<i32, ServerFnError> {
Ok(COUNT.load(Ordering::Relaxed))
}
#[server]
#[server(AdjustServerCount, "/api")]
pub async fn adjust_server_count(
delta: i32,
msg: String,
@@ -32,7 +33,7 @@ pub async fn adjust_server_count(
Ok(new)
}
#[server]
#[server(ClearServerCount, "/api")]
pub async fn clear_server_count() -> Result<i32, ServerFnError> {
COUNT.store(0, Ordering::Relaxed);
_ = COUNT_CHANNEL.send(&0).await;
@@ -146,8 +147,6 @@ pub fn Counter() -> impl IntoView {
// but uses HTML forms to submit the actions
#[component]
pub fn FormCounter() -> impl IntoView {
// these struct names are auto-generated by #[server]
// they are just the PascalCased versions of the function names
let adjust = create_server_action::<AdjustServerCount>();
let clear = create_server_action::<ClearServerCount>();

View File

@@ -5,15 +5,12 @@ edition = "2021"
[dependencies]
leptos = { path = "../../leptos", features = ["csr"] }
leptos_meta = { path = "../../meta", features = ["csr"] }
log = "0.4"
console_log = "1"
console_error_panic_hook = "0.1.7"
[dev-dependencies]
wasm-bindgen = "0.2.87"
wasm-bindgen-test = "0.3.37"
pretty_assertions = "1.4.0"
wasm-bindgen-test = "0.3.0"
[dev-dependencies.web-sys]
features = [

View File

@@ -1,6 +1,5 @@
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/wasm-test.toml" },
{ path = "../cargo-make/trunk_server.toml" },
{ path = "../cargo-make/playwright-test.toml" },
]

View File

@@ -1,7 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<link data-trunk rel="rust" data-wasm-opt="z" data-weak-refs />
</head>
<body></body>
</html>
<head>
<title>Counters (Stable)</title>
<link data-trunk rel="rust" data-wasm-opt="z" data-weak-refs/>
</head>
<body></body>
</html>

View File

@@ -1,106 +0,0 @@
use leptos::*;
use leptos_meta::*;
const MANY_COUNTERS: usize = 1000;
type CounterHolder = Vec<(usize, (ReadSignal<i32>, WriteSignal<i32>))>;
#[derive(Copy, Clone)]
struct CounterUpdater {
set_counters: WriteSignal<CounterHolder>,
}
#[component]
pub fn Counters() -> impl IntoView {
let (next_counter_id, set_next_counter_id) = create_signal(0);
let (counters, set_counters) = create_signal::<CounterHolder>(vec![]);
provide_context(CounterUpdater { set_counters });
let add_counter = move |_| {
let id = next_counter_id.get();
let sig = create_signal(0);
set_counters.update(move |counters| counters.push((id, sig)));
set_next_counter_id.update(|id| *id += 1);
};
let add_many_counters = move |_| {
let next_id = next_counter_id.get();
let new_counters = (next_id..next_id + MANY_COUNTERS).map(|id| {
let signal = create_signal(0);
(id, signal)
});
set_counters.update(move |counters| counters.extend(new_counters));
set_next_counter_id.update(|id| *id += MANY_COUNTERS);
};
let clear_counters = move |_| {
set_counters.update(|counters| counters.clear());
};
view! {
<Title text="Counters (Stable)" />
<div>
<button on:click=add_counter>
"Add Counter"
</button>
<button on:click=add_many_counters>
{format!("Add {MANY_COUNTERS} Counters")}
</button>
<button on:click=clear_counters>
"Clear Counters"
</button>
<p>
"Total: "
<span data-testid="total">{move ||
counters.get()
.iter()
.map(|(_, (count, _))| count.get())
.sum::<i32>()
.to_string()
}</span>
" from "
<span data-testid="counters">{move || counters.with(|counters| counters.len()).to_string()}</span>
" counters."
</p>
<ul>
<For
each={move || counters.get()}
key={|counter| counter.0}
view=move |(id, (value, set_value))| {
view! {
<Counter id value set_value/>
}
}
/>
</ul>
</div>
}
}
#[component]
fn Counter(
id: usize,
value: ReadSignal<i32>,
set_value: WriteSignal<i32>,
) -> impl IntoView {
let CounterUpdater { set_counters } = use_context().unwrap();
let input = move |ev| {
set_value
.set(event_target_value(&ev).parse::<i32>().unwrap_or_default())
};
view! {
<li>
<button data-testid="decrement_count" on:click=move |_| set_value.update(move |value| *value -= 1)>"-1"</button>
<input data-testid="counter_input" type="text"
prop:value={move || value.get().to_string()}
on:input=input
/>
<span>{value}</span>
<button data-testid="increment_count" on:click=move |_| set_value.update(move |value| *value += 1)>"+1"</button>
<button data-testid="remove_counter" on:click=move |_| set_counters.update(move |counters| counters.retain(|(counter_id, _)| counter_id != &id))>"x"</button>
</li>
}
}

View File

@@ -1,4 +1,3 @@
use counters_stable::Counters;
use leptos::*;
fn main() {
@@ -6,3 +5,106 @@ fn main() {
console_error_panic_hook::set_once();
mount_to_body(|| view! { <Counters/> })
}
const MANY_COUNTERS: usize = 1000;
type CounterHolder = Vec<(usize, (ReadSignal<i32>, WriteSignal<i32>))>;
#[derive(Copy, Clone)]
struct CounterUpdater {
set_counters: WriteSignal<CounterHolder>,
}
#[component]
pub fn Counters() -> impl IntoView {
let (next_counter_id, set_next_counter_id) = create_signal(0);
let (counters, set_counters) = create_signal::<CounterHolder>(vec![]);
provide_context(CounterUpdater { set_counters });
let add_counter = move |_| {
let id = next_counter_id.get();
let sig = create_signal(0);
set_counters.update(move |counters| counters.push((id, sig)));
set_next_counter_id.update(|id| *id += 1);
};
let add_many_counters = move |_| {
let next_id = next_counter_id.get();
let new_counters = (next_id..next_id + MANY_COUNTERS).map(|id| {
let signal = create_signal(0);
(id, signal)
});
set_counters.update(move |counters| counters.extend(new_counters));
set_next_counter_id.update(|id| *id += MANY_COUNTERS);
};
let clear_counters = move |_| {
set_counters.update(|counters| counters.clear());
};
view! {
<div>
<button on:click=add_counter>
"Add Counter"
</button>
<button on:click=add_many_counters>
{format!("Add {MANY_COUNTERS} Counters")}
</button>
<button on:click=clear_counters>
"Clear Counters"
</button>
<p>
"Total: "
<span data-testid="total">{move ||
counters.get()
.iter()
.map(|(_, (count, _))| count.get())
.sum::<i32>()
.to_string()
}</span>
" from "
<span data-testid="counters">{move || counters.with(|counters| counters.len()).to_string()}</span>
" counters."
</p>
<ul>
<For
each={move || counters.get()}
key={|counter| counter.0}
view=move |(id, (value, set_value))| {
view! {
<Counter id value set_value/>
}
}
/>
</ul>
</div>
}
}
#[component]
fn Counter(
id: usize,
value: ReadSignal<i32>,
set_value: WriteSignal<i32>,
) -> impl IntoView {
let CounterUpdater { set_counters } = use_context().unwrap();
let input = move |ev| {
set_value
.set(event_target_value(&ev).parse::<i32>().unwrap_or_default())
};
view! {
<li>
<button id="decrement_count" on:click=move |_| set_value.update(move |value| *value -= 1)>"-1"</button>
<input type="text"
prop:value={move || value.get().to_string()}
on:input=input
/>
<span>{value}</span>
<button id="increment_count" on:click=move |_| set_value.update(move |value| *value += 1)>"+1"</button>
<button on:click=move |_| set_counters.update(move |counters| counters.retain(|(counter_id, _)| counter_id != &id))>"x"</button>
</li>
}
}

View File

@@ -1,17 +0,0 @@
use super::*;
use crate::counters_page as ui;
use pretty_assertions::assert_eq;
#[wasm_bindgen_test]
fn should_increase_the_number_of_counters() {
// Given
ui::view_counters();
// When
ui::add_1k_counters();
ui::add_1k_counters();
ui::add_1k_counters();
// Then
assert_eq!(ui::counters(), 3000);
}

View File

@@ -1,17 +0,0 @@
use super::*;
use crate::counters_page as ui;
use pretty_assertions::assert_eq;
#[wasm_bindgen_test]
fn should_increase_the_number_of_counters() {
// Given
ui::view_counters();
// When
ui::add_counter();
ui::add_counter();
ui::add_counter();
// Then
assert_eq!(ui::counters(), 3);
}

View File

@@ -1,19 +0,0 @@
use super::*;
use crate::counters_page as ui;
use pretty_assertions::assert_eq;
#[wasm_bindgen_test]
fn should_reset_the_counts() {
// Given
ui::view_counters();
ui::add_counter();
ui::add_counter();
ui::add_counter();
// When
ui::clear_counters();
// Then
assert_eq!(ui::total(), 0);
assert_eq!(ui::counters(), 0);
}

View File

@@ -1,18 +0,0 @@
use super::*;
use crate::counters_page as ui;
use pretty_assertions::assert_eq;
#[wasm_bindgen_test]
fn should_decrease_the_total_count() {
// Given
ui::view_counters();
ui::add_counter();
// When
ui::decrement_counter(1);
ui::decrement_counter(1);
ui::decrement_counter(1);
// Then
assert_eq!(ui::total(), -3);
}

View File

@@ -1,112 +0,0 @@
use counters_stable::Counters;
use leptos::*;
use wasm_bindgen::JsCast;
use web_sys::{Element, Event, EventInit, HtmlElement, HtmlInputElement};
// Actions
pub fn add_1k_counters() {
find_by_text("Add 1000 Counters").click();
}
pub fn add_counter() {
find_by_text("Add Counter").click();
}
pub fn clear_counters() {
find_by_text("Clear Counters").click();
}
pub fn decrement_counter(index: u32) {
counter_html_element(index, "decrement_count").click();
}
pub fn enter_count(index: u32, count: i32) {
let input = counter_input_element(index, "counter_input");
input.set_value(count.to_string().as_str());
let mut event_init = EventInit::new();
event_init.bubbles(true);
let event = Event::new_with_event_init_dict("input", &event_init).unwrap();
input.dispatch_event(&event).unwrap();
}
pub fn increment_counter(index: u32) {
counter_html_element(index, "increment_count").click();
}
pub fn remove_counter(index: u32) {
counter_html_element(index, "remove_counter").click();
}
pub fn view_counters() {
remove_existing_counters();
mount_to_body(|| view! { <Counters/> });
}
// Results
pub fn counters() -> i32 {
data_test_id("counters").parse::<i32>().unwrap()
}
pub fn title() -> String {
leptos::document().title()
}
pub fn total() -> i32 {
data_test_id("total").parse::<i32>().unwrap()
}
// Internal
fn counter_element(index: u32, text: &str) -> Element {
let selector =
format!("li:nth-child({}) [data-testid=\"{}\"]", index, text);
leptos::document()
.query_selector(&selector)
.unwrap()
.unwrap()
}
fn counter_html_element(index: u32, text: &str) -> HtmlElement {
counter_element(index, text)
.dyn_into::<HtmlElement>()
.unwrap()
}
fn counter_input_element(index: u32, text: &str) -> HtmlInputElement {
counter_element(index, text)
.dyn_into::<HtmlInputElement>()
.unwrap()
}
fn data_test_id(id: &str) -> String {
let selector = format!("[data-testid=\"{}\"]", id);
leptos::document()
.query_selector(&selector)
.unwrap()
.expect("counters not found")
.text_content()
.unwrap()
}
fn find_by_text(text: &str) -> HtmlElement {
let xpath = format!("//*[text()='{}']", text);
let document = leptos::document();
document
.evaluate(&xpath, &document)
.unwrap()
.iterate_next()
.unwrap()
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap()
}
fn remove_existing_counters() {
if let Some(counter) =
leptos::document().query_selector("body div").unwrap()
{
counter.remove();
}
}

View File

@@ -1 +0,0 @@
pub mod counters_page;

View File

@@ -1,18 +0,0 @@
use super::*;
use crate::counters_page as ui;
use pretty_assertions::assert_eq;
#[wasm_bindgen_test]
fn should_increase_the_total_count() {
// Given
ui::view_counters();
ui::add_counter();
// When
ui::increment_counter(1);
ui::increment_counter(1);
ui::increment_counter(1);
// Then
assert_eq!(ui::total(), 3);
}

View File

@@ -1,16 +0,0 @@
use wasm_bindgen_test::*;
// Test Suites
pub mod add_1k_counters;
pub mod add_counter;
pub mod clear_counters;
pub mod decrement_counter;
pub mod enter_count;
pub mod increment_counter;
pub mod remove_counter;
pub mod view_counters;
pub mod fixtures;
pub use fixtures::*;
wasm_bindgen_test_configure!(run_in_browser);

View File

@@ -1,18 +0,0 @@
use super::*;
use crate::counters_page as ui;
use pretty_assertions::assert_eq;
#[wasm_bindgen_test]
fn should_decrement_the_number_of_counters() {
// Given
ui::view_counters();
ui::add_counter();
ui::add_counter();
ui::add_counter();
// When
ui::remove_counter(2);
// Then
assert_eq!(ui::counters(), 2);
}

View File

@@ -1,22 +0,0 @@
use super::*;
use crate::counters_page as ui;
use pretty_assertions::assert_eq;
#[wasm_bindgen_test]
fn should_see_the_initial_counts() {
// When
ui::view_counters();
// Then
assert_eq!(ui::total(), 0);
assert_eq!(ui::counters(), 0);
}
#[wasm_bindgen_test]
fn should_see_the_title() {
// When
ui::view_counters();
// Then
assert_eq!(ui::title(), "Counters (Stable)");
}

View File

@@ -16,7 +16,7 @@ actix-web = { version = "4", optional = true, features = ["macros"] }
console_log = "1"
console_error_panic_hook = "0.1"
cfg-if = "1"
leptos = { path = "../../leptos", features = ["nightly"] }
leptos = { path = "../../leptos", features = ["nightly", "islands"] }
leptos_meta = { path = "../../meta", features = ["nightly"] }
leptos_actix = { path = "../../integrations/actix", optional = true }
leptos_router = { path = "../../router", features = ["nightly"] }
@@ -41,6 +41,12 @@ ssr = [
"leptos_router/ssr",
]
[profile.wasm-release]
inherits = "release"
opt-level = 'z'
lto = true
codegen-units = 1
[package.metadata.cargo-all-features]
denylist = ["actix-files", "actix-web", "leptos_actix"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
@@ -88,3 +94,5 @@ lib-features = ["hydrate"]
#
# Optional. Defaults to false.
lib-default-features = false
lib-profile-release = "wasm-release"

View File

@@ -36,8 +36,8 @@ pub fn Stories() -> impl IntoView {
let (pending, set_pending) = create_signal(false);
let hide_more_link = move || {
stories.get().unwrap_or(None).unwrap_or_default().len() < 28
|| pending()
pending()
|| stories.get().unwrap_or(None).unwrap_or_default().len() < 28
};
view! {
@@ -65,16 +65,20 @@ pub fn Stories() -> impl IntoView {
}}
</span>
<span>"page " {page}</span>
<span class="page-link"
class:disabled=hide_more_link
aria-hidden=hide_more_link
<Transition
fallback=move || view! { <p>"Loading..."</p> }
>
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
aria-label="Next Page"
<span class="page-link"
class:disabled=hide_more_link
aria-hidden=hide_more_link
>
"more >"
</a>
</span>
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
aria-label="Next Page"
>
"more >"
</a>
</span>
</Transition>
</div>
<main class="news-list">
<div>

View File

@@ -25,45 +25,48 @@ pub fn Story() -> impl IntoView {
};
view! {
<Suspense fallback=|| view! { "Loading..." }>
<>
<Meta name="description" content=meta_description/>
{move || story.get().map(|story| match story {
None => view! { <div class="item-view">"Error loading this story."</div> },
Some(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">
{story.points}
" points | by "
<A href=format!("/users/{user}")>{user.clone()}</A>
{format!(" {}", story.time_ago)}
</p>})}
<Suspense fallback=|| view! { "Loading..." }>
{move || story.get().map(|story| match story {
None => view! { <div class="item-view">"Error loading this story."</div> },
Some(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">
{story.points}
" points | by "
<A href=format!("/users/{user}")>{user.clone()}</A>
{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
view=move |comment| view! { <Comment comment /> }
/>
</ul>
</div>
</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
view=move |comment| view! { <Comment comment /> }
/>
</ul>
</div>
</div>
}})}
</Suspense>
}})
}
</Suspense>
</>
}
}

View File

@@ -36,11 +36,12 @@ pub fn Stories() -> impl IntoView {
let (pending, set_pending) = create_signal(false);
let hide_more_link = move || {
stories.get().unwrap_or(None).unwrap_or_default().len() < 28
|| pending()
pending()
|| stories.get().unwrap_or(None).unwrap_or_default().len() < 28
};
view! {
<div class="news-view">
<div class="news-list-nav">
<span>
@@ -64,16 +65,20 @@ pub fn Stories() -> impl IntoView {
}}
</span>
<span>"page " {page}</span>
<span class="page-link"
class:disabled=hide_more_link
aria-hidden=hide_more_link
<Transition
fallback=move || view! { <p>"Loading..."</p> }
>
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
aria-label="Next Page"
<span class="page-link"
class:disabled=hide_more_link
aria-hidden=hide_more_link
>
"more >"
</a>
</span>
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
aria-label="Next Page"
>
"more >"
</a>
</span>
</Transition>
</div>
<main class="news-list">
<div>

View File

@@ -25,45 +25,48 @@ pub fn Story() -> impl IntoView {
};
view! {
<Suspense fallback=|| view! { "Loading..." }>
<>
<Meta name="description" content=meta_description/>
{move || story.get().map(|story| match story {
None => view! { <div class="item-view">"Error loading this story."</div> },
Some(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">
{story.points}
" points | by "
<A href=format!("/users/{user}")>{user.clone()}</A>
{format!(" {}", story.time_ago)}
</p>})}
<Suspense fallback=|| view! { "Loading..." }>
{move || story.get().map(|story| match story {
None => view! { <div class="item-view">"Error loading this story."</div> },
Some(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">
{story.points}
" points | by "
<A href=format!("/users/{user}")>{user.clone()}</A>
{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
view=move | comment| view! { <Comment comment /> }
/>
</ul>
</div>
</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
view=move | comment| view! { <Comment comment /> }
/>
</ul>
</div>
</div>
}})}
</Suspense>
}})
}
</Suspense>
</>
}
}

View File

@@ -0,0 +1,3 @@
[unstable]
build-std = ["std", "panic_abort", "core", "alloc"]
build-std-features = ["panic_immediate_abort"]

View File

@@ -0,0 +1 @@
target

13
examples/hackernews_islands/.gitignore vendored Normal file
View File

@@ -0,0 +1,13 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
.direnv
fly.toml

View File

@@ -0,0 +1,112 @@
[package]
name = "hackernews"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[profile.release]
panic = "abort"
[dependencies]
actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", optional = true, features = ["macros"] }
console_log = "1"
console_error_panic_hook = "0.1"
cfg-if = "1"
leptos = { git = "https://github.com/leptos-rs/leptos", branch = "islands", features = [
"islands",
"nightly",
] }
leptos_meta = { git = "https://github.com/leptos-rs/leptos", branch = "islands", features = [
"nightly",
] }
leptos_router = { git = "https://github.com/leptos-rs/leptos", branch = "islands", features = [
"nightly",
] }
leptos_actix = { git = "https://github.com/leptos-rs/leptos", branch = "islands", optional = true, features = [
#"nonce",
"islands",
] }
log = "0.4"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
reqwest = { version = "0.11", default-features = false, features = [
"rustls-tls",
"json",
] }
tracing = "0.1"
# openssl = { version = "0.10", features = ["v110"] }
wasm-bindgen = "0.2"
web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] }
wee_alloc = "0.4.5"
[features]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:actix-files",
"dep:actix-web",
"dep:leptos_actix",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
]
[profile.wasm-release]
inherits = "release"
opt-level = 'z'
lto = true
codegen-units = 1
[package.metadata.cargo-all-features]
denylist = ["actix-files", "actix-web", "leptos_actix"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "hackernews"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "./style.css"
# [Optional] Files in the asset-dir will be copied to the site-root directory
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "0.0.0.0:8080"
#site-addr = "127.0.0.1:3004"
# The port to use for automatic reload monitoring
reload-port = 3005
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
end2end-cmd = "npx playwright test"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target
#
# Optional. Can be over-ridden with the command line parameter --bin-features
bin-features = ["ssr"]
# If the --no-default-features flag should be used when compiling the bin target
#
# Optional. Defaults to false.
bin-default-features = false
# The features to use when compiling the lib target
#
# Optional. Can be over-ridden with the command line parameter --lib-features
lib-features = ["hydrate"]
# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false
lib-profile-release = "wasm-release"

View File

@@ -0,0 +1,25 @@
FROM rustlang/rust:nightly-bullseye as builder
RUN wget https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz
#RUN tar -xvf cargo-binstall-x86_64-unknown-linux-musl.tgz
#RUN cp cargo-binstall /usr/local/cargo/bin
#RUN cargo binstall cargo-leptos -y
#RUN rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu
#RUN rustup target add wasm32-unknown-unknown
RUN mkdir -p /app
WORKDIR /app
COPY . .
RUN cargo build --release --no-default-features --features=ssr
RUN ls -l /app/target
FROM rustlang/rust:nightly-bullseye as runner
COPY --from=builder /app/target/release/hackernews /app/
COPY --from=builder /app/pkg /app
COPY --from=builder /app/Cargo.toml /app/
WORKDIR /app
ENV RUST_LOG="info"
ENV LEPTOS_OUTPUT_NAME="hackernews"
ENV APP_ENVIRONMENT="production"
ENV LEPTOS_SITE_ADDR="0.0.0.0:8080"
ENV LEPTOS_SITE_ROOT="site"
EXPOSE 8080
CMD ["/app/hackernews"]

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Greg Johnston
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1 @@
extend = [{ path = "../cargo-make/main.toml" }]

View File

@@ -0,0 +1,43 @@
# Leptos Hacker News Example
This example creates a basic clone of the Hacker News site. It showcases Leptos' ability to create both a client-side rendered app, and a server side rendered app with hydration, in a single repository
## Client Side Rendering
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
app into one CSR bundle. Make sure you have trunk installed with `cargo install trunk`.
## Server Side Rendering with cargo-leptos
cargo-leptos is now the easiest and most featureful way to build server side rendered apps with hydration. It provides automatic recompilation of client and server code, wasm optimisation, CSS minification, and more! Check out more about it [here](https://github.com/akesson/cargo-leptos)
1. Install cargo-leptos
```bash
cargo install --locked cargo-leptos
```
2. Build the site in watch mode, recompiling on file changes
```bash
cargo leptos watch
```
Open browser on [http://localhost:3000/](http://localhost:3000/)
3. When ready to deploy, run
```bash
cargo leptos build --release
```
## Server Side Rendering without cargo-leptos
To run it as a server side app with hydration, you'll need to have wasm-pack installed.
0. Edit the `[package.metadata.leptos]` section and set `site-root` to `"."`. You'll also want to change the path of the `<StyleSheet / >` component in the root component to point towards the CSS file in the root. This tells leptos that the WASM/JS files generated by wasm-pack are available at `./pkg` and that the CSS files are no longer processed by cargo-leptos. Building to alternative folders is not supported at this time. You'll also want to edit the call to `get_configuration()` to pass in `Some(Cargo.toml)`, so that Leptos will read the settings instead of cargo-leptos. If you do so, your file/folder names cannot include dashes..
1. Install wasm-pack
```bash
cargo install wasm-pack
```
2. Build the Webassembly used to hydrate the HTML from the server
```bash
wasm-pack build --target=web --debug --no-default-features --features=hydrate
```
3. Run the server to serve the Webassembly, JS, and HTML
```bash
cargo run --no-default-features --features=ssr
```

View File

@@ -0,0 +1,11 @@
mv .cargo/config.wasm.toml .cargo/config.toml
wasm-pack build --target=web --features=hydrate --release
cd pkg
rm *.br
cp hackernews.js hackernews.unmin.js
cat hackernews.unmin.js | esbuild > hackernews.js
brotli hackernews.js
brotli hackernews_bg.wasm
brotli style.css
cd ..
mv .cargo/config.toml .cargo/config.wasm.toml

View File

@@ -0,0 +1,15 @@
# fly.toml app configuration file generated for leptos-hackernews-islands on 2023-07-27T08:08:20-04:00
#
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
#
app = "leptos-hackernews-islands"
primary_region = "bos"
[http_service]
internal_port = 8080
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 0
processes = ["app"]

View File

@@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<link data-trunk rel="rust" data-wasm-opt="z" data-cargo-features="csr"/>
<link data-trunk rel="css" href="./style.css"/>
</head>
<body></body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1 @@
body{color:#1e1333;background-color:#f2f3f5;margin:0;padding-top:55px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;font-size:15px;overflow-y:scroll}a{color:#1e1333;text-decoration:none}.header{z-index:999;background-color:#1e1333;height:55px;position:fixed;top:0;left:0;right:0}.header .inner{box-sizing:border-box;max-width:800px;margin:0 auto;padding:15px 5px}.header a{color:#fffc;vertical-align:middle;letter-spacing:.075em;margin-right:1.8em;font-weight:300;line-height:24px;transition:color .15s;display:inline-block}.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;float:right;margin:0;font-size:.9em}.logo{vertical-align:middle;width:24px;margin-right:10px;display:inline-block}.view{max-width:800px;margin:0 auto;position:relative}.fade-enter-active,.fade-exit-active{transition:all .2s}.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{text-align:center;z-index:998;padding:15px 30px;position:fixed;top:55px;left:0;right:0;box-shadow:0 1px 2px #0000001a}.news-list-nav .page-link{margin:0 1em}.news-list-nav .disabled{color:#aaa}.news-list{width:100%;margin:30px 0;transition:all .5s cubic-bezier(.55,0,.1,1);position:absolute}.news-list ul{margin:0;padding:0;list-style-type:none}@media (max-width:600px){.news-list{margin:10px 0}}.news-item{background-color:#fff;border-bottom:1px solid #eee;padding:20px 30px 20px 80px;line-height:20px;position:relative}.news-item .score{color:#1e1333;text-align:center;width:80px;margin-top:-10px;font-size:1.1em;font-weight:700;position:absolute;top:50%;left:0}.news-item .host,.news-item .meta{color:#626262;font-size:.85em}.news-item .host a,.news-item .meta a{color:#626262;text-decoration:underline}.news-item .host a:hover,.news-item .meta a:hover{color:#1e1333}.item-view-header{background-color:#fff;padding:1.8em 2em 1em;box-shadow:0 1px 2px #0000001a}.item-view-header h1{margin:0 .5em 0 0;font-size:1.5em;display:inline}.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;padding:1em 0;font-size:1.1em;position:relative}.item-view-comments-header .spinner{margin:-15px 0;display:inline-block}.comment-children{margin:0;padding:0;list-style-type:none}@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{margin:1em 0;font-size:.9em}.comment .by{color:#626262}.comment .by a{color:#626262;text-decoration:underline}.comment .text{overflow-wrap:break-word}.comment .text a:hover{color:#1e1333}.comment .text pre{white-space:pre-wrap}.comment .toggle{background-color:#fffbf2;border-radius:4px;padding:.3em .5em}.comment .toggle a{color:#626262;cursor:pointer}.comment .toggle.open{background-color:#0000;margin-bottom:-.5em;padding:0}.user-view{box-sizing:border-box;background-color:#fff;padding:2em 3em}.user-view h1{margin:0;font-size:1.5em}.user-view .meta{padding:0;list-style-type:none}.user-view .label{min-width:4em;display:inline-block}.user-view .about{margin:1em 0}.user-view .links a{text-decoration:underline}leptos-island,leptos-children{display:contents}

View File

@@ -0,0 +1,64 @@
use leptos::Serializable;
use serde::{Deserialize, Serialize};
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")
}
#[cfg(feature = "ssr")]
pub async fn fetch_api<T>(path: &str) -> Option<T>
where
T: Serializable,
{
use actix_web::web;
let path = path.to_string();
leptos_actix::extract(|client: web::Data<reqwest::Client>| async move {
let client = client.into_inner();
let json = client.get(&path).send().await.ok()?.text().await.ok()?;
T::de(&json).map_err(|e| log::error!("{e}")).ok()
})
.await
.ok()
.flatten()
}
#[derive(Debug, Deserialize, Serialize, 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,
#[serde(default)]
pub comments: Option<Vec<Comment>>,
pub comments_count: Option<usize>,
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
pub struct Comment {
pub id: usize,
pub level: usize,
pub user: Option<String>,
pub time: usize,
pub time_ago: String,
pub content: Option<String>,
pub comments: Vec<Comment>,
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
pub struct User {
pub created: usize,
pub id: String,
pub karma: i32,
pub about: Option<String>,
}

View File

@@ -0,0 +1,50 @@
#![feature(lazy_cell)]
use cfg_if::cfg_if;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
mod api;
mod routes;
use routes::{nav::*, stories::*, story::*, users::*};
#[component]
pub fn App() -> impl IntoView {
provide_meta_context();
view! {
<Stylesheet id="leptos" href="/style.css"/>
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<Meta name="description" content="Leptos implementation of a HackerNews demo."/>
<Router>
<Nav />
<main>
<Routes>
<Route path="users/:id" view=User ssr=SsrMode::InOrder/>
<Route path="stories/:id" view=Story ssr=SsrMode::InOrder/>
<Route path=":stories?" view=Stories ssr=SsrMode::InOrder/>
</Routes>
</main>
</Router>
}
}
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
cfg_if! {
if #[cfg(feature = "hydrate")] {
use wasm_bindgen::prelude::wasm_bindgen;
extern crate wee_alloc;
// Use `wee_alloc` as the global allocator.
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
#[wasm_bindgen]
pub fn hydrate() {
#[cfg(debug_assertions)]
console_error_panic_hook::set_once();
leptos::leptos_dom::HydrationCtx::stop_hydrating();
}
}
}

View File

@@ -0,0 +1,84 @@
use cfg_if::cfg_if;
use leptos::*;
static CSS: &[u8] = include_bytes!("../pkg/style.css.br");
static JS: &[u8] = include_bytes!("../pkg/hackernews.js.br");
static WASM: &[u8] = include_bytes!("../pkg/hackernews_bg.wasm.br");
// boilerplate to run in different modes
cfg_if! {
// server-only stuff
if #[cfg(feature = "ssr")] {
use actix_files::{Files};
use actix_web::*;
use hackernews::{App};
use leptos_actix::{LeptosRoutes, generate_route_list};
#[get("/style.css")]
async fn css() -> impl Responder {
HttpResponse::Ok()
.append_header(("content-encoding", "br"))
.content_type("text/css")
.body(CSS)
}
#[get("/pkg/hackernews.js")]
async fn js() -> impl Responder {
HttpResponse::Ok()
.append_header(("content-encoding", "br"))
.content_type("text/javascript")
.body(JS)
}
#[get("/pkg/hackernews_bg.wasm")]
async fn wasm() -> impl Responder {
HttpResponse::Ok()
.append_header(("content-encoding", "br"))
.content_type("application/wasm")
.body(WASM)
}
#[get("/favicon.ico")]
async fn favicon() -> impl Responder {
actix_files::NamedFile::open_async("./public/favicon.ico").await
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// Setting this to None means we'll be using cargo-leptos and its env vars.
let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
let addr = conf.leptos_options.site_addr;
// Generate the list of routes in your Leptos App
let routes = generate_route_list(App);
HttpServer::new(move || {
let leptos_options = &conf.leptos_options;
let site_root = &leptos_options.site_root;
App::new()
.service(css)
.service(favicon)
.service(js)
.service(wasm)
.app_data(actix_web::web::Data::new(reqwest::Client::new()))
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), App)
.service(Files::new("/", site_root))
.wrap(middleware::Compress::default())
})
.bind(&addr)?
.run()
.await
}
} else {
fn main() {
use hackernews::{App};
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(App)
}
}
}

View File

@@ -0,0 +1,4 @@
pub mod nav;
pub mod stories;
pub mod story;
pub mod users;

View File

@@ -0,0 +1,30 @@
use leptos::{component, view, IntoView};
use leptos_router::*;
#[component]
pub fn Nav() -> impl IntoView {
view! {
<header class="header">
<nav class="inner">
<A href="/home">
<strong>"HN"</strong>
</A>
<A href="/new">
<strong>"New"</strong>
</A>
<A href="/show">
<strong>"Show"</strong>
</A>
<A href="/ask">
<strong>"Ask"</strong>
</A>
<A href="/job">
<strong>"Jobs"</strong>
</A>
<a class="github" href="http://github.com/gbj/leptos" target="_blank" rel="noreferrer">
"Built with Leptos"
</a>
</nav>
</header>
}
}

View File

@@ -0,0 +1,164 @@
use crate::api;
use leptos::*;
use leptos_router::*;
fn category(from: &str) -> String {
match from {
"new" => "newest",
"show" => "show",
"ask" => "ask",
"job" => "jobs",
_ => "news",
}
.to_string()
}
#[server(FetchStories, "/api")]
pub async fn fetch_stories(
story_type: String,
page: usize,
) -> Result<Vec<api::Story>, ServerFnError> {
let path = format!("{}?page={}", category(&story_type), page);
Ok(api::fetch_api::<Vec<api::Story>>(&api::story(&path))
.await
.unwrap_or_default())
}
#[component]
pub fn Stories() -> impl IntoView {
let query = use_query_map();
let params = use_params_map();
let page = move || {
query
.with(|q| q.get("page").and_then(|page| page.parse::<usize>().ok()))
.unwrap_or(1)
};
let story_type = move || {
params
.with(|p| p.get("stories").cloned())
.unwrap_or_else(|| "top".to_string())
};
let stories = create_resource(
move || (page(), story_type()),
move |(page, story_type)| fetch_stories(category(&story_type), page),
);
let (pending, set_pending) = create_signal(false);
let hide_more_link = move || {
pending()
|| stories
.with(|stories| {
stories.as_ref().map(|s| s.len() < 28).unwrap_or_default()
})
.unwrap_or_default()
};
view! {
<div class="news-view">
<div class="news-list-nav">
<span>
{move || if page() > 1 {
view! {
<a class="page-link"
href=move || format!("/{}?page={}", story_type(), page() - 1)
attr:aria_label="Previous Page"
>
"< prev"
</a>
}.into_any()
} else {
view! {
<span class="page-link disabled" aria-hidden="true">
"< prev"
</span>
}.into_any()
}}
</span>
<span>"page " {page}</span>
<span class="page-link"
class:disabled=hide_more_link
aria-hidden=hide_more_link
>
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
aria-label="Next Page"
>
"more >"
</a>
</span>
</div>
<main class="news-list">
<div>
<Transition
fallback=|| ()
set_pending=set_pending.into()
>
{move || stories.read().map(|story| story.map(|stories| view! {
<ul>
<For
each=move || stories.clone()
key=|story| story.id
view=move |story: api::Story| {
view! {
<Story story/>
}
}
/>
</ul>
}.into_any()))}
</Transition>
</div>
</main>
</div>
}
}
#[component]
fn Story(story: api::Story) -> impl IntoView {
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>
}.into_view()
} else {
let title = story.title.clone();
view! { <A href=format!("/stories/{}", story.id)>{title.clone()}</A> }.into_view()
}}
</span>
<br />
<span class="meta">
{if story.story_type != "job" {
view! {
<span>
{"by "}
{story.user.map(|user| view ! { <A href=format!("/users/{user}")>{user.clone()}</A>})}
{format!(" {} | ", story.time_ago)}
<A href=format!("/stories/{}", story.id)>
{if story.comments_count.unwrap_or_default() > 0 {
format!("{} comments", story.comments_count.unwrap_or_default())
} else {
"discuss".into()
}}
</A>
</span>
}.into_view()
} else {
let title = story.title.clone();
view! { <A href=format!("/item/{}", story.id)>{title.clone()}</A> }.into_view()
}}
</span>
{(story.story_type != "link").then(|| view! {
" "
<span class="label">{story.story_type}</span>
})}
</li>
}
}

View File

@@ -0,0 +1,141 @@
use crate::api;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
use std::cell::RefCell;
#[server(FetchStory, "/api")]
pub async fn fetch_story(
id: String,
) -> Result<RefCell<Option<api::Story>>, ServerFnError> {
Ok(RefCell::new(
api::fetch_api::<api::Story>(&api::story(&format!("item/{id}"))).await,
))
}
#[component]
pub fn Story() -> impl IntoView {
let params = use_params_map();
let story = create_resource(
move || params().get("id").cloned().unwrap_or_default(),
move |id| async move {
if id.is_empty() {
Ok(RefCell::new(None))
} else {
fetch_story(id).await
}
},
);
let meta_description = move || {
story
.with(|story| {
story
.as_ref()
.map(|story| {
story.borrow().as_ref().map(|story| story.title.clone())
})
.ok()
})
.flatten()
.flatten()
.unwrap_or_else(|| "Loading story...".to_string())
};
let story_view = move || {
story.with(|story| {
story.as_ref().ok().and_then(|story| {
let story: Option<api::Story> = story.borrow_mut().take();
story.map(|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">
{story.points}
" points | by "
<A href=format!("/users/{user}")>{user.clone()}</A>
{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">
{story.comments.unwrap_or_default().into_iter()
.map(|comment: api::Comment| view! { <Comment comment /> })
.collect_view()}
</ul>
</div>
</div>
}})})})
};
view! {
<Meta name="description" content=meta_description/>
<Suspense fallback=|| ()>
{story_view}
</Suspense>
}
}
#[island]
pub fn Toggle(children: Children) -> impl IntoView {
let (open, set_open) = create_signal(true);
let children = std::cell::LazyCell::new(|| children().into_view());
view! {
<div class="toggle" class:open=open>
<a
on:click=move |_| set_open.update(|n| *n = !*n)
onclick // necessary for event bubbling on iOS Safari FML
>
{move || if open() {
"[-]"
} else {
"[+] comments collapsed"
}}
</a>
</div>
<ul
class="comment-children"
style:display=move || if open() {
"block"
} else {
"none"
}
>
{move || open().then(|| children.clone())}
</ul>
}
}
#[component]
pub fn Comment(comment: api::Comment) -> impl IntoView {
view! {
<li class="comment">
<div class="by">
<A href=format!("/users/{}", comment.user.clone().unwrap_or_default())>{comment.user.clone()}</A>
{format!(" {}", comment.time_ago)}
</div>
<div class="text" inner_html=comment.content></div>
{(!comment.comments.is_empty()).then(|| {
view! {
<Toggle>
{comment.comments.into_iter()
.map(|comment: api::Comment| view! { <Comment comment /> })
.collect_view()}
</Toggle>
}
})}
</li>
}
}

View File

@@ -0,0 +1,53 @@
use crate::api::{self, User};
use leptos::*;
use leptos_router::*;
#[server(FetchUser, "/api")]
pub async fn fetch_user(
id: String,
) -> Result<Option<api::User>, ServerFnError> {
Ok(api::fetch_api::<User>(&api::user(&id)).await)
}
#[component]
pub fn User() -> impl IntoView {
let params = use_params_map();
let user = create_resource(
move || params().get("id").cloned().unwrap_or_default(),
move |id| async move {
if id.is_empty() {
Ok(None)
} else {
fetch_user(id).await
}
},
);
view! {
<div class="user-view">
<Suspense fallback=|| ()>
{move || user.read().map(|user| user.map(|user| match user {
None => view! { <h1>"User not found."</h1> }.into_view(),
Some(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>
<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>
}.into_view()
}))}
</Suspense>
</div>
}
}

View 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: #1E1333;
overflow-y: scroll
}
a {
color: #1E1333;
text-decoration: none
}
.header {
background-color: #1E1333;
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: #1E1333;
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: #1E1333
}
.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: #1E1333
}
.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
}

View File

@@ -0,0 +1,3 @@
[unstable]
build-std = ["std", "panic_abort", "core", "alloc"]
build-std-features = ["panic_immediate_abort"]

View File

@@ -0,0 +1,112 @@
[package]
name = "hackernews"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[profile.release]
codegen-units = 1
lto = true
[dependencies]
console_log = "1.0.0"
console_error_panic_hook = "0.1.7"
cfg-if = "1.0.0"
leptos = { git = "https://github.com/leptos-rs/leptos", rev = "1eba12f", features = [
"nightly",
"islands",
] }
leptos_meta = { git = "https://github.com/leptos-rs/leptos", rev = "1eba12f", features = [
"nightly",
] }
leptos_axum = { git = "https://github.com/leptos-rs/leptos", rev = "1eba12f", optional = true, features = [
"islands",
] }
leptos_router = { git = "https://github.com/leptos-rs/leptos", rev = "1eba12f", features = [
"nightly",
] }
log = "0.4.17"
simple_logger = "4.0.0"
serde = { version = "1.0.148", features = ["derive"] }
tracing = "0.1"
gloo-net = { version = "0.2.5", features = ["http"] }
reqwest = { version = "0.11.13", features = ["json"] }
axum = { version = "0.6.1", optional = true, features = ["http2"] }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.4", features = [
"fs",
"compression-br",
], 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"
wee_alloc = "0.4.5"
lazy_static = "1.4.0"
[features]
default = []
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:axum",
"dep:tower",
"dep:tower-http",
"dep:tokio",
"dep:http",
"leptos/ssr",
"leptos_axum",
"leptos_meta/ssr",
"leptos_router/ssr",
]
[package.metadata.cargo-all-features]
denylist = ["axum", "tower", "tower-http", "tokio", "http", "leptos_axum"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "hackernews"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "./style.css"
# [Optional] Files in the asset-dir will be copied to the site-root directory
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
#site-addr = "127.0.0.1:3000"
site-addr = "0.0.0.0:8080"
# The port to use for automatic reload monitoring
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
end2end-cmd = "npx playwright test"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target
#
# Optional. Can be over-ridden with the command line parameter --bin-features
bin-features = ["ssr"]
# If the --no-default-features flag should be used when compiling the bin target
#
# Optional. Defaults to false.
bin-default-features = false
# The features to use when compiling the lib target
#
# Optional. Can be over-ridden with the command line parameter --lib-features
lib-features = ["hydrate"]
# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false

View File

@@ -0,0 +1,25 @@
FROM rustlang/rust:nightly-bullseye as builder
RUN wget https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz
#RUN tar -xvf cargo-binstall-x86_64-unknown-linux-musl.tgz
#RUN cp cargo-binstall /usr/local/cargo/bin
#RUN cargo binstall cargo-leptos -y
#RUN rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu
#RUN rustup target add wasm32-unknown-unknown
RUN mkdir -p /app
WORKDIR /app
COPY . .
RUN cargo build --release --no-default-features --features=ssr
RUN ls -l /app/target
FROM rustlang/rust:nightly-bullseye as runner
COPY --from=builder /app/target/release/hackernews /app/
COPY --from=builder /app/pkg /app
COPY --from=builder /app/Cargo.toml /app/
WORKDIR /app
ENV RUST_LOG="info"
ENV LEPTOS_OUTPUT_NAME="hackernews"
ENV APP_ENVIRONMENT="production"
ENV LEPTOS_SITE_ADDR="0.0.0.0:8080"
ENV LEPTOS_SITE_ROOT="site"
EXPOSE 8080
CMD ["/app/hackernews"]

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Greg Johnston
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1 @@
extend = [{ path = "../cargo-make/main.toml" }]

View File

@@ -0,0 +1,43 @@
# Leptos Hacker News Example with Axum
This example creates a basic clone of the Hacker News site. It showcases Leptos' ability to create both a client-side rendered app, and a server side rendered app with hydration, in a single repository. This repo differs from the main Hacker News example by using Axum as it's server.
## Client Side Rendering
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
app into one CSR bundle. Make sure you have trunk installed with `cargo install trunk`.
## Server Side Rendering with cargo-leptos
cargo-leptos is now the easiest and most featureful way to build server side rendered apps with hydration. It provides automatic recompilation of client and server code, wasm optimisation, CSS minification, and more! Check out more about it [here](https://github.com/akesson/cargo-leptos)
1. Install cargo-leptos
```bash
cargo install --locked cargo-leptos
```
2. Build the site in watch mode, recompiling on file changes
```bash
cargo leptos watch
```
Open browser on [http://localhost:3000/](http://localhost:3000/)
3. When ready to deploy, run
```bash
cargo leptos build --release
```
## Server Side Rendering without cargo-leptos
To run it as a server side app with hydration, you'll need to have wasm-pack installed.
0. Edit the `[package.metadata.leptos]` section and set `site-root` to `"."`. You'll also want to change the path of the `<StyleSheet / >` component in the root component to point towards the CSS file in the root. This tells leptos that the WASM/JS files generated by wasm-pack are available at `./pkg` and that the CSS files are no longer processed by cargo-leptos. Building to alternative folders is not supported at this time. You'll also want to edit the call to `get_configuration()` to pass in `Some(Cargo.toml)`, so that Leptos will read the settings instead of cargo-leptos. If you do so, your file/folder names cannot include dashes..
1. Install wasm-pack
```bash
cargo install wasm-pack
```
2. Build the Webassembly used to hydrate the HTML from the server
```bash
wasm-pack build --target=web --debug --no-default-features --features=hydrate
```
3. Run the server to serve the Webassembly, JS, and HTML
```bash
cargo run --no-default-features --features=ssr
```

View File

@@ -0,0 +1,8 @@
wasm-pack build --target=web --features=hydrate --release
cd pkg
rm *.br
cp hackernews.js hackernews.unmin.js
cat hackernews.unmin.js | esbuild > hackernews.js
brotli hackernews.js
brotli hackernews_bg.wasm
brotli style.css

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,60 @@
use leptos::Serializable;
use serde::{Deserialize, Serialize};
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")
}
lazy_static::lazy_static! {
static ref CLIENT: reqwest::Client = reqwest::Client::new();
}
#[cfg(feature = "ssr")]
pub async fn fetch_api<T>(path: &str) -> Option<T>
where
T: Serializable,
{
let json = CLIENT.get(path).send().await.ok()?.text().await.ok()?;
T::de(&json).map_err(|e| log::error!("{e}")).ok()
}
#[derive(Debug, Deserialize, Serialize, 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,
#[serde(default)]
pub comments: Option<Vec<Comment>>,
pub comments_count: Option<usize>,
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
pub struct Comment {
pub id: usize,
pub level: usize,
pub user: Option<String>,
pub time: usize,
pub time_ago: String,
pub content: Option<String>,
pub comments: Vec<Comment>,
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
pub struct User {
pub created: usize,
pub id: String,
pub karma: i32,
pub about: Option<String>,
}

View File

@@ -0,0 +1,28 @@
use leptos::{view, Errors, For, IntoView, RwSignal, View};
// A basic function to display errors served by the error boundaries. Feel free to do more complicated things
// here than just displaying them
pub fn error_template(errors: Option<RwSignal<Errors>>) -> View {
let Some(errors) = errors else {
panic!("No Errors found and we expected errors!");
};
view! {
<h1>"Errors"</h1>
<For
// a function that returns the items we're iterating over; a signal is fine
each=errors
// a unique key for each item as a reference
key=|(key, _)| key.clone()
// renders each item to a view
view= move | (_, error)| {
let error_string = error.to_string();
view! {
<p>"Error: " {error_string}</p>
}
}
/>
}
.into_view()
}

View File

@@ -0,0 +1,44 @@
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
extract::State,
response::IntoResponse,
http::{Request, Response, StatusCode, Uri},
};
use axum::response::Response as AxumResponse;
use tower::ServiceExt;
use tower_http::services::ServeDir;
use leptos::{LeptosOptions};
use crate::error_template::error_template;
pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse {
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();
if res.status() == StatusCode::OK {
res.into_response()
} else{
let handler = leptos_axum::render_app_to_stream(options.to_owned(), || error_template( None));
handler(req).await.into_response()
}
}
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
match ServeDir::new(root).oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
)),
}
}
}
}

View File

@@ -0,0 +1,63 @@
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
http::{Request, Response, StatusCode, Uri},
};
use tower::ServiceExt;
use tower_http::services::ServeDir;
pub async fn file_handler(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
let res = get_static_file(uri.clone(), "/pkg").await?;
if res.status() == StatusCode::NOT_FOUND {
// try with `.html`
// TODO: handle if the Uri has query parameters
match format!("{}.html", uri).parse() {
Ok(uri_html) => get_static_file(uri_html, "/pkg").await,
Err(_) => Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string())),
}
} else {
Ok(res)
}
}
pub async fn get_static_file_handler(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
let res = get_static_file(uri.clone(), "/static").await?;
if res.status() == StatusCode::NOT_FOUND {
Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string()))
} else {
Ok(res)
}
}
async fn get_static_file(uri: Uri, base: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
let req = Request::builder().uri(&uri).body(Body::empty()).unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// When run normally, the root should be the crate root
if base == "/static" {
match ServeDir::new("./static").oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
))
}
} else if base == "/pkg" {
match ServeDir::new("./pkg").oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
)),
}
} else{
Err((StatusCode::NOT_FOUND, "Not Found".to_string()))
}
}
}
}

View File

@@ -0,0 +1,50 @@
#![feature(lazy_cell)]
use cfg_if::cfg_if;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
mod api;
mod routes;
use routes::{nav::*, stories::*, story::*, users::*};
#[component]
pub fn App() -> impl IntoView {
provide_meta_context();
view! {
<Stylesheet id="leptos" href="/style.css"/>
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<Meta name="description" content="Leptos implementation of a HackerNews demo."/>
<Router>
<Nav />
<main>
<Routes>
<Route path="users/:id" view=User ssr=SsrMode::InOrder/>
<Route path="stories/:id" view=Story ssr=SsrMode::InOrder/>
<Route path=":stories?" view=Stories ssr=SsrMode::InOrder/>
</Routes>
</main>
</Router>
}
}
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
cfg_if! {
if #[cfg(feature = "hydrate")] {
use wasm_bindgen::prelude::wasm_bindgen;
extern crate wee_alloc;
// Use `wee_alloc` as the global allocator.
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
#[wasm_bindgen]
pub fn hydrate() {
#[cfg(debug_assertions)]
console_error_panic_hook::set_once();
leptos::leptos_dom::HydrationCtx::stop_hydrating();
}
}
}

View File

@@ -0,0 +1,46 @@
#[cfg(feature = "ssr")]
mod ssr_imports {
pub use axum::Router;
pub use leptos::*;
pub use leptos_axum::{generate_route_list, LeptosRoutes};
pub use tower_http::{compression::CompressionLayer, services::ServeFile};
}
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use hackernews::*;
use ssr_imports::*;
let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
let routes = generate_route_list(|| view! { <App/> }).await;
// build our application with a route
let app = Router::new()
.route_service("/favicon.ico", ServeFile::new("./public/favicon.ico"))
.route_service(
"/style.css",
ServeFile::new("./pkg/style.css").precompressed_br(),
)
.route_service(
"/pkg/hackernews.js",
ServeFile::new("./pkg/hackernews.js").precompressed_br(),
)
.route_service(
"/pkg/hackernews_bg.wasm",
ServeFile::new("./pkg/hackernews_bg.wasm").precompressed_br(),
)
.leptos_routes(&leptos_options, routes, App)
//.layer(CompressionLayer::new())
.with_state(leptos_options);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
log!("listening on {}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}

View File

@@ -0,0 +1,4 @@
pub mod nav;
pub mod stories;
pub mod story;
pub mod users;

View File

@@ -0,0 +1,30 @@
use leptos::{component, view, IntoView};
use leptos_router::*;
#[component]
pub fn Nav() -> impl IntoView {
view! {
<header class="header">
<nav class="inner">
<A href="/home">
<strong>"HN"</strong>
</A>
<A href="/new">
<strong>"New"</strong>
</A>
<A href="/show">
<strong>"Show"</strong>
</A>
<A href="/ask">
<strong>"Ask"</strong>
</A>
<A href="/job">
<strong>"Jobs"</strong>
</A>
<a class="github" href="http://github.com/gbj/leptos" target="_blank" rel="noreferrer">
"Built with Leptos"
</a>
</nav>
</header>
}
}

View File

@@ -0,0 +1,164 @@
use crate::api;
use leptos::*;
use leptos_router::*;
fn category(from: &str) -> String {
match from {
"new" => "newest",
"show" => "show",
"ask" => "ask",
"job" => "jobs",
_ => "news",
}
.to_string()
}
#[server(FetchStories, "/api")]
pub async fn fetch_stories(
story_type: String,
page: usize,
) -> Result<Vec<api::Story>, ServerFnError> {
let path = format!("{}?page={}", category(&story_type), page);
Ok(api::fetch_api::<Vec<api::Story>>(&api::story(&path))
.await
.unwrap_or_default())
}
#[component]
pub fn Stories() -> impl IntoView {
let query = use_query_map();
let params = use_params_map();
let page = move || {
query
.with(|q| q.get("page").and_then(|page| page.parse::<usize>().ok()))
.unwrap_or(1)
};
let story_type = move || {
params
.with(|p| p.get("stories").cloned())
.unwrap_or_else(|| "top".to_string())
};
let stories = create_resource(
move || (page(), story_type()),
move |(page, story_type)| fetch_stories(category(&story_type), page),
);
let (pending, set_pending) = create_signal(false);
let hide_more_link = move || {
pending()
|| stories
.with(|stories| {
stories.as_ref().map(|s| s.len() < 28).unwrap_or_default()
})
.unwrap_or_default()
};
view! {
<div class="news-view">
<div class="news-list-nav">
<span>
{move || if page() > 1 {
view! {
<a class="page-link"
href=move || format!("/{}?page={}", story_type(), page() - 1)
attr:aria_label="Previous Page"
>
"< prev"
</a>
}.into_any()
} else {
view! {
<span class="page-link disabled" aria-hidden="true">
"< prev"
</span>
}.into_any()
}}
</span>
<span>"page " {page}</span>
<span class="page-link"
class:disabled=hide_more_link
aria-hidden=hide_more_link
>
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
aria-label="Next Page"
>
"more >"
</a>
</span>
</div>
<main class="news-list">
<div>
<Transition
fallback=|| ()
set_pending=set_pending.into()
>
{move || stories.read().map(|story| story.map(|stories| view! {
<ul>
<For
each=move || stories.clone()
key=|story| story.id
view=move |story: api::Story| {
view! {
<Story story/>
}
}
/>
</ul>
}.into_any()))}
</Transition>
</div>
</main>
</div>
}
}
#[component]
fn Story(story: api::Story) -> impl IntoView {
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>
}.into_view()
} else {
let title = story.title.clone();
view! { <A href=format!("/stories/{}", story.id)>{title.clone()}</A> }.into_view()
}}
</span>
<br />
<span class="meta">
{if story.story_type != "job" {
view! {
<span>
{"by "}
{story.user.map(|user| view ! { <A href=format!("/users/{user}")>{user.clone()}</A>})}
{format!(" {} | ", story.time_ago)}
<A href=format!("/stories/{}", story.id)>
{if story.comments_count.unwrap_or_default() > 0 {
format!("{} comments", story.comments_count.unwrap_or_default())
} else {
"discuss".into()
}}
</A>
</span>
}.into_view()
} else {
let title = story.title.clone();
view! { <A href=format!("/item/{}", story.id)>{title.clone()}</A> }.into_view()
}}
</span>
{(story.story_type != "link").then(|| view! {
" "
<span class="label">{story.story_type}</span>
})}
</li>
}
}

View File

@@ -0,0 +1,138 @@
use crate::api;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
use std::cell::RefCell;
#[server(FetchStory, "/api")]
pub async fn fetch_story(
id: String,
) -> Result<RefCell<Option<api::Story>>, ServerFnError> {
Ok(RefCell::new(
api::fetch_api::<api::Story>(&api::story(&format!("item/{id}"))).await,
))
}
#[component]
pub fn Story() -> impl IntoView {
let params = use_params_map();
let story = create_resource(
move || params().get("id").cloned().unwrap_or_default(),
move |id| async move {
if id.is_empty() {
Ok(RefCell::new(None))
} else {
fetch_story(id).await
}
},
);
let meta_description = move || {
story
.with(|story| {
story
.as_ref()
.map(|story| {
story.borrow().as_ref().map(|story| story.title.clone())
})
.ok()
})
.flatten()
.flatten()
.unwrap_or_else(|| "Loading story...".to_string())
};
let story_view = move || {
story.with(|story| {
story.as_ref().ok().and_then(|story| {
let story: Option<api::Story> = story.borrow_mut().take();
story.map(|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">
{story.points}
" points | by "
<A href=format!("/users/{user}")>{user.clone()}</A>
{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">
{story.comments.unwrap_or_default().into_iter()
.map(|comment: api::Comment| view! { <Comment comment /> })
.collect_view()}
</ul>
</div>
</div>
}})})})
};
view! {
<Meta name="description" content=meta_description/>
<Suspense fallback=|| ()>
{story_view}
</Suspense>
}
}
#[island]
pub fn Toggle(children: Children) -> impl IntoView {
let (open, set_open) = create_signal(true);
let children = std::cell::LazyCell::new(|| children().into_view());
view! {
<div class="toggle" class:open=open>
<a on:click=move |_| set_open.update(|n| *n = !*n)>
{move || if open() {
"[-]"
} else {
"[+] comments collapsed"
}}
</a>
</div>
<ul
class="comment-children"
style:display=move || if open() {
"block"
} else {
"none"
}
>
{move || open().then(|| children.clone())}
</ul>
}
}
#[component]
pub fn Comment(comment: api::Comment) -> impl IntoView {
view! {
<li class="comment">
<div class="by">
<A href=format!("/users/{}", comment.user.clone().unwrap_or_default())>{comment.user.clone()}</A>
{format!(" {}", comment.time_ago)}
</div>
<div class="text" inner_html=comment.content></div>
{(!comment.comments.is_empty()).then(|| {
view! {
<Toggle>
{comment.comments.into_iter()
.map(|comment: api::Comment| view! { <Comment comment /> })
.collect_view()}
</Toggle>
}
})}
</li>
}
}

View File

@@ -0,0 +1,53 @@
use crate::api::{self, User};
use leptos::*;
use leptos_router::*;
#[server(FetchUser, "/api")]
pub async fn fetch_user(
id: String,
) -> Result<Option<api::User>, ServerFnError> {
Ok(api::fetch_api::<User>(&api::user(&id)).await)
}
#[component]
pub fn User() -> impl IntoView {
let params = use_params_map();
let user = create_resource(
move || params().get("id").cloned().unwrap_or_default(),
move |id| async move {
if id.is_empty() {
Ok(None)
} else {
fetch_user(id).await
}
},
);
view! {
<div class="user-view">
<Suspense fallback=|| ()>
{move || user.read().map(|user| user.map(|user| match user {
None => view! { <h1>"User not found."</h1> }.into_view(),
Some(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>
<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>
}.into_view()
}))}
</Suspense>
</div>
}
}

View 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
}

View File

@@ -107,8 +107,7 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
}
}
// The struct name and path prefix arguments are optional.
#[server]
#[server(DeleteTodo, "/api")]
pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
let pool = pool()?;

View File

@@ -173,7 +173,7 @@ pub struct PostMetadata {
title: String,
}
#[server]
#[server(ListPostMetadata, "/api")]
pub async fn list_post_metadata() -> Result<Vec<PostMetadata>, ServerFnError> {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
Ok(POSTS
@@ -185,7 +185,7 @@ pub async fn list_post_metadata() -> Result<Vec<PostMetadata>, ServerFnError> {
.collect())
}
#[server]
#[server(GetPost, "/api")]
pub async fn get_post(id: usize) -> Result<Option<Post>, ServerFnError> {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
Ok(POSTS.iter().find(|post| post.id == id).cloned())

View File

@@ -173,7 +173,7 @@ pub struct PostMetadata {
title: String,
}
#[server]
#[server(ListPostMetadata, "/api")]
pub async fn list_post_metadata() -> Result<Vec<PostMetadata>, ServerFnError> {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
Ok(POSTS
@@ -185,7 +185,7 @@ pub async fn list_post_metadata() -> Result<Vec<PostMetadata>, ServerFnError> {
.collect())
}
#[server]
#[server(GetPost, "/api")]
pub async fn get_post(id: usize) -> Result<Option<Post>, ServerFnError> {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
Ok(POSTS.iter().find(|post| post.id == id).cloned())

View File

@@ -78,4 +78,4 @@ cargo leptos watch # or cargo run...
cargo make test-ui
```
_See the [E2E README](./e2e/README.md) for more information about the testing strategy._
_See the [E2E README](./e2e/README.md) for more information about the testing strategy._

View File

@@ -8,13 +8,4 @@ condition = { env_true = ["RUN_AUTOMATICALLY"] }
[tasks.test-ui]
command = "cargo"
args = [
"test",
"--test",
"app_suite",
"--",
"--retry",
"2",
"--fail-fast",
"${@}",
]
args = ["test", "--test", "app_suite", "--", "--fail-fast", "${@}"]

View File

@@ -4,14 +4,14 @@ use leptos_router::*;
const WAIT_ONE_SECOND: u64 = 1;
const WAIT_TWO_SECONDS: u64 = 2;
#[server]
#[server(FirstWaitFn "/api")]
async fn first_wait_fn(seconds: u64) -> Result<(), ServerFnError> {
tokio::time::sleep(tokio::time::Duration::from_secs(seconds)).await;
Ok(())
}
#[server]
#[server(SecondWaitFn "/api")]
async fn second_wait_fn(seconds: u64) -> Result<(), ServerFnError> {
tokio::time::sleep(tokio::time::Duration::from_secs(seconds)).await;

View File

@@ -62,8 +62,7 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
}
}
// The struct name and path prefix arguments are optional.
#[server]
#[server(DeleteTodo, "/api")]
pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
let mut conn = db().await?;

View File

@@ -79,8 +79,7 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
}
}
// The struct name and path prefix arguments are optional.
#[server]
#[server(DeleteTodo, "/api")]
pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
let mut conn = db().await?;

View File

@@ -79,8 +79,7 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
}
}
// The struct name and path prefix arguments are optional.
#[server]
#[server(DeleteTodo, "/api")]
pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
let mut conn = db().await?;

View File

@@ -23,3 +23,4 @@ tokio = { version = "1", features = ["rt"] }
[features]
nonce = ["leptos/nonce"]
islands = ["leptos_integration_utils/islands"]

View File

@@ -27,7 +27,6 @@ use leptos_router::*;
use parking_lot::RwLock;
use regex::Regex;
use std::{fmt::Display, future::Future, sync::Arc};
#[cfg(debug_assertions)]
use tracing::instrument;
/// This struct lets you define headers and override the status of the Response from an Element or a Server Function
/// Typically contained inside of a ResponseOptions. Setting this is useful for cookies and custom responses.

View File

@@ -25,3 +25,4 @@ once_cell = "1.17"
[features]
nonce = ["leptos/nonce"]
islands = ["leptos_integration_utils/islands"]

View File

@@ -13,4 +13,7 @@ leptos = { workspace = true, features = ["ssr"] }
leptos_hot_reload = { workspace = true }
leptos_meta = { workspace = true, features = ["ssr"] }
leptos_config = { workspace = true }
tracing="0.1.37"
tracing = "0.1.37"
[features]
islands = []

View File

@@ -7,18 +7,14 @@ extern crate tracing;
#[tracing::instrument(level = "trace", fields(error), skip_all)]
fn autoreload(nonce_str: &str, options: &LeptosOptions) -> String {
let reload_port = match options.reload_external_port {
Some(val) => val,
None => options.reload_port,
};
let site_ip = &options.site_addr.ip().to_string();
let reload_port = options.reload_port;
match std::env::var("LEPTOS_WATCH").is_ok() {
true => format!(
r#"
<script crossorigin=""{nonce_str}>(function () {{
{}
let protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
let host = window.location.hostname;
let ws = new WebSocket(protocol + host + ':{reload_port}/live_reload');
let ws = new WebSocket('ws://{site_ip}:{reload_port}/live_reload');
ws.onmessage = (ev) => {{
let msg = JSON.parse(ev.data);
if (msg.all) window.location.reload();
@@ -76,6 +72,27 @@ pub fn html_parts_separated(
.as_ref()
.map(|meta| meta.dehydrate())
.unwrap_or_default();
let import_callback = if cfg!(feature = "islands") {
/* r#"() => {
for (let e of document.querySelectorAll("leptos-island")) {
let l = e.dataset.component;
console.log("hydrating island");
mod["_island_" + l];
}
mod.hydrate();
}"# */
r#"() => {
for (let e of document.querySelectorAll("leptos-island")) {
let l = e.dataset.component;
mod["_island_" + l](e);
}
mod.hydrate();
}
"#
//r#"()=>{for(let e of document.querySelectorAll("leptos-island")){let l=e.dataset.component;mod["_island_"+l](e)};mod.hydrate();}"#
} else {
"() => mod.hydrate()"
};
let head = format!(
r#"<!DOCTYPE html>
<html{html_metadata}>
@@ -85,7 +102,21 @@ pub fn html_parts_separated(
{head}
<link rel="modulepreload" href="/{pkg_path}/{output_name}.js"{nonce}>
<link rel="preload" href="/{pkg_path}/{wasm_output_name}.wasm" as="fetch" type="application/wasm" crossorigin=""{nonce}>
<script type="module"{nonce}>import init, {{ hydrate }} from '/{pkg_path}/{output_name}.js'; init('/{pkg_path}/{wasm_output_name}.wasm').then(hydrate);</script>
<script type="module"{nonce}>
function idle(c) {{
if ("requestIdleCallback" in window) {{
window.requestIdleCallback(c);
}} else {{
c();
}}
}}
idle(() => {{
import('/{pkg_path}/{output_name}.js')
.then(mod => {{
mod.default('/{pkg_path}/{wasm_output_name}.wasm').then({import_callback});
}})
}});
</script>
{leptos_autoreload}
"#
);

View File

@@ -22,3 +22,4 @@ parking_lot = "0.12.1"
[features]
nonce = ["leptos/nonce"]
islands = ["leptos_integration_utils/islands"]

View File

@@ -10,6 +10,7 @@ readme = "../README.md"
[dependencies]
cfg-if = "1"
const_format = "0.2"
leptos_dom = { workspace = true }
leptos_macro = { workspace = true }
leptos_reactive = { workspace = true }
@@ -17,27 +18,34 @@ leptos_server = { workspace = true }
leptos_config = { workspace = true }
tracing = "0.1"
typed-builder = "0.14"
serde = { optional = true }
serde_json = { optional = true }
server_fn = { workspace = true }
web-sys = { version = "0.3.63", optional = true }
wasm-bindgen = { version = "0.2", optional = true }
xxhash-rust = "0.8"
[dev-dependencies]
leptos = { path = "." }
[features]
default = ["serde"]
template_macro = ["leptos_dom/web", "web-sys", "wasm-bindgen"]
template_macro = ["leptos_dom/web", "dep:web-sys", "dep:wasm-bindgen"]
csr = [
"leptos_dom/csr",
"leptos_macro/csr",
"leptos_reactive/csr",
"leptos_server/csr",
"dep:wasm-bindgen",
"dep:web-sys",
]
hydrate = [
"leptos_dom/hydrate",
"leptos_macro/hydrate",
"leptos_reactive/hydrate",
"leptos_server/hydrate",
"dep:wasm-bindgen",
"dep:web-sys",
]
default-tls = ["leptos_server/default-tls", "server_fn/default-tls"]
rustls = ["leptos_server/rustls", "server_fn/rustls"]
@@ -60,6 +68,13 @@ miniserde = ["leptos_reactive/miniserde"]
rkyv = ["leptos_reactive/rkyv"]
tracing = ["leptos_macro/tracing"]
nonce = ["leptos_dom/nonce"]
islands = [
"leptos_dom/islands",
"leptos_macro/islands",
"leptos_reactive/islands",
"dep:serde",
"dep:serde_json",
]
[package.metadata.cargo-all-features]
denylist = [

View File

@@ -151,14 +151,23 @@ pub use leptos_config::{self, get_configuration, LeptosOptions};
any(feature = "csr", feature = "hydrate")
)))]
/// Utilities for server-side rendering HTML.
#[cfg(any(doc, feature = "ssr"))]
pub mod ssr {
pub use leptos_dom::{ssr::*, ssr_in_order::*};
}
pub use leptos_dom::{
self, create_node_ref, debug_warn, document, error, ev, helpers::*, html,
log, math, mount_to, mount_to_body, nonce, svg, warn, window, Attribute,
Class, CollectView, Errors, Fragment, HtmlElement, IntoAttribute,
IntoClass, IntoProperty, IntoStyle, IntoView, NodeRef, Property, View,
self, create_node_ref, debug_warn, document, error, ev,
helpers::{
event_target, event_target_checked, event_target_value,
request_animation_frame, request_animation_frame_with_handle,
request_idle_callback, request_idle_callback_with_handle, set_interval,
set_interval_with_handle, set_timeout, set_timeout_with_handle,
window_event_listener, window_event_listener_untyped,
},
html, log, math, mount_to, mount_to_body, mount_to_with_stop_hydrating,
nonce, svg, warn, window, Attribute, Class, CollectView, Errors, Fragment,
HtmlElement, IntoAttribute, IntoClass, IntoProperty, IntoStyle, IntoView,
NodeRef, Property, View,
};
/// Types to make it easier to handle errors in your application.
@@ -167,7 +176,7 @@ pub mod error {
}
#[cfg(not(any(target_arch = "wasm32", feature = "template_macro")))]
pub use leptos_macro::view as template;
pub use leptos_macro::{component, server, slot, view, Params};
pub use leptos_macro::{component, island, server, slot, view, Params};
pub use leptos_reactive::*;
pub use leptos_server::{
self, create_action, create_multi_action, create_server_action,
@@ -185,16 +194,40 @@ mod for_loop;
mod show;
pub use animated_show::*;
pub use for_loop::*;
#[cfg(feature = "islands")]
pub use serde;
#[cfg(feature = "islands")]
pub use serde_json;
pub use show::*;
pub use suspense_component::*;
mod suspense_component;
mod text_prop;
mod transition;
// used by the component macro to generate islands
#[doc(hidden)]
pub use const_format;
pub use text_prop::TextProp;
#[cfg(any(debug_assertions, feature = "ssr"))]
#[doc(hidden)]
pub use tracing;
pub use transition::*;
#[doc(hidden)]
#[cfg(any(
feature = "csr",
feature = "hydrate",
feature = "template_macro"
))]
pub use wasm_bindgen; // used in islands
#[doc(hidden)]
#[cfg(any(
feature = "csr",
feature = "hydrate",
feature = "template_macro"
))]
pub use web_sys; // used in islands
// used by the component macro to generate islands
#[doc(hidden)]
pub use xxhash_rust;
extern crate self as leptos;
/// The most common type for the `children` property on components,

View File

@@ -73,6 +73,9 @@ where
let owner =
Owner::current().expect("<Suspense/> created with no reactive owner");
// provided for fallback
provide_context(context);
// provide this SuspenseContext to any resources below it
// run in a memo so the children are children of this parent
let children = create_memo({
@@ -83,14 +86,6 @@ where
}
});
// likewise for the fallback
let fallback = create_memo({
move |_| {
provide_context(context);
fallback().into_view()
}
});
let current_id = HydrationCtx::next_component();
let child = DynChild::new({
@@ -103,7 +98,7 @@ where
if context.ready() {
children_rendered
} else {
fallback.get_untracked()
fallback().into_view()
}
}
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
@@ -125,7 +120,6 @@ where
// show the fallback, but also prepare to stream HTML
else {
HydrationCtx::continue_from(current_id);
let runtime = leptos_reactive::current_runtime();
SharedContext::register_suspense(
context,
@@ -134,9 +128,6 @@ where
{
let orig_children = Rc::clone(&orig_children);
move || {
leptos_reactive::set_current_runtime(
runtime,
);
with_owner(owner, {
move || {
HydrationCtx::continue_from(
@@ -158,9 +149,6 @@ where
{
let orig_children = Rc::clone(&orig_children);
move || {
leptos_reactive::set_current_runtime(
runtime,
);
with_owner(owner, {
move || {
HydrationCtx::continue_from(
@@ -180,7 +168,7 @@ where
);
// return the fallback for now, wrapped in fragment identifier
fallback.get_untracked()
fallback().into_view()
}
}
}

Some files were not shown because too many files have changed in this diff Show More