There are several ways to interact with the JavaScript world:
- Dynamically
- Using
Emit
attribute
- Through a foreign interface
- Special attributes
Fable.Core implements the F# dynamic operators so
you can easily access an object property by name (without static check) as follows:
1:
2:
3:
4:
5:
|
open Fable.Core
printfn "Value: %O" jsObject?myProperty
jsObject?myProperty <- 5 // Assignment is also possible
|
However, the F# compiler won't let you apply the property directly to other expressions.
For that, use the $
operator and pass the arguments as a tuple.
1:
2:
3:
|
open Fable.Core
let result = jsObject?myMethod $ (1, 2)
|
If you want to call the function with the new
keyword, use Fable.Core.createNew
instead.
1:
2:
3:
|
open Fable.Core
let instance = createNew jsObject?method (1, 2)
|
And when you need to create JS object literals, use createObj
:
1:
2:
3:
4:
5:
6:
7:
8:
9:
|
open Fable.Core
let data =
createObj [
"todos" ==> Storage.fetch()
"newTodo" ==> ""
"editedTodo" ==> None
"visibility" ==> "all"
]
|
The todomvc sample is a good example
on how to program dynamically with Fable.
You can use the Emit
attribute to decorate a function. Every call to the
function will then be replaced inline by the content of the attribute
with the placeholders $0
, $1
, $2
... replaced by the arguments. For example,
the following code will generate JavaScript as seen below.
1:
2:
3:
4:
5:
6:
|
open Fable.Core
[<Emit("$0 + $1")>]
let add (x: int) (y: string): float = failwith "JS only"
let result = add 1 "2"
|
When you don't know the exact number of arguments you can use the following syntax:
1:
2:
3:
|
type Test() =
[<Emit("$0($1...)")>]
member __.Invoke([<ParamArray>] args: int[]): obj = failwith "JS only"
|
It's also possible to pass syntax conditioned to optional parameters.
1:
2:
3:
|
type Test() =
[<Emit("$0[$1]{{=$2}}")>]
member __.Item with get(): float = failwith "JS only" and set(v: float): unit = failwith "JS only"
|
The content of Emit
will actually be parsed by Babel so it will still be
validated somehow. However, it's not advised to abuse this method, as the
code in the template will remain obscure to Fable and may prevent some
optimizations.
Defining a foreign interface is trivial: just create a F# interface and the
compiler will call its properties or methods by name. The tricky part is to
tell the compiler where the objects should be retrieved from. Normally, they
will be exposed as values of an imported module, so you just need to indicate
the compiler where this module is coming from using the Import
attribute (see below).
For example, if you want to use string_decoder
from node, just write:
1:
2:
3:
4:
5:
6:
7:
|
[<Import("*","string_decoder")>]
module string_decoder =
type NodeStringDecoder =
abstract write: buffer: Buffer -> strings
abstract detectIncompleteChar: buffer: Buffer -> float
let StringDecoder: NodeStringDecoder = failwith "JS only"
|
If the module or value is globally accessible in JavaScript,
you can use the Global
attribute without parameters instead.
If a method accepts a lambda make sure to use System.Func
in the signature to force
the compiler uncurry any lambda passed as parameter.
A good starting point for foreign interfaces are Typescript definition files
and there's a script to make the bulk work of translating the file into F#. You can install it from npm.
See the README for more information.
1:
|
npm install -g ts2fable
|
You can find common definitions already parsed here. Some of them are available
in npm, just search for fable-import
packages.
The Import
attribute can be applied to modules, types and even functions.
It will translate to ES2015 import statements,
which can be later transformed to commonjs
, amd
or umd
imports by Babel.
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
|
// Namespace imports
[<Import("*", from="my-module")>] // F#
import * from "my-module" // JS
// Member imports
[<Import("myFunction", from="my-module")>] // F#
import { myFunction } from "my-module" // JS
// Default imports
[<Import("default", from="express")>] // F#
import express from express // JS
|
Besides Emit
, Import
and Global
attributes, there are some attributes available
in the Fable.Core
namespace to ease the interaction with JS in some particular cases.
In TypeScript there's a concept of Union Types
which differs from union types in F#. The former are just used to statically check a function argument
accepting different types. In Fable, they're translated as Erased Union Types
whose cases must have one and only one single data field. After compilation, the wrapping
will be erased and only the data field will remain. To define an erased union type, just attach
the Erase
attribute to the type. Example:
1:
2:
3:
4:
5:
6:
7:
8:
|
open Fable.Core
[<Erase>]
type MyErasedType =
| String of string
| Number of int
myLib.myMethod(String "test")
|
1:
|
myLib.myMethod("test")
|
Fable.Core
already includes predefined erased types which can be used as follows:
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
|
open Fable.Core
type Test() =
member x.Value = "Test"
let myMethod (arg: U3<string, int, Test>) =
match arg with
| U3.Case1 s -> s
| U3.Case2 i -> string i
| U3.Case3 t -> t.Value
|
Similarly, in TypeScript it's possible to define String Literal Types
which are similar to enumerations with an underlying string value.
Fable allows the same feature by using union types and the StringEnum
attribute.
These union types must not have any data fields as they will be compiled
to a string matching the name of the union case.
By default, the compiled string will have the first letter lowered.
If you want to prevent this or use a different text than the union
case name, use the CompiledName
attribute:
1:
2:
3:
4:
5:
6:
7:
8:
|
open Fable.Core
[<StringEnum>]
type MyStrings =
| Vertical
| [<CompiledName("Horizontal")>] Horizontal
myLib.myMethod(Vertical, Horizontal)
|
1:
|
myLib.myMethod("vertical", "Horizontal")
|
Many JS libraries accept a plain object to specify different options.
With Fable, you can use union types to define these options in a more
static-safe and F#-idiomatic manner. The union cases of a type with the
KeyValueList
attribute act as a key value pair, so they should have a
single data field. (If there's no data field the value is assumed to be true
.)
When Fable encounters a list of such an union type, it will compile it as
a plain JS object.
As with StringEnum
the first letter of the key (the union case name)
will be lowered. Again, you can modify this behaviour with the CompiledName
attribute.
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
|
open Fable.Core
[<KeyValueList>]
type MyOptions =
| Flag1
| Name of string
| [<CompiledName("QTY")>] QTY of int
myLib.myMethod [
Name "Fable"
QTY 5
Flag1
]
|
1:
2:
3:
4:
5:
|
myLib.myMethod({
name: "Fable",
QTY: 5,
flag1: true
})
|
If necessary you can cheat the compiler using tuples:
1:
|
myLib.myMethod [Name "Fable"; unbox("level", 4)]
|
1:
|
myLib.myMethod({ name: "Fable", level: 4 })
|
As these lists will be compiled as JS objects, please note you
cannot apply the usual list operations to them (e.g. appending).
If you want to manipulate the "fake" lists you must implement the
methods yourself. For example:
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
|
[<KeyValueList>]
type CSSProp =
| Border of string
| Display of string
[<Emit("Object.assign({}, $0, $1)")>]
let ( ++ ) (a:'a list) (b:'a list) : 'a list = failwith "JS Only"
let niceBorder = [ Border "1px solid blue" ]
let style = [ Display "inline-block" ] ++ niceBorder
|
namespace Microsoft.FSharp.Core
val printfn : format:Printf.TextWriterFormat<'T> -> 'T
Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.printfn
val result : obj
Full name: interacting.result
val instance : 'a
Full name: interacting.instance
val data : 'a
Full name: interacting.data
union case Option.None: Option<'T>
val add : x:int -> y:string -> float
Full name: interacting.add
val x : int
Multiple items
val int : value:'T -> int (requires member op_Explicit)
Full name: Microsoft.FSharp.Core.Operators.int
--------------------
type int = int32
Full name: Microsoft.FSharp.Core.int
--------------------
type int<'Measure> = int
Full name: Microsoft.FSharp.Core.int<_>
val y : string
Multiple items
val string : value:'T -> string
Full name: Microsoft.FSharp.Core.Operators.string
--------------------
type string = System.String
Full name: Microsoft.FSharp.Core.string
Multiple items
val float : value:'T -> float (requires member op_Explicit)
Full name: Microsoft.FSharp.Core.Operators.float
--------------------
type float = System.Double
Full name: Microsoft.FSharp.Core.float
--------------------
type float<'Measure> = float
Full name: Microsoft.FSharp.Core.float<_>
val failwith : message:string -> 'T
Full name: Microsoft.FSharp.Core.Operators.failwith
val result : float
Full name: interacting.result
Multiple items
type Test =
new : unit -> Test
member Invoke : args:int [] -> obj
Full name: interacting.Test
--------------------
new : unit -> Test
member Test.Invoke : args:int [] -> obj
Full name: interacting.Test.Invoke
val args : int []
type obj = System.Object
Full name: Microsoft.FSharp.Core.obj
val __ : Test
val set : elements:seq<'T> -> Set<'T> (requires comparison)
Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.set
type unit = Unit
Full name: Microsoft.FSharp.Core.unit
type NodeStringDecoder =
interface
abstract member detectIncompleteChar : buffer:'a0 -> float
abstract member write : buffer:'a0 -> 'a1
end
Full name: interacting.string_decoder.NodeStringDecoder
abstract member NodeStringDecoder.write : buffer:'a0 -> 'a1
Full name: interacting.string_decoder.NodeStringDecoder.write
abstract member NodeStringDecoder.detectIncompleteChar : buffer:'a0 -> float
Full name: interacting.string_decoder.NodeStringDecoder.detectIncompleteChar
val StringDecoder : NodeStringDecoder
Full name: interacting.string_decoder.StringDecoder
type MyErasedType =
| String of string
| Number of int
Full name: interacting.MyErasedType
Multiple items
union case MyErasedType.String: string -> MyErasedType
--------------------
module String
from Microsoft.FSharp.Core
union case MyErasedType.Number: int -> MyErasedType
module String
from Microsoft.FSharp.Core
val myMethod : arg:'a -> 'b
Full name: interacting.myMethod
val arg : 'a
type MyStrings =
| Vertical
| Horizontal
Full name: interacting.MyStrings
union case MyStrings.Vertical: MyStrings
Multiple items
type CompiledNameAttribute =
inherit Attribute
new : compiledName:string -> CompiledNameAttribute
member CompiledName : string
Full name: Microsoft.FSharp.Core.CompiledNameAttribute
--------------------
new : compiledName:string -> CompiledNameAttribute
union case MyStrings.Horizontal: MyStrings
type MyOptions =
| Flag1
| Name of string
| QTY of int
Full name: interacting.MyOptions
union case MyOptions.Flag1: MyOptions
union case MyOptions.Name: string -> MyOptions
union case MyOptions.QTY: int -> MyOptions
val unbox : value:obj -> 'T
Full name: Microsoft.FSharp.Core.Operators.unbox
type 'T list = List<'T>
Full name: Microsoft.FSharp.Collections.list<_>