• This repository has been archived on 01/Jan/2023
  • Stars
    star
    199
  • Rank 196,105 (Top 4 %)
  • Language
    F#
  • Created about 7 years ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Minimalistic real-worldish blogging platform, written entirely in F#, made as a learning reference for building large Elmish apps

Tabula Rasa Build Status

A minimalistic real-worldish blog engine written entirely in F#. Specifically made as a learning resource when building apps with the SAFE stack. This application features many concerns of large apps such as:

  • Using third-party react libraries via interop
  • Deep nested views
  • Deep nested routing
  • Message interception as means for component communication
  • Logging
  • Database access
  • User security: authentication and authorization
  • Type-safe RPC communication
  • Realtime type-safe messaging via web sockets

Screen recordings

first.gif

second.gif

bridge.gif

The server uses the following tech

The client uses the following tech

Communication Protocol

To understand how the application works and what it does, you simply take a look the protocol between the client and server:

type IBlogApi = {  
    getBlogInfo : unit -> Async<Result<BlogInfo, string>>
    login : LoginInfo -> Async<LoginResult>
    getPosts : unit -> Async<list<BlogPostItem>>
    getPostBySlug : string -> Async<Option<BlogPostItem>>
    getDrafts : AuthToken -> SecureResponse<list<BlogPostItem>>
    publishNewPost : SecureRequest<NewBlogPostReq> -> SecureResponse<AddPostResult> 
    savePostAsDraft : SecureRequest<NewBlogPostReq> -> SecureResponse<AddPostResult>
    deleteDraftById : SecureRequest<int> -> SecureResponse<DeleteDraftResult>
    publishDraft : SecureRequest<int> -> SecureResponse<PublishDraftResult>
    deletePublishedArticleById : SecureRequest<int> -> SecureResponse<DeletePostResult>
    turnArticleToDraft: SecureRequest<int> -> SecureResponse<MakeDraftResult>
    getPostById : SecureRequest<int> -> SecureResponse<Option<BlogPostItem>>
    savePostChanges : SecureRequest<BlogPostItem> -> SecureResponse<Result<bool, string>>
    updateBlogInfo : SecureRequest<BlogInfo> -> SecureResponse<Result<SuccessMsg, ErrorMsg>>
    togglePostFeatured : SecureRequest<int> -> SecureResponse<Result<string, string>>
    updatePassword : SecureRequest<UpdatePasswordInfo> -> SecureResponse<Result<string, string>> 
}

Thanks to Fable.Remoting, this application does not need to handle data serialization/deserialization and routing between client and server, it is all done for us which means that the code is 99% domain models and domain logic.

You will often see calls made to server from the client like these:

| ToggleFeatured postId ->
    let nextState = { state with IsTogglingFeatured = Some postId }
    let request = { Token = authToken; Body = postId }
    let toggleFeatureCmd = 
        Cmd.fromAsync {
            Value = Server.api.togglePostFeatured request
            Error = fun ex -> ToggleFeaturedFinished (Error "Network error while toggling post featured")
            Success = function 
                | Error authError -> ToggleFeaturedFinished (Error "User was unauthorized")
                | Ok toggleResult -> ToggleFeaturedFinished toggleResult
        } 
    nextState, toggleFeatureCmd

Client Application Layout

The client application layout is how the components are structured in the project. The components are written in a consistent pattern that is reflected by the file system as follows:

ParentComponent 
   | 
   | - Types.fs
   | - State.fs
   | - View.fs
   | - ChildComponent
        | 
        | - Types.fs
        | - State.fs
        | - View.fs

Where the client is a tree of UI components:

App 
 |
 | - About
 | - Posts 
      | 
      | - SinglePost
      | - AllPosts 
 |
 | - Admin
      | 
      | - Login
      | - Backoffice
           | 
           | - PublishedArticles
           | - Drafts 
           | - Settings 
           | - NewArticle
           | - EditArticle 

Component Types

Every component comes with a Types.fs file that contains mostly three things

  • State data model that the component keeps track of
  • Msg type that represents the events that can occur
  • Pages represents the current page and sub pages that a component can have

The State keeps track of the CurrentPage but it will never update it by hand: the CurrentPage is only updated in response to url changes and these changes will dispatch a message to change the value of the CurrentPage along with dispatching other messages related to loading the data for the component

