Closed AngelMunoz closed 3 years ago
Regarding the "Function" based components that rely on lit only check this stackblitz
https://stackblitz.com/edit/js-rh5qqq?file=index.js
I'm not sure if fable can treat classes as values, but one thing is for sure it has to be a class because you need to extend LitElement
which also inherits from HTMLElement
which... can only be inherited by classes AFAIK but if we can include that JS script/translate to fable we should be good to go and have around 80 - 90% Lit support besides class based bindings that might be missing
we could ditch haunted completely given that we already have hooks with directives (the only one missing would be useController
)
I'll close this based the discussion we had on #8
Thanks a lot for this @AngelMunoz! Yes, if we're going to provide bindings for lit-html we should also provide bindings for lit-element (still unsure what are the differences between lit-element and @lit/reactive-element, maybe we just need the latter?). Still wondering if we should explore the function approach before trying to use the class. With decorators, we can just build the JS class on the fly as haunted does or have a class with the common functionality and inherit with the render function as the HookDirective does. The new decorators feature in fable 3.3 is going to allow reflection info on the method. But F#/.NET attributes only accept literal values (strings, numbers and booleans, basically), so for styles and properties initialization we would need something like a hook:
[<LitElement("my-component")>]
let MyComponent () =
let props = LitElement.init initStyles (fun () ->
{| name = Prop("default")
age = Prop(5, attribute=true, hasChanged= fun x y -> x < y) |} // Sadly, you cannot be younger, so age only changes up
html $"""<p>Name: {props.name.Value} ({props.age.Value} years old)</p>"""
This should appear on top of the function and we can make a first run of the function to get the styles and properties. Then we can attach them to the generated class. For lifecycle we can just use the effects hooks. What do you think?
I found the constructors for Number, Object and similar ones but Didn't find String, should we add that in Fable.Core.JS?
I think I was trying to make devs just use F# instead, but we've already added other constructors with F#/.NET equivalents, so probably we can add JS.String as well for completeness.
still unsure what are the differences between lit-element and @lit/reactive-element, maybe we just need the latter?
ReactiveElement
is the base class for LitElement
I think for our purposes we can simply use the LitElement as our base class, as long as we are able to provide the static property properties
we should be able to "react" to changes when consumers change properties or attributes (if the component autor allows it)
or have a class with the common functionality and inherit with the render function as the HookDirective does.
I think the link is pointing to this PR, I think that would be the ideal way to do it taking the example on the stackblitz link above this is the key behavior
const cls = class extends LitElement {
// react to changes in the attributes
render() {
const fnArgs = Object.keys(opts?.props).reduce((curr, propName) => {
// Lit will observe properties as long as we specify them in
// the static properties geter
curr[propName] = this[propName];
// if the value is not set it's likely that the user specified
if (opts?.props[propName]?.attribute && !curr[propName]) {
// attribute property, the attribute property can be a string or a boolean
if (typeof opts?.props[propName]?.attribute === 'string') {
// if it's a string it means they used a different attributeNaeme
curr[propName] = this.getAttribute(attribute);
} else {
// otherwise it should be the provided key
curr[propName] = this.getAttribute(propName);
}
}
return curr;
}, {});
// a the end simply invoke
return viewFn(fnArgs);
}
};
then I add the static properties to cls
we could also add the static styles there
that can be an array of the tagged literal css
or even a constructable stylesheet (the ones I added to Fable.ShadowStyles)
I like the args first approach used by haunted rather than the LitElement.init because it kind of lets the author focus on the function rather than the configuration, the configuration can come later when you register the component (because you can't skip that anyways either by decorator or by hand) one thing we should make very very clear to users is that attributes and properties are always optional (if we go for the args first approach) because there is no mechanism that can force a consumer to specify a particular attribute/property
for the configuration I've been thinking over the last two days how to make it more... F#'ish, I was thinking about using a DU somewhat like this
type AttributeConverter =
| Complex of {| fromAttribute: string option -> obj option -> obj; toAttribute: obj -> obj -> obj |}
| Simple of string option -> obj option -> obj
type Property =
| Internal // { state: true }
| Attribute // { attribute: true } -> 'myName' in html
| CustomAttribute of string // { attribute: differentName }
| Type of obj // { type: Number }
| HasChanged of obj -> obj -> bool
| Converter of AttributeConverter
| Reflective // { reflect: true }
| NoAccessor // { noAcessor: true}
let props = [
"age", [ Internal ] // { age: { state: true } }
"color",
[ Type(JS.Object);
Converter (Simple(fun color -> stringToColor color)) ]
// { color: { type: Object, converter: (color) => stringToColor(color) } }
]
and then just create the static props for the user with the given definitions
defineComponent ("my-component", MyComponent, props)
I guess we can use the hook to do that or a mix of both approaches?
I'm fairly unfamilar still of how those work (kinda gave me the idea after looking at decompiled source but haven't been able to dive deep with them)
I think we can then re-open this one and add the missing things for the Function based Lit components
defineComponent ("my-component", MyComponent, props)
Oh something that is missing here as well is the ability to turn on/off the shadow DOM for a particular component which is crucial until constructable stylesheets arrive in all browsers and to have interop with existing css frameworks like boostrap/bulma/etc
which I addresed it on the stackblitz by adding the props object into the options and then picking the props from the options... it's kind of weird
reference for the property declaration interface https://github.com/lit/lit/blob/20b4dd3fbfc3b8313be8fb98af61d222a82f96d1/packages/reactive-element/src/reactive-element.ts#L111
I'm doing some progress on the attributes as I thought they could be used but the functions for the HasChanged
case get curried and that won't work with Lit, here's a Fable Repl is there something I can use to compile them uncurried?
Sorry, I've fixed the links in my previous comment :)
I think for our purposes we can simply use the LitElement as our base class,
Yes, it seems ReactiveElement
is just LitElement minus the rendering with lit-html. I guess it's designed to be wrapper of components in other frameworks: React, Vue, Svelte.
I like the args first approach used by haunted rather than the LitElement.init because it kind of lets the author focus on the function rather than the configuration, the configuration can come later when you register the component
That's true, although I see a couple of problems with using the arguments for the properties in Web Components:
one thing we should make very very clear to users is that attributes and properties are always optional
If I understand the Lit examples correctly, if you provide a default value you don't need to deal with undefined options. This would be another point in making the props available trough an init function.
- First, this way Fable devs would understand the function can be called directly (as we do with React components), but if I understand it correctly, web components must be instantiated in HTML.
That's a fair point,I think this will require some guidance regarding on "how-to register components"
because even if they have unit as a parameter, some people might feel tempted to call functions from other modules into another function which might break stuff since some components will depend on the correct this
value
that's the reason why on the Fable.Lit.Templates
I declared such functions private and only exposed a register
one precisely because components as they are must be created via html or document.create('my-component')
If I understand the Lit examples correctly, if you provide a default value you don't need to deal with undefined options.
that's true, but at runtime consumers can assign null/undefined to properties and remove the attributes as well even with the init approach we would need to remember users that the runtime doesn't play by the F# rules (which is somewhat similar to the C# <-> F# interop)
@AngelMunoz It still needs some work but I added a draft implementation of the LitElement
decorator so we can convert render functions to web components with an init function and react-style hooks. This is what it looks like, I think it's a nice way of defining web components and it makes for a nice transition of an Elmish component to a web component, what do you think?
Hey @alfonsogarciacaro the only thing that might be confusing here is the translation between camelCase and dash-case for attributes
in your example you used hourColor="red"
this will always be converted to lowercase by browsers, for properties that's fine because lit uses .hourColor={color}
for attributes it is expected to be something along hour-color={strColor}
But based on the comments of the PR, I guess with the Prop optional arguments this can be fixed Prop("red", customAttr = "hour-color")
or somewhat along those lines
This is an intial implementation of the Lit based (class components) in a separate package, these should give us a head start on "standard" Lit, we can refine this PR with extra things
For the LitElement class I didn't add every member since most of those are for internal/advanced use cases I don't think they're needed at least not right now
Also we can scrap this completely and do it on a separate repository