Merge pull request #1349 from atty303/issue-1179

feat: props spread
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2024-01-08 09:48:25 -08:00
commit 4fc2802eab
35 files changed
+1035 -265

No files matched your search

+2
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", default-features = false, 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" }
-1
View File
@@ -139,7 +139,6 @@ Missing Features
Missing examples
- Shared state
- Root-less element groups
- Spread props
- Custom elements
- Component Children: Pass children into child components
- Render To string: Render a mounted virtualdom to a string
+36
View File
@@ -0,0 +1,36 @@
use dioxus::prelude::*;
fn main() {
let mut dom = VirtualDom::new(app);
let _ = dom.rebuild();
let html = dioxus_ssr::render(&dom);
println!("{}", html);
}
fn app(cx: Scope) -> Element {
render! {
Component {
width: "10px",
extra_data: "hello{1}",
extra_data2: "hello{2}",
height: "10px",
left: 1
}
}
}
#[component]
fn Component<'a>(cx: Scope<'a, Props<'a>>) -> Element<'a> {
render! {
audio { ..cx.props.attributes, "1: {cx.props.extra_data}\n2: {cx.props.extra_data2}" }
}
}
#[derive(Props)]
struct Props<'a> {
#[props(extends = GlobalAttributes)]
attributes: Vec<Attribute<'a>>,
extra_data: &'a str,
extra_data2: &'a str,
}
+17 -3
View File
@@ -166,7 +166,7 @@ impl Writer<'_> {
fn write_attributes(
&mut self,
attributes: &[ElementAttrNamed],
attributes: &[AttributeType],
key: &Option<IfmtInput>,
sameline: bool,
) -> Result {
@@ -188,7 +188,7 @@ impl Writer<'_> {
while let Some(attr) = attr_iter.next() {
self.out.indent_level += 1;
if !sameline {
self.write_comments(attr.attr.start())?;
self.write_comments(attr.start())?;
}
self.out.indent_level -= 1;
@@ -289,7 +289,14 @@ impl Writer<'_> {
Ok(())
}
fn write_attribute(&mut self, attr: &ElementAttrNamed) -> Result {
fn write_attribute(&mut self, attr: &AttributeType) -> Result {
match attr {
AttributeType::Named(attr) => self.write_named_attribute(attr),
AttributeType::Spread(attr) => self.write_spread_attribute(attr),
}
}
fn write_named_attribute(&mut self, attr: &ElementAttrNamed) -> Result {
self.write_attribute_name(&attr.attr.name)?;
write!(self.out, ": ")?;
self.write_attribute_value(&attr.attr.value)?;
@@ -297,6 +304,13 @@ impl Writer<'_> {
Ok(())
}
fn write_spread_attribute(&mut self, attr: &Expr) -> Result {
write!(self.out, "..")?;
write!(self.out, "{}", prettyplease::unparse_expr(attr))?;
Ok(())
}
// make sure the comments are actually relevant to this element.
// test by making sure this element is the primary element on this line
pub fn current_span_is_primary(&self, location: Span) -> bool {
+19 -11
View File
@@ -1,4 +1,4 @@
use dioxus_rsx::{BodyNode, ElementAttrNamed, ElementAttrValue, ForLoop};
use dioxus_rsx::{AttributeType, BodyNode, ElementAttrValue, ForLoop};
use proc_macro2::{LineColumn, Span};
use quote::ToTokens;
use std::{
@@ -165,12 +165,12 @@ impl<'a> Writer<'a> {
}
}
pub(crate) fn is_short_attrs(&mut self, attributes: &[ElementAttrNamed]) -> usize {
pub(crate) fn is_short_attrs(&mut self, attributes: &[AttributeType]) -> usize {
let mut total = 0;
for attr in attributes {
if self.current_span_is_primary(attr.attr.start()) {
'line: for line in self.src[..attr.attr.start().start().line - 1].iter().rev() {
if self.current_span_is_primary(attr.start()) {
'line: for line in self.src[..attr.start().start().line - 1].iter().rev() {
match (line.trim().starts_with("//"), line.is_empty()) {
(true, _) => return 100000,
(_, true) => continue 'line,
@@ -179,16 +179,24 @@ impl<'a> Writer<'a> {
}
}
total += match &attr.attr.name {
dioxus_rsx::ElementAttrName::BuiltIn(name) => {
let name = name.to_string();
name.len()
match attr {
AttributeType::Named(attr) => {
let name_len = match &attr.attr.name {
dioxus_rsx::ElementAttrName::BuiltIn(name) => {
let name = name.to_string();
name.len()
}
dioxus_rsx::ElementAttrName::Custom(name) => name.value().len() + 2,
};
total += name_len;
total += self.attr_value_len(&attr.attr.value);
}
AttributeType::Spread(expr) => {
let expr_len = self.retrieve_formatted_expr(expr).len();
total += expr_len + 3;
}
dioxus_rsx::ElementAttrName::Custom(name) => name.value().len() + 2,
};
total += self.attr_value_len(&attr.attr.value);
total += 6;
}
+2 -1
View File
@@ -15,10 +15,11 @@ proc-macro = true
[dependencies]
proc-macro2 = { version = "1.0" }
quote = "1.0"
syn = { version = "2.0", features = ["full", "extra-traits"] }
syn = { version = "2.0", features = ["full", "extra-traits", "visit"] }
dioxus-rsx = { workspace = true }
dioxus-core = { workspace = true }
constcat = "0.3.0"
convert_case = "^0.6.0"
prettyplease = "0.2.15"
# testing
+277 -24
View File
@@ -24,6 +24,10 @@ pub fn impl_my_derive(ast: &syn::DeriveInput) -> Result<TokenStream, Error> {
.included_fields()
.map(|f| struct_info.field_impl(f))
.collect::<Result<Vec<_>, _>>()?;
let extends = struct_info
.extend_fields()
.map(|f| struct_info.extends_impl(f))
.collect::<Result<Vec<_>, _>>()?;
let fields = quote!(#(#fields)*).into_iter();
let required_fields = struct_info
.included_fields()
@@ -36,6 +40,7 @@ pub fn impl_my_derive(ast: &syn::DeriveInput) -> Result<TokenStream, Error> {
#builder_creation
#conversion_helper
#( #fields )*
#( #extends )*
#( #required_fields )*
#build_method
}
@@ -167,8 +172,8 @@ mod field_info {
use proc_macro2::TokenStream;
use quote::quote;
use syn::spanned::Spanned;
use syn::Expr;
use syn::{parse::Error, punctuated::Punctuated};
use syn::{Expr, Path};
use super::util::{
expr_to_single_string, ident_to_type, path_to_single_string, strip_raw_ident_prefix,
@@ -199,6 +204,13 @@ mod field_info {
);
}
// extended field is automatically empty
if !builder_attr.extends.is_empty() {
builder_attr.default = Some(
syn::parse(quote!(::core::default::Default::default()).into()).unwrap(),
);
}
// auto detect optional
let strip_option_auto = builder_attr.strip_option
|| !builder_attr.ignore_option
@@ -253,6 +265,7 @@ mod field_info {
pub auto_into: bool,
pub strip_option: bool,
pub ignore_option: bool,
pub extends: Vec<Path>,
}
impl FieldBuilderAttr {
@@ -305,6 +318,17 @@ mod field_info {
let name = expr_to_single_string(&assign.left)
.ok_or_else(|| Error::new_spanned(&assign.left, "Expected identifier"))?;
match name.as_str() {
"extends" => {
if let syn::Expr::Path(path) = *assign.right {
self.extends.push(path.path);
Ok(())
} else {
Err(Error::new_spanned(
assign.right,
"Expected simple identifier",
))
}
}
"default" => {
self.default = Some(*assign.right);
Ok(())
@@ -359,6 +383,11 @@ mod field_info {
Ok(())
}
"extend" => {
self.extends.push(path.path);
Ok(())
}
_ => {
macro_rules! handle_fields {
( $( $flag:expr, $field:ident, $already:expr; )* ) => {
@@ -462,11 +491,14 @@ fn type_from_inside_option(ty: &syn::Type, check_option_name: bool) -> Option<&s
}
mod struct_info {
use convert_case::{Case, Casing};
use proc_macro2::TokenStream;
use quote::quote;
use syn::parse::Error;
use syn::punctuated::Punctuated;
use syn::Expr;
use syn::spanned::Spanned;
use syn::visit::Visit;
use syn::{parse_quote, Expr, Ident};
use super::field_info::{FieldBuilderAttr, FieldInfo};
use super::util::{
@@ -489,7 +521,46 @@ mod struct_info {
impl<'a> StructInfo<'a> {
pub fn included_fields(&self) -> impl Iterator<Item = &FieldInfo<'a>> {
self.fields.iter().filter(|f| !f.builder_attr.skip)
self.fields
.iter()
.filter(|f| !f.builder_attr.skip && f.builder_attr.extends.is_empty())
}
pub fn extend_fields(&self) -> impl Iterator<Item = &FieldInfo<'a>> {
self.fields
.iter()
.filter(|f| !f.builder_attr.extends.is_empty())
}
fn extend_lifetime(&self) -> syn::Result<Option<syn::Lifetime>> {
let first_extend = self.extend_fields().next();
match first_extend {
Some(f) => {
struct VisitFirstLifetime(Option<syn::Lifetime>);
impl Visit<'_> for VisitFirstLifetime {
fn visit_lifetime(&mut self, lifetime: &'_ syn::Lifetime) {
if self.0.is_none() {
self.0 = Some(lifetime.clone());
}
}
}
let name = f.name;
let mut visitor = VisitFirstLifetime(None);
visitor.visit_type(f.ty);
visitor.0.ok_or_else(|| {
syn::Error::new_spanned(
name,
"Unable to find lifetime for extended field. Please specify it manually",
)
}).map(Some)
}
None => Ok(None),
}
}
pub fn new(
@@ -536,7 +607,17 @@ mod struct_info {
// Therefore, we will generate code that shortcircuits the "comparison" in memoization
let are_there_generics = !self.generics.params.is_empty();
let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl();
let extend_lifetime = self.extend_lifetime()?;
let generics = self.generics.clone();
let (_, ty_generics, where_clause) = generics.split_for_impl();
let impl_generics = self.modify_generics(|g| {
if extend_lifetime.is_none() {
g.params.insert(0, parse_quote!('__bump));
}
});
let (impl_generics, _, _) = impl_generics.split_for_impl();
let (_, b_initial_generics, _) = self.generics.split_for_impl();
let all_fields_param = syn::GenericParam::Type(
syn::Ident::new("TypedBuilderFields", proc_macro2::Span::call_site()).into(),
);
@@ -544,7 +625,7 @@ mod struct_info {
g.params.insert(0, all_fields_param.clone());
});
let empties_tuple = type_tuple(self.included_fields().map(|_| empty_type()));
let generics_with_empty = modify_types_generics_hack(&ty_generics, |args| {
let generics_with_empty = modify_types_generics_hack(&b_initial_generics, |args| {
args.insert(0, syn::GenericArgument::Type(empties_tuple.clone().into()));
});
let phantom_generics = self.generics.params.iter().filter_map(|param| match param {
@@ -603,8 +684,7 @@ Finally, call `.build()` to create the instance of `{name}`.
quote!(#[doc(hidden)])
};
let (b_generics_impl, b_generics_ty, b_generics_where_extras_predicates) =
b_generics.split_for_impl();
let (_, _, b_generics_where_extras_predicates) = b_generics.split_for_impl();
let mut b_generics_where: syn::WhereClause = syn::parse2(quote! {
where TypedBuilderFields: Clone
})?;
@@ -624,12 +704,39 @@ Finally, call `.build()` to create the instance of `{name}`.
false => quote! { true },
};
let extend_fields = self.extend_fields().map(|f| {
let name = f.name;
let ty = f.ty;
quote!(#name: #ty)
});
let extend_fields_value = self.extend_fields().map(|f| {
let name = f.name;
quote!(#name: Vec::new())
});
let has_extend_fields = self.extend_fields().next().is_some();
let take_bump = if has_extend_fields {
quote!(bump: _cx.bump(),)
} else {
quote!()
};
let bump_field = if has_extend_fields {
quote!(bump: & #extend_lifetime ::dioxus::core::exports::bumpalo::Bump,)
} else {
quote!()
};
let extend_lifetime = extend_lifetime.unwrap_or(syn::Lifetime::new(
"'__bump",
proc_macro2::Span::call_site(),
));
Ok(quote! {
impl #impl_generics #name #ty_generics #where_clause {
#[doc = #builder_method_doc]
#[allow(dead_code, clippy::type_complexity)]
#vis fn builder() -> #builder_name #generics_with_empty {
#vis fn builder(_cx: & #extend_lifetime ::dioxus::prelude::ScopeState) -> #builder_name #generics_with_empty {
#builder_name {
#(#extend_fields_value,)*
#take_bump
fields: #empties_tuple,
_phantom: ::core::default::Default::default(),
}
@@ -640,26 +747,19 @@ Finally, call `.build()` to create the instance of `{name}`.
#builder_type_doc
#[allow(dead_code, non_camel_case_types, non_snake_case)]
#vis struct #builder_name #b_generics {
#(#extend_fields,)*
#bump_field
fields: #all_fields_param,
_phantom: (#( #phantom_generics ),*),
}
impl #b_generics_impl Clone for #builder_name #b_generics_ty #b_generics_where {
fn clone(&self) -> Self {
Self {
fields: self.fields.clone(),
_phantom: ::core::default::Default::default(),
}
}
}
impl #impl_generics ::dioxus::prelude::Properties for #name #ty_generics
impl #impl_generics ::dioxus::prelude::Properties<#extend_lifetime> for #name #ty_generics
#b_generics_where_extras_predicates
{
type Builder = #builder_name #generics_with_empty;
const IS_STATIC: bool = #is_static;
fn builder() -> Self::Builder {
#name::builder()
fn builder(_cx: &#extend_lifetime ::dioxus::prelude::ScopeState) -> Self::Builder {
#name::builder(_cx)
}
unsafe fn memoize(&self, other: &Self) -> bool {
#can_memoize
@@ -694,11 +794,143 @@ Finally, call `.build()` to create the instance of `{name}`.
})
}
pub fn extends_impl(&self, field: &FieldInfo) -> Result<TokenStream, Error> {
let StructInfo {
ref builder_name, ..
} = *self;
let field_name = field.name;
let descructuring = self.included_fields().map(|f| {
if f.ordinal == field.ordinal {
quote!(_)
} else {
let name = f.name;
quote!(#name)
}
});
let reconstructing = self.included_fields().map(|f| f.name);
// Add the bump lifetime to the generics
let mut ty_generics: Vec<syn::GenericArgument> = self
.generics
.params
.iter()
.map(|generic_param| match generic_param {
syn::GenericParam::Type(type_param) => {
let ident = type_param.ident.clone();
syn::parse(quote!(#ident).into()).unwrap()
}
syn::GenericParam::Lifetime(lifetime_def) => {
syn::GenericArgument::Lifetime(lifetime_def.lifetime.clone())
}
syn::GenericParam::Const(const_param) => {
let ident = const_param.ident.clone();
syn::parse(quote!(#ident).into()).unwrap()
}
})
.collect();
let mut target_generics_tuple = empty_type_tuple();
let mut ty_generics_tuple = empty_type_tuple();
let generics = self.modify_generics(|g| {
let index_after_lifetime_in_generics = g
.params
.iter()
.filter(|arg| matches!(arg, syn::GenericParam::Lifetime(_)))
.count();
for f in self.included_fields() {
if f.ordinal == field.ordinal {
ty_generics_tuple.elems.push_value(empty_type());
target_generics_tuple
.elems
.push_value(f.tuplized_type_ty_param());
} else {
g.params
.insert(index_after_lifetime_in_generics, f.generic_ty_param());
let generic_argument: syn::Type = f.type_ident();
ty_generics_tuple.elems.push_value(generic_argument.clone());
target_generics_tuple.elems.push_value(generic_argument);
}
ty_generics_tuple.elems.push_punct(Default::default());
target_generics_tuple.elems.push_punct(Default::default());
}
});
let mut target_generics = ty_generics.clone();
let index_after_lifetime_in_generics = target_generics
.iter()
.filter(|arg| matches!(arg, syn::GenericArgument::Lifetime(_)))
.count();
target_generics.insert(
index_after_lifetime_in_generics,
syn::GenericArgument::Type(target_generics_tuple.into()),
);
ty_generics.insert(
index_after_lifetime_in_generics,
syn::GenericArgument::Type(ty_generics_tuple.into()),
);
let (impl_generics, _, where_clause) = generics.split_for_impl();
let forward_extended_fields = self.extend_fields().map(|f| {
let name = f.name;
quote!(#name: self.#name)
});
let extend_lifetime = self.extend_lifetime()?.ok_or(Error::new_spanned(
field_name,
"Unable to find lifetime for extended field. Please specify it manually",
))?;
let extends_impl = field.builder_attr.extends.iter().map(|path| {
let name_str = path_to_single_string(path).unwrap();
let camel_name = name_str.to_case(Case::UpperCamel);
let marker_name = Ident::new(
format!("{}Extension", &camel_name).as_str(),
path.span(),
);
quote! {
#[allow(dead_code, non_camel_case_types, missing_docs)]
impl #impl_generics dioxus_elements::extensions::#marker_name < #extend_lifetime > for #builder_name < #( #ty_generics ),* > #where_clause {}
}
});
Ok(quote! {
#[allow(dead_code, non_camel_case_types, missing_docs)]
impl #impl_generics ::dioxus::prelude::HasAttributes<#extend_lifetime> for #builder_name < #( #ty_generics ),* > #where_clause {
fn push_attribute(
mut self,
name: &#extend_lifetime str,
ns: Option<&'static str>,
attr: impl ::dioxus::prelude::IntoAttributeValue<#extend_lifetime>,
volatile: bool
) -> Self {
let ( #(#descructuring,)* ) = self.fields;
self.#field_name.push(
::dioxus::core::Attribute::new(
name,
{
use ::dioxus::prelude::IntoAttributeValue;
attr.into_value(self.bump)
},
ns,
volatile,
)
);
#builder_name {
#(#forward_extended_fields,)*
bump: self.bump,
fields: ( #(#reconstructing,)* ),
_phantom: self._phantom,
}
}
}
#(#extends_impl)*
})
}
pub fn field_impl(&self, field: &FieldInfo) -> Result<TokenStream, Error> {
let FieldInfo {
name: field_name,
ty: field_type,
..
name: field_name, ..
} = field;
if *field_name == "key" {
return Err(Error::new_spanned(field_name, "Naming a prop `key` is not allowed because the name can conflict with the built in key attribute. See https://dioxuslabs.com/learn/0.4/reference/dynamic_rendering#rendering-lists for more information about keys"));
@@ -717,6 +949,12 @@ Finally, call `.build()` to create the instance of `{name}`.
});
let reconstructing = self.included_fields().map(|f| f.name);
let FieldInfo {
name: field_name,
ty: field_type,
..
} = field;
// Add the bump lifetime to the generics
let mut ty_generics: Vec<syn::GenericArgument> = self
.generics
.params
@@ -800,6 +1038,16 @@ Finally, call `.build()` to create the instance of `{name}`.
);
let repeated_fields_error_message = format!("Repeated field {field_name}");
let forward_extended_fields = self.extend_fields().map(|f| {
let name = f.name;
quote!(#name: self.#name)
});
let forward_bump = if self.extend_fields().next().is_some() {
quote!(bump: self.bump,)
} else {
quote!()
};
Ok(quote! {
#[allow(dead_code, non_camel_case_types, missing_docs)]
impl #impl_generics #builder_name < #( #ty_generics ),* > #where_clause {
@@ -809,6 +1057,8 @@ Finally, call `.build()` to create the instance of `{name}`.
let #field_name = (#arg_expr,);
let ( #(#descructuring,)* ) = self.fields;
#builder_name {
#(#forward_extended_fields,)*
#forward_bump
fields: ( #(#reconstructing,)* ),
_phantom: self._phantom,
}
@@ -842,6 +1092,7 @@ Finally, call `.build()` to create the instance of `{name}`.
name: ref field_name,
..
} = field;
// Add a bump lifetime to the generics
let mut builder_generics: Vec<syn::GenericArgument> = self
.generics
.params
@@ -1009,7 +1260,9 @@ Finally, call `.build()` to create the instance of `{name}`.
// reordering based on that, but for now this much simpler thing is a reasonable approach.
let assignments = self.fields.iter().map(|field| {
let name = &field.name;
if let Some(ref default) = field.builder_attr.default {
if !field.builder_attr.extends.is_empty() {
quote!(let #name = self.#name;)
} else if let Some(ref default) = field.builder_attr.default {
if field.builder_attr.skip {
quote!(let #name = #default;)
} else {
+24 -7
View File
@@ -1,6 +1,7 @@
use crate::any_props::AnyProps;
use crate::innerlude::{
BorrowedAttributeValue, ElementPath, ElementRef, VComponent, VPlaceholder, VText,
AttributeType, BorrowedAttributeValue, ElementPath, ElementRef, MountedAttribute, VComponent,
VPlaceholder, VText,
};
use crate::mutations::Mutation;
use crate::mutations::Mutation::*;
@@ -314,7 +315,7 @@ impl<'b> VirtualDom {
let id = self.assign_static_node_as_dynamic(path, root);
loop {
self.write_attribute(node, attr_id, &node.dynamic_attrs[attr_id], id);
self.write_attribute_type(node, &node.dynamic_attrs[attr_id], attr_id, id);
// Only push the dynamic attributes forward if they match the current path (same element)
match attrs.next_if(|(_, p)| *p == path) {
@@ -325,25 +326,41 @@ impl<'b> VirtualDom {
}
}
fn write_attribute(
fn write_attribute_type(
&mut self,
template: &'b VNode<'b>,
vnode: &'b VNode<'b>,
attribute: &'b MountedAttribute<'b>,
idx: usize,
attribute: &'b crate::Attribute<'b>,
id: ElementId,
) {
// Make sure we set the attribute's associated id
attribute.mounted_element.set(id);
match &attribute.ty {
AttributeType::Single(attribute) => self.write_attribute(vnode, attribute, idx, id),
AttributeType::Many(attribute) => {
for attribute in *attribute {
self.write_attribute(vnode, attribute, idx, id);
}
}
}
}
pub(crate) fn write_attribute(
&mut self,
vnode: &'b VNode<'b>,
attribute: &'b crate::Attribute<'b>,
idx: usize,
id: ElementId,
) {
// Safety: we promise not to re-alias this text later on after committing it to the mutation
let unbounded_name: &str = unsafe { std::mem::transmute(attribute.name) };
match &attribute.value {
AttributeValue::Listener(_) => {
let path = &template.template.get().attr_paths[idx];
let path = &vnode.template.get().attr_paths[idx];
let element_ref = ElementRef {
path: ElementPath { path },
template: template.stable_id().unwrap(),
template: vnode.stable_id().unwrap(),
scope: self.runtime.current_scope_id().unwrap_or(ScopeId(0)),
};
self.elements[id.0] = Some(element_ref);
+79 -10
View File
@@ -2,8 +2,8 @@ use crate::{
any_props::AnyProps,
arena::ElementId,
innerlude::{
BorrowedAttributeValue, DirtyScope, ElementPath, ElementRef, VComponent, VPlaceholder,
VText,
AttributeType, BorrowedAttributeValue, DirtyScope, ElementPath, ElementRef, VComponent,
VPlaceholder, VText,
},
mutations::Mutation,
nodes::RenderReturn,
@@ -126,14 +126,54 @@ impl<'b> VirtualDom {
.dynamic_attrs
.iter()
.zip(right_template.dynamic_attrs.iter())
.for_each(|(left_attr, right_attr)| {
.enumerate()
.for_each(|(idx, (left_attr, right_attr))| {
// Move over the ID from the old to the new
let mounted_element = left_attr.mounted_element.get();
right_attr.mounted_element.set(mounted_element);
let mounted_id = left_attr.mounted_element.get();
right_attr.mounted_element.set(mounted_id);
// If the attributes are different (or volatile), we need to update them
if left_attr.value != right_attr.value || left_attr.volatile {
self.update_attribute(right_attr, left_attr);
match (&left_attr.ty, &right_attr.ty) {
(AttributeType::Single(left), AttributeType::Single(right)) => {
self.diff_attribute(left, right, mounted_id)
}
(AttributeType::Many(left), AttributeType::Many(right)) => {
let mut left_iter = left.iter().peekable();
let mut right_iter = right.iter().peekable();
loop {
match (left_iter.peek(), right_iter.peek()) {
(Some(left), Some(right)) => {
// check which name is greater
match left.name.cmp(right.name) {
std::cmp::Ordering::Less => self.remove_attribute(
left.name,
left.namespace,
mounted_id,
),
std::cmp::Ordering::Greater => self.write_attribute(
right_template,
right,
idx,
mounted_id,
),
std::cmp::Ordering::Equal => {
self.diff_attribute(left, right, mounted_id)
}
}
}
(Some(_), None) => {
let left = left_iter.next().unwrap();
self.remove_attribute(left.name, left.namespace, mounted_id)
}
(None, Some(_)) => {
let right = right_iter.next().unwrap();
self.write_attribute(right_template, right, idx, mounted_id)
}
(None, None) => break,
}
}
}
_ => unreachable!("The macro should never generate this case"),
}
});
@@ -164,6 +204,18 @@ impl<'b> VirtualDom {
}
}
fn diff_attribute(
&mut self,
left_attr: &'b Attribute<'b>,
right_attr: &'b Attribute<'b>,
id: ElementId,
) {
// If the attributes are different (or volatile), we need to update them
if left_attr.value != right_attr.value || left_attr.volatile {
self.update_attribute(right_attr, left_attr, id);
}
}
fn diff_dynamic_node(
&mut self,
left_node: &'b DynamicNode<'b>,
@@ -184,12 +236,29 @@ impl<'b> VirtualDom {
};
}
fn update_attribute(&mut self, right_attr: &'b Attribute<'b>, left_attr: &'b Attribute) {
fn remove_attribute(&mut self, name: &'b str, ns: Option<&'static str>, id: ElementId) {
let name = unsafe { std::mem::transmute(name) };
let value: BorrowedAttributeValue<'b> = BorrowedAttributeValue::None;
let value = unsafe { std::mem::transmute(value) };
self.mutations.push(Mutation::SetAttribute {
id,
ns,
name,
value,
});
}
fn update_attribute(
&mut self,
right_attr: &'b Attribute<'b>,
left_attr: &'b Attribute<'b>,
id: ElementId,
) {
let name = unsafe { std::mem::transmute(left_attr.name) };
let value: BorrowedAttributeValue<'b> = (&right_attr.value).into();
let value = unsafe { std::mem::transmute(value) };
self.mutations.push(Mutation::SetAttribute {
id: left_attr.mounted_element.get(),
id,
ns: right_attr.namespace,
name,
value,
+2 -2
View File
@@ -334,10 +334,10 @@ where
}
}
}
impl<'a> Properties for ErrorBoundaryProps<'a> {
impl<'a> Properties<'a> for ErrorBoundaryProps<'a> {
type Builder = ErrorBoundaryPropsBuilder<'a, ((), ())>;
const IS_STATIC: bool = false;
fn builder() -> Self::Builder {
fn builder(_: &'a ScopeState) -> Self::Builder {
ErrorBoundaryProps::builder()
}
unsafe fn memoize(&self, _: &Self) -> bool {
+2 -2
View File
@@ -92,10 +92,10 @@ impl<'a, const A: bool> FragmentBuilder<'a, A> {
/// })
/// }
/// ```
impl<'a> Properties for FragmentProps<'a> {
impl<'a> Properties<'_> for FragmentProps<'a> {
type Builder = FragmentBuilder<'a, false>;
const IS_STATIC: bool = false;
fn builder() -> Self::Builder {
fn builder(_cx: &ScopeState) -> Self::Builder {
FragmentBuilder(None)
}
unsafe fn memoize(&self, _other: &Self) -> bool {
+9 -9
View File
@@ -76,11 +76,11 @@ 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,
LazyNodes, Mutation, Mutations, Properties, RenderReturn, Scope, ScopeId, ScopeState, Scoped,
TaskId, Template, TemplateAttribute, TemplateNode, VComponent, VNode, VPlaceholder, VText,
VirtualDom,
fc_to_builder, vdom_is_rendering, AnyValue, Attribute, AttributeType, AttributeValue,
BorrowedAttributeValue, CapturedError, Component, DynamicNode, Element, ElementId, Event,
Fragment, HasAttributes, IntoDynNode, LazyNodes, MountedAttribute, Mutation, Mutations,
Properties, RenderReturn, Scope, ScopeId, ScopeState, Scoped, TaskId, Template,
TemplateAttribute, TemplateNode, VComponent, VNode, VPlaceholder, VText, VirtualDom,
};
/// The purpose of this module is to alleviate imports of many common types
@@ -91,10 +91,10 @@ pub mod prelude {
consume_context, consume_context_from_scope, current_scope_id, fc_to_builder, has_context,
provide_context, provide_context_to_scope, provide_root_context, push_future,
remove_future, schedule_update_any, spawn, spawn_forever, suspend, use_error_boundary,
AnyValue, Component, Element, ErrorBoundary, Event, EventHandler, Fragment,
IntoAttributeValue, IntoDynNode, LazyNodes, Properties, Runtime, RuntimeGuard, Scope,
ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, Throw,
VNode, VirtualDom,
AnyValue, Attribute, AttributeType, Component, Element, ErrorBoundary, Event, EventHandler,
Fragment, HasAttributes, IntoAttributeValue, IntoDynNode, LazyNodes, MountedAttribute,
Properties, Runtime, RuntimeGuard, Scope, ScopeId, ScopeState, Scoped, TaskId, Template,
TemplateAttribute, TemplateNode, Throw, VNode, VirtualDom,
};
}
+90 -9
View File
@@ -64,7 +64,7 @@ pub struct VNode<'a> {
pub dynamic_nodes: &'a [DynamicNode<'a>],
/// The dynamic parts of the template
pub dynamic_attrs: &'a [Attribute<'a>],
pub dynamic_attrs: &'a [MountedAttribute<'a>],
}
impl<'a> VNode<'a> {
@@ -92,7 +92,7 @@ impl<'a> VNode<'a> {
template: Template<'static>,
root_ids: bumpalo::collections::Vec<'a, ElementId>,
dynamic_nodes: &'a [DynamicNode<'a>],
dynamic_attrs: &'a [Attribute<'a>],
dynamic_attrs: &'a [MountedAttribute<'a>],
) -> Self {
Self {
key,
@@ -445,6 +445,51 @@ pub enum TemplateAttribute<'a> {
},
}
/// An attribute with information about its position in the DOM and the element it was mounted to
#[derive(Debug)]
pub struct MountedAttribute<'a> {
pub(crate) ty: AttributeType<'a>,
/// The element in the DOM that this attribute belongs to
pub(crate) mounted_element: Cell<ElementId>,
}
impl<'a> From<Attribute<'a>> for MountedAttribute<'a> {
fn from(attr: Attribute<'a>) -> Self {
Self {
ty: AttributeType::Single(attr),
mounted_element: Default::default(),
}
}
}
impl<'a> From<&'a [Attribute<'a>]> for MountedAttribute<'a> {
fn from(attr: &'a [Attribute<'a>]) -> Self {
Self {
ty: AttributeType::Many(attr),
mounted_element: Default::default(),
}
}
}
impl<'a> From<&'a Vec<Attribute<'a>>> for MountedAttribute<'a> {
fn from(attr: &'a Vec<Attribute<'a>>) -> Self {
attr.as_slice().into()
}
}
impl<'a> MountedAttribute<'a> {
/// Get the type of this attribute
pub fn attribute_type(&self) -> &AttributeType<'a> {
&self.ty
}
/// Get the element that this attribute is mounted to
pub fn mounted_element(&self) -> ElementId {
self.mounted_element.get()
}
}
/// An attribute on a DOM node, such as `id="my-thing"` or `href="https://example.com"`
#[derive(Debug)]
pub struct Attribute<'a> {
@@ -461,9 +506,6 @@ pub struct Attribute<'a> {
/// An indication of we should always try and set the attribute. Used in controlled components to ensure changes are propagated
pub volatile: bool,
/// The element in the DOM that this attribute belongs to
pub(crate) mounted_element: Cell<ElementId>,
}
impl<'a> Attribute<'a> {
@@ -479,13 +521,40 @@ impl<'a> Attribute<'a> {
value,
namespace,
volatile,
mounted_element: Cell::new(ElementId::default()),
}
}
}
/// The type of an attribute
#[derive(Debug)]
pub enum AttributeType<'a> {
/// A single attribute
Single(Attribute<'a>),
/// Many different attributes sorted by name
Many(&'a [Attribute<'a>]),
}
impl<'a> AttributeType<'a> {
/// Call the given function on each attribute
pub fn for_each<'b, F>(&'b self, mut f: F)
where
F: FnMut(&'b Attribute<'a>),
{
match self {
Self::Single(attr) => f(attr),
Self::Many(attrs) => attrs.iter().for_each(f),
}
}
/// Get the element that this attribute is mounted to
pub fn mounted_element(&self) -> ElementId {
self.mounted_element.get()
/// Try to call the given function on each attribute
pub fn try_for_each<'b, F, E>(&'b self, mut f: F) -> Result<(), E>
where
F: FnMut(&'b Attribute<'a>) -> Result<(), E>,
{
match self {
Self::Single(attr) => f(attr),
Self::Many(attrs) => attrs.iter().try_for_each(f),
}
}
}
@@ -870,3 +939,15 @@ impl<'a, T: IntoAttributeValue<'a>> IntoAttributeValue<'a> for Option<T> {
}
}
}
/// A trait for anything that has a dynamic list of attributes
pub trait HasAttributes<'a> {
/// Push an attribute onto the list of attributes
fn push_attribute(
self,
name: &'a str,
ns: Option<&'static str>,
attr: impl IntoAttributeValue<'a>,
volatile: bool,
) -> Self;
}
+9 -6
View File
@@ -32,7 +32,7 @@ use crate::innerlude::*;
/// data: &'a str
/// }
/// ```
pub trait Properties: Sized {
pub trait Properties<'a>: Sized {
/// The type of the builder for this component.
/// Used to create "in-progress" versions of the props.
type Builder;
@@ -41,7 +41,7 @@ pub trait Properties: Sized {
const IS_STATIC: bool;
/// Create a builder for this component.
fn builder() -> Self::Builder;
fn builder(cx: &'a ScopeState) -> Self::Builder;
/// Memoization can only happen if the props are valid for the 'static lifetime
///
@@ -51,10 +51,10 @@ pub trait Properties: Sized {
unsafe fn memoize(&self, other: &Self) -> bool;
}
impl Properties for () {
impl Properties<'_> for () {
type Builder = EmptyBuilder;
const IS_STATIC: bool = true;
fn builder() -> Self::Builder {
fn builder(_cx: &ScopeState) -> Self::Builder {
EmptyBuilder {}
}
unsafe fn memoize(&self, _other: &Self) -> bool {
@@ -70,8 +70,11 @@ impl EmptyBuilder {
/// This utility function launches the builder method so rsx! and html! macros can use the typed-builder pattern
/// to initialize a component's props.
pub fn fc_to_builder<'a, T: Properties + 'a>(_: fn(Scope<'a, T>) -> Element<'a>) -> T::Builder {
T::builder()
pub fn fc_to_builder<'a, T: Properties<'a> + 'a>(
cx: &'a ScopeState,
_: fn(Scope<'a, T>) -> Element<'a>,
) -> T::Builder {
T::builder(cx)
}
#[cfg(not(miri))]
+26 -21
View File
@@ -7,7 +7,8 @@ use crate::{
nodes::{IntoAttributeValue, IntoDynNode, RenderReturn},
runtime::Runtime,
scope_context::ScopeContext,
AnyValue, Attribute, AttributeValue, Element, Event, Properties, TaskId,
AnyValue, Attribute, AttributeType, AttributeValue, Element, Event, MountedAttribute,
Properties, TaskId,
};
use bumpalo::{boxed::Box as BumpBox, Bump};
use std::{
@@ -350,20 +351,22 @@ impl<'src> ScopeState {
let mut listeners = self.attributes_to_drop_before_render.borrow_mut();
for attr in element.dynamic_attrs {
match attr.value {
// We need to drop listeners before the next render because they may borrow data from the borrowed props which will be dropped
AttributeValue::Listener(_) => {
let unbounded = unsafe { std::mem::transmute(attr as *const Attribute) };
listeners.push(unbounded);
}
// We need to drop any values manually to make sure that their drop implementation is called before the next render
AttributeValue::Any(_) => {
let unbounded = unsafe { std::mem::transmute(attr as *const Attribute) };
self.previous_frame().add_attribute_to_drop(unbounded);
}
attr.ty.for_each(|attr| {
match attr.value {
// We need to drop listeners before the next render because they may borrow data from the borrowed props which will be dropped
AttributeValue::Listener(_) => {
let unbounded = unsafe { std::mem::transmute(attr as *const Attribute) };
listeners.push(unbounded);
}
// We need to drop any values manually to make sure that their drop implementation is called before the next render
AttributeValue::Any(_) => {
let unbounded = unsafe { std::mem::transmute(attr as *const Attribute) };
self.previous_frame().add_attribute_to_drop(unbounded);
}
_ => (),
}
_ => (),
}
})
}
let mut props = self.borrowed_props.borrow_mut();
@@ -419,13 +422,15 @@ impl<'src> ScopeState {
value: impl IntoAttributeValue<'src>,
namespace: Option<&'static str>,
volatile: bool,
) -> Attribute<'src> {
Attribute {
name,
namespace,
volatile,
) -> MountedAttribute<'src> {
MountedAttribute {
ty: AttributeType::Single(Attribute {
name,
namespace,
volatile,
value: value.into_value(self.bump()),
}),
mounted_element: Default::default(),
value: value.into_value(self.bump()),
}
}
@@ -451,7 +456,7 @@ impl<'src> ScopeState {
) -> DynamicNode<'src>
where
// The properties must be valid until the next bump frame
P: Properties + 'src,
P: Properties<'src> + 'src,
// The current bump allocator frame must outlive the child's borrowed props
'src: 'child,
{
+42 -31
View File
@@ -4,8 +4,8 @@
use crate::{
any_props::VProps,
arena::ElementId,
innerlude::{DirtyScope, ElementRef, ErrorBoundary, Mutations, Scheduler, SchedulerMsg},
arena::{ElementId, ElementRef},
innerlude::{DirtyScope, ErrorBoundary, Mutations, Scheduler, SchedulerMsg},
mutations::Mutation,
nodes::RenderReturn,
nodes::{Template, TemplateId},
@@ -371,7 +371,6 @@ impl VirtualDom {
.get(parent_path.template.0)
.cloned()
.map(|el| (*parent_path, el));
let mut listeners = vec![];
// We will clone this later. The data itself is wrapped in RC to be used in callbacks if required
let uievent = Event {
@@ -383,6 +382,8 @@ impl VirtualDom {
if bubbles {
// Loop through each dynamic attribute (in a depth first order) in this template before moving up to the template's parent.
while let Some((path, el_ref)) = parent_node {
let mut listeners = vec![];
// safety: we maintain references of all vnodes in the element slab
let template = unsafe { el_ref.unwrap().as_ref() };
let node_template = template.template.get();
@@ -392,10 +393,14 @@ impl VirtualDom {
let this_path = node_template.attr_paths[idx];
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
if attr.name.trim_start_matches("on") == name
&& target_path.is_decendant(&this_path)
{
listeners.push(&attr.value);
if target_path.is_decendant(&this_path) {
attr.ty.for_each(|attribute| {
if attribute.name.trim_start_matches("on") == name {
if let AttributeValue::Listener(listener) = &attribute.value {
listeners.push(listener);
}
}
});
// Break if this is the exact target element.
// This means we won't call two listeners with the same name on the same element. This should be
@@ -408,20 +413,18 @@ impl VirtualDom {
// Now that we've accumulated all the parent attributes for the target element, call them in reverse order
// We check the bubble state between each call to see if the event has been stopped from bubbling
for listener in listeners.drain(..).rev() {
if let AttributeValue::Listener(listener) = listener {
let origin = path.scope;
self.runtime.scope_stack.borrow_mut().push(origin);
self.runtime.rendering.set(false);
if let Some(cb) = listener.borrow_mut().as_deref_mut() {
cb(uievent.clone());
}
self.runtime.scope_stack.borrow_mut().pop();
self.runtime.rendering.set(true);
for listener in listeners.into_iter().rev() {
let origin = path.scope;
self.runtime.scope_stack.borrow_mut().push(origin);
self.runtime.rendering.set(false);
if let Some(cb) = listener.borrow_mut().as_deref_mut() {
cb(uievent.clone());
}
self.runtime.scope_stack.borrow_mut().pop();
self.runtime.rendering.set(true);
if !uievent.propagates.get() {
return;
}
if !uievent.propagates.get() {
return;
}
}
@@ -445,18 +448,26 @@ impl VirtualDom {
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
// Only call the listener if this is the exact target element.
if attr.name.trim_start_matches("on") == name && target_path == this_path {
if let AttributeValue::Listener(listener) = &attr.value {
let origin = path.scope;
self.runtime.scope_stack.borrow_mut().push(origin);
self.runtime.rendering.set(false);
if let Some(cb) = listener.borrow_mut().as_deref_mut() {
cb(uievent.clone());
}
self.runtime.scope_stack.borrow_mut().pop();
self.runtime.rendering.set(true);
if target_path == this_path {
let mut should_stop = false;
attr.ty.for_each(|attribute| {
if attribute.name.trim_start_matches("on") == name {
if let AttributeValue::Listener(listener) = &attribute.value {
let origin = path.scope;
self.runtime.scope_stack.borrow_mut().push(origin);
self.runtime.rendering.set(false);
if let Some(cb) = listener.borrow_mut().as_deref_mut() {
cb(uievent.clone());
}
self.runtime.scope_stack.borrow_mut().pop();
self.runtime.rendering.set(true);
break;
should_stop = true;
}
}
});
if should_stop {
return;
}
}
}
+4 -3
View File
@@ -1,7 +1,7 @@
#![cfg(not(miri))]
use dioxus::prelude::Props;
use dioxus_core::*;
use dioxus_core::{MountedAttribute, *};
use std::{cfg, collections::HashSet};
fn random_ns() -> Option<&'static str> {
@@ -206,7 +206,7 @@ fn create_random_dynamic_node(cx: &ScopeState, depth: usize) -> DynamicNode {
}
}
fn create_random_dynamic_attr(cx: &ScopeState) -> Attribute {
fn create_random_dynamic_attr(cx: &ScopeState) -> MountedAttribute {
let value = match rand::random::<u8>() % 7 {
0 => AttributeValue::Text(Box::leak(
format!("{}", rand::random::<usize>()).into_boxed_str(),
@@ -218,7 +218,7 @@ fn create_random_dynamic_attr(cx: &ScopeState) -> Attribute {
5 => AttributeValue::None,
6 => {
let value = cx.listener(|e: Event<String>| println!("{:?}", e));
return Attribute::new("ondata", value, None, false);
return Attribute::new("ondata", value, None, false).into();
}
_ => unreachable!(),
};
@@ -228,6 +228,7 @@ fn create_random_dynamic_attr(cx: &ScopeState) -> Attribute {
random_ns(),
rand::random(),
)
.into()
}
static mut TEMPLATE_COUNT: usize = 0;
+28
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"] }
+76
View File
@@ -0,0 +1,76 @@
use proc_macro::TokenStream;
use convert_case::{Case, Casing};
use quote::{quote, ToTokens, TokenStreamExt};
use syn::__private::TokenStream2;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::{braced, parse_macro_input, Ident, Token};
#[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 == "ELEMENT",
name,
attrs,
})
}
}
impl ToTokens for ImplExtensionAttributes {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let name = &self.name;
let name_string = name.to_string();
let camel_name = name_string
.strip_prefix("r#")
.unwrap_or(&name_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());
if !self.is_element {
tokens.append_all(quote! {
struct #impl_name;
impl #name for #impl_name {}
});
}
let impls = self.attrs.iter().map(|ident| {
let d = if self.is_element {
quote! { #name::#ident }
} else {
quote! { <#impl_name as #name>::#ident }
};
quote! {
fn #ident(self, value: impl IntoAttributeValue<'a>) -> Self {
let d = #d;
self.push_attribute(d.0, d.1, value, d.2)
}
}
});
tokens.append_all(quote! {
pub trait #extension_name<'a>: HasAttributes<'a> + Sized {
#(#impls)*
}
});
}
}
@@ -0,0 +1 @@
fn main() {}
@@ -0,0 +1,5 @@
#[test]
fn tests() {
let t = trybuild::TestCases::new();
t.pass("tests/01-simple.rs");
}
+1
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 }
+14 -2
View File
@@ -1,9 +1,14 @@
#![allow(non_upper_case_globals)]
use dioxus_core::prelude::IntoAttributeValue;
use dioxus_core::HasAttributes;
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);
@@ -378,6 +383,13 @@ macro_rules! builder_constructors {
}
);
)*
pub(crate) mod extensions {
use super::*;
$(
impl_extension_attributes![ELEMENT $name { $($fil,)* }];
)*
}
};
}
+2 -2
View File
@@ -12,7 +12,7 @@ macro_rules! impl_event {
$(
$( #[$attr] )*
#[inline]
pub fn $name<'a, E: crate::EventReturn<T>, T>(_cx: &'a ::dioxus_core::ScopeState, mut _f: impl FnMut(::dioxus_core::Event<$data>) -> E + 'a) -> ::dioxus_core::Attribute<'a> {
pub fn $name<'a, E: crate::EventReturn<T>, T>(_cx: &'a ::dioxus_core::ScopeState, mut _f: impl FnMut(::dioxus_core::Event<$data>) -> E + 'a) -> ::dioxus_core::MountedAttribute<'a> {
::dioxus_core::Attribute::new(
impl_event!(@name $name $($js_name)?),
_cx.listener(move |e: ::dioxus_core::Event<crate::PlatformEventData>| {
@@ -20,7 +20,7 @@ macro_rules! impl_event {
}),
None,
false,
)
).into()
}
)*
};
+17 -1
View File
@@ -78,7 +78,7 @@ impl_event! {
#[deprecated(since = "0.5.0", note = "use ondoubleclick instead")]
ondblclick
ondoubleclick: "ondblclick"
// ondoubleclick: "ondblclick"
/// onmousedown
onmousedown
@@ -104,6 +104,22 @@ impl_event! {
onmouseup
}
#[inline]
pub fn ondoubleclick<'a, E: crate::EventReturn<T>, T>(
_cx: &'a ::dioxus_core::ScopeState,
mut _f: impl FnMut(::dioxus_core::Event<MouseData>) -> E + 'a,
) -> ::dioxus_core::MountedAttribute<'a> {
::dioxus_core::Attribute::new(
"ondblclick",
_cx.listener(move |e: ::dioxus_core::Event<MouseData>| {
_f(e).spawn(_cx);
}),
None,
false,
)
.into()
}
impl MouseData {
/// Create a new instance of MouseData
pub fn new(inner: impl HasMouseData + 'static) -> Self {
+6
View File
@@ -1,5 +1,9 @@
#![allow(non_upper_case_globals)]
use dioxus_core::prelude::IntoAttributeValue;
use dioxus_core::HasAttributes;
use dioxus_html_internal_macro::impl_extension_attributes;
use crate::AttributeDiscription;
#[cfg(feature = "hot-reload-context")]
@@ -106,6 +110,8 @@ macro_rules! trait_methods {
)*
None
}
impl_extension_attributes![GLOBAL $trait { $($name,)* }];
};
// Rename the incoming ident and apply a custom namespace
+7
View File
@@ -46,10 +46,17 @@ pub use render_template::*;
#[cfg(feature = "eval")]
pub mod eval;
pub mod extensions {
pub use crate::elements::extensions::*;
pub use crate::global_attributes::{GlobalAttributesExtension, SvgAttributesExtension};
}
pub mod prelude {
pub use crate::elements::extensions::*;
#[cfg(feature = "eval")]
pub use crate::eval::*;
pub use crate::events::*;
pub use crate::global_attributes::{GlobalAttributesExtension, SvgAttributesExtension};
pub use crate::point_interaction::*;
pub use keyboard_types::{self, Code, Key, Location, Modifiers};
}
+21 -6
View File
@@ -204,6 +204,20 @@ fn create_random_dynamic_node(cx: &ScopeState, depth: usize) -> DynamicNode {
}
}
fn create_random_dynamic_mounted_attr(cx: &ScopeState) -> MountedAttribute {
match rand::random::<u8>() % 2 {
0 => MountedAttribute::from(
&*cx.bump().alloc(
(0..(rand::random::<u8>() % 3) as usize)
.map(|_| create_random_dynamic_attr(cx))
.collect::<Vec<_>>(),
),
),
1 => MountedAttribute::from(create_random_dynamic_attr(cx)),
_ => unreachable!(),
}
}
fn create_random_dynamic_attr(cx: &ScopeState) -> Attribute {
let value = match rand::random::<u8>() % 6 {
0 => AttributeValue::Text(Box::leak(
@@ -214,7 +228,6 @@ fn create_random_dynamic_attr(cx: &ScopeState) -> Attribute {
3 => AttributeValue::Bool(rand::random()),
4 => cx.any_value(rand::random::<usize>()),
5 => AttributeValue::None,
// Listener(RefCell<Option<ListenerCb<'a>>>),
_ => unreachable!(),
};
Attribute::new(
@@ -271,11 +284,13 @@ fn create_random_element(cx: Scope<DepthProps>) -> Element {
.collect();
cx.bump().alloc(dynamic_nodes)
},
cx.bump().alloc(
(0..template.attr_paths.len())
.map(|_| create_random_dynamic_attr(cx))
.collect::<Vec<_>>(),
),
cx.bump()
.alloc(
(0..template.attr_paths.len())
.map(|_| create_random_dynamic_mounted_attr(cx))
.collect::<Vec<_>>(),
)
.as_slice(),
);
Some(node)
}
+8 -7
View File
@@ -5,7 +5,8 @@
use convert_case::{Case, Casing};
use dioxus_html::{map_html_attribute_to_rsx, map_html_element_to_rsx};
use dioxus_rsx::{
BodyNode, CallBody, Component, Element, ElementAttr, ElementAttrNamed, ElementName, IfmtInput,
AttributeType, BodyNode, CallBody, Component, Element, ElementAttr, ElementAttrNamed,
ElementName, IfmtInput,
};
pub use html_parser::{Dom, Node};
use proc_macro2::{Ident, Span};
@@ -63,16 +64,16 @@ pub fn rsx_node_from_html(node: &Node) -> Option<BodyNode> {
}
};
ElementAttrNamed {
AttributeType::Named(ElementAttrNamed {
el_name: el_name.clone(),
attr,
}
})
})
.collect();
let class = el.classes.join(" ");
if !class.is_empty() {
attributes.push(ElementAttrNamed {
attributes.push(AttributeType::Named(ElementAttrNamed {
el_name: el_name.clone(),
attr: ElementAttr {
name: dioxus_rsx::ElementAttrName::BuiltIn(Ident::new(
@@ -81,11 +82,11 @@ pub fn rsx_node_from_html(node: &Node) -> Option<BodyNode> {
)),
value: dioxus_rsx::ElementAttrValue::AttrLiteral(ifmt_from_text(&class)),
},
});
}));
}
if let Some(id) = &el.id {
attributes.push(ElementAttrNamed {
attributes.push(AttributeType::Named(ElementAttrNamed {
el_name: el_name.clone(),
attr: ElementAttr {
name: dioxus_rsx::ElementAttrName::BuiltIn(Ident::new(
@@ -94,7 +95,7 @@ pub fn rsx_node_from_html(node: &Node) -> Option<BodyNode> {
)),
value: dioxus_rsx::ElementAttrValue::AttrLiteral(ifmt_from_text(id)),
},
});
}));
}
let children = el.children.iter().filter_map(rsx_node_from_html).collect();
+46 -1
View File
@@ -4,14 +4,59 @@ use super::*;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{quote, ToTokens, TokenStreamExt};
use syn::{parse_quote, Expr, ExprIf, Ident, LitStr};
use syn::{parse_quote, spanned::Spanned, Expr, ExprIf, Ident, LitStr};
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
pub enum AttributeType {
Named(ElementAttrNamed),
Spread(Expr),
}
impl AttributeType {
pub fn start(&self) -> Span {
match self {
AttributeType::Named(n) => n.attr.start(),
AttributeType::Spread(e) => e.span(),
}
}
pub(crate) fn try_combine(&self, other: &Self) -> Option<Self> {
match (self, other) {
(Self::Named(a), Self::Named(b)) => a.try_combine(b).map(Self::Named),
_ => None,
}
}
}
impl ToTokens for AttributeType {
fn to_tokens(&self, tokens: &mut TokenStream2) {
match self {
AttributeType::Named(n) => tokens.append_all(quote! { #n }),
AttributeType::Spread(e) => tokens.append_all(quote! { (&#e).into() }),
}
}
}
#[derive(Clone, Debug)]
pub struct ElementAttrNamed {
pub el_name: ElementName,
pub attr: ElementAttr,
}
impl Hash for ElementAttrNamed {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.attr.name.hash(state);
}
}
impl PartialEq for ElementAttrNamed {
fn eq(&self, other: &Self) -> bool {
self.attr == other.attr
}
}
impl Eq for ElementAttrNamed {}
impl ElementAttrNamed {
pub(crate) fn try_combine(&self, other: &Self) -> Option<Self> {
if self.el_name == other.el_name && self.attr.name == other.attr.name {
+2 -2
View File
@@ -151,8 +151,8 @@ impl ToTokens for Component {
}
None => {
let mut toks = match prop_gen_args {
Some(gen_args) => quote! { fc_to_builder(#name #gen_args) },
None => quote! { fc_to_builder(#name) },
Some(gen_args) => quote! { fc_to_builder(__cx, #name #gen_args) },
None => quote! { fc_to_builder(__cx, #name) },
};
for field in &self.fields {
match field.name.to_string().as_str() {
+69 -24
View File
@@ -8,7 +8,7 @@ use syn::{
parse::{Parse, ParseBuffer, ParseStream},
punctuated::Punctuated,
spanned::Spanned,
Ident, LitStr, Result, Token,
Expr, Ident, LitStr, Result, Token,
};
// =======================================
@@ -18,8 +18,8 @@ use syn::{
pub struct Element {
pub name: ElementName,
pub key: Option<IfmtInput>,
pub attributes: Vec<ElementAttrNamed>,
pub merged_attributes: Vec<ElementAttrNamed>,
pub attributes: Vec<AttributeType>,
pub merged_attributes: Vec<AttributeType>,
pub children: Vec<BodyNode>,
pub brace: syn::token::Brace,
}
@@ -32,7 +32,7 @@ impl Parse for Element {
let content: ParseBuffer;
let brace = syn::braced!(content in stream);
let mut attributes: Vec<ElementAttrNamed> = vec![];
let mut attributes: Vec<AttributeType> = vec![];
let mut children: Vec<BodyNode> = vec![];
let mut key = None;
@@ -42,6 +42,22 @@ impl Parse for Element {
// "def": 456,
// abc: 123,
loop {
if content.peek(Token![..]) {
content.parse::<Token![..]>()?;
let expr = content.parse::<Expr>()?;
let span = expr.span();
attributes.push(attribute::AttributeType::Spread(expr));
if content.is_empty() {
break;
}
if content.parse::<Token![,]>().is_err() {
missing_trailing_comma!(span);
}
continue;
}
// Parse the raw literal fields
if content.peek(LitStr) && content.peek2(Token![:]) && !content.peek3(Token![:]) {
let name = content.parse::<LitStr>()?;
@@ -50,13 +66,13 @@ impl Parse for Element {
content.parse::<Token![:]>()?;
let value = content.parse::<ElementAttrValue>()?;
attributes.push(ElementAttrNamed {
attributes.push(attribute::AttributeType::Named(ElementAttrNamed {
el_name: el_name.clone(),
attr: ElementAttr {
name: ElementAttrName::Custom(name),
value,
},
});
}));
if content.is_empty() {
break;
@@ -79,13 +95,13 @@ impl Parse for Element {
let span = content.span();
if name_str.starts_with("on") {
attributes.push(ElementAttrNamed {
attributes.push(attribute::AttributeType::Named(ElementAttrNamed {
el_name: el_name.clone(),
attr: ElementAttr {
name: ElementAttrName::BuiltIn(name),
value: ElementAttrValue::EventTokens(content.parse()?),
},
});
}));
} else {
match name_str.as_str() {
"key" => {
@@ -93,13 +109,13 @@ impl Parse for Element {
}
_ => {
let value = content.parse::<ElementAttrValue>()?;
attributes.push(ElementAttrNamed {
attributes.push(attribute::AttributeType::Named(ElementAttrNamed {
el_name: el_name.clone(),
attr: ElementAttr {
name: ElementAttrName::BuiltIn(name),
value,
},
});
}));
}
}
}
@@ -108,7 +124,6 @@ impl Parse for Element {
break;
}
// todo: add a message saying you need to include commas between fields
if content.parse::<Token![,]>().is_err() {
missing_trailing_comma!(span);
}
@@ -120,12 +135,26 @@ impl Parse for Element {
// Deduplicate any attributes that can be combined
// For example, if there are two `class` attributes, combine them into one
let mut merged_attributes: Vec<ElementAttrNamed> = Vec::new();
let mut merged_attributes: Vec<AttributeType> = Vec::new();
for attr in &attributes {
if let Some(old_attr_index) = merged_attributes
.iter()
.position(|a| a.attr.name == attr.attr.name)
{
if let Some(old_attr_index) = merged_attributes.iter().position(|a| {
matches!((a, attr), (
AttributeType::Named(ElementAttrNamed {
attr: ElementAttr {
name: ElementAttrName::BuiltIn(old_name),
..
},
..
}),
AttributeType::Named(ElementAttrNamed {
attr: ElementAttr {
name: ElementAttrName::BuiltIn(new_name),
..
},
..
}),
) if old_name == new_name)
}) {
let old_attr = &mut merged_attributes[old_attr_index];
if let Some(combined) = old_attr.try_combine(attr) {
*old_attr = combined;
@@ -173,15 +202,31 @@ impl ToTokens for Element {
None => quote! { None },
};
let listeners = self
.merged_attributes
.iter()
.filter(|f| matches!(f.attr.value, ElementAttrValue::EventTokens { .. }));
let listeners = self.merged_attributes.iter().filter(|f| {
matches!(
f,
AttributeType::Named(ElementAttrNamed {
attr: ElementAttr {
value: ElementAttrValue::EventTokens { .. },
..
},
..
})
)
});
let attr = self
.merged_attributes
.iter()
.filter(|f| !matches!(f.attr.value, ElementAttrValue::EventTokens { .. }));
let attr = self.merged_attributes.iter().filter(|f| {
!matches!(
f,
AttributeType::Named(ElementAttrNamed {
attr: ElementAttr {
value: ElementAttrValue::EventTokens { .. },
..
},
..
})
)
});
tokens.append_all(quote! {
__cx.element(
+64 -48
View File
@@ -268,7 +268,7 @@ impl<'a> ToTokens for TemplateRenderer<'a> {
#[cfg(feature = "hot_reload")]
#[derive(Default, Debug)]
struct DynamicMapping {
attribute_to_idx: std::collections::HashMap<ElementAttr, Vec<usize>>,
attribute_to_idx: std::collections::HashMap<AttributeType, Vec<usize>>,
last_attribute_idx: usize,
node_to_idx: std::collections::HashMap<BodyNode, Vec<usize>>,
last_element_idx: usize,
@@ -284,7 +284,7 @@ impl DynamicMapping {
new
}
fn get_attribute_idx(&mut self, attr: &ElementAttr) -> Option<usize> {
fn get_attribute_idx(&mut self, attr: &AttributeType) -> Option<usize> {
self.attribute_to_idx
.get_mut(attr)
.and_then(|idxs| idxs.pop())
@@ -294,7 +294,7 @@ impl DynamicMapping {
self.node_to_idx.get_mut(node).and_then(|idxs| idxs.pop())
}
fn insert_attribute(&mut self, attr: ElementAttr) -> usize {
fn insert_attribute(&mut self, attr: AttributeType) -> usize {
let idx = self.last_attribute_idx;
self.last_attribute_idx += 1;
@@ -316,10 +316,17 @@ impl DynamicMapping {
match node {
BodyNode::Element(el) => {
for attr in el.merged_attributes {
match &attr.attr.value {
ElementAttrValue::AttrLiteral(input) if input.is_static() => {}
match &attr {
AttributeType::Named(ElementAttrNamed {
attr:
ElementAttr {
value: ElementAttrValue::AttrLiteral(input),
..
},
..
}) if input.is_static() => {}
_ => {
self.insert_attribute(attr.attr);
self.insert_attribute(attr);
}
}
}
@@ -347,7 +354,7 @@ impl DynamicMapping {
#[derive(Default, Debug)]
pub struct DynamicContext<'a> {
dynamic_nodes: Vec<&'a BodyNode>,
dynamic_attributes: Vec<&'a ElementAttrNamed>,
dynamic_attributes: Vec<&'a AttributeType>,
current_path: Vec<u8>,
node_paths: Vec<Vec<u8>>,
@@ -367,10 +374,16 @@ impl<'a> DynamicContext<'a> {
let mut static_attrs = Vec::new();
for attr in &el.merged_attributes {
match &attr.attr.value {
ElementAttrValue::AttrLiteral(value) if value.is_static() => {
match &attr {
AttributeType::Named(ElementAttrNamed {
attr:
ElementAttr {
value: ElementAttrValue::AttrLiteral(value),
name,
},
..
}) if value.is_static() => {
let value = value.source.as_ref().unwrap();
let name = &attr.attr.name;
let attribute_name_rust = name.to_string();
let (name, namespace) =
Ctx::map_attribute(&element_name_rust, &attribute_name_rust)
@@ -384,7 +397,7 @@ impl<'a> DynamicContext<'a> {
_ => {
let idx = match mapping {
Some(mapping) => mapping.get_attribute_idx(&attr.attr)?,
Some(mapping) => mapping.get_attribute_idx(attr)?,
None => self.dynamic_attributes.len(),
};
self.dynamic_attributes.push(attr);
@@ -454,47 +467,50 @@ impl<'a> DynamicContext<'a> {
ElementName::Ident(i) => quote! { dioxus_elements::#i::#name },
ElementName::Custom(_) => quote! { None },
};
let static_attrs = el
.merged_attributes
.iter()
.map(|attr| match &attr.attr.value {
ElementAttrValue::AttrLiteral(value) if value.is_static() => {
let value = value.to_static().unwrap();
let ns = {
match &attr.attr.name {
ElementAttrName::BuiltIn(name) => ns(quote!(#name.1)),
ElementAttrName::Custom(_) => quote!(None),
}
};
let name = &attr.attr.name;
let name = match (el_name, name) {
(ElementName::Ident(_), ElementAttrName::BuiltIn(_)) => {
quote! { #el_name::#name.0 }
}
_ => {
let as_string = name.to_string();
quote! { #as_string }
}
};
quote! {
::dioxus::core::TemplateAttribute::Static {
name: #name,
namespace: #ns,
value: #value,
let static_attrs = el.merged_attributes.iter().map(|attr| match attr {
AttributeType::Named(ElementAttrNamed {
attr:
ElementAttr {
value: ElementAttrValue::AttrLiteral(value),
name,
},
..
}) if value.is_static() => {
let value = value.to_static().unwrap();
let ns = {
match name {
ElementAttrName::BuiltIn(name) => ns(quote!(#name.1)),
ElementAttrName::Custom(_) => quote!(None),
}
};
let name = match (el_name, name) {
(ElementName::Ident(_), ElementAttrName::BuiltIn(_)) => {
quote! { #el_name::#name.0 }
}
_ => {
let as_string = name.to_string();
quote! { #as_string }
}
};
quote! {
::dioxus::core::TemplateAttribute::Static {
name: #name,
namespace: #ns,
value: #value,
// todo: we don't diff these so we never apply the volatile flag
// volatile: dioxus_elements::#el_name::#name.2,
}
// todo: we don't diff these so we never apply the volatile flag
// volatile: dioxus_elements::#el_name::#name.2,
}
}
}
_ => {
let ct = self.dynamic_attributes.len();
self.dynamic_attributes.push(attr);
self.attr_paths.push(self.current_path.clone());
quote! { ::dioxus::core::TemplateAttribute::Dynamic { id: #ct } }
}
});
_ => {
let ct = self.dynamic_attributes.len();
self.dynamic_attributes.push(attr);
self.attr_paths.push(self.current_path.clone());
quote! { ::dioxus::core::TemplateAttribute::Dynamic { id: #ct } }
}
});
let attrs = quote! { #(#static_attrs),*};
+19 -27
View File
@@ -91,27 +91,29 @@ impl Renderer {
match segment {
Segment::Attr(idx) => {
let attr = &template.dynamic_attrs[*idx];
if attr.name == "dangerous_inner_html" {
inner_html = Some(attr);
} else if attr.namespace == Some("style") {
accumulated_dynamic_styles.push(attr);
} else if BOOL_ATTRS.contains(&attr.name) {
if truthy(&attr.value) {
write!(buf, " {}=", attr.name)?;
write_value(buf, &attr.value)?;
attr.attribute_type().try_for_each(|attr| {
if attr.name == "dangerous_inner_html" {
inner_html = Some(attr);
} else if attr.namespace == Some("style") {
accumulated_dynamic_styles.push(attr);
} else if BOOL_ATTRS.contains(&attr.name) {
if truthy(&attr.value) {
write_attribute(buf, attr)?;
}
} else {
write_attribute(buf, attr)?;
}
} else {
write_attribute(buf, attr)?;
}
if self.pre_render {
if let AttributeValue::Listener(_) = &attr.value {
// The onmounted event doesn't need a DOM listener
if attr.name != "onmounted" {
accumulated_listeners.push(attr.name);
if self.pre_render {
if let AttributeValue::Listener(_) = &attr.value {
// The onmounted event doesn't need a DOM listener
if attr.name != "onmounted" {
accumulated_listeners.push(attr.name);
}
}
}
}
Ok(())
})?;
}
Segment::Node(idx) => match &template.dynamic_nodes[*idx] {
DynamicNode::Component(node) => {
@@ -424,16 +426,6 @@ pub(crate) fn write_attribute(buf: &mut impl Write, attr: &Attribute) -> std::fm
}
}
pub(crate) fn write_value(buf: &mut impl Write, value: &AttributeValue) -> std::fmt::Result {
match value {
AttributeValue::Text(value) => write!(buf, "\"{}\"", value),
AttributeValue::Bool(value) => write!(buf, "{}", value),
AttributeValue::Int(value) => write!(buf, "{}", value),
AttributeValue::Float(value) => write!(buf, "{}", value),
_ => Ok(()),
}
}
pub(crate) fn write_value_unquoted(
buf: &mut impl Write,
value: &AttributeValue,
+9 -5
View File
@@ -1,4 +1,5 @@
use crate::dom::WebsysDom;
use dioxus_core::AttributeValue;
use dioxus_core::{DynamicNode, ElementId, ScopeState, TemplateNode, VNode, VirtualDom};
#[derive(Debug)]
@@ -81,12 +82,15 @@ impl WebsysDom {
if let dioxus_core::TemplateAttribute::Dynamic { id } = attr {
let attribute = &vnode.dynamic_attrs[*id];
let id = attribute.mounted_element();
mounted_id = Some(id);
if let dioxus_core::AttributeValue::Listener(_) = attribute.value {
if attribute.name == "onmounted" {
to_mount.push(id);
attribute.attribute_type().for_each(|attribute| {
let value = &attribute.value;
mounted_id = Some(id);
if let AttributeValue::Listener(_) = value {
if attribute.name == "onmounted" {
to_mount.push(id);
}
}
}
});
}
}
if let Some(id) = mounted_id {