Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
4 changes: 2 additions & 2 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
"isRoot": true,
"tools": {
"fable": {
"version": "4.9.0",
"version": "5.0.0-rc.7",
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

There is an official fable 5 already published.

"commands": [
"fable"
]
}
}
}
}
6 changes: 3 additions & 3 deletions Build.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
<Compile Include="Build.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Fake.Core.Target" Version="6.0.0" />
<PackageReference Include="Fake.DotNet.Cli" Version="6.0.0" />
<PackageReference Include="Fake.IO.FileSystem" Version="6.0.0" />
<PackageReference Include="Fake.Core.Target" Version="6.1.4" />
<PackageReference Include="Fake.DotNet.Cli" Version="6.1.4" />
<PackageReference Include="Fake.IO.FileSystem" Version="6.1.4" />
</ItemGroup>
</Project>
4 changes: 2 additions & 2 deletions global.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "8.0.100",
"version": "10.0.201",
"rollForward": "latestMinor"
}
}
}
20 changes: 12 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
{
"type": "module",
"private": true,
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"scripts": {
"prestart": "dotnet tool restore",
"start": "dotnet fable watch ./src/ElmishStore.Example --outDir ./src/ElmishStore.Example/.fable-build --run vite"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.17",
"postcss": "^8.4.33",
"@tailwindcss/postcss": "^4.2.2",
"@vitejs/plugin-react": "^6.0.1",
"autoprefixer": "^10.4.27",
"postcss": "^8.5.9",
"remotedev": "^0.2.9",
"tailwindcss": "^3.4.1",
"vite": "^5.0.11"
"tailwindcss": "^4.2.2",
"vite": "^8.0.8"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"use-sync-external-store": "^1.2.0"
"react": "^19.2.5",
"react-dom": "^19.2.5",
"use-sync-external-store": "^1.6.0"
}
}
4 changes: 2 additions & 2 deletions postcss.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export default {
plugins: {
tailwindcss: {},
"@tailwindcss/postcss": {},
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Do we still need postcss for tailwind 4?

autoprefixer: {},
},
}
}
2 changes: 1 addition & 1 deletion src/ElmishStore.Example/App.fs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ open Feliz

ReactDOM
.createRoot(Browser.Dom.document.getElementById "elmish-app")
.render (React.strictMode [ View.AppView() ])
.render (React.StrictMode [ View.AppView() ])
4 changes: 2 additions & 2 deletions src/ElmishStore.Example/ElmishStore.Example.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@
<EmbeddedResource Remove="public\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Fable.Elmish.Debugger" Version="4.0.0" />
<PackageReference Include="Fable.Elmish.Debugger" Version="4.2.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ElmishStore\ElmishStore.fsproj" />
</ItemGroup>
</Project>
</Project>
9 changes: 5 additions & 4 deletions src/ElmishStore.Example/ModelStore.fs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ open Elmish.Debug

let store =
Program.mkProgram init update (fun _ _ -> ())
|> ElmishProgram.setup "main"
#if DEBUG
|> Program.withConsoleTrace
|> Program.withDebugger
|> ElmishProgram.mapProgram Program.withConsoleTrace
|> ElmishProgram.mapProgram Program.withDebugger
#endif
|> ElmishStore.createStore "main"
|> ElmishProgram.run

