Getting started with Fable and Feliz
tl;dr
You can also use the Feliz template to get a similar result of what we are going to do here, however this post is intentially showing the steps to manually set up Feliz and explain some things on the go.
Intro
When I started my F# journey two or so years ago, I was quite hooked by the language, its features and some extending technologies like Fable. The general sentiment of folks who try this thing is that they never want to go back (I might be one of those now).
However really getting started with a new thing, especially if it is quite different that what you are used to, takes some time. I wanted to relatively fast dive into more advanced stuff like Fable and Fable.React. I thought that when using a template or example application that shows what is possible plus some F# beginner tutorials I could go from there. However this approach had problems:
- F# has quite a different syntax then C# or Java. When you try out a language with a similar paradigm (let's say from Java to C#), you will feel familiar and won't have too many problems. Switching from an object-oriented language to a function one however is difficult. Switching from C# to F# will first be uncomfortable. Less curyl braclets, white space is important, type annotations often not needed, pattern matching, computation expressions, pipings...
- Usually F# tutorials or docs not only come with plain F#, but additional tooling (like Paket, Femto) and other architectural patterns (like Elmish) are added. These totally exist for a reason, however when learning a new thing these just add noise and add a complexity and chances for errors that were frustrating.
- Documentation/content: As the community is relatively small, there is not too much general F# content out there. I don't remember the Fable documentation at that time, now when revisiting it looks pretty good. However I am still missing walk throughs and bigger tutorials (so fixing this gap a little with this post).
- Webpack: Some time ago Webpack was needed to run Fable.React. That also was a pain point for me. Nowadays there is Vite, which is way easier to setup. So one problem less overall.
Now after quite some time revisiting, getting somewhere with Fable and Feliz does not seem to hard anymore, however the documentation is still a bit scattered and I didnt see a good walkthrough to get started with Feliz, without prior knowledge about Fable.
The following resources have been helpful when learning, so visit them if you want to learn more:
- fable.io/docs/
- zaid-ajaj.github.io/Feliz/
- tpetricek.github.io/Fable/docs/interacting.html
- medium.com/@zaid.naom/f-interop-with-javascript-in-fable-the-complete-guide
Initial setup
Before we begin you need the dotnet sdk and node installed. I am using node 18 and dotnet 7 when writing this post.
Lets create a simple F# console application, install fable as a dotnet tool and compile the Program.fs
file to typescript:
dotnet new console -lang F# --name GettingStartedWithFablecd GettingStartedWithFabledotnet new tool-manifestdotnet tool install fabledotnet fable --lang ts
Voilà, if you have no errors than you might just have compiled some F# to Typescript!. Lets check the files that we have now:
./.config/dotnet-tools.json
is our dotnet tool manifest file, containing the fable tool, that allows us to run dotnet fable
to compile F# to some other language:
GettingStartedWithFable.fsproj
is our F# project file, referencing all F# files that need to be compiled:
Program.fs
contains our F# code:
Program.fs.ts
contains the resulting TypeScript code:
There are other folders like ./bin/
./fable_modules/
and ./obj/
that we can ignore.
Add vite
In order to run our code we can either compile it to JavaScript and execute with node, or we can use a bundler like webpack or vite. I will use vite in this case. Let's add a couple of files to our folder:
package.json
for our frontend depenencies, scripts and other metadata:
tsconfig.json
for our TypeScript compiler options:
the target
of "es2022"
here is important, older options seem not to be supported by Fable.
index.html
as our root html file:
vite.config.ts
for our vite configuration:
When running
npm inpm run client
Opening the website that is running on localhost we get a white screen. However when looking into the developer console we should see Hello from F#
!
Add Fable.Browser.Dom
In order to interact with the DOM, we can use the Fable.Browser.Dom
package. Let's add it to our project and update Program.fs
:
dotnet add package Fable.Browser.Dom
Our GettingStartedWithFable.fsproj
should now look like this:
Running dotnet fable --lang ts
or dotnet fable watch --lang ts
(to continuously watch for changes) will update our Program.fs.ts
file to look like this:
And our website should now say Hello from F# and Fable!
on the screen!
Cool. Now lets look at some things Fable.Browser.Dom gives us, to work with Javascript from F#.
In my experiments watching for changes often did not work. So just running dotnet fable --lang ts
usually worked better.
Fable.Browser.Dom
As seen in the last section, Fable.Browser.Dom
gives us access to the document
object to interact with the DOM. There are some more constructs that we can use to interact with JavaScript, like jsNative
and Emit
which we will look at in the next section.
Emit
The Emit
attribute allows us to write raw javascript/typescript:
[<Emit("(5 as number)")>]let x: obj = jsNative
The attribute is added to a value, so that it can be used in F# code. You should give the value a type that best represents the given JavaScript/TypeScript code. Whenever this value is used it will be replaced with the given JavaScript/TypeScript code during compilation.
For example:
Results in:
Note that this is not typesafe, as you can put any string into the Emit attribute.
jsNative
The jsNative
value that we have seen, is a value that is never evaluated. If it would be, an exception would be raised (this can happen if you try to run the F# code directly, and not compile it to another language).
jsNative
can be used when we want to use some JavaScript code that we know is or will be there at runtime. In the previous case we know the Emit
attribute will provide the JavaScript wherever x
is used. In other cases, for example if we want to call some JavaScript code that we know or expect to be there, we also can use jsNative
as we see in the next example.
Writing native JavaScript/Typescript and use it in F#
One cool thing about Fable is that in the end we just run JavaScript/TypeScript (or whatever language we are targeting), and it is possible to add native code ourselves and use it in F#.
Let's add a new file hello.js
that is just plain JavaScript (we could also write TypeScript, but it would not make a big difference):
And in our Program.fs
we can now import this function with the Import
attribute:
open Fable.Core[<Import("hello", "./hello.js")>]let hello: unit -> unit = jsNativehello ()
Note that we also typed the function to be unit -> unit
, which should represent the given JavaScript code very well. Note again that this is not type-safe. We ourselves decide what type we want to use here. Afterwards the function can be called as if it was a normal F# function.
Generated code:
For some popular packages there exists Fable bindings. So others already have typed the packages for us, and we can use them in a type-safe style.
Besides importing by attribute, it is also possible to use the function import
which can be used if we open Fable.Core.JsInterop
.
Results in
It looks slightly different but essentially it is the same.
If we would want to use a published npm package, we just need to add them to package.json
, run npm install
and add an import similar to how we did earlier, but use the package name for the path (same concept as in JavaScript):
Having some understanding of Emit
, jsNative
and Import
should give us enough knowledge to practically use Fable.
Other Fable JavaScript/TypeScript features like interacting with the Global
object can be found on the Fable website.
Add Feliz
Now let's add the Feliz
package in order to write React code in F#:
dotnet add package Feliznpm i react react-dom @types/react
Feliz provides basically a DSL that looks similar to React and comes with typings for all native html/react components (like div, span, h1, button and css styles). The code here is equivalent to:
import React from "react"import { render } from "react-dom"function Counter() { const [count, setCount] = React.useState(0) return ( <div> <h1>{count}</h1> <button onClick={() => setCount(count + 1)}>Increment</button> </div> )}render(<Counter />, document.getElementById("root"))
Add a third party library
Let's add a third party library to our frontend that gives us some nice ui components to work with:
npm i "@mantine/core"
In normal react/jsx code, after importing we can just instantiate a component and pass it some props. With Feliz we usually use a helper function, that is named after the component has one parameter, that is a list of props. This helper function then instantiates the component with Feliz.Interop.reactApi.createElement
and does some transformation on the props so that the component is instantiated correctly:
open Browseropen Felizopen Fable.Core.JsInteropmodule Mantine = let TextInput props = Interop.reactApi.createElement (import "TextInput" "@mantine/core", createObj props)open Mantine[<ReactComponent>]let Counter () = Html.div [ TextInput [ "label" ==> "First Name" "placeholder" ==> "Enter your first name..." ] ]ReactDOM.render (Counter(), document.getElementById "root")
createElement
comes from Feliz, and it is they way to instantiate a React component. It takes a tuple, where the first element is the imported compment and the second element is the props. The props here need to be a plain JavaScript object. In order to create such an object, we can use the createOb
from Fable, which takes a sequence of key value pairs, where the key is the prop name, and the value is the value to be passed to the prop. The ==>
operator creates a tuple from its two arguments. So this code
TextInput [ "label" ==> "First Name" "placeholder" ==> "Enter your first name..." ]
is the same as
TextInput [ ("label", "First Name"); ("placeholder", "Enter your first name...") ]
The createObj
inside our helper function then constructs a JavaScript object from these tuples:
{ label: "First Name", placeholder: "Enter your first name..." }
which is passed to the component, the same way as it would be done in normal React (not in JSX, but the resulting plain JavaScript).
This code
open Browseropen Felizopen Fable.Core.JsInteropmodule Mantine = let TextInput props = Interop.reactApi.createElement (import "TextInput" "@mantine/core", createObj props)open Mantine[<ReactComponent>]let Counter () = Html.div [ TextInput [ "label" ==> "First Name" "placeholder" ==> "Enter your first name..." ] ]ReactDOM.render (Counter(), document.getElementById "root")
will be compiled by Fable into
It is not your typical react code that we would write ourselves, but JSX would also be compiled to something similar.
Other mantine components now could be added in a similar fashion:
open Browseropen Felizopen Fable.Core.JsInteropmodule Mantine = let TextInput props = Interop.reactApi.createElement (import "TextInput" "@mantine/core", createObj props) let Space props = Interop.reactApi.createElement (import "Space" "@mantine/core", createObj props) let AppShell props = Interop.reactApi.createElement (import "AppShell" "@mantine/core", createObj props)//...
Note that we didn't yet type props
parameter. Currently it is a sequence of key value pairs, where the key is of type string
, and the value is obj
(props: seq<string*obj>
). This is not yet type safe, as we could pass any string as key - even if this prop does not exist -, and give it any value - even though only specific types could be allowed.
If we want to only allow setting Label and Placeholder, and define that both values are of type string, we could do the following:
Other props can be added in a similar way. Note that we don't need to use the exact same types our library uses. Instead we can use the F# type system and transfrom these in our helper function so that it matches the expected types of the library.
A simpler way to import components was mentioned by Zaid, the author of Feliz:
open Fableopen Felizopen Fable.Coreopen Browser[<Erase>]type Mantine() = [<ReactComponent(import = "TextInput", from = "@mantine/core")>] static member TextInput(?placeholder: string, ?label: string) = React.imported ()[<ReactComponent>]let Counter () = Html.div [ Mantine.TextInput(label = "First Name", placeholder = "Enter your first name...") ]ReactDOM.render (Counter(), document.getElementById "root")
This looks better. Only if we want to use custom prop types that not exactly match the components props, I would use the earlier mentioned approach, where we have the chance to change the props in our wrapper function.
In order to speed up the process of typing props, we can use the ts2fable
tool, which can generate F# code from TypeScript definitions. The generated code will not be perfect, at least with the mantine package the generated code was pretty broken, but it will give a good starting point.
As shown in this post, Fable allows to generate TypeScript code. So it is also possible to create a hybrid solution, where we use Fable and "native" TypeScript/React or JavaScript side by side. In the next post we will explore this option.