dioxus icon indicating copy to clipboard operation
dioxus copied to clipboard

Ergonomic custom elements

Open olanod opened this issue 1 year ago • 6 comments

With this PR I propose that all lowercase identifiers followed by braces are treated as elements instead of components, currently only single identifiers that are in diouxus_html are treated as such. Allowing identifiers with a path is the most ergonomic and simple way I found to express custom elements, this paired with the custom_elements macro that generates the DioxusElement definition inside a sun-module and uses the module and struct name to define the element tag results in a decent way to declare this kind of elements.

rsx!(
  div { "normal element" },
  some::element { "also an element" }, // <some-element>
)

This change would probably break code that uses lowercase components but since the docs and general convention seems to be to declare components starting with uppercase I thought this is acceptable.

olanod avatar Sep 02 '22 12:09 olanod

I agree that it should be easier to define custom components, but the distinction between a path-separated element and a built-in element is seems arbitrary. There is a PR in progress that would allow components to be lowercase (https://github.com/DioxusLabs/dioxus/pull/490) which should solve this issue. That PR removes the distinction between components and elements for the macro. With that approach we could remove the dioxus_elements module and just use any elements in scope that implements DioxusElement. Exposing a custom_element macro would probably be a good idea.

Edit: Importing the entire html namespace in the prelude causes a lot of collisions will variables/functions, so the above is a bad idea. It is largely unavoidable for the builder api, but for macros we can do better

ealmloff avatar Sep 05 '22 20:09 ealmloff

About the "arbitrary" distinction, do you mean <builtinelement> vs <custom-element>? if that's the case, it's not arbitrary, it's actually how it is, any element in the standard now and in the future will always be without hyphens and custom elements always have to include a hyphen to be valid.

olanod avatar Sep 05 '22 22:09 olanod

👍I was not aware of that, my bad. Although, there are some other concerns as well:

  • this solution would break once lowercase components are implemented
  • this is a breaking change for a lot of user code (the api is not stable, but making upgrading easy would be nice)

ealmloff avatar Sep 06 '22 00:09 ealmloff

Yeah this approach does come with the mentioned drawbacks and that's why I bring it up for consideration.
That said I personally don't think is bad to "standardize" and make more strict the syntax on how things are defined so people don't have to guess and immediately recognize a dioxus component, a custom element or a built-in element. Currently the components syntax is very permissive since it's more of a catch-all approach(i.e. if it's not built-in is component), ideally we settle for a single way to declare components?(e.g. CamelCase?) that would mean there's gonna be breaking changes anyway.

olanod avatar Sep 06 '22 08:09 olanod

I've been thinking about this some more.

  • We could use the same way of defining custom components proposed here, but transform it to the current form (turning ::'s into _'s) in the rsx macro. This would make the change non-breaking.

  • Correct me if I'm wrong, but the main use case I see for custom elements is web components. If you look at the elements in webcompoents.org many have names that are not related to a namespace or have a common name that could collide with a rust crate. For example Polymer Elements has some popular collections of elements. App elements is one of those collections. With this proposal it would produce a module app that could collide with the name of a variable or function. We can't entirely avoid this problem, but generally longer names (like the full foo_bar instead of foo) generally collide less.

Without the builder api I think this would work: What if we prefixed all elements with dioxus_element_(element name)? (a would be dioxus_element_a, div would be dioxus_element_div, etc.) This would allow the definitions to be anywhere and still avoid namespace collisions. The rsx macro could handle the prefixing and a macro similar to custom elements could make creating them easier.

custom_elements! {
    // foo-bar gets turned into dioxus_element_foo_bar
    foo-bar {
        abc,
        def,
    },
}

But this would make the builder api ugly...

ealmloff avatar Sep 11 '22 14:09 ealmloff

Or here is a solution that could work for all of the above: The rsx macro expands this code:

rsx! {
    div {
        name: "{my_class}"
    }
}

To:

LazyNodes::new(move |__cx: NodeFactory| -> VNode {
    use dioxus_elements::{GlobalAttributes, SvgAttributes};
    __cx.element(
        dioxus_elements::div,
        __cx.bump().alloc([]),
        __cx
            .bump()
            .alloc([
                dioxus_elements::div
                    .name(
                        __cx,
                        format_args!("{my_class}"),
                    ),
            ]),
        __cx.bump().alloc([]),
        None,
    )
})

We could instead expand it like this (with an added Copy bound for DioxusElement):

LazyNodes::new(move |__cx: NodeFactory| -> VNode {
        use dioxus_elements::{GlobalAttributes, SvgAttributes};
        let __div = {
            use dioxus_elements::*;
            div
        };
        __cx.element(
            __div,
            __cx.bump().alloc([]),
            __cx
                .bump()
                .alloc([
                    __div
                        .class(
                            __cx,
                            format_args!("{my_class}"),
                        ),
                ]),
            __cx.bump().alloc([]),
            None,
        )
    })

This would allow you to define a custom element easily without breaking changes and hopefully less namespace collisions.

ealmloff avatar Sep 12 '22 00:09 ealmloff

@Demonthos here goes an updated version based on your suggestion. Now I made the macro less magic removing the namespace module weirdness and the tag needs to be specified explicitly.

olanod avatar Oct 14 '22 16:10 olanod

I like the idea of getting rid of the "dioxus_elements" namespace but this PR has the issue where the html namespace and its attributes collide with local variables, etc.

I think we can get it working by implementing a trait like "Html" for some dioxus-exported object. Adding a new element would just be done by extending that object with your own trait.

I'll look into it. @olanod Are you still interested in seeing this PR through?

jkelleyrtp avatar Dec 24 '22 00:12 jkelleyrtp

We can close this PR if a better approach comes up that supports custom elements :)

olanod avatar Dec 24 '22 07:12 olanod

I think I've got something that uses traits against the ScopeState object... but it could use a separate NodeFactory namespace. To use it, you'd just import your trait that defines the set of custom elements.

This will let us export the macro we use to power the HTML crate for everyone, including you as an enduser.

custom_elements! {
    HtmlElements;
    div {},
    link {
        crossorigin: StringAttr,
        href: StringAttr,
        hreflang: StringAttr,
        integrity: StringAttr,
    },
}

By declaring the type alongside the item, it should also allow for typesafe attributes.

jkelleyrtp avatar Jan 04 '23 01:01 jkelleyrtp