Parsing query string inside fragment with Elm - elm

For reasons I need to parse something which looks is formatted as a query string (i.e. key=value&another_key=another_value), but which is in the fragment of a URL, e.g:
http://example.com/callback#id_token=my_long_jwt&state=some_state
If the # was a ?, then it would be easy using <?>, but I can't figure out how (or if) I can use Url.Parser.Query when it's in a fragment.
I can get the fragment as a String:
import Url.Parser as Parser
import Url.Parser exposing ((</>))
type Route = Callback String | NotFound
route : Parser.Parser (Route -> a) a
route = Parser.map Callback (Parser.s "callback" </> fragmentWithDefault )
fragmentWithDefault : Parser.Parser (String -> a) a
fragmentWithDefault = Parser.fragment <| Maybe.withDefault "no_fragment"
toRoute : Url.Url -> Route
toRoute url = Maybe.withDefault NotFound (Url.Parser.parse route url)
But there is no API in Url.Parser.Query which runs a Url.Parser.Query.Parser against which I can run the String in Callback.

The Url parser just treats Url fragment as a String but you can replace # with ? before parsing:
Maybe.withDefault url <| Url.fromString <| String.replace '#' '?' <| Url.toString url
then you will get your fragment in query part so you can parse it normally.

Related

How to create SPA with Elm 0.19?

