vide-collabo / Vide

Vide - for state-aware function composition, usable from modern UIs to digital signal processing.
https://vide-dev.io/
Apache License 2.0
89 stars 4 forks source link

Custom Elements/Custom Events #2

Open AngelMunoz opened 1 year ago

AngelMunoz commented 1 year ago

Hey there here I am with the first questions.

I'll set a little bit of context first: I'm quite a fan of custom elements and standards-based web dev in general so one of the first things that comes to my mind when trying out a web framework is how well are those supported.

This is a vanilla js use case I'd like to be able to do with vide

<sl-tab-group>
  <sl-tab slot="nav" panel="general">General</sl-tab>
  <sl-tab slot="nav" panel="custom">Custom</sl-tab>
  <sl-tab slot="nav" panel="advanced">Advanced</sl-tab>
</sl-tab-group>
<button>Select advanced tab</button>

And in my js something like

const btn = document.querySelector("button")
const tabGroup = document.querySelector("sl-tab-group")
btn.addEventListener('click', () => tabGroup.show("advanced"));

// here event is a CustomEvent<{ name: string }> event
tabGroup.addEventListener('sl-tab-show', (event) => { console.log("tab selected", event.detail.name) }

I'm not sure if that is possible with vide right now I tried to find something in my limited experience at the moment but no luck so far

Here's how my pseudo code for vide would ideally look like

let MyTabsComponent() = vide {
  custom "sl-tab-group" {
    // custom element version to provide the generic
    custom.oncustom<{| name: string |}>("sl-show", (fun event -> printfn "%A" event.details.name))

    // alternative non-generic version as one can emit non-generic events as well
    custom.onevent("sl-show", (fun (event: Event) ->
      let ev = (Event :?> CustomEvent<{| name: string |}>)
      printfn "%A" event.details.name))

    custom "sl-tab" {
      custom.slot "nav"
      custom.attr("panel", "general")
      "General"
    }
    custom "sl-tab" {
      custom.slot "nav"
      custom.attr("panel", "custom")
      "Custom"
    }
    custom "sl-tab" {
      custom.slot "nav"
      custom.attr("panel", "advanced")
      "Advanced"
    }
  }
  button {
    "Select Advanced Tab"
    button.onclick (fun _ -> 
      // get a ref somehow here?
      ref.show "advanced"
  }
}

In this case I'm not very familiar with the DSL yet but if this is an area I can contribute to I'd gladly step in to see what I can do.

In the case of the typings I don't mind if everything is string * obj there's a "generator" project I had for Sutil for these kinds of web component libraries, I might be able to revive it to let it generate a fully typed vide DSL as well, but my concerns are more in the out of the box experience and how can we consume these standard features that are not so F# friendly

I'll keep dropping more questions as I keep trying vide but for the moment let me know what you think about that

SchlenkR commented 1 year ago

Hey Angel,

thanks for the input. There are currently several ways of doing this, which I'll describe.

I added some small convenience functions and published 0.0.15

Untyped (ad-hoc) custom elements

The e function creates an element with the given tag name. It provides all global attributes and events, and it's possible to set attributes using attr / attrBoolean or register event handlers using on. The e function is just a shorthand:

let e (tagName: string) = HtmlGARenderRetCnBuilder<HTMLElement>(tagName)

Here's an example how to use it:

vide {
    div {
        (e "untyped-custom-elem")
            .attr("untyped-custom-attribute", "Hello there")
            .on("untyped-custom-event", fun evt -> ()) {
                "Hello there again"
        }
    }
}

Typed custom elements

Like any other HTML element builder available in the Vide.Fable library, it is possible to specify custom element builders using similar pattern. How does that look like?

module MyCustomElementDefinitions =
    open Browser.Types
    open Vide
    open Vide.HtmlElementBuilders

    type typedCustomElementBuilder() =
        inherit HtmlGARenderRetCnBuilder<HTMLElement>("typed-custom-element")
        member this.myAttribute(value: string) =
            this.attr("myAttribute", value)
        member this.myBooleanAttribute(value: bool) =
            this.attrBoolean("myBooleanAttribute", value)
        member this.onMyEvent(handler) =
            this.on("myEvent", handler)

    [<AutoOpen>]
    type MyCustomElements =
        static member inline typedCustomElement = typedCustomElementBuilder()

Here, a typed-custom-element is defined by inheriting from HtmlGARenderRetCnBuilder that is based upon the HTMLElement dom interface. Custom events and attributed are added as members, which use the same attr, attrBoolean, and on methods.

As convenience, another type MyCustomElements specifies a static property to allow omitting () when using the custom element in a view.

Using the element then looks like this:

open Vide
open type Vide.Html
open MyCustomElementDefinitions

let view = 
    vide {
        div {
            typedCustomElement
                .myBooleanAttribute(false)
                .myAttribute("Hello")
                .onMyEvent(fun evt -> ()) {
                    "typedCustomElement can have content because it inherits from HtmlGARenderRetCnBuilder."
                    "if you don't like that, inherit from HtmlGARenderRetC0Builder."
                }
        }
    }

There's a demo here.

Template / Slot

I just added the slot element and slot global attribute (template element already existed), but I never used it and don't know if it will work.

I'm not sure if all of your ideas are addressed. At least I think this is missing, and I don't understand what is your intention:

// get a ref somehow here?

I'd be happy to discuss this issue further, and I appreciate more of those use cases. Thank you so much, @AngelMunoz for your help and input!