Elm: Update a command coming from nested module - elm

In my Elm app, there is a parent and a child. I would like the child to send an HTTP command and then the parent would update it on the fly by adding the backend config that the child model doesn't have.
The way I see it is:
The child sends the HTTP command (update function)
The command is transferred to the parent (update function)
The parent adds the config to command (is this possible??)
The command is finally executed with all the required parameters
I would like to implement HTTP actions in children modules so my app remains modular while keeping the config in one place.
Any help is welcome. Maybe a different approach can solve my problem too!

Keep all the configuration details in a module that handles the Http Requests. For example, you can have this code in Requests.elm
import Http
import Task
url = "http://ip.jsontest.com/"
getIP : (String -> msg) -> Cmd msg
getIP tagger =
Http.getString url
|> Task.perform (\e -> tagger (toString e)) tagger
and then in the Component code have something like
update msg model =
case msg of
GetIP -> model ! [Requests.getIP ShowIP]
ShowIP str -> {model | ipStr = str} ! []
all the configuration details are hidden from the component. Only an API of access is exposed.
LATER EDIT:
Another option would be to define and pass around a dynamic Context. You can manage this context at the top level and just pass the current version around when you update things. It would look like this
In Requests.elm
import Http
import Task
type alias Context =
{ url : String }
getIP : Context -> (String -> msg) -> Cmd msg
getIP ctx tagger =
Http.getString ctx.url
|> Task.perform (\e -> tagger (toString e)) tagger
in Components:
update ctx msg model =
case msg of
GetIP -> model ! [Requests.getIP ctx ShowIP]
ShowIP str -> {model | ipStr = str} ! []

Related

How to write a test for an http get request in elm

I'm trying to check the data received after sending a request and for that I need to write a test to make sure of it.
These are the three functions handling the data.
fetchConfigs : Cmd Msg
fetchConfigs =
Http.get
{ url = "http://localhost:8080/api/sso/configs"
, expect = Http.expectJson GotConfigs responseDecoder
}
responseDecoder : Decoder Config
responseDecoder =
map2 Config
(field "identifier" string)
(field "ssoType" int)
type Msg
= GotConfigs (Result Http.Error Config)
Have a look at https://package.elm-lang.org/packages/avh4/elm-program-test/latest/ProgramTest. From the package description:
This module allows you to interact with your program by simulating
user interactions and external events (like HTTP responses and ports),
and making assertions about the HTML it renders and the external
requests it makes.

Intercepting 401 for any response in Elm