I am trying to build a SPA with Elm and create three pages, that should show the content, depends on URL.
The content of these three pages are similar, for example Page.elm:
module Page.NotFound exposing (Msg(..), content)
import Html exposing (..)
import Html.Attributes exposing (..)
---- UPDATE ----
type Msg
= NotFoundMsg
content : Html Msg
content =
p [] [ text "Sorry can not find page." ]
In the Main.elm, I have the following code:
module Main exposing (Model, Msg(..), init, main, update, view)
import API.Keycloak as Keycloak exposing (..)
import Browser
import Browser.Navigation as Nav
import Html exposing (..)
import Html.Attributes exposing (..)
import Json.Decode as Decode
import Page.Account as Account
import Page.Home as Home
import Page.NotFound as NotFound
import Route
import Url
import Url.Parser exposing ((</>), Parser, int, map, oneOf, parse, s, string)
---- MODEL ----
type alias Model =
{ key : Nav.Key
, url : Url.Url
, auth : Result String Keycloak.Struct
}
init : Decode.Value -> Url.Url -> Nav.Key -> ( Model, Cmd Msg )
init flags url key =
( Model key url (Keycloak.validate flags), Cmd.none )
---- ROUTE ----
type Route
= Account
---- UPDATE ----
type Msg
= PageNotFound NotFound.Msg
| PageAccount Account.Msg
| PageHome Home.Msg
| LinkClicked Browser.UrlRequest
| UrlChanged Url.Url
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
LinkClicked urlRequest ->
case urlRequest of
Browser.Internal url ->
( model, Nav.pushUrl model.key (Url.toString url) )
Browser.External href ->
( model, Nav.load href )
UrlChanged url ->
( { model | url = url }
, Cmd.none
)
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions _ =
Sub.none
---- VIEW ----
info : Html Msg
info =
header [] [ text "Header" ]
createLink : String -> Html Msg
createLink path =
a [ href ("/" ++ path) ] [ text path ]
navigation : Html Msg
navigation =
ul []
[ li [] [ createLink "home" ]
, li [] [ createLink "account" ]
]
content : Model -> Html Msg
content model =
main_ []
[ case parse Route.parser model.url of
Just path ->
matchedRoute path
Nothing ->
NotFound.content
]
matchedRoute : Route.Route -> Html Msg
matchedRoute path =
case path of
Route.Home ->
Home.content
Route.Account ->
Account.content
body : Model -> List (Html Msg)
body model =
[ info
, navigation
, content model
]
view : Model -> Browser.Document Msg
view model =
{ title = "Cockpit"
, body = body model
}
---- PROGRAM ----
main : Program Decode.Value Model Msg
main =
Browser.application
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
, onUrlChange = UrlChanged
, onUrlRequest = LinkClicked
}
The compiler complains:
-- TYPE MISMATCH -------------- /home/developer/Desktop/elm/cockpit/src/Main.elm
The 2nd branch of this `case` does not match all the previous branches:
104| [ case parse Route.parser model.url of
105| Just path ->
106| matchedRoute path
107|
108| Nothing ->
109| NotFound.content
^^^^^^^^^^^^^^^^
This `content` value is a:
Html NotFound.Msg
But all the previous branches result in:
Html Msg
Hint: All branches in a `case` must produce the same type of values. This way,
no matter which branch we take, the result is always a consistent shape. Read
<https://elm-lang.org/0.19.0/union-types> to learn how to “mix” types.
-- TYPE MISMATCH -------------- /home/developer/Desktop/elm/cockpit/src/Main.elm
Something is off with the 2nd branch of this `case` expression:
120| Account.content
^^^^^^^^^^^^^^^
This `content` value is a:
Html Account.Msg
But the type annotation on `matchedRoute` says it should be:
Html Msg
-- TYPE MISMATCH -------------- /home/developer/Desktop/elm/cockpit/src/Main.elm
Something is off with the 1st branch of this `case` expression:
117| Home.content
^^^^^^^^^^^^
This `content` value is a:
Html Home.Msg
But the type annotation on `matchedRoute` says it should be:
Html Msg
Detected errors in 1 module.
I know that the type is wrong, but do not know, how to prove it.
How can I get it to work?
I also looked at the example from https://github.com/rtfeldman/elm-spa-example/blob/master/src/Main.elm but could not figure, how does it work.
You have multiple Msg types, which is OK, but it can lead to confusion. In short: Main.Msg is not the same type as NotFound.Msg.
The function matchedRoute returns a Html Main.Msg while the function NotFound.content returns a Html NotFound.Msg; completely different types.
You're already 99% of the way there because you have a PageNotFound NotFound.Msg type constructor which produces a Main.Msg. This allows you to wrap the NotFound.Msg in a Main.Msg. It should be a matter of doing PageNotFound NotFound.content in your Nothing -> branch.
The problem is that the Msg type referred to by NotFound.content is NotFound.Msg, the Msg type referred to by Main.matchedRoute is Main.Msg, and these do not unify automatically. So when you use these in different branches of a case expression, the compiler will tell you they are different and can't be unified into a single type for the case expression to return.
So you have to convert one to the other, and the usual way to do that is to add a variant to the "outer" msg type (Main.Msg) that wraps the "inner" msg type (NotFound.Msg). Fortunately you've already added that variant as PageNotFound NotFound.Msg, so we can move on.
The next step is to do the wrapping of NotFound.Msgs in PageNotFounds. Unfortunately, we rarely get to handle values of NotFound.Msg alone, it's usually wrapped in some other type like Html or Cmd, which is trickier to deal with. Fortunately, Evan was foreknowing enough to predict this scenario and added Cmd.map and Html.map for us to use. Just like List.map and Maybe.map, Cmd.map and Html.map takes a function a -> b and uses it to convert Html as or Cmd as to Html bs or Cmd bs respectively.
So, all you really need to do here is use Html.map with PageNotFound on NotFound.content:
content : Model -> Html Msg
content model =
main_ []
[ case parse Route.parser model.url of
Just path ->
matchedRoute path
Nothing ->
NotFound.content |> Html.map PageNotFound
]
Both branches will now return Main.Msg and the compiler should be happy :)
And btw, in elm-spa-example, this is being done here

Getting type signatures for a function in elm

