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] Static templates in lit-html 2.0 #4

Closed alfonsogarciacaro closed 2 years ago

alfonsogarciacaro commented 3 years ago

I tried the Fable.Lit and at the beginning they seemed to be working but then I realized the whole DOM was updated for every render. Some debugging revealed the reason is in lit-html 2.0 (currently rc-004) they're using a WeakMap to identify templates already rendered. I think I've managed to fix this in Fable by compiling to JS template strings (even if I've to wrap them again to conform to FormattableString API), but with Feliz.Lit.toLit this is not possible because templates are generated on-the-fly.

A workaround is tricky. Even if we could keep a "static" reference (but that can be garbage collected) per toLit call and have our own WeakMap, we cannot be sure the template will be the same every time because Feliz code is dynamic. The only solutions I can think of atm are:

What do you think @AngelMunoz?

AngelMunoz commented 3 years ago

this might sound silly (and let me know if it is) and I might not be understanding Lit's code there but if we produce a DOM string without holes would that allow Lit's caching to prevent re-rendering all of the time until said DOM string changed?

I would think that if in the end we produce a different DOM it would create a different cache entry and so on

as an example:

let toLit (node: Node) = 
    // the code for the node
   let result = ...
   html result

also what about getting a hash of the whole node and using it as the only whole on that template?

the hash would need to be reproducible, I'm not sure if that can be done with Feliz.Lit

otherwise I don't think there might be an easy solution here ('ill try to think on something else) anyways if that's not the case, we can always point to Sutil if the users want a type safe DSL that uses DOM nodes under the hood (making it as compatible as Lit with the rest of the ecosystem)

alfonsogarciacaro commented 3 years ago

If I understand the code of lit-html 2.0 correctly, instead of building the TemplateResult directly in the html function they pass the deconstructed template more or less as is (the strings parts and the holes) with some metadata, then parse the template in render and cache the result in a WeakMap cache. It's important to note they use the strings array as the keys of the cache for two reasons: first it's ok the holes change, and second WeakMap keys must be objects (not strings or numbers).

Sorry I forgot to include it in the solutions above, but a possibility is to use our own cache for example with a hash as you said. The problem is we cannot use a WeakMap because we don't have a static object reference. So the cache would grow indefinitely as the app runs. Probably it wouldn't be an issue in real apps with not many Feliz templates embedded in Lit, and we could clean it up from time to time, but still it's not a nice design.

AngelMunoz commented 3 years ago

but still it's not a nice design

I agree

Probably it wouldn't be an issue in real apps with not many Feliz templates embedded in Lit

Hmm that could be a problem then from feedback I've got over the last few days there are people who want to run pure Feliz.Lit

AngelMunoz commented 3 years ago

do you have a particular "playground" for this? I think we could try with this directive as well but I'm afraid that at this point I'm just thinking in circles

https://lit.dev/docs/api/directives/#templateContent

alfonsogarciacaro commented 3 years ago

Oh, my. I think I got something! By emitting an empty JS template we can get a static reference that should be garbage collected when it's not needed anymore (e.g. when user moves to another page). Thanks to this we can cache the string parts which should also make Feliz templates faster The caveat is the template must be "static": https://github.com/alfonsogarciacaro/Elmish.Lit/blob/7fcfcd02c77c4cb8cf7cd251947b282bca618c06/sample/src/App.fs#L35-L52

Please give it a try to the latest commit. Note this needs latest fable (3.2.12). I will publish new versions of the package soon.

AngelMunoz commented 3 years ago

I stoped the clock and tried this version

let buttonFeliz (model: Model) dispatch =
    Html.button [
        Attr.className "button"
        Ev.onClick (fun _ -> not model.ShowClock |> ShowClock |> dispatch)
        if model.ShowClock then
            Html.text "Hide clock"
        else
            Html.strong "Show clock"
    ]
    |> toLitStatic

