Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add controllers in LitElement.init #27

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions sample/Clock.fs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ open Lit.Elmish
module Helpers =
let hmr = HMR.createToken()

type MouseController =
inherit ReactiveController
abstract x: float
abstract y: float

[<Import("MouseController", from="./controllers.js"); Emit("new $0($1)")>]
let createMouseController (host: LitElement): MouseController = jsNative

type Time =
| Hour of int
| Minute of int
Expand Down Expand Up @@ -125,10 +133,12 @@ let select options selected dispatch =
</div>
"""

let initEl (config: LitConfig<_>) =
let initEl (config: LitConfig<_,_>) =
let split (str: string) =
str.Split(',') |> Array.map (fun x -> x.Trim()) |> Array.toList

config.controllers <- {| mouse = Controller.Of(createMouseController) |}

config.props <-
{|
hourColor = Prop.Of("lightgreen", attribute="hour-color")
Expand Down Expand Up @@ -176,13 +186,14 @@ let initEl (config: LitConfig<_>) =

[<LitElement("my-clock")>]
let Clock() =
let host, props = LitElement.init initEl
let hourColor = props.hourColor.Value
let colors = props.minuteColors.Value
let host = LitElement.init initEl
let hourColor = host.props.hourColor.Value
let colors = host.props.minuteColors.Value

Hook.useHmr(hmr)
let model, dispatch = Hook.useElmish(init, update)
let time = model.CurrentTime
let mouse = host.controllers.mouse.Value

html $"""
<svg viewBox="0 0 100 100"
Expand All @@ -208,12 +219,12 @@ let Clock() =
</svg>

