What happens if we send a number instead of a string from JavaScript to our Elm app? Let’s find out by replacing Hey Elm!
with 10
in the callback function given to app.ports.sendData.subscribe
inside beginning-elm/index.html
.
Reload index.html
in a browser and click the Send Data to JavaScript button. Number 10
doesn’t appear on the page. To find out why, open the browser console.
Unlike JavaScript, Elm is very strict about data types. Since we said the receiveData
port will receive a value of type Model
, which is just String
behind the scenes, Elm doesn’t let any other types through that port.
This is a great news because Elm has our back if the JavaScript code misbehaves by trying to sneak in a type we aren’t expecting. We can actually make this mismatched type detection process even better by displaying a helpful message to the users instead of just crashing our app.
Which Types are Allowed through Ports?
Before we improve our app by not letting it crash whenever a wrong type is received from JavaScript, let’s understand what types of data Elm allows through both incoming and outgoing ports.
Interacting with JavaScript code from an Elm app is very similar to how we interact with an HTTP server. Therefore, to keep things simple Elm prefers to stick with JSON when sending and receiving data from JavaScript as well.
JSON stands for JavaScript Object Notation and is derived from JavaScript. It’s not a coincidence that all of the valid types in JSON listed below — except null
— are also available in JavaScript. This makes converting JSON values to JavaScript and vice versa incredibly easy.
Note: In JavaScript, null
is a value whose type is object
. Unfortunately, this is a bug in the language according to Brendan Eich — the creator of JavaScript. Luckily, JSON sidestepped this bug by creating a separate type for null
.
-
string - A string must be written in double quotes and looks very similar to an Elm string.
-
number - A number must be an integer or a float.
-
boolean - A boolean must be either
true
orfalse
. -
null -
null
is used to indicate an absence of a value. -
Array - An array can contain any other JSON values including arrays themselves.
-
object - An object consists of key value pairs.
Despite its roots in JavaScript, JSON is language independent. In fact, it was originally created to simplify the interchange of data between applications regardless of which language they were written in. Now you can see why the designers of Elm chose JSON for communicating with JavaScript code.
The table below shows the mapping between Elm and JSON values when we send and receive data from JavaScript via ports.
Elm Value | Elm Type | JSON Value | JSON Type | JavaScript Type |
---|---|---|---|---|
"Hello JavaScript" |
String |
"Hello JavaScript" |
string |
string |
10 |
Int |
10 |
number |
number |
3.14 |
Float |
3.14 |
number |
number |
True |
Bool |
true |
boolean |
boolean |
Nothing |
Maybe a |
null |
null |
object |
{ age = 25 } |
Record |
{"age" : 25} |
object |
object |
( 9, "Pam", False ) |
Tuple |
[9, "Pam", False] |
array |
Array |
[ 1, 2 ] |
List Int |
[1, 2] |
array |
Array |
[ "Mr", "Robot" ] |
List String |
["Mr", "Robot"] |
array |
Array |
Decoding JSON Values Received from JavaScript
By default, the runtime is in charge of converting data between Elm and JavaScript. For example, when receiving data from JavaScript, the runtime first converts JavaScript values to JSON and then decodes the JSON into corresponding Elm values. That’s why we didn’t have to decode the data coming from JavaScript ourselves. Elm knew how to translate it properly just by looking at the type of our incoming port function.
A downside of letting the runtime do the decoding behind the scenes is that if the JavaScript code sends an incorrect type, our app simply crashes. A better approach is to do the decoding ourselves. That way if anything goes wrong, we’ll be able to show a proper error message to the user.
Modifying Incoming Data’s Type
Let’s start by changing the type of receiveData
incoming port from Model
to Value
in PortExamples.elm
.
The Value
type represents a JSON value. It’s defined in the Json.Encode
module, but the Json.Decode
module also makes it available through the use of type alias
.
This way we don’t have to import Json.Encode
if all we’re doing is use the Value
type. Import Json.Decode
in PortExamples.elm
.
We’ve also exposed the Error
type, string
decoder and decodeValue
function. We’ll use them later.
Handling ReceivedDataFromJS
Differently
Now that the type of our incoming data has changed from Model
to Value
, we need to modify the ReceivedDataFromJS
message in PortExamples.elm
.
This means we can’t simply return incoming data as model inside the ReceivedDataFromJS data ->
branch in update
anymore. We need to decode the incoming JSON first. Let’s make that change.
decodeString vs decodeValue
In the Decoding JSON section, we used decodeString
to parse a raw string fetched from an HTTP server into JSON and then used the string
decoder to transform that JSON into an Elm string.
The figure below shows the process decodeString
goes through while decoding values from a raw JSON string.
The code we just added to the ReceivedDataFromJS value ->
branch in update
doesn’t use decodeString
. It uses decodeValue
instead. That’s because the data coming from JavaScript is already a valid JSON. An HTTP server on the other hand sends a raw string which must be parsed first to make sure that it’s a valid JSON. That’s why we had to use decodeString
in the Decoding JSON section. The decodeValue
function skips the parsing altogether and focuses on transforming a valid JSON into an Elm value as shown in the figure below.
Here’s how decodeString
and decodeValue
’s type signatures differ:
Modifying the Model
We’re using a case
expression to handle both success and failure scenarios inside the ReceivedDataFromJS value ->
branch of update
.
If decoding succeeds, the decoded value gets assigned to the dataFromJS
field in our model. If it fails, we need to store the error in jsonError
field. Let’s add those fields to Model
in PortExamples.elm
.
We also need to modify init
to comply with the new model structure.
Displaying Error Message
The only thing left is to display the error message produced by decodeValue
if decoding fails. Replace the view
function in PortExamples.elm
with the following code.
The jsonError
field in our model is of type Json.Decode.Error
. Here’s how it’s defined:
Note: If you don’t remember how the Error
type works, you may want to review the Decoding JSON - Part 1 section.
The error message we’re interested in is inside Failure
. That’s why we ignored all other data constructors.
Testing
We’re now ready to test. Recompile PortExamples.elm
by running the following command from the beginning-elm
directory in terminal.
Everything should compile fine. Reload the beginning-elm/index.html
file in a browser and open browser console. Click the Send Data to JavaScript button. Elm now shows a friendly error message on the page itself instead of crashing the app and pointing out what went wrong in the console.
Let’s fix that error by sending Hey Elm!
instead of 10
from JavaScript in index.html
.
Reload index.html
and click the Send Data to JavaScript button. You should see Hey Elm!
.
You may be wondering why we had to go through such an elaborate process to receive a simple string from JavaScript. Actually the process for receiving more complex data from JavaScript is also very similar. The only difference is that we’ll need to write decoders that are much more sophisticated than string
.
Sending Complex Data to JavaScript
The process for sending complex Elm data to JavaScript is quite similar to the one we used for sending a string in the Sending Data to Javascript section. Why don’t we create some complex data in our Elm app and try to send it to JavaScript to understand this process better? Here’s how the data we want to send looks after it gets translated to JSON:
Let’s use records to represent various JSON objects shown above. Add the following code right below the Model
type in PortExamples.elm
.
Next add a new field called dataToJS
to Model
in PortExamples.elm
.
We need to update init
to create an initial value for dataToJS
. Replace the current implementation of init
with the following in PortExamples.elm
.
Since we want to send more complex data to JavaScript, we need to change the type of sendData
outgoing port from String
to ComplexData
in PortExamples.elm
.
We also need to modify the SendDataToJS ->
branch in update
to use the dataToJS
field in our model instead of "Hello JavaScript!"
.
We’re now ready to recompile PortExamples.elm
. Run the following command from the beginning-elm
directory in terminal.
Let’s make the console output easier to read by converting the JavaScript value sent by Elm to a raw JSON string. Replace data
with JSON.stringify(data)
in index.html
.
Reload index.html
and open browser console. Click Send Data to JavaScript and you should see a JSON representation of the complex data sent from Elm in the console.
The runtime translated individual Elm types contained in complexData
to corresponding JSON types. All of that translation happened behind the scenes. We didn’t have to do anything other than hand Elm value over to the outgoing port.
Summary
One of the biggest advantages of using Elm is it guarantees there won’t be any type errors when an app is running. To fulfill that guarantee, Elm must reject all incorrectly typed values from entering the app. The runtime will throw an error as soon as it detects an incorrect type trying to sneak through an incoming port.
It’s not a good practice to let Elm crash our app if automatic decoding fails. We can prevent that by decoding the incoming data ourselves. This also allows us to display a friendly message to the user if decoding does fail. Here is the entire code from PortExamples.elm
for your reference: