If you haven't read part one or part two yet, the context might be helpful.
Also, this is going to use a slight variation on Elm's normal Html
module. If
you want to read more about why / how that is, I'll be sure to write about it in
another blog post.
Last time, we were able to send NodeJS a list of routes, but those routes were just strings, so our web server was kinda boring...
This time around, we're going to make our routes look more like this:
homepage : Html msg
homepage =
html [ lang "en" ]
[ head []
[ title "Homepage"
, meta
[ name "description"
, content "This is the homepage."
]
, link
[ href "https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.2/css/bulma.min.css"
, rel "stylesheet"
]
]
, body []
[ div [ class "hero is-fullheight has-text-centered is-info" ]
[ div [ class "hero-body" ]
[ div [ class "container" ]
[ h1 [ class "title is-1" ]
[ text "Elm ExpressJS" ]
, h2 [ class "subtitle is-4" ]
[ text "A web server built with elm!" ]
, a
[ href "/people"
, class "button is-info is-inverted is-outlined"
]
[ text "To the people page!" ]
]
]
]
]
]
And then get something like this:
and be able to navigate to this:
because our server is actually rendering HTML and sending that as a response:
<html lang="en">
<head>
<title>People</title>
<meta name="description" content="This is the people page." />
<link href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.2/css/bulma.min.css" rel="stylesheet" />
<link rel="icon" href="https://seeklogo.com/images/E/elm-logo-9DEF2A830B-seeklogo.com.png" />
</head>
<body>
<div class="hero is-fullheight has-text-centered is-success">
<div class="hero-body">
<div class="container">
<h1 class="title is-1">People Page</h1>
<h2 class="subtitle is-4">Look at all those humans!</h2>
<a href="/" class="button is-success is-inverted is-outlined">Back to homepage</a>
</div>
</div>
</div>
</body>
</html>
Kinda neat, right?
Let's dive into how we can make this possible by updating our Elm app!
Route
Javascript gets information from Elm by receiving a list of routes. Last time,
our Route
records looked like this:
type alias Route =
{ path : String
, response : String
}
Let's tweak that data structure, so we can support more than just a String
in our
Elm code.
type alias Route =
{ path : String
, response : Response
}
We'll still have to turn it back to a String
before sending things over to
Javascript, but that's a problem for our port logic, not our Route
!
Let's define what the Response
type looks like.
Unfortunately (and for good reason), the native Elm Html
package doesn't expose a toString
function.
We can't convert Html -> String
that easily.
Representing HTML isn't that complicated. And I've already mocked up a
simpler version that we'll be referring to as Ssr.Html
for the rest of the app.
(It doesn't have all the cool JS event stuff in it, but the server doesn't run client-side JS, so no worries).
Elm has a language feature called "custom types" that we'll use here to cover
all the possible ways we can make the response
string for our ExpressJS app.
Let's make one now:
type Response
= PlainText String
| Html Ssr.Html
Response
is the type of thing we'll be making, and PlainText
and
Html
both act as ways to create Response
types.
Those two words are also called "type constructors", because they construct
things of type Response
.
Let's take a quick look at the types to understand how they work:
PlainText
PlainText : String -> Response
PlainText
just needs a String
to make a Response
.
response : Response
response =
PlainText "Hello!"
Html
Html : Ssr.Html -> Response
Html
just needs an Ssr.Html
value to make a Response
.
response : Response
response =
Html (h1 [ class "title" ] [ text "Hello!" ])
By using a custom type, we will be able to make one toString
function that
turns a Response
into a String
:
toString : Response -> String
toString response =
case response of
PlainText someString ->
someString
Html someHtml ->
Ssr.Html.toString someHtml
Using the custom type will let us extract out the values. (A String
in the
first case, and a Ssr.Html
in the second).
For the PlainText
case, we can return the String
directly.
For the Html
case, we can use Ssr.Html.toString
, which will convert our Ssr.Html
into a String
for us!
Another bonus: if we add more options to Response
later, Elm's compiler will
bring us back to this case of
expression and remind us to add the new case.
Response
First, let's change our routes
to use the new type:
import Ssr.Html exposing (h1, text)
import Ssr.Html.Attributes exposing (class)
type Response
= PlainText String
| Html Ssr.Html
routes =
[ Route "/"
(Html
( h1 [ class "title" ]
[ text "This is the homepage!" ]
)
)
, Route "/people"
(PlainText "This is the people page!")
]
We should create a new type, and update outgoing
port to talk to our
ExpressJS app.
This is necessary because JS doesn't know what "custom types" are!
type alias JsRoute =
{ path : String
, response : String
}
port outgoing : List JsRoute -> Cmd msg
If we make a simple function that turns Route -> JsRoute
, using the
toString
defined above:
toString : Response -> String
toString response =
case response of
PlainText someString ->
someString
Html someHtml ->
Ssr.Html.toString someHtml
toJsRoute : Route -> JsRoute
toJsRoute route =
JsRoute
route.path
(toString route.response)
Finally, our init
function needs to use that function to convert our
List Route
to List JSRoute
before calling outgoing
:
init : Flags -> (Model, Cmd Msg)
init flags =
( ""
, outgoing (List.map toJsRoute routes)
)
Let's rebuild our app:
npm run build
npm run start
Head over to http://localhost:3000 and look at that h1
tag!
And check out that page source:
<h1 class="title">This is the homepage!</h1>
We can use the html
, body
, and other functions in Ssr.Html
to make a full
web page with actual CSS and JS too!
Looks like we did it again! HTML coming from Elm? Is this the future??
There's still a lot of interesting areas to explore before this is complete:
Passing Request
information to Elm.
Reusing Elm HTML on client and server.
Extending Response
to make JSON
or XML
api endpoints?
Where are the cat GIFs?
If you'd like to play around with the project, it's source code is available on Github over here.
(I've organized the content into an Express
module, so the Main
module has
all your routes.)
Thanks for reading, maybe I'll continue this exploration further later on!