I'm building an application in Elm where most API calls are protected; i.e. the user needs to be logged in for the API call to work. If the user is not logged in, they will receive a 401 Unauthorized response. I want the application to redirect to the login page if any response is a 401.
Currently, I only have this redirect set up for a single API call. Here is a stripped-down version of the code to give an idea of how it's set up:
-- Util/Api.elm
type alias Data data =
{ data : data
}
-- Resources/Expense.elm
getExpenses : (Progress (Api.Data (List Expense)) -> msg) -> Sub msg
getExpenses msg =
(dataDecoder expenseListDecoder)
|> Http.get expensesEndpoint
|> Progress.track expensesEndpoint msg
-- Main/Msg.elm
type Msg
= ExpenseListMsg ExpenseListMsg
| RedirectToLogin
-- Main/Update.elm
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
ExpenseListMsg msg ->
ExpenseList.Update.update msg model
GoTo path ->
model ! [ Navigation.newUrl path ]
RedirectToLogin ->
model ! [ Navigation.load "path/to/login" ]
-- ExpenseList/Msg.elm
type ExpenseListMsg
= GetExpensesProgress (Progress (Api.Data (List Expense)))
| SetLoading
-- ExpenseList/Update.elm
update : ExpenseListMsg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
SetLoading ->
{ model | expenses = setExpensesLoading model.expenses } ! []
GetExpensesProgress (Done { data }) ->
{ model | expenses = addExpenses model.expenses data } ! []
GetExpensesProgress (Fail (BadStatus { status })) ->
case status.code of
401 ->
model ! [ msgToCmd RedirectToLogin ]
_ ->
model ! []
GetExpensesProgress (Fail error) ->
model ! []
GetExpensesProgress progress ->
{ model | expenses = setExpensesLoading model.expenses } ! []
Essentially, I want to move the logic around 401 responses from ExpenseList/Update.elm up to Main/Update.elm so that I can use it for any request I want.
I attempted a number of things, but nothing quite worked with Elm's type system. For example, one thing I wanted to do was try to do a nested pattern match with missing specificity in the middle, e.g.:
-- Main/Update.elm
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
ApiCall (messageType (msg (Fail (BadStatus { status })))) ->
case status of ->
. . .
. . .
I was hoping something like this would work and would match a message that looked like: ApiCall (ExpenseListMsg (GetExpensesProgress (Fail (BadStatus)))). Unfortunately, it's not proper Elm syntax, so my code didn't compile.
How can I write something that will allow me to mark an API call as protected and catch 401 errors at the top level in Main.Update.update?
Currently, the API call is encapsulated by the ExpenseList/Update module. This encapsulation is what makes the API call results unavailable to the Main module. The interaction works like this: Main -> FeatureModule -> API
Because the API is what provides the information needed to determine whether the app should redirect to the login page, and you want the Main module to perform the redirect, the Main module needs access to the API. Hence, the encapsulation needs to go. Instead, you can:
Have an API module which provides the low-level API functionality by producing Tasks. Unlike producing Cmds, this allows the caller, such as the Main module to decide how to handle the result of the Task, which can be acquired by converting the Task to a Cmd and giving it to the Elm runtime for execution.
Have the ExpenseList.Update module use the API module to create Tasks.
With this arrangement:
The Main module sends high-level commands to a feature module, which then uses the API module to produce the low-level instructions, which are then provided to the Main module.
The Main module doesn't need to care what those low-level instructions are, it simply converts the Task to a Cmd and waits for the result.
When the result comes back it's in a low-level format (ex. Success/Fail). At this point the Main module can jump in and handle the redirect on a 401 error. Otherwise, it can pass the result to a feature module so it may handle the result.

Not Found: "Navigation.Parser" and "Navigation.makeParser"

I have the following errors that I am struggling to resolve based on the latest version of elm-lang/Navigation:
-- NAMING ERROR ------------------------------------------------------- Home.elm
Cannot find variable Navigation.makeParser.
231| Navigation.makeParser parse
^^^^^^^^^^^^^^^^^^^^^ Navigation does not expose makeParser.
-- NAMING ERROR ------------------------------------------------------- Home.elm
Cannot find type Navigation.Parser.
229| urlParser : Navigation.Parser Route
^^^^^^^^^^^^^^^^^ Navigation does not expose Parser.
Note:
It looks like Parser and makePaser were removed from Navigation in version 2.1.0.
Is there an updated example of how to do navigation leveraging the urlParser function?
I have the following:
import Navigation exposing (..)
main : Program Never
main =
Navigation.program urlParser
{ model = model
, update = update
, urlUpdate = urlUpdate
, view = view
}
...
-- NAVIGATION
parse : Navigation.Location -> Route
parse { pathname } =
let
one =
Debug.log "path" pathname
in
case pathname of
"index.html" ->
HomeRoute
_ ->
NotFound
urlParser : Navigation.Parser Route
urlParser =
Navigation.makeParser parse
The Parser concept was removed during the Elm 0.18 to simplify the API. Now you just need to supply a function that takes a Location and returns a Msg as the first parameter to program function.
That function could simply be a Msg constructor that takes a Location argument, as shown in the example from the examples directory (here is live example on ellie-app.com)
type Msg
= UrlChange Navigation.Location
Your update function would then handle the UrlChange Msg and act accordingly. You can still use Location parsing packages like evancz/url-parser.

