{"id":15059401,"url":"https://github.com/lue-bird/elm-state-interface","last_synced_at":"2025-04-10T05:30:25.721Z","repository":{"id":206319251,"uuid":"716354275","full_name":"lue-bird/elm-state-interface","owner":"lue-bird","description":"define an app in a simple, safe and declarative way","archived":false,"fork":false,"pushed_at":"2024-10-05T00:37:11.000Z","size":1504,"stargazers_count":16,"open_issues_count":0,"forks_count":2,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-05T00:11:31.384Z","etag":null,"topics":["architecture","declarative","elm","simple","tea","the-elm-architecture"],"latest_commit_sha":null,"homepage":"https://dark.elm.dmy.fr/packages/lue-bird/elm-state-interface/latest/","language":"Elm","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lue-bird.png","metadata":{"files":{"readme":"README.md","changelog":"changes.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-11-09T00:44:47.000Z","updated_at":"2025-03-29T15:17:45.000Z","dependencies_parsed_at":"2023-11-22T02:31:36.684Z","dependency_job_id":"7ad911b2-403f-4582-bd2a-dddf8b68b70e","html_url":"https://github.com/lue-bird/elm-state-interface","commit_stats":null,"previous_names":["lue-bird/from-model-view-update-sub-cmd-to-state-interface","lue-bird/elm-state-interface"],"tags_count":16,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lue-bird%2Felm-state-interface","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lue-bird%2Felm-state-interface/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lue-bird%2Felm-state-interface/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lue-bird%2Felm-state-interface/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lue-bird","download_url":"https://codeload.github.com/lue-bird/elm-state-interface/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248162883,"owners_count":21057829,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["architecture","declarative","elm","simple","tea","the-elm-architecture"],"created_at":"2024-09-24T22:43:06.850Z","updated_at":"2025-04-10T05:30:25.689Z","avatar_url":"https://github.com/lue-bird.png","language":"Elm","funding_links":[],"categories":[],"sub_categories":[],"readme":"### define an app in a simple, safe and declarative way\n\n\u003e 🌱 New to elm? Learn about [the basics](https://guide.elm-lang.org/core_language) and [types](https://guide.elm-lang.org/types/), [install elm](https://guide.elm-lang.org/install/elm) and set up an editor like [vs code](https://github.com/elm-tooling/elm-language-client-vscode?tab=readme-ov-file#install)\n\n\u003e If you know TEA, [get quick overview of the differences](#comparison-to-the-elm-architecture)\n\nHere's a simple app that shows a number and a button that increments it\n\n```elm\nimport Web\nimport Web.Dom\n\napp =\n    { initialState = 0\n    , interface =\n        \\state -\u003e\n            Web.Dom.element \"div\"\n                []\n                [ state |\u003e String.fromInt |\u003e Web.Dom.text\n                , Web.Dom.element \"button\"\n                    [ Web.Dom.listenTo \"click\" ]\n                    [ \"+\" |\u003e Web.Dom.text ]\n                    |\u003e Web.Dom.futureMap (\\_ -\u003e state + 1)\n                ]\n                |\u003e Web.Dom.render\n    }\n```\n\n\u003e To play around with the examples, set up a [playground](https://github.com/lue-bird/elm-state-interface-hello):\n\u003e ```bash\n\u003e git clone https://github.com/lue-bird/elm-state-interface-hello.git \u0026\u0026 cd elm-state-interface-hello \u0026\u0026 npm install \u0026\u0026 npx vite\n\u003e ```\n\u003e http://localhost:5173/ now shows your app. Open `src/App.elm` in your editor to paste in examples.\n\nThe \"state\" is everything your app knows internally. Here it's the counter number, starting at 0.\n\nWe build the interface to the outside world (html, audio, server communication, ...) based on our current state.\nIn our example, this function has the type\n```elm\ninterface : Int -\u003e Interface Int\n```\n`Interface Int` means that the interface can come back with an `Int` some time in the future,\nin our case an incremented counter state.\nThe app will use this result as the new state.\n\nWe use [`Web.Dom.listenTo`](https://dark.elm.dmy.fr/packages/lue-bird/elm-state-interface/latest/Web-Dom#listenTo) to be notified when a user clicks the button.\nWithout [`Web.Dom.futureMap`](https://dark.elm.dmy.fr/packages/lue-bird/elm-state-interface/latest/Web-Dom#futureMap), our interface would have the type\n```elm\nWeb.Dom.element \"button\" [ Web.Dom.listenTo \"click\" ] []\n    |\u003e Web.Dom.render\n: Interface Json.Decode.Value\n```\nwhich means this interface will on click come back with an event as [json](https://dark.elm.dmy.fr/packages/elm/json/latest/).\nLater, we'll see how to get information out of this kind of event.\n\nRight now, we use [`Web.Dom.futureMap`](https://dark.elm.dmy.fr/packages/lue-bird/elm-state-interface/latest/Web-Dom#futureMap) to change the information the interface will send back to us in the future by just ignoring the event `\\_ -\u003e` and returning the incremented state.\n\nYou can change the value that comes back from an interface in many places, like\n```elm\nWeb.Dom.element \"button\" [ Web.Dom.listenTo \"click\" ] []\n    |\u003e Web.Dom.render\n    |\u003e Web.interfaceFutureMap (\\_ -\u003e state + 1)\n: Interface Int\n```\nor\n```elm\nWeb.Dom.element \"button\"\n    [ Web.Dom.listenTo \"click\"\n        |\u003e Web.Dom.modifierFutureMap (\\_ -\u003e state + 1)\n    , Web.Dom.listenTo \"dblclick\"\n        |\u003e Web.Dom.modifierFutureMap (\\_ -\u003e state + 10)\n    ]\n    []\n    |\u003e Web.Dom.render\n: Interface Int\n```\nBasically anywhere a type used for an interface has data it can come back with.\n\nIf we only perform actions without any events coming back, we get the type\n```elm\n\"never change\" |\u003e Web.Dom.text |\u003e Web.Dom.render\n: Interface nothingEverComesBack\n\n\"never change\" |\u003e Web.Console.log\n: Interface nothingEverComesBack\n```\nBecause `nothingEverComesBack` is a variable, it will fit for any other interface type.\n\n\nIt's nice to give this number state you pass around a name\n\n```elm\nimport Web\nimport Web.Dom\n\ntype State\n    = Counter Int\n\napp : { initialState : State, interface : State -\u003e Web.Interface State }\napp =\n    { initialState = Counter 0\n    , interface =\n        \\(Counter counter) -\u003e\n            Web.Dom.element \"div\"\n                []\n                [ counter |\u003e String.fromInt |\u003e Web.Dom.text\n                , Web.Dom.element \"button\"\n                    [ Web.Dom.listenTo \"click\"\n                        |\u003e Web.Dom.modifierFutureMap\n                            (\\_ -\u003e Counter (counter + 1))\n                    ]\n                    [ \"+\" |\u003e Web.Dom.text ]\n                ]\n                |\u003e Web.Dom.render\n    }\n```\nIt allows us to annotate pieces of our app,\nprevents mixing up numbers with your state values\nand makes it easy to in the future add other possible states.\n\nTry adding another button that decrements the shown number.\n\nDone?\nHere we go:\n\n```elm\nimport Web\nimport Web.Dom\n\ntype State\n    = Counter Int\n\napp =\n    { initialState = Counter 0\n    , interface =\n        \\(Counter counter) -\u003e\n            Web.Dom.element \"div\"\n                []\n                [ Web.Dom.element \"button\"\n                    [ Web.Dom.listenTo \"click\"\n                        |\u003e Web.Dom.modifierFutureMap\n                            (\\_ -\u003e Counter (counter + 1))\n                    ]\n                    [ \"+\" |\u003e Web.Dom.text ]\n                , counter |\u003e String.fromInt |\u003e Web.Dom.text\n                , Web.Dom.element \"button\"\n                    [ Web.Dom.listenTo \"click\"\n                        |\u003e Web.Dom.modifierFutureMap\n                            (\\_ -\u003e Counter (counter - 1))\n                    ]\n                    [ \"-\" |\u003e Web.Dom.text ]\n                ]\n                |\u003e Web.Dom.render\n    }\n```\n\nit can be nice to separate behaviour from the interface by explicitly listing all the possible things we expect to see on the outside\n\n```elm\nimport Web\nimport Web.Dom\n\ntype State\n    = Counter Int\n\ntype Event\n    = MinusClicked\n    | PlusClicked\n\napp =\n    { initialState = WaitingForInitialUrl\n    , interface =\n        \\(Counter counter) -\u003e\n            Web.Dom.element \"div\"\n                []\n                [ Web.Dom.element \"button\"\n                    [ Web.Dom.listenTo \"click\"\n                        |\u003e Web.Dom.modifierFutureMap (\\_ -\u003e PlusClicked)\n                    ]\n                    [ \"+\" |\u003e Web.Dom.text ]\n                , Web.Dom.element \"div\"\n                    []\n                    [ counter |\u003e String.fromInt |\u003e Web.Dom.text ]\n                , Web.Dom.element \"button\"\n                    [ Web.Dom.listenTo \"click\"\n                        |\u003e Web.Dom.modifierFutureMap (\\_ -\u003e MinusClicked)\n                    ]\n                    [ \"-\" |\u003e Web.Dom.text ]\n                ]\n                |\u003e Web.Dom.render\n                |\u003e Web.interfaceFutureMap\n                    (\\event -\u003e\n                        case event of\n                            MinusClicked -\u003e\n                                Counter (counter - 1)\n                            \n                            PlusClicked -\u003e\n                                Counter (counter + 1)\n                    )\n    }\n```\nNow you just need a quick look at `Event` to see what kind of user interactions the app will react to.\n\nSoon we'll extend this example app with the ability to manage its url.\nBefore that, we have to know when anything from the interface actually triggers something on the outside.\n\nIn this example we have a text input field together with\na text showing whether the entered text is a palindrome or not.\n\n```elm\nimport Web\nimport Web.Dom\n\ntype State\n    = State { text : String, warnings : List String }\n\ntype Event\n    = TextFieldContentChanged (Result Json.Decode.Error String)\n\napp =\n    { initialState = State { text = \"\", warnings = [] }\n    , interface =\n        \\(State state) -\u003e\n            [ Web.Dom.element \"div\"\n                []\n                [ Web.Dom.element \"input\"\n                    [ Web.Dom.stringProperty \"value\" state.text\n                    , Web.Dom.listenTo \"change\"\n                        |\u003e Web.Dom.modifierFutureMap\n                            (\\eventJson -\u003e\n                                eventJson\n                                    |\u003e Json.Decode.decodeString\n                                        (Json.Decode.field \"target\" (Json.Decode.field \"value\" Json.Decode.string))\n                                    |\u003e TextFieldContentChanged\n                            )\n                    ]\n                    []\n                , Web.Dom.text\n                    (if state.text == (state.text |\u003e String.reverse) then\n                        \"is a palindrome\"\n                    \n                     else\n                        \"is not a palindrome\"\n                    )\n                ]\n                |\u003e Web.Dom.render\n            , state.warnings\n                |\u003e List.map (\\warning -\u003e Web.Console.warn warning)\n                |\u003e Web.interfaceBatch\n            ]\n                |\u003e Web.interfaceBatch\n                |\u003e Web.interfaceFutureMap\n                    (\\event -\u003e\n                        case event of\n                            TextFieldContentChanged (Ok newText) -\u003e\n                                State { state | text = newText }\n\n                            TextFieldContentChanged (Err error) -\u003e\n                                State\n                                    { state\n                                        | warnings =\n                                            state.warnings\n                                                |\u003e (::) (error |\u003e Json.Decode.errorToString)\n                                    }\n                    )\n    }\n```\nTo learn about these \"json decoder\" things, you can read [the official guide](https://guide.elm-lang.org/effects/json). You can skip the first section with the app code.\n\nNow, how does the `warnings` thing work?\n\u003e **an `Interface` for an action ≠ performing that action now**\n\n```elm\ninterface =\n    \\_ -\u003e\n        Web.Console.log \"Hello\"\n```\nwhen will it print to the console? All the time? Every time the state changes?\n\nHere's where an `Interface` is different from a command and similar imperative code.\nThere are 2 triggers:\n  - the updated `Interface` has an interface the old `Interface` didn't.\n    E.g. since we log each individual warning in the `Interface`, whenever we add a new warning it will be logged\n  - a previously existing interface is absent in the updated `Interface`.\n    E.g. if we play menu music and now switch to the game interface which doesn't include this music, the menu music will stop.\n    Or similarly, if we now don't include an HTTP GET request that we once had in the `Interface`,\n    the request gets canceled if it's still active.\n\nHere's a little exercise for the first point:\n```elm\ninterface =\n    \\state -\u003e\n        Web.Console.log\n            (case state |\u003e Basics.remainderBy 2 of\n                0 -\u003e\n                    \"even\"\n                \n                _ -\u003e\n                    \"odd\"\n            )\n```\nthe state will take on the values: `1 → 2 → 2 → 4`. What will have been printed to the console?\n\nDone? Here's the solution\n  - `state → 1`: we had no prior interface, so the `Web.Console.log \"odd\"` is a new addition. `odd` is printed\n  - `state → 2`: our prior interface had `Web.Console.log \"odd\"` which is not part of the new one.\n    But there's a new additional interface: `Web.Console.log \"even\"`. `even` is printed\n  - `state → 2`: our prior interface had `Web.Console.log \"even\"` which is also part of the new one. Nothing is printed\n  - `state → 4`: our prior interface had `Web.Console.log \"even\"` which is also part of the new one. Nothing is printed\n\nSo in the end, the app will have printed\n```\neven\nodd\n```\n\nThat being said, I encourage you to not think about when exactly certain actions on the outside are triggered. Try just thinking along the lines of\n\u003e \"oh, if the app is in this state, these actions will have happened at some point\"\n\nand not worry about what interfaces could have been constructed previously.\n\nNow then, here's the promised counter+url example\n```elm\nimport Web\nimport Web.Dom\nimport AppUrl exposing (AppUrl) -- lydell/elm-app-url\n\ntype State\n    = Counter Int\n    | WaitingForInitialUrl\n\ntype CounterEvent\n    = MinusClicked\n    | PlusClicked\n    | UserWentToUrl AppUrl\n\napp =\n    { initialState = WaitingForInitialUrl\n    , interface = interface\n    }\n\ninterface : State -\u003e Web.Interface State\ninterface =\n    \\state -\u003e\n        case state of\n            Counter counter -\u003e\n                [ Web.Dom.element \"div\"\n                    []\n                    [ Web.Dom.element \"button\"\n                        [ Web.Dom.listenTo \"click\" ]\n                        [ \"+\" |\u003e Web.Dom.text ]\n                        |\u003e Web.Dom.futureMap (\\_ -\u003e PlusClicked)\n                    , Web.Dom.element \"div\"\n                        []\n                        [ counter |\u003e String.fromInt |\u003e Web.Dom.text ]\n                    , Web.Dom.element \"button\"\n                        [ Web.Dom.listenTo \"click\" ]\n                        [ \"-\" |\u003e Web.Dom.text ]\n                        |\u003e Web.Dom.futureMap (\\_ -\u003e MinusClicked)\n                    ]\n                    |\u003e Web.Dom.render\n                , Web.Navigation.pushUrl\n                    { path = []\n                    , queryParameters = Dict.singleton \"counter\" [ counter |\u003e String.fromInt ]\n                    , fragment = Nothing\n                    }\n                , Web.Navigation.movementListen |\u003e Web.interfaceFutureMap UserWentToUrl\n                ]\n                    |\u003e Web.interfaceBatch\n                    |\u003e Web.interfaceFutureMap\n                        (\\event -\u003e\n                            case event of\n                                MinusClicked -\u003e\n                                    Counter (counter - 1)\n                                \n                                PlusClicked -\u003e\n                                    Counter (counter + 1)\n                                \n                                UserWentToUrl newUrl -\u003e\n                                    Counter (newUrl |\u003e counterUrlParse |\u003e Maybe.withDefault counter)\n                        )\n            \n            WaitingForInitialUrl -\u003e\n                Web.Navigation.urlRequest\n                    |\u003e Web.interfaceFutureMap\n                        (\\initialUrl -\u003e\n                            Counter (initialUrl |\u003e counterUrlParse |\u003e Maybe.withDefault 0)\n                        )\n\ncounterUrlParse : AppUrl -\u003e Maybe Int\ncounterUrlParse appUrl =\n    appUrl.queryParameters\n        |\u003e Dict.get \"counter\"\n        |\u003e Maybe.andThen List.head\n        |\u003e Maybe.map String.fromInt\n```\nSince events like a click on the minus button can only happen if we're in the `Counter` state,\nwe have everything we need to update the state.\n\nIf you want to learn a bit more about app url parsing and building, visit [lydell/elm-app-url](https://dark.elm.dmy.fr/packages/lydell/elm-app-url/latest/)\n\nAnd what's the deal with [`movementListen`](https://dark.elm.dmy.fr/packages/lue-bird/elm-state-interface/latest/Web-Navigation#movementListen) vs [`urlRequest`](https://dark.elm.dmy.fr/packages/lue-bird/elm-state-interface/latest/Web-Navigation#urlRequest)?\nDon't both just give you the latest url?\n\n\u003e **an `Interface` that requests ≠ `Interface` that listens**\n\nThe Elm Architecture uses command/task types for one and subscription types for the other.\nIn state-interface, these 2 look identical:\n```elm\nWeb.Window.sizeRequest : Interface { width : Int, height : Int }\nWeb.Window.resizeListen : Interface { width : Int, height : Int }\n```\n\"-Listen\" is equivalent to subscription in The Elm Architecture, \"-Request\" is roughly like command/task.\nSo trying to keep your window size state updated using\n```elm\nWeb.Window.sizeRequest\n    |\u003e Web.interfaceFutureMap (\\windowSize -\u003e { state | windowSize = windowSize })\n```\nis not going to work as the request will only be executed once.\n\nSo the full solution to always get the current window size is\n```elm\n[ Web.Window.sizeRequest, Web.Window.resizeListen ]\n    |\u003e Web.interfaceBatch\n    |\u003e Web.interfaceFutureMap (\\windowSize -\u003e { state | windowSize = windowSize })\n```\n  - `sizeRequest` will send you the initial window size first, then never again\n  - `resizeListen` sends you all the changes to the size\n    (for as long as you have it in your interface)\n\nWhy can't we do the same in the counter + url example above?\n```elm\n[ Navigation.urlRequest, Web.Navigation.movementListen ]\n    |\u003e Web.interfaceBatch\n    |\u003e Web.interfaceFutureMap UserWentToUrl\n```\nIn combination with editing the url programmatically\nyou have to keep one thing in mind:\nIt could happen that you push a new url before the requested initial url is sent to you\nin which case you'll receive the url pushed by you.\n\n\u003e Whenever **order of actions** is important, let your **state** represent that!\n\n\n## what we need to actually run it as an elm program\n\nFor a minimal working setup, the [playground](https://github.com/lue-bird/elm-state-interface-hello) has everything you need.\nFor example apps, see [example/](https://github.com/lue-bird/elm-state-interface/tree/main/example). You'll see that the basic setup hasn't changed.\n\nIn case you want to create your own setup instead:\n\n```elm\nport module Main exposing (main)\n```\n```elm\nimport Web\nimport Json.Encode -- elm/json\n\nmain : Web.Program ..your state type..\nmain =\n    Web.program\n        { initialState = ..your initial state..\n        , interface = ..your interface..\n        , ports = { fromJs = fromJs, toJs = toJs }\n        }\n\nport toJs : Json.Encode.Value -\u003e Cmd event_\nport fromJs : (Json.Encode.Value -\u003e event) -\u003e Sub event\n```\n\nThese \"ports\" are the connection points to the actual implementations of the interfaces.\nTo set them up:\n\n```bash\nnpm install @lue-bird/elm-state-interface\n```\nin js\n```javascript\nimport * as Web from \"@lue-bird/elm-state-interface\";\n// import your Main.elm. Name and path depend on bundler+plugin\n\nconst elmApp = Main.init();\nWeb.programStart({\n    ports : elmApp.ports,\n    domElement : document.getElementById(\"your-app-element-id\")\n});\n```\n\nIf you're not familiar with The Elm Architecture, skip to [\"future\"](#future)\n\n## comparison to The Elm Architecture\n\n[This discourse post](https://discourse.elm-lang.org/t/define-an-app-in-a-simple-safe-and-declarative-way-using-state-interface/9748) goes a bit more into detail but in short:\nThe Elm Architecture with its model, view, msg, update, sub, cmd, task\ncan be reduced down to a state and a view/subscriptions-like `state -\u003e Interface state` function that combines what you'd use cmds, tasks, subs and view for.\nThis means\n\n  - actions (like changing the url) aren't tied to a specific event happening but to a specific state\n  - update is part of the interface and having an intermediate event type is optional (but often useful)\n  - when updating based on an event, there's no need to case on the relevant state. (Either use the state from the `case` in the interface in an inner update, or safely include the state in the event)\n\n[jump back up](#define-an-app-in-a-simple-safe-and-declarative-way)\n\n## comparison to tasks\n\nSimplified examples.\n\nwith [`andrewMacmurray/elm-concurrent-task`](https://dark.elm.dmy.fr/packages/andrewMacmurray/elm-concurrent-task/latest/):\n```elm\ntype alias State =\n    Result\n        Http.NonSuccessStatus\n        { icon : Image\n        , content : String\n        }\n\ntype Event\n    = IconAndContentArrived (Result Http.Error { icon : Image, content : String })\n\n{ init =\n    \\() -\u003e\n        ( Err Http.NotAsked\n        , ConcurrentTask.succeed (\\icon content -\u003e { icon = icon, content = content })\n            |\u003e ConcurrentTask.andMap\n                (Http.request { url = \"...\", decoder = Image.jsonDecoder })\n            |\u003e ConcurrentTask.andMap\n                (Http.request { url = \"...\", decoder = Json.Decode.string })\n            |\u003e ConcurrentTask.attempt { onComplete = IconAndContentArrived }\n        )\n, update =\n    \\event state -\u003e\n        case event of\n            IconAndContentArrived iconAndContent -\u003e\n                ( iconAndContent |\u003e Result.mapError Http.Error\n                , Cmd.none\n                )\n, view =\n    \\state -\u003e\n        case state of\n            Ok iconAndContent -\u003e\n                ..your ui using iconAndContent..\n            \n            Err ... -\u003e\n                ..error ui..\n, subscriptions = ...\n}\n```\nwith state-interface:\n```elm\ntype alias State =\n    { icon : Result Http.NonSuccessStatus Image\n    , content : Result Http.NonSuccessStatus String\n    }\n\n{ initialState = { icon = Err Http.NotAsked, content = Err Http.NotAsked }\n, interface =\n    \\state -\u003e\n        case ( state.icon, state.content ) of\n            ( Ok icon, Ok content ) -\u003e\n                ..your ui using icon and content..\n            \n            _ -\u003e\n                [ case state.icon of\n                    Ok _ -\u003e\n                        Web.interfaceNone\n                    Err _ -\u003e\n                        Http.request { url = \"...\", decoder = Image.jsonDecoder }\n                            |\u003e Web.interfaceFutureMap (\\result -\u003e { state | icon = result })\n                , case state.content of\n                    Ok _ -\u003e\n                        Web.interfaceNone\n                    Err _ -\u003e\n                        Http.request { url = \"...\", decoder = Json.Decode.string }\n                            |\u003e Web.interfaceFutureMap (\\result -\u003e { state | content = result })\n                , ..error ui..\n                ]\n                    |\u003e Web.interfaceBatch\n}\n```\nwhich feels a bit more explicit, declarative and less wiring-heavy at least.\n\nNote: This example is only supposed to show differences in architecture.\nUnlike [`andrewMacmurray/elm-concurrent-task`](https://dark.elm.dmy.fr/packages/andrewMacmurray/elm-concurrent-task/latest/), `elm-state-interface` does not allow custom tasks/interfaces.\nInstead, the goal of this package is to publish more browser APIs like gamepads instead of users doing the work only for their own projects. Since I'm a noob in the js world, feedback and contributions are super welcome ❀\n\n## present\n\nThere should be feature-parity with elm's exposed browser APIs ([tell me](https://github.com/lue-bird/elm-state-interface/issues/new) if I've missed some!) plus a couple of APIs that elm's exposed browser APIs don't offer, including websockets, localstorage, audio, clipboard.\n\nFor now, some more niche interfaces like [`WebGL.Texture.loadWith`](https://dark.elm.dmy.fr/packages/elm-explorations/webgl/latest/WebGL-Texture#loadWith) are left out.\n\n## future\n\n  - ⛵ add more [example projects](https://github.com/lue-bird/elm-state-interface/tree/main/example). Would you like to see something specific? Or maybe you're motivated to make one yourself 👀\n  - 📐 `Web.Dom.element \"div\" ...` etc are a bit clumsy. Sadly, most ui packages out there only convert to a type inaccessible to `state-interface`, making them incompatible. Though a port of them would be awesome, a good first step may be creating a package for generating the html/svg/... elements, inspired by [`Orasund/elm-html-style`](https://dark.elm.dmy.fr/packages/Orasund/elm-html-style/latest/)\n  - 🔋 add the web APIs you miss the most. Maybe [MIDI](https://developer.mozilla.org/en-US/docs/Web/API/Web_MIDI_API), [speech](https://developer.mozilla.org/en-US/docs/Web/API/Web_Speech_API), [sensors](https://developer.mozilla.org/en-US/docs/Web/API/Sensor_APIs) or [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)?\n  - 🗃️ add basic `node` or `deno` APIs (only with help!)\n\nIf you have knowledge in these fields on the js side, have pointers or already \na basic implementation using ports, [come by](https://github.com/lue-bird/elm-state-interface/discussions/new/choose)!\n\nNote: The package is very much not designed to be easily extensible.\nAdding stuff _will_ force a major version bump.\nThe module and interface structure is also not equipped to support multiple platforms.\n\n## thanks 🌱\n  - [andrewMacmurray for `elm-concurrent-task`](https://dark.elm.dmy.fr/packages/andrewMacmurray/elm-concurrent-task/latest/) which was used as the base for many of the js implementations\n  - [elm-radio hosts for the episode about concurrent-task](https://elm-radio.com/episode/elm-concurrent-task) which motivated me to make a package out of it\n  - [a-teammate (Malte H)](https://github.com/a-teammate) for lots of valuable feedback 💚\n  - [MartinSStewart for `elm-audio`](https://dark.elm.dmy.fr/packages/MartinSStewart/elm-audio/latest/) which inspired the audio API\n  - [noordstar for `elm-gamepad`](https://dark.elm.dmy.fr/packages/noordstar/elm-gamepad/latest) which inspired the gamepads API\n  - [kageurufu for `elm-websockets`](https://dark.elm.dmy.fr/packages/kageurufu/elm-websockets/latest/) which inspired me to also provide a websocket API\n\n## where's X?\nTo not be blocked on missing interfaces, you have the option of [custom elements](https://guide.elm-lang.org/interop/custom_elements), custom events and [data attributes](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes) at least.\n\nIf that would only work short term: Fork it!\nAnd if you think everyone would profit, [opening a PR](https://github.com/lue-bird/elm-state-interface/pulls) would be awesome.\n\nI don't believe I will add the ability to provide custom interfaces\nfor simplicity reasons alone (I aggressively don't want state-interface to become like an elm-review in terms of complexity).\nIf you have a different vision for this project, fork and adapt it all you want 👨‍🍳.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flue-bird%2Felm-state-interface","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flue-bird%2Felm-state-interface","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flue-bird%2Felm-state-interface/lists"}