Important Concepts: Data Locality and Message Interception

Following these principles to help us write components in isolation:

  • Child components don't know anything about their parents
  • Child components don't know anything about their siblings
  • Parent components manage child state and communication between children

The best example of these concepts is the interaction between the following components:

        Admin
          |
   ---------------
   |             |
Backoffice     Login     

Message Interception by example

Definition: Message interception is having control over how messages flow in your application, allowing for communication between components that don't know each other even exist.

Login doesn't know anything going on in the application as a whole, it just has a form for the user to input his credentials and try to login to the server to obtain an authorization token. When the token is obtained, a LoginSuccess token message is dispatched. However, this very message is intercepted by Admin (the parent of Login), updating the state of Admin:

// Admin/State.fs

let update msg (state: State) =
    match msg with
    | LoginMsg loginMsg ->
        match loginMsg with 
        // intercept the LoginSuccess message dispatched by the child component
        | Login.Types.Msg.LoginSuccess token ->
            let nextState = 
                { state with Login = state.Login
                             SecurityToken = Some token }
            nextState, Urls.navigate [ Urls.admin ] 
        // propagate other messages to child component
        | _ -> 
            let nextLoginState, nextLoginCmd = Admin.Login.State.update loginMsg state.Login
            let nextAdminState = { state with Login = nextLoginState }
            nextAdminState, Cmd.map LoginMsg nextLoginCmd

After updating the state of Admin to include the security token obtained from Login, the application navigates to the admin pages using Urls.navigate [ Urls.admin ]. Now the navigation will succeed, because navigating to the admin is allowed only if the admin has a security token defined:

// App/State.fs -> inside handleUpdatedUrl

| Admin.Types.Page.Backoffice backofficePage ->
    match state.Admin.SecurityToken with
    | None -> 
        // navigating to one of the admins backoffice pages 
        // without a security token? then you need to login first
        Cmd.batch [ Urls.navigate [ Urls.login ]
                    showInfo "You must be logged in first" ] 
    | Some userSecurityToken ->
        // then user is already logged in 
        // for each specific page, dispatch the appropriate message 
        // for initial loading of that data of that page
        match backofficePage with 
        | Admin.Backoffice.Types.Page.Drafts -> 
            Admin.Backoffice.Drafts.Types.LoadDrafts
            |> Admin.Backoffice.Types.Msg.DraftsMsg
            |> Admin.Types.Msg.BackofficeMsg 
            |> AdminMsg 
            |> Cmd.ofMsg
        
        | Admin.Backoffice.Types.Page.PublishedPosts -> 
            Admin.Backoffice.PublishedPosts.Types.LoadPublishedPosts
            |> Admin.Backoffice.Types.Msg.PublishedPostsMsg
            |> Admin.Types.Msg.BackofficeMsg
            |> AdminMsg
            |> Cmd.ofMsg 

        | Admin.Backoffice.Types.Page.Settings ->
            Admin.Backoffice.Settings.Types.Msg.LoadBlogInfo
            |> Admin.Backoffice.Types.Msg.SettingsMsg
            |> Admin.Types.Msg.BackofficeMsg
            |> AdminMsg
            |> Cmd.ofMsg 
         
        | Admin.Backoffice.Types.Page.EditArticle postId ->
            Admin.Backoffice.EditArticle.Types.Msg.LoadArticleToEdit postId 
            |> Admin.Backoffice.Types.Msg.EditArticleMsg 
            |> Admin.Types.Msg.BackofficeMsg
            |> AdminMsg 
            |> Cmd.ofMsg 
        
        | otherPage -> 
            Cmd.none

Another concrete example in this application: when you update the settings, the root component intercepts the "Changed settings" message and reloads it's blog information with the new settings accordingly

Data Locality by example

Definition: Data Locality is having control over the data that is available to certain components, without access to global state.

Fact: Components of Backoffice need to make secure requests, hence they need a security token available whenever a request is to be made.

Requirement: Once the user is inside a component of Backoffice, there will always be a SecurityToken available to that component. This is because I don't want to check whether there is a security token or not everytime I want to make a web request, because if there isn't one, there is an internal inconsistency: the user shouldn't have been able to reach the Backoffice component in the first place.

Problem: The security token is only acquired after the user logs in from Login, but before that there isn't a security token, hence the type of the token will be SecurityToken: string option but we don't want an optional token, we want an actual token once we are logged in.

Solution: Login and components of Backoffice cannot be siblings, Login is happy with the security token being optional, while Backoffice insists on having a token at any given time. So we introduce a parent: Admin that handles the optionalness of the security token! The Admin will disallow the user from reaching Backoffice if there isn't a security token, and if there is one, it will be propagated to the backoffice:

// Admin/State.fs -> update
| BackofficeMsg msg ->
    match msg with 
    | Backoffice.Types.Msg.Logout -> 
        // intercept logout message of the backoffice child
        let nextState, _ = init()
        nextState, Urls.navigate [ Urls.posts ]
    | _ -> 
        match state.SecurityToken with 
        | Some token -> 
            let prevBackofficeState = state.Backoffice
            let nextBackofficeState, nextBackofficeCmd = 
                // pass security token down to backoffice
                Backoffice.State.update token msg prevBackofficeState
            let nextAdminState = { state with Backoffice = nextBackofficeState }
            nextAdminState, Cmd.map BackofficeMsg nextBackofficeCmd
        | None ->
            state, Cmd.none

Unit-testable at the composition root level:

The composition root is where the application functionality gets all the dependencies it needs to run to application like the database and a logger. In this application, the composition root is where we construct an implementation for the IBlogApi protocol:

let liftAsync x = async { return x }

/// Composition root of the application
let createBlogApi (logger: ILogger) (database: LiteDatabase) : IBlogApi = 
     // create initial admin guest admin if one does not exists
    Admin.writeAdminIfDoesNotExists database Admin.guestAdmin 
    let getBlogInfo() = async { return Admin.blogInfo database }
    let getPosts() = async { return BlogPosts.getPublishedArticles database } 
    let blogApi : IBlogApi = {   
        getBlogInfo = getBlogInfo
        getPosts = getPosts 
        login = Admin.login logger database >> liftAsync
        publishNewPost = BlogPosts.publishNewPost logger database
        getPostBySlug =  BlogPosts.getPostBySlug database >> liftAsync
        savePostAsDraft = BlogPosts.saveAsDraft logger database 
        getDrafts = BlogPosts.getAllDrafts database
        deleteDraftById = BlogPosts.deleteDraft logger database 
        publishDraft = BlogPosts.publishDraft database
        deletePublishedArticleById = BlogPosts.deletePublishedArticle database 
        turnArticleToDraft = BlogPosts.turnArticleToDraft database
        getPostById = BlogPosts.getPostById database 
        savePostChanges = BlogPosts.savePostChanges database
        updateBlogInfo = Admin.updateBlogInfo database
        togglePostFeatured = BlogPosts.togglePostFeatured database 
        updatePassword = Admin.updatePassword logger database
    }

    blogApi

Because LiteDB already includes an in-memory database and Serilog provides a simple no-op logger, you can write unit tests right off the bat at the application level:

// creates a disposable in memory database
let useDatabase (f: LiteDatabase -> unit) = 
    let mapper = FSharpBsonMapper()
    use memoryStream = new MemoryStream()
    use db = new LiteDatabase(memoryStream, mapper)
    f db

testCase "Login with default credentials works" <| fun _ -> 
    useDatabase <| fun db -> 
        let logger = Serilog.Log.Logger 
        let testBlogApi = WebApp.createBlogApi logger db 
        let loginInfo = { Username = "guest"; Password = "guest" }
        let result = Async.RunSynchronously (testBlogApi.login loginInfo)
        match result with 
        | LoginResult.Success token -> pass() 
        | _ -> fail()

Of course you can also test the individual functions seperately because every function is also unit testable as long as you provide a database instance and a logger.

Responsive using different UI's

As opposed to using CSS to show or hide elements based on screen size, I used react-responsive to make a completely different app for small-sized screens, implemented as

let app blogInfo state dispatch =
  div 
   [ ]
   [ mediaQuery 
      [ MinWidth 601 ]
      [ desktopApp blogInfo state dispatch ]
     mediaQuery 
      [ MaxWidth 600 ] 
      [ mobileApp blogInfo state dispatch ] ]

Security with JWT

User authentication and authorization happen though secure requests, these requests include the JSON web token to authorize the user. The user acquires these JWT's when logging in and everything is stateless. An example of a secure request with it's handler on the server:

// Client

