This post is a follow-up to the excellent Enterprise Tic-Tac-Toe by Scott Wlaschin. We’ll continue where Scott left off, transpile his F# to JavaScript using Fable and replace the console interface with a web interface to make it even more enterprisey.

JavaScript is Assembly for the Web

Back in 2011, Scott Hanselman made the analogy that JavaScript is Assembly Language for the Web. The analogy caused a lot of debate, but we’ve since seen the emergence of many transpilers to JavaScript, so with regards to JavaScript is as low-level as a web programming language goes, it appears that Scott was on to something.

A transpiler is a type of compiler that takes the source code of a program written in one programming language as its input and produces the equivalent source code in another programming language.

- Wikipedia

The advantages (naturally) depend on the transpiler, but some general advantages are additional features and enforcing good practices in the generated JavaScript.

Apps Everywhere

Fable

Fable is a F# to JavaScript transpiler created by Alfonso Garcia-Caro. It brings all the good parts of functional programming to JavaScript development, works directly on F# source code and supports most of the F# core library and some of .NET BCL.

With the release of Fable 1.0 beta (codename narumi), Fable integrates with .NET Core and Webpack. The integration with .NET Core makes it possible to use the .NET Core command-line (CLI) to easily manage Fable projects and with F# being part of the .NET family you get access to a comprehensive core library. Webpack is a JavaScript module bundler already used by many developers and integrating with it has various advantages and allows Fable to focus on creating JavaScript you can be proud of.

If you want to give Fable a quick go, check out the Fable REPL, which runs entirely in the browser by transpiling the F# compiler to JavaScript using Fable. Amazing stuff! 👏

Getting started

The new Template Engine in .NET Core makes it easy to create new templates as well as use existing ones, so let’s install the Fable Template and initialize a new Fable project:

mkdir FablelousEnterpriseTicTacToe
cd FablelousEnterpriseTicTacToe

dotnet new -i Fable.Template::*
dotnet new fable

This creates the template Simple Fable App, which is basically the Canvas sample in an updated version and contains the following files:

  • public\index.html Web page with a reference to the transpiled output, bundle.js.
  • src\App.fs F# code that is transpiled to bundle.js.
  • FablelousEnterpriseTicTacToe.fsproj F# project file.
  • package.json Node.js manifest.
  • README.md Markdown file typically included in projects as an introduction.
  • webpack.config.js Webpack configuration.

As Fable is a hybrid of a .NET and Node.js application, we restore/install dependencies specified in FablelousTicTacToe.fsproj and package.json respectively:

dotnet restore
npm install

Finally, we transpile and start a web server running the Simple Fable App:

dotnet fable npm-run start

The app can be viewed in a browser by going to http://localhost:8080.

Simple Fable App

Adding a web interface

With Fable up and running, we’re now ready to continue where Scott Wlaschin left off. In src\App.fs, we keep the module declaration and add an import declaration, Fable.Import.Browser, which contains the browser-related code:

module FablelousEnterpriseTicTacToe

open Fable.Import.Browser

Everything else is replaced with with the modules TicTacToeDomain and TicTacToeImplementation from enterprise-tic-tac-toe-2.fsx. The modules ConsoleApplication, ConsoleUi and Logger are not needed as we’re replacing the console interface with a web interface.

If the Fable server is still running, the code will automatically be transpiled upon saving (hot reloaded). Otherwise re-run dotnet fable npm-run start. So, Fable just took about 250 lines of F# code never intended for running in the browser and transpiled it to JavaScript. Magic! ✨

CSS Grid Layout

For the web interface we’ll use CSS Grid Layout, which is a layout system that after a long journey finally is available in all major browsers. It’s a two-dimensional grid-based system, and as such, it fits perfectly for constructing our Tic-Tac-Toe board, which is basically a grid of 3 x 3 cells.

In public/index.html, we add the following HTML to represent the board. A parent div, Board, and 9 child divs with ids corresponding to their position on the board. The ids are identical to the discriminated union values used in the F# code and are used for referencing the divs from the code.

<div id="Board">
    <div id="LeftTop"></div>
    <div id="HCenterTop"></div>
    <div id="RightTop"></div>
    <div id="LeftVCenter"></div>
    <div id="HCenterVCenter"></div>
    <div id="RightVCenter"></div>
    <div id="LeftBottom"></div>
    <div id="HCenterBottom"></div>
    <div id="RightBottom"></div>
</div>

In the stylesheet, we specify that the board is to be displayed as a grid of 3 columns with a width of 100 pixels and 3 rows with a height of 100 pixels, and a gap of 4 pixels between each cell.

#Board {
    background-color: black;
    display: grid; 
    grid-template-columns: repeat(3, 100px);    
    grid-template-rows: repeat(3, 100px);
    grid-gap: 4px;
    width: 308px;
}

#Board > div {
    background-color: white;
}

The result is a board that looks like this:

CSS Grid Layout

We also add some additional styling, a button for (re)starting the game, NewGameButton and a div element for displaying the status of the game, GameStatus. More about this in the next section.