and it seems to be rendering only when the model changes, and previously if I remember correctly it was re-rendering even if there were no changes I'm not sure you need to call from/to toLit and toLitStatic

fsharp-lit-static image

also the cache you set doesn't seem to grow either

I think this is the goal right?

AngelMunoz commented 3 years ago

Also I saw this weird behavior, if you click a lot , I'm not sure if this has to do with Elmish or the template code something seems to block the render at some point

lit-weird

texastoland commented 3 years ago

@alfonsogarciacaro Can you link to a playground or Lit docs to grok the issue?

alfonsogarciacaro commented 3 years ago

Thanks for checking @AngelMunoz! Strange, the render doesn't stop for me even if I click a lot. Maybe my machine is slower than yours πŸ˜…

@texastoland You can try the app in the sample folder, just by running npm i && npm start in that directory, but please read the comments below.

AngelMunoz commented 3 years ago

Following discussion here about relying too much on Haunted, I took a stab at an initial custom Hook implementation. They required some more magic from Fable compiler but they seems to work now, although I'm not sure if it's the best approach because I never understood how React hooks work internally πŸ˜… In this case, HookComponent would be similar to Haunted VirtualComponent in the sense it's not an actual web component. The main advantage is you can call them as a typed function.

After dabling into lit-html 2 here https://github.com/AngelMunoz/Fable.Haunted/pull/7 I'm afraid we shouldn't get too comfortable with hooks that use directives underneath like virtual since those are the ones that will indeed break from lit-html 1 -> 2 the rest of the hook seem to be stable, since they don't seem to depend on any particular Lit based behavior, I think haunted hooks are fairly stable and once we're done with beta stages we can rely on them for a while knowing they'll use our own render function (lit-html 2) and certainly already do whar you would expec them to do, the only lacking thing is the virtual component but I guess that will fall on our hands.

Let me know when you release these packages! I'll give them a try and keep pumping samples and see if the community starts picking it up 😁

AngelMunoz commented 3 years ago

Regarding the directive for a virtual component I played with this here https://github.com/AngelMunoz/directive-experiments/blob/main/src/index.ts and while I can make the virtual component get rendered and use hooks like useState when the parent re-renders making the reference inside the weakmap be collected a couple of clicks later this "reseting" the virtual component to the initial state

Also my parts seem to be out of place, I'm not sure what I'm doing wrong

image

I tried to do a copy/paste kind of exploration because I'm unfamiliar with both directives and haunted internals

I based my code updates from this page https://lit.dev/docs/releases/upgrade/#overview-of-directive-api-changes

so any eyes and help could be great

Also.... we could try to go for our own hooks with Controllers although we might need to do some js/ts for those I'm not sure how fable could fit in there

alfonsogarciacaro commented 3 years ago

Hmm, I'm more and more convinced that we shouldn't put too much effort on haunted virtual components. The library author doesn't seem to use them so that's a sign they won't see a lot of love in the near future. Probably we can remove the VirtualComponent from Fable.Haunted (sorry!) and just recommend the library for users who want to declare web components in a React fashion. At one point we should also make it possible to declare web components Γ -la-Lit (or even the standard way).

If users want to keep some local state between lit-html renders (without declaring an actual web component), this simple hook implementation seems to work and doesn't require extra dependencies so hopefully that's enough for now.

I've written many adapters for cool JS libraries (Vue, Svelte) but none has wide adoption. I believe the reason is that most users have already written big apps with React and it's too painful to rewrite them. Even for new projects it's difficult to use a new library if you've already many components (and workflows) written in React. I think being able to write HTML in F# is hugely beneficial for Fable apps but we need to focus on integration and minimize the steps for that πŸ‘

I've published the packages with the new names. Note the package ids are prefixed with Fable. but the projects are Lit/Lit.React/Lit.Elmish/Lit.Feliz.

AngelMunoz commented 3 years ago

Hmm, I'm more and more convinced that we shouldn't put too much effort on haunted virtual components

