Skip to main content

Fable

The following is a collection of short guides and tips for common pitfalls when using Fable for frontend development. Feel free to checkout the categories on the in-page table of contents in the sidebar and navigate to the topic you are interested in.

Fable official docs

Altough I might repeat some of the content here, I highly recommend you to check out the official Fable JavaScript documentation. It contains a lot of useful information.

JavaScript Objects

When working with JavaScript libraries or even Browser APIs you will often encounter JavaScript objects.

const MyObject = {
name: "Feliz",
version: 3,
isAwesome: true
}

Altough this looks very similiar to a F# record type, it is not the same. Fable transpiles F# record types to classes. If you want to create a plain JavaScript object, you have multiple options. You should use the one that fits your use case the best.

  • Use a anonymous record types

    Very flexible and can be created inline.

    let myObject = {| name = "Feliz"; version = 3; isAwesome = true |}
  • Use a f# class with the [<PojoAttribute>] attribute

    Allows for optional parameters.

    [<Fable.Core.JS.PojoAttribute>]
    type POJOClass(?name: string, ?version: int, ?isAwesome: bool) =
    member val name = name with get, set
    member val version = version with get, set
    member val isAwesome = isAwesome with get, set

    let Pojo_Class = POJOClass(name = "Fable", isAwesome = true)

    let Pojo_Class2 = POJOClass("Fable")
  • Use a f# class with the [<ParamObject; Emit("$0")>] combination

    [<AllowNullLiteral>]
    [<Global>] // do not generate code for this with Fable
    type Options
    [<ParamObject>] // pass all parameters as a single JS object
    [<Emit("$0")>] // ... and return only the parameters after transpilation to JS.
    (
    searchTerm: string,
    ?isCaseSensitive: bool
    ) =
    member val searchTerm: string = jsNative with get, set
    member val isCaseSensitive: bool option = jsNative with get, set

    let options1 = new Options("foo")
    let options2 = new Options(Regex("foo"))
Fable Transpilation comparisons

Here you can find direct fable transpilation comparisons between the different approaches.

Direct comparison between multiple POJO approaches
module Examples.Guides.POJO

// Record types might look like JavaScript objects, but Fable does not treat them as such.

/// This will be a class and NOT a plain JS object!
type POJORecord = {
name: string
version: int
isAwesome: bool
}

let Pojo_RecordType: POJORecord = {
name = "Feliz"
version = 3
isAwesome = true
}

// Anonymous records are transpiled to plain JS objects!

let Pojo_AnonymousRecordType = {|
name = "Fable";
version = 4;
isAwesome = true;
noTypeReference = true
|}

// You can also write type references for anonymous records.
type POJOAnonymousRecord = {|
name: string
version: int
isAwesome: bool
|}

let Pojo_AnonymousRecordTypeWithType : POJOAnonymousRecord = {|
name = "Fable"
version = 4
isAwesome = true
|}

// You can also use the [<PojoAttribute>] to create POJOs from classes.
// This allows for optional properties

[<Fable.Core.JS.PojoAttribute>]
type POJOClass(?name: string, ?version: int, ?isAwesome: bool) =
member val name = name with get, set
member val version = version with get, set
member val isAwesome = isAwesome with get, set

let Pojo_Class = POJOClass(name = "Fable", isAwesome = true)

// You can also recreate the POJOAttribute by yourself using Emit and Erase

open Fable.Core

[<AllowNullLiteral>]
[<Global>] // do not generate code for this with Fable
type Options
[<ParamObject>] // pass all parameters as a single JS object
[<Emit("$0")>] // ... and return only the parameters after transpilation to JS.
(
searchTerm: string,
?isCaseSensitive: bool
) =
member val searchTerm: string = jsNative with get, set
member val isCaseSensitive: bool option = jsNative with get, set

Promises and Async

The f# native way of handling asynchronous computations is using async. However, when working with JavaScript libraries and the browser environment, you will often encounter Promises.

Promises

Promises are the JavaScript way of handling asynchronous operations. In Fable, I highly recommend using the Fable.Promise nuget package, which provides a clean API for working with Promises in Fable.

Fable.Promise

Fable.Promise is a library that provides a simple and efficient way to work with JavaScript Promises in Fable. It allows you to create, chain, and handle Promises using F# computational expressions.

module Examples.Guides.FablePromise

let fetchDataMock = fun () ->
promise {
do! Promise.sleep 2000
return [1;2;3;4;5]
}

