fable-compiler / Fable.Lit

Write Fable Elmish apps with Lit
https://fable.io/Fable.Lit/
MIT License
93 stars 13 forks source link

[Feliz.Lit] Uncaught TypeError: Cannot read property '0' of undefined #8

Closed AngelMunoz closed 3 years ago

AngelMunoz commented 3 years ago

When using Two hook components with Feliz.Lit like the following

Fable CLI: 3.3.0-beta-002

[<HookComponent>]
let counter (props: {| initial: int option |}) =
    let count, setCount =
        Hook.useState (props.initial |> Option.defaultValue 0)

    Html.div [
        Html.p $"Home: {count}"
        Html.button [
            Ev.onClick (fun _ -> setCount (count + 1))
            Html.text "Increment"
        ]
        Html.button [
            Ev.onClick (fun _ -> setCount (count - 1))
            Html.text "Decrement"
        ]
        Html.button [
            Ev.onClick (fun _ -> setCount (0))
            Html.text "Reset"
        ]
    ]

[<HookComponent>]
let app () =
    Html.article [
        Html.main [
            Html.section [
                Html.h1 "Functions with parameters and state!"
                counter ({| initial = None |}) 
                counter ({| initial = Some 100 |})
            ]
        ]
    ]
    |> Feliz.toLit

LitHtml.render (app (), document.querySelector ("#app"))

causes an error like this

