WARNING: this repository is NOT maintained any longer. Various automated pull request regarding security issues are ignored.
Writing Fable React Bindings together
This repo wants to be a collaborative workshop where we write Fable react Bindings together.
It provides a minimal Fable app showcasing a selection of (react) components with their fable bindings. Its purpose is to test and develop bindings for existing react-components that don't have F# type bindings available yet.
Here you will find:
- how to write fable react bindings
- fable react binding for
- CkEditor 4
- React Hamburgers
- more to come ...
Credit where credit is due: A lot of the information found here is taken from fable2-samples and Using third party React components This repo is based on fable2-samples/minimal.
- Before you get started: Requirements
- Getting started: Building and running the app
- Project structure
- Adding a new binding
- dotnet SDK 2.1 or higher
- node.js with npm
- An F# editor like Visual Studio, Visual Studio Code with Ionide or JetBrains Rider.
- Install JS dependencies:
npm install
- Start Webpack dev server:
npx webpack-dev-server
ornpm start
- After the first compilation is finished, in your browser open: http://localhost:8080/
Any modification you do to the F# code will be reflected in the web page after saving.
JS dependencies are declared in package.json
, while package-lock.json
is a lock file automatically generated.
Webpack is a JS bundler with extensions, like a static dev server that enables hot reloading on code changes. Fable interacts with Webpack through the fable-loader
. Configuration for Webpack is defined in the webpack.config.js
file. Note this sample only includes basic Webpack configuration for development mode, if you want to see a more comprehensive configuration check the Fable webpack-config-template.
The sample only contains two F# files: the project (.fsproj) and a source file (.fs) in the src
folder.
The index.html
file and other assets like an icon can be found in the public
folder.
You'll have to know what React is, how a react component is used and what 'props' are. It's not necessary to know how to write React components, since you won't be writing implementations.
More important is to have some basic knowledge of F#. What a Module is, what Records and Discriminated Unions are and how to call functions.
You should have an idea of what Fable and Fable.React are and be familiar with the basic syntax of calling React components in Fable.
Using npm, install the React component you want to provide type bindings for.
For example, if you want to provide a type binding for CKEditor4-react you'll have to run this command inside the project's root folder:
npm install ckeditor4-react
Open the project in vs code with the ionide plugin activated. Create a new .fs file in the folder fable-react-bindings\src\component-bindings
.
For example, for the CKEditor4-react component we'll create a new file fable-react-bindings\src\component-bindings\CKEditor4.fs
. In this file we'll create all the necessary type bindings and the create function to make the component available for use.
A React component written in Javascript can be exported in multiple ways: default import
, member import
and namespace import
. We'll assume here that the component was exported with the default
keyword. If one of the other export methods was used, see Using third party React components section 3 for more info.
In javascript an import statement of a default export would look like this:
import CKEditor4 from 'ckeditor4-react';
Making the component available for F#/Fable consumption would look like this:
let inline CKEditor (props: IHTMLProp seq) : ReactElement =
ofImport "default" "ckeditor4-react" (keyValueList CaseRules.LowerFirst props) []
Note that CKEditor4Props
won't compile since we are yet to define the props.
To define the props we'll have to look up the react component's documentation. Definitely don't forget to take the documentation's url and paste it in a comment block on top of the binding's file.
Props are defined by providing a Discriminated Union of all the options. Each case in the Discriminated Union can reference a primitive (like string or int), a Record or another Discriminated Union.
For example:
type CKEditorProps =
| Data of string
| EditorUrl of string
| Type of EditorType
| ReadOnly of bool
Where EditorType
is given by:
type EditorType =
| Inline
| Classic
If one of the props is treated as a string enum in Javascript then the [<StringEnum>]
(docs) attribute can be useful for defining helper types.
For example:
[<StringEnum>]
type EditorType =
| Inline
| Classic
Try to make illegal states invalid. You'll need to have some knowledge about modeling with F# to get the optimal solutions sometimes, especially for more complex components.
If you don't know how to represent something you can always represent an option by obj
and change it later.
For example:
type CKEditorProps =
| Data of string
| EditorUrl of string
| Type of EditorType
| ReadOnly of bool
| OnChange of (obj -> unit) // CKEditor passes a huge event with all sorts of data and methods // TODO provide a type definition for the event?
See also: Using third party React components
You can enable your component to accept any html prop by tagging your props with the IHTMLProp
interface.
For example:
type CKEditorProps =
| Data of string
| EditorUrl of string
| Type of EditorType
| ReadOnly of bool
| OnChange of (obj -> unit) // CKEditor passes a huge event with all sorts of data and methods // TODO provide a type definition for the event?
interface IHTMLProp
When one of the props is another list of props, you might need to use a static member
on the Discriminated Union, using unbox
and keyValueList
.
For example:
type CKEditorProps =
| Data of string
| EditorUrl of string
| Type of EditorType
| ReadOnly of bool
| OnChange of (obj -> unit) // CKEditor passes a huge event with all sorts of data and methods // TODO provide a type definition for the event?
static member Config (editorConfig: EditorConfig list) : CKEditorProps = unbox ("config", keyValueList CaseRules.LowerFirst editorConfig)
interface IHTMLProp
By using a static member
we can enforce arbitrary restrictions.
For example, the CKEditor4 has an option ForcePasteAsPlainText
which accepts either a boolean
value (true/false) or a string
which must contain the value "allow-word". Instead of accepting any string
, boolean
or obj
we can restrict the options to only the allowable states. This we way know at compile-time that we past a correct value:
static member ForcePasteAsPlainText (option: ForcePasteAsPlainTextOption) =
match option with
| PlainText -> unbox ("forcePasteAsPlainText", true)
| PreserveFormatting -> unbox ("forcePasteAsPlainText", false)
| AllowWord -> unbox ("forcePasteAsPlainText", "allow-word")
where ForcePastAsPlainTextOption is given by:
type ForcePasteAsPlainTextOption =
| PlainText
| PreserveFormatting
| AllowWord
To showcase and test a component at runtime it has to be added to ComponentsPage.fs
. The page
element consists of a div containing all the showcased components. Adding a line there referencing the component's create function will make the component show up in the list.
For example, if the div looks like this:
let page =
div [] [
Component1.ComponentCreate [] |> displayComponent "First Component"
Component2.ComponentCreate [] |> displayComponent "Second Component"
]
We can update it to this:
let page =
div [] [
Component1.ComponentCreate [] |> displayComponent "First Component"
Component2.ComponentCreate [] |> displayComponent "Second Component"
CKEditor4.CKEditor [] |> displayComponent "CKEditor 4"
]