let fetchData(setLoading: bool -> unit, setErrorMsg: System.Exception option -> unit, setData: list<int> -> unit) =
// f# computational expression (CE), `promise { ... }`
promise {
setLoading true
// `let!` is alike to `let data = await ...` in JS/TS
let! data = fetchDataMock()
setLoading false // set loading to false AFTER data is fetched
return data
}
|> Promise.map (fun data -> // could have been done inside the CE too, this is just for demonstration
setData data
)
|> Promise.catch (fun err ->
// handle error here
setLoading false
setErrorMsg (Some err)
)

tip

Checkout the Fable.Promise docs for more examples!

The example above returns a Promise<unit>, which cannot be directly returned in a prop.onClick handler. You can use fetchData(...) |> Promise.start to make the f# compiler happy.

Async

In f# async is is the native way to handle asynchronous computations. It is similar to JavaScript's async/await syntax.

Interacting with native JavaScript libraries, as well as f# libraries might require you to use both async and promise. But they can be conviniently converted between each other using Async.AwaitPromise and Async.StartAsPromise.

module Examples.Guides.Async

let nativeJsPromiseMock() = promise {
do! Promise.sleep 2000
return [1;2;3;4;5]
}

open Fable.Core // contains `Async.AwaitPromise` and `Async.StartAsPromise`

let fetchData(setLoading: bool -> unit, setErrorMsg: System.Exception option -> unit, setData: list<int> -> unit) =
async {
setLoading true
try
let! data =
nativeJsPromiseMock()
|> Async.AwaitPromise // transform js promise to f# async
setLoading false // set loading to false AFTER data is fetched
setData data
with err ->
// handle error here
setLoading false
setErrorMsg (Some err)
}
|> Async.StartAsPromise // return async to promise
danger

The most important difference between async and promise is that async is lazy and will not start executing until you explicitly start it using Async.Start or Async.StartImmediate. On the other hand, promise is eager and starts executing as soon as it is created.

Where are the Browser Types?

Here is a example of global namespaces in a browser environment and where to find them in Fable:

module Examples.Guides.BrowserTypesGlobalNamespaces

open Browser.Dom

let getandSetWindowDimensions() =
let width = window.innerWidth
let height = window.innerHeight
console.log("Window dimensions:", width, height)
let newDiv = document.createElement("div")
newDiv.textContent <- sprintf "Window dimensions: %A x %A" width height
document.body.appendChild newDiv |> ignore

Here is an example of using browser event types:

tip

Checkout the section on Promises for more information on how to work with async code in Fable.

module Examples.Guides.BrowserTypesEvents

open Browser.Types // for MouseEvent and File types
open Fable.Core.JsInterop // only used for `?` operator

let myButtonOnClick(e: MouseEvent) =
Browser.Dom.console.log("Button clicked at coordinates:", e.clientX, e.clientY)

let fileUpload(file: File, setData, setLoading) =
if isNull file then
()
else
promise {
// `.text()` is inherited from the `Blob` type: https://developer.mozilla.org/en-US/docs/Web/API/Blob
let! (text: string) = file?text()
let values: string [] [] =
text.Split([| "\r\n"; "\n" |], System.StringSplitOptions.None)
|> Array.map (fun line ->
line.Split([| "\t" |], System.StringSplitOptions.None)
|> Array.map (fun value -> value.Trim())
)
do! setData file.name values
}
|> Promise.catch (fun ex ->
Browser.Dom.console.error ex
setLoading false
)
|> Promise.start

unbox<_>

unbox<_> is the Fable equivalent of TypeScript's as keyword. It is used to cast a value to a different type. However, it does not perform any runtime checks, so if the value is not of the expected type, it can lead to runtime errors.

It should only be used if you are sure about the type of the value you are casting.

Here is a example using [<StringEnum>] (which makes fable transpile the union cases to string literals) and setting the value to local storage:

module Examples.Guides.UnboxStringEnum

open Fable.Core
open Feliz

open Fable.Core.JsInterop // open this to use the `!!` operator

[<StringEnum; RequireQualifiedAccess>]
type Theme =
| Light
| Dark
| System

let setTheme (theme: Theme) =
// We can do either `unbox<string> theme` or `unbox theme`
// `unbox theme` works because the compiler can infer the target type from the context of the function call
Browser.Dom.document.documentElement.setAttribute("data-theme", unbox<string> theme)
Browser.Dom.window.localStorage.setItem("theme", unbox<string> theme)
// We can also use the `!!` operator to unbox. It works the same way as `unbox` by type inference
// It can be used after `open Fable.Core.JsInterop`.
Browser.Dom.window.sessionStorage.setItem("theme", !!theme)
info

!! is a shorthand operator for unbox and can be used after opening Fable.Core.JsInterop.