Skip to content

Commit

Permalink
WIP Update to elmish 4
Browse files Browse the repository at this point in the history
  • Loading branch information
roboz0r committed Jul 4, 2024
1 parent cfc422c commit 98de8f6
Show file tree
Hide file tree
Showing 9 changed files with 648 additions and 87 deletions.
10 changes: 6 additions & 4 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@
"isRoot": true,
"tools": {
"fable": {
"version": "3.6.1",
"version": "4.19.3",
"commands": [
"fable"
]
],
"rollForward": false
},
"femto": {
"version": "0.9.0",
"version": "0.19.0",
"commands": [
"femto"
]
],
"rollForward": false
}
}
}
2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "6.0.100",
"version": "8.0.300",
"rollForward": "minor"
}
}
664 changes: 609 additions & 55 deletions package-lock.json

Large diffs are not rendered by default.

25 changes: 13 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,22 @@
"docs:deploy": "npm run docs && gh-pages -d build/docs"
},
"dependencies": {
"lit": "^2.0.0",
"react": "^17.0.2",
"react-dom": "^17.0.2"
"lit": "^3.1.4",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@web/test-runner": "^0.13.18",
"@web/test-runner-commands": "^0.5.13",
"bulma": "^0.9.3",
"@web/test-runner": "^0.18.2",
"@web/test-runner-commands": "^0.9.0",
"@web/test-runner-playwright": "^0.11.0",
"bulma": "^1.0.1",
"fable-publish-utils": "^2.2.0",
"gatsby-remark-vscode": "^3.3.0",
"gh-pages": "^3.2.3",
"nacara": "^1.0.0-beta-020",
"nacara-layout-standard": "^1.0.0-beta-014",
"shx": "^0.3.3",
"vite": "^2.6.14",
"gatsby-remark-vscode": "^3.3.1",
"gh-pages": "^6.1.1",
"nacara": "^1.8.0",
"nacara-layout-standard": "^1.8.0",
"shx": "^0.3.4",
"vite": "^5.3.3",
"vscode-theme-onelight": "github:akamud/vscode-theme-onelight"
}
}
2 changes: 1 addition & 1 deletion sample/Clock.fs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ let init() =
|> IntervalId
|> dispatch

Model.Empty, Cmd.ofSub subscribe
Model.Empty, Cmd.ofEffect subscribe

let update msg model =
match msg with
Expand Down
20 changes: 12 additions & 8 deletions src/Lit.Elmish/Lit.Elmish.fs
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ module Program =
criteria, fun model -> handler' model; handler model)

