├── .travis.yml ├── .gitignore ├── elm.json ├── LICENSE ├── tests └── Update │ └── FetchSpec.elm ├── README.md └── src └── Update └── Fetch.elm /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elm 2 | elm: 3 | - elm0.19.0 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .*.swp 2 | .DS_Store 3 | elm-stuff 4 | index.js 5 | elm.js 6 | index.html 7 | node_modules 8 | build 9 | tests/Doc 10 | .idea -------------------------------------------------------------------------------- /elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "package", 3 | "name": "Gizra/elm-fetch", 4 | "summary": "Some conveniences for implementing the `update` function along with `fetch`", 5 | "license": "MIT", 6 | "version": "1.0.0", 7 | "exposed-modules": [ 8 | "Update.Fetch" 9 | ], 10 | "elm-version": "0.19.0 <= v < 0.20.0", 11 | "dependencies": { 12 | "ccapndave/elm-update-extra": "4.0.0 <= v < 5.0.0", 13 | "elm/core": "1.0.0 <= v < 2.0.0", 14 | "elm/html": "1.0.0 <= v < 2.0.0" 15 | }, 16 | "test-dependencies": { 17 | "elm-explorations/test": "1.1.0 <= v < 2.0.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017, Gizra Internet Solutions Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/Update/FetchSpec.elm: -------------------------------------------------------------------------------- 1 | module Update.FetchSpec exposing (spec) 2 | 3 | import Expect 4 | import List 5 | import Test exposing (..) 6 | import Update.Fetch exposing (andThenFetch) 7 | 8 | 9 | type alias Model = 10 | List String 11 | 12 | 13 | emptyModel : Model 14 | emptyModel = 15 | [] 16 | 17 | 18 | type Msg 19 | = AddString String 20 | 21 | 22 | update : Msg -> Model -> ( Model, Cmd Msg ) 23 | update msg model = 24 | case msg of 25 | AddString s -> 26 | ( s :: model, Cmd.none ) 27 | 28 | 29 | {-| 30 | 31 | - By adding "fetch-4" only if "fetch-3" is present, we're testing that we really recurse. 32 | 33 | - By adding multiple strings in the initial case, we're testing that we only recurse once 34 | the full list is processed. Otherwise, we'll end up with duplicate strings in our model. 35 | 36 | -} 37 | fetch : Model -> List Msg 38 | fetch model = 39 | if List.member "fetch-4" model then 40 | [] 41 | 42 | else if List.member "fetch-3" model then 43 | [ AddString "fetch-4" ] 44 | 45 | else 46 | [ AddString "fetch-1" 47 | , AddString "fetch-2" 48 | , AddString "fetch-3" 49 | ] 50 | 51 | 52 | updateAndThenFetch : Msg -> Model -> ( Model, Cmd Msg ) 53 | updateAndThenFetch = 54 | andThenFetch fetch update 55 | 56 | 57 | spec : Test 58 | spec = 59 | describe "Update.Update" 60 | [ describe "andThenFetch" 61 | [ test "it recurses once full list processed, and eventually terminates" <| 62 | \_ -> 63 | updateAndThenFetch (AddString "test") emptyModel 64 | |> Tuple.first 65 | |> Expect.equal [ "fetch-4", "fetch-3", "fetch-2", "fetch-1", "test" ] 66 | ] 67 | ] 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/Gizra/elm-fetch.svg?branch=master)](https://travis-ci.org/Gizra/elm-fetch) 2 | 3 | # elm-fetch 4 | 5 | Some conveniences for implementing the `update` function with `fetch`. 6 | 7 | The primary purpose of the `update` function is to take a `msg` and 8 | return an updated `model` (and possibly a `Cmd`). So, what one ordinarily does 9 | in an `update` function is run a `case` statement on the `msg`, doing whatever 10 | is appropriate for the provided `msg`. 11 | 12 | However, there are times when we want to take certain actions based not on the 13 | `msg` we just received, but instead on the state of our `model` generally ... 14 | particularly the state of whatever is controlling our `view`. For instance, we 15 | may need to load some data from the backend in order to support the current 16 | view. That will require a `Cmd`, but it is logically independent of the 17 | current `msg`. Well, I suppose you could try to "catch" every message that 18 | creates a requirement to fetch some data, but that is error prone -- it would 19 | be nicer to just compute what messages are necessary, given the state of the 20 | model. 21 | 22 | So, imagine that you have a function like this: 23 | 24 | fetch : Model -> List Msg 25 | 26 | Its job is to look at the model and decide whether there are any messages that 27 | ought to be processed (e.g. to fetch some needed data, but it isn't really 28 | limited to that). It's convenient for this to be a separate function (that is, 29 | separate from `update`), because it doesn't depend on the `msg` ... it will be 30 | a function of something in the model which indicates what the `view` needs. 31 | 32 | However, we also need to apply this `fetch` function. That is where `andThenFetch` 33 | comes in. You provide: 34 | 35 | - your `fetch` function 36 | - your `update` function 37 | 38 | What you get back is a function that has the same signature as your `update` 39 | function ... that is, it is also in the form `Msg -> Model -> (Model, Cmd 40 | Msg)`. However, after it calls your own `update` function, it calls your 41 | `fetch` function with the resulting `Model`, and then feeds those results back 42 | into your `update` function. So, whatever messages your `fetch` function 43 | returns will be processed. 44 | 45 | Note that this will run recursively ... that is, when your `fetch` function 46 | returns some messages to process, it will be called again with the results of 47 | processing those messages. So, you will need something in your model to 48 | keep track of actions in progress (e.g. `RemoteData`) so that you don't 49 | repeatedly kick off the same action in an infinite loop. 50 | 51 | To use this function, you would normally just supply the first two parameters 52 | ... that is, your `fetch` function and your normal `update` function. What you 53 | will then get back is a function of the form `msg -> model -> ( model, Cmd msg)` ... 54 | that is, an `update` function that you can then pass to `programWithFlags` (or 55 | use in another way). 56 | 57 | Your `main` function would look something like: 58 | 59 | ```elm 60 | main = 61 | Browser.element 62 | { init = App.Update.init 63 | 64 | -- We wire `andThenFetch` here. 65 | , update = Update.Fetch.andThenFetch App.Fetch.fetch App.Update.update 66 | , view = App.View.view 67 | , subscriptions = App.Update.subscriptions 68 | } 69 | ``` 70 | 71 | `App.Update.update` is your normal update function. 72 | `App.Fetch.fetch` will likely look something like this, assuming you have a Page called `Items`: 73 | 74 | ```elm 75 | {-| Call the needed `fetch` function, based on the active page. 76 | -} 77 | fetch : App.Model.Model -> List App.Model.Msg 78 | fetch model = 79 | case model.activePage of 80 | Items -> 81 | Pages.Items.Fetch.fetch model.backend 82 | |> List.map (\subMsg -> MsgBackend subMsg) 83 | ``` 84 | 85 | # Maintainers 86 | 87 | * [@rgrempel](https://github.com/rgrempel) 88 | * [@amitaibu](https://github.com/amitaibu) 89 | 90 | -------------------------------------------------------------------------------- /src/Update/Fetch.elm: -------------------------------------------------------------------------------- 1 | module Update.Fetch exposing (andThenFetch, sequenceExtra) 2 | 3 | {-| Some conveniences for implementing the `update` function with `fetch`. 4 | 5 | Using the various functions in 6 | [ccapndave/elm-update-extra](http://package.elm-lang.org/packages/ccapndave/elm-update-extra/latest) 7 | is also highly recommended. 8 | 9 | @docs andThenFetch, sequenceExtra 10 | 11 | -} 12 | 13 | import List 14 | import Update.Extra exposing (sequence) 15 | 16 | 17 | {-| Like `Update.Extra.sequence`, but for `update` signatures that also 18 | return a list of extra messages for the caller to handle. 19 | 20 | Essentially, this allows you to recursively apply a whole sequence of messages, 21 | collecting their results. So, with `Update.Extra.sequence` you can do something 22 | like this: 23 | 24 | update : Msg -> Model -> ( Model, Cmd Msg ) 25 | update msg model = 26 | case msg of 27 | SomeMsg -> 28 | sequence update 29 | [ AnotherMsg, YetAnotherMsg, AThirdMsg ] 30 | ( model, Cmd.none ) 31 | 32 | Isn't that nice? Essentially, you get a really expressive way of constructing 33 | a "composite" message. Plus, you can do something first ... that is, you could 34 | modify the model, and have your own `Cmd`, and then feed that into the `sequence` 35 | for further processing. 36 | 37 | So, what is `sequenceExtra`? It deals with an alternative `update` signature, 38 | in which we're returning a third element to our tuple, with extra messages that 39 | the caller is expected to handle. So, you can follow the same idiom as shown above. 40 | 41 | update : Msg -> Model -> ( Model, Cmd Msg, List extraMsg ) 42 | update msg model = 43 | case msg of 44 | SomeMsg -> 45 | sequenceExtra update 46 | [ AnotherMsg, YetAnotherMsg, AThirdMsg ] 47 | ( model, Cmd.none, [] ) 48 | 49 | -} 50 | sequenceExtra : 51 | (msg -> model -> ( model, Cmd msg, List extraMsg )) 52 | -> List msg 53 | -> ( model, Cmd msg, List extraMsg ) 54 | -> ( model, Cmd msg, List extraMsg ) 55 | sequenceExtra updater msgs startingPoint = 56 | List.foldl 57 | (\eachMsg ( modelSoFar, cmdsSoFar, msgsSoFar ) -> 58 | let 59 | ( newModel, newCmd, newMsgs ) = 60 | updater eachMsg modelSoFar 61 | in 62 | ( newModel 63 | , Cmd.batch [ cmdsSoFar, newCmd ] 64 | , msgsSoFar ++ newMsgs 65 | ) 66 | ) 67 | startingPoint 68 | msgs 69 | 70 | 71 | {-| The primary purpose of the `update` function is to take a `msg` and 72 | return an updated `model` (and possibly a `Cmd`). So, what one ordinarily does 73 | in an `update` function is run a `case` statement on the `msg`, doing whatever 74 | is appropriate for the provided `msg`. 75 | 76 | However, there are times when we want to take certain actions based not on the 77 | `msg` we just received, but instead on the state of our `model` generally ... 78 | particularly the state of whatever is controlling our `view`. For instance, we 79 | may need to load some data from the backend in order to support the current 80 | view. That will require a `Cmd`, but it is logically independent of the 81 | current `msg`. Well, I suppose you could try to "catch" every message that 82 | creates a requirement to fetch some data, but that is error prone -- it would 83 | be nicer to just compute what messages are necessary, given the state of the 84 | model. 85 | 86 | So, imagine that you have a function like this: 87 | 88 | fetch : Model -> List Msg 89 | 90 | Its job is to look at the model and decide whether there are any messages that 91 | ought to be processed (e.g. to fetch some needed data, but it isn't really 92 | limited to that). It's convenient for this to be a separate function (that is, 93 | separate from `update`), because it doesn't depend on the `msg` ... it will be 94 | a function of something in the model which indicates what the `view` needs. 95 | 96 | However, we also need to apply this `fetch` function. That is where `andThenFetch` 97 | comes in. You provide: 98 | 99 | - your `fetch` function 100 | - your `update` function 101 | 102 | What you get back is a function that has the same signature as your `update` 103 | function ... that is, it is also in the form `Msg -> Model -> (Model, Cmd 104 | Msg)`. However, after it calls your own `update` function, it calls your 105 | `fetch` function with the resulting `Model`, and then feeds those results back 106 | into your `update` function. So, whatever messages your `fetch` function 107 | returns will be processed. 108 | 109 | Note that this will run recursively ... that is, when your `fetch` function 110 | returns some messages to process, it will be called again with the results of 111 | processing those messages. So, you will need something in your model to 112 | keep track of actions in progress (e.g. `RemoteData`) so that you don't 113 | repeatedly kick off the same action in an infinite loop. 114 | 115 | To use this function, you would normally just supply the first two parameters 116 | ... that is, your `fetch` function and your normal `update` function. What you 117 | will then get back is a function of the form `msg -> model -> ( model, Cmd msg)` ... 118 | that is, an `update` function that you can then pass to `programWithFlags` (or 119 | use in another way). 120 | 121 | Your `main` function would look something like: 122 | 123 | main = 124 | Browser.element 125 | { init = App.Update.init 126 | 127 | -- We wire `andThenFetch` here. 128 | , update = Update.Fetch.andThenFetch App.Fetch.fetch App.Update.update 129 | , view = App.View.view 130 | , subscriptions = App.Update.subscriptions 131 | } 132 | 133 | `App.Update.update` is your normal update function. 134 | `App.Fetch.fetch` will likely look something like this, assuming you have a Page called `Items`: 135 | 136 | {-| Call the needed `fetch` function, based on the active page. 137 | -} 138 | fetch : App.Model.Model -> List App.Model.Msg 139 | fetch model = 140 | case model.activePage of 141 | Items -> 142 | Pages.Items.Fetch.fetch model.backend 143 | |> List.map (\subMsg -> MsgBackend subMsg) 144 | 145 | -} 146 | andThenFetch : (model -> List msg) -> (msg -> model -> ( model, Cmd msg )) -> msg -> model -> ( model, Cmd msg ) 147 | andThenFetch fetch update msg model = 148 | -- First, we do the "regular" update, then we apply the `fetch` logic. In 149 | -- principle, we could integrate with `animationFrame` in some way. Since 150 | -- this is driven by the needs of the view, there's no need to check any 151 | -- faster than the view is drawn. 152 | update msg model 153 | |> applyFetch fetch update 154 | 155 | 156 | applyFetch : (model -> List msg) -> (msg -> model -> ( model, Cmd msg )) -> ( model, Cmd msg ) -> ( model, Cmd msg ) 157 | applyFetch fetch update resultSoFar = 158 | -- Note that we call ourselves recursively. So, it's vitally important that 159 | -- the `fetch` implementations use a `WebData`-like strategy to indicate 160 | -- that a request is in progress, and doesn't need to be triggered again. 161 | -- Otherwise, we'll immediately be in an infinite loop. 162 | -- 163 | -- We initially sequence through the app's `update`, and only recurse once 164 | -- all the messages have been processed. 165 | let 166 | msgs = 167 | fetch (Tuple.first resultSoFar) 168 | in 169 | if List.isEmpty msgs then 170 | resultSoFar 171 | 172 | else 173 | sequence update msgs resultSoFar 174 | |> applyFetch fetch update 175 | --------------------------------------------------------------------------------