feat: props spread

This commit is contained in:
Koji AGAWA 2023-08-11 11:59:52 +09:00
parent 1ab5a03aef
commit d573f5dfd5
14 changed files with 301 additions and 4 deletions

View file

@ -9,6 +9,7 @@ members = [
"packages/extension",
"packages/router",
"packages/html",
"packages/html-internal-macro",
"packages/hooks",
"packages/web",
"packages/ssr",
@ -60,6 +61,7 @@ dioxus-core-macro = { path = "packages/core-macro", version = "0.4.0" }
dioxus-router = { path = "packages/router", version = "0.4.1" }
dioxus-router-macro = { path = "packages/router-macro", version = "0.4.1" }
dioxus-html = { path = "packages/html", version = "0.4.0" }
dioxus-html-internal-macro = { path = "packages/html-internal-macro", version = "0.4.0" }
dioxus-hooks = { path = "packages/hooks", version = "0.4.0" }
dioxus-web = { path = "packages/web", version = "0.4.0" }
dioxus-ssr = { path = "packages/ssr", version = "0.4.0" }

View file

@ -49,6 +49,7 @@ impl Writer<'_> {
attributes,
children,
brace,
extra_attributes,
} = el;
/*

View file

@ -73,8 +73,8 @@ pub(crate) mod innerlude {
}
pub use crate::innerlude::{
fc_to_builder, vdom_is_rendering, AnyValue, Attribute, AttributeValue, BorrowedAttributeValue,
CapturedError, Component, DynamicNode, Element, ElementId, Event, Fragment, IntoDynNode,
fc_to_builder, vdom_is_rendering, AnyValue, Attribute, AttributeBox, AttributeValue, BorrowedAttributeValue,
CapturedError, Component, DynamicNode, Element, ElementId, Event, Fragment, HasAttributesBox, IntoDynNode,
LazyNodes, Mutation, Mutations, Properties, RenderReturn, Scope, ScopeId, ScopeState, Scoped,
TaskId, Template, TemplateAttribute, TemplateNode, VComponent, VNode, VPlaceholder, VText,
VirtualDom,

View file

@ -832,3 +832,34 @@ impl<'a, T: IntoAttributeValue<'a>> IntoAttributeValue<'a> for Option<T> {
}
}
}
pub struct AttributeBox<'a> {
/// The name of the attribute.
pub name: &'a str,
/// The value of the attribute
pub value: Box<dyn IntoAttributeValue<'a> + 'static>,
/// The namespace of the attribute.
///
/// Doesnt exist in the html spec. Used in Dioxus to denote “style” tags and other attribute groups.
pub namespace: Option<&'static str>,
/// An indication of we should always try and set the attribute. Used in controlled components to ensure changes are propagated
pub volatile: bool,
}
impl<'a> AttributeBox<'a> {
pub fn new(name: &'a str, value: impl IntoAttributeValue<'a> + 'static, namespace: Option<&'static str>, volatile: bool) -> Self {
Self {
name,
value: Box::new(value),
namespace,
volatile,
}
}
}
pub trait HasAttributesBox<'a, T> {
fn push_attribute(self, attr: AttributeBox<'a>) -> T;
}

View file

@ -0,0 +1,121 @@
use dioxus::core_macro::render;
use dioxus::prelude::rsx;
use dioxus_core::{AttributeBox, Element, HasAttributesBox, Scope};
use dioxus_html::{ExtendedAudioMarker, ExtendedGlobalAttributesMarker};
#[test]
fn props_spread() {
pub struct FooProps<'a> {
pub open: Option<&'a str>,
attributes: Vec<AttributeBox<'a>>,
}
// -----
impl<'a> FooProps<'a> {
#[doc = "\nCreate a builder for building `FooProps`.\nOn the builder, call `.open(...)`(optional) to set the values of the fields.\nFinally, call `.build()` to create the instance of `FooProps`.\n "]
#[allow(dead_code)]
pub fn builder() -> FooPropsBuilder<'a, ((), ), > {
FooPropsBuilder { fields: ((), ), attributes: Vec::new(), _phantom: core::default::Default::default() }
}
}
#[must_use]
#[doc(hidden)]
#[allow(dead_code, non_camel_case_types, non_snake_case)]
pub struct FooPropsBuilder<'a, TypedBuilderFields, > {
fields: TypedBuilderFields,
attributes: Vec<AttributeBox<'a>>,
_phantom: ( core::marker::PhantomData<&'a ()> ),
}
//impl<'a, TypedBuilderFields, > Clone for FooPropsBuilder<'a, TypedBuilderFields, > where TypedBuilderFields: Clone { fn clone(&self) -> Self { Self { fields: self.fields.clone(), attributes: self.attributes, _phantom: Default::default() } } }
impl<'a> dioxus::prelude::Properties for FooProps<'a> {
type Builder = FooPropsBuilder<'a, ((), ), >;
const IS_STATIC: bool = false;
fn builder() -> Self::Builder { FooProps::builder() }
unsafe fn memoize(&self, other: &Self) -> bool { false }
}
#[doc(hidden)]
#[allow(dead_code, non_camel_case_types, non_snake_case)]
pub trait FooPropsBuilder_Optional<T> { fn into_value<F: FnOnce() -> T>(self, default: F) -> T; }
impl<T> FooPropsBuilder_Optional<T> for () { fn into_value<F: FnOnce() -> T>(self, default: F) -> T { default() } }
impl<T> FooPropsBuilder_Optional<T> for (T, ) { fn into_value<F: FnOnce() -> T>(self, _: F) -> T { self.0 } }
#[allow(dead_code, non_camel_case_types, missing_docs)]
impl<'a> FooPropsBuilder<'a, ((), )> {
pub fn open(self, open: &'a str) -> FooPropsBuilder<'a, ((Option<&'a str>,
// pub attributes: Vec<Attribute<'a>>,
), )> {
let open = (Some(open), );
let (_, ) = self.fields;
FooPropsBuilder { fields: (open, ), attributes: self.attributes, _phantom: self._phantom }
}
}
#[doc(hidden)]
#[allow(dead_code, non_camel_case_types, non_snake_case)]
pub enum FooPropsBuilder_Error_Repeated_field_open {}
#[doc(hidden)]
#[allow(dead_code, non_camel_case_types, missing_docs)]
impl<'a> FooPropsBuilder<'a, ((Option<&'a str>,
// pub attributes: Vec<Attribute<'a>>,
), )> {
#[deprecated(note = "Repeated field open")]
pub fn open(self, _: FooPropsBuilder_Error_Repeated_field_open) -> FooPropsBuilder<'a, ((Option<&'a str>,
// pub attributes: Vec<Attribute<'a>>,
), )> { self }
}
#[allow(dead_code, non_camel_case_types, missing_docs)]
impl<'a, __open: FooPropsBuilder_Optional<Option<&'a str>>> FooPropsBuilder<'a, (__open, ), > {
pub fn build(self) -> FooProps<'a> {
let (open, ) = self.fields;
let open = FooPropsBuilder_Optional::into_value(open, || Default::default());
FooProps { open, attributes: self.attributes }
}
}
// -----
impl<'a, A> HasAttributesBox<'a, FooPropsBuilder<'a, (A, )>> for FooPropsBuilder<'a, (A, )> {
fn push_attribute(self, attr: AttributeBox<'a>) -> FooPropsBuilder<'a, (A, )> {
let mut attrs = Vec::from(self.attributes);
attrs.push(attr);
FooPropsBuilder { fields: self.fields, attributes: attrs, _phantom: self._phantom }
}
}
impl<A,> ExtendedGlobalAttributesMarker for FooPropsBuilder<'_, (A,)> {}
impl<A,> ExtendedAudioMarker for FooPropsBuilder<'_, (A,)> {}
use dioxus::prelude::*;
use dioxus_html::AudioExtension;
#[allow(non_snake_case)]
pub fn Foo<'a>(cx: Scope<'a, FooProps<'a>>) -> Element<'a> {
let muted = false;
let attributes = cx.props.attributes;
render! {
// rsx! {
// audio {
// muted: muted,
// }
// }
::dioxus::core::LazyNodes::new(move |__cx: &::dioxus::core::ScopeState| -> ::dioxus::core::VNode {
static TEMPLATE: ::dioxus::core::Template = ::dioxus::core::Template { name: concat!(file!(), ":", line!(), ":", column!(), ":", "" ), roots: &[::dioxus::core::TemplateNode::Element { tag: dioxus_elements::audio::TAG_NAME, namespace: dioxus_elements::audio::NAME_SPACE, attrs: &[::dioxus::core::TemplateAttribute::Dynamic { id: 0usize }], children: &[] }], node_paths: &[], attr_paths: &[&[0u8]] };
let mut attrs = vec![__cx.attr(dioxus_elements::audio::muted.0, muted, dioxus_elements::audio::muted.1, dioxus_elements::audio::muted.2)];
for attr in attributes {
attrs.push(__cx.attr(attr.name, attr.value.into_value(__cx.bump()), attr.namespace, attr.volatile));
};
::dioxus::core::VNode {
parent: None,
key: None,
template: std::cell::Cell::new(TEMPLATE),
root_ids: Default::default(),
dynamic_nodes: __cx.bump().alloc([]),
dynamic_attrs: __cx.bump().alloc(attrs),
}
})
}
}
rsx! {
Foo {
autoplay: true,
controls: true,
}
};
}

View file

@ -0,0 +1,28 @@
[package]
name = "dioxus-html-internal-macro"
version = { workspace = true }
edition = "2021"
repository = "https://github.com/DioxusLabs/dioxus/"
homepage = "https://dioxuslabs.com"
keywords = ["dom", "ui", "gui", "react", "liveview"]
license = "MIT OR Apache-2.0"
description = "HTML function macros for Dioxus"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
proc-macro2 = "1.0.66"
syn = { version = "2", features = ["full"] }
quote = "^1.0.26"
convert_case = "^0.6.0"
[lib]
proc-macro = true
[[test]]
name = "tests"
path = "tests/progress.rs"
[dev-dependencies]
trybuild = { version = "1.0.82", features = ["diff"] }

View file

@ -0,0 +1,83 @@
use proc_macro::TokenStream;
use convert_case::{Case, Casing};
use quote::{quote, TokenStreamExt, ToTokens};
use syn::{braced, Ident, parse_macro_input, Token};
use syn::__private::TokenStream2;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
#[proc_macro]
pub fn impl_extension_attributes(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as ImplExtensionAttributes);
input.to_token_stream().into()
}
struct ImplExtensionAttributes {
is_element: bool,
name: Ident,
attrs: Punctuated<Ident, Token![,]>,
}
impl Parse for ImplExtensionAttributes {
fn parse(input: ParseStream) -> syn::Result<Self> {
let content;
let element: Ident = input.parse()?;
let name = input.parse()?;
braced!(content in input);
let attrs = content.parse_terminated(Ident::parse, Token![,])?;
Ok(ImplExtensionAttributes {
is_element: element.to_string() == "ELEMENT",
name,
attrs,
})
}
}
impl ToTokens for ImplExtensionAttributes {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let name = &self.name;
let camel_name = name.to_string().to_case(Case::UpperCamel);
let impl_name = Ident::new(format!("{}Impl", &camel_name).as_str(), name.span());
let extension_name = Ident::new(format!("{}Extension", &camel_name).as_str(), name.span());
let marker_name = Ident::new(format!("Extended{}Marker", &camel_name).as_str(), name.span());
if !self.is_element {
tokens.append_all(quote! {
trait #impl_name {}
impl #name for #impl_name {}
});
}
let defs = self.attrs.iter().map(|ident| {
quote! {
fn #ident(self, value: impl IntoAttributeValue<'a> + 'static) -> Self;
}
});
let impls = self.attrs.iter().map(|ident| {
let d = if self.is_element {
quote! { #name::#ident }
} else {
quote! { #impl_name::#ident }
};
quote! {
fn #ident(self, value: impl IntoAttributeValue<'a> + 'static) -> Self {
let d = #d;
self.push_attribute(AttributeBox::new(d.0, value, d.1, d.2))
}
}
});
tokens.append_all(quote! {
pub trait #marker_name {}
pub trait #extension_name<'a> {
#(#defs)*
}
impl<'a, T> #extension_name<'a> for T where T: HasAttributesBox<'a, T> + #marker_name {
#(#impls)*
}
});
}
}

View file

@ -0,0 +1 @@
fn main() {}

View file

@ -0,0 +1,5 @@
#[test]
fn tests() {
let t = trybuild::TestCases::new();
t.pass("tests/01-simple.rs");
}

View file

@ -12,6 +12,7 @@ keywords = ["dom", "ui", "gui", "react"]
[dependencies]
dioxus-core = { workspace = true }
dioxus-rsx = { workspace = true, features = ["hot_reload"], optional = true }
dioxus-html-internal-macro = { workspace = true }
serde = { version = "1", features = ["derive"], optional = true }
serde_repr = { version = "0.1", optional = true }
wasm-bindgen = { workspace = true, optional = true }

View file

@ -1,9 +1,15 @@
#![allow(non_upper_case_globals)]
use dioxus_core::AttributeBox;
use dioxus_core::HasAttributesBox;
use dioxus_core::prelude::IntoAttributeValue;
use dioxus_html_internal_macro::impl_extension_attributes;
#[cfg(feature = "hot-reload-context")]
use dioxus_rsx::HotReloadingContext;
#[cfg(feature = "hot-reload-context")]
use crate::{map_global_attributes, map_svg_attributes};
use crate::{GlobalAttributes, SvgAttributes};
#[cfg(feature = "hot-reload-context")]
use dioxus_rsx::HotReloadingContext;
pub type AttributeDiscription = (&'static str, Option<&'static str>, bool);
@ -106,6 +112,8 @@ macro_rules! impl_element {
}
impl GlobalAttributes for $name {}
impl_extension_attributes![ELEMENT $name { $($fil,)* }];
};
(

View file

@ -1,5 +1,10 @@
#![allow(non_upper_case_globals)]
use dioxus_core::HasAttributesBox;
use dioxus_core::AttributeBox;
use dioxus_core::prelude::IntoAttributeValue;
use dioxus_html_internal_macro::impl_extension_attributes;
use crate::AttributeDiscription;
#[cfg(feature = "hot-reload-context")]
@ -62,6 +67,8 @@ macro_rules! trait_methods {
)*
None
}
impl_extension_attributes![GLOBAL $trait { $($name,)* }];
};
// Rename the incoming ident and apply a custom namespace

View file

@ -73,6 +73,7 @@ pub fn rsx_node_from_html(node: &Node) -> Option<BodyNode> {
attributes,
key: None,
brace: Default::default(),
extra_attributes: None,
}))
}

View file

@ -21,6 +21,7 @@ pub struct Element {
pub attributes: Vec<ElementAttrNamed>,
pub children: Vec<BodyNode>,
pub brace: syn::token::Brace,
pub extra_attributes: Option<Expr>,
}
impl Parse for Element {
@ -35,6 +36,7 @@ impl Parse for Element {
let mut children: Vec<BodyNode> = vec![];
let mut key = None;
let mut _el_ref = None;
let mut extra_attributes = None;
// parse fields with commas
// break when we don't get this pattern anymore
@ -42,6 +44,11 @@ impl Parse for Element {
// "def": 456,
// abc: 123,
loop {
if content.peek(Token![...]) {
content.parse::<Token![...]>()?;
extra_attributes = Some(content.parse::<Expr>()?);
}
// Parse the raw literal fields
if content.peek(LitStr) && content.peek2(Token![:]) && !content.peek3(Token![:]) {
let name = content.parse::<LitStr>()?;
@ -160,6 +167,7 @@ impl Parse for Element {
attributes,
children,
brace,
extra_attributes
})
}
}