diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9e9f637ff..7728e4232 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -52,10 +52,10 @@ jobs: if: matrix.rust == '1.58.0' env: RUN_UI_TESTS: true - run: cargo test + run: cargo test --all-features - name: Run tests (excluding UI) - run: cargo test + run: cargo test --all-features if: matrix.rust != '1.58.0' - name: Run headless browser tests @@ -124,6 +124,7 @@ jobs: key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - name: Run tests + # Note that we don't have --all-features for packages/sycamore because of https://github.com/rust-lang/miri/issues/1038 run: | cd packages/sycamore cargo miri test diff --git a/docs/src/main.rs b/docs/src/main.rs index 5198d7ceb..c35fb41e4 100644 --- a/docs/src/main.rs +++ b/docs/src/main.rs @@ -61,7 +61,7 @@ fn parse(path: &Path) -> Result> { Some(event) } else { let tmp = tmp.take().unwrap(); - let anchor = tmp.name.trim().to_lowercase().replace(" ", "-"); + let anchor = tmp.name.trim().to_lowercase().replace(' ', "-"); let name = tmp.name.clone(); if level == HeadingLevel::H2 { outline_tmp.push(tmp); @@ -72,10 +72,7 @@ fn parse(path: &Path) -> Result> { l.children.push(tmp); } Some(Event::Html(CowStr::from(format!( - "<{level} id=\"{anchor}\">{name}", - level = level, - anchor = anchor, - name = name + "<{level} id=\"{anchor}\">{name}" )))) } } diff --git a/examples/hello-builder/Cargo.toml b/examples/hello-builder/Cargo.toml index 2980226ee..fb48ef1ae 100644 --- a/examples/hello-builder/Cargo.toml +++ b/examples/hello-builder/Cargo.toml @@ -1,9 +1,7 @@ [package] -authors = ["Luke Chu <37006668+lukechu10@users.noreply.github.com>"] -edition = "2021" name = "hello-builder" -publish = false version = "0.1.0" +edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -11,5 +9,6 @@ version = "0.1.0" console_error_panic_hook = "0.1.7" console_log = "0.2.0" log = "0.4.14" -sycamore = { path = "../../packages/sycamore", features = ["experimental-builder-html"] } -wasm-bindgen = "0.2.78" +sycamore = { path = "../../packages/sycamore", features = [ + "experimental-builder-agnostic", +] } diff --git a/examples/hello-builder/src/main.rs b/examples/hello-builder/src/main.rs index 5dd87ce19..31bc341ff 100644 --- a/examples/hello-builder/src/main.rs +++ b/examples/hello-builder/src/main.rs @@ -1,31 +1,30 @@ -use sycamore::builder::html::*; +//! Look ma, No `view!`! +//! +//! This example demonstrates the basics of the builder API for constructing views, as an +//! alternative to using the `view!` macro. + +use sycamore::builder::prelude::*; use sycamore::prelude::*; #[component] fn App(ctx: ScopeRef) -> View { let name = ctx.create_signal(String::new()); - - div(ctx) - .child( - h1(ctx) - .text("Hello ") - .dyn_child(move || { - if *ctx.create_selector(move || !name.get().is_empty()).get() { - span(ctx).dyn_text(move || name.get().to_string()).build() - } else { - span(ctx).text("World").build() - } - }) - .text("!") - .build(), - ) - .child(input(ctx).bind_value(name).build()) - .build() + h(div) + .c(h(h1) + .t("Hello ") + .dyn_if( + || !name.get().is_empty(), + || h(span).dyn_t(|| name.get().to_string()), + || h(span).t("World").view(ctx), + ) + .t("!")) + .c(h(input).bind_value(name)) + .view(ctx) } fn main() { console_error_panic_hook::set_once(); console_log::init_with_level(log::Level::Debug).unwrap(); - sycamore::render(|ctx| view! {ctx, App() }); + sycamore::render(|ctx| component(|| App(ctx, ()))); } diff --git a/packages/sycamore-macro/src/view/codegen.rs b/packages/sycamore-macro/src/view/codegen.rs index d328663d3..259a64d31 100644 --- a/packages/sycamore-macro/src/view/codegen.rs +++ b/packages/sycamore-macro/src/view/codegen.rs @@ -75,7 +75,7 @@ impl Codegen { let quote_tag = match tag { ElementTag::Builtin(id) => quote! { let __el = ::sycamore::generic_node::GenericNode::element( - <::sycamore::html::#id as ::sycamore::html::SycamoreElement>::TAG_NAME + <::sycamore::html::#id as ::sycamore::generic_node::SycamoreElement>::TAG_NAME ); }, ElementTag::Custom(tag_s) => quote! { diff --git a/packages/sycamore/Cargo.toml b/packages/sycamore/Cargo.toml index 7a5d2736e..1d6d6d0a5 100644 --- a/packages/sycamore/Cargo.toml +++ b/packages/sycamore/Cargo.toml @@ -53,7 +53,6 @@ wasm-bindgen-test = "0.3.29" default = ["dom", "wasm-bindgen-interning"] dom = [] experimental-builder-agnostic = [] -experimental-builder-html = ["experimental-builder-agnostic"] experimental-hydrate = ["sycamore-macro/experimental-hydrate"] ssr = ["html-escape", "once_cell", "experimental-hydrate", "sycamore-macro/ssr"] suspense = ["futures", "wasm-bindgen-futures", "sycamore-futures"] diff --git a/packages/sycamore/src/builder.rs b/packages/sycamore/src/builder.rs new file mode 100644 index 000000000..d85d3abf8 --- /dev/null +++ b/packages/sycamore/src/builder.rs @@ -0,0 +1,747 @@ +//! The builder pattern API for creating UI elements. +//! +//! This API is rendering-backend agnostic and can be used with any rendering backend, not just +//! HTML. + +use js_sys::Reflect; +use std::iter::FromIterator; +use std::marker::PhantomData; +use std::rc::Rc; +use wasm_bindgen::prelude::*; + +use crate::component::component_scope; +use crate::generic_node::{GenericNode, Html, SycamoreElement}; +use crate::noderef::NodeRef; +use crate::reactive::*; +use crate::utils::render; +use crate::view::View; + +/// The prelude for the builder API. This is independent from the _sycamore prelude_, aka. +/// [`sycamore::prelude`]. +/// +/// In most cases, it is idiomatic to use a glob import (aka wildcard import) at the beginning of +/// your Rust source file. +/// +/// ```rust +/// use sycamore::builder::prelude::*; +/// use sycamore::prelude::*; +/// ``` +pub mod prelude { + pub use super::component; + pub use super::fragment; + pub use super::h; + pub use crate::html::*; +} + +/// A factory for building [`View`]s. +pub struct ElementBuilder<'a, G: GenericNode, F: FnOnce(ScopeRef<'a>) -> G + 'a>( + F, + PhantomData<&'a ()>, +); + +/// A trait that is implemented only for [`ElementBuilder`] and [`View`]. +/// This should be considered implementation details and should not be used. +pub trait ElementBuilderOrView<'a, G: GenericNode> { + fn into_view(self, ctx: ScopeRef<'a>) -> View; +} + +impl<'a, G: GenericNode> ElementBuilderOrView<'a, G> for View { + fn into_view(self, _: ScopeRef<'a>) -> View { + self + } +} + +impl<'a, G: GenericNode, F: FnOnce(ScopeRef<'a>) -> G + 'a> ElementBuilderOrView<'a, G> + for ElementBuilder<'a, G, F> +{ + fn into_view(self, ctx: ScopeRef<'a>) -> View { + self.view(ctx) + } +} + +/// Construct a new [`ElementBuilder`] from a [`SycamoreElement`]. +/// +/// # Example +/// ``` +/// # use sycamore::builder::prelude::*; +/// # use sycamore::prelude::*; +/// # fn _test1(ctx: ScopeRef) -> View { +/// h(a) +/// # .view(ctx) } +/// # fn _test2(ctx: ScopeRef) -> View { +/// h(button) +/// # .view(ctx) } +/// # fn _test3(ctx: ScopeRef) -> View { +/// h(div) +/// # .view(ctx) } +/// // etc... +/// ``` +pub fn h<'a, E: SycamoreElement, G: GenericNode>( + _: E, +) -> ElementBuilder<'a, G, impl FnOnce(ScopeRef<'a>) -> G> { + ElementBuilder::new(move |_| G::element(E::TAG_NAME)) +} + +impl<'a, G: GenericNode, F: FnOnce(ScopeRef<'a>) -> G + 'a> ElementBuilder<'a, G, F> { + fn new(f: F) -> Self { + Self(f, PhantomData) + } + + /// Utility function for composing new [`ElementBuilder`]s. + fn map( + self, + f: impl FnOnce(ScopeRef<'a>, &G) + 'a, + ) -> ElementBuilder<'a, G, impl FnOnce(ScopeRef<'a>) -> G + 'a> { + ElementBuilder::new(move |ctx| { + let el = (self.0)(ctx); + f(ctx, &el); + el + }) + } + + /// Set the attribute of the element. + /// + /// # Example + /// ``` + /// # use sycamore::builder::prelude::*; + /// # use sycamore::prelude::*; + /// # fn _test(ctx: ScopeRef) -> View { + /// h(button).attr("type", "submit") + /// # .view(ctx) } + /// ``` + pub fn attr( + self, + name: &'a str, + value: impl AsRef + 'a, + ) -> ElementBuilder<'a, G, impl FnOnce(ScopeRef<'a>) -> G + 'a> { + self.map(move |_, el| el.set_attribute(name, value.as_ref())) + } + + /// Set the boolean attribute of the element. + /// + /// # Example + /// ``` + /// # use sycamore::builder::prelude::*; + /// # use sycamore::prelude::*; + /// # fn _test(ctx: ScopeRef) -> View { + /// h(input).bool_attr("required", true) + /// # .view(ctx) } + /// ``` + pub fn bool_attr( + self, + name: &'a str, + value: bool, + ) -> ElementBuilder<'a, G, impl FnOnce(ScopeRef<'a>) -> G + 'a> { + self.map(move |_, el| { + if value { + el.set_attribute(name, ""); + } + }) + } + + /// Adds a dynamic attribute on the node. + /// + /// If `value` is `None`, the attribute will be removed from the node. + /// + /// # Example + /// ``` + /// # use sycamore::builder::prelude::*; + /// # use sycamore::prelude::*; + /// # fn _test(ctx: ScopeRef) -> View { + /// let input_type = ctx.create_signal("text"); + /// h(input).dyn_attr("type", || Some(*input_type.get())) + /// # .view(ctx) } + /// ``` + pub fn dyn_attr + 'a>( + self, + name: &'a str, + mut value: impl FnMut() -> Option + 'a, + ) -> ElementBuilder<'a, G, impl FnOnce(ScopeRef<'a>) -> G + 'a> { + self.map(move |ctx, el| { + let el = el.clone(); + ctx.create_effect(move || { + let value = value(); + if let Some(value) = value { + el.set_attribute(name, value.as_ref()); + } else { + el.remove_attribute(name); + } + }); + }) + } + + /// Adds a dynamic boolean attribute on the node. + /// + /// # Example + /// ``` + /// # use sycamore::builder::prelude::*; + /// # use sycamore::prelude::*; + /// # fn _test(ctx: ScopeRef) -> View { + /// let required = ctx.create_signal(true); + /// h(input).dyn_bool_attr("required", || *required.get()) + /// # .view(ctx) } + /// ``` + pub fn dyn_bool_attr( + self, + name: &'a str, + mut value: impl FnMut() -> bool + 'a, + ) -> ElementBuilder<'a, G, impl FnOnce(ScopeRef<'a>) -> G + 'a> { + self.map(move |ctx, el| { + let el = el.clone(); + ctx.create_effect(move || { + if value() { + el.set_attribute(name, ""); + } else { + el.remove_attribute(name); + } + }); + }) + } + + /// Adds a class to the element. This is a shorthand for [`Self::attr`] with the `class` + /// attribute. + /// + /// # Example + /// ``` + /// # use sycamore::builder::prelude::*; + /// # use sycamore::prelude::*; + /// # fn _test(ctx: ScopeRef) -> View { + /// h(button).class("bg-green-500").t("My button") + /// # .view(ctx) } + /// ``` + pub fn class( + self, + class: impl AsRef + 'a, + ) -> ElementBuilder<'a, G, impl FnOnce(ScopeRef<'a>) -> G + 'a> { + self.map(move |_, el| el.add_class(class.as_ref())) + } + + /// Adds a dynamic class on the node. + /// + /// If `value` is `None`, the class will be removed from the element. + /// + /// # Example + /// # Example + /// ``` + /// # use sycamore::builder::prelude::*; + /// # use sycamore::prelude::*; + /// # fn _test(ctx: ScopeRef) -> View { + /// let checked_class = ctx.create_signal(false); + /// h(input) + /// .attr("type", "checkbox") + /// .dyn_class("bg-red-500", || *checked_class.get()) + /// # .view(ctx) } + /// ``` + pub fn dyn_class( + self, + class: impl AsRef + 'a, + mut apply: impl FnMut() -> bool + 'a, + ) -> ElementBuilder<'a, G, impl FnOnce(ScopeRef<'a>) -> G + 'a> { + self.map(move |ctx, el| { + let el = el.clone(); + ctx.create_effect(move || { + if apply() { + el.add_class(class.as_ref()); + } else { + el.remove_class(class.as_ref()); + } + }); + }) + } + + /// Sets the id of an element. This is a shorthand for [`Self::attr`] with the `id` attribute. + /// + /// # Example + /// ``` + /// # use sycamore::builder::prelude::*; + /// # use sycamore::prelude::*; + /// # fn _test(ctx: ScopeRef) -> View { + /// h(button).id("my-button") + /// # .view(ctx) } + /// ``` + pub fn id( + self, + class: impl AsRef + 'a, + ) -> ElementBuilder<'a, G, impl FnOnce(ScopeRef<'a>) -> G + 'a> { + self.map(move |_, el| el.set_attribute("id", class.as_ref())) + } + + /// Set a property on the element. + /// + /// # Example + /// ``` + /// # use sycamore::builder::prelude::*; + /// # use sycamore::prelude::*; + /// # fn _test(ctx: ScopeRef) -> View { + /// h(input).prop("value", "I am the value set.") + /// # .view(ctx) } + /// ``` + pub fn prop( + self, + name: impl AsRef + 'a, + property: impl Into + 'a, + ) -> ElementBuilder<'a, G, impl FnOnce(ScopeRef<'a>) -> G + 'a> { + self.map(move |_, el| el.set_property(name.as_ref(), &property.into())) + } + + /// Set a dynamic property on the element. + /// + /// # Example + /// ``` + /// # use sycamore::builder::prelude::*; + /// # use sycamore::prelude::*; + /// # fn _test(ctx: ScopeRef) -> View { + /// let checked = ctx.create_signal(false); + /// h(input) + /// .attr("type", "checkbox") + /// .dyn_prop("checked", || *checked.get()) + /// # .view(ctx) } + /// ``` + pub fn dyn_prop + 'a>( + self, + name: impl AsRef + 'a, + mut property: impl FnMut() -> V + 'a, + ) -> ElementBuilder<'a, G, impl FnOnce(ScopeRef<'a>) -> G + 'a> { + self.map(move |ctx, el| { + let el = el.clone(); + ctx.create_effect(move || { + el.set_property(name.as_ref(), &property().into()); + }); + }) + } + + /// Insert a text node under this element. The inserted child is static by default. + /// + /// # Example + /// ``` + /// # use sycamore::builder::prelude::*; + /// # use sycamore::prelude::*; + /// # fn _test(ctx: ScopeRef) -> View { + /// h(p) + /// .t("Hello World!") + /// .t("Text nodes can be chained as well.") + /// .t("More text...") + /// # .view(ctx) } + /// ``` + pub fn t(self, text: &'a str) -> ElementBuilder<'a, G, impl FnOnce(ScopeRef<'a>) -> G + 'a> { + self.map(|_, el| el.append_child(&G::text_node(text))) + } + + /// Adds a dynamic text node. + /// + /// # Example + /// ``` + /// # use sycamore::builder::prelude::*; + /// # use sycamore::prelude::*; + /// # fn _test(ctx: ScopeRef) -> View { + /// let name = ctx.create_signal("Sycamore"); + /// h(p) + /// .t("Name: ") + /// .dyn_t(|| name.get().to_string()) + /// # .view(ctx) } + /// ``` + pub fn dyn_t + 'a>( + self, + f: impl FnMut() -> S + 'a, + ) -> ElementBuilder<'a, G, impl FnOnce(ScopeRef<'a>) -> G + 'a> { + self.map(|ctx, el| { + let memo = ctx.create_memo(f); + Self::dyn_c_internal(ctx, el, move || { + View::new_node(G::text_node(memo.get().as_ref().as_ref())) + }); + }) + } + + /// Insert a child node under this element. The inserted child is static by default. + /// + /// # Example + /// ``` + /// # use sycamore::builder::prelude::*; + /// # use sycamore::prelude::*; + /// # fn _test(ctx: ScopeRef) -> View { + /// let input_type = ctx.create_signal("text"); + /// h(div).c( + /// h(h1).t("I am a child") + /// ) + /// # .view(ctx) } + /// ``` + pub fn c( + self, + c: impl ElementBuilderOrView<'a, G>, + ) -> ElementBuilder<'a, G, impl FnOnce(ScopeRef<'a>) -> G + 'a> { + self.map(|ctx, el| render::insert(ctx, el, c.into_view(ctx), None, None, true)) + } + + /// Internal implementation for [`Self::dyn_c`] and [`Self::dyn_t`]. + fn dyn_c_internal(ctx: ScopeRef<'a>, el: &G, f: impl FnMut() -> View + 'a) { + #[allow(unused_imports)] + use std::any::{Any, TypeId}; + + #[cfg(feature = "ssr")] + if TypeId::of::() == TypeId::of::() { + // If Server Side Rendering, insert beginning tag for hydration purposes. + el.append_child(&G::marker_with_text("#")); + // Create end marker. This is needed to make sure that the node is inserted into the + // right place. + let end_marker = G::marker_with_text("/"); + el.append_child(&end_marker); + render::insert( + ctx, + el, + View::new_dyn(ctx, f), + None, + Some(&end_marker), + true, /* We don't know if this is the only child or not so we pessimistically + * set this to true. */ + ); + return; + } + #[cfg(feature = "experimental-hydrate")] + if TypeId::of::() == TypeId::of::() { + use crate::utils::hydrate::web::*; + // Get start and end markers. + let el_hn = ::downcast_ref::(el).unwrap(); + let initial = get_next_marker(&el_hn.inner_element()); + // Do not drop the HydrateNode because it will be cast into a GenericNode. + let initial = ::std::mem::ManuallyDrop::new(initial); + // SAFETY: This is safe because we already checked that the type is HydrateNode. + // __initial is wrapped inside ManuallyDrop to prevent double drop. + let initial = unsafe { ::std::ptr::read(&initial as *const _ as *const _) }; + render::insert( + ctx, + el, + View::new_dyn(ctx, f), + initial, + None, + true, /* We don't know if this is the only child or not so we pessimistically + * set this to true. */ + ); + return; + } + // G is neither SsrNode nor HydrateNode. Proceed normally. + let marker = G::marker(); + el.append_child(&marker); + render::insert(ctx, el, View::new_dyn(ctx, f), None, Some(&marker), true); + } + + /// Internal implementation for [`Self::dyn_c_scoped`] and [`Self::dyn_if`]. + fn dyn_c_internal_scoped( + ctx: ScopeRef<'a>, + el: &G, + f: impl FnMut(BoundedScopeRef<'_, 'a>) -> View + 'a, + ) { + #[allow(unused_imports)] + use std::any::{Any, TypeId}; + + #[cfg(feature = "ssr")] + if TypeId::of::() == TypeId::of::() { + // If Server Side Rendering, insert beginning tag for hydration purposes. + el.append_child(&G::marker_with_text("#")); + // Create end marker. This is needed to make sure that the node is inserted into the + // right place. + let end_marker = G::marker_with_text("/"); + el.append_child(&end_marker); + render::insert( + ctx, + el, + View::new_dyn_scoped(ctx, f), + None, + Some(&end_marker), + true, /* We don't know if this is the only child or not so we + * pessimistically set this to true. */ + ); + return; + } + #[cfg(feature = "experimental-hydrate")] + if TypeId::of::() == TypeId::of::() { + use crate::utils::hydrate::web::*; + // Get start and end markers. + let el_hn = ::downcast_ref::(el).unwrap(); + let initial = get_next_marker(&el_hn.inner_element()); + // Do not drop the HydrateNode because it will be cast into a GenericNode. + let initial = ::std::mem::ManuallyDrop::new(initial); + // SAFETY: This is safe because we already checked that the type is HydrateNode. + // __initial is wrapped inside ManuallyDrop to prevent double drop. + let initial = unsafe { ::std::ptr::read(&initial as *const _ as *const _) }; + render::insert( + ctx, + el, + View::new_dyn_scoped(ctx, f), + initial, + None, + true, /* We don't know if this is the only child or not so we + * pessimistically set this to true. */ + ); + return; + } + // G is neither SsrNode nor HydrateNode. Proceed normally. + let marker = G::marker(); + el.append_child(&marker); + render::insert( + ctx, + el, + View::new_dyn_scoped(ctx, f), + None, + Some(&marker), + true, + ); + } + + /// Adds a dynamic child. Note that most times, [`dyn_if`](Self::dyn_if) can be used instead + /// which is more ergonomic. + /// + /// # Example + /// ``` + /// # use sycamore::builder::prelude::*; + /// # use sycamore::prelude::*; + /// # fn some_view() -> View { todo!() } + /// # fn _test(ctx: ScopeRef) -> View { + /// let a_view = || some_view(); + /// h(div).dyn_c(a_view) + /// # .view(ctx) } + /// ``` + pub fn dyn_c + 'a>( + self, + mut f: impl FnMut() -> O + 'a, + ) -> ElementBuilder<'a, G, impl FnOnce(ScopeRef<'a>) -> G + 'a> { + self.map(move |ctx, el| Self::dyn_c_internal(ctx, el, move || f().into_view(ctx))) + } + + /// Adds a dynamic, conditional view. + /// + /// # Example + /// ``` + /// # use sycamore::builder::prelude::*; + /// # use sycamore::prelude::*; + /// # fn _test(ctx: ScopeRef) -> View { + /// let visible = ctx.create_signal(true); + /// h(div).dyn_if( + /// || *visible.get(), + /// || h(p).t("Now you see me"), + /// || h(p).t("Now you don't!"), + /// ) + /// # .view(ctx) } + /// ``` + pub fn dyn_if + 'a, O2: ElementBuilderOrView<'a, G> + 'a>( + self, + cond: impl Fn() -> bool + 'a, + mut then: impl FnMut() -> O1 + 'a, + mut r#else: impl FnMut() -> O2 + 'a, + ) -> ElementBuilder<'a, G, impl FnOnce(ScopeRef<'a>) -> G + 'a> { + let cond = Rc::new(cond); + self.map(move |ctx, el| { + // FIXME: should be dyn_c_internal_scoped to prevent memory leaks. + Self::dyn_c_internal(ctx, el, move || { + if *ctx + .create_selector({ + let cond = Rc::clone(&cond); + #[allow(clippy::redundant_closure)] // FIXME: clippy false positive + move || cond() + }) + .get() + { + then().into_view(ctx) + } else { + r#else().into_view(ctx) + } + }); + }) + } + + /// Adds a dynamic child that is created in a new reactive scope. + /// + /// [`dyn_c`](Self::dyn_c) uses [`create_effect`](Scope::create_effect) whereas this method uses + /// [`create_effect_scoped`](Scope::create_effect_scoped). + pub fn dyn_c_scoped( + self, + f: impl FnMut(BoundedScopeRef<'_, 'a>) -> View + 'a, + ) -> ElementBuilder<'a, G, impl FnOnce(ScopeRef<'a>) -> G + 'a> { + self.map(|ctx, el| Self::dyn_c_internal_scoped(ctx, el, f)) + } + + /// Attach an event handler to the element. + /// + /// # Example + /// ``` + /// # use sycamore::builder::prelude::*; + /// # use sycamore::prelude::*; + /// # fn _test(ctx: ScopeRef) -> View { + /// h(button) + /// .t("My button") + /// .on("click", |_| web_sys::console::log_1(&"Clicked".into())) + /// # .view(ctx) } + /// ``` + pub fn on( + self, + name: &'a str, + handler: impl Fn(G::EventType) + 'a, + ) -> ElementBuilder<'a, G, impl FnOnce(ScopeRef<'a>) -> G + 'a> { + self.map(move |ctx, el| el.event(ctx, name, Box::new(handler))) + } + + /// Get a hold of the raw element by using a [`NodeRef`]. + /// + /// # Example + /// ``` + /// # use sycamore::builder::prelude::*; + /// # use sycamore::prelude::*; + /// # fn _test(ctx: ScopeRef) -> View { + /// let node_ref = ctx.create_node_ref(); + /// h(input).bind_ref(node_ref.clone()) + /// # .view(ctx) } + /// ``` + pub fn bind_ref( + self, + node_ref: NodeRef, + ) -> ElementBuilder<'a, G, impl FnOnce(ScopeRef<'a>) -> G + 'a> { + self.map(move |_, el| node_ref.set(el.clone())) + } + + /// Construct a [`View`] by evaluating the lazy [`ElementBuilder`]. + /// + /// This is the method that should be called at the end of the building chain. + /// + /// # Example + /// ``` + /// # use sycamore::builder::prelude::*; + /// # use sycamore::prelude::*; + /// #[component] + /// fn MyComponent(ctx: ScopeRef) -> View { + /// h(div) + /// /* builder stuff... */ + /// .view(ctx) + /// } + /// ``` + pub fn view(self, ctx: ScopeRef<'a>) -> View { + let el = (self.0)(ctx); + View::new_node(el) + } +} + +/// HTML-specific builder methods. +impl<'a, G: Html, F: FnOnce(ScopeRef<'a>) -> G + 'a> ElementBuilder<'a, G, F> { + /// Binds a [`Signal`] to the `value` property of the node. + /// + /// The [`Signal`] will be automatically updated when the value is updated. + /// + /// # Example + /// ``` + /// # use sycamore::builder::prelude::*; + /// # use sycamore::prelude::*; + /// # fn _test(ctx: ScopeRef) -> View { + /// let value = ctx.create_signal(String::new()); + /// h(input).bind_value(value) + /// # .view(ctx) } + /// ``` + pub fn bind_value( + self, + sub: &'a Signal, + ) -> ElementBuilder<'a, G, impl FnOnce(ScopeRef<'a>) -> G + 'a> { + self.map(move |ctx, el| { + ctx.create_effect({ + let el = el.clone(); + move || { + el.set_property("value", &sub.get().as_str().into()); + } + }); + el.event( + ctx, + "input", + Box::new(move |e: web_sys::Event| { + let val = Reflect::get( + &e.target().expect("missing target on input event"), + &"value".into(), + ) + .expect("missing property `value`") + .as_string() + .expect("value should be a string"); + sub.set(val); + }), + ); + }) + } + + /// Binds a [`Signal`] to the `checked` property of the node. + /// + /// The [`Signal`] will be automatically updated when the value is updated. + /// + /// # Example + /// ``` + /// # use sycamore::builder::prelude::*; + /// # use sycamore::prelude::*; + /// # fn _test(ctx: ScopeRef) -> View { + /// let checked = ctx.create_signal(true); + /// h(input).attr("type", "checkbox").bind_checked(checked) + /// # .view(ctx) } + /// ``` + pub fn bind_checked( + self, + sub: &'a Signal, + ) -> ElementBuilder<'a, G, impl FnOnce(ScopeRef<'a>) -> G + 'a> { + self.map(move |ctx, el| { + ctx.create_effect({ + let el = el.clone(); + move || { + el.set_property("checked", &(*sub.get()).into()); + } + }); + el.event( + ctx, + "change", + Box::new(move |e: web_sys::Event| { + let val = Reflect::get( + &e.target().expect("missing target on change event"), + &"checked".into(), + ) + .expect("missing property `checked`") + .as_bool() + .expect("could not get property `checked` as a bool"); + sub.set(val); + }), + ); + }) + } +} + +/// Instantiate a component as a [`View`]. +/// +/// # Example +/// ``` +/// # use sycamore::builder::prelude::*; +/// # use sycamore::prelude::*; +/// #[component] +/// fn MyComponent(ctx: ScopeRef) -> View { +/// h(h1).t("I am a component").view(ctx) +/// } +/// +/// // Elsewhere... +/// # fn view(ctx: ScopeRef) -> View { +/// component(|| MyComponent(ctx, ())) +/// # } +/// ``` +pub fn component(f: impl FnOnce() -> View) -> View +where + G: GenericNode + Html, +{ + component_scope(f) +} + +/// Create a [`View`] from an array of [`View`]. +/// +/// # Example +/// ``` +/// # use sycamore::builder::prelude::*; +/// # use sycamore::prelude::*; +/// # fn _test(ctx: ScopeRef) -> View { +/// fragment([ +/// h(div).view(ctx), +/// h(div).view(ctx), +/// ]) +/// # } +/// ``` +pub fn fragment(parts: [View; N]) -> View +where + G: GenericNode, +{ + View::new_fragment(Vec::from_iter(parts.to_vec())) +} diff --git a/packages/sycamore/src/builder/agnostic/mod.rs b/packages/sycamore/src/builder/agnostic/mod.rs deleted file mode 100644 index 0fe4a3488..000000000 --- a/packages/sycamore/src/builder/agnostic/mod.rs +++ /dev/null @@ -1,667 +0,0 @@ -//! The renderer-agnostic API. - -use js_sys::Reflect; -use std::collections::HashMap; -use std::iter::FromIterator; -use wasm_bindgen::prelude::*; -use wasm_bindgen::JsCast; - -use crate::component::component_scope; -use crate::generic_node::{GenericNode, Html}; -use crate::noderef::NodeRef; -use crate::reactive::*; -use crate::utils::render; -use crate::view::View; - -pub mod prelude { - pub use super::component; - pub use super::fragment; - pub use super::node; -} - -/// Create [`NodeBuilder`] to create UI elements. -/// -/// # Example -/// ``` -/// # use sycamore::prelude::*; -/// # fn _test() -> View { -/// node("div").build() -/// # } -/// # fn _test2() -> View { -/// node("a").build() -/// # } -/// ``` -pub fn node<'a, G>(ctx: ScopeRef<'a>, tag: &'static str) -> NodeBuilder<'a, G> -where - G: GenericNode, -{ - NodeBuilder { - ctx, - element: G::element(tag), - } -} - -/// Instantiate a component as a [`View`]. -/// -/// # Example -/// ``` -/// use sycamore::prelude::*; -/// # use sycamore::builder::html::*; -/// #[component(MyComponent)] -/// fn my_component() -> View { -/// h1().text("I am a component").build() -/// } -/// -/// // Elsewhere in another component. -/// # fn view() -> View { -/// component::<_, MyComponent<_>>(()) -/// # } -/// ``` -pub fn component(f: impl FnOnce() -> View) -> View -where - G: GenericNode + Html, -{ - component_scope(f) -} - -/// Create a [`View`] from an array of [`View`]. -/// -/// # Example -/// ``` -/// # use sycamore::prelude::*; -/// # use sycamore::builder::html::*; -/// # fn _test() -> View { -/// fragment([ -/// div().build(), -/// div().build() -/// ]) -/// # } -/// ``` -pub fn fragment(parts: [View; N]) -> View -where - G: GenericNode, -{ - View::new_fragment(Vec::from_iter(parts.to_vec())) -} - -/// The main type powering the builder API. -#[derive(Clone)] -pub struct NodeBuilder<'a, G> -where - G: GenericNode, -{ - ctx: ScopeRef<'a>, - element: G, -} - -impl<'a, G> NodeBuilder<'a, G> -where - G: GenericNode, -{ - /// Add a child [`View`]. - /// - /// # Example - /// ``` - /// # use sycamore::prelude::*; - /// # use sycamore::builder::html::*; - /// # fn _test() -> View { - /// div() - /// .child(h1().text("I am a child").build()) - /// .build() - /// # } - /// ``` - pub fn child(&self, child: View) -> &Self { - render::insert(self.ctx, &self.element, child, None, None, true); - - self - } - - /// Add a dynamic child [`View`] - /// - /// # Example - /// ``` - /// # use sycamore::prelude::*; - /// # use sycamore::builder::html::*; - /// # fn _test() -> View { - /// let visible = Signal::new(true); - /// - /// div() - /// .dyn_child( - /// move || { - /// if *visible.get() { - /// h1().text("I am a child").build() - /// } else { View::empty() } - /// } - /// ) - /// .build() - /// # } - /// ``` - pub fn dyn_child(&self, child: impl FnMut() -> View + 'a) -> &Self { - #[allow(unused_imports)] - use std::any::{Any, TypeId}; - - #[cfg(feature = "ssr")] - if TypeId::of::() == TypeId::of::() { - // If Server Side Rendering, insert beginning tag for hydration purposes. - self.element.append_child(&G::marker_with_text("#")); - // Create end marker. This is needed to make sure that the node is inserted into the - // right place. - let end_marker = G::marker_with_text("/"); - self.element.append_child(&end_marker); - render::insert( - self.ctx, - &self.element, - View::new_dyn(self.ctx, child), - None, - Some(&end_marker), - true, /* We don't know if this is the only child or not so we pessimistically - * set this to true. */ - ); - return self; - } - #[cfg(feature = "experimental-hydrate")] - if TypeId::of::() == TypeId::of::() { - use crate::utils::hydrate::web::*; - // Get start and end markers. - let el = - ::downcast_ref::(&self.element).unwrap(); - let initial = get_next_marker(&el.inner_element()); - // Do not drop the HydrateNode because it will be cast into a GenericNode. - let initial = ::std::mem::ManuallyDrop::new(initial); - // SAFETY: This is safe because we already checked that the type is HydrateNode. - // __initial is wrapped inside ManuallyDrop to prevent double drop. - let initial = unsafe { ::std::ptr::read(&initial as *const _ as *const _) }; - render::insert( - self.ctx, - &self.element, - View::new_dyn(self.ctx, child), - initial, - None, - true, /* We don't know if this is the only child or not so we pessimistically - * set this to true. */ - ); - return self; - } - // G is neither SsrNode nor HydrateNode. Proceed normally. - let marker = G::marker(); - self.element.append_child(&marker); - render::insert( - self.ctx, - &self.element, - View::new_dyn(self.ctx, child), - None, - Some(&marker), - true, - ); - self - } - - /// Adds a text node. - /// - /// # Example - /// ``` - /// # use sycamore::prelude::*; - /// # use sycamore::builder::html::*; - /// # fn _test() -> View { - /// h1().text("I am text").build() - /// } - /// ``` - pub fn text(&self, text: impl AsRef) -> &Self { - self.element.append_child(&G::text_node(text.as_ref())); - - self - } - - /// Adds a dynamic text node. - /// - /// # Example - /// ``` - /// # use sycamore::prelude::*; - /// # use sycamore::builder::html::*; - /// # fn _test() -> View { - /// let required = Signal::new(false); - /// - /// h1() - /// .text("Email") - /// .dyn_text( - /// move || { - /// if *required.get() { " *" } else { "" } - /// } - /// ).build() - /// } - /// ``` - pub fn dyn_text(&self, text: F) -> &Self - where - F: FnMut() -> O + 'a, - O: AsRef + 'static, - { - let memo = self.ctx.create_memo(text); - - self.dyn_child(move || View::new_node(G::text_node(memo.get().as_ref().as_ref()))); - - self - } - - /// Renders a component as a child. - /// - /// # Example - /// ``` - /// use sycamore::prelude::*; - /// # use sycamore::builder::html::*; - /// #[component(MyComponent)] - /// fn my_component() -> View { - /// h1().text("My component").build() - /// } - /// - /// # fn _test() -> View { - /// div().component::>(()).build() - /// } - /// ``` - pub fn component(&self, f: impl FnOnce() -> View) -> &Self { - self.child(component_scope(f)); - - self - } - - /// Convenience function for adding an `id` to a node. - /// - /// # Example - /// ``` - /// # use sycamore::prelude::*; - /// # use sycamore::builder::html::*; - /// # fn _test() -> View { - /// button().id("my-button").build() - /// # } - /// ``` - pub fn id(&self, id: impl AsRef) -> &Self { - self.attr("id", id.as_ref()) - } - - /// Set an attribute on the node. - /// - /// # Example - /// ``` - /// # use sycamore::prelude::*; - /// # use sycamore::builder::html::*; - /// # fn _test() -> View { - /// button().attr("type", "submit").build() - /// # } - /// ``` - pub fn attr(&self, name: N, value: Va) -> &Self - where - N: AsRef, - Va: AsRef, - { - self.element.set_attribute(name.as_ref(), value.as_ref()); - - self - } - - /// Set a boolean attribute on the node. - /// - /// # Example - /// ``` - /// # use sycamore::prelude::*; - /// # use sycamore::builder::html::*; - /// # fn _test() -> View { - /// input().bool_attr("required", true).build() - /// # } - /// ``` - pub fn bool_attr(&self, name: N, value: bool) -> &Self - where - N: AsRef, - { - if value { - self.attr(name.as_ref(), ""); - } else { - self.element.remove_attribute(name.as_ref()); - } - - self - } - - /// Adds a dynamic attribute on the node. - /// - /// If `value` is `None`, the attribute will be completely removed - /// from the node. - /// - /// # Example - /// ``` - /// # use sycamore::prelude::*; - /// # use sycamore::builder::html::*; - /// # fn _test() -> View { - /// let input_type = Signal::new(Some("text")); - /// - /// input() - /// .dyn_attr("type", input_type.handle()) - /// .build() - /// } - /// ``` - pub fn dyn_attr(&self, name: N, value: ReadSignal>) -> &Self - where - N: ToString, - T: ToString + 'a, - { - let element = self.element.clone(); - - let name = name.to_string(); - - self.ctx.create_effect(move || { - let v = value.get(); - - if let Some(v) = &*v { - element.set_attribute(name.as_ref(), v.to_string().as_ref()); - } else { - element.remove_attribute(name.as_ref()); - } - }); - - self - } - - /// Adds a dynamic boolean attribute on the node. - /// - /// # Example - /// ``` - /// # use sycamore::prelude::*; - /// # use sycamore::builder::html::*; - /// # fn _test() -> View { - /// let required = Signal::new(true); - /// - /// input() - /// .dyn_bool_attr("required", required.handle()).build() - /// } - /// ``` - pub fn dyn_bool_attr(&self, name: N, value: ReadSignal) -> &Self - where - N: ToString, - { - let element = self.element.clone(); - - let name = name.to_string(); - - self.ctx.create_effect(move || { - let v = value.get(); - - if *v { - element.set_attribute(name.as_ref(), ""); - } else { - element.remove_attribute(name.as_ref()); - } - }); - - self - } - - /// Set a property on the node. - /// - /// # Example - /// ``` - /// # use sycamore::prelude::*; - /// # use sycamore::builder::html::*; - /// # fn _test() -> View { - /// input().prop("value", "I am the value set.").build() - /// # } - /// ``` - pub fn prop(&self, name: N, property: Va) -> &Self - where - N: AsRef, - Va: Into, - { - self.element.set_property(name.as_ref(), &property.into()); - - self - } - - /// Adds a dynamic property on the node. - /// - /// If `value` is `None`, the attribute will be completely removed - /// from the node. - /// - /// # Example - /// ``` - /// # use sycamore::prelude::*; - /// # use sycamore::builder::html::*; - /// # fn _test() -> View { - /// let checked = Signal::new(Some(false)); - /// - /// input() - /// .attr("type", "checkbox") - /// .dyn_prop("checked", checked.handle()) - /// .build() - /// } - /// ``` - pub fn dyn_prop(&self, name: N, value: &'a ReadSignal>) -> &Self - where - N: ToString, - T: ToString + 'a, - { - let element = self.element.clone(); - - let name = name.to_string(); - - self.ctx.create_effect(move || { - let v = value.get(); - - if let Some(v) = &*v { - element.set_attribute(name.as_ref(), v.to_string().as_ref()); - } else { - element.remove_attribute(name.as_ref()); - } - }); - - self - } - - /// Adds a class to the node. - /// - /// # Example - /// ``` - /// # use sycamore::prelude::*; - /// # use sycamore::builder::html::*; - /// # fn _test() -> View { - /// button().class("bg-green-500").text("My Button").build() - /// # } - /// ``` - pub fn class(&self, class: impl ToString) -> &Self { - self.element.add_class(class.to_string().as_ref()); - - self - } - - /// Adds a dynamic class on the node. - /// - /// If `value` is `None`, the attribute will be completely removed - /// from the node. - /// - /// # Example - /// ``` - /// # use sycamore::prelude::*; - /// # use sycamore::builder::html::*; - /// # fn _test() -> View { - /// let checked_class = Signal::new(false); - /// - /// input() - /// .attr("type", "checkbox") - /// .dyn_class("bg-red-500", checked_class.handle()) - /// .build() - /// } - /// ``` - pub fn dyn_class(&self, class: impl ToString, apply: ReadSignal) -> &Self { - let class = class.to_string(); - let element = self.element.clone(); - - self.ctx.create_effect(move || { - let apply = apply.get(); - - if *apply { - element.add_class(class.as_ref()); - } else { - element.remove_class(class.as_ref()); - } - }); - - self - } - - #[allow(dead_code)] - #[doc(hidden)] - fn styles(&self, styles: HashMap) -> &Self { - let styles = styles - .iter() - .map(|(k, v)| format!("{}: {}", k, v)) - .collect::>() - .join(";"); - - self.attr("style", styles); - - self - } - - /// Adds an event listener to the node. - /// - /// # Example - /// ``` - /// # use sycamore::prelude::*; - /// # use sycamore::builder::html::*; - /// # fn _test() -> View { - /// button() - /// .text("My Button") - /// .on( - /// "click", - /// |_| { web_sys::console::log_1(&"Clicked".into()) } - /// ) - /// .build() - /// # } - /// ``` - pub fn on(&self, event: E, handler: H) -> &Self - where - E: AsRef, - H: Fn(G::EventType) + 'a, - { - self.element - .event(self.ctx, event.as_ref(), Box::new(handler)); - - self - } - - /// Get a hold of the raw element by using a [`NodeRef`]. - /// - /// # Example - /// ``` - /// # use sycamore::prelude::*; - /// # use sycamore::builder::html::*; - /// # fn _test() -> View { - /// let node_ref = NodeRef::new(); - /// - /// input() - /// .bind_ref(node_ref.clone()) - /// .build() - /// # } - /// ``` - pub fn bind_ref(&self, node_ref: NodeRef) -> &Self { - node_ref.set(self.element.clone()); - - self - } - - /// Builds the [`NodeBuilder`] and returns a [`View`]. - /// - /// This is the function that should be called at the end of the node - /// building chain. - /// - /// # Example - /// ``` - /// # use sycamore::prelude::*; - /// # use sycamore::builder::html::*; - /// # fn _test() -> View { - /// input() - /// /* builder stuff */ - /// .build() - /// # } - /// ``` - pub fn build(&self) -> View { - View::new_node(self.element.to_owned()) - } -} - -impl<'a, G> NodeBuilder<'a, G> -where - G: GenericNode + Html, -{ - /// Binds `sub` to the `value` property of the node. - /// - /// `sub` will be automatically updated when the value is updated. - /// - /// # Example - /// ``` - /// # use sycamore::prelude::*; - /// # use sycamore::builder::html::*; - /// # fn _test() -> View { - /// let value = Signal::new(String::new()); - /// - /// input() - /// .bind_value(value) - /// .build() - /// # } - /// ``` - pub fn bind_value(&self, sub: &'a Signal) -> &Self { - let sub_handle = self.ctx.create_memo(|| Some((*sub.get()).clone())); - - self.dyn_prop("value", sub_handle); - - self.on("input", move |e| { - let value = Reflect::get( - &e.target() - .expect("Target missing on input event.") - .unchecked_into::(), - &"value".into(), - ) - .expect("Missing value prop.") - .as_string() - .expect("Value should be a string."); - - sub.set(value); - }) - } - - /// Binds `sub` to the `checked` property of the node. - /// - /// `sub` will be automatically updated when the value is updated. - /// - /// # Example - /// ``` - /// # use sycamore::prelude::*; - /// # use sycamore::builder::html::*; - /// # fn _test() -> View { - /// let checked = Signal::new(false); - /// - /// input() - /// .attr("type", "checkbox") - /// .bind_checked(checked) - /// .build() - /// # } - /// ``` - pub fn bind_checked(&self, sub: &'a Signal) -> &Self { - let sub_handle = self.ctx.create_memo(|| Some(*sub.get())); - - self.dyn_prop("checked", sub_handle); - - self.on("change", move |e| { - let value = Reflect::get( - &e.target().expect("Target missing on change event."), - &"checked".into(), - ) - .expect("Failed to get checked prop.") - .as_bool(); - - if let Some(value) = value { - sub.set(value); - } else { - panic!( - "Checked is only available on input elements with type attribute set to checkbox." - ); - } - }) - } -} diff --git a/packages/sycamore/src/builder/html.rs b/packages/sycamore/src/builder/html.rs deleted file mode 100644 index 61d097fdd..000000000 --- a/packages/sycamore/src/builder/html.rs +++ /dev/null @@ -1,168 +0,0 @@ -//! HTML-specific builder functions. - -use crate::prelude::*; - -macro_rules! generate_tag_functions { - ( - $vis:vis enum $name:ident { - $($tag:ident),* $(,)? - } - ) => { - paste::paste! { - $( - pub fn [<$tag:lower>]<'a, G: GenericNode>(ctx: ScopeRef<'a>) -> crate::builder::agnostic::NodeBuilder<'a, G> { - crate::builder::agnostic::node(ctx, stringify!([<$tag:lower>])) - } - )* - } - }; -} - -// Source https://developer.mozilla.org/en-US/docs/Web/HTML/Element#demarcating_edits -generate_tag_functions! { - enum HtmlTag { - // Main Root - Html, - - // Document markup - Base, - Head, - Link, - Meta, - Style, - Title, - - // Sectioning Root - Body, - - // Content sectioning - Address, - Article, - Aside, - Footer, - Header, - H1, - H2, - H3, - H4, - H5, - H6, - Main, - Nav, - Section, - - // Text content - Blockquote, - Dd, - Div, - Dl, - Dt, - Figcaption, - Figure, - Hr, - Li, - Ol, - P, - Pre, - Ul, - - // Inline text semantics - A, - Abbr, - B, - Bdi, - Bdo, - Br, - Cite, - Code, - Data, - Dfn, - Em, - I, - Kbd, - Mark, - Q, - Rp, - Rt, - Ruby, - S, - Samp, - Small, - Span, - Strong, - Sub, - Sup, - Time, - U, - Var, - Wbr, - - // Image and multimedia - Area, - Audio, - Img, - Map, - Track, - Video, - - // Embeded content - Embed, - Iframe, - Object, - Param, - Picture, - Portal, - Source, - - // SVG and MathML - Svg, - Math, - - // Scripting - Canvas, - Noscript, - Script, - - // Demarcating edits - Del, - Ins, - - // Table content - Caption, - Col, - Colgroup, - Table, - Tbody, - Td, - Tfoot, - Th, - Thead, - Tr, - - // Forms - Button, - Datalist, - Fieldset, - Form, - Input, - Label, - Legend, - Meter, - Optgroup, - Option, - Output, - Progress, - Select, - Textarea, - - // Interactive elements - Details, - Dialog, - Menu, - Summary, - - // Web components - Slot, - Template, - } -} diff --git a/packages/sycamore/src/builder/mod.rs b/packages/sycamore/src/builder/mod.rs deleted file mode 100644 index f8008203a..000000000 --- a/packages/sycamore/src/builder/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! The builder pattern API for creating UI elements. - -pub mod agnostic; -#[cfg(feature = "experimental-builder-html")] -pub mod html; diff --git a/packages/sycamore/src/generic_node/dom_node.rs b/packages/sycamore/src/generic_node/dom_node.rs index 3e5947ffe..f98c282bb 100644 --- a/packages/sycamore/src/generic_node/dom_node.rs +++ b/packages/sycamore/src/generic_node/dom_node.rs @@ -166,7 +166,10 @@ impl GenericNode for DomNode { } fn text_node_int(int: i32) -> Self { - let node = document().unchecked_into::().create_text_node_int(int).into(); + let node = document() + .unchecked_into::() + .create_text_node_int(int) + .into(); DomNode { id: Default::default(), node, diff --git a/packages/sycamore/src/generic_node/mod.rs b/packages/sycamore/src/generic_node/mod.rs index a81efae3d..1e0678577 100644 --- a/packages/sycamore/src/generic_node/mod.rs +++ b/packages/sycamore/src/generic_node/mod.rs @@ -22,6 +22,13 @@ pub use hydrate_dom::*; #[cfg(feature = "ssr")] pub use ssr_node::*; +/// Represents an element. For instance, this trait is implemented for all types in the +/// [`html`](crate::html) module. +pub trait SycamoreElement { + const TAG_NAME: &'static str; + const NAME_SPACE: Option<&'static str>; +} + /// Abstraction over a rendering backend. /// /// You would probably use this trait as a trait bound when you want to accept any rendering diff --git a/packages/sycamore/src/html/mod.rs b/packages/sycamore/src/html/mod.rs index 56a213d29..0bb951ccf 100644 --- a/packages/sycamore/src/html/mod.rs +++ b/packages/sycamore/src/html/mod.rs @@ -1,12 +1,8 @@ //! HTML tag definitions. //! -//! _Documentation sources: https://developer.mozilla.org/en-US/_ +//! _Documentation sources: _ -/// Represents an element. -pub trait SycamoreElement { - const TAG_NAME: &'static str; - const NAME_SPACE: Option<&'static str>; -} +use crate::generic_node::SycamoreElement; /// MBE for generating elements. macro_rules! define_elements { diff --git a/packages/sycamore/src/lib.rs b/packages/sycamore/src/lib.rs index 60e707ccf..9b3b864ad 100644 --- a/packages/sycamore/src/lib.rs +++ b/packages/sycamore/src/lib.rs @@ -8,16 +8,20 @@ //! ## Feature Flags //! - `dom` (_default_) - Enables rendering templates to DOM nodes. Only useful on //! `wasm32-unknown-unknown` target. +//! //! - `experimental-builder-agnostic` - Enables the agnostic backend builder API. -//! - `experimental-builder-html` - Enables the HTML specific backend builder API. Also enables -//! `experimental-builder-agnostic`. +//! //! - `experimental-hydrate` - Enables client-side hydration support. +//! //! - `suspense` - Enables wrappers around `wasm-bindgen-futures` to make it easier to extend a //! reactive scope into an `async` function. +//! //! - `ssr` - Enables rendering templates to static strings (useful for Server Side Rendering / //! Pre-rendering). +//! //! - `serde` - Enables serializing and deserializing `Signal`s and other wrapper types using //! `serde`. +//! //! - `wasm-bindgen-interning` (_default_) - Enables interning for `wasm-bindgen` strings. This //! improves performance at a slight cost in binary size. If you want to minimize the size of the //! result `.wasm` binary, you might want to disable this. diff --git a/packages/sycamore/src/view.rs b/packages/sycamore/src/view.rs index 146f6393c..fa287c997 100644 --- a/packages/sycamore/src/view.rs +++ b/packages/sycamore/src/view.rs @@ -58,7 +58,8 @@ impl View { ) -> Self { let signal = ctx.create_ref(RefCell::new(None::>>)); ctx.create_effect_scoped(move |ctx| { - // SAFETY: `f` takes the same parameter as the child ctx provided by `create_effect_scoped`. + // SAFETY: `f` takes the same parameter as the child ctx provided by + // `create_effect_scoped`. let view = f(unsafe { std::mem::transmute(ctx) }); if signal.borrow().is_some() { signal.borrow().as_ref().unwrap().set(view); @@ -223,8 +224,8 @@ impl IntoView for T { // Strings and string slices. specialize_as_ref_to_str!(&str, String, Rc, Rc, Cow<'_, str>); - // Numbers that are smaller than can be represented by an `i32` use fast-path by passing value directly to JS. - // Note that `u16` and `u32` cannot be represented by an `i32` + // Numbers that are smaller than can be represented by an `i32` use fast-path by passing + // value directly to JS. Note that `u16` and `u32` cannot be represented by an `i32` specialize_num!(i8, i16, i32, u8); // Number that are bigger than an `i32`. specialize_big_num!(i64, i128, isize, u16, u32, u64, u128, usize); diff --git a/website/src/content.rs b/website/src/content.rs index 9557ea567..6d9bd5525 100644 --- a/website/src/content.rs +++ b/website/src/content.rs @@ -33,7 +33,7 @@ pub fn OutlineView(ctx: ScopeRef, outline: Vec) -> View { let Outline { name, children } = item; let nested = children.iter().map(|x| { let name = x.name.clone(); - let href = format!("#{}", x.name.trim().to_lowercase().replace(" ", "-")); + let href = format!("#{}", x.name.trim().to_lowercase().replace(' ', "-")); view! { ctx, li { a( @@ -47,7 +47,7 @@ pub fn OutlineView(ctx: ScopeRef, outline: Vec) -> View { }).collect(); let nested = View::new_fragment(nested); - let href = format!("#{}", name.trim().to_lowercase().replace(" ", "-")); + let href = format!("#{}", name.trim().to_lowercase().replace(' ', "-")); view! { ctx, li {