| ToggleFeatured postId ->
    let nextState = { state with IsTogglingFeatured = Some postId }
    let request = { Token = authToken; Body = postId }
    let toggleFeatureCmd = 
        Cmd.fromAsync {
            Value = Server.api.togglePostFeatured request
            Error = fun ex -> ToggleFeaturedFinished (Error "Network error while toggling post featured")
            Success = function 
                | Error authError -> ToggleFeaturedFinished (Error "User was unauthorized")
                | Ok toggleResult -> ToggleFeaturedFinished toggleResult
        } 
    nextState, toggleFeatureCmd

And it is handled like this on the server:

// Server

let togglePostFeatured (db: LiteDatabase) = 
    Security.authorizeAdmin <| fun postId admin -> 
        let posts = db.GetCollection<BlogPost> "posts"
        match posts.tryFindOne <@ fun post -> post.Id = postId @> with 
        | None -> Error "Blog post could not be found"
        | Some post -> 
            let modifiedPost = { post with IsFeatured = not post.IsFeatured }
            if posts.Update modifiedPost 
            then Ok "Post was successfully updated" 
            else Error "Error occured while updating the blog post"

See Modeling Authentication and Authorization in Fable.Remoting to learn more

Try out on your machine

Requirements:

Start watch build on windows:

git clone https://github.com/Zaid-Ajaj/tabula-rasa.git 
cd tabula-rasa
build.cmd Watch 

On linux/mac you can use bash

git clone https://github.com/Zaid-Ajaj/tabula-rasa.git 
cd tabula-rasa
./build.sh Watch

This will start the build and create the LiteDb (single file) database for the first time if it does not already exist. The database will be in the application data directory of your OS under the tabula-rasa directory with name TabulaRasa.db along with the newly generated secret key used for generating secure Json web tokens. The "application data directory" on most linux systems will be ~/.config/, resulting in a directory ~/.config/tabula-rasa/.

When the build finishes, you can navigate to http://localhost:8090 to start using the application. Once you make changes to either server or client, it will automatically re-compile the app.

Once the application starts, the home page will tell you "There aren't any stories published yet" because the database is still empty. You can then navigate to http://localhost:8090/#login to login in as an admin who can write stories. The default credentials are Username = guest and Password = guest.

More

There is a lot to talk about with this application, but the best way to learn from it is by actually trying it out and going through the code yourself. If you need clarification or explanation on why a code snippet is written the way it is, just open an issue with your question :)

More Repositories

1

Feliz

A fresh retake of the React API in Fable and a collection of high-quality components to build React applications in F#, optimized for happiness
F#
533
star
2

the-elmish-book

A practical guide to building modern and reliable web applications in F# from first principles
HTML
338
star
3

Npgsql.FSharp

Thin F# wrapper around Npgsql, the PostgreSQL database driver for .NET
F#
318
star
4

Fable.Remoting

Type-safe communication layer (RPC-style) for F# featuring Fable and .NET Apps
F#
273
star
5

LiteDB.FSharp

Advanced F# Support for LiteDB, an embedded NoSql database for .NET with type-safe query expression through F# quotations
F#
179
star
6

Femto

Femto is a CLI tool that automatically resolves npm packages used by Fable bindings
F#
153
star
7

Snowflaqe

A dotnet CLI to generate type-safe GraphQL clients for F# and Fable with automatic deserialization, static query verification and type checking
F#
150
star
8

ThrowawayDb

Dead simple integration tests with SQL Server or Postgres throwaway databases that are created on the fly, used briefly then disposed of automagically.
C#
140
star
9

Npgsql.FSharp.Analyzer

F# analyzer that provides embedded SQL syntax analysis, type-checking for parameters and result sets and nullable column detection when writing queries using Npgsql.FSharp.
C#
132
star
10

Hawaii

dotnet CLI tool to generate type-safe F# and Fable clients from OpenAPI/Swagger or OData services
F#
122
star
11

SAFE.Simplified

A lightweight alternative template of SAFE for happy cross-IDE full-stack F# development
F#
101
star
12

Fable.SimpleHttp

Http with Fable, made simple.
F#
82
star
13

ClosedXML.SimpleSheets

Easily generate Excel sheets from F#
F#
80
star
14

DustyTables

Thin F# API for SqlClient for easy data access to ms sql server with functional seasoning on top
F#
74
star
15

Giraffe.GoodRead

