Compare commits

..

20 Commits

Author SHA1 Message Date
Greg Johnston
d5ad8f5ae4 fix: don't disable non-reactive access warnings at component body level 2023-07-03 16:13:45 -04:00
Greg Johnston
6b3e9cf85f fix: improve diagnostics for untracked reads on create_slice 2023-07-03 16:09:32 -04:00
Greg Johnston
66f54e7f1a docs: add docs on responses/redirects and clarification re: Axum State(_) extractors (#1272) 2023-07-03 09:58:02 -04:00
Greg Johnston
81e416b085 fix: error messages in dyn_classes (#1270) 2023-07-03 09:57:50 -04:00
Marc-Stefan Cassola
a5f73b441c feat: added watch helper (#1262) 2023-07-03 09:29:40 -04:00
Greg Johnston
0f1ebccad5 fix: clearing <For/> that has a previous sibling in release mode (fixes #1258) (#1267) 2023-07-02 17:27:39 -04:00
Greg Johnston
2f01df6185 fix: HtmlElement::dyn_classes() when adding classes (#1265) 2023-07-02 17:27:24 -04:00
martin frances
c4982319fe chore: ran cargo clippy --fix and reviewed changes. (#1259) 2023-07-02 17:27:14 -04:00
Michael Zimmermann
8fb4e88439 feat: implement PartialEq on ServerFnError (#1260)
This allows returning it in a memo.
2023-07-02 17:22:06 -04:00
Greg Johnston
e821efca07 chore: new cargo fmt (#1266) 2023-07-02 17:01:39 -04:00
Sridhar Ratnakumar
568f7b21ae example/readme: Link to 'VS Browser' ext; format. (#1261) 2023-07-02 16:56:59 -04:00
Greg Johnston
d3c0f5320c docs: update 02_getting_started.md (#1256) 2023-06-30 17:26:33 -04:00
Greg Johnston
5adc88bf50 fix: hot-reloading view marker line number (#1255) 2023-06-30 14:03:54 -04:00
Greg Johnston
67300adf41 fix: regression in ability to use signals directly in the view in stable (#1254) 2023-06-30 11:59:29 -04:00
afiqzx
4a3a67bf37 feat: add fallback support for workspace in get_config_from_str (#1249) 2023-06-30 10:44:27 -04:00
Dương
8150847218 test(router_example): add playwright tests (#1247)
* implemented e2e tests for router example

* chore(router_example): cleanup e2e/package.json
2023-06-30 10:40:33 -04:00
Greg Johnston
8cb95b4646 docs: update server fn docs (#1252) 2023-06-30 10:40:06 -04:00
Joseph Cruz
df4ce904a0 test(counters_stable): add missing e2e tests (#1251)
* test(counters_stable): remove unused ids

* test(counters_stable): enter count

* refactor(counters_stable/e2e): improve test names

* refactor(counters_stable): move page object

* refactor(counters_stable/e2e): target nth counter

* test(counters_stable): remove counter

* refactor(counters_stable/e2e): change description
2023-06-30 08:52:48 -04:00
Ari Seyhun
1cc3a43268 chore: remove unused variable warnings with ssr props (#1244) 2023-06-30 08:05:20 -04:00
Greg Johnston
d5a862a406 v0.4.0 (#1250) 2023-06-30 07:51:07 -04:00
46 changed files with 890 additions and 139 deletions

View File

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

View File

@@ -31,7 +31,7 @@ cargo init leptos-tutorial
`cd` into your new `leptos-tutorial` project and add `leptos` as a dependency
```bash
cargo add leptos
cargo add leptos --features=csr,nightly # or just csr if you're using stable Rust
```
Create a simple `index.html` in the root of the `leptos-tutorial` directory

View File

@@ -62,6 +62,8 @@ pub async fn axum_extract(cx: Scope) -> Result<String, ServerFnError> {
These are relatively simple examples accessing basic data from the server. But you can use extractors to access things like headers, cookies, database connection pools, and more, using the exact same `extract()` pattern.
> Note: For now, the Axum `extract` function only supports extractors for which the state is `()`, i.e., you can't yet use it to extract `State(_)`. You can access `State(_)` by using a custom handler that extracts the state and then provides it via context. [Click here for an example](https://github.com/leptos-rs/leptos/blob/a5f73b441c079f9138102b3a7d8d4828f045448c/examples/session_auth_axum/src/main.rs#L91-L92).
## A Note about Data-Loading Patterns
Because Actix and (especially) Axum are built on the idea of a single round-trip HTTP request and response, you typically run extractors near the “top” of your application (i.e., before you start rendering) and use the extracted data to determine how that should be rendered. Before you render a `<button>`, you load all the data your app could need. And any given route handler needs to know all the data that will need to be extracted by that route.

View File

@@ -1 +1,74 @@
# Responses and Redirects
Extractors provide an easy way to access request data inside server functions. Leptos also provides a way to modify the HTTP response, using the `ResponseOptions` type (see docs for [Actix](https://docs.rs/leptos_actix/latest/leptos_actix/struct.ResponseOptions.html) or [Axum](https://docs.rs/leptos_axum/latest/leptos_axum/struct.ResponseOptions.html)) types and the `redirect` helper function (see docs for [Actix](https://docs.rs/leptos_actix/latest/leptos_actix/fn.redirect.html) or [Axum](https://docs.rs/leptos_axum/latest/leptos_axum/fn.redirect.html)).
## `ResponseOptions`
`ResponseOptions` is provided via context during the initial server rendering response and during any subsequent server function call. It allows you to easily set the status code for the HTTP response, or to add headers to the HTTP response, e.g., to set cookies.
```rust
#[server(TeaAndCookies)]
pub async fn tea_and_cookies(cx: Scope) -> Result<(), ServerFnError> {
use actix_web::{cookie::Cookie, http::header, http::header::HeaderValue};
use leptos_actix::ResponseOptions;
// pull ResponseOptions from context
let response = expect_context::<ResponseOptions>(cx);
// set the HTTP status code
response.set_status(StatusCode::IM_A_TEAPOT);
// set a cookie in the HTTP response
let mut cookie = Cookie::build("biscuits", "yes").finish();
if let Ok(cookie) = HeaderValue::from_str(&cookie.to_string()) {
res.insert_header(header::SET_COOKIE, cookie);
}
}
```
## `redirect`
One common modification to an HTTP response is to redirect to another page. The Actix and Axum integrations provide a `redirect` function to make this easy to do. `redirect` simply sets an HTTP status code of `302 Found` and sets the `Location` header.
Heres a simplified example from our [`session_auth_axum` example](https://github.com/leptos-rs/leptos/blob/a5f73b441c079f9138102b3a7d8d4828f045448c/examples/session_auth_axum/src/auth.rs#L154-L181).
```rust
#[server(Login, "/api")]
pub async fn login(
cx: Scope,
username: String,
password: String,
remember: Option<String>,
) -> Result<(), ServerFnError> {
// pull the DB pool and auth provider from context
let pool = pool(cx)?;
let auth = auth(cx)?;
// check whether the user exists
let user: User = User::get_from_username(username, &pool)
.await
.ok_or_else(|| {
ServerFnError::ServerError("User does not exist.".into())
})?;
// check whether the user has provided the correct password
match verify(password, &user.password)? {
// if the password is correct...
true => {
// log the user in
auth.login_user(user.id);
auth.remember_user(remember.is_some());
// and redirect to the home page
leptos_axum::redirect(cx, "/");
Ok(())
}
// if not, return an error
false => Err(ServerFnError::ServerError(
"Password does not match.".to_string(),
)),
}
}
```
This server function can then be used from your application. This `redirect` works well with the progressively-enhanced `<ActionForm/>` component: without JS/WASM, the server response will redirect because of the status code and header. With JS/WASM, the `<ActionForm/>` will detect the redirect in the server function response, and use client-side navigation to redirect to the new page.

View File

@@ -1,8 +1,8 @@
import { test, expect } from "@playwright/test";
import { CountersPage } from "./counters_page";
import { CountersPage } from "./fixtures/counters_page";
test.describe("Add 1000 Counters", () => {
test("should increment the total count by 1K", async ({ page }) => {
test("should increase the number of counters", async ({ page }) => {
const ui = new CountersPage(page);
await Promise.all([

View File

@@ -1,8 +1,8 @@
import { test, expect } from "@playwright/test";
import { CountersPage } from "./counters_page";
import { CountersPage } from "./fixtures/counters_page";
test.describe("Add Counter", () => {
test("should increment the total count", async ({ page }) => {
test("should increase the number of counters", async ({ page }) => {
const ui = new CountersPage(page);
await ui.goto();

View File

@@ -1,5 +1,5 @@
import { test, expect } from "@playwright/test";
import { CountersPage } from "./counters_page";
import { CountersPage } from "./fixtures/counters_page";
test.describe("Clear Counters", () => {
test("should reset the counts", async ({ page }) => {

View File

@@ -1,8 +1,8 @@
import { test, expect } from "@playwright/test";
import { CountersPage } from "./counters_page";
import { CountersPage } from "./fixtures/counters_page";
test.describe("Decrement Count", () => {
test("should decrement the total count", async ({ page }) => {
test("should decrease the total count", async ({ page }) => {
const ui = new CountersPage(page);
await ui.goto();
await ui.addCounter();

View File

@@ -0,0 +1,31 @@
import { test, expect } from "@playwright/test";
import { CountersPage } from "./fixtures/counters_page";
test.describe("Enter Count", () => {
test("should increase the total count", async ({ page }) => {
const ui = new CountersPage(page);
await ui.goto();
await ui.addCounter();
await ui.enterCount("5");
await expect(ui.total).toHaveText("5");
await expect(ui.counters).toHaveText("1");
});
test("should decrease the total count", async ({ page }) => {
const ui = new CountersPage(page);
await ui.goto();
await ui.addCounter();
await ui.addCounter();
await ui.addCounter();
await ui.enterCount("100");
await ui.enterCount("100", 1);
await ui.enterCount("100", 2);
await ui.enterCount("50", 1);
await expect(ui.total).toHaveText("250");
await expect(ui.counters).toHaveText("3");
});
});

View File

@@ -5,8 +5,11 @@ export class CountersPage {
readonly addCounterButton: Locator;
readonly addOneThousandCountersButton: Locator;
readonly clearCountersButton: Locator;
readonly decrementCountButton: Locator;
readonly incrementCountButton: Locator;
readonly counterInput: Locator;
readonly decrementCountButton: Locator;
readonly removeCountButton: Locator;
readonly total: Locator;
readonly counters: Locator;
@@ -32,9 +35,15 @@ export class CountersPage {
hasText: "+1",
});
this.removeCountButton = page.locator("button", {
hasText: "x",
});
this.total = page.getByTestId("total");
this.counters = page.getByTestId("counters");
this.counterInput = page.getByRole("textbox");
}
async goto() {
@@ -52,17 +61,17 @@ export class CountersPage {
this.addOneThousandCountersButton.click();
}
async decrementCount() {
async decrementCount(index: number = 0) {
await Promise.all([
this.decrementCountButton.waitFor(),
this.decrementCountButton.click(),
this.decrementCountButton.nth(index).waitFor(),
this.decrementCountButton.nth(index).click(),
]);
}
async incrementCount() {
async incrementCount(index: number = 0) {
await Promise.all([
this.incrementCountButton.waitFor(),
this.incrementCountButton.click(),
this.incrementCountButton.nth(index).waitFor(),
this.incrementCountButton.nth(index).click(),
]);
}
@@ -72,4 +81,18 @@ export class CountersPage {
this.clearCountersButton.click(),
]);
}
async enterCount(count: string, index: number = 0) {
await Promise.all([
this.counterInput.nth(index).waitFor(),
this.counterInput.nth(index).fill(count),
]);
}
async removeCounter(index: number = 0) {
await Promise.all([
this.removeCountButton.nth(index).waitFor(),
this.removeCountButton.nth(index).click(),
]);
}
}

View File

@@ -1,8 +1,8 @@
import { test, expect } from "@playwright/test";
import { CountersPage } from "./counters_page";
import { CountersPage } from "./fixtures/counters_page";
test.describe("Increment Count", () => {
test("should increment the total count", async ({ page }) => {
test("should increase the total count", async ({ page }) => {
const ui = new CountersPage(page);
await ui.goto();
await ui.addCounter();

View File

@@ -0,0 +1,18 @@
import { test, expect } from "@playwright/test";
import { CountersPage } from "./fixtures/counters_page";
test.describe("Remove Counter", () => {
test("should decrement the number of counters", async ({ page }) => {
const ui = new CountersPage(page);
await ui.goto();
await ui.addCounter();
await ui.addCounter();
await ui.addCounter();
await ui.removeCounter(1);
await expect(ui.total).toHaveText("0");
await expect(ui.counters).toHaveText("2");
});
});

View File

@@ -1,8 +1,8 @@
import { test, expect } from "@playwright/test";
import { CountersPage } from "./counters_page";
import { CountersPage } from "./fixtures/counters_page";
test.describe("View Counters", () => {
test("should_see_the_title", async ({ page }) => {
test("should see the title", async ({ page }) => {
const ui = new CountersPage(page);
await ui.goto();

View File

@@ -56,7 +56,7 @@ pub fn Counters(cx: Scope) -> impl IntoView {
</button>
<p>
"Total: "
<span id="total" data-testid="total">{move ||
<span data-testid="total">{move ||
counters.get()
.iter()
.map(|(_, (count, _))| count.get())
@@ -64,7 +64,7 @@ pub fn Counters(cx: Scope) -> impl IntoView {
.to_string()
}</span>
" from "
<span id="counters" data-testid="counters">{move || counters.with(|counters| counters.len()).to_string()}</span>
<span data-testid="counters">{move || counters.with(|counters| counters.len()).to_string()}</span>
" counters."
</p>
<ul>
@@ -104,7 +104,7 @@ fn Counter(
prop:value={move || value.get().to_string()}
on:input=input
/>
<span>{move || value.get().to_string()}</span>
<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>

21
examples/router/.gitignore vendored Normal file
View File

@@ -0,0 +1,21 @@
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
# Support playwright testing
node_modules/
test-results/
end2end/playwright-report/
playwright/.cache/
pnpm-lock.yaml
# Support trunk
dist

View File

@@ -1,4 +1,8 @@
extend = { path = "../cargo-make/main.toml" }
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/trunk_server.toml" },
{ path = "../cargo-make/playwright-test.toml" },
]
[tasks.build]
command = "cargo"

36
examples/router/e2e/package-lock.json generated Normal file
View File

@@ -0,0 +1,36 @@
{
"name": "e2e",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@playwright/test": {
"version": "1.35.1",
"dev": true,
"requires": {
"@types/node": "*",
"fsevents": "2.3.2",
"playwright-core": "1.35.1"
}
},
"@types/node": {
"version": "20.3.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.2.tgz",
"integrity": "sha512-vOBLVQeCQfIcF/2Y7eKFTqrMnizK5lRNQ7ykML/5RuwVXVWxYkgwS7xbt4B6fKCUPgbSL5FSsjHQpaGQP/dQmw==",
"dev": true
},
"fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"optional": true
},
"playwright-core": {
"version": "1.35.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.35.1.tgz",
"integrity": "sha512-pNXb6CQ7OqmGDRspEjlxE49w+4YtR6a3X6mT1hZXeJHWmsEz7SunmvZeiG/+y1yyMZdHnnn73WKYdtV1er0Xyg==",
"dev": true
}
}
}

View File

@@ -0,0 +1,7 @@
{
"private": "true",
"scripts": {},
"devDependencies": {
"@playwright/test": "^1.35.1"
}
}

View File

@@ -0,0 +1,77 @@
import { defineConfig, devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "./tests",
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 10 : 10,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "list",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://127.0.0.1:8080",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
// {
// name: "firefox",
// use: { ...devices["Desktop Firefox"] },
// },
// {
// name: "webkit",
// use: { ...devices["Desktop Safari"] },
// },
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ..devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
// webServer: {
// command: "cd ../ && trunk serve",
// url: "http://127.0.0.1:8080",
// reuseExistingServer: false, //!process.env.CI,
// },
});

View File

@@ -0,0 +1,30 @@
import { test, expect } from "@playwright/test";
test.describe("Test Router example", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/");
});
const links = [
{ label: "Bill Smith", url: "/0" },
{ label: "Tim Jones", url: "/1" },
{ label: "Sally Stevens", url: "/2" },
{ label: "About", url: "/about" },
{ label: "Settings", url: "/settings" },
];
links.forEach(({ label, url }) => {
test(`Can navigate to ${label}`, async ({ page }) => {
await page.getByRole("link", { name: label }).click();
await expect(page.getByRole("heading", { name: label })).toBeVisible();
await expect(page).toHaveURL(url);
});
});
test("Can redirect to home", async ({ page }) => {
await page.getByRole("link", { name: "About" }).click();
await page.getByRole("link", { name: "Redirect to Home" }).click();
await expect(page).toHaveURL("/");
});
});

View File

@@ -0,0 +1,11 @@
{
"private": true,
"scripts": {
"start-server": "trunk serve",
"e2e": "cargo make test-playwright",
"e2e:auto-start": "start-server-and-test start-server http://127.0.0.1:8080 e2e"
},
"devDependencies": {
"start-server-and-test": "^2.0.0"
}
}

View File

@@ -52,8 +52,9 @@ If you're using VS Code, add the following to your `settings.json`
Install [Tailwind CSS Intellisense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss).
Install "VS Browser" extension, a browser at the right window.
Allow vscode Ports forward: 3000, 3001.
Install [VS Browser](https://marketplace.visualstudio.com/items?itemName=Phu1237.vs-browser) extension (allows you to open a browser at the right window.
Allow vscode Ports forward: 3000, 3001.
## Notes about Tooling

View File

@@ -1315,6 +1315,12 @@ impl<B> From<Request<B>> for ExtractorHelper {
/// .map_err(|e| ServerFnError::ServerError("Could not extract method and query...".to_string()))
/// }
/// ```
///
/// > Note: For now, the Axum `extract` function only supports extractors for
/// which the state is `()`, i.e., you can't yet use it to extract `State(_)`.
/// You can access `State(_)` by using a custom handler that extracts the state
/// and then provides it via context.
/// [Click here for an example](https://github.com/leptos-rs/leptos/blob/a5f73b441c079f9138102b3a7d8d4828f045448c/examples/session_auth_axum/src/main.rs#L91-L92).
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub async fn extract<T, U>(
cx: Scope,

View File

@@ -73,7 +73,7 @@ where
let child = DynChild::new({
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
let current_id = current_id.clone();
let current_id = current_id;
let children = Rc::new(orig_children(cx).into_view(cx));
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
@@ -94,10 +94,10 @@ where
// run the child; we'll probably throw this away, but it will register resource reads
//let after_original_child = HydrationCtx::peek();
let initial = {
{
// no resources were read under this, so just return the child
if context.pending_resources.get() == 0 {
HydrationCtx::continue_from(current_id.clone());
HydrationCtx::continue_from(current_id);
DynChild::new({
let children = Rc::clone(&children);
move || (*children).clone()
@@ -115,9 +115,7 @@ where
{
let orig_children = Rc::clone(&orig_children);
move || {
HydrationCtx::continue_from(
current_id.clone(),
);
HydrationCtx::continue_from(current_id);
DynChild::new({
let orig_children =
orig_children(cx).into_view(cx);
@@ -132,9 +130,7 @@ where
{
let orig_children = Rc::clone(&orig_children);
move || {
HydrationCtx::continue_from(
current_id.clone(),
);
HydrationCtx::continue_from(current_id);
DynChild::new({
let orig_children =
orig_children(cx).into_view(cx);
@@ -149,9 +145,7 @@ where
// return the fallback for now, wrapped in fragment identifier
fallback().into_view(cx)
}
};
initial
}
}
}
})

View File

@@ -153,16 +153,30 @@ impl TryFrom<String> for Env {
/// If an env var is specified, like `LEPTOS_ENV`, it will override a setting in the file.
pub fn get_config_from_str(text: &str) -> Result<ConfFile, LeptosConfigError> {
let re: Regex = Regex::new(r#"(?m)^\[package.metadata.leptos\]"#).unwrap();
let start = match re.find(text) {
Some(found) => found.start(),
None => return Err(LeptosConfigError::ConfigSectionNotFound),
let re_workspace: Regex =
Regex::new(r#"(?m)^\[\[workspace.metadata.leptos\]\]"#).unwrap();
let metadata_name;
let start;
match re.find(text) {
Some(found) => {
metadata_name = "[package.metadata.leptos]";
start = found.start();
}
None => match re_workspace.find(text) {
Some(found) => {
metadata_name = "[[workspace.metadata.leptos]]";
start = found.start();
}
None => return Err(LeptosConfigError::ConfigSectionNotFound),
},
};
// so that serde error messages have right line number
let newlines = text[..start].matches('\n').count();
let input = "\n".repeat(newlines) + &text[start..];
let toml = input
.replace("[package.metadata.leptos]", "[leptos_options]")
.replace(metadata_name, "[leptos_options]")
.replace('-', "_");
let settings = Config::builder()
// Read the "default" configuration file

View File

@@ -271,8 +271,9 @@ where
let mut repr = ComponentRepr::new_with_id(name, id);
// disposed automatically when the parent scope is disposed
let (child, _) = cx
.run_child_scope(|cx| cx.untrack(|| children_fn(cx).into_view(cx)));
let (child, _) = cx.run_child_scope(|cx| {
cx.untrack_with_diagnostics(|| children_fn(cx).into_view(cx))
});
repr.children.push(child);

View File

@@ -822,7 +822,11 @@ fn apply_diff<T, EF, V>(
#[cfg(not(debug_assertions))]
parent.append_with_node_1(closing).unwrap();
} else {
#[cfg(debug_assertions)]
range.set_start_after(opening).unwrap();
#[cfg(not(debug_assertions))]
range.set_start_before(opening).unwrap();
range.set_end_before(closing).unwrap();
range.delete_contents().unwrap();

View File

@@ -733,22 +733,24 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
.collect::<SmallVec<[Cow<'static, str>; 4]>>(
);
let mut new_classes = classes
let new_classes = classes
.iter()
.flat_map(|classes| classes.split_whitespace());
if let Some(prev_classes) = prev_classes {
let new_classes =
new_classes.collect::<SmallVec<[_; 4]>>();
let mut old_classes = prev_classes
.iter()
.flat_map(|classes| classes.split_whitespace());
// Remove old classes
for prev_class in old_classes.clone() {
if !new_classes.any(|c| c == prev_class) {
if !new_classes.iter().any(|c| c == &prev_class) {
class_list.remove_1(prev_class).unwrap_or_else(
|err| {
panic!(
"failed to add class \
"failed to remove class \
`{prev_class}`, error: {err:#?}"
)
},
@@ -761,7 +763,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
if !old_classes.any(|c| c == class) {
class_list.add_1(class).unwrap_or_else(|err| {
panic!(
"failed to remove class `{class}`, \
"failed to add class `{class}`, \
error: {err:#?}"
)
});

View File

@@ -33,7 +33,7 @@ pub use html::HtmlElement;
use html::{AnyElement, ElementDescriptor};
pub use hydration::{HydrationCtx, HydrationKey};
use leptos_reactive::Scope;
#[cfg(feature = "stable")]
#[cfg(not(feature = "nightly"))]
use leptos_reactive::{
MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet,
};
@@ -143,7 +143,7 @@ where
}
}
#[cfg(feature = "stable")]
#[cfg(not(feature = "nightly"))]
impl<T> IntoView for ReadSignal<T>
where
T: IntoView + Clone,
@@ -156,7 +156,7 @@ where
DynChild::new(move || self.get()).into_view(cx)
}
}
#[cfg(feature = "stable")]
#[cfg(not(feature = "nightly"))]
impl<T> IntoView for RwSignal<T>
where
T: IntoView + Clone,
@@ -169,7 +169,7 @@ where
DynChild::new(move || self.get()).into_view(cx)
}
}
#[cfg(feature = "stable")]
#[cfg(not(feature = "nightly"))]
impl<T> IntoView for Memo<T>
where
T: IntoView + Clone,
@@ -182,7 +182,7 @@ where
DynChild::new(move || self.get()).into_view(cx)
}
}
#[cfg(feature = "stable")]
#[cfg(not(feature = "nightly"))]
impl<T> IntoView for Signal<T>
where
T: IntoView + Clone,
@@ -195,7 +195,7 @@ where
DynChild::new(move || self.get()).into_view(cx)
}
}
#[cfg(feature = "stable")]
#[cfg(not(feature = "nightly"))]
impl<T> IntoView for MaybeSignal<T>
where
T: IntoView + Clone,

View File

@@ -296,7 +296,7 @@ fn ooo_body_stream_recurse(
fragments.chain(resources).chain(
futures::stream::once(async move {
let pending = cx.pending_fragments();
if pending.len() > 0 {
if !pending.is_empty() {
let fragments = FuturesUnordered::new();
let serializers = cx.serialization_resolvers();
for (fragment_id, data) in pending {

View File

@@ -70,7 +70,7 @@ impl ViewMacros {
let mut views = Vec::new();
for view in visitor.views {
let span = view.span();
let id = span_to_stable_id(path, span);
let id = span_to_stable_id(path, span.start().line);
let mut tokens = view.tokens.clone().into_iter();
tokens.next(); // cx
tokens.next(); // ,
@@ -148,15 +148,11 @@ impl<'ast> Visit<'ast> for ViewMacroVisitor<'ast> {
}
}
pub fn span_to_stable_id(
path: impl AsRef<Path>,
site: proc_macro2::Span,
) -> String {
pub fn span_to_stable_id(path: impl AsRef<Path>, line: usize) -> String {
let file = path
.as_ref()
.to_str()
.unwrap_or_default()
.replace(['/', '\\'], "-");
let start = site.start();
format!("{}-{:?}", file, start.line)
format!("{file}-{line}")
}

View File

@@ -3,7 +3,7 @@ function patch(json) {
try {
const views = JSON.parse(json);
for (const [id, patches] of views) {
console.log("[HOT RELOAD]", patches);
console.log("[HOT RELOAD]", id, patches);
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT),
open = `leptos-view|${id}|open`,
close = `leptos-view|${id}|close`;

View File

@@ -438,15 +438,18 @@ impl Docs {
let mut attrs = attrs
.iter()
.filter_map(|attr| {
let Meta::NameValue(attr ) = &attr.meta else {
return None
let Meta::NameValue(attr) = &attr.meta else {
return None;
};
if !attr.path.is_ident("doc") {
return None
return None;
}
let Some(val) = value_to_string(&attr.value) else {
abort!(attr, "expected string literal in value of doc comment");
abort!(
attr,
"expected string literal in value of doc comment"
);
};
Some((val, attr.path.span()))

View File

@@ -384,7 +384,7 @@ fn normalized_call_site(site: proc_macro::Span) -> Option<String> {
if #[cfg(all(debug_assertions, feature = "nightly"))] {
Some(leptos_hot_reload::span_to_stable_id(
site.source_file().path(),
site.into()
site.start().line()
))
} else {
_ = site;
@@ -793,15 +793,18 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
/// If you call a server function from the client (i.e., when the `csr` or `hydrate` features
/// are enabled), it will instead make a network request to the server.
///
/// You can specify one, two, or three arguments to the server function:
/// You can specify one, two, three, or four arguments to the server function:
/// 1. **Required**: A type name that will be used to identify and register the server function
/// (e.g., `MyServerFn`).
/// 2. *Optional*: A URL prefix at which the function will be mounted when its registered
/// (e.g., `"/api"`). Defaults to `"/"`.
/// 3. *Optional*: either `"Cbor"` (specifying that it should use the binary `cbor` format for
/// serialization) or `"Url"` (specifying that it should be use a URL-encoded form-data string).
/// Defaults to `"Url"`. If you want to use this server function to power a `<form>` that will
/// work without WebAssembly, the encoding must be `"Url"`.
/// 3. *Optional*: The encoding for the server function (`"Url"`, `"Cbor"`, `"GetJson"`, or `"GetCbor`". See **Server Function Encodings** below.)
/// 4. *Optional*: A specific endpoint path to be used in the URL. (By default, a unique path will be generated.)
///
/// ```rust,ignore
/// // will generate a server function at `/api-prefix/hello`
/// #[server(MyServerFnType, "/api-prefix", "Url", "hello")]
/// ```
///
/// The server function itself can take any number of arguments, each of which should be serializable
/// and deserializable with `serde`. Optionally, its first argument can be a Leptos
@@ -821,17 +824,16 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
/// ```
///
/// Note the following:
/// - You must **register** the server function by calling `T::register()` somewhere in your main function.
/// - **Server functions must be `async`.** Even if the work being done inside the function body
/// can run synchronously on the server, from the clients perspective it involves an asynchronous
/// function call.
/// - **Server functions must return `Result<T, ServerFnError>`.** Even if the work being done
/// inside the function body cant fail, the processes of serialization/deserialization and the
/// network call are fallible.
/// - **Return types must be [Serializable](https://docs.rs/leptos/latest/leptos/trait.Serializable.html).**
/// - **Return types must implement [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html).**
/// This should be fairly obvious: we have to serialize arguments to send them to the server, and we
/// need to deserialize the result to return it to the client.
/// - **Arguments must be implement [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html)
/// - **Arguments must implement [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html)
/// and [`DeserializeOwned`](https://docs.rs/serde/latest/serde/de/trait.DeserializeOwned.html).**
/// They are serialized as an `application/x-www-form-urlencoded`
/// form data using [`serde_qs`](https://docs.rs/serde_qs/latest/serde_qs/) or as `application/cbor`
@@ -840,6 +842,9 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
/// - **The `Scope` comes from the server.** Optionally, the first argument of a server function
/// can be a Leptos `Scope`. This scope can be used to inject dependencies like the HTTP request
/// or response or other server-only dependencies, but it does *not* have access to reactive state that exists in the client.
/// - Your server must be ready to handle the server functions at the API prefix you list. The easiest way to do this
/// is to use the `handle_server_fns` function from [`leptos_actix`](https://docs.rs/leptos_actix/latest/leptos_actix/fn.handle_server_fns.html)
/// or [`leptos_axum`](https://docs.rs/leptos_axum/latest/leptos_axum/fn.handle_server_fns.html).
///
/// ## Server Function Encodings
///

View File

@@ -582,6 +582,13 @@ fn attribute_to_tokens_ssr<'a>(
{
// ignore props for SSR
// ignore classes and styles: we'll handle these separately
if name.starts_with("prop:") {
let value = attr.value();
exprs_for_compiler.push(quote! {
#[allow(unused_braces)]
{ _ = #value; }
});
}
} else if name == "inner_html" {
return attr.value();
} else {
@@ -1367,8 +1374,8 @@ pub(crate) fn parse_event_name(name: &str) -> (TokenStream, bool, bool) {
let is_custom = event_type == "Custom";
let Ok(event_type) = event_type.parse::<TokenStream>() else {
abort!(event_type, "couldn't parse event name");
};
abort!(event_type, "couldn't parse event name");
};
let event_type = if is_custom {
quote! { Custom::new(#name) }
@@ -1397,7 +1404,10 @@ pub(crate) fn slot_to_tokens(
let span = node.name().span();
let Some(parent_slots) = parent_slots else {
proc_macro_error::emit_error!(span, "slots cannot be used inside HTML elements");
proc_macro_error::emit_error!(
span,
"slots cannot be used inside HTML elements"
);
return;
};

View File

@@ -95,6 +95,7 @@ mod spawn_microtask;
mod stored_value;
pub mod suspense;
mod trigger;
mod watch;
pub use context::*;
pub use diagnostics::SpecialNonReactiveZone;
@@ -116,6 +117,7 @@ pub use spawn_microtask::*;
pub use stored_value::*;
pub use suspense::{GlobalSuspenseContext, SuspenseContext};
pub use trigger::*;
pub use watch::*;
mod macros {
macro_rules! debug_warn {

View File

@@ -1,11 +1,12 @@
#![forbid(unsafe_code)]
use crate::{
hydration::SharedContext,
node::{NodeId, ReactiveNode, ReactiveNodeState, ReactiveNodeType},
AnyComputation, AnyResource, Effect, Memo, MemoState, ReadSignal,
ResourceId, ResourceState, RwSignal, Scope, ScopeDisposer, ScopeId,
ScopeProperty, SerializableResource, StoredValueId, Trigger,
UnserializableResource, WriteSignal,
ScopeProperty, SerializableResource, SpecialNonReactiveZone, StoredValueId,
Trigger, UnserializableResource, WriteSignal,
};
use cfg_if::cfg_if;
use core::hash::BuildHasherDefault;
@@ -358,6 +359,7 @@ impl Debug for Runtime {
.finish()
}
}
/// Get the selected runtime from the thread-local set of runtimes. On the server,
/// this will return the correct runtime. In the browser, there should only be one runtime.
#[cfg_attr(
@@ -472,6 +474,43 @@ impl RuntimeId {
ret
}
#[cfg_attr(
any(debug_assertions, features = "ssr"),
instrument(level = "trace", skip_all,)
)]
#[inline(always)]
pub(crate) fn untrack<T>(
self,
f: impl FnOnce() -> T,
diagnostics: bool,
) -> T {
with_runtime(self, |runtime| {
let untracked_result;
if !diagnostics {
SpecialNonReactiveZone::enter();
}
let prev_observer =
SetObserverOnDrop(self, runtime.observer.take());
untracked_result = f();
runtime.observer.set(prev_observer.1);
std::mem::forget(prev_observer); // avoid Drop
if !diagnostics {
SpecialNonReactiveZone::exit();
}
untracked_result
})
.expect(
"tried to run untracked function in a runtime that has been \
disposed",
)
}
#[track_caller]
#[inline(always)] // only because it's placed here to fit in with the other create methods
pub(crate) fn create_trigger(self) -> Trigger {
@@ -681,6 +720,81 @@ impl RuntimeId {
)
}
pub(crate) fn watch<W, T>(
self,
deps: impl Fn() -> W + 'static,
callback: impl Fn(&W, Option<&W>, Option<T>) -> T + Clone + 'static,
immediate: bool,
) -> (NodeId, impl Fn() + Clone)
where
W: Clone + 'static,
T: 'static,
{
let cur_deps_value = Rc::new(RefCell::new(None::<W>));
let prev_deps_value = Rc::new(RefCell::new(None::<W>));
let prev_callback_value = Rc::new(RefCell::new(None::<T>));
let wrapped_callback = {
let cur_deps_value = Rc::clone(&cur_deps_value);
let prev_deps_value = Rc::clone(&prev_deps_value);
let prev_callback_value = Rc::clone(&prev_callback_value);
move || {
callback(
cur_deps_value.borrow().as_ref().expect(
"this will not be called before there is deps value",
),
prev_deps_value.borrow().as_ref(),
prev_callback_value.take(),
)
}
};
let effect_fn = {
let prev_callback_value = Rc::clone(&prev_callback_value);
move |did_run_before: Option<()>| {
let deps_value = deps();
let did_run_before = did_run_before.is_some();
if !immediate && !did_run_before {
prev_deps_value.replace(Some(deps_value));
return;
}
cur_deps_value.replace(Some(deps_value.clone()));
let callback_value =
Some(self.untrack(wrapped_callback.clone(), false));
prev_callback_value.replace(callback_value);
prev_deps_value.replace(Some(deps_value));
}
};
let id = self.create_concrete_effect(
Rc::new(RefCell::new(None::<()>)),
Rc::new(Effect {
f: effect_fn,
ty: PhantomData,
#[cfg(any(debug_assertions, feature = "ssr"))]
defined_at: std::panic::Location::caller(),
}),
);
(id, move || {
with_runtime(self, |runtime| {
runtime.nodes.borrow_mut().remove(id);
runtime.node_sources.borrow_mut().remove(id);
})
.expect(
"tried to stop a watch in a runtime that has been disposed",
);
})
}
#[track_caller]
#[inline(always)]
pub(crate) fn create_memo<T>(
@@ -828,3 +942,13 @@ impl std::hash::Hash for Runtime {
std::ptr::hash(&self, state);
}
}
struct SetObserverOnDrop(RuntimeId, Option<NodeId>);
impl Drop for SetObserverOnDrop {
fn drop(&mut self) {
_ = with_runtime(self.0, |rt| {
rt.observer.set(self.1);
});
}
}

View File

@@ -5,8 +5,7 @@ use crate::{
node::NodeId,
runtime::{with_runtime, RuntimeId},
suspense::StreamChunk,
PinnedFuture, ResourceId, SpecialNonReactiveZone, StoredValueId,
SuspenseContext,
PinnedFuture, ResourceId, StoredValueId, SuspenseContext,
};
use futures::stream::FuturesUnordered;
use std::{
@@ -209,37 +208,14 @@ impl Scope {
)]
#[inline(always)]
pub fn untrack<T>(&self, f: impl FnOnce() -> T) -> T {
with_runtime(self.runtime, |runtime| {
let untracked_result;
SpecialNonReactiveZone::enter();
let prev_observer =
SetObserverOnDrop(self.runtime, runtime.observer.take());
untracked_result = f();
runtime.observer.set(prev_observer.1);
std::mem::forget(prev_observer); // avoid Drop
SpecialNonReactiveZone::exit();
untracked_result
})
.expect(
"tried to run untracked function in a runtime that has been \
disposed",
)
self.runtime.untrack(f, false)
}
}
struct SetObserverOnDrop(RuntimeId, Option<NodeId>);
impl Drop for SetObserverOnDrop {
fn drop(&mut self) {
_ = with_runtime(self.0, |rt| {
rt.observer.set(self.1);
});
#[doc(hidden)]
/// Suspends reactive tracking but keeps the diagnostic warnings for
/// untracked functions.
pub fn untrack_with_diagnostics<T>(&self, f: impl FnOnce() -> T) -> T {
self.runtime.untrack(f, true)
}
}
@@ -349,6 +325,27 @@ impl Scope {
any(debug_assertions, features = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub(crate) fn remove_scope_property(&self, prop: ScopeProperty) {
_ = with_runtime(self.runtime, |runtime| {
let scopes = runtime.scopes.borrow();
if let Some(scope) = scopes.get(self.id) {
let mut scope = scope.borrow_mut();
if let Some(index) = scope.iter().position(|p| p == &prop) {
scope.swap_remove(index);
}
} else {
console_warn(
"tried to remove property to a scope that has been \
disposed",
)
}
})
}
#[cfg_attr(
any(debug_assertions, features = "ssr"),
instrument(level = "trace", skip_all,)
)]
/// Returns the the parent Scope, if any.
pub fn parent(&self) -> Option<Scope> {
match with_runtime(self.runtime, |runtime| {
@@ -392,7 +389,7 @@ slotmap::new_key_type! {
pub struct ScopeId;
}
#[derive(Debug)]
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub(crate) enum ScopeProperty {
Trigger(NodeId),
Signal(NodeId),

View File

@@ -68,6 +68,7 @@ use crate::{
/// // setting name only causes name to log, not count
/// set_name.set("Bob".into());
/// ```
#[track_caller]
pub fn create_slice<T, O, S>(
cx: Scope,
signal: RwSignal<T>,
@@ -85,6 +86,7 @@ where
/// Takes a memoized, read-only slice of a signal. This is equivalent to the
/// read-only half of [`create_slice`].
#[track_caller]
pub fn create_read_slice<T, O>(
cx: Scope,
signal: RwSignal<T>,
@@ -98,6 +100,7 @@ where
/// Creates a setter to access one slice of a signal. This is equivalent to the
/// write-only half of [`create_slice`].
#[track_caller]
pub fn create_write_slice<T, O>(
cx: Scope,
signal: RwSignal<T>,

View File

@@ -0,0 +1,114 @@
use crate::{Scope, ScopeProperty};
/// A version of [`create_effect`] that listens to any dependency that is accessed inside `deps` and returns
/// a stop handler.
/// The return value of `deps` is passed into `callback` as an argument together with the previous value.
/// Additionally the last return value of `callback` is provided as a third argument as is done in [`create_effect`].
///
/// ## Usage
///
/// ```
/// # use leptos_reactive::*;
/// # use log;
/// # create_scope(create_runtime(), |cx| {
/// let (num, set_num) = create_signal(cx, 0);
///
/// let stop = watch(
/// cx,
/// move || num.get(),
/// move |num, prev_num, _| {
/// log::debug!("Number: {}; Prev: {:?}", num, prev_num);
/// },
/// false,
/// );
///
/// set_num.set(1); // > "Number: 1; Prev: Some(0)"
///
/// stop(); // stop watching
///
/// set_num.set(2); // (nothing happens)
/// # }).dispose();
/// ```
///
/// The callback itself doesn't track any signal that is accessed within it.
///
/// ```
/// # use leptos_reactive::*;
/// # use log;
/// # create_scope(create_runtime(), |cx| {
/// let (num, set_num) = create_signal(cx, 0);
/// let (cb_num, set_cb_num) = create_signal(cx, 0);
///
/// watch(
/// cx,
/// move || num.get(),
/// move |num, _, _| {
/// log::debug!("Number: {}; Cb: {}", num, cb_num.get());
/// },
/// false,
/// );
///
/// set_num.set(1); // > "Number: 1; Cb: 0"
///
/// set_cb_num.set(1); // (nothing happens)
///
/// set_num.set(2); // > "Number: 2; Cb: 1"
/// # }).dispose();
/// ```
///
/// ## Immediate
///
/// If the final parameter `immediate` is true, the `callback` will run immediately.
/// If it's `false`, the `callback` will run only after
/// the first change is detected of any signal that is accessed in `deps`.
///
/// ```
/// # use leptos_reactive::*;
/// # use log;
/// # create_scope(create_runtime(), |cx| {
/// let (num, set_num) = create_signal(cx, 0);
///
/// watch(
/// cx,
/// move || num.get(),
/// move |num, prev_num, _| {
/// log::debug!("Number: {}; Prev: {:?}", num, prev_num);
/// },
/// true,
/// ); // > "Number: 0; Prev: None"
///
/// set_num.set(1); // > "Number: 1; Prev: Some(0)"
/// # }).dispose();
/// ```
#[cfg_attr(
any(debug_assertions, feature="ssr"),
instrument(
level = "trace",
skip_all,
fields(
scope = ?cx.id,
ty = %std::any::type_name::<T>()
)
)
)]
#[track_caller]
#[inline(always)]
pub fn watch<W, T>(
cx: Scope,
deps: impl Fn() -> W + 'static,
callback: impl Fn(&W, Option<&W>, Option<T>) -> T + Clone + 'static,
immediate: bool,
) -> impl Fn() + Clone
where
W: Clone + 'static,
T: 'static,
{
let (e, stop) = cx.runtime.watch(deps, callback, immediate);
let prop = ScopeProperty::Effect(e);
cx.push_scope_property(prop);
move || {
stop();
cx.remove_scope_property(prop);
}
}

View File

@@ -0,0 +1,140 @@
use leptos_reactive::{
create_runtime, create_scope, create_signal, watch, SignalGet, SignalSet,
};
use std::{cell::RefCell, rc::Rc};
#[test]
fn watch_runs() {
create_scope(create_runtime(), |cx| {
let (a, set_a) = create_signal(cx, -1);
// simulate an arbitrary side effect
let b = Rc::new(RefCell::new(String::new()));
let stop = watch(
cx,
move || a.get(),
{
let b = b.clone();
move |a, prev_a, prev_ret| {
let formatted = format!(
"Value is {}; Prev is {:?}; Prev return is {:?}",
a, prev_a, prev_ret
);
*b.borrow_mut() = formatted;
a + 10
}
},
false,
);
assert_eq!(b.borrow().as_str(), "");
set_a.set(1);
assert_eq!(
b.borrow().as_str(),
"Value is 1; Prev is Some(-1); Prev return is None"
);
set_a.set(2);
assert_eq!(
b.borrow().as_str(),
"Value is 2; Prev is Some(1); Prev return is Some(11)"
);
stop();
*b.borrow_mut() = "nothing happened".to_string();
set_a.set(3);
assert_eq!(b.borrow().as_str(), "nothing happened");
})
.dispose()
}
#[test]
fn watch_runs_immediately() {
create_scope(create_runtime(), |cx| {
let (a, set_a) = create_signal(cx, -1);
// simulate an arbitrary side effect
let b = Rc::new(RefCell::new(String::new()));
let _ = watch(
cx,
move || a.get(),
{
let b = b.clone();
move |a, prev_a, prev_ret| {
let formatted = format!(
"Value is {}; Prev is {:?}; Prev return is {:?}",
a, prev_a, prev_ret
);
*b.borrow_mut() = formatted;
a + 10
}
},
true,
);
assert_eq!(
b.borrow().as_str(),
"Value is -1; Prev is None; Prev return is None"
);
set_a.set(1);
assert_eq!(
b.borrow().as_str(),
"Value is 1; Prev is Some(-1); Prev return is Some(9)"
);
})
.dispose()
}
#[test]
fn watch_ignores_callback() {
create_scope(create_runtime(), |cx| {
let (a, set_a) = create_signal(cx, -1);
let (b, set_b) = create_signal(cx, 0);
// simulate an arbitrary side effect
let s = Rc::new(RefCell::new(String::new()));
let _ = watch(
cx,
move || a.get(),
{
let s = s.clone();
move |a, _, _| {
let formatted =
format!("Value a is {}; Value b is {}", a, b.get());
*s.borrow_mut() = formatted;
}
},
false,
);
set_a.set(1);
assert_eq!(s.borrow().as_str(), "Value a is 1; Value b is 0");
*s.borrow_mut() = "nothing happened".to_string();
set_b.set(10);
assert_eq!(s.borrow().as_str(), "nothing happened");
set_a.set(2);
assert_eq!(s.borrow().as_str(), "Value a is 2; Value b is 10");
})
.dispose()
}

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_meta"
version = "0.3.0"
version = "0.4.0"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router"
version = "0.3.0"
version = "0.4.0"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"

View File

@@ -10,16 +10,18 @@ use syn::__private::ToTokens;
/// This means that its body will only run on the server, i.e., when the `ssr`
/// feature is enabled.
///
/// You can specify one, two, or three arguments to the server function:
/// You can specify one, two, three, or four arguments to the server function:
/// 1. **Required**: A type name that will be used to identify and register the server function
/// (e.g., `MyServerFn`).
/// 2. *Optional*: A URL prefix at which the function will be mounted when its registered
/// (e.g., `"/api"`). Defaults to `"/"`.
/// 3. *Optional*: either `"Cbor"` (specifying that it should use the binary `cbor` format for
/// serialization), `"Url"` (specifying that it should be use a URL-encoded form-data string).
/// Defaults to `"Url"`. If you want to use this server function to power a `<form>` that will
/// work without WebAssembly, the encoding must be `"Url"`. If you want to use this server function
/// using Get instead of Post methods, the encoding must be `"GetCbor"` or `"GetJson"`.
/// 3. *Optional*: The encoding for the server function (`"Url"`, `"Cbor"`, `"GetJson"`, or `"GetCbor`". See **Server Function Encodings** below.)
/// 4. *Optional*: A specific endpoint path to be used in the URL. (By default, a unique path will be generated.)
///
/// ```rust,ignore
/// // will generate a server function at `/api-prefix/hello`
/// #[server(MyServerFnType, "/api-prefix", "Url", "hello")]
/// ```
///
/// The server function itself can take any number of arguments, each of which should be serializable
/// and deserializable with `serde`.

View File

@@ -54,7 +54,7 @@ impl From<ServerFnError> for Error {
/// Unlike [`ServerFnErrorErr`], this does not implement [`std::error::Error`].
/// This means that other error types can easily be converted into it using the
/// `?` operator.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ServerFnError {
/// Error while trying to register the server function (only occurs in case of poisoned RwLock).
Registration(String),

View File

@@ -385,9 +385,9 @@ impl Parse for ServerFnBody {
let docs = attrs
.iter()
.filter_map(|attr| {
let Meta::NameValue(attr ) = &attr.meta else {
return None
};
let Meta::NameValue(attr) = &attr.meta else {
return None;
};
if !attr.path.is_ident("doc") {
return None;
}