diff --git a/paket.dependencies b/paket.dependencies index 82e37f2b..81c1548d 100644 --- a/paket.dependencies +++ b/paket.dependencies @@ -31,10 +31,10 @@ nuget Fable.Browser.MediaQueryList #github elmish/react:master #github fulma/Fulma:1e763d852112307370675a0cf692415a53d1993f# -#github elmish/elmish:69569e39876213e91c283c1c76bbe0fb88fba28 - +github elmish/elmish:v3.x github fable-compiler/fable-promise:master github alfonsogarciacaro/Feliz.Engine:main +github alfonsogarciacaro/Feliz.Snabbdom:main github davedawkins/Feliz.Engine.Bulma:main github davedawkins/Sutil:main diff --git a/paket.lock b/paket.lock index a604768f..38933ae2 100644 --- a/paket.lock +++ b/paket.lock @@ -91,7 +91,20 @@ NUGET Feliz.CompilerPlugins (1.6) Fable.AST (>= 3.0) FSharp.Core (>= 4.7.2) + FsCheck (2.16) + FSharp.Core (>= 4.2.3) + FsCheck.NUnit (2.16) + FsCheck (2.16) + NUnit (>= 3.13.1 < 4.0) FSharp.Core (5.0.2) + Microsoft.NET.Test.Sdk (16.11) + Microsoft.NETCore.Platforms (5.0.2) + NETStandard.Library (2.0.3) + Microsoft.NETCore.Platforms (>= 1.1) + NUnit (3.13.2) + NETStandard.Library (>= 2.0) + NUnit3TestAdapter (4.0) + SourceLink.Create.CommandLine (2.8.3) Thoth.Elmish.Toast (2.1) Fable.Core (>= 3.0) Fable.Elmish (>= 3.0) @@ -101,11 +114,26 @@ NUGET Thoth.Json (6.0) Fable.Core (>= 3.1.6) FSharp.Core (>= 4.7.2) + Unquote (6.1) + FSharp.Core (>= 4.7.2) GITHUB + remote: elmish/elmish + FULLPROJECT (8354fe939afe8a5de633586915c7da31b29e162a) + Fable.Core + FsCheck + FsCheck.NUnit + FSharp.Core + Microsoft.NET.Test.Sdk + nunit + NUnit3TestAdapter + SourceLink.Create.CommandLine + Unquote remote: fable-compiler/fable-promise - FULLPROJECT (f5ad55f26b037d1c19865a96f9295a1eb4bb06e3) + FULLPROJECT (703b555263cafc86f90ce82a1405864443a041cc) remote: alfonsogarciacaro/Feliz.Engine FULLPROJECT (f08a3bd19ea854ace8b308c4eb2290c0c001e916) + remote: alfonsogarciacaro/Feliz.Snabbdom + FULLPROJECT (7b28ccbceebcc9b66a8cb67aab89b7ae9576dbfb) remote: davedawkins/Feliz.Engine.Bulma FULLPROJECT (e960f99e842b362f6cc54524d857d12557da520b) remote: davedawkins/Sutil diff --git a/src/Fable.Repl.Lib/Elmish.Snabbdom/Elmish.Snabbdom.HMR.fs b/src/Fable.Repl.Lib/Elmish.Snabbdom/Elmish.Snabbdom.HMR.fs new file mode 100644 index 00000000..9906c735 --- /dev/null +++ b/src/Fable.Repl.Lib/Elmish.Snabbdom/Elmish.Snabbdom.HMR.fs @@ -0,0 +1,351 @@ +// From https://github.com/elmish/hmr +namespace Elmish.Snabbdom.HMR + +open Fable.Core +open Fable.Core.JsInterop +open Browser +open Elmish + +module Bindings = + [] + type Status = + /// The process is waiting for a call to check (see below) + | [] Idle + /// The process is checking for updates + | [] Check + /// The process is getting ready for the update (e.g. downloading the updated module) + | [] Prepare + /// The update is prepared and available + | [] Ready + /// The process is calling the dispose handlers on the modules that will be replaced + | [] Dispose + /// The process is calling the accept handlers and re-executing self-accepted modules + | [] Apply + /// An update was aborted, but the system is still in it's previous state + | [] Abort + /// An update has thrown an exception and the system's state has been compromised + | [] Fail + + type ApplyOptions = + /// Ignore changes made to unaccepted modules. + abstract ignoreUnaccepted : bool option with get, set + /// Ignore changes made to declined modules. + abstract ignoreDeclined : bool option with get, set + /// Ignore errors throw in accept handlers, error handlers and while reevaluating module. + abstract ignoreErrored : bool option with get, set + /// Notifier for declined modules + abstract onDeclined : (obj -> unit) option with get, set + /// Notifier for unaccepted modules + abstract onUnaccepted : (obj -> unit) option with get, set + /// Notifier for accepted modules + abstract onAccepted : (obj -> unit) option with get, set + /// Notifier for disposed modules + abstract onDisposed : (obj -> unit) option with get, set + /// Notifier for errors + abstract onErrored : (obj -> unit) option with get, set + + + [] + type IHot = + + /// **Description** + /// Retrieve the current status of the hot module replacement process. + /// **Parameters** + /// + /// + /// **Output Type** + /// * `Status` + /// + /// **Exceptions** + /// + abstract status: unit -> Status + + /// **Description** + /// Accept updates for the given dependencies and fire a callback to react to those updates. + /// **Parameters** + /// * `dependencies` - parameter of type `U2` - Either a string or an array of strings + /// * `errorHandler` - parameter of type `(obj -> unit) option` - Function to fire when the dependencies are updated + /// **Output Type** + /// * `unit` + /// + /// **Exceptions** + /// + abstract accept: dependencies: U2 * ?errorHandler: (obj -> unit) -> unit + + /// **Description** + /// Accept updates for itself. + /// **Parameters** + /// * `errorHandler` - parameter of type `(obj -> unit) option` - Function to fire when the dependencies are updated + /// + /// **Output Type** + /// * `unit` + /// + /// **Exceptions** + /// + abstract accept: ?errorHandler: (obj -> unit) -> unit + + /// **Description** + /// Reject updates for the given dependencies forcing the update to fail with a 'decline' code. + /// **Parameters** + /// * `dependencies` - parameter of type `U2` - Either a string or an array of strings + /// + /// **Output Type** + /// * `unit` + /// + /// **Exceptions** + /// + abstract decline: dependencies: U2 -> unit + + /// **Description** + /// Reject updates for itself. + /// **Parameters** + /// + /// + /// **Output Type** + /// * `unit` + /// + /// **Exceptions** + /// + abstract decline: unit -> unit + + /// **Description** + /// Add a handler which is executed when the current module code is replaced. + /// This should be used to remove any persistent resource you have claimed or created. + /// If you want to transfer state to the updated module, add it to given `data` parameter. + /// This object will be available at `module.hot.data` after the update. + /// **Parameters** + /// * `data` - parameter of type `obj` + /// + /// **Output Type** + /// * `unit` + /// + /// **Exceptions** + /// + abstract dispose: data: obj -> unit + + /// **Description** + /// Add a handler which is executed when the current module code is replaced. + /// This should be used to remove any persistent resource you have claimed or created. + /// If you want to transfer state to the updated module, add it to given `data` parameter. + /// This object will be available at `module.hot.data` after the update. + /// **Parameters** + /// * `handler` - parameter of type `obj -> unit` + /// + /// **Output Type** + /// * `unit` + /// + /// **Exceptions** + /// + abstract addDisposeHandler: handler: (obj -> unit) -> unit + + /// **Description** + /// Remove the callback added via `dispose` or `addDisposeHandler`. + /// **Parameters** + /// * `callback` - parameter of type `obj -> unit` + /// + /// **Output Type** + /// * `unit` + /// + /// **Exceptions** + /// + abstract removeDisposeHandler: callback: (obj -> unit) -> unit + + /// **Description** + /// Test all loaded modules for updates and, if updates exist, `apply` them. + /// **Parameters** + /// * `autoApply` - parameter of type `U2` + /// + /// **Output Type** + /// * `JS.Promise` + /// + /// **Exceptions** + /// + abstract check: autoApply: U2 -> JS.Promise + + /// **Description** + /// Continue the update process (as long as `module.hot.status() === 'ready'`). + /// **Parameters** + /// * `options` - parameter of type `U2` + /// + /// **Output Type** + /// * `JS.Promise` + /// + /// **Exceptions** + /// + abstract apply: options : ApplyOptions -> JS.Promise + + /// **Description** + /// Register a function to listen for changes in `status`. + /// **Parameters** + /// + /// + /// **Output Type** + /// * `unit` + /// + /// **Exceptions** + /// + abstract addStatusHandler: (obj -> unit) -> unit + + /// **Description** + /// Remove a registered status handler. + /// **Parameters** + /// * `callback` - parameter of type `obj -> unit` + /// + /// **Output Type** + /// * `unit` + /// + /// **Exceptions** + /// + abstract removeStatusHandler: callback: (obj -> unit) -> unit + + type IModule = + abstract hot: IHot with get, set + + let [] ``module`` : IModule = jsNative + +module HMR = Bindings + +[] +module Program = + + module Internal = + type Platform = + | Browser + + let platform = + Browser + + let tryRestoreState (hot : HMR.IHot) = + match platform with + | Browser -> + let data = hot?data + if not (isNull data) && not (isNull data?hmrState) then + Some data?hmrState + else + None + + let saveState (data : obj) (hmrState : obj) = + match platform with + | Browser -> + data?hmrState <- hmrState + + type Msg<'msg> = + | UserMsg of 'msg + | Stop + + type Model<'model> = + | Inactive + | Active of 'model + + /// Start the dispatch loop with `'arg` for the init() function. + let inline runWith (arg: 'arg) (program: Program<'arg, 'model, 'msg, 'view>) = +#if !DEBUG + Program.runWith arg program +#else + let mutable hmrState : obj = null + let hot = HMR.``module``.hot + + if not (isNull hot) then + window?Elmish_HMR_Count <- + if isNull window?Elmish_HMR_Count then + 0 + else + window?Elmish_HMR_Count + 1 + + hot.accept() |> ignore + + match Internal.tryRestoreState hot with + | Some previousState -> + hmrState <- previousState + | None -> () + + let map (model, cmd) = + model, cmd |> Cmd.map UserMsg + + let mapUpdate update (msg : Msg<'msg>) (model : Model<'model>) = + let newModel,cmd = + match msg with + | UserMsg msg -> + match model with + | Inactive -> model, Cmd.none + | Active userModel -> + let newModel, cmd = update msg userModel + Active newModel, cmd + + | Stop -> + Inactive, Cmd.none + |> map + + hmrState <- newModel + newModel,cmd + + let createModel (model, cmd) = + Active model, cmd + + let mapInit init = + if isNull hmrState then + init >> map >> createModel + else + (fun _ -> unbox> hmrState, Cmd.none) + + let mapSetState setState (model : Model<'model>) dispatch = + match model with + | Inactive -> () + | Active userModel -> + setState userModel (UserMsg >> dispatch) + + let hmrSubscription = + let handler dispatch = + if not (isNull hot) then + hot.dispose(fun data -> + Internal.saveState data hmrState + + dispatch Stop + ) + [ handler ] + + let mapSubscribe subscribe model = + match model with + | Inactive -> Cmd.none + | Active userModel -> + Cmd.batch [ subscribe userModel |> Cmd.map UserMsg + hmrSubscription ] + + let mapView view = + // This function will never be executed because we are using a local reference to access `program.view`. + // For example, + // ```fs + // let withReactUnoptimized placeholderId (program: Program<_,_,_,_>) = + // let setState model dispatch = + // Fable.Import.ReactDom.render( + // lazyView2With (fun x y -> obj.ReferenceEquals(x,y)) program.view model dispatch, + // ^-- Here program is coming from the function parameters and not + // from the last program composition used to run the applicaiton + // document.getElementById(placeholderId) + // ) + // + // { program with setState = setState } + // ```*) + fun model dispatch -> + match model with + | Inactive -> + """ +You are using HMR and this Elmish application has been marked as inactive. +You should not see this message + """ + |> failwith + | Active userModel -> + view userModel (UserMsg >> dispatch) + + program + |> Program.map mapInit mapUpdate mapView mapSetState mapSubscribe + |> Program.runWith arg +#endif + + /// Start the dispatch loop with `unit` for the init() function. + let inline run (program: Program) = +#if !DEBUG + Program.run program +#else + runWith () program +#endif diff --git a/src/Fable.Repl.Lib/Elmish.Snabbdom/Elmish.Snabbdom.fs b/src/Fable.Repl.Lib/Elmish.Snabbdom/Elmish.Snabbdom.fs new file mode 100644 index 00000000..1253e1aa --- /dev/null +++ b/src/Fable.Repl.Lib/Elmish.Snabbdom/Elmish.Snabbdom.fs @@ -0,0 +1,124 @@ +[] +module Elmish.Snabbdom.Program + +open Fable.Core +open Fable.Core.JsInterop +open Elmish +open Browser.Types +open Feliz.Snabbdom + +module private rec Util = + let copyTo (target: obj) (source: obj) = + JS.Constructors.Object.assign(target, source) |> ignore + + let partialPatch (oldVNode: Snabbdom.VNode) (newVnode: Snabbdom.VNode) = + Snabbdom.Helper.Patch(oldVNode, newVnode) |> copyTo oldVNode + +open Util + +/// When mounting an app on a virtual node, `setNewArg` will be used +/// to transform new arguments into messages and dispatch them +let withSetNewArg (setNewArg: 'arg -> 'msg) (program: Program<'arg, 'model, 'msg, Node>) = + program?setNewArg <- setNewArg + program + +let __mountOnVNodeWith (init: (Program<'arg, 'model, 'msg, Node> -> unit) -> unit) (sel: string) (arg: 'arg): Node = + Html.custom(sel, [ + Hook.insert (fun vnode -> init(fun program -> + let mutable oldVNode = vnode + let mutable oldModel: 'model option = None + + + let setState model dispatch = + match oldModel with + | Some m when obj.ReferenceEquals(m, model) -> () + | _ -> + let newVNode = + Html.custom(sel, [ + Program.view program model dispatch + ]) |> Node.AsVNode + + if oldVNode.children.Length = 0 && jsIn "setNewArg" program then + newVNode.data?setNewArg <- (program?setNewArg >> dispatch) + + partialPatch oldVNode newVNode + oldVNode <- newVNode + + program + |> Program.withSetState setState + |> Program.runWith arg + )) + + Hook.prepatch (fun oldVNode newVNode -> + oldVNode |> copyTo newVNode + if jsIn "setNewArg" newVNode.data then + newVNode.data?setNewArg(arg) + ) + ]) + +/// Mounts an Elmish app onto a Snabbdom virtual node. +/// The selector `tag[#id][.classes]` is important to distinguish it from sibling nodes. +let mountOnVNodeWith (selector: string) (arg: 'arg) (program: Program<'arg, 'model, 'msg, Node>): Node = + __mountOnVNodeWith (fun cont -> cont(program)) selector arg + +/// Mounts an Elmish app onto a Snabbdom virtual node. +/// The selector `tag[#id][.classes]` is important to distinguish it from sibling nodes. +let mountOnVNode (selector: string) (program: Program<_,_,_,_>): Node = + __mountOnVNodeWith (fun cont -> cont(program)) selector () + +// [ import($0).then(m => cont(m.mkProgram($1)))")>] +// let __importAndMkProgram(path: string) (arg: 'arg): (Program<'arg, 'model, 'msg, Node> -> unit) -> unit = jsNative + +// let inline importAndMountOnVNode (path: string) sel: Node = +// __mountOnVNodeWith (__importAndMkProgram (path + Compiler.extension) ()) sel () + +// let inline importAndMountOnVNodeWith (path: string) sel (arg: 'arg): Node = +// __mountOnVNodeWith (__importAndMkProgram (path + Compiler.extension) arg) sel arg + +let __lazyOnVNodeWith (mkProgram: JS.Promise<'arg -> Program<'arg, 'model, 'msg, Node>>) sel arg: Node = + __mountOnVNodeWith (fun cont -> + mkProgram.``then``(fun mkProgram -> mkProgram arg |> cont) |> ignore + ) sel arg + +/// Like `mountOnVNodeWith` but the external module is only loaded when the node is inserted into the DOM. +/// Pass a direct reference to `mkProgram` function (avoid pipes). +/// For the argument avoid referencing a declared type in the external module (you can use anonymous records). +// let inline lazyOnVNodeWith (mkProgram: 'arg -> Program<'arg, 'model, 'msg, Node>) (selector: string) (arg: 'arg): Node = +// __lazyOnVNodeWith (importValueDynamic mkProgram) selector arg + +// /// Like `mountOnVNode` but the external module is only loaded when the node is inserted into the DOM. +// /// Pass a direct reference to `mkProgram` function (avoid pipes). +// let inline lazyOnVNode (mkProgram: unit -> Program) (selector: string): Node = +// __lazyOnVNodeWith (importValueDynamic mkProgram) selector () + +/// Mounts an Elmish program on an +let mountWithId (id: string) (program: Program<'arg, 'model, 'msg, Node>): Program<'arg, 'model, 'msg, Node> = + let parent = Browser.Dom.document.getElementById(id) + // Snabbdom expects el to be empty, but this is not the case in HMR reloads + if parent.children.length > 0 then parent.innerHTML <- "" + + // Don't mount directly on the element as Snabbdom will replace it + // and we won't be able to locate it in hot reloads + let el = Browser.Dom.document.createElement("div") + parent.appendChild(el) |> ignore + + let mutable oldVNode: Snabbdom.VNode option = None + let mutable oldModel: 'model option = None + + let setState model dispatch = + match oldModel with + | Some m when obj.ReferenceEquals(m, model) -> () + | _ -> + let newVNode = Program.view program model dispatch |> Node.AsVNode + match oldVNode with + | None -> Snabbdom.Helper.Patch(el, newVNode) |> ignore + | Some oldVNode -> Snabbdom.Helper.Patch(oldVNode, newVNode) |> ignore + oldVNode <- Some newVNode + oldModel <- Some model + + program + |> Program.withSetState setState + +/// Alias of `mountWithId` +let withSnabbdom (id: string) program = + mountWithId id program diff --git a/src/Fable.Repl.Lib/Elmish.Snabbdom/Elmish.Snabbdom.fsproj b/src/Fable.Repl.Lib/Elmish.Snabbdom/Elmish.Snabbdom.fsproj new file mode 100644 index 00000000..371e5bac --- /dev/null +++ b/src/Fable.Repl.Lib/Elmish.Snabbdom/Elmish.Snabbdom.fsproj @@ -0,0 +1,22 @@ + + + 1.0.0 + 1.0.0-beta-005 + netstandard2.0 + true + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Fable.Repl.Lib/Elmish.Snabbdom/RELEASE_NOTES.md b/src/Fable.Repl.Lib/Elmish.Snabbdom/RELEASE_NOTES.md new file mode 100644 index 00000000..8c5ae4e1 --- /dev/null +++ b/src/Fable.Repl.Lib/Elmish.Snabbdom/RELEASE_NOTES.md @@ -0,0 +1,19 @@ +### 1.0.0-beta-005 + +* Improve lazily loaded components + +### 1.0.0-beta-004 + +* Update to latest Feliz.Engine & Fable.Core + +### 1.0.0-beta-003 + +* Third beta release + +### 1.0.0-beta-002 + +* Second beta release + +### 1.0.0-beta-001 + +* First beta release diff --git a/src/Fable.Repl.Lib/Elmish/Fable.Elmish.fsproj b/src/Fable.Repl.Lib/Elmish/Fable.Elmish.fsproj new file mode 100644 index 00000000..6806c430 --- /dev/null +++ b/src/Fable.Repl.Lib/Elmish/Fable.Elmish.fsproj @@ -0,0 +1,17 @@ + + + netstandard2.0 + true + $(DefineConstants);FABLE_COMPILER + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Fable.Repl.Lib/Elmish/cmd.fs b/src/Fable.Repl.Lib/Elmish/cmd.fs new file mode 100644 index 00000000..ba1fe942 --- /dev/null +++ b/src/Fable.Repl.Lib/Elmish/cmd.fs @@ -0,0 +1,292 @@ +(** +Cmd +--------- +Core abstractions for dispatching messages in Elmish. + +*) + +namespace Elmish + +open System + +/// Dispatch - feed new message into the processing loop +type Dispatch<'msg> = 'msg -> unit + +/// Subscription - return immediately, but may schedule dispatch of a message at any time +type Sub<'msg> = Dispatch<'msg> -> unit + +/// Cmd - container for subscriptions that may produce messages +type Cmd<'msg> = Sub<'msg> list + +/// Cmd module for creating and manipulating commands +[] +module Cmd = + /// Execute the commands using the supplied dispatcher + let internal exec onError (dispatch: Dispatch<'msg>) (cmd: Cmd<'msg>) = + cmd |> List.iter (fun call -> try call dispatch with ex -> onError ex) + + /// None - no commands, also known as `[]` + let none : Cmd<'msg> = + [] + + /// When emitting the message, map to another type + let map (f: 'a -> 'msg) (cmd: Cmd<'a>) : Cmd<'msg> = + cmd |> List.map (fun g -> (fun dispatch -> f >> dispatch) >> g) + + /// Aggregate multiple commands + let batch (cmds: #seq>) : Cmd<'msg> = + cmds |> List.concat + + /// Command to call the subscriber + let ofSub (sub: Sub<'msg>) : Cmd<'msg> = + [sub] + + module OfFunc = + /// Command to evaluate a simple function and map the result + /// into success or error (of exception) + let either (task: 'a -> _) (arg: 'a) (ofSuccess: _ -> 'msg) (ofError: _ -> 'msg) : Cmd<'msg> = + let bind dispatch = + try + task arg + |> (ofSuccess >> dispatch) + with x -> + x |> (ofError >> dispatch) + [bind] + + /// Command to evaluate a simple function and map the success to a message + /// discarding any possible error + let perform (task: 'a -> _) (arg: 'a) (ofSuccess: _ -> 'msg) : Cmd<'msg> = + let bind dispatch = + try + task arg + |> (ofSuccess >> dispatch) + with x -> + () + [bind] + + /// Command to evaluate a simple function and map the error (in case of exception) + let attempt (task: 'a -> unit) (arg: 'a) (ofError: _ -> 'msg) : Cmd<'msg> = + let bind dispatch = + try + task arg + with x -> + x |> (ofError >> dispatch) + [bind] + + /// Command to issue a specific message + let result (msg:'msg) : Cmd<'msg> = + [fun dispatch -> dispatch msg] + + module OfAsyncWith = + /// Command that will evaluate an async block and map the result + /// into success or error (of exception) + let either (start: Async -> unit) + (task: 'a -> Async<_>) + (arg: 'a) + (ofSuccess: _ -> 'msg) + (ofError: _ -> 'msg) : Cmd<'msg> = + let bind dispatch = + async { + let! r = task arg |> Async.Catch + dispatch (match r with + | Choice1Of2 x -> ofSuccess x + | Choice2Of2 x -> ofError x) + } + [bind >> start] + + /// Command that will evaluate an async block and map the success + let perform (start: Async -> unit) + (task: 'a -> Async<_>) + (arg: 'a) + (ofSuccess: _ -> 'msg) : Cmd<'msg> = + let bind dispatch = + async { + let! r = task arg |> Async.Catch + match r with + | Choice1Of2 x -> dispatch (ofSuccess x) + | _ -> () + } + [bind >> start] + + /// Command that will evaluate an async block and map the error (of exception) + let attempt (start: Async -> unit) + (task: 'a -> Async<_>) + (arg: 'a) + (ofError: _ -> 'msg) : Cmd<'msg> = + let bind dispatch = + async { + let! r = task arg |> Async.Catch + match r with + | Choice2Of2 x -> dispatch (ofError x) + | _ -> () + } + [bind >> start] + + /// Command that will evaluate an async block to the message + let result (start: Async -> unit) + (task: Async<'msg>) : Cmd<'msg> = + let bind dispatch = + async { + let! r = task + dispatch r + } + [bind >> start] + + module OfAsync = +#if FABLE_COMPILER + let start x = Timer.delay 0 (fun _ -> Async.StartImmediate x) +#else + let start x = Async.Start x +#endif + /// Command that will evaluate an async block and map the result + /// into success or error (of exception) + let either (task: 'a -> Async<_>) + (arg: 'a) + (ofSuccess: _ -> 'msg) + (ofError: _ -> 'msg) : Cmd<'msg> = + OfAsyncWith.either start task arg ofSuccess ofError + + /// Command that will evaluate an async block and map the success + let perform (task: 'a -> Async<_>) + (arg: 'a) + (ofSuccess: _ -> 'msg) : Cmd<'msg> = + OfAsyncWith.perform start task arg ofSuccess + + /// Command that will evaluate an async block and map the error (of exception) + let attempt (task: 'a -> Async<_>) + (arg: 'a) + (ofError: _ -> 'msg) : Cmd<'msg> = + OfAsyncWith.attempt start task arg ofError + + /// Command that will evaluate an async block to the message + let result (task: Async<'msg>) : Cmd<'msg> = + OfAsyncWith.result start task + + module OfAsyncImmediate = + /// Command that will evaluate an async block and map the result + /// into success or error (of exception) + let either (task: 'a -> Async<_>) + (arg: 'a) + (ofSuccess: _ -> 'msg) + (ofError: _ -> 'msg) : Cmd<'msg> = + OfAsyncWith.either Async.StartImmediate task arg ofSuccess ofError + + /// Command that will evaluate an async block and map the success + let perform (task: 'a -> Async<_>) + (arg: 'a) + (ofSuccess: _ -> 'msg) : Cmd<'msg> = + OfAsyncWith.perform Async.StartImmediate task arg ofSuccess + + /// Command that will evaluate an async block and map the error (of exception) + let attempt (task: 'a -> Async<_>) + (arg: 'a) + (ofError: _ -> 'msg) : Cmd<'msg> = + OfAsyncWith.attempt Async.StartImmediate task arg ofError + + /// Command that will evaluate an async block to the message + let result (task: Async<'msg>) : Cmd<'msg> = + OfAsyncWith.result Async.StartImmediate task + +#if FABLE_COMPILER + module OfPromise = + /// Command to call `promise` block and map the results + let either (task: 'a -> Fable.Core.JS.Promise<_>) + (arg:'a) + (ofSuccess: _ -> 'msg) + (ofError: #exn -> 'msg) : Cmd<'msg> = + let bind dispatch = + (task arg) + .``then``(ofSuccess >> dispatch) + .catch(unbox >> ofError >> dispatch) + |> ignore + [bind] + + /// Command to call `promise` block and map the success + let perform (task: 'a -> Fable.Core.JS.Promise<_>) + (arg:'a) + (ofSuccess: _ -> 'msg) = + let bind dispatch = + (task arg) + .``then``(ofSuccess >> dispatch) + |> ignore + [bind] + + /// Command to call `promise` block and map the error + let attempt (task: 'a -> Fable.Core.JS.Promise<_>) + (arg:'a) + (ofError: #exn -> 'msg) : Cmd<'msg> = + let bind dispatch = + (task arg) + .catch(unbox >> ofError >> dispatch) + |> ignore + [bind] + + /// Command to dispatch the `promise` result + let result (task: Fable.Core.JS.Promise<'msg>) = + let bind dispatch = + task.``then`` dispatch + |> ignore + [bind] + + [] + let ofPromise (task: 'a -> Fable.Core.JS.Promise<_>) + (arg:'a) + (ofSuccess: _ -> 'msg) + (ofError: _ -> 'msg) : Cmd<'msg> = + OfPromise.either task arg ofSuccess ofError +#else + open System.Threading.Tasks + module OfTask = + /// Command to call a task and map the results + let either (task: 'a -> Task<_>) + (arg:'a) + (ofSuccess: _ -> 'msg) + (ofError: _ -> 'msg) : Cmd<'msg> = + OfAsync.either (task >> Async.AwaitTask) arg ofSuccess ofError + + /// Command to call a task and map the success + let perform (task: 'a -> Task<_>) + (arg:'a) + (ofSuccess: _ -> 'msg) : Cmd<'msg> = + OfAsync.perform (task >> Async.AwaitTask) arg ofSuccess + + /// Command to call a task and map the error + let attempt (task: 'a -> Task<_>) + (arg:'a) + (ofError: _ -> 'msg) : Cmd<'msg> = + OfAsync.attempt (task >> Async.AwaitTask) arg ofError + + /// Command and map the task success + let result (task: Task<'msg>) : Cmd<'msg> = + OfAsync.result (task |> Async.AwaitTask) + + [] + let ofTask (task: 'a -> Task<_>) + (arg:'a) + (ofSuccess: _ -> 'msg) + (ofError: _ -> 'msg) : Cmd<'msg> = + OfTask.either task arg ofSuccess ofError +#endif + + // Synonymous with `OfFunc.result`, may be removed in the future + let ofMsg (msg:'msg) : Cmd<'msg> = + OfFunc.result msg + + [] + let ofAsync (task: 'a -> Async<_>) + (arg: 'a) + (ofSuccess: _ -> 'msg) + (ofError: _ -> 'msg) : Cmd<'msg> = + OfAsync.either task arg ofSuccess ofError + + [] + let ofFunc (task: 'a -> _) (arg: 'a) (ofSuccess: _ -> 'msg) (ofError: _ -> 'msg) : Cmd<'msg> = + OfFunc.either task arg ofSuccess ofError + + [] + let performFunc (task: 'a -> _) (arg: 'a) (ofSuccess: _ -> 'msg) : Cmd<'msg> = + OfFunc.perform task arg ofSuccess + + [] + let attemptFunc (task: 'a -> unit) (arg: 'a) (ofError: _ -> 'msg) : Cmd<'msg> = + OfFunc.attempt task arg ofError diff --git a/src/Fable.Repl.Lib/Elmish/paket.references b/src/Fable.Repl.Lib/Elmish/paket.references new file mode 100644 index 00000000..2387b156 --- /dev/null +++ b/src/Fable.Repl.Lib/Elmish/paket.references @@ -0,0 +1,2 @@ +FSharp.Core +Fable.Core \ No newline at end of file diff --git a/src/Fable.Repl.Lib/Elmish/prelude.fs b/src/Fable.Repl.Lib/Elmish/prelude.fs new file mode 100644 index 00000000..03754b58 --- /dev/null +++ b/src/Fable.Repl.Lib/Elmish/prelude.fs @@ -0,0 +1,35 @@ +namespace Elmish + +(** +Log +--------- +Basic cross-platform logging API. + +*) +module internal Log = + +#if FABLE_COMPILER + open Fable.Core.JS + + let onError (text: string, ex: exn) = console.error (text,ex) + let toConsole(text: string, o: #obj) = console.log(text,o) + +#else +#if NETSTANDARD2_0 + let onError (text: string, ex: exn) = System.Diagnostics.Trace.TraceError("{0}: {1}", text, ex) + let toConsole(text: string, o: #obj) = printfn "%s: %A" text o +#else + let onError (text: string, ex: exn) = System.Console.Error.WriteLine("{0}: {1}", text, ex) + let toConsole(text: string, o: #obj) = printfn "%s: %A" text o +#endif +#endif + +#if FABLE_COMPILER +module internal Timer = + open System.Timers + let delay interval callback = + let t = new Timer(float interval, AutoReset = false) + t.Elapsed.Add callback + t.Enabled <- true + t.Start() +#endif diff --git a/src/Fable.Repl.Lib/Elmish/program.fs b/src/Fable.Repl.Lib/Elmish/program.fs new file mode 100644 index 00000000..a5277b4c --- /dev/null +++ b/src/Fable.Repl.Lib/Elmish/program.fs @@ -0,0 +1,169 @@ +(** +Program +--------- +Core abstractions for creating and running the dispatch loop. + +*) + +namespace Elmish + + +/// Program type captures various aspects of program behavior +type Program<'arg, 'model, 'msg, 'view> = private { + init : 'arg -> 'model * Cmd<'msg> + update : 'msg -> 'model -> 'model * Cmd<'msg> + subscribe : 'model -> Cmd<'msg> + view : 'model -> Dispatch<'msg> -> 'view + setState : 'model -> Dispatch<'msg> -> unit + onError : (string*exn) -> unit + syncDispatch: Dispatch<'msg> -> Dispatch<'msg> +} + +/// Program module - functions to manipulate program instances +[] +[] +module Program = + /// Typical program, new commands are produced by `init` and `update` along with the new state. + let mkProgram + (init : 'arg -> 'model * Cmd<'msg>) + (update : 'msg -> 'model -> 'model * Cmd<'msg>) + (view : 'model -> Dispatch<'msg> -> 'view) = + { init = init + update = update + view = view + setState = fun model -> view model >> ignore + subscribe = fun _ -> Cmd.none + onError = Log.onError + syncDispatch = id } + + /// Simple program that produces only new state with `init` and `update`. + let mkSimple + (init : 'arg -> 'model) + (update : 'msg -> 'model -> 'model) + (view : 'model -> Dispatch<'msg> -> 'view) = + { init = init >> fun state -> state,Cmd.none + update = fun msg -> update msg >> fun state -> state,Cmd.none + view = view + setState = fun model -> view model >> ignore + subscribe = fun _ -> Cmd.none + onError = Log.onError + syncDispatch = id } + + /// Subscribe to external source of events. + /// The subscription is called once - with the initial model, but can dispatch new messages at any time. + let withSubscription (subscribe : 'model -> Cmd<'msg>) (program: Program<'arg, 'model, 'msg, 'view>) = + let sub model = + Cmd.batch [ program.subscribe model + subscribe model ] + { program with subscribe = sub } + + /// Trace all the updates to the console + let withConsoleTrace (program: Program<'arg, 'model, 'msg, 'view>) = + let traceInit (arg:'arg) = + let initModel,cmd = program.init arg + Log.toConsole ("Initial state:", initModel) + initModel,cmd + + let traceUpdate msg model = + Log.toConsole ("New message:", msg) + let newModel,cmd = program.update msg model + Log.toConsole ("Updated state:", newModel) + newModel,cmd + + { program with + init = traceInit + update = traceUpdate } + + /// Trace all the messages as they update the model + let withTrace trace (program: Program<'arg, 'model, 'msg, 'view>) = + let update msg model = + let state,cmd = program.update msg model + trace msg state + state,cmd + { program + with update = update } + + /// Handle dispatch loop exceptions + let withErrorHandler onError (program: Program<'arg, 'model, 'msg, 'view>) = + { program + with onError = onError } + + /// For library authors only: map existing error handler and return new `Program` + let mapErrorHandler map (program: Program<'arg, 'model, 'msg, 'view>) = + { program + with onError = map program.onError } + + /// For library authors only: get the current error handler + let onError (program: Program<'arg, 'model, 'msg, 'view>) = + program.onError + + /// For library authors only: function to render the view with the latest state + let withSetState (setState:'model -> Dispatch<'msg> -> unit) + (program: Program<'arg, 'model, 'msg, 'view>) = + { program + with setState = setState } + + /// For library authors only: return the function to render the state + let setState (program: Program<'arg, 'model, 'msg, 'view>) = + program.setState + + /// For library authors only: return the view function + let view (program: Program<'arg, 'model, 'msg, 'view>) = + program.view + + /// For library authors only: function to synchronize the dispatch function + let withSyncDispatch (syncDispatch:Dispatch<'msg> -> Dispatch<'msg>) + (program: Program<'arg, 'model, 'msg, 'view>) = + { program + with syncDispatch = syncDispatch } + + /// For library authors only: map the program type + let map mapInit mapUpdate mapView mapSetState mapSubscribe + (program: Program<'arg, 'model, 'msg, 'view>) = + { init = mapInit program.init + update = mapUpdate program.update + view = mapView program.view + setState = mapSetState program.setState + subscribe = mapSubscribe program.subscribe + onError = program.onError + syncDispatch = id } + + /// Start the program loop. + /// arg: argument to pass to the init() function. + /// program: program created with 'mkSimple' or 'mkProgram'. + let runWith (arg: 'arg) (program: Program<'arg, 'model, 'msg, 'view>) = + let (model,cmd) = program.init arg + let rb = RingBuffer 10 + let mutable reentered = false + let mutable state = model + let rec dispatch msg = + if reentered then + rb.Push msg + else + reentered <- true + let mutable nextMsg = Some msg + while Option.isSome nextMsg do + let msg = nextMsg.Value + try + let (model',cmd') = program.update msg state + program.setState model' syncDispatch + cmd' |> Cmd.exec (fun ex -> program.onError (sprintf "Error in command while handling: %A" msg, ex)) syncDispatch + state <- model' + with ex -> + program.onError (sprintf "Unable to process the message: %A" msg, ex) + nextMsg <- rb.Pop() + reentered <- false + and syncDispatch = program.syncDispatch dispatch + + program.setState model syncDispatch + let sub = + try + program.subscribe model + with ex -> + program.onError ("Unable to subscribe:", ex) + Cmd.none + Cmd.batch [sub; cmd] + |> Cmd.exec (fun ex -> program.onError ("Error intitializing:", ex)) syncDispatch + + /// Start the dispatch loop with `unit` for the init() function. + let run (program: Program) = runWith () program diff --git a/src/Fable.Repl.Lib/Elmish/ring.fs b/src/Fable.Repl.Lib/Elmish/ring.fs new file mode 100644 index 00000000..c24ec8a1 --- /dev/null +++ b/src/Fable.Repl.Lib/Elmish/ring.fs @@ -0,0 +1,46 @@ +namespace Elmish +open System + +[] +type internal RingState<'item> = + | Writable of wx:'item array * ix:int + | ReadWritable of rw:'item array * wix:int * rix:int + +type internal RingBuffer<'item>(size) = + let doubleSize ix (items: 'item array) = + seq { yield! items |> Seq.skip ix + yield! items |> Seq.take ix + for _ in 0..items.Length do + yield Unchecked.defaultof<'item> } + |> Array.ofSeq + + let mutable state : 'item RingState = + Writable (Array.zeroCreate (max size 10), 0) + + member __.Pop() = + match state with + | ReadWritable (items, wix, rix) -> + let rix' = (rix + 1) % items.Length + match rix' = wix with + | true -> + state <- Writable(items, wix) + | _ -> + state <- ReadWritable(items, wix, rix') + Some items.[rix] + | _ -> + None + + member __.Push (item:'item) = + match state with + | Writable (items, ix) -> + items.[ix] <- item + let wix = (ix + 1) % items.Length + state <- ReadWritable(items, wix, ix) + | ReadWritable (items, wix, rix) -> + items.[wix] <- item + let wix' = (wix + 1) % items.Length + match wix' = rix with + | true -> + state <- ReadWritable(items |> doubleSize rix, items.Length, 0) + | _ -> + state <- ReadWritable(items, wix', rix) \ No newline at end of file diff --git a/src/Fable.Repl.Lib/Fable.Repl.Lib.fsproj b/src/Fable.Repl.Lib/Fable.Repl.Lib.fsproj index a5a8be1a..7d79e051 100644 --- a/src/Fable.Repl.Lib/Fable.Repl.Lib.fsproj +++ b/src/Fable.Repl.Lib/Fable.Repl.Lib.fsproj @@ -48,10 +48,16 @@ - + + + + + + + + + + diff --git a/src/Fable.Repl.Lib/Feliz.Snabbdom/Feliz.Snabbdom.fs b/src/Fable.Repl.Lib/Feliz.Snabbdom/Feliz.Snabbdom.fs new file mode 100644 index 00000000..b4a5d302 --- /dev/null +++ b/src/Fable.Repl.Lib/Feliz.Snabbdom/Feliz.Snabbdom.fs @@ -0,0 +1,209 @@ +module Feliz.Snabbdom + +open System +open Fable.Core +open Fable.Core.JsInterop +open Feliz +open Snabbdom + +[] +type StyleHook = + | None + | Delayed + | Remove + | Destroy + +type Node = + | Key of Guid + | Text of string + | El of VNode + | Hook of string * obj + | Style of string * obj * StyleHook + | Attr of string * obj + | Event of string * obj + | Fragment of Node list + +let private makeNode tag nodes = + let mutable transformArrayHooks = false + + let rec add isHook (o: obj) keys (v: obj) = + match keys with + | [] -> failwith "Empty key list" + | [key] -> + if isHook && isIn key o then + transformArrayHooks <- true + emitJsStatement (o, key, v) """ + if (Array.isArray($0[$1])) { + $0[$1].push($2); + } else { + $0[$1] = [$0[$1], $2] + }""" + else + o?(key) <- v + | key::keys -> + if isNull o?(key) then o?(key) <- obj() + add isHook (o?(key)) keys v + + let rec addNodes (data: obj) (children: ResizeArray<_>) (nodes: Node seq) = + nodes |> Seq.iter (function + | Key k -> data?key <- k + | Text s -> children.Add(Helper.Text s) + | El vnode -> children.Add(vnode) + | Hook(k, v) -> add true data ["hook"; k] v + | Style(k, v, StyleHook.None) -> add false data ["style"; k] v + | Style(k, v, StyleHook.Delayed) -> add false data ["style"; "delayed"; k] v + | Style(k, v, StyleHook.Remove) -> add false data ["style"; "remove"; k] v + | Style(k, v, StyleHook.Destroy) -> add false data ["style"; "destroy"; k] v + | Attr(k, v) -> add false data ["attrs"; k] v + | Event(k, v) -> add false data ["on"; k] v + | Fragment nodes -> addNodes data children nodes + ) + + let data = obj() + let children = ResizeArray() + addNodes data children nodes + if transformArrayHooks then + emitJsStatement (data) """ + Object.keys($0.hook) + .filter(k => Array.isArray($0.hook[k])) + .forEach(k => { + const cbs = $0.hook[k]; + $0.hook[k] = function() { + for (let cb of cbs) { + cb.apply(void 0, arguments) + } + } + })""" + + Snabbdom.h(tag, data, children) |> El + +open System.Runtime.CompilerServices + +[] +type Extensions() = + static let withStyleHook hook nodes = + nodes |> Seq.choose (function + | Style(k, v, _) -> Some(Style(k, v, hook)) + | _ -> None // error? + ) |> Seq.toList |> Fragment + + [] + static member delayed(e: CssEngine, nodes: Node seq) = + withStyleHook StyleHook.Delayed nodes + + [] + static member remove(e: CssEngine, nodes: Node seq) = + withStyleHook StyleHook.Remove nodes + + [] + static member destroy(e: CssEngine, nodes: Node seq) = + withStyleHook StyleHook.Destroy nodes + +type Browser.Types.EventTarget with + member this.AsInputEl = + this :?> Browser.Types.HTMLInputElement + +type Node with + static member AsVNode = function + | El vnode -> vnode + | Fragment [El vnode] -> vnode + | Fragment nodes -> makeNode "div" nodes |> Node.AsVNode + | _ -> failwith "not a vnode" + +let Html = HtmlEngine(makeNode, Text, fun () -> Fragment []) + +let Svg = SvgEngine(makeNode, Text, fun () -> Fragment []) + +let Attr = AttrEngine((fun k v -> Attr(k, v)), (fun k v -> Attr(k, v))) + +let Css = CssEngine(fun k v -> Style(k, v, StyleHook.None)) + +let Ev = EventEngine(fun k f -> Event(k.ToLowerInvariant(), f)) + +type Hook = + /// a vnode has been added + static member init (f: Func) = Node.Hook("init", f) + /// a DOM element has been created based on a vnode + static member create (f: Func) = Node.Hook("create", f) + /// an element has been inserted into the DOM + static member insert (f: Func) = Node.Hook("insert", f) + /// an element is about to be patched + static member prepatch (f: Func) = Node.Hook("prepatch", f) + /// an element is being updated + static member update (f: Func) = Node.Hook("update", f) + /// an element has been patched + static member postpatch (f: Func) = Node.Hook("postpatch", f) + /// an element is directly or indirectly being removed + static member destroy (f: Func) = Node.Hook("destroy", f) + /// an element is directly being removed from the DOM + static member remove (f: Func unit), unit>) = Node.Hook("remove", f) + + /// The disposable returned by the hook when the element is inserted into the DOM + /// will be disposed when that element is directly or indirectly removed from the DOM + static member insert (f: VNode -> IDisposable) = + Fragment [ + Hook.insert (fun (v: VNode) -> + let disp = f v + v.data?disposable <- disp) + + Hook.update (fun oldNode newNode -> + newNode.data?disposable <- oldNode.data?disposable + ) + + Hook.destroy (fun (v: VNode) -> + v.data?disposable + |> Option.ofObj + |> Option.iter (fun (d: IDisposable) -> d.Dispose())) + ] + + static member subscribe(arg: 'arg, onInsert: VNode -> IObserver<'arg>) = + Fragment [ + Hook.insert (fun (v: VNode) -> + v.data?observer <- onInsert v) + + Hook.update (fun oldNode newNode -> + let obs: IObserver<'arg> = oldNode.data?observer + obs.OnNext(arg) + newNode.data?observer <- obs + ) + + Hook.destroy (fun (v: VNode) -> + let obs: IObserver<'arg> = v.data?observer + obs.OnCompleted()) + ] + + static member subscribe(arg: 'arg, onInsert: VNode -> ('arg -> unit)) = + Hook.subscribe(arg, fun vnode -> + let onNext = onInsert vnode + { new IObserver<'arg> with + member _.OnNext(v) = onNext v + member _.OnCompleted() = () + member _.OnError(_) = () }) + +module Disposable = + let make f = + { new IDisposable with + member _.Dispose() = f() } + + let concat (disps: IDisposable list) = + make (fun () -> disps |> List.iter (fun d -> d.Dispose())) + +let private attachEvent (f: Browser.Types.Event -> unit) (el: Browser.Types.Node) (eventType: string) = + el.addEventListener (eventType, f) + Disposable.make (fun () -> el.removeEventListener (eventType, f)) + +let private mkEventEngine (node: Browser.Types.Node) = + EventEngine(fun e f -> e.ToLowerInvariant() |> attachEvent f node) + +let BodyEv = mkEventEngine(Browser.Dom.document.body) + +let memoizeWith (render: 'arg -> Node) getId equals arg = + Helper.Memo(getId arg, (fun m -> render m |> Node.AsVNode), arg, equals) |> El + +let memoizeWithId (render: 'arg -> Node) getId arg = + Helper.Memo(getId arg, (fun m -> render m |> Node.AsVNode), arg) |> El + +// let inline getId x = (^a: (member Id: Guid) x) + +// let inline memoize (render: 'arg -> Node) arg = +// memoizeWithId render getId arg diff --git a/src/Fable.Repl.Lib/Feliz.Snabbdom/Feliz.Snabbdom.fsproj b/src/Fable.Repl.Lib/Feliz.Snabbdom/Feliz.Snabbdom.fsproj new file mode 100644 index 00000000..777cca0a --- /dev/null +++ b/src/Fable.Repl.Lib/Feliz.Snabbdom/Feliz.Snabbdom.fsproj @@ -0,0 +1,22 @@ + + + 1.0.0 + 1.0.0-beta-004 + netstandard2.0 + true + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Fable.Repl.Lib/Feliz.Snabbdom/RELEASE_NOTES.md b/src/Fable.Repl.Lib/Feliz.Snabbdom/RELEASE_NOTES.md new file mode 100644 index 00000000..bb53a43f --- /dev/null +++ b/src/Fable.Repl.Lib/Feliz.Snabbdom/RELEASE_NOTES.md @@ -0,0 +1,15 @@ +### 1.0.0-beta-004 + +* Update to latest Feliz.Engine & Fable.Core + +### 1.0.0-beta-003 + +* Third beta release + +### 1.0.0-beta-002 + +* Second beta release + +### 1.0.0-beta-001 + +* First beta release diff --git a/src/Fable.Repl.Lib/Feliz.Snabbdom/Snabbdom.fs b/src/Fable.Repl.Lib/Feliz.Snabbdom/Snabbdom.fs new file mode 100644 index 00000000..3d5614b2 --- /dev/null +++ b/src/Fable.Repl.Lib/Feliz.Snabbdom/Snabbdom.fs @@ -0,0 +1,51 @@ +module Snabbdom + +open System +open Fable.Core + +type Module = interface end + +type VNode = + abstract sel: string with get, set + abstract key: string with get, set + abstract data: obj with get, set + abstract children: VNode[] with get, set + abstract text: string with get, set + abstract elm: Browser.Types.HTMLElement with get, set + +type Patch = delegate of VNode * VNode -> VNode + +// [] +[] +let attributesModule: Module = jsNative + +// [] +[] +let styleModule: Module = jsNative + +// [] +[] +let eventListenersModule: Module = jsNative + +// [] +[] +let h(tag: string, props: obj, children: ResizeArray): VNode = jsNative + +// [] +[] +let init(modules: Module[]): Patch = jsNative + +[] +let memo(key: Guid, render: 'arg -> VNode, arg: 'arg, equals: ('arg -> 'arg -> bool) option): VNode = jsNative + +type Helper() = + static let patcher = init [| + attributesModule + styleModule + eventListenersModule + |] + static member Empty: VNode = unbox null + static member Text(str: string): VNode = unbox str + static member Patch(oldNode: VNode, newNode: VNode): VNode = patcher.Invoke(oldNode, newNode) + static member Patch(el: Browser.Types.HTMLElement, vnode: VNode): VNode = patcher.Invoke(unbox el, vnode) + static member Memo(key: Guid, render: 'Arg -> VNode, arg: 'Arg, ?equals: 'Arg -> 'Arg -> bool): VNode = memo(key, render, arg, equals) \ No newline at end of file diff --git a/src/Fable.Repl.Lib/Feliz.Snabbdom/snabbdom.min.js b/src/Fable.Repl.Lib/Feliz.Snabbdom/snabbdom.min.js new file mode 100644 index 00000000..f9c00508 --- /dev/null +++ b/src/Fable.Repl.Lib/Feliz.Snabbdom/snabbdom.min.js @@ -0,0 +1,393 @@ +function n(e, t, n, o, r) { + return { sel: e, data: t, children: n, text: o, elm: r, key: void 0 === t ? void 0 : t.key }; +} +const o = Array.isArray; +function r(e) { + return "string" == typeof e || "number" == typeof e; +} +function i(e, t, n) { + if (((e.ns = "http://www.w3.org/2000/svg"), "foreignObject" !== n && void 0 !== t)) + for (let e = 0; e < t.length; ++e) { + const n = t[e].data; + void 0 !== n && i(n, t[e].children, t[e].sel); + } +} +function l(e, t, l) { + var a, + d, + s, + u = {}; + if ((void 0 !== l ? (null !== t && (u = t), o(l) ? (a = l) : r(l) ? (d = l) : l && l.sel && (a = [l])) : null != t && (o(t) ? (a = t) : r(t) ? (d = t) : t && t.sel ? (a = [t]) : (u = t)), void 0 !== a)) + for (s = 0; s < a.length; ++s) r(a[s]) && (a[s] = n(void 0, void 0, void 0, a[s], void 0)); + return "s" !== e[0] || "v" !== e[1] || "g" !== e[2] || (3 !== e.length && "." !== e[3] && "#" !== e[3]) || i(u, a, e), n(e, u, a, d, void 0); +} +function a(e, t) { + var n, + o = t.elm, + r = e.data.attrs, + i = t.data.attrs; + if ((r || i) && r !== i) { + for (n in ((r = r || {}), (i = i || {}))) { + const e = i[n]; + r[n] !== e && + (!0 === e + ? o.setAttribute(n, "") + : !1 === e + ? o.removeAttribute(n) + : 120 !== n.charCodeAt(0) + ? o.setAttribute(n, e) + : 58 === n.charCodeAt(3) + ? o.setAttributeNS("http://www.w3.org/XML/1998/namespace", n, e) + : 58 === n.charCodeAt(5) + ? o.setAttributeNS("http://www.w3.org/1999/xlink", n, e) + : o.setAttribute(n, e)); + } + for (n in r) n in i || o.removeAttribute(n); + } +} +const d = { create: a, update: a }; +var s = ("undefined" != typeof window && window.requestAnimationFrame.bind(window)) || setTimeout, + u = !1; +function c(e, t, n) { + var o; + (o = function () { + e[t] = n; + }), + s(function () { + s(o); + }); +} +function f(e, t) { + var n, + o, + r = t.elm, + i = e.data.style, + l = t.data.style; + if ((i || l) && i !== l) { + l = l || {}; + var a = "delayed" in (i = i || {}); + for (o in i) l[o] || ("-" === o[0] && "-" === o[1] ? r.style.removeProperty(o) : (r.style[o] = "")); + for (o in l) + if (((n = l[o]), "delayed" === o && l.delayed)) for (const e in l.delayed) (n = l.delayed[e]), (a && n === i.delayed[e]) || c(r.style, e, n); + else "remove" !== o && n !== i[o] && ("-" === o[0] && "-" === o[1] ? r.style.setProperty(o, n) : (r.style[o] = n)); + } +} +const v = { + pre: function () { + u = !1; + }, + create: f, + update: f, + destroy: function (e) { + var t, + n, + o = e.elm, + r = e.data.style; + if (r && (t = r.destroy)) for (n in t) o.style[n] = t[n]; + }, + remove: function (e, t) { + var n = e.data.style; + if (n && n.remove) { + var o; + u || (e.elm.offsetLeft, (u = !0)); + var r = e.elm, + i = 0, + l = n.remove, + a = 0, + d = []; + for (o in l) d.push(o), (r.style[o] = l[o]); + for (var s = getComputedStyle(r)["transition-property"].split(", "); i < s.length; ++i) -1 !== d.indexOf(s[i]) && a++; + r.addEventListener("transitionend", function (e) { + e.target === r && --a, 0 === a && t(); + }); + } else t(); + }, +}; +function m(e, t, n) { + if ("function" == typeof e) e.call(t, n, t); + else if ("object" == typeof e) for (var o = 0; o < e.length; o++) m(e[o], t, n); +} +function p(e, t) { + var n = e.type, + o = t.data.on; + o && o[n] && m(o[n], t, e); +} +function _h(e, t) { + var n, + o = e.data.on, + r = e.listener, + i = e.elm, + l = t && t.data.on, + a = t && t.elm; + if (o !== l) { + if (o && r) + if (l) for (n in o) l[n] || i.removeEventListener(n, r, !1); + else for (n in o) i.removeEventListener(n, r, !1); + if (l) { + var d = (t.listener = + e.listener || + function e(t) { + p(t, e.vnode); + }); + if (((d.vnode = t), o)) for (n in l) o[n] || a.addEventListener(n, d, !1); + else for (n in l) a.addEventListener(n, d, !1); + } + } +} +const y = { create: _h, update: _h, destroy: _h }, + g = { + createElement: function (e) { + return document.createElement(e); + }, + createElementNS: function (e, t) { + return document.createElementNS(e, t); + }, + createTextNode: function (e) { + return document.createTextNode(e); + }, + createComment: function (e) { + return document.createComment(e); + }, + insertBefore: function (e, t, n) { + e.insertBefore(t, n); + }, + removeChild: function (e, t) { + e.removeChild(t); + }, + appendChild: function (e, t) { + e.appendChild(t); + }, + parentNode: function (e) { + return e.parentNode; + }, + nextSibling: function (e) { + return e.nextSibling; + }, + tagName: function (e) { + return e.tagName; + }, + setTextContent: function (e, t) { + e.textContent = t; + }, + getTextContent: function (e) { + return e.textContent; + }, + isElement: function (e) { + return 1 === e.nodeType; + }, + isText: function (e) { + return 3 === e.nodeType; + }, + isComment: function (e) { + return 8 === e.nodeType; + }, + }; +function x(e) { + return void 0 === e; +} +function b(e) { + return void 0 !== e; +} +const C = n("", {}, [], void 0, void 0); +function k(e, t) { + return e.key === t.key && e.sel === t.sel; +} +function w(e, t, n) { + var o; + const r = {}; + for (let i = t; i <= n; ++i) { + const t = null === (o = e[i]) || void 0 === o ? void 0 : o.key; + void 0 !== t && (r[t] = i); + } + return r; +} +const A = ["create", "update", "remove", "destroy", "pre", "post"]; +function N(e, t) { + let i, l; + const a = { create: [], update: [], remove: [], destroy: [], pre: [], post: [] }, + d = void 0 !== t ? t : g; + for (i = 0; i < A.length; ++i) + for (a[A[i]] = [], l = 0; l < e.length; ++l) { + const t = e[l][A[i]]; + void 0 !== t && a[A[i]].push(t); + } + function s(e) { + const t = e.id ? "#" + e.id : "", + o = e.className ? "." + e.className.split(" ").join(".") : ""; + return n(d.tagName(e).toLowerCase() + t + o, {}, [], void 0, e); + } + function u(e, t) { + return function () { + if (0 == --t) { + const t = d.parentNode(e); + d.removeChild(t, e); + } + }; + } + function c(e, t) { + var n, i; + let l, + s = e.data; + if (void 0 !== s) { + const t = null === (n = s.hook) || void 0 === n ? void 0 : n.init; + b(t) && (t(e), (s = e.data)); + } + const u = e.children, + f = e.sel; + if ("!" === f) x(e.text) && (e.text = ""), (e.elm = d.createComment(e.text)); + else if (void 0 !== f) { + const n = f.indexOf("#"), + v = f.indexOf(".", n), + m = n > 0 ? n : f.length, + p = v > 0 ? v : f.length, + h = -1 !== n || -1 !== v ? f.slice(0, Math.min(m, p)) : f, + y = (e.elm = b(s) && b((l = s.ns)) ? d.createElementNS(l, h) : d.createElement(h)); + for (m < p && y.setAttribute("id", f.slice(m + 1, p)), v > 0 && y.setAttribute("class", f.slice(p + 1).replace(/\./g, " ")), l = 0; l < a.create.length; ++l) a.create[l](C, e); + if (o(u)) + for (l = 0; l < u.length; ++l) { + const e = u[l]; + null != e && d.appendChild(y, c(e, t)); + } + else r(e.text) && d.appendChild(y, d.createTextNode(e.text)); + const g = e.data.hook; + b(g) && (null === (i = g.create) || void 0 === i || i.call(g, C, e), g.insert && t.push(e)); + } else e.elm = d.createTextNode(e.text); + return e.elm; + } + function f(e, t, n, o, r, i) { + for (; o <= r; ++o) { + const r = n[o]; + null != r && d.insertBefore(e, c(r, i), t); + } + } + function v(e) { + var t, n; + const o = e.data; + if (void 0 !== o) { + null === (n = null === (t = null == o ? void 0 : o.hook) || void 0 === t ? void 0 : t.destroy) || void 0 === n || n.call(t, e); + for (let t = 0; t < a.destroy.length; ++t) a.destroy[t](e); + if (void 0 !== e.children) + for (let t = 0; t < e.children.length; ++t) { + const n = e.children[t]; + null != n && "string" != typeof n && v(n); + } + } + } + function m(e, t, n, o) { + for (var r, i; n <= o; ++n) { + let o, l; + const s = t[n]; + if (null != s) + if (b(s.sel)) { + v(s), (o = a.remove.length + 1), (l = u(s.elm, o)); + for (let e = 0; e < a.remove.length; ++e) a.remove[e](s, l); + const e = null === (i = null === (r = null == s ? void 0 : s.data) || void 0 === r ? void 0 : r.hook) || void 0 === i ? void 0 : i.remove; + b(e) ? e(s, l) : l(); + } else d.removeChild(e, s.elm); + } + } + function p(e, t, n) { + var o, r, i, l, s; + const u = null === (o = t.data) || void 0 === o ? void 0 : o.hook; + null === (r = null == u ? void 0 : u.prepatch) || void 0 === r || r.call(u, e, t); + const v = (t.elm = e.elm), + h = e.children, + y = t.children; + if (e !== t) { + if (void 0 !== t.data) { + for (let n = 0; n < a.update.length; ++n) a.update[n](e, t); + null === (l = null === (i = t.data.hook) || void 0 === i ? void 0 : i.update) || void 0 === l || l.call(i, e, t); + } + x(t.text) + ? b(h) && b(y) + ? h !== y && + (function (e, t, n, o) { + let r, + i, + l, + a, + s = 0, + u = 0, + v = t.length - 1, + h = t[0], + y = t[v], + g = n.length - 1, + b = n[0], + C = n[g]; + for (; s <= v && u <= g; ) + null == h + ? (h = t[++s]) + : null == y + ? (y = t[--v]) + : null == b + ? (b = n[++u]) + : null == C + ? (C = n[--g]) + : k(h, b) + ? (p(h, b, o), (h = t[++s]), (b = n[++u])) + : k(y, C) + ? (p(y, C, o), (y = t[--v]), (C = n[--g])) + : k(h, C) + ? (p(h, C, o), d.insertBefore(e, h.elm, d.nextSibling(y.elm)), (h = t[++s]), (C = n[--g])) + : k(y, b) + ? (p(y, b, o), d.insertBefore(e, y.elm, h.elm), (y = t[--v]), (b = n[++u])) + : (void 0 === r && (r = w(t, s, v)), + (i = r[b.key]), + x(i) ? d.insertBefore(e, c(b, o), h.elm) : ((l = t[i]), l.sel !== b.sel ? d.insertBefore(e, c(b, o), h.elm) : (p(l, b, o), (t[i] = void 0), d.insertBefore(e, l.elm, h.elm))), + (b = n[++u])); + (s <= v || u <= g) && (s > v ? ((a = null == n[g + 1] ? null : n[g + 1].elm), f(e, a, n, u, g, o)) : m(e, t, s, v)); + })(v, h, y, n) + : b(y) + ? (b(e.text) && d.setTextContent(v, ""), f(v, null, y, 0, y.length - 1, n)) + : b(h) + ? m(v, h, 0, h.length - 1) + : b(e.text) && d.setTextContent(v, "") + : e.text !== t.text && (b(h) && m(v, h, 0, h.length - 1), d.setTextContent(v, t.text)), + null === (s = null == u ? void 0 : u.postpatch) || void 0 === s || s.call(u, e, t); + } + } + return function (e, t) { + let n, o, r; + const i = []; + for (n = 0; n < a.pre.length; ++n) a.pre[n](); + for ( + (function (e) { + return void 0 !== e.sel; + })(e) || (e = s(e)), + k(e, t) ? p(e, t, i) : ((o = e.elm), (r = d.parentNode(o)), c(t, i), null !== r && (d.insertBefore(r, t.elm, d.nextSibling(o)), m(r, [e], 0, 0))), + n = 0; + n < i.length; + ++n + ) + i[n].data.hook.insert(i[n]); + for (n = 0; n < a.post.length; ++n) a.post[n](); + return t; + }; +} +function T(e, t, n, o) { + return ( + (o = o || ((e, t) => (Array.isArray(e) ? Array.isArray(t) && e.length === t.length && e.reduce((e, n, o) => e && n === t[o], !0) : e === t))), + l("memo", { + key: e, + arg: n, + hook: { + init(o) { + Object.assign(o, t(n)), + (o.data.hook = o.data.hook || {}), + (o.data.hook.create = () => { + (o.sel = "memo"), (o.key = e), (o.data.arg = n); + }); + }, + prepatch(r, i) { + o(r.data.arg, n) ? Object.assign(i, r) : (Object.assign(i, t(n)), (i.sel = "memo"), (i.key = e), (i.data.arg = n)); + }, + }, + }) + ); +} +export const attributesModule = d; +export const eventListenersModule = y; +export const h = l; +export const init = N; +export const memo = T; +export const styleModule = v; \ No newline at end of file diff --git a/src/Fable.Repl.Lib/Sutil/Types.fs b/src/Fable.Repl.Lib/Sutil/Types.fs index abf6aa07..fe439750 100644 --- a/src/Fable.Repl.Lib/Sutil/Types.fs +++ b/src/Fable.Repl.Lib/Sutil/Types.fs @@ -181,40 +181,40 @@ module Cmd = let start x = Timer.delay 0 (fun _ -> Async.StartImmediate x) #else - let inline start x = Async.Start x + let start x = Async.Start x #endif /// Command that will evaluate an async block and map the result /// into success or error (of exception) - let inline either (task: 'a -> Async<_>) (arg: 'a) (ofSuccess: _ -> 'msg) (ofError: _ -> 'msg) : Cmd<'msg> = + let either (task: 'a -> Async<_>) (arg: 'a) (ofSuccess: _ -> 'msg) (ofError: _ -> 'msg) : Cmd<'msg> = OfAsyncWith.either start task arg ofSuccess ofError /// Command that will evaluate an async block and map the success - let inline perform (task: 'a -> Async<_>) (arg: 'a) (ofSuccess: _ -> 'msg) : Cmd<'msg> = + let perform (task: 'a -> Async<_>) (arg: 'a) (ofSuccess: _ -> 'msg) : Cmd<'msg> = OfAsyncWith.perform start task arg ofSuccess /// Command that will evaluate an async block and map the error (of exception) - let inline attempt (task: 'a -> Async<_>) (arg: 'a) (ofError: _ -> 'msg) : Cmd<'msg> = + let attempt (task: 'a -> Async<_>) (arg: 'a) (ofError: _ -> 'msg) : Cmd<'msg> = OfAsyncWith.attempt start task arg ofError /// Command that will evaluate an async block to the message - let inline result (task: Async<'msg>) : Cmd<'msg> = OfAsyncWith.result start task + let result (task: Async<'msg>) : Cmd<'msg> = OfAsyncWith.result start task module OfAsyncImmediate = /// Command that will evaluate an async block and map the result /// into success or error (of exception) - let inline either (task: 'a -> Async<_>) (arg: 'a) (ofSuccess: _ -> 'msg) (ofError: _ -> 'msg) : Cmd<'msg> = + let either (task: 'a -> Async<_>) (arg: 'a) (ofSuccess: _ -> 'msg) (ofError: _ -> 'msg) : Cmd<'msg> = OfAsyncWith.either Async.StartImmediate task arg ofSuccess ofError /// Command that will evaluate an async block and map the success - let inline perform (task: 'a -> Async<_>) (arg: 'a) (ofSuccess: _ -> 'msg) : Cmd<'msg> = + let perform (task: 'a -> Async<_>) (arg: 'a) (ofSuccess: _ -> 'msg) : Cmd<'msg> = OfAsyncWith.perform Async.StartImmediate task arg ofSuccess /// Command that will evaluate an async block and map the error (of exception) - let inline attempt (task: 'a -> Async<_>) (arg: 'a) (ofError: _ -> 'msg) : Cmd<'msg> = + let attempt (task: 'a -> Async<_>) (arg: 'a) (ofError: _ -> 'msg) : Cmd<'msg> = OfAsyncWith.attempt Async.StartImmediate task arg ofError /// Command that will evaluate an async block to the message - let inline result (task: Async<'msg>) : Cmd<'msg> = + let result (task: Async<'msg>) : Cmd<'msg> = OfAsyncWith.result Async.StartImmediate task #if FABLE_COMPILER @@ -256,7 +256,7 @@ module Cmd = [ bind ] [] - let inline ofPromise + let ofPromise (task: 'a -> Fable.Core.JS.Promise<_>) (arg: 'a) (ofSuccess: _ -> 'msg) @@ -268,22 +268,22 @@ module Cmd = module OfTask = /// Command to call a task and map the results - let inline either (task: 'a -> Task<_>) (arg: 'a) (ofSuccess: _ -> 'msg) (ofError: _ -> 'msg) : Cmd<'msg> = + let either (task: 'a -> Task<_>) (arg: 'a) (ofSuccess: _ -> 'msg) (ofError: _ -> 'msg) : Cmd<'msg> = OfAsync.either (task >> Async.AwaitTask) arg ofSuccess ofError /// Command to call a task and map the success - let inline perform (task: 'a -> Task<_>) (arg: 'a) (ofSuccess: _ -> 'msg) : Cmd<'msg> = + let perform (task: 'a -> Task<_>) (arg: 'a) (ofSuccess: _ -> 'msg) : Cmd<'msg> = OfAsync.perform (task >> Async.AwaitTask) arg ofSuccess /// Command to call a task and map the error - let inline attempt (task: 'a -> Task<_>) (arg: 'a) (ofError: _ -> 'msg) : Cmd<'msg> = + let attempt (task: 'a -> Task<_>) (arg: 'a) (ofError: _ -> 'msg) : Cmd<'msg> = OfAsync.attempt (task >> Async.AwaitTask) arg ofError /// Command and map the task success - let inline result (task: Task<'msg>) : Cmd<'msg> = + let result (task: Task<'msg>) : Cmd<'msg> = OfAsync.result (task |> Async.AwaitTask) [] - let inline ofTask (task: 'a -> Task<_>) (arg: 'a) (ofSuccess: _ -> 'msg) (ofError: _ -> 'msg) : Cmd<'msg> = + let ofTask (task: 'a -> Task<_>) (arg: 'a) (ofSuccess: _ -> 'msg) (ofError: _ -> 'msg) : Cmd<'msg> = OfTask.either task arg ofSuccess ofError #endif diff --git a/src/metadata/Fable.Repl.Lib.dll b/src/metadata/Fable.Repl.Lib.dll index 7f23e5f3..3e6ea3eb 100644 Binary files a/src/metadata/Fable.Repl.Lib.dll and b/src/metadata/Fable.Repl.Lib.dll differ