Practical dependency injection in Giraffe that gets out of your way
F#
73
star
16

Feliz.Router

A router component for React and Elmish that is focused, powerful and extremely easy to use.
F#
72
star
17

Fable.SimpleJson

A library for working with JSON in Fable projects
F#
56
star
18

SAFE.React

Full Stack F# powered by ASP.NET Core on the backend and modern React on the frontend.
F#
54
star
19

Fable.Mocha

Fable library for a proper testing story using different runners such as mocha, standalone browsers and dotnet
F#
51
star
20

fabulous-simple-elements

An alternative view rendering API for Fabulous (Elmish Xamarin.Forms) that is easy to use and simple to read, inspired by Elmish on the web.
F#
46
star
21

desktop-feliz-with-photino

F#
43
star
22

fsharp-weekly

F# Weekly mobile, available for Android (soon iOS and UWP too)
F#
42
star
23

SAFE-TodoList

The simplest Todo app showcasing a client-server application written entirely in F#
F#
41
star
24

Giraffe.SerilogExtensions

Dead simple library to integrate Serilog within Giraffe apps: implemented as a composable HttpHandler and has native destructuring of F# types.
F#
30
star
25

dotnetconf-react-with-fsharp

Demo application used in DotnetCONF to build React applications with F#
F#
30
star
26

Elmish.SweetAlert

SweetAlert integration for Fable, made with ❤️ to work in Elmish apps. https://zaid-ajaj.github.io/Elmish.SweetAlert/
F#
29
star
27

Fable.DateFunctions

Fable binding for date-fns javascript library, implemented as extension methods for DateTime. See https://zaid-ajaj.github.io/Fable.DateFunctions/
F#
27
star
28

Fable.CloudFlareWorkers

Write CloudFlare Workers in idiomatic, type-safe F# and compile them to JS using Fable
F#
24
star
29

Elmish.Toastr

Toastr integration with Fable, implemented as Elmish commands https://zaid-ajaj.github.io/Elmish.Toastr/
F#
24
star
30

elmish-getting-started

A simple and minimalistic template to easily get up and running with Elmish and Fable
F#
22
star
31

Giraffe.QueryReader

HttpHandler for easily working with query string parameters within Giraffe apps.
F#
21
star
32

navigation-bar-with-feliz

Modern navigation bar built with Feliz
JavaScript
20
star
33

fable-getting-started

Template for getting started with Fable
JavaScript
19
star
34

Giraffe.JsonTherapy

Simply extract JSON values from HTTP requests without defining intermediate types or using model binding
F#
19
star
35

Feliz.ViewEngine.Htmx

A library that allows using Htmx attributes with Feliz.ViewEngine
F#
18
star
36

Fable.SimpleXml

A library for easily parsing and working with XML in Fable projects
F#
17
star
37

AlgebraFs

A simple computer algebra system (CAS), written in F# for fun and learning.
F#
15
star
38

Fable.React.Flatpickr

Fable binding for react-flatpickr that is ready to use within Elmish applications
F#
14
star
39

elmish-login-flow-validation

Elmish sample that demonstrates a login flow with user input validation
F#
13
star
40

pulumi-schema-explorer

Web application and UI to explore Pulumi schemas
F#
13
star
41

Fable.SqlClient

Fable Node client for Microsoft SQL Server, built around a node-mssql binding
F#
12
star
42

pulumi-csharp-analyzer

Roslyn-based static code analysis for pulumi programs written in C#
C#
11
star
43

Elmish.AnimatedTree

An animated tree user interface made for Elmish applications
JavaScript
11
star
44

elmish-composition

A library to compare the different compositional techniques in Elmish applications
JavaScript
10
star
45

Cable

Type-safe client-server communication for C# featuring Bridge.NET and NancyFx
C#
10
star
46

Nancy.Serilog

Nancy plugin for application-wide logging using Serilog
C#
9
star
47

scaling-elmish-programs

FableConf 2018 slides and apps used in the presentation
F#
8
star
48

Suave.SerilogExtensions

Suave plugin to use the awesome Serilog library as the logger for your application
F#
8
star
49

ElmCounterWPF

Pure F#/Xaml counter using Elmish.WPF and XAML type provider
F#
7
star
50

ReactCSharpDemo

Using React in C# with Bridge
C#
7
star
51

Feliz.AntDesign