http://localhost:8080/dist/.fable/Fable.Lit.Feliz.1.0.0-beta-003/Lit.Feliz.fs.js [:97:578]
TypeError: Cannot read property '0' of undefined
    at http://localhost:8080/dist/.fable/Fable.Lit.Feliz.1.0.0-beta-003/Lit.Feliz.fs.js:97:578
    at uncurriedFn (http://localhost:8080/dist/.fable/fable-library.3.3.0-beta-002/Util.js:520:44)
    at fold (http://localhost:8080/dist/.fable/fable-library.3.3.0-beta-002/List.js:405:15)
    at http://localhost:8080/dist/.fable/Fable.Lit.Feliz.1.0.0-beta-003/Lit.Feliz.fs.js:84:36
    at http://localhost:8080/dist/.fable/Fable.Lit.Feliz.1.0.0-beta-003/Lit.Feliz.fs.js:97:347
    at uncurriedFn (http://localhost:8080/dist/.fable/fable-library.3.3.0-beta-002/Util.js:520:44)
    at fold (http://localhost:8080/dist/.fable/fable-library.3.3.0-beta-002/List.js:405:15)
    at http://localhost:8080/dist/.fable/Fable.Lit.Feliz.1.0.0-beta-003/Lit.Feliz.fs.js:84:36
    at http://localhost:8080/dist/.fable/Fable.Lit.Feliz.1.0.0-beta-003/Lit.Feliz.fs.js:97:347
    at uncurriedFn (http://localhost:8080/dist/.fable/fable-library.3.3.0-beta-002/Util.js:520:44)

It gets fixed if you change these lines

// at the end of counter
|> Feliz.toLit
// inside

[<HookComponent>]
let app () =
    Html.article [
        Html.main [
            Html.section [
                Html.h1 "Functions with parameters and state!"
                counter ({| initial = None |}) |> Feliz.ofLit
                counter ({| initial = Some 100 |}) |> Feliz.ofLit
            ]
        ]
    ]
    |> Feliz.toLit

but if you try the following doesn't work

counter ({| initial = None |}) |> Feliz.toLit |> Feliz.ofLit
AngelMunoz commented 3 years ago

Looks like for some reason inside inner in Util.buildTemplate the text node is undefined image

            | Text v ->

                JS.console.log(parts, values, v)
                ""::parts, (box v::values)

sorry it was in the next iteration image

it throws in that v

alfonsogarciacaro commented 3 years ago

Yes, this is my main concern with the Lit.Feliz API. Basically you should convert to Lit Feliz nodes before returning them in a function, and you cannot Feliz nodes conditionally. But it's difficult to restrict this at compile, because Feliz nodes happily accept other Feliz nodes as children. The only solution I can think of right now is to make the Feliz Engines only available through a function argument, but this makes things more verbose and cannot restrict conditional nodes either.

Feliz.toLit(fun f -> f.Html.div [ f.Attr.className "foo" ])

Not sure what's the problem in specific case. But HookComponent is a lit directive , so passing it a function that doesn't return a lit template is not going to work.

AngelMunoz commented 3 years ago

Hmm sorry I think I understood this last time you mentioned it, looks like I didn't this is kind of unfortunate... Hmm this might be a bit of a stretch but if we enable lit components we should be able to use ReactiveControllers and imitate hooks a little although not a 100% replacement

but at least we can provide the simple ones

type StateController<'T>(host: ReactiveControllerHost, init: 'T) = 
    let mutable state = init
    member _.State with get() = state

    member setState(update: 'T -> 'T): unit = 
        state <- update(state)
        host.requestUpdate()

    // nothing to do for this particular usecase
    interface ReactiveController with
        member _.hostConnected() = ()

        member _.hostDisconnected() = ()

        member _.hostUpdate() = ()

        member _.hostUpdated() = ()

let childCounter(host: obj) =
    // jsThis should be good enough as well but sometimes for some reason
    // the functions get compiled as a lambda, no idea why
    let ctrl = StateController<int>(host, 0)
    Html.div [
        Html.p $"Child Count: {ctrl.State}"
        Html.button [ 
            Ev.onClick(fun _ -> ctrl.setState(fun state -> state + 1));
            Html.text "Increment" ]
        Html.button [ 
            Ev.onClick(fun _ -> ctrl.setState(fun state -> state - 1));
            Html.text "Decrement" ]
        Html.button [ 
            Ev.onClick(fun _ -> ctrl.setState(fun _ -> 0); Html.text "Reset" ]
    ]

let app() = 
    let host = jsThis
    let ctrl = StateController<int>(host, 0)

    Html.div [
        Html.p $"Parent Count: {ctrl.State}"
        Html.button [ 
            Ev.onClick(fun _ -> ctrl.setState(fun state -> state + 1));
            Html.text "Increment" ]
        Html.button [ 
            Ev.onClick(fun _ -> ctrl.setState(fun state -> state - 1));
            Html.text "Decrement" ]
        Html.button [ 
            Ev.onClick(fun _ -> ctrl.setState(fun _ -> 0); Html.text "Reset" ]
        childCounter host
    ]

// TODO:
registerLitComponent("tag-name", app, options)

another approach would be to create an internal ReactiveElement and handle state updates within it which is what useController does in haunted

thoughts?

alfonsogarciacaro commented 3 years ago

Actually, what I found the biggest appealing with lit-html is the possibility of embedding HTML in F#. If we're just offering an API similar (but with differences) to Feliz and different ways/concepts to manage state, just being able to define web components (something that is already possible in React) won't be a strong reason for Fable devs to try this. So I'm considering now to deprecate Lit.Feliz (except for the styles). If we want to have a light way to declare web components with a Feliz-like API we should probably focus on adding an adapter to Sutil (as you're already doing) or Feliz.Snabbdom.

AngelMunoz commented 3 years ago

So I'm considering now to deprecate Lit.Feliz (except for the styles)

I agree completely I was a little anxious because I guess in my excitement I teased a little about typesafety with Lit yet I couldn't help you to make it work 😔

If we want to have a light way to declare web components with a Feliz-like API we should probably focus on adding an adapter to Sutil (as you're already doing) or Feliz.Snabbdom.

I agree here as well, I think I'll just add the Haunted adapter for Sutil as well and leave Haunted as the entry point for people who want web components while I figure out a more focused approach that works best for Fable users, I'll take care of that.

with this I'll close this issue and the PR I had for the LitExtensions, I think that as you mention the safer approach right now is to focus on the embedded HTML, anything else can come later.