Interaction

We follow the approach in the original code, but instead of ConsoleApplication and ConsoleUi we’ll have WebApplication and WebUi.

We add a click event listener to the NewGameButton element, which calls the WebUi.startGame function with TicTacToeAPI as an argument. The last line initializes the game when visiting the web page.

module WebApplication = 

    let newGameButtonClick() =
        let api = TicTacToeImplementation.api
        WebUi.startGame api

    let newGameButton = document.getElementById("NewGameButton") :?> HTMLButtonElement
    newGameButton.addEventListener_click(fun _ -> newGameButtonClick(); null)   

WebApplication.newGameButtonClick()

The WebUi.startGame function calls TicTacToeAPI.newGame, which returns a MoveResult that is the result of a move and includes game state and capabilities for the next move, if any. Read Enterprise Tic-Tac-Toe, part 2 to learn more about the capability-centric approach. Next, the gameLoop function is called with TicTacToeAPI and MoveResult as arguments.

module WebUi = 

    // ... more code ...

    let startGame api = 
        let moveResult = api.newGame()
        gameLoop api moveResult

The gameLoop function is called continuously during gameplay and is defined as a recursive function. When using recursive functions one should be cautious of stack overflow, but in our case the nested number of calls is guaranteed to be small, so not a problem. The MoveResult is matched to determine if the game is tied, won or it is either Player X or O to move. All matches update the GameStatus element, (re)display the board and remove all listeners. If either Player X or O to move, listeners are added for the possible moves.

    let rec gameLoop api moveResult =

        // ... more code ...

        match moveResult with
        | GameTied displayInfo ->
            document.getElementById("GameStatus").innerText <- "Game Tied"
            displayInfo |> displayCells
            removeListeners
        | GameWon (displayInfo, player) -> 
            document.getElementById("GameStatus").innerText <- "Game Won by " + getUnionCaseName(player)
            displayInfo |> displayCells
            removeListeners
        | PlayerOToMove (displayInfo, nextMoves) ->
            document.getElementById("GameStatus").innerText <- "Player O to move"
            displayInfo |> displayCells
            removeListeners
            nextMoves |> List.iteri (fun i moveInfo -> addListener moveInfo.posToPlay nextMoves i)
        | PlayerXToMove (displayInfo, nextMoves) ->
            document.getElementById("GameStatus").innerText <- "Player X to move"
            displayInfo |> displayCells
            removeListeners
            nextMoves |> List.iteri (fun i moveInfo -> addListener moveInfo.posToPlay nextMoves i)

The displayCells function displays the board. It retrieves DisplayInfo, which contains the state of all cells, as an argument. For each cell the innertext of the corresponding div is set to either X, O or empty (if not played). getUnionCaseName is a function for getting the name of a union case and getIdByCellPosition is a function to get the id of a cell position in order to reference cells (divs) from the code.

        let getUnionCaseName (e:'a) = (FSharpValue.GetUnionFields(e, typeof<'a>) |> fst).Name

        let getIdByCellPosition cellPosition =
            let horizPosition, vertPosition = cellPosition
            getUnionCaseName(horizPosition) + getUnionCaseName(vertPosition)

        let displayCells displayInfo = 
            let cells = displayInfo.cells
            let cellToStr cell = 
                match cell.state with
                | Empty -> ""            
                | Played player ->
                    match player with
                    | PlayerO -> "O"
                    | PlayerX -> "X"

            cells |> 
                List.iter (fun cell ->
                    let id = getIdByCellPosition cell.pos
                    document.getElementById(id).innerText <- cellToStr cell)

The easiest way to remove all event listeners on the board is to clone the Board element, which will remove all event listeners, and replace the Board element with the clone:

        let removeListeners =
            let oldElement = document.getElementById("Board")
            let newElement = oldElement.cloneNode(true) :?> HTMLDivElement
            oldElement.parentNode.replaceChild(newElement, oldElement) |> ignore

If either Player X or Player O’s turn to move, an event listener is added for all possible moves. The attached play function constructs a new MoveResult and calls gameLoop to continue the game.

        let getCapability selectedIndex nextMoves = 
            if selectedIndex < List.length nextMoves then
                let move = nextMoves.Item selectedIndex 
                Some move.capability 
            else
                None    

        let play (nextMoves : NextMoveInfo list, nextMoveIndex) =
            match getCapability nextMoveIndex nextMoves with
            | Some capability -> 
                let moveResult = capability()
                gameLoop api moveResult
            | _ -> failwith "Capability not found"

        let addListener cell nextMoves i =
            let id = getIdByCellPosition cellPosition
            let element = document.getElementById(id)
            element.addEventListener_click(fun _ -> play (nextMoves, i); null)


Wrapping up

We took F# code never intended for the browser and added a web interface also written in F# to create a tic-tac-toe game that can be played in the browser. All this without writing a single line of JavaScript. Thanks to Scott Wlaschin for the original code and to Fable for making web development great again.

The source code is available at FablelousEnterpriseTicTacToe and you can try out the game below.