AntDesign bindings using Feliz syntax for a clean, discoverable and type-safe React components. (WIP)
F#
7
star
52

Fable.SimpleJson.Python

A library for working with JSON in F# Fable projects targeting Python
F#
7
star
53

hawaii-samples-feliz-petstore

This is a sample application that shows how to use Feliz with a client generated by Hawaii
F#
6
star
54

elmish-wpf-template

A template for easily getting started with building WPF apps using Elmish
F#
5
star
55

elmish-routing

F#
4
star
56

Bridge.Redux

Bindings of the Redux library for the Bridge transpiler
C#
4
star
57

Fable.Requests

Fable library for making HTTP requests targeting Python
F#
4
star
58

elmish-todo-exercises

F#
4
star
59

ExcelPluginTemplate

Template project to create Excel Add-ins with F# and ExcelDNA
F#
3
star
60

elmish-calc

Calculator application with Fable and Fable-Elmish
F#
3
star
61

interop-fableconf2019

JavaScript
3
star
62

FifteenPuzzleWithFeliz

The small fifteen puzzle game I am building live on Twitch
JavaScript
3
star
63

FSharp.SimpleJson

A library for easily parsing, transforming and converting JSON in F#, ported from Fable.SimpleJson
F#
3
star
64

docute-starter

a simple and easy to use project to start writing documentation with Docute
HTML
3
star
65

Bridge.ChartJS

Bindings for Chart.js library to be used in Bridge.NET projects
C#
3
star
66

Bridge.CanvasJS

CanvasJS C#-wrapper for Bridge.NET project.
C#
3
star
67

pulumi-workshop-automation-fsharp

This workshop will walk you through the basics of using the Automation API of Pulumi to create and deploy a Pulumi stack programmatically with F#
F#
3
star
68

MandelbrotHaskell

Mandelbrot fractal as text (ASCII), written in Haskell
Haskell
2
star
69

hawaii-samples-petstore

PetStore API sample with Hawaii
F#
2
star
70

fsharp-exchange-2021

Code repository for the F# eXchange 2021 conference
F#
2
star
71

terraform-basic-vpc-to-pulumi

HCL
2
star
72

Cable.ArticleSample

Type-safe web app sample used to demonstrate Cable
C#
2
star
73

SAFE-FileUploadDownload

F#
2
star
74

login-with-url-extended

F#
2
star
75

hawaii-samples-odata-trippin

F#
2
star
76

ReactReduxTodoApp

Todo app written in pure C# with React and Redux
C#
2
star
77

Simple-Feliz-i18n

Using Feliz template, this repository shows how to implement a simple internationalization mechanism that makes parts of the application be dependent on the current users' locale.
JavaScript
2
star
78

Fable.React.Responsive

Fable binding for react-responsive that is ready to use within Elmish applications
F#
1
star
79

remoting-pure-kestrel

The simplest AspNetCore sample with Fable.Remoting using the Kestrel server
F#
1
star
80

Bridge.SweetAlert

SweetAlert bindings for the Bridge.NET project.
CSS
1
star
81

Image-Processor

A program to process and filter images, written in C#.
C#
1
star
82

FableSuaveSample

Fable and Suave sample for integration-testing Fable.Remoting with Suave to the death
F#
1
star
83

ChristianHolidaysAssignment

C++
1
star
84

gitbook-enhanced-katex

Math typesetting using KaTex for gitBook
JavaScript
1
star
85

RemotingJsonBenchmarks

F#
1
star
86

pulumi-codegen-dotnet

Building Pulumi Codegen tooling in F#
F#
1
star
87

elmish-todo-part1

F#
1
star
88

WindowsExplorer

A WPF application that mimics Windows Explorer functionality
C#
1
star
89

elmish-hackernews-part1

F#
1
star
90

elmish-todo-part2

F#
1
star
91

fast-refresh-bug-reproduction

JavaScript
1
star
92

tagmeme-analyzer

Static code analyzer that checks for exhaustive pattern matching, union case typos and redundant arguments in tagmeme
JavaScript
1
star
93

xmlhttprequest-in-elmish

F#
1
star
94

Bridge.Ractive

Bindings of Ractive.js to be used in Bridge.NET projects.
JavaScript
1
star
95

elmish-hackernews-part3

F#
1
star
96

elmish-todo-part3

F#
1
star