/// Adds a cumulative subscription
let addSubscription (subscribe: 'model -> Cmd<'msg>) (program: Program<'arg, 'model, 'msg, 'view>): Program<'arg, 'model, 'msg, 'view> =
let addSubscription (subscribe: 'model -> Sub<'msg>) (program: Program<'arg, 'model, 'msg, 'view>): Program<'arg, 'model, 'msg, 'view> =
program
|> Program.mapSubscription (fun subscribe' model ->
Cmd.batch [
Sub.batch [
subscribe' model
subscribe model
])
Expand All @@ -64,9 +64,13 @@ module Program =
let mutable disp: IDisposable option = None
program
|> addSubscription (fun model ->
Cmd.ofSub (fun dispatch ->
disp <- subscribe model dispatch |> Some
)
[
[ Guid.NewGuid().ToString() ],
(fun dispatch ->
let d = subscribe model dispatch
disp <- Some d
d)
]
)
|> addTerminationHandler (fun _ ->
disp |> Option.iter (fun d -> d.Dispose()))
Expand Down Expand Up @@ -117,10 +121,10 @@ module LitElmishExtensions =

let mapSetState _setState = obs.SetState

let mapSubscribe subscribe model =
let mapSubscribe (subscribe: ('State -> Sub<'Msg>)) model =
match model with
| Active model -> subscribe model |> Cmd.map UserMsg
| Inactive -> Cmd.none
| Active model -> subscribe model |> Sub.map "" UserMsg
| Inactive -> Sub.Empty

let mapTermination (criteria, handler) =
(function Stop -> true | UserMsg msg -> criteria msg),
Expand Down
2 changes: 1 addition & 1 deletion src/Lit.Elmish/Lit.Elmish.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<Content Include="*.fsproj; *.fs" PackagePath="fable\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Fable.Elmish" Version="4.0.0-beta-3" />
<PackageReference Include="Fable.Elmish" Version="4.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Lit\Lit.fsproj" />
Expand Down
8 changes: 4 additions & 4 deletions src/Lit/Lit.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@
<Content Include="*.fsproj; *.fs; package.json" PackagePath="fable\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FSharp.Core" Version="5.0.0" />
<PackageReference Include="Fable.Core" Version="3.6.0" />
<PackageReference Include="Fable.Browser.Dom" Version="2.5.0" />
<PackageReference Include="Fable.Promise" Version="3.1.0" />
<PackageReference Include="FSharp.Core" Version="8.0.300" />
<PackageReference Include="Fable.Core" Version="4.3.0" />
<PackageReference Include="Fable.Browser.Dom" Version="2.17.0" />
<PackageReference Include="Fable.Promise" Version="3.2.0" />
</ItemGroup>
</Project>
2 changes: 1 addition & 1 deletion test/Test.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<ProjectReference Include="..\src\Lit.Test\Lit.Test.fsproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Fable.Elmish" Version="4.0.0-beta-3" />
<!-- <PackageReference Include="Fable.Elmish" Version="4.0.0-beta-3" /> -->
<!-- <PackageReference Include="Fable.Lit.Test" Version="1.0.0-rc-001" /> -->
</ItemGroup>
</Project>

9 comments on commit 98de8f6

@juselius
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@roboz0r I quickly glanced through your changes for Elmish 4, and as far as I can tell the changes look pretty solid. Is there a reason for it being WIP? What's missing? I'm planning to pull your changes and add support for Lit ContextProvider and ContextConsumer.

@roboz0r
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@juselius I think these changes were essentially fine however the other issue I was having with the library was the inline css styles didn't work. I wasn't getting errors they just weren't appearing at all. I couldn't understand what was going on with the css-related code at all and it seemed like the repo was otherwise inactive so I dropped what I was working on.

@juselius
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the heads up @roboz0r! I have a vague memory of having trouble with the inline css also in earlier versions, but I don't remember the details. I have grown very fond of Lit, and we'll try to breath new life into it now that Alfonso seems to have gone AWOL.

@roboz0r
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@juselius Just FYI, I identified the bug breaking styles over in the Fable compiler and trying to find a solution.

@juselius
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Brilliant @roboz0r! Good work Sherlock :)

I have added basic support for Lit contexts in my Fable.Lit fork. The implementation is usable, but not entirely complete. I'll try to add the missing pieces during the weekend.

@roboz0r
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In debugging this I ended up producing a class based wrapper over Lit which I think I ultimately prefer as it follows the Lit docs much more closely.

[<AttachMembers>]
type SimpleGreeting() =
    inherit LitElement()

    static member properties =
        PropertyDeclarations.create [ "name", (PropertyDeclaration<string>(state = false)) ]

    static member styles =
        css $""":host {{ color: {Color.red}; font-weight: bold; }}"""

    member val name = "World" with get, set

    override this.render() =
        html $"""<div><h1>Hello, {this.name}!</h1></div>"""

In a brief reading of contexts it seems like that could become the storage for the state root for a Lit app serving a similar purpose to the Elmish style, maintaining an Elmish MVU pattern to these class based components would probably look more like Elmish.WPF where the View part is replaced by Bindings to a stateful component. I'm curious what you think

@juselius
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very good!

I think there is ample room for both approaches, depending on preferences. I came to React and Lit via Fable (my only real experience with JS is writing F# bindings). For a long time I thought that the Fable.Lit bindings closely followed the Lit API, because I never actually read the Lit docs...

What I like about the Fable.Lit bindings is that they closely mimic React, making it quite easy to convert between the two. In general, I prefer functional style over OO, as OOP has a much higher cognitive load on my brain.

I completed the Context implementation in Fable.Lit (not fully tested yet). We use contexts to store Elmish/Reducer models and dispatch functions, allowing us easily to issue updates between components in separate branches of the DOM tree. It's a nice way to manage complexity in large apps. Here is a nice reference: https://react.dev/learn/scaling-up-with-reducer-and-context.

@roboz0r
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I try to go with a functional first style. Wherever possible, write in terms of immutable data and pure functions but where mutable state is required, like with components, I prefer to switch to an OO style. That's a signal for me that I need to state thinking statefully rather than in terms of data flow.

I need to build something larger to get an idea how it works but I think making a distinction between:

  • Templates: Just functions and args
  • Components: Objects with internal state and lifecycle events

Might be the sweet spot.

Reading the Reducer + Context docs is almost exactly what I had in mind, though I'd prefer to still pass in the context object rather than implicitly getting it via useContext. It's also similar to the react components and Feliz.UseElmish hooks.

@juselius
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that passing the context explicitly is clearer, but I justify the useContext() approach by thinking of it as a Reader monad, hehe.

Please sign in to comment.