[<Hook>]
let useSelector (selector: Model -> 'a) = React.useElmishStore (store, selector)
Expand All @@ -26,4 +27,4 @@ let useSelector (selector: Model -> 'a) = React.useElmishStore (store, selector)
let useSelectorMemoized (memoizedSelector: Model -> 'a) =
React.useElmishStoreMemoized (store, memoizedSelector)

let dispatch = store.Dispatch
let dispatch = store.Dispatch
5 changes: 2 additions & 3 deletions src/ElmishStore.Example/styles/styles.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
@charset "utf-8";

@tailwind base;
@tailwind components;
@tailwind utilities;
@import "tailwindcss";
@source "../.fable-build";
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

explain that change

158 changes: 108 additions & 50 deletions src/ElmishStore/ElmishStore.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ namespace ElmishStore

open Elmish
open Fable.Core
open ElmishStore
open System.Collections.Generic

type ElmishStore<'model, 'msg> = {
Expand All @@ -12,61 +11,86 @@ type ElmishStore<'model, 'msg> = {
Subscribe: UseSyncExternalStoreSubscribe
}

type private StoreState<'arg, 'model, 'msg> = {
type private StoreState<'model, 'msg> = {
Store: ElmishStore<'model, 'msg>
SetTermination: bool -> unit
}

module ElmishStore =
type private RuntimeState<'model, 'msg> = {
UniqueName: string
mutable State: 'model option
mutable FinalDispatch: Dispatch<'msg> option
mutable ShouldTerminate: bool
Subscribers: ResizeArray<unit -> unit>
mutable PreviousStore: StoreState<'model, 'msg> option
}

let mutable private stores: Dictionary<string, obj> = Dictionary<string, obj>()
type ElmishProgramState<'arg, 'model, 'msg, 'view> = {
Program: Program<'arg, 'model, 'msg, 'view>
Run: Program<'arg, 'model, 'msg, 'view> -> 'arg -> ElmishStore<'model, 'msg>
}

let private initiate
uniqueName
(arg: 'arg)
(program: Program<'arg, 'model, 'msg, unit>)
(getState: unit -> 'model option)
=
module private Internal =

let mutable state = getState ()
let mutable finalDispatch = None
let mutable shouldTerminate = false
let mutable stores: Dictionary<string, obj> = Dictionary<string, obj>()

let setTermination should = shouldTerminate <- should
let tryGetStoreState (uniqueName: string) =
if stores.ContainsKey(uniqueName) then
Some(stores[uniqueName] |> unbox<StoreState<'model, 'msg>>)
else
None

module ElmishProgram =

let setup (uniqueName: string) (program: Program<'arg, 'model, 'msg, 'view>) =
let runtime = {
UniqueName = uniqueName
State = None
FinalDispatch = None
ShouldTerminate = false
Subscribers = ResizeArray()
PreviousStore = None
}

let dispatch msg =
match finalDispatch with
match runtime.FinalDispatch with
| Some finalDispatch -> finalDispatch msg
| None -> failwith "You're using initial dispatch. That shouldn't happen."

let subscribers = ResizeArray<unit -> unit>()

let subscribe callback =
subscribers.Add(callback)
fun () -> subscribers.Remove(callback) |> ignore
runtime.Subscribers.Add(callback)
fun () -> runtime.Subscribers.Remove(callback) |> ignore

let mapSetState setState model dispatch =
setState model dispatch
let oldModel = state
state <- Some model
finalDispatch <- Some dispatch
let oldModel = runtime.State
runtime.State <- Some model
runtime.FinalDispatch <- Some dispatch

match runtime.PreviousStore with
| Some previousStore ->
previousStore.SetTermination true
runtime.PreviousStore <- None
| None -> ()

// Skip re-renders if model hasn't changed
if not (obj.ReferenceEquals(model, oldModel)) then
subscribers |> Seq.iter (fun callback -> callback ())
runtime.Subscribers |> Seq.iter (fun callback -> callback ())

let mapInit userInit arg =
if state.IsSome then state.Value, Cmd.none else userInit arg
match runtime.PreviousStore with
| Some previousStore ->
let model = previousStore.Store.GetModel()
runtime.State <- Some model
model, Cmd.none
| None -> userInit arg

let mapTermination (predicate, terminate) =
let pred msg = predicate msg || shouldTerminate
let pred msg = predicate msg || runtime.ShouldTerminate
pred, terminate

program
|> Program.map mapInit id id mapSetState id mapTermination
|> Program.runWith arg

let getState () =
match state with
match runtime.State with
| Some state -> state
| None -> failwith "State is not initialized. That shouldn't happen."

Expand All @@ -76,64 +100,98 @@ module ElmishStore =
Subscribe = UseSyncExternalStoreSubscribe subscribe
}

let storeState = {
Store = store
SetTermination = setTermination
let preparedProgram =
program
|> Program.map mapInit id id mapSetState id mapTermination

let run (program: Program<'arg, 'model, 'msg, 'view>) (arg: 'arg) =
runtime.PreviousStore <- Internal.tryGetStoreState runtime.UniqueName
runtime.ShouldTerminate <- false

let setTermination should = runtime.ShouldTerminate <- should

program
|> Program.runWith arg

let storeState = {
Store = store
SetTermination = setTermination
}

Internal.stores[runtime.UniqueName] <- box storeState
store

{
Program = preparedProgram
Run = run
}

stores[uniqueName] <- box storeState
store
let mapProgram
(mapper: Program<'arg, 'model, 'msg, 'view> -> Program<'arg, 'model, 'msg, 'view>)
(elmishProgram: ElmishProgramState<'arg, 'model, 'msg, 'view>)
=
{
Program = mapper elmishProgram.Program
Run = fun program arg -> elmishProgram.Run program arg
}

let createStoreWith uniqueName (arg: 'arg) (program: Program<'arg, 'model, 'msg, unit>) =
let runWith (arg: 'arg) (elmishProgram: ElmishProgramState<'arg, 'model, 'msg, 'view>) =
elmishProgram.Run elmishProgram.Program arg

let getState =
if stores.ContainsKey(uniqueName) then
let storeState = stores[uniqueName] |> unbox<StoreState<'arg, 'model, 'msg>>
storeState.SetTermination true
(fun () -> Some(storeState.Store.GetModel()))
else
(fun () -> None)
let inline run (elmishProgram: ElmishProgramState<unit, 'model, 'msg, 'view>) =
runWith () elmishProgram

initiate uniqueName arg program getState
let createStoreWith (uniqueName: string) (arg: 'arg) (program: Program<'arg, 'model, 'msg, 'view>) =
program
|> setup uniqueName
|> runWith arg

let inline createStore uniqueName program : ElmishStore<'model, 'msg> =
let inline createStore (uniqueName: string) (program: Program<unit, 'model, 'msg, 'view>) =
createStoreWith uniqueName () program

module ElmishStore =

let createStoreWith (uniqueName: string) (arg: 'arg) (program: Program<'arg, 'model, 'msg, 'view>) =
ElmishProgram.createStoreWith uniqueName arg program

let inline createStore (uniqueName: string) (program: Program<unit, 'model, 'msg, 'view>) : ElmishStore<'model, 'msg> =
ElmishProgram.createStore uniqueName program

[<Erase>]
type ElmishStore =

static member Create
(
program: Program<'arg, 'model, 'msg, unit>,
arg: 'arg,
uniqueName
uniqueName: string
) : ElmishStore<'model, 'msg> =
ElmishStore.createStoreWith uniqueName arg program

static member inline Create(program: Program<unit, 'model, 'msg, unit>, uniqueName) =
static member inline Create(program: Program<unit, 'model, 'msg, unit>, uniqueName: string) =
ElmishStore.Create(program, (), uniqueName)

static member inline Create
(
init: 'arg -> 'model * Cmd<'msg>,
update: 'msg -> 'model -> 'model * Cmd<'msg>,
arg: 'arg,
uniqueName
uniqueName: string
) =
ElmishStore.Create((Program.mkProgram init update (fun _ _ -> ())), arg, uniqueName)

static member inline Create
(
init: unit -> 'model * Cmd<'msg>,
update: 'msg -> 'model -> 'model * Cmd<'msg>,
uniqueName
uniqueName: string
) =
ElmishStore.Create(Program.mkProgram init update (fun _ _ -> ()), uniqueName)

static member inline Create
(
init: 'model * Cmd<'msg>,
update: 'msg -> 'model -> 'model * Cmd<'msg>,
uniqueName
uniqueName: string
) =
ElmishStore.Create(Program.mkProgram (fun () -> init) update (fun _ _ -> ()), uniqueName)
10 changes: 5 additions & 5 deletions src/ElmishStore/ElmishStore.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<PackageTags>fsharp;fable;react;elmish</PackageTags>
<Authors>Łukasz Krzywizna</Authors>
<Company>SelectView Data Solutions</Company>
<Version>0.1.0</Version>
<Version>0.2.0</Version>
<TargetFramework>net8.0</TargetFramework>
<PackageReadmeFile>readme.md</PackageReadmeFile>
<RepositoryUrl>https://github.com/SelectViewData/elmish-store</RepositoryUrl>
Expand All @@ -28,8 +28,8 @@
<Content Include="..\..\readme.md" Pack="true" PackagePath="\"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Fable.Elmish" Version="4.1.0" />
<PackageReference Include="Feliz" Version="2.7.0" />
<PackageReference Include="Feliz.CompilerPlugins" Version="2.2.0" />
<PackageReference Include="Fable.Elmish" Version="5.0.2" />
<PackageReference Include="Feliz" Version="3.2.0" />
<PackageReference Include="Feliz.CompilerPlugins" Version="3.1.0" />
</ItemGroup>
</Project>
</Project>
Loading