This repository was originally born as bindings to write Svelte apps with Fable, but has evolved into providing stores à-la-Svelte that can also be used with other UI rendering mechanisms like React, and also in non-Fable environments like dotnet console apps, Fabulous or Bolero.
A store is just an observable that keeps an internal value and exposes an Update
function to change it and report to subscribers. Because it's just an observable, it's compatible with the Observable
module in FSharp.Core and other Reactive libraries. But the stores have an additional interesting property: they report immediately their current value upon subscription, which makes them directly usable by UI components.
You may be thinking: "An update function? This looks like mutation, not very functional". You're right, this is why Fable.Store provides helpers to build stores after common functional patterns, like Elmish MVU, FRP with Fable.Reaction or async workflows to represent state matchines (based on an original idea by Tomas Petricek).
Svelte is a new way to write web apps in a declarative way without using a Virtual DOM. The best way to learn about Svelte and its merit is by checking their great website with interactive tutorials.
Svelte uses HTML-like templates. If you like it but prefer an all-F# approach, check David Dawkins awesome work with Sutil!
Svelte has its own mechanism to create bindings, but it allows you to use external stores with a simple API for interaction, so you probably already guessed where we are heading to.
First, if you are using VS Code, install the "Svelte for VS Code" extension which provides great tooling to work with .svelte files.
In your F# project, install the Fable.SvelteStore
dependency (--prerelease
at the time of writing). Then from npm also install svelte
itself and the svelte-loader
for Webpack. All the dependencies (both .NET and JS) can be installed automatically by using Femto.
In a Svelte-only app, you'll likely have a main .js file like this one pointing to the root Svelte component as the entry point in your Webpack config.
Svelte uses files with the .svelte
(surprise!) extension for component declaration. From these you can just import code from the JS files generated by Fable. We've found that an effective way is to export a function to make the logic store from F# and then import and call it from Svelte.
// TodoMVC.fs
let makeStore props =
let store, dispatch = SvelteStore.makeElmish init update ignore props
store, SvelteStore.makeDispatcher dispatch
// TodoMVC.svelte
<script>
import { makeStore } from "./TodoMVC.fs.js";
const [store, dispatch] = makeStore(props);
// ...
</script>
Note Svelte uses a special syntax (
$
prefix) to access the value from the store. Check their tutorial and the .svelte files insamples/App/src
.
Ok, so you can get the model from your F# logic to render the UI. But in most cases you will also want to listen to UI events and send messages to the logic. In Fable apps it's common to model the messages with a union type, but we cannot instantiate F# unions from JS (at least not easily). Using something like string literals plus an object array for the arguments is not ideal either. Fable.SvelteStore provides a makeDispatcher
helper to create a JS object at compile-time (through a Fable plugin) from a dispatch: Msg -> unit
function. Let's see it with an example, if we have the following F# code:
type Msg =
| MouseDown of x:float * y:float * offsetX:float * offsetY:float
| MouseMove of x:float * y:float
| MouseUp
// ...
let dispatch (msg: Msg) = stream.Trigger(msg, store.update)
let dispatcher = SvelteStore.makeDispatcher dispatch
The following JS code will be generated:
export dispatcher = {
mouseDown: (x, y, offsetX, offsetY) => dispatch(new Msg(0, x, y, offsetX, offsetY)),
mouseMove: (x, y) => dispatch(new Msg(1, x, y)),
mouseUp: () => dispatch(new Msg(2)),
};
Now we can easily send messages to F# from Svelte:
<svelte:body
on:mousemove={(ev) => dispatch.mouseMove(ev.x, ev.y)}
on:mouseup={(_) => dispatch.mouseUp()} />
The dispatcher is still not super useful unless we have some help from the IDE to give us a warning when we make a misspelling in JS. To solve this we can use a Typescript .d.ts
declaration file like this one:
// Dragging.fs.d.ts
import { Readable } from "svelte/store";
export function makeStore(): [Readable<{
position: [number, number],
offset: [number, number],
}>, {
mouseDown: (x: number, y: number, offsetX: number, offsetY: number) => void,
mouseMove: (x: number, y: number) => void,
mouseUp: () => void,
}]
You can generate the
.d.ts
automatically by decorating themakeStore
function with theSveltePlugins.GenerateDeclaration
attribute. Just be aware the plugin may not handle complex cases.
With this, you can use Typescript in your .svelte file or just the // @ts-check
declaration to get type checking even in Javascript!
// Dragging.svelte
// @ts-check
import { makeStore } from "./Dragging.fs.js";
let [store, dispatch] = makeStore();
// This gives an error because numeric arguments are expected
dispatch.mouseDown("foo")
Run the samples to see how all these pieces work together:
cd samples/App
npm install && npm start
The store allows to you link your logic directly to individual UI components instead of imposing a rigid structure for your whole app. If you're using the MVU pattern this is very similar to useElmish in Feliz apps, but the store provides you with more flexibility to integrate different patterns as well as to communicate with multiple UI components. Anyways, this belongs to a future, more detailed post. For now you can have a look at this React example.
No! Thanks to Zaid Ajaj you can easily integrate Svelte components into your Feliz/React apps using the SvelteComponent plugin. Check how it's done in samples/FelizSvelte
!