Open jiggneshhgohel opened 7 years ago
Hi
When you input http://localhost:3000/players
directly on your url you are doing a request to your server. Your server needs to know what page to serve in this case. What you are seeing is the server not knowing what to give you.
For the server http://localhost:3000/
, http://localhost:3000/players
and http://localhost:3000/players/1
are all different pages.
However http://localhost:3000/
, http://localhost:3000/#players
and http://localhost:3000/#players/
is all the same page as the hash is ignored by the server.
So what you need is a server that gives you the same page regardless of the path. So if you hit http://localhost:3000/
it should give you the Elm app. If you hit http://localhost:3000/players
it should as well.
The key of all this is the server implementation. In the example you can see this here: elm-live --pushstate
The elm-tutorial-app uses webpack dev server. Have a look at https://webpack.js.org/configuration/dev-server/#devserver-historyapifallback , however you will need to learn a bit of webpack to do this.
Thanks @sporto for the elaborate explanation.
As a matter of fact after few hours of posting this issue I searched elm lang navigation 404 on Google and I found this Stackoverflow Post which you have answered and it mentions the same solution i.e using webpack's historyApiFallback
option. I will go through it and the link you have given above and try to get this working.
While I do that I am adding below my understanding, after your explanation above, regarding how the pieces fit together and I would request you to provide your confirmation on the same:
When we first time input http://localhost:3000
in address bar this triggers the init
flow
Main.elm
init : Location -> ( Model, Cmd Msg )
init location =
let
currentRoute =
Routing.parseLocation location
in
( initialModel currentRoute, fetchPlayers )
matches the route map PlayersRoute top
and sets it as the initial route in the model
Models.elm
initialModel : Route -> Model
initialModel route =
{ players = RemoteData.Loading
, route = route
}
After setting the initial route, it triggers command fetchPlayers
which as can be seen requests data from our API server listening at port 4000 http://localhost:4000
Commands.elm
fetchPlayers : Cmd Msg
fetchPlayers =
Http.get fetchPlayersUrl playersDecoder
|> RemoteData.sendRequest
|> Cmd.map Msgs.OnFetchPlayers
fetchPlayersUrl : String
fetchPlayersUrl =
"http://localhost:4000/players"
Once the data is fetched it triggers a msg Msgs.OnFetchPlayers
which as be seen below sets the fetched Players list from API server and sets it on the Model
Update.elm
...
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Msgs.OnFetchPlayers response ->
( { model | players = response }, Cmd.none )
...
Then the view is rendered which matches the case model.route of
as Models.PlayersRoute
and hence displays the Players Listing. This players listing is rendered on Client server listening at port 3000 http://localhost:3000
View.elm
...
..
view : Model -> Html Msg
view model =
div []
[ page model
]
page : Model -> Html Msg
page model =
case model.route of
Models.PlayersRoute ->
Players.List.view model.players
Models.PlayerRoute id ->
playerEditPage model id
Models.NotFoundRoute ->
notFoundView
....
Now that the top route is handled everything now stays in Elm-court. The navigation button or links when clicked requests data from remote server (the API server serving at port 4000). So http://localhost:3000/players
when manually accessed returns an error but when accessed via Elm renders data for that route fetching it from http://localhost:4000/players
Similarly when http://localhost:3000/players/2
is manually accessed returns an error but when accessed via Elm renders data for that route fetching it from http://localhost:4000/players/2
.
And that perfectly makes sense because Client server (serving on port 3000) doesn't have any routes configured to served from itself. Each route configured is aimed to be served from API server (serving on port 4000)
Now a question arises that if Client server is not configured to serve any route then how come it is able to serve the top
route? The reasoning behind the working of it as per my understanding is:
We are using webpack-dev-server and as per its documentation
it serves the files in the current directory, unless you configure a specific content base.
To load your bundled files, you will need to create an index.html file in the build folder from which static files are served (--content-base option).
And we do have an index.html in our src folder configured to be rendered like this:
in webpack.config.js
...
module.exports = {
entry: {
app: [
"./src/index.js"
]
},
...
in index.js
...
require('./index.html');
...
@sporto can you please confirm this understanding of mine of the flow? If I am wrong at any place requesting you to rectify me.
Thanks.
@sporto One more confusion I have is regarding loading data for Edit route /players/:id
:
When we click on Edit btn and the flow reaches to View.elm, in page
function case model.route of
matches Models.PlayerRoute id
and it executes the function playerEditPage model id
View.elm
...
view : Model -> Html Msg
view model =
div []
[ page model
]
page : Model -> Html Msg
page model =
case model.route of
Models.PlayersRoute ->
Players.List.view model.players
Models.PlayerRoute id ->
playerEditPage model id
Models.NotFoundRoute ->
notFoundView
playerEditPage : Model -> PlayerId -> Html Msg
playerEditPage model playerId =
case model.players of
RemoteData.NotAsked ->
text ""
RemoteData.Loading ->
text "Loading ..."
RemoteData.Success players ->
let
maybePlayer =
players
|> List.filter (\player -> player.id == playerId)
|> List.head
in
case maybePlayer of
Just player ->
Players.Edit.view player
Nothing ->
notFoundView
RemoteData.Failure err ->
text (toString err)
...
So does this function playerEditPage
uses the players
data set on Model, before, when handled msg Msgs.OnFetchPlayers
and then filters the data from that list for matching player id?
Update.elm
...
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Msgs.OnFetchPlayers response ->
( { model | players = response }, Cmd.none )
...
I strongly feel that that should be the case. But can you please confirm this understanding of mine?
If I am correct then consider the scenario where in players list doesn't contain some specific information related to a Player which is not available in the list, say Player's Profile information. This profile information should usually be available in the player's details end-point exposed by API GET /players/:id
which returns response like
{
"id": "2",
"name": "Lance",
"level": 1,
"profile": {
picture: "https://mypictures.com/players/2/picture.jpg",
address: "my address",
rating: 3
}
}
Obviously then the following logic cannot help in rendering the profile information because it as of now just filters data from pre-obtained players list and to achieve the desired purpose we should explicitly request data from end-point http://localhost:4000/players/2
correct?
...
playerEditPage : Model -> PlayerId -> Html Msg
playerEditPage model playerId =
case model.players of
RemoteData.NotAsked ->
text ""
RemoteData.Loading ->
text "Loading ..."
RemoteData.Success players ->
let
maybePlayer =
players
|> List.filter (\player -> player.id == playerId)
|> List.head
in
case maybePlayer of
Just player ->
Players.Edit.view player
Nothing ->
notFoundView
RemoteData.Failure err ->
text (toString err)
...
Thanks.
I configured historyApiFallback
of webpack devserver to use index.html
and it does work. However, font-awesome icons does not shown when refreshed on pages. It restores when I refresh on route root /
.
@sporto First of all Thanks a lot for this awesome tutorial. From start-to-end I didn't faced any discrepancy or errors executing the code given in tutorial on my dev machine.
Now coming to the issue I am facing:
In section Navigation approaches here you have mentioned the cons of using hash routing and as an alternative suggesting to use path routing which does look intuitive compared to the hash routing. I followed the example you referred to in https://github.com/sporto/elm-navigation-pushstate and updated my code such that it uses path routing.
The changes I made can be found below:
Msgs.elm
Routing.elm
CustomHtmlEvents.elm (newly introduced)
Players/List.elm
Players/Edit.elm
And that does work! Please refer the screenshots attached while navigating:
1.png (top route)
2.png (Player route)
3.png (Players route accessed via clicking the List link on top-left in 2.png)
Now what I am unable to understand is that when I try to access routes clicking navigation links they work. However when I manually change the route in browser's address bar like
http://localhost:3000/players
I get followinghttp://localhost:3000/players/2
I get followinghttp://localhost:3000/
- This one does work by showing the Listing.http://localhost:3000/#players
- This one too works by showing the Listing but I am wondering why? Why this "hash" routing works?However
http://localhost:3000/#players/2
doesn't work.So I am seeking your help in understanding the above behavior regarding manually changing the URL in address bar and they returning 404 response and how to fix them up to behave as expected i.e. whether I type in the URL or use navigation button or links they should show the desired data.
I have just started learning Elm and also novice to Functional Programming so please bear me on any questions I asked which sound silly to you.
Again Thank you so much for this wonderful tutorial. It demonstrates sheer clarity you have with this tutorial and is definitely one of the foremost resources one should refer to get started with Web App Development with Elm and at the same time getting acquainted the language's core concepts.