I'm using elm 0.18.
Let's say I have a function that strings together a bunch of stuff that I threw together in a hurry. It works, but I'm not sure what it's type signature is, and I'd like elm to tell me (or hint for me) that type signature.
For example, I use graphql and have a function that takes a graphql string, a decoder (which also doesn't have a type signature), and a Cmd Msg, and runs it through HttpBuilder.
graphQLPost graphiql decoder msg =
HttpBuilder.post (url ++ "api")
|> HttpBuilder.withStringBody "text/plain" graphiql
|> HttpBuilder.withExpect (Http.expectJson decoder)
|> HttpBuilder.send msg
This works, though I don't know why. I tried fitting it with the type signature graphQLPost : String -> Json.Decode.Decoder -> Cmd Msg, but I get an error.
Figuring out this type signature is not as important to me as finding a way to induce them through elm. Is there a command that I can enter into elm-repl or something that will tell me the signature?
Elm REPL will do this for you:
> import Http
> import HttpBuilder
> type Msg = Msg
> url = "..."
"..." : String
> graphQLPost graphiql decoder msg = \
| HttpBuilder.post (url ++ "api") \
| |> HttpBuilder.withStringBody "text/plain" graphiql \
| |> HttpBuilder.withExpect (Http.expectJson decoder) \
| |> HttpBuilder.send msg
<function>
: String
-> Json.Decode.Decoder a
-> (Result.Result Http.Error a -> msg)
-> Platform.Cmd.Cmd msg
When you write a function and hit <Enter>, it shows you the signature. In this case the signature is:
graphQLPost : String
-> Json.Decode.Decoder a
-> (Result.Result Http.Error a -> msg)
-> Platform.Cmd.Cmd msg
Running elm-make with the --warn option will cause the compiler to suggest that you include a type annotation on functions that don't have one, and it will provide one for you to copy and paste in.
Also, some editor integrations, such as the Visual Studio Code language extension for Elm, will display these kinds of warnings as a hint icon that you can click to add the missing type annotation automatically. You can set a keyboard shortcut for this to do it without your hands leaving the keyboard.

Elm get raw JSON string

I'm trying to build a very simple app that will just output the raw JSON object from an api.
So I want a function that will take a url parameter and ideally return the JSON string.
I have the following code:
decode: String -> String
decode jsonString =
Decode.decodeString jsonString
apiResonse : String -> String
apiResonse url =
let
url = "https://api.giphy.com/v1/gifs/random?api_key=dc6zaTOxFJmzC&tag=cats"
request = Http.get Decode.decodeString url
in
Http.send NewRequest request
But I'm struggling to understand the decoder part of the function. If anyone could help me that would be great.
If you just want to get the HTTP response as a string value, use Http.getString. The example you posted using Http.get assumes the result is in JSON and forces you to decode it to an Elm value.
Here is a modified example of the random cat generator code which just displays a dump of the response JSON instead of a cat picture:
getRandomGif : String -> Cmd Msg
getRandomGif topic =
let
url =
"https://api.giphy.com/v1/gifs/random?api_key=dc6zaTOxFJmzC&tag=" ++ topic
in
Http.send NewGif (Http.getString url)
Here is a working example on ellie-app.com

How to filter a Signal on page load

For the sake of learning, I'm trying to load content only when I click on a button. So far I've managed to :
Reload the content when I click the button.
And Filter the Signal when I click (if the String I send is not "GETPERF")
But my problem is that the Ajax call is still triggered once the page loads.
Here's the code:
-- SIGNALS & MAILBOX
inbox : Signal.Mailbox String
inbox =
Signal.mailbox "SOME TEXT"
result : Signal.Mailbox String
result =
Signal.mailbox ""
-- VIEW
view : String -> Html
view msg =
div [] [
h1 [] [text "Mailbox3"],
p [] [text msg],
button
[onClick inbox.address "GETPERF"]
[text "click perf"],
]
main : Signal Html
main =
Signal.map view result.signal
-- TASK & EFFECTS
port fetchReadme : Signal (Task Http.Error ())
port fetchReadme =
inbox.signal
|> Signal.filter (\sig -> sig == "GETPERF" ) "boo"
|> Signal.map (\_ -> Http.getString "http://localhost:3000/dates" `andThen` report)
report : String -> Task x ()
report html =
Signal.send result.address html
Is there any way to prevent the first Ajax call on page load ? (Or some more idiomatic way of doing all this ?)
The reason you're getting an initial ajax request is that Signal.filter is still keeping that initial value of "boo" (See the Signal.filter documentation here). That value is ignored in the next Signal.map statement by your use of the underscore parameter, but the Http Task is still getting returned and that's why you see an initial ajax request on page load.
Instead of using Signal.filter, you could write a conditional that only sends the ajax request in the correct circumstances, when sig is "GETPERF". And if sig is not "GETPERF" (as in page load), you can, in essence, do nothing by returning Task.succeed (). Here is a refactored fetchReadme function with these changes:
port fetchReadme : Signal (Task Http.Error ())
port fetchReadme =
let
fetchAndReport sig =
if sig == "GETPERF" then
Http.getString "http://localhost:3000/dates"
`andThen` report
else
Task.succeed ()
in
Signal.map fetchAndReport inbox.signal

How to get query parameters in Elm?

In my Elm program, I'd like to initialize my model based on the query string.
For example, if the query string is ?w=3&h=5 I'd like to have:
initialModel =
{ width = 3
, height = 5
}
Is that possible to achieve this in Elm, or the only way to do this is to get the query parameters in Javascript and pass them via a port?
Elm 0.19
For elm 0.19 the below concept is the same. Both of these packages still exist but have been moved and relabeled as the official elm/url and elm/browser libraries.
Elm 0.18
This example uses evancz/url-parser and elm-lang/navigation. There are a few kinks that aren't straightforward in the documentation, but I've explained them briefly below. The example should speak for itself.
module Main exposing (..)
import Html as H exposing (..)
import Navigation exposing (Location)
import UrlParser as UP exposing ((</>), (<?>), top, parsePath, oneOf, s, stringParam, Parser)
import Maybe.Extra as MaybeExtra exposing (unwrap)
type Route
= UrlRoute (Maybe String) (Maybe String)
| NotFoundRoute
type Msg
= UrlParser Navigation.Location
type alias Model =
{ location : Route
, w : String
, h : String
}
type alias SearchParams =
{ w : Maybe String, h : Maybe String }
main =
Navigation.program UrlParser
{ init = init
, view = view
, update = update
, subscriptions = (\_ -> Sub.none)
}
init : Location -> ( Model, Cmd Msg )
init location =
let
currentPath =
parseLocation location
in
( initialModel currentPath
, Cmd.none
)
parseLocation : Location -> Route
parseLocation location =
case (parsePath matchers location) of
Just route ->
route
Nothing ->
NotFoundRoute
matchers : Parser (Route -> a) a
matchers =
UP.map UrlRoute (UP.s "index" <?> UP.stringParam "w" <?> UP.stringParam "h")
initialModel : Route -> Model
initialModel route =
{ location = route
, w = MaybeExtra.unwrap "" (\x -> Maybe.withDefault "" x.w) (parseParams route)
, h = MaybeExtra.unwrap "" (\x -> Maybe.withDefault "" x.h) (parseParams route)
}
parseParams : Route -> Maybe SearchParams
parseParams route =
case route of
UrlRoute w h ->
Just { w = w, h = h }
NotFoundRoute ->
Nothing
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
UrlParser location ->
( model
, Cmd.none
)
view : Model -> Html msg
view model =
div []
[ h1 [] [ text "URL Info" ]
, div [] [ text ("W is: " ++ model.w) ]
, div [] [ text ("H is: " ++ model.h) ]
]
The "trick" is to create another type alias to place your query params inside of. In the above example I've created the type SearchParams. After creating this type we just use an initialModel that takes in the currentPath.
From there, our model can extract the query params with Maybe.withDefault (it needs to be a Maybe type because the params may not be there). Once we have our data in the model we just print it out in the view.
Hope this helps!
There is no built-in core library way to access the URL. You can use ports and the community library jessitron/elm-param-parsing.
If you also want to set the URL, you can again use ports, or you can use the History API, for which there are bindings in TheSeamau5/elm-history.
Unfortunately jessitron/elm-param-parsing doesn't work with Elm 0.18.
Use elm-lang/navigation package:
http://package.elm-lang.org/packages/elm-lang/navigation/latest/Navigation
https://github.com/elm-lang/navigation/tree/2.1.0
especially this function:
program
: (Location -> msg)
-> { init : Location -> (model, Cmd msg), update : msg -> model -> (model, Cmd msg), view : model -> Html msg, subscriptions : model -> Sub msg }
-> Program Never model msg
In the second parameter you can see "init : Location -> (model, Cmd msg)". This should handle reading of initial URL. To complement that, first parameter is a function which gets called every time URL changes.
(I am aware it's an old question, but this link popped out when I was looking for the solution to the same problem and accepted answer didn't help)