Compare commits

..

5 Commits

Author SHA1 Message Date
Greg Johnston
fdf6ebaeaf fix: typed route params with #[derive(Params)] 2023-02-07 13:13:51 -05:00
martin frances
e59ee6329e Minor: Clippy router now uses types OnFormData and OnResponse. (#484) 2023-02-07 09:52:29 -05:00
Greg Johnston
a2b31a51d9 fix: errors on 404 page in axum_errors example (#485) 2023-02-07 09:51:52 -05:00
Jan
b0a98d8b4f Better styling for router related components (#477) 2023-02-06 18:34:39 -05:00
Greg Johnston
6931d3904b remove unnecessary "openssl" feature from Actix examples (#480) 2023-02-06 09:10:09 -05:00
10 changed files with 156 additions and 39 deletions

View File

@@ -8,7 +8,7 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", optional = true, features = ["openssl", "macros"] }
actix-web = { version = "4", optional = true, features = ["macros"] }
broadcaster = "1"
console_log = "0.2"
console_error_panic_hook = "0.1"

View File

@@ -9,12 +9,14 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
anyhow = "1"
actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", optional = true, features = ["openssl", "macros"] }
actix-web = { version = "4", optional = true, features = ["macros"] }
console_log = "0.2"
console_error_panic_hook = "0.1"
futures = "0.3"
cfg-if = "1"
leptos = { path = "../../leptos", default-features = false, features = ["serde"] }
leptos = { path = "../../leptos", default-features = false, features = [
"serde",
] }
leptos_meta = { path = "../../meta", default-features = false }
leptos_actix = { path = "../../integrations/actix", default-features = false, optional = true }
leptos_router = { path = "../../router", default-features = false }
@@ -47,26 +49,26 @@ 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"
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"
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 = "127.0.0.1:3000"
# The port to use for automatic reload monitoring
reload-port = 3001
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"
browserquery = "defaults"
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
watch = false
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target
@@ -87,4 +89,4 @@ 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-default-features = false

View File

@@ -86,21 +86,19 @@ pub fn ContactList(cx: Scope) -> impl IntoView {
}
}
#[derive(Params, PartialEq, Clone, Debug)]
pub struct ContactParams {
id: usize,
}
#[component]
pub fn Contact(cx: Scope) -> impl IntoView {
log::debug!("rendering <Contact/>");
let params = use_params_map(cx);
let params = use_params::<ContactParams>(cx);
let contact = create_resource(
cx,
move || {
params()
.get("id")
.cloned()
.unwrap_or_default()
.parse::<usize>()
.ok()
},
move || params().map(|params| params.id).ok(),
// any of the following would work (they're identical)
// move |id| async move { get_contact(id).await }
// move |id| get_contact(id),

View File

@@ -8,7 +8,7 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
actix-files = { version = "0.6.2", optional = true }
actix-web = { version = "4.2.1", optional = true, features = ["openssl", "macros"] }
actix-web = { version = "4.2.1", optional = true, features = ["macros"] }
anyhow = "1.0.68"
broadcaster = "1.0.0"
console_log = "0.2.0"
@@ -49,26 +49,26 @@ 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 = "todo_app_sqlite"
output-name = "todo_app_sqlite"
# 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"
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 = "127.0.0.1:3000"
# The port to use for automatic reload monitoring
reload-port = 3001
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"
browserquery = "defaults"
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
watch = false
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target

View File

@@ -176,3 +176,25 @@ pub type ChildrenFn = Box<dyn Fn(Scope) -> Fragment>;
/// A type for the `children` property on components that can be called
/// more than once, but may mutate the children.
pub type ChildrenFnMut = Box<dyn FnMut(Scope) -> Fragment>;
/// A type for taking anything that implements [`IntoAttribute`].
/// Very usefull inside components.
///
/// ## Example
/// ```rust
/// use leptos::*;
///
/// #[component]
/// pub fn MyHeading(
/// cx: Scope,
/// text: String,
/// #[prop(optional, into)]
/// class: Option<AttributeValue>
/// ) -> impl IntoView {
/// view!{
/// cx,
/// <h1 class=class>{text}</h1>
/// }
/// }
/// ```
pub type AttributeValue = Box<dyn IntoAttribute>;

View File

@@ -102,24 +102,67 @@ impl std::fmt::Debug for Attribute {
pub trait IntoAttribute {
/// Converts the object into an [Attribute].
fn into_attribute(self, cx: Scope) -> Attribute;
/// Helper function for dealing with [Box<dyn IntoAttribute>]
fn into_attribute_boxed(self: Box<Self>, cx: Scope) -> Attribute;
}
impl<T: IntoAttribute + 'static> From<T> for Box<dyn IntoAttribute> {
fn from(value: T) -> Self {
Box::new(value)
}
}
impl IntoAttribute for Attribute {
#[inline]
fn into_attribute(self, _: Scope) -> Attribute {
self
}
#[inline]
fn into_attribute_boxed(self: Box<Self>, _: Scope) -> Attribute {
*self
}
}
macro_rules! impl_into_attr_boxed {
() => {
#[inline]
fn into_attribute_boxed(self: Box<Self>, cx: Scope) -> Attribute {
self.into_attribute(cx)
}
};
}
impl IntoAttribute for Option<Attribute> {
fn into_attribute(self, cx: Scope) -> Attribute {
self.unwrap_or(Attribute::Option(cx, None))
}
impl_into_attr_boxed! {}
}
impl IntoAttribute for String {
fn into_attribute(self, _: Scope) -> Attribute {
Attribute::String(self)
}
impl_into_attr_boxed! {}
}
impl IntoAttribute for bool {
fn into_attribute(self, _: Scope) -> Attribute {
Attribute::Bool(self)
}
impl_into_attr_boxed! {}
}
impl IntoAttribute for Option<String> {
fn into_attribute(self, cx: Scope) -> Attribute {
Attribute::Option(cx, self)
}
impl_into_attr_boxed! {}
}
impl<T, U> IntoAttribute for T
@@ -131,12 +174,35 @@ where
let modified_fn = Rc::new(move || (self)().into_attribute(cx));
Attribute::Fn(cx, modified_fn)
}
impl_into_attr_boxed! {}
}
impl<T: IntoAttribute> IntoAttribute for (Scope, T) {
fn into_attribute(self, _: Scope) -> Attribute {
self.1.into_attribute(self.0)
}
impl_into_attr_boxed! {}
}
impl IntoAttribute for (Scope, Option<Box<dyn IntoAttribute>>) {
fn into_attribute(self, _: Scope) -> Attribute {
match self.1 {
Some(bx) => bx.into_attribute_boxed(self.0),
None => Attribute::Option(self.0, None),
}
}
impl_into_attr_boxed! {}
}
impl IntoAttribute for (Scope, Box<dyn IntoAttribute>) {
fn into_attribute(self, _: Scope) -> Attribute {
self.1.into_attribute_boxed(self.0)
}
impl_into_attr_boxed! {}
}
macro_rules! attr_type {
@@ -145,12 +211,22 @@ macro_rules! attr_type {
fn into_attribute(self, _: Scope) -> Attribute {
Attribute::String(self.to_string())
}
#[inline]
fn into_attribute_boxed(self: Box<Self>, cx: Scope) -> Attribute {
self.into_attribute(cx)
}
}
impl IntoAttribute for Option<$attr_type> {
fn into_attribute(self, cx: Scope) -> Attribute {
Attribute::Option(cx, self.map(|n| n.to_string()))
}
#[inline]
fn into_attribute_boxed(self: Box<Self>, cx: Scope) -> Attribute {
self.into_attribute(cx)
}
}
};
}

View File

@@ -29,7 +29,7 @@ pub fn impl_params(ast: &syn::DeriveInput) -> proc_macro::TokenStream {
let gen = quote! {
impl Params for #name {
fn from_map(map: &::leptos_router::ParamsMap) -> Result<Self, ::leptos_router::RouterError> {
fn from_map(map: &::leptos_router::ParamsMap) -> Result<Self, ::leptos_router::ParamsError> {
Ok(Self {
#(#fields,)*
})

View File

@@ -4,6 +4,9 @@ use std::{error::Error, rc::Rc};
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
type OnFormData = Rc<dyn Fn(&web_sys::FormData)>;
type OnResponse = Rc<dyn Fn(&web_sys::Response)>;
/// An HTML [`form`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form) progressively
/// enhanced to use client-side routing.
#[component]
@@ -30,13 +33,14 @@ pub fn Form<A>(
error: Option<RwSignal<Option<Box<dyn Error>>>>,
/// A callback will be called with the [FormData](web_sys::FormData) when the form is submitted.
#[prop(optional)]
#[allow(clippy::type_complexity)]
on_form_data: Option<Rc<dyn Fn(&web_sys::FormData)>>,
on_form_data: Option<OnFormData>,
/// Sets the `class` attribute on the underlying `<form>` tag, making it easier to style.
#[prop(optional, into)]
class: Option<AttributeValue>,
/// A callback will be called with the [Response](web_sys::Response) the server sends in response
/// to a form submission.
#[prop(optional)]
#[allow(clippy::type_complexity)]
on_response: Option<Rc<dyn Fn(&web_sys::Response)>>,
on_response: Option<OnResponse>,
/// Component children; should include the HTML of the form elements.
children: Children,
) -> impl IntoView
@@ -50,8 +54,9 @@ where
enctype: Option<String>,
version: Option<RwSignal<usize>>,
error: Option<RwSignal<Option<Box<dyn Error>>>>,
#[allow(clippy::type_complexity)] on_form_data: Option<Rc<dyn Fn(&web_sys::FormData)>>,
#[allow(clippy::type_complexity)] on_response: Option<Rc<dyn Fn(&web_sys::Response)>>,
on_form_data: Option<OnFormData>,
on_response: Option<OnResponse>,
class: Option<Attribute>,
children: Children,
) -> HtmlElement<Form> {
let action_version = version;
@@ -128,6 +133,7 @@ where
action=move || action.get()
enctype=enctype
on:submit=on_submit
class=class
>
{children(cx)}
</form>
@@ -135,6 +141,7 @@ where
}
let action = use_resolved_path(cx, move || action.to_href()());
let class = class.map(|bx| bx.into_attribute_boxed(cx));
inner(
cx,
method,
@@ -144,6 +151,7 @@ where
error,
on_form_data,
on_response,
class,
children,
)
}
@@ -158,6 +166,9 @@ pub fn ActionForm<I, O>(
/// by default using [create_server_action](leptos_server::create_server_action) or added
/// manually using [leptos_server::Action::using_server_fn].
action: Action<I, Result<O, ServerFnError>>,
/// Sets the `class` attribute on the underlying `<form>` tag, making it easier to style.
#[prop(optional, into)]
class: Option<AttributeValue>,
/// Component children; should include the HTML of the form elements.
children: Children,
) -> impl IntoView
@@ -208,7 +219,7 @@ where
action.set_pending(false);
});
});
let class = class.map(|bx| bx.into_attribute_boxed(cx));
Form(
cx,
FormProps::builder()
@@ -217,6 +228,7 @@ where
.on_form_data(on_form_data)
.on_response(on_response)
.method("post")
.class(class)
.children(children)
.build(),
)
@@ -232,6 +244,9 @@ pub fn MultiActionForm<I, O>(
/// by default using [create_server_action](leptos_server::create_server_action) or added
/// manually using [leptos_server::Action::using_server_fn].
action: MultiAction<I, Result<O, ServerFnError>>,
/// Sets the `class` attribute on the underlying `<form>` tag, making it easier to style.
#[prop(optional, into)]
class: Option<AttributeValue>,
/// Component children; should include the HTML of the form elements.
children: Children,
) -> impl IntoView
@@ -265,10 +280,12 @@ where
}
};
let class = class.map(|bx| bx.into_attribute_boxed(cx));
view! { cx,
<form
method="POST"
action=action
class=class
on:submit=on_submit
>
{children(cx)}

View File

@@ -63,7 +63,7 @@ pub fn A<H>(
replace: bool,
/// Sets the `class` attribute on the underlying `<a>` tag, making it easier to style.
#[prop(optional, into)]
class: Option<MaybeSignal<String>>,
class: Option<AttributeValue>,
/// The nodes or elements to be shown inside the link.
children: Children,
) -> impl IntoView
@@ -76,7 +76,7 @@ where
exact: bool,
state: Option<State>,
replace: bool,
class: Option<MaybeSignal<String>>,
class: Option<AttributeValue>,
children: Children,
) -> HtmlElement<A> {
let location = use_location(cx);
@@ -104,7 +104,7 @@ where
prop:state={state.map(|s| s.to_js_value())}
prop:replace={replace}
aria-current=move || if is_active.get() { Some("page") } else { None }
class=move || class.as_ref().map(|class| class.get())
class=class
>
{children(cx)}
</a>

View File

@@ -45,8 +45,10 @@
//! // LR will enhance the active <a> link with the [aria-current] attribute
//! // we can use this for styling them with CSS like `[aria-current] { font-weight: bold; }`
//! <A href="contacts">"Contacts"</A>
//! <A href="about">"About"</A>
//! <A href="settings">"Settings"</A>
//! // But we can also use a normal class attribute like it is a normal component
//! <A href="settings" class="my-class">"Settings"</A>
//! // It also supports signals!
//! <A href="about" class=move || "my-class">"About"</A>
//! </nav>
//! <main>
//! // <Routes/> both defines our routes and shows them on the page