I agree, all of the scheduler stuff seems un-intuitive at best and I think providing better integration with Lit could prove to be a better option

At one point we should also make it possible to declare web components Γ -la-Lit (or even the standard way).

I could re-invest time on the function based web components I was thinking in Sutil but adding a couple of inspirations from haunted

what do you think about actually allowing class style components here? I know classes are sometimes... too much for some people and other kind of annoying topic are decorators but there are javascript API's and with the tagged templates classes are more JSX/TSX stylish I'll explore this area but I don't think I'll spend an extra amount of time for that

I've written many adapters for cool JS libraries (Vue, Svelte) but none has wide adoption. I believe the reason is that most users have already written big apps with React and it's too painful to rewrite them.

I wouldn't expect react users to move, but Ideally I'd expect new users or users familiars with other common frameworks to pick up the familiar html syntax and then move on to more F# flavored stuff either with Feliz.Lit or the original Feliz

I think being able to write HTML in F# is hugely beneficial for Fable apps but we need to focus on integration and minimize the steps for that πŸ‘

I think I'll spend some time here to put out resources regarding these, either docs or samples and blog entries if the information is there hopefully it will be easier for new users to pick up

I've published the packages with the new names. Note the package ids are prefixed with Fable. but the projects are Lit/Lit.React/Lit.Elmish/Lit.Feliz.

I'll update the haunted bindings to use lit2 (remove the virtual components, /sadpanda) and the dotnet templates as well

Thank you very much for your time on this! I really appreciate it I'll try to make it worth it πŸ˜† with more content targeted to userland

AngelMunoz commented 3 years ago

I updated all of the samples and templates plus a blog post :) at least what I have been sampling hasn't broken with lit-html v2

also btw I did some cursed experiments here to see how dificult class style components with lit can be (i.e the the standard way) because some people (outside F#) really seem to like the JSX style but I don't think I'll pursue that goal too much I think at least for now the standard way for F# (data and functions) is good enough

in case you want to check those as well here they are

https://github.com/AngelMunoz/cursed-lit-experiments

Edit: I even got the "reactive controllers" feature from Lit@2 working

alfonsogarciacaro commented 3 years ago

In principle it shouldn't be an issue to use classes for web components. Although I'm checking Lit components and they seem to be very Typescript-oriented so not sure whether they will be a good fit for F#, moreover as Lit's website recommends mixins and controllers to organize (which reminds me a bit of the problems we had with React classes and Higher-order-components). We also need to take into account the upgrade path: that is, how many changes users need to do to convert a piece of their App (I assume an Elmish-like MVU component) into a web component,

Fable 3.3 will include decorators, not exactly the same as in Typescript but will work in a similar fashion. We could use to define web components out of a function as Haunted seems to do.

AngelMunoz commented 3 years ago

The javascript docs are quite late but they are comming, AFAIK Lit will support javascript only components as well, the decorators get translated into static properties I had some success on that repository I think If you don't mind we could release class based bindings as part of an extensions package, I don't think people will opt in for that too much

Haunted actually uses the same approach I had in mind with https://github.com/davedawkins/Sutil/issues/32 albeit in a more formalized fashion, I think if we wanted to go that route we could simply provide our own factory function that does that I'll see if I can get something later on but I feel we're very much on route to have most things supported in this package

AngelMunoz commented 3 years ago

Regarding function based components with Lit@2 check this https://github.com/alfonsogarciacaro/Fable.Lit/pull/7#issuecomment-909549075

AngelMunoz commented 2 years ago

The javascript docs are quite late but they are comming

The Lit JS documentation samples is in preview you can access it with the following link https://lit.dev/docs/?mods=jsSamples

alfonsogarciacaro commented 2 years ago

Let's keep the Lit.Feliz project in the repo just in case, but we can "deprecate" it by removing it from the sample and the readme. We can revisit this if we still feel the need of using Lit with a typed API.

AngelMunoz commented 2 years ago

sounds good, I will update the dotnet templates as well