Elm Http Request on Init

I'm pretty new to Elm, but slowly getting familiar with it. I'm doing some simple Http Requests, which are successful in every aspect (I get the data to display, etc.)
For the moment I can only get my fetchData to trigger onClick, I would like to initialize this on init, but I'm having trouble wrapping my head around this.
....here is my fetchData function:
fetchData : Cmd Msg
fetchData =
Http.get repoInfoListDecoder "http://example/api"
|> Task.mapError toString
|> Task.perform ErrorOccurred DataFetched
Which is currently trigger in the view by a onClick event:
.....
, button [ onClick FetchData ] [ text "Load Data" ]
.....
I want to avoid requesting the data by a button, instead I want the data to load when the app is initialized. Here is my init:
init =
let
model =
{ message = "Welcome", repos = [] }
in
model ! []
And my update (kind of stripped...for example's sakes):
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
NoOp ->
model ! []
...........
FetchData ->
{ model | message="hi" } ! [fetchData]
...........
DataFetched repos ->
{ model | repos = repos, message = "The data has been fetched!" } ! []
Does this makes sense? I'm just having difficulty initializing fetchData when the app loads, so I can display my data without having to hit a button to fetch it.
Thanks in advance!
When you use Html.program, your init function returns the initial data for your Model and Command to be executed.
Commands are side-effects, like HTTP requests.
Try changing init so it runs a Command right away:
init: (Model, Cmd Msg)
init =
let
model =
{ message = "Welcome", repos = [] }
in
model ! [ fetchData ]
Here is the type signature of Html.App.program.
init accepts Cmd, you simply give your Cmd here.
program
: { init : (model, Cmd msg), update : msg -> model -> (model, Cmd msg), subscriptions : model -> Sub msg, view : model -> Html msg }
-> Program Never
Another way is to use Task, which hasn't been mentioned yet. You could try something like
initWithFlags : Flags -> ( Model, Cmd Msg)
initWithFlags flags =
( { user = flags.user }, (Task.perform fetchData) )

How to use ports inside StartApp in Elm

In my app that based on the StartApp package I have a port to communicate from inside the to JS. At the moment I call this port using a mailbox
requestPalette :
{ address : Signal.Address String
, signal : Signal String
}
requestPalette = Signal.mailbox ""
requestPaletteFilter : Signal String
requestPaletteFilter =
Signal.filter (String.isEmpty >> not) "" requestPalette.signal
|> settledAfter (300 * Time.millisecond)
port request : Signal String
port request = requestPaletteFilter
and using it like this:
[on "input" targetValue (\str -> Signal.message requestPalette.address str)
I wonder if there is a way to this inside of the update function instead of sending the message from the view.
This applies to elm 0.16 (and before), in elm 0.17 subscriptions have changed into ports
In order to send a signal to a mailbox from an update, you'll need to use StartApp as opposed to StartApp.Simple, since the former allows for Effects in the update function.
At a bare minimum, you're going to probably have an Action like this, which defines a No-Op and an action for sending the string request:
type Action
= NoOp
| SendRequest String
Your update function will now include something like the following case for the new SendRequest action. Since you're using StartApp, which deals in Effects, you must call Effects.task, and the Task you're mapping to an Effect must be of type Action, which is why we have the Task.succeed NoOp return value.
update action model =
case action of
NoOp ->
(model, Effects.none)
SendRequest str ->
let
sendTask =
Signal.send requestPalette.address str
`Task.andThen` (\_ -> Task.succeed NoOp)
in
(model, sendTask |> Effects.task)
Now your click event handler in the view can go back to using the address passed into the view:
[ on "input" targetValue (Signal.message address << SendRequest) ]
I've got a working example of the above in this gist. You'll just need to subscribe to the request port in javascript to see it in action.