<div class="container">
<p id="message">This is a clock</p>
<p id="message">This is a clock: {mouse.x}, {mouse.y}</p>
{select colors model.MinuteHandColor (fun color ->
host.renderRoot.querySelector("#message").textContent <- "Color selected"
List.tryFindIndex ((=) color) colors
|> Option.iter (fun i ->
props.evenColor.Value <- (i + 1) % 2 = 0)
host.props.evenColor.Value <- (i + 1) % 2 = 0)
MinuteHandColor color |> dispatch)}
</div>
"""
Expand Down
48 changes: 48 additions & 0 deletions sample/controllers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { LitElement } from "lit";

export class MouseController {

constructor(host) {
this.host = host;
host.addController(this);
this.x = 0;
this.y = 0;
}

_updatePosition =
/**
*
* @param {MouseEvent} event
*/
(event) => {
this.x = event.x;
this.y = event.y;
this.host.requestUpdate();
};

hostConnected() {
window.addEventListener('mousemove', this._updatePosition);
}

hostDisconnected() {
window.removeEventListener('mousemove', this._updatePosition);
}

}



class MyControlledElement extends LitElement {

constructor() {
super();
this.mouseCtrl = new MouseController(this);
}

render() {
return `Cursor position: ${this.mouseCtrl.x} - ${this.mouseCtrl.y}`;
}

}

customElements.define("my-controlled-element", MyControlledElement);
73 changes: 73 additions & 0 deletions src/Lit/Lit.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ namespace Lit
open System
open Browser.Types
open Fable.Core
open Lit

module Types =
type RefValue<'T> =
Expand Down Expand Up @@ -44,6 +45,78 @@ type TemplateResult =
type CSSResult =
abstract cssText: string
abstract styleSheet: CSSStyleSheet

// https://lit.dev/docs/api/controllers/#ReactiveController
// make an empty interface, all of the
// reactive controller methods are optional anyways
// and make each method implement the base
/// Common interface for Reactive Controller Methods
type ReactiveControllerBase = interface end

type ReactiveHostConnected =
inherit ReactiveControllerBase

/// <summary>
/// Called when the host is connected to the component tree. For custom
/// element hosts, this corresponds to the `connectedCallback()` lifecycle,
/// which is only called when the component is connected to the document.
/// </summary>
abstract hostConnected: unit -> unit

type ReactiveHostDisconnected =
inherit ReactiveControllerBase
/// <summary>
/// Called when the host is disconnected from the component tree. For custom
/// element hosts, this corresponds to the `disconnectedCallback()` lifecycle,
/// which is called the host or an ancestor component is disconnected from the
/// document.
/// </summary>
abstract hostDisconnected: unit -> unit

type ReactiveHostUpdate =
inherit ReactiveControllerBase

/// <summary>
/// Called during the client-side host update, just before the host calls
/// its own update.
///
/// Code in `update()` can depend on the DOM as it is not called in
/// server-side rendering.
/// </summary>
abstract hostUpdate: unit -> unit

type ReactiveHostUpdated =
inherit ReactiveControllerBase

/// <summary>
/// Called after a host update, just before the host calls firstUpdated and
/// updated. It is not called in server-side rendering.
/// </summary>
abstract hostUpdated: unit -> unit

/// <summary>
/// A Reactive Controller is an object that enables sub-component code
/// organization and reuse by aggregating the state, behavior, and lifecycle
/// hooks related to a single feature.
///
/// Controllers are added to a host component, or other object that implements
/// the `ReactiveControllerHost` interface, via the `addController()` method.
/// They can hook their host components's lifecycle by implementing one or more
/// of the lifecycle callbacks, or initiate an update of the host component by
/// calling `requestUpdate()` on the host.
/// </summary>
type ReactiveController =
inherit ReactiveHostConnected
inherit ReactiveHostDisconnected
inherit ReactiveHostUpdate
inherit ReactiveHostUpdated

// https://lit.dev/docs/api/controllers/#ReactiveControllerHost
type ReactiveControllerHost =
abstract updateComplete: JS.Promise<bool>
abstract addController: ReactiveControllerBase -> unit
abstract removeController: ReactiveControllerBase -> unit
abstract requestUpdate: unit -> unit

type LitBindings =
/// <summary>
Expand Down
83 changes: 63 additions & 20 deletions src/Lit/LitElement.fs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ open Fable.Core.JsInterop
open Browser
open Browser.Types
open HMRTypes
open Lit

// LitElement should inherit HTMLElement but HTMLElement
// is still implemented as interface in Fable.Browser
Expand All @@ -20,6 +21,18 @@ type LitElement() =
member _.requestUpdate(): unit = jsNative
/// Returns a promise that will resolve when the element has finished updating.
member _.updateComplete: JS.Promise<unit> = jsNative
member _.addController(controller: ReactiveControllerBase): unit = jsNative
member _.removeController(controller: ReactiveControllerBase): unit = jsNative

// Compiler trick: we use a different generic type, but they both
// refer to the same imported type

[<Import("LitElement", "lit")>]
type LitElement<'Props, 'Ctrls>() =
inherit LitElement()
[<Emit("$0")>]
member _.props: 'Props = jsNative
member _.controllers: 'Ctrls = jsNative

module private LitElementUtil =
module Types =
Expand Down Expand Up @@ -128,8 +141,22 @@ and Prop<'T> internal (defaultValue: 'T, options: obj) =
[<Emit("$0{{ = $1}}")>]
member _.Value with get() = defaultValue and set(_: 'T) = ()

type Controller internal (init: LitElement -> ReactiveControllerBase) =
let mutable value = Unchecked.defaultof<ReactiveControllerBase>
member internal _.Init(host) = value <- init host
member _.Value = value

/// Creates a controller out of an initialization function
static member Of<'T when 'T :> ReactiveControllerBase>(init: LitElement -> 'T) =
Controller<'T>(init)

and Controller<'T when 'T :> ReactiveControllerBase> (init) =
inherit Controller(fun host -> upcast init host)
member _.Value = base.Value :?> 'T

/// Configuration values for the LitElement instances
type LitConfig<'Props> =
type LitConfig<'Props, 'Ctrls> =
abstract controllers: 'Ctrls with get, set
///<summary>
/// An object containing the reactive properties definitions for the web components to react to changes
/// </summary>
Expand All @@ -151,23 +178,25 @@ type LitConfig<'Props> =
/// Whether the element should render to shadow or light DOM (defaults to true).
abstract useShadowDom: bool with get, set

type ILitElementInit<'Props> =
abstract init: initFn: (LitConfig<'Props> -> JS.Promise<unit>) -> LitElement * 'Props
type ILitElementInit<'Props, 'Ctrls> =
abstract init: initFn: (LitConfig<'Props, 'Ctrls> -> JS.Promise<unit>) -> LitElement<'Props, 'Ctrls>

type LitElementInit<'Props>() =
type LitElementInit<'Props, 'Ctrls>() =
let mutable _initPromise: JS.Promise<unit> = null
let mutable _useShadowDom = true
let mutable _ctrls = Unchecked.defaultof<'Ctrls>
let mutable _props = Unchecked.defaultof<'Props>
let mutable _styles = Unchecked.defaultof<CSSResult list>

member _.InitPromise = _initPromise

interface LitConfig<'Props> with
interface LitConfig<'Props, 'Ctrls> with
member _.controllers with get() = _ctrls and set v = _ctrls <- v
member _.props with get() = _props and set v = _props <- v
member _.styles with get() = _styles and set v = _styles <- v
member _.useShadowDom with get() = _useShadowDom and set v = _useShadowDom <- v

interface ILitElementInit<'Props> with
interface ILitElementInit<'Props, 'Ctrls> with
member this.init initFn =
_initPromise <- initFn this
Unchecked.defaultof<_>
Expand All @@ -176,13 +205,13 @@ type LitElementInit<'Props>() =
member _.hooks = failInit()

[<AbstractClass; AttachMembers>]
type LitHookElement<'Props>(initProps: obj -> unit) =
inherit LitElement()
type LitHookElement<'Props, 'Ctrls>(init: obj -> unit) =
inherit LitElement<'Props, 'Ctrls>()
let _hooks = HookContext(jsThis)
#if DEBUG
let mutable _hmrSub: IDisposable option = None
#endif
do initProps(jsThis)
do init(jsThis)

abstract renderFn: JS.Function with get, set
abstract name: string
Expand Down Expand Up @@ -218,8 +247,8 @@ type LitHookElement<'Props>(initProps: obj -> unit) =
|> Some
#endif

interface ILitElementInit<'Props> with
member this.init(_) = this :> LitElement, box this :?> 'Props
interface ILitElementInit<'Props, 'Ctrls> with
member this.init(_) = this :> LitElement<'Props, 'Ctrls>

interface IHookProvider with
member _.hooks = _hooks
Expand All @@ -245,7 +274,7 @@ type LitElementAttribute(name: string) =

config.InitPromise
|> Promise.iter (fun _ ->
let config = config :> LitConfig<obj>
let config = config :> LitConfig<obj, obj>

let styles =
if isNotNull config.styles then List.toArray config.styles |> Some
Expand Down Expand Up @@ -278,14 +307,28 @@ type LitElementAttribute(name: string) =
else
None, fun _ -> ()

let initCtrls =
if isNotNull config.controllers then
fun (host: LitElement) ->
JS.Constructors.Object.values(config.controllers)
|> Seq.iter (function
| :? Controller as ctrl -> ctrl.Init(host)
| _ -> ())
host?controllers <- config.controllers
else fun _ -> ()

let init host =
initProps host
initCtrls host

let classExpr =
let baseClass = jsConstructor<LitHookElement<obj>>
let baseClass = jsConstructor<LitHookElement<obj, obj>>
#if !DEBUG
emitJsExpr (baseClass, renderFn, initProps) HookUtil.RENDER_FN_CLASS_EXPR
emitJsExpr (baseClass, renderFn, init) HookUtil.RENDER_FN_CLASS_EXPR
#else
let renderRef = LitBindings.createRef()
renderRef.value <- renderFn
emitJsExpr (baseClass, renderRef, mi.Name, initProps) HookUtil.HMR_CLASS_EXPR
emitJsExpr (baseClass, renderRef, mi.Name, init) HookUtil.HMR_CLASS_EXPR
#endif

propsOptions |> Option.iter (fun props -> defineGetter(classExpr, "properties", fun () -> props))
Expand Down Expand Up @@ -370,16 +413,16 @@ module LitElementExtensions =
/// Initializes the LitElement instance and registers the element in the custom elements registry
/// </summary>
static member inline init(): LitElement =
jsThis<ILitElementInit<unit>>.init(fun _ -> Promise.lift ()) |> fst
upcast jsThis<ILitElementInit<unit, unit>>.init(fun _ -> Promise.lift ())

/// <summary>
/// Initializes the LitElement instance, reactive properties and registers the element in the custom elements registry
/// </summary>
static member inline init(initFn: LitConfig<'Props> -> unit): LitElement * 'Props =
jsThis<ILitElementInit<'Props>>.init(initFn >> Promise.lift)
static member inline init(initFn: LitConfig<'Props, 'Ctrls> -> unit): LitElement<'Props, 'Ctrls> =
jsThis<ILitElementInit<'Props, 'Ctrls>>.init(initFn >> Promise.lift)

/// <summary>
/// Initializes the LitElement instance, reactive properties and registers the element in the custom elements registry
/// </summary>
static member inline initAsync(initFn: LitConfig<'Props> -> JS.Promise<unit>): LitElement * 'Props =
jsThis<ILitElementInit<'Props>>.init(initFn)
static member inline initAsync(initFn: LitConfig<'Props, 'Ctrls> -> JS.Promise<unit>): LitElement<'Props, 'Ctrls> =
jsThis<ILitElementInit<'Props, 'Ctrls>>.init(initFn)
8 changes: 4 additions & 4 deletions test/HookTest.fs
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,10 @@ let DisposableContainer (r: ref<int>) =

[<LitElement("test-disposable")>]
let DisposableW () =
let _, props = LitElement.init(fun cfg ->
let host = LitElement.init(fun cfg ->
cfg.props <- {| r = Prop.Of(ref 0, attribute="") |}
)
let r = props.r.Value
let r = host.props.r.Value
let value, setValue = Hook.useState 5

Hook.useEffectOnce
Expand All @@ -132,10 +132,10 @@ let DisposableW () =

[<LitElement("test-disposable-container")>]
let DisposableContainerW () =
let _, props = LitElement.init(fun cfg ->
let host = LitElement.init(fun cfg ->
cfg.props <- {| r = Prop.Of(ref 0, attribute="") |}
)
let r = props.r.Value
let r = host.props.r.Value
let disposed, setDisposed = Hook.useState false

let body =
Expand Down
Loading