Fable and Cordova, a Mobile Story

Read Time: 9 minutes

Today’s article will look at combining Fable and Cordova to make a mobile application. For those not familar with these projects, a little context may be helpful. Fable compiles F# to Javascript and Cordova facilitates mobile app development using HTML and Javascript. Throw in some Elmish, and its a party. Joining these technologies provides some unique possibilities. Specifically this is an interesting way to leverage F# for mobile development.

The goal will be to make a very simple hello world mobile app. There are a couple steps to get there, none of which are too complicated. To follow along, you will need the following prerequisites. Make sure you have .NET Core and NodeJs installed. Next, Cordova must be installed: npm install -g cordova. Now that the building blocks are installed, it is time to get started.

First, the Cordova project will be created and initialized with the supported platforms. browser is useful for the development process, while android is the real target. If you’re of an iOS leaning, it can be the mobile target instead, or as an addition.

1
2
3
4
5
cordova create HelloWorld
cd HelloWorld
cordova platform add browser
cordova platform add android
# For iOS support: cordova platform add ios

Second, the Fable part of the project needs created. From inside the main project, create a new F# app and add the basic Fable requirements. The requirements come in two parts: dotnet and javascript. For this sample app this will be all that is needed, although a fuller functional app may have more dependencies.

1
2
3
4
dotnet new console -lang F# -n App
cd App
dotnet add package Fable.Core
dotnet add package Fable.Elmish.React

In the main project directory add the javascript dependencies.

1
2
3
4
5
npm install fable-compiler --save
npm install fable-loader --save
npm install @babel/core --save
npm install react --save
npm install react-dom --save

Third, it is time to wire the projects together, which is where webpack enters the picture. The Fable sample project already uses webpack, the config just needs some adjustments in order to publish the results to cordova (instead of a website).

1
2
npm install webpack  --save
npm install webpack-cli --save

In the main project directory, webpack needs a config file webpack.config.js file with the following contents. This tells webpack to process the newly create F# app project, and use the fable-loader to process files. The destination bundle file generated is placed in Cordova’s www/js directory.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var path = require("path");

module.exports = {
mode: "development",
entry: "./App/App.fsproj",
output: {
path: path.join(__dirname, "./www/js"),
filename: "bundle.js",
},
module: {
rules: [{
test: /\.fs(x|proj)?$/,
use: "fable-loader"
}]
}
}

The project directory should look something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
HelloWorld
├── App
│   ├── App.fsproj
│   └── Program.fs
├── config.xml
├── hooks
├── node_modules
├── package.json
├── package-lock.json
├── platforms
├── plugins
├── webpack.config.js
└── www
├── css
├── img
├── index.html
└── js

Now that the main project structures have been put into place, it is time to put the code together. The Cordova project needs to be able to consume the generated Fable bundle created by webpack. This is done by editing the Cordova index page www/index.html. The contents can be reduced to below. The key points here are the elmish-app div and the bundle.js include.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Security-Policy" content="default-src 'self' data: gap: https://ssl.gstatic.com 'unsafe-eval'; style-src 'self' 'uns
afe-inline'; media-src *; img-src 'self' data: content:;">
<meta name="format-detection" content="telephone=no">
<meta name="msapplication-tap-highlight" content="no">
<meta name="viewport" content="initial-scale=1, width=device-width, viewport-fit=cover">
<link rel="stylesheet" type="text/css" href="css/index.css">
<title>Hello World</title>
</head>
<body>
<div id="elmish-app" class="elmish-app"></div>
<script type="text/javascript" src="cordova.js"></script>
<script type="text/javascript" src="js/index.js"></script>
<script src="js/bundle.js"></script>
</body>
</html>

Cordova is set to accept some Fable code, now it is time to get to the F#. The contents of App/Program.fs are below. Since this is about wiring Fable to Cordova I don’t want to spend too much time on the specific F# code. But the code takes a person’s name as input and replies with a Hello message. To add a bit of mobile flavor, it also tracks current acceleration of the device using the devicemotion event, and publishes that to the screen as well.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
module App

open System
open Browser
open Browser.Types
open Elmish
open Elmish.React
open Fable.React
open Fable.React.Props
open Fable.Import

type Model =
{ Name :string; Message :string; Acceleration :string }

type Msg =
| Greet of string
| DeviceMotion of float * float * float

let initModel() =
{ Model.Name = ""; Message = ""; Acceleration = "" }

let newMessage (name :string) =
sprintf "Hello %s, it is %s" name (DateTime.Now.ToString("yyyy-MM-dd"))

let newAcceleration x y z =
sprintf "Acceleration: (%0.3f, %0.3f, %0.3f)" x y z

let updateModel (msg :Msg) (model :Model) =
match msg with
| Greet(name) -> { model with
Name = name;
Message = newMessage name }
| DeviceMotion(x, y, z) -> { model with
Acceleration = newAcceleration x y z }
let view (model :Model) dispatch =
div []
[
div []
[
str "My name is:"
input
[
AutoFocus true
OnInput (fun e -> dispatch (Greet ((e.target:?>HTMLInputElement).value)))
Value (model.Name)
]
]
div []
[
str model.Message
br []
str model.Acceleration
]
]

let deviceMotion initial =
let sub dispatch =
window.addEventListener("devicemotion", fun e ->
let e' = e :?> DeviceMotionEvent
dispatch (DeviceMotion (e'.acceleration.x, e'.acceleration.y, e'.acceleration.z)))
Cmd.ofSub sub

Program.mkSimple initModel updateModel view
|> Program.withReactBatched "elmish-app"
|> Program.withSubscription deviceMotion
|> Program.withConsoleTrace
|> Program.run

All that is left now is to build and run the app. There are a couple things to keep track of: building the Fable app with webpack, running the app in the browser for development, and building. The commands below touch the surface of development and building, but they at least get you started.

1
2
3
4
5
6
7
8
9
10
11
12
# Run webpack on the fable app
npx webpack

# Run cordova in the browser
cordova run browser

# Build and run on the phone or emulator
cordova build android
cordova run android

# Install it on the phone
adb install

A running app looks like this:

Cordova App

Being able to build is great, but development can be cumbersome without the ability to watch and auto-build file changes. Taking the next step, let’s add some convenience to the process. Add the following to package.json.

1
2
3
4
"scripts": {
...
"watch": "npx webpack -w"
}

The above command is just the interface, it needs some modifications to webpack.config.js to perform the desired actions. The below changes wire into the afterEmit hook to refresh the cordova browser (and start the local service if not already started). The watchOptions aren’t strictly needed for this, but it helps to stop rebuild spamming, and lower file watch needs. These values can be tuned more for specific needs.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const exec = require("child_process").exec;
var cordovaServerStarted = false;

module.exports = {
...
watchOptions: {
aggregateTimeout: 500,
ignored: [ "node_modules" ]
},
plugins: [
{
apply: (compiler) => {
compiler.hooks.afterEmit.tap("AfterEmitPlugin", (_compilation) => {
exec("cordova prepare browser", (err, stdout, stderr) => {
if (stdout) { process.stdout.write(stdout); }
if (stderr) { process.stderr.write(stderr); }
});

if (!cordovaServerStarted) {
cordovaServerStarted = true;
exec("cordova run browser", (err, stdout, stderr) => {
if (stdout) { process.stdout.write(stdout); }
if (stderr) { process.stderr.write(stderr); }
});
}
});
}
}
]
};

Now, a simple npm run watch will start the Cordova browser, watch for application updates, and auto-build as files are edited. The development process just got way easier. As a reminder, this is just a sample; more goes into properly building everything out. With that said, this provides a good starting point to getting F# running on mobile using Cordova. I hope you found this useful and/or interesting. Until next time…