├── starter-kit └── hmr │ ├── Source │ ├── styles │ │ └── app.styl │ ├── static │ │ ├── index.js │ │ └── index.html │ ├── Helpers.fsx │ ├── Entry.fsx │ └── Main.fsx │ ├── fableconfig.json │ ├── README.md │ ├── server.js │ ├── package.json │ └── webpack.config.js ├── src └── Fable.Arch │ ├── paket.references │ ├── fableconfig.json │ ├── Fable.Arch.React.fs │ ├── RouteParserTesting.fsx │ ├── Fable.Arch.Navigation.fs │ ├── Fable.Arch.Virtualdom.fs │ └── Fable.Arch.fsproj ├── docs ├── samples │ ├── template │ │ ├── README.md │ │ ├── package.json │ │ ├── sample.fsx │ │ ├── build.js │ │ └── public │ │ │ └── index.html │ ├── reactCounter │ │ ├── README.md │ │ ├── package.json │ │ ├── build.js │ │ ├── sample.fsx │ │ └── public │ │ │ └── index.html │ ├── clock │ │ ├── package.json │ │ ├── build.js │ │ ├── public │ │ │ └── index.html │ │ ├── sample.fsx │ │ └── README.md │ ├── counter │ │ ├── package.json │ │ ├── build.js │ │ ├── README.md │ │ ├── sample.fsx │ │ └── public │ │ │ └── index.html │ ├── echo │ │ ├── package.json │ │ ├── build.js │ │ ├── public │ │ │ └── index.html │ │ ├── README.md │ │ └── sample.fsx │ ├── hello │ │ ├── package.json │ │ ├── build.js │ │ ├── README.md │ │ ├── sample.fsx │ │ └── public │ │ │ └── index.html │ ├── routing │ │ ├── package.json │ │ ├── build.js │ │ ├── public │ │ │ └── index.html │ │ ├── README.md │ │ └── sample.fsx │ ├── subscriber │ │ ├── README.md │ │ ├── package.json │ │ ├── build.js │ │ ├── sample.fsx │ │ └── public │ │ │ └── index.html │ ├── calculator │ │ ├── package.json │ │ ├── build.js │ │ ├── public │ │ │ └── index.html │ │ ├── sample.fsx │ │ └── README.md │ ├── navigation │ │ ├── package.json │ │ ├── build.js │ │ ├── README.md │ │ ├── public │ │ │ └── index.html │ │ └── sample.fsx │ └── nestedcounter │ │ ├── package.json │ │ ├── build.js │ │ ├── public │ │ └── index.html │ │ ├── README.md │ │ └── sample.fsx ├── Settings.FSharpLint ├── doc_files │ ├── subscriber.md │ └── hmr.md ├── scss │ └── main.sass ├── package.json ├── src │ ├── Pages │ │ ├── About.fs │ │ ├── Index.fs │ │ ├── Docs │ │ │ ├── Docs_Dispatcher.fs │ │ │ └── Docs_Viewer.fs │ │ └── Sample │ │ │ ├── Sample_Viewer.fs │ │ │ └── Sample_Dispatcher.fs │ ├── DocGen.fs │ ├── Navbar.fs │ ├── libs │ │ ├── Fable.Import.Marked.fs │ │ └── Fable.Import.PrismJS.fs │ ├── Header.fs │ ├── Common.fs │ └── WebApp.fsproj ├── index.html ├── WebApp.sln ├── build.js └── public │ └── css │ └── prism.css ├── RELEASE_NOTES.md ├── package.json ├── .travis.yml ├── README.md └── .gitignore /starter-kit/hmr/Source/styles/app.styl: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Fable.Arch/paket.references: -------------------------------------------------------------------------------- 1 | FSharp.Core -------------------------------------------------------------------------------- /docs/samples/template/README.md: -------------------------------------------------------------------------------- 1 | # Sample name 2 | 3 | In this file you can put the documentation shown when the sample is displayed on the docs site. -------------------------------------------------------------------------------- /starter-kit/hmr/fableconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "module": "amd", 3 | "sourceMaps": true, 4 | "projFile": "./Source/Entry.fsx", 5 | "outDir": "out" 6 | } 7 | -------------------------------------------------------------------------------- /docs/samples/reactCounter/README.md: -------------------------------------------------------------------------------- 1 | # React counter 2 | 3 | This sample is a direct port from [Counter sample](#/sample/counter?height=300) using React renderer. 4 | -------------------------------------------------------------------------------- /starter-kit/hmr/Source/static/index.js: -------------------------------------------------------------------------------- 1 | // Pull in desired css/stylus files 2 | require( '../styles/app.styl' ); 3 | 4 | // Call Entry point 5 | var Entry = require( '../../out/Entry' ); 6 | -------------------------------------------------------------------------------- /starter-kit/hmr/README.md: -------------------------------------------------------------------------------- 1 | ## Features 2 | 3 | * Hot module replacement for 4 | * Javascript over Fable (with source map support) 5 | * Css over stylus preprocessor 6 | * Templating of your `index.html` 7 | * Serving static files from `img/` folder 8 | -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | ### 0.10.1 2 | 3 | * Add vnode function to inject virtual-dom in view 4 | * Add virtual-dom hook support 5 | 6 | ### 0.10.0 7 | 8 | * Implements `start` and `startAndExposeMessageSink` 9 | 10 | ### 0.10.0-alpha.1 11 | 12 | * Upgrade to Fable 0.7.0 13 | -------------------------------------------------------------------------------- /starter-kit/hmr/Source/Helpers.fsx: -------------------------------------------------------------------------------- 1 | #r "../node_modules/fable-core/Fable.Core.dll" 2 | namespace Helpers 3 | 4 | open Fable.Core 5 | open Fable.Import 6 | open Fable.Import.Browser 7 | 8 | [] 9 | module Helpers = 10 | 11 | let isNotNull x = 12 | not (isNull x) 13 | -------------------------------------------------------------------------------- /starter-kit/hmr/Source/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Websocket Echo with HMR 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Fable.Arch/fableconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "projFile": "./Fable.Arch.fsproj", 3 | "sourceMaps": true, 4 | "outDir": "../../npm", 5 | "module": "es2015", 6 | "dll": true, 7 | "targets": { 8 | "umd": { 9 | "outDir": "../../npm/umd", 10 | "module": "umd", 11 | "dll": false 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /docs/Settings.FSharpLint: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | False 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fable-arch", 3 | "version": "0.0.0", 4 | "description": "Fable Arch is a generic version of the elm architecture implemented with Fable in F#.", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/fable-compiler/fable-arch" 8 | }, 9 | "license": "Apache-2.0", 10 | "author": "Tomas Jansson", 11 | "keywords": [ 12 | "fable", 13 | "fable-compiler", 14 | "fsharp", 15 | "F#" 16 | ], 17 | "devDependencies": { 18 | "fable-compiler": "^0.7.43", 19 | "fable-core": "^0.7.28", 20 | "fable-react": "^0.8.0", 21 | "fs-extra": "^1.0.0" 22 | }, 23 | "homepage": "https://github.com/fable-compiler/Fable#readme" 24 | } 25 | -------------------------------------------------------------------------------- /starter-kit/hmr/Source/Entry.fsx: -------------------------------------------------------------------------------- 1 | #r "../node_modules/fable-core/Fable.Core.dll" 2 | #load "Helpers.fsx" 3 | #load "Main.fsx" 4 | 5 | open Fable.Core 6 | open Fable.Import 7 | open Fable.Import.Browser 8 | 9 | open Fable.Core.JsInterop 10 | open Helpers 11 | 12 | 13 | let contentNode = "#app" 14 | 15 | #if DEV_HMR 16 | 17 | type IModule = 18 | abstract hot: obj with get, set 19 | 20 | let [] [] Module : IModule = failwith "JS only" 21 | 22 | let node = document.querySelector contentNode 23 | 24 | if isNotNull Module.hot then 25 | Module.hot?accept() |> ignore 26 | 27 | Module.hot?dispose(fun _ -> 28 | node.removeChild(node.firstChild) |> ignore 29 | ) |> ignore 30 | #endif 31 | 32 | App.Main.start contentNode () 33 | -------------------------------------------------------------------------------- /starter-kit/hmr/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file runs a webpack-dev-server, using the API. 3 | * 4 | * For more information on the options passed to WebpackDevServer, 5 | * see the webpack-dev-server API docs: 6 | * https://github.com/webpack/docs/wiki/webpack-dev-server#api 7 | */ 8 | var WebpackDevServer = require('webpack-dev-server'); 9 | var webpack = require('webpack'); 10 | var config = require('./webpack.config.js'); 11 | 12 | var compiler = webpack(config); 13 | var server = new WebpackDevServer(compiler, { 14 | contentBase: 'out', 15 | hot: true, 16 | stats: { 17 | colors: true, 18 | }, 19 | watchOptions: { 20 | aggregateTimeout: 300, 21 | poll: 1000 22 | } 23 | }); 24 | server.listen(8080, 'localhost', function() {}); 25 | -------------------------------------------------------------------------------- /docs/samples/clock/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "node build.js", 5 | "build-dev": "node build.js dev", 6 | "dev": "concurrently \"npm run serve\" \"npm run watch\"", 7 | "serve": "http-server . -c-1 -s", 8 | "setup": "node setup.js", 9 | "watch": "node build.js dev" 10 | }, 11 | "dependencies": { 12 | "babel-runtime": "^6.20.0", 13 | "concurrently": "^3.1.0", 14 | "fable-core": "^0.7.26", 15 | "fable-powerpack": "^0.0.19", 16 | "virtual-dom": "^2.1.1" 17 | }, 18 | "devDependencies": { 19 | "babel-plugin-transform-runtime": "^6.15.0", 20 | "fable-arch": "^0.10.0", 21 | "fable-compiler": "^0.7.42", 22 | "fs-extra": "^1.0.0", 23 | "http-server": "^0.9.0" 24 | }, 25 | "engines": { 26 | "fable": "^0.7.42" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /docs/samples/counter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "node build.js", 5 | "build-dev": "node build.js dev", 6 | "dev": "concurrently \"npm run serve\" \"npm run watch\"", 7 | "serve": "http-server . -c-1 -s", 8 | "setup": "node setup.js", 9 | "watch": "node build.js dev" 10 | }, 11 | "dependencies": { 12 | "babel-runtime": "^6.20.0", 13 | "concurrently": "^3.1.0", 14 | "fable-core": "^0.7.26", 15 | "fable-powerpack": "^0.0.19", 16 | "virtual-dom": "^2.1.1" 17 | }, 18 | "devDependencies": { 19 | "babel-plugin-transform-runtime": "^6.15.0", 20 | "fable-arch": "^0.10.0", 21 | "fable-compiler": "^0.7.42", 22 | "fs-extra": "^1.0.0", 23 | "http-server": "^0.9.0" 24 | }, 25 | "engines": { 26 | "fable": "^0.7.42" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /docs/samples/echo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "node build.js", 5 | "build-dev": "node build.js dev", 6 | "dev": "concurrently \"npm run serve\" \"npm run watch\"", 7 | "serve": "http-server . -c-1 -s", 8 | "setup": "node setup.js", 9 | "watch": "node build.js dev" 10 | }, 11 | "dependencies": { 12 | "babel-runtime": "^6.20.0", 13 | "concurrently": "^3.1.0", 14 | "fable-core": "^0.7.26", 15 | "fable-powerpack": "^0.0.19", 16 | "virtual-dom": "^2.1.1" 17 | }, 18 | "devDependencies": { 19 | "babel-plugin-transform-runtime": "^6.15.0", 20 | "fable-arch": "^0.10.0", 21 | "fable-compiler": "^0.7.42", 22 | "fs-extra": "^1.0.0", 23 | "http-server": "^0.9.0" 24 | }, 25 | "engines": { 26 | "fable": "^0.7.42" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /docs/samples/hello/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "node build.js", 5 | "build-dev": "node build.js dev", 6 | "dev": "concurrently \"npm run serve\" \"npm run watch\"", 7 | "serve": "http-server . -c-1 -s", 8 | "setup": "node setup.js", 9 | "watch": "node build.js dev" 10 | }, 11 | "dependencies": { 12 | "babel-runtime": "^6.20.0", 13 | "concurrently": "^3.1.0", 14 | "fable-core": "^0.7.26", 15 | "fable-powerpack": "^0.0.19", 16 | "virtual-dom": "^2.1.1" 17 | }, 18 | "devDependencies": { 19 | "babel-plugin-transform-runtime": "^6.15.0", 20 | "fable-arch": "^0.10.0", 21 | "fable-compiler": "^0.7.42", 22 | "fs-extra": "^1.0.0", 23 | "http-server": "^0.9.0" 24 | }, 25 | "engines": { 26 | "fable": "^0.7.42" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /docs/samples/routing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "node build.js", 5 | "build-dev": "node build.js dev", 6 | "dev": "concurrently \"npm run serve\" \"npm run watch\"", 7 | "serve": "http-server . -c-1 -s", 8 | "setup": "node setup.js", 9 | "watch": "node build.js dev" 10 | }, 11 | "dependencies": { 12 | "babel-runtime": "^6.20.0", 13 | "concurrently": "^3.1.0", 14 | "fable-core": "^0.7.26", 15 | "fable-powerpack": "^0.0.19", 16 | "virtual-dom": "^2.1.1" 17 | }, 18 | "devDependencies": { 19 | "babel-plugin-transform-runtime": "^6.15.0", 20 | "fable-arch": "^0.10.0", 21 | "fable-compiler": "^0.7.42", 22 | "fs-extra": "^1.0.0", 23 | "http-server": "^0.9.0" 24 | }, 25 | "engines": { 26 | "fable": "^0.7.42" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /docs/samples/subscriber/README.md: -------------------------------------------------------------------------------- 1 | # Subscriber sample 2 | 3 | This sample take the [hello sample](#/sample/hello?height=350) and add a subscriber to it. 4 | 5 | ## Register subscriber 6 | 7 | To register a subscriber we use `withSubscriber` function. 8 | 9 | This function signature is `(ModelChanged<'a,'b> -> unit) -> app:AppSpecification<'b,'a,'c> -> AppSpecification<'b,'a,'c>` 10 | 11 | 12 | So we need to provide it with a function taking a `ModelChanged` argument and return unit. We also need to pass it the `AppSpecification` on which to attach. 13 | 14 | ```fsharp 15 | createSimpleApp "" view update Virtualdom.createRender 16 | |> withStartNodeSelector "#sample" 17 | // Next line is how we register the subscriber 18 | |> withSubscriber (fun x -> Fable.Import.Browser.console.log("Event received: ", x)) 19 | |> start 20 | ``` 21 | -------------------------------------------------------------------------------- /docs/samples/template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "node build.js", 5 | "build-dev": "node build.js dev", 6 | "dev": "concurrently \"npm run serve\" \"npm run watch\"", 7 | "serve": "http-server . -c-1 -s", 8 | "setup": "node setup.js", 9 | "watch": "node build.js dev" 10 | }, 11 | "dependencies": { 12 | "babel-runtime": "^6.20.0", 13 | "concurrently": "^3.1.0", 14 | "fable-core": "^0.7.26", 15 | "fable-powerpack": "^0.0.19", 16 | "virtual-dom": "^2.1.1" 17 | }, 18 | "devDependencies": { 19 | "babel-plugin-transform-runtime": "^6.15.0", 20 | "fable-arch": "^0.10.0", 21 | "fable-compiler": "^0.7.42", 22 | "fs-extra": "^1.0.0", 23 | "http-server": "^0.9.0" 24 | }, 25 | "engines": { 26 | "fable": "^0.7.42" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /docs/samples/calculator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "node build.js", 5 | "build-dev": "node build.js dev", 6 | "dev": "concurrently \"npm run serve\" \"npm run watch\"", 7 | "serve": "http-server . -c-1 -s", 8 | "setup": "node setup.js", 9 | "watch": "node build.js dev" 10 | }, 11 | "dependencies": { 12 | "babel-runtime": "^6.20.0", 13 | "concurrently": "^3.1.0", 14 | "fable-core": "^0.7.26", 15 | "fable-powerpack": "^0.0.19", 16 | "virtual-dom": "^2.1.1" 17 | }, 18 | "devDependencies": { 19 | "babel-plugin-transform-runtime": "^6.15.0", 20 | "fable-arch": "^0.10.0", 21 | "fable-compiler": "^0.7.42", 22 | "fs-extra": "^1.0.0", 23 | "http-server": "^0.9.0" 24 | }, 25 | "engines": { 26 | "fable": "^0.7.42" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /docs/samples/navigation/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "node build.js", 5 | "build-dev": "node build.js dev", 6 | "dev": "concurrently \"npm run serve\" \"npm run watch\"", 7 | "serve": "http-server . -c-1 -s", 8 | "setup": "node setup.js", 9 | "watch": "node build.js dev" 10 | }, 11 | "dependencies": { 12 | "babel-runtime": "^6.20.0", 13 | "concurrently": "^3.1.0", 14 | "fable-core": "^0.7.26", 15 | "fable-powerpack": "^0.0.19", 16 | "virtual-dom": "^2.1.1" 17 | }, 18 | "devDependencies": { 19 | "babel-plugin-transform-runtime": "^6.15.0", 20 | "fable-arch": "^0.10.0", 21 | "fable-compiler": "^0.7.42", 22 | "fs-extra": "^1.0.0", 23 | "http-server": "^0.9.0" 24 | }, 25 | "engines": { 26 | "fable": "^0.7.42" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /docs/samples/nestedcounter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "node build.js", 5 | "build-dev": "node build.js dev", 6 | "dev": "concurrently \"npm run serve\" \"npm run watch\"", 7 | "serve": "http-server . -c-1 -s", 8 | "setup": "node setup.js", 9 | "watch": "node build.js dev" 10 | }, 11 | "dependencies": { 12 | "babel-runtime": "^6.20.0", 13 | "concurrently": "^3.1.0", 14 | "fable-core": "^0.7.26", 15 | "fable-powerpack": "^0.0.19", 16 | "virtual-dom": "^2.1.1" 17 | }, 18 | "devDependencies": { 19 | "babel-plugin-transform-runtime": "^6.15.0", 20 | "fable-arch": "^0.10.0", 21 | "fable-compiler": "^0.7.42", 22 | "fs-extra": "^1.0.0", 23 | "http-server": "^0.9.0" 24 | }, 25 | "engines": { 26 | "fable": "^0.7.42" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /docs/samples/reactCounter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "node build.js", 5 | "build-dev": "node build.js dev", 6 | "dev": "concurrently \"npm run serve\" \"npm run watch\"", 7 | "serve": "http-server . -c-1 -s", 8 | "setup": "node setup.js", 9 | "watch": "node build.js dev" 10 | }, 11 | "dependencies": { 12 | "babel-runtime": "^6.20.0", 13 | "concurrently": "^3.1.0", 14 | "fable-core": "^0.7.26", 15 | "fable-powerpack": "^0.0.19", 16 | "virtual-dom": "^2.1.1" 17 | }, 18 | "devDependencies": { 19 | "babel-plugin-transform-runtime": "^6.15.0", 20 | "fable-arch": "^0.10.0", 21 | "fable-compiler": "^0.7.42", 22 | "fs-extra": "^1.0.0", 23 | "http-server": "^0.9.0" 24 | }, 25 | "engines": { 26 | "fable": "^0.7.42" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /docs/samples/subscriber/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "node build.js", 5 | "build-dev": "node build.js dev", 6 | "dev": "concurrently \"npm run serve\" \"npm run watch\"", 7 | "serve": "http-server . -c-1 -s", 8 | "setup": "node setup.js", 9 | "watch": "node build.js dev" 10 | }, 11 | "dependencies": { 12 | "babel-runtime": "^6.20.0", 13 | "concurrently": "^3.1.0", 14 | "fable-core": "^0.7.26", 15 | "fable-powerpack": "^0.0.19", 16 | "virtual-dom": "^2.1.1" 17 | }, 18 | "devDependencies": { 19 | "babel-plugin-transform-runtime": "^6.15.0", 20 | "fable-arch": "^0.10.0", 21 | "fable-compiler": "^0.7.42", 22 | "fs-extra": "^1.0.0", 23 | "http-server": "^0.9.0" 24 | }, 25 | "engines": { 26 | "fable": "^0.7.42" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /docs/doc_files/subscriber.md: -------------------------------------------------------------------------------- 1 | # Subscriber 2 | 3 | A subscriber is a function attach to an application. This subscriber will be notify every time an event/message is being handle by the application. 4 | 5 | ## Signature 6 | 7 | 8 | As we can see here a subscriber is a function taking `ModelChanged<'TMessage,'TModel>` as an entry and return `unit`. 9 | 10 | ModelChanged type definition: 11 | ```fsharp 12 | type ModelChanged<'TMessage, 'TModel> = 13 | { 14 | PreviousState: 'TModel 15 | Message: 'TMessage 16 | CurrentState: 'TModel 17 | } 18 | ``` 19 | 20 | ## Sample 21 | 22 | Here a sample, showing how to print in the console all the message handle by the app. 23 | 24 | ```fsharp 25 | createApp Model.Initial view update Virtualdom.createRender 26 | |> withStartNodeSelector "#app" 27 | // Here is the subscriber 28 | |> withSubscriber (fun x -> console.log("Action received:", x.Message)) 29 | |> start 30 | |> ignore 31 | ``` 32 | -------------------------------------------------------------------------------- /docs/scss/main.sass: -------------------------------------------------------------------------------- 1 | // Colors 2 | $purple: #7a325d 3 | $primary: $purple 4 | $black: #202020 5 | 6 | @import '../node_modules/bulma/bulma' 7 | 8 | .navbar-bg 9 | background-color: $black 10 | .nav 11 | background-color: $black 12 | .title 13 | color: #686868 14 | font-weight: 600 15 | 16 | 17 | // Override bulma .number class for prismjs classes 18 | .number 19 | align-items: center 20 | background-color: $background 21 | border-radius: 290486px 22 | display: inline 23 | font-size: 12px 24 | height: 2em 25 | justify-content: center 26 | margin-right: 0em 27 | min-width: 2.5em 28 | padding: 0 29 | text-align: center 30 | vertical-align: top 31 | 32 | #twitter 33 | color: #55acee 34 | border-color: #55acee 35 | 36 | #github 37 | color: $black 38 | border-color: $black 39 | 40 | // Needed otherwise iframes show a disabled scrollbar 41 | html, 42 | body 43 | overflow-y: auto 44 | overflow-x: hidden 45 | 46 | .sample-viewer 47 | width: 100% 48 | box-shadow: 0px 0px 2px $purple 49 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fable-vdom-web-app", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "node build.js", 7 | "build-dev": "node build.js dev", 8 | "dev": "concurrently \"npm run serve\" \"npm run watch\"", 9 | "serve": "http-server . -c-1 -s", 10 | "setup": "node setup.js", 11 | "watch": "node build.js dev", 12 | "build-css": "node-sass scss/main.sass public/css/style.css", 13 | "watch-css": "node-sass scss/main.sass public/css/style.css -w scss" 14 | }, 15 | "dependencies": { 16 | "babel-runtime": "^6.20.0", 17 | "bulma": "^0.3.0", 18 | "concurrently": "^3.1.0", 19 | "fable-core": "^0.7.26", 20 | "fable-powerpack": "^0.0.19", 21 | "fable-react": "^0.8.5", 22 | "node-sass": "^4.3.0", 23 | "virtual-dom": "^2.1.1" 24 | }, 25 | "devDependencies": { 26 | "babel-plugin-transform-runtime": "^6.15.0", 27 | "fable-arch": "^0.10.0", 28 | "fable-compiler": "^0.7.42", 29 | "fs-extra": "^1.0.0", 30 | "http-server": "^0.9.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /docs/src/Pages/About.fs: -------------------------------------------------------------------------------- 1 | namespace WebApp.Pages 2 | 3 | open Fable.Import 4 | open Fable.Arch 5 | open Fable.Arch.Html 6 | 7 | module About = 8 | 9 | let markdownText = 10 | " 11 | # About 12 | 13 | This website is written with: 14 | 15 | - [Fable](http://fable.io/) a transpiler F# to Javascript 16 | - [Fable-arch](https://github.com/fable-compiler/fable-arch) a set of tools for building modern web applications inspired by the [elm architecture](http://guide.elm-lang.org/architecture/index.html). 17 | - [Bulma](http://bulma.io/) a modern CSS framework based on Flexbox 18 | - [Marked](https://github.com/chjj/marked) a markdown parser and compiler. Built for speed 19 | - [PrismJS](http://prismjs.com/) a lightweight, extensible syntax highlighter 20 | " 21 | 22 | let view () = 23 | div 24 | [ classy "section" ] 25 | [ div 26 | [ classy "container" ] 27 | [ div 28 | [ classy "content" 29 | property "innerHTML" (Marked.Globals.marked.parse(markdownText)) 30 | ] 31 | [] 32 | ] 33 | ] 34 | -------------------------------------------------------------------------------- /docs/samples/template/sample.fsx: -------------------------------------------------------------------------------- 1 | // If you are using the sample in standalone please switch the import lines 2 | // #r "node_modules/fable-core/Fable.Core.dll" 3 | // #r "node_modules/fable-arch/Fable.Arch.dll" 4 | // Imports for docs site mode 5 | #r "../../node_modules/fable-core/Fable.Core.dll" 6 | #r "../../node_modules/fable-arch/Fable.Arch.dll" 7 | 8 | open Fable.Arch 9 | open Fable.Arch.App 10 | open Fable.Arch.App.AppApi 11 | open Fable.Arch.Html 12 | 13 | type Model = string 14 | 15 | type Actions = 16 | | NoOp 17 | 18 | let update model action = 19 | match action with 20 | | NoOp -> model 21 | 22 | let view (model: Model) = 23 | div 24 | [] 25 | [ text "Your application is running" ] 26 | 27 | // Using createSimpleApp instead of createApp since our 28 | // update function doesn't generate any actions. See 29 | // some of the other more advanced examples for how to 30 | // use createApp. In addition to the application functions 31 | // we also need to specify which renderer to use. 32 | createSimpleApp "" view update Virtualdom.createRender 33 | |> withStartNodeSelector "#sample" 34 | |> start 35 | -------------------------------------------------------------------------------- /docs/src/DocGen.fs: -------------------------------------------------------------------------------- 1 | namespace WebApp 2 | 3 | open Fable.Core 4 | open Fable.Import 5 | open Fable.PowerPack.Fetch 6 | open Fable.PowerPack 7 | open System.Text.RegularExpressions 8 | 9 | module DocGen = 10 | 11 | let sampleSourceDirectory = "src/Pages/Sample" 12 | 13 | let docFilesDirectory = "doc_files" 14 | 15 | #if DEV 16 | let rawUrl = sprintf "http://%s" Browser.location.host 17 | let createSampleDirectoryURL sampleName = 18 | sprintf "%s/samples/%s/public/" rawUrl sampleName 19 | #else 20 | let rawUrl = "https://raw.githubusercontent.com/fable-compiler/fable-arch/gh-pages/" 21 | let createSampleDirectoryURL sampleName = 22 | sprintf "http://fable.io/fable-arch/samples/%s/public" sampleName 23 | #endif 24 | 25 | let createDocFilesDirectoryURL fileName = 26 | sprintf "%s/%s/%s.md" rawUrl docFilesDirectory fileName 27 | 28 | let createDocURL fileName = 29 | sprintf "#/docs?fileName=%s" fileName 30 | 31 | 32 | let createSampleReadmeURL sampleName = 33 | sprintf "%s/samples/%s/README.md" rawUrl sampleName 34 | 35 | let githubURL sampleName = 36 | sprintf "http://github.com/fable-compiler/fable-arch/tree/master/docs/samples/%s" sampleName 37 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Fable-arch - F# |> Babel 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: csharp 2 | before_deploy: 3 | - cd src/Fable.Arch 4 | deploy: 5 | provider: npm 6 | email: tomas.jansson@gmail.com 7 | api_key: 8 | secure: qTksJDsQtgnDiEoVdT6AMEIaZ5kxAE+2A2KJvL/8AlZQ2db0ZOo+yAX2EHukK2dEH82wlr3cLabcwKGz+QK7EoZzMJ+b6G+/RRzl1jGbPDza5Jyuq9Iae9BCLWKgOxSJiFF4GdQbNex+FpN/sbQEGcoMwQ3AIbaHbvWhccMgsUXEXaQz4OFtBar0roxC64gSfE0ecapdbW7mDcSNhrUgpujXhd+kuaaBu91Mc6EDXMgf3tCoDFI/Dno+4T7S4rZHqX3Y+PCDNXPIQzSAA7rOX41pM14+3s+g6/78KIxhgP03pkCY1EEN/RqhAXr7obAH4KdCOvuMKkWdRJkd/G0bgTDUz/VwVN0SK9tqN36LcPYGty9p2Af0hSbpaTVpcb4gzsZXq//8FPedvnhuocCuMP+FfiIl9lyEq0cXXa35Z3fS+bDDX4R0XMu2baejxh80pLl4VbQfS1XAUuMnckRWywm6ucUYv8lOnih1r4yDFk396eGY9Qahu9xIFfzKwk/r8Hcu8TiLTwE7EWzXlM2fy4fCn+s21ziNkdfNpNAHRmm1MFnasleNCdbeLOCuaYksb6AOK4XDF1MhN3665s+7VM0uMP0NkgTdX20U6oCHPLmMmdHBpWAMUxPO184KdEKqefRlLHkJGAySdT6Uw/3gS+1oaXMEPgKMlEsPMky2ePo= 9 | on: 10 | repo: fable-compiler/fable-arch 11 | env: 12 | - TRAVIS_NODE_VERSION="7" 13 | install: 14 | - rm -rf ~/.nvm && git clone https://github.com/creationix/nvm.git ~/.nvm && (cd ~/.nvm && git checkout `git describe --abbrev=0 --tags`) && source ~/.nvm/nvm.sh && nvm install $TRAVIS_NODE_VERSION 15 | before_script: 16 | - npm install -g npm@'>=3' 17 | - npm install 18 | script: node build.js 19 | -------------------------------------------------------------------------------- /docs/samples/clock/build.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs-extra'); 3 | const fable = require('fable-compiler'); 4 | 5 | const BUILD_DIR = "public" 6 | const JS_DIR = "js" 7 | const DEST_FILE = "bundle.js" 8 | const PKG_JSON = "package.json" 9 | const PROJ_FILE = "sample.fsx" 10 | 11 | const fableconfig = { 12 | "babelPlugins": [ "transform-runtime" ], 13 | "projFile": PROJ_FILE, 14 | "rollup": { 15 | "dest": path.join(BUILD_DIR, JS_DIR, DEST_FILE), 16 | "external": ["virtual-dom"], 17 | "globals": { 18 | "virtualDom": "virtual-dom" 19 | } 20 | } 21 | }; 22 | 23 | const fableconfigDev = 24 | Object.assign({ 25 | "sourceMaps": true, 26 | "watch": true, 27 | "symbols": "DEV" 28 | }, fableconfig) 29 | 30 | const targets = { 31 | clean() { 32 | return fable.promisify(fs.remove, path.join(BUILD_DIR, JS_DIR)) 33 | }, 34 | build() { 35 | return this.clean() 36 | .then(_ => fable.compile(fableconfig)) 37 | }, 38 | dev() { 39 | return this.clean() 40 | .then(_ => fable.compile(fableconfigDev)) 41 | } 42 | } 43 | 44 | // As with FAKE scripts, run a default target if no one is specified 45 | targets[process.argv[2] || "build"]().catch(err => { 46 | console.log(err); 47 | process.exit(-1); 48 | }); 49 | -------------------------------------------------------------------------------- /docs/samples/counter/build.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs-extra'); 3 | const fable = require('fable-compiler'); 4 | 5 | const BUILD_DIR = "public" 6 | const JS_DIR = "js" 7 | const DEST_FILE = "bundle.js" 8 | const PKG_JSON = "package.json" 9 | const PROJ_FILE = "sample.fsx" 10 | 11 | const fableconfig = { 12 | "babelPlugins": [ "transform-runtime" ], 13 | "projFile": PROJ_FILE, 14 | "rollup": { 15 | "dest": path.join(BUILD_DIR, JS_DIR, DEST_FILE), 16 | "external": ["virtual-dom"], 17 | "globals": { 18 | "virtualDom": "virtual-dom" 19 | } 20 | } 21 | }; 22 | 23 | const fableconfigDev = 24 | Object.assign({ 25 | "sourceMaps": true, 26 | "watch": true, 27 | "symbols": "DEV" 28 | }, fableconfig) 29 | 30 | const targets = { 31 | clean() { 32 | return fable.promisify(fs.remove, path.join(BUILD_DIR, JS_DIR)) 33 | }, 34 | build() { 35 | return this.clean() 36 | .then(_ => fable.compile(fableconfig)) 37 | }, 38 | dev() { 39 | return this.clean() 40 | .then(_ => fable.compile(fableconfigDev)) 41 | } 42 | } 43 | 44 | // As with FAKE scripts, run a default target if no one is specified 45 | targets[process.argv[2] || "build"]().catch(err => { 46 | console.log(err); 47 | process.exit(-1); 48 | }); 49 | -------------------------------------------------------------------------------- /docs/samples/echo/build.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs-extra'); 3 | const fable = require('fable-compiler'); 4 | 5 | const BUILD_DIR = "public" 6 | const JS_DIR = "js" 7 | const DEST_FILE = "bundle.js" 8 | const PKG_JSON = "package.json" 9 | const PROJ_FILE = "sample.fsx" 10 | 11 | const fableconfig = { 12 | "babelPlugins": [ "transform-runtime" ], 13 | "projFile": PROJ_FILE, 14 | "rollup": { 15 | "dest": path.join(BUILD_DIR, JS_DIR, DEST_FILE), 16 | "external": ["virtual-dom"], 17 | "globals": { 18 | "virtualDom": "virtual-dom" 19 | } 20 | } 21 | }; 22 | 23 | const fableconfigDev = 24 | Object.assign({ 25 | "sourceMaps": true, 26 | "watch": true, 27 | "symbols": "DEV" 28 | }, fableconfig) 29 | 30 | const targets = { 31 | clean() { 32 | return fable.promisify(fs.remove, path.join(BUILD_DIR, JS_DIR)) 33 | }, 34 | build() { 35 | return this.clean() 36 | .then(_ => fable.compile(fableconfig)) 37 | }, 38 | dev() { 39 | return this.clean() 40 | .then(_ => fable.compile(fableconfigDev)) 41 | } 42 | } 43 | 44 | // As with FAKE scripts, run a default target if no one is specified 45 | targets[process.argv[2] || "build"]().catch(err => { 46 | console.log(err); 47 | process.exit(-1); 48 | }); 49 | -------------------------------------------------------------------------------- /docs/samples/hello/build.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs-extra'); 3 | const fable = require('fable-compiler'); 4 | 5 | const BUILD_DIR = "public" 6 | const JS_DIR = "js" 7 | const DEST_FILE = "bundle.js" 8 | const PKG_JSON = "package.json" 9 | const PROJ_FILE = "sample.fsx" 10 | 11 | const fableconfig = { 12 | "babelPlugins": [ "transform-runtime" ], 13 | "projFile": PROJ_FILE, 14 | "rollup": { 15 | "dest": path.join(BUILD_DIR, JS_DIR, DEST_FILE), 16 | "external": ["virtual-dom"], 17 | "globals": { 18 | "virtualDom": "virtual-dom" 19 | } 20 | } 21 | }; 22 | 23 | const fableconfigDev = 24 | Object.assign({ 25 | "sourceMaps": true, 26 | "watch": true, 27 | "symbols": "DEV" 28 | }, fableconfig) 29 | 30 | const targets = { 31 | clean() { 32 | return fable.promisify(fs.remove, path.join(BUILD_DIR, JS_DIR)) 33 | }, 34 | build() { 35 | return this.clean() 36 | .then(_ => fable.compile(fableconfig)) 37 | }, 38 | dev() { 39 | return this.clean() 40 | .then(_ => fable.compile(fableconfigDev)) 41 | } 42 | } 43 | 44 | // As with FAKE scripts, run a default target if no one is specified 45 | targets[process.argv[2] || "build"]().catch(err => { 46 | console.log(err); 47 | process.exit(-1); 48 | }); 49 | -------------------------------------------------------------------------------- /docs/samples/calculator/build.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs-extra'); 3 | const fable = require('fable-compiler'); 4 | 5 | const BUILD_DIR = "public" 6 | const JS_DIR = "js" 7 | const DEST_FILE = "bundle.js" 8 | const PKG_JSON = "package.json" 9 | const PROJ_FILE = "sample.fsx" 10 | 11 | const fableconfig = { 12 | "babelPlugins": [ "transform-runtime" ], 13 | "projFile": PROJ_FILE, 14 | "rollup": { 15 | "dest": path.join(BUILD_DIR, JS_DIR, DEST_FILE), 16 | "external": ["virtual-dom"], 17 | "globals": { 18 | "virtualDom": "virtual-dom" 19 | } 20 | } 21 | }; 22 | 23 | const fableconfigDev = 24 | Object.assign({ 25 | "sourceMaps": true, 26 | "watch": true, 27 | "symbols": "DEV" 28 | }, fableconfig) 29 | 30 | const targets = { 31 | clean() { 32 | return fable.promisify(fs.remove, path.join(BUILD_DIR, JS_DIR)) 33 | }, 34 | build() { 35 | return this.clean() 36 | .then(_ => fable.compile(fableconfig)) 37 | }, 38 | dev() { 39 | return this.clean() 40 | .then(_ => fable.compile(fableconfigDev)) 41 | } 42 | } 43 | 44 | // As with FAKE scripts, run a default target if no one is specified 45 | targets[process.argv[2] || "build"]().catch(err => { 46 | console.log(err); 47 | process.exit(-1); 48 | }); 49 | -------------------------------------------------------------------------------- /docs/samples/navigation/build.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs-extra'); 3 | const fable = require('fable-compiler'); 4 | 5 | const BUILD_DIR = "public" 6 | const JS_DIR = "js" 7 | const DEST_FILE = "bundle.js" 8 | const PKG_JSON = "package.json" 9 | const PROJ_FILE = "sample.fsx" 10 | 11 | const fableconfig = { 12 | "babelPlugins": [ "transform-runtime" ], 13 | "projFile": PROJ_FILE, 14 | "rollup": { 15 | "dest": path.join(BUILD_DIR, JS_DIR, DEST_FILE), 16 | "external": ["virtual-dom"], 17 | "globals": { 18 | "virtualDom": "virtual-dom" 19 | } 20 | } 21 | }; 22 | 23 | const fableconfigDev = 24 | Object.assign({ 25 | "sourceMaps": true, 26 | "watch": true, 27 | "symbols": "DEV" 28 | }, fableconfig) 29 | 30 | const targets = { 31 | clean() { 32 | return fable.promisify(fs.remove, path.join(BUILD_DIR, JS_DIR)) 33 | }, 34 | build() { 35 | return this.clean() 36 | .then(_ => fable.compile(fableconfig)) 37 | }, 38 | dev() { 39 | return this.clean() 40 | .then(_ => fable.compile(fableconfigDev)) 41 | } 42 | } 43 | 44 | // As with FAKE scripts, run a default target if no one is specified 45 | targets[process.argv[2] || "build"]().catch(err => { 46 | console.log(err); 47 | process.exit(-1); 48 | }); 49 | -------------------------------------------------------------------------------- /docs/samples/routing/build.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs-extra'); 3 | const fable = require('fable-compiler'); 4 | 5 | const BUILD_DIR = "public" 6 | const JS_DIR = "js" 7 | const DEST_FILE = "bundle.js" 8 | const PKG_JSON = "package.json" 9 | const PROJ_FILE = "sample.fsx" 10 | 11 | const fableconfig = { 12 | "babelPlugins": [ "transform-runtime" ], 13 | "projFile": PROJ_FILE, 14 | "rollup": { 15 | "dest": path.join(BUILD_DIR, JS_DIR, DEST_FILE), 16 | "external": ["virtual-dom"], 17 | "globals": { 18 | "virtualDom": "virtual-dom" 19 | } 20 | } 21 | }; 22 | 23 | const fableconfigDev = 24 | Object.assign({ 25 | //"sourceMaps": true, 26 | "watch": true, 27 | "symbols": "DEV" 28 | }, fableconfig) 29 | 30 | const targets = { 31 | clean() { 32 | return fable.promisify(fs.remove, path.join(BUILD_DIR, JS_DIR)) 33 | }, 34 | build() { 35 | return this.clean() 36 | .then(_ => fable.compile(fableconfig)) 37 | }, 38 | dev() { 39 | return this.clean() 40 | .then(_ => fable.compile(fableconfigDev)) 41 | } 42 | } 43 | 44 | // As with FAKE scripts, run a default target if no one is specified 45 | targets[process.argv[2] || "build"]().catch(err => { 46 | console.log(err); 47 | process.exit(-1); 48 | }); 49 | -------------------------------------------------------------------------------- /docs/samples/subscriber/build.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs-extra'); 3 | const fable = require('fable-compiler'); 4 | 5 | const BUILD_DIR = "public" 6 | const JS_DIR = "js" 7 | const DEST_FILE = "bundle.js" 8 | const PKG_JSON = "package.json" 9 | const PROJ_FILE = "sample.fsx" 10 | 11 | const fableconfig = { 12 | "babelPlugins": [ "transform-runtime" ], 13 | "projFile": PROJ_FILE, 14 | "rollup": { 15 | "dest": path.join(BUILD_DIR, JS_DIR, DEST_FILE), 16 | "external": ["virtual-dom"], 17 | "globals": { 18 | "virtualDom": "virtual-dom" 19 | } 20 | } 21 | }; 22 | 23 | const fableconfigDev = 24 | Object.assign({ 25 | "sourceMaps": true, 26 | "watch": true, 27 | "symbols": "DEV" 28 | }, fableconfig) 29 | 30 | const targets = { 31 | clean() { 32 | return fable.promisify(fs.remove, path.join(BUILD_DIR, JS_DIR)) 33 | }, 34 | build() { 35 | return this.clean() 36 | .then(_ => fable.compile(fableconfig)) 37 | }, 38 | dev() { 39 | return this.clean() 40 | .then(_ => fable.compile(fableconfigDev)) 41 | } 42 | } 43 | 44 | // As with FAKE scripts, run a default target if no one is specified 45 | targets[process.argv[2] || "build"]().catch(err => { 46 | console.log(err); 47 | process.exit(-1); 48 | }); 49 | -------------------------------------------------------------------------------- /docs/samples/template/build.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs-extra'); 3 | const fable = require('fable-compiler'); 4 | 5 | const BUILD_DIR = "public" 6 | const JS_DIR = "js" 7 | const DEST_FILE = "bundle.js" 8 | const PKG_JSON = "package.json" 9 | const PROJ_FILE = "sample.fsx" 10 | 11 | const fableconfig = { 12 | "babelPlugins": [ "transform-runtime" ], 13 | "projFile": PROJ_FILE, 14 | "rollup": { 15 | "dest": path.join(BUILD_DIR, JS_DIR, DEST_FILE), 16 | "external": ["virtual-dom"], 17 | "globals": { 18 | "virtualDom": "virtual-dom" 19 | } 20 | } 21 | }; 22 | 23 | const fableconfigDev = 24 | Object.assign({ 25 | "sourceMaps": true, 26 | "watch": true, 27 | "symbols": "DEV" 28 | }, fableconfig) 29 | 30 | const targets = { 31 | clean() { 32 | return fable.promisify(fs.remove, path.join(BUILD_DIR, JS_DIR)) 33 | }, 34 | build() { 35 | return this.clean() 36 | .then(_ => fable.compile(fableconfig)) 37 | }, 38 | dev() { 39 | return this.clean() 40 | .then(_ => fable.compile(fableconfigDev)) 41 | } 42 | } 43 | 44 | // As with FAKE scripts, run a default target if no one is specified 45 | targets[process.argv[2] || "build"]().catch(err => { 46 | console.log(err); 47 | process.exit(-1); 48 | }); 49 | -------------------------------------------------------------------------------- /docs/samples/nestedcounter/build.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs-extra'); 3 | const fable = require('fable-compiler'); 4 | 5 | const BUILD_DIR = "public" 6 | const JS_DIR = "js" 7 | const DEST_FILE = "bundle.js" 8 | const PKG_JSON = "package.json" 9 | const PROJ_FILE = "sample.fsx" 10 | 11 | const fableconfig = { 12 | "babelPlugins": [ "transform-runtime" ], 13 | "projFile": PROJ_FILE, 14 | "rollup": { 15 | "dest": path.join(BUILD_DIR, JS_DIR, DEST_FILE), 16 | "external": ["virtual-dom"], 17 | "globals": { 18 | "virtualDom": "virtual-dom" 19 | } 20 | } 21 | }; 22 | 23 | const fableconfigDev = 24 | Object.assign({ 25 | "sourceMaps": true, 26 | "watch": true, 27 | "symbols": "DEV" 28 | }, fableconfig) 29 | 30 | const targets = { 31 | clean() { 32 | return fable.promisify(fs.remove, path.join(BUILD_DIR, JS_DIR)) 33 | }, 34 | build() { 35 | return this.clean() 36 | .then(_ => fable.compile(fableconfig)) 37 | }, 38 | dev() { 39 | return this.clean() 40 | .then(_ => fable.compile(fableconfigDev)) 41 | } 42 | } 43 | 44 | // As with FAKE scripts, run a default target if no one is specified 45 | targets[process.argv[2] || "build"]().catch(err => { 46 | console.log(err); 47 | process.exit(-1); 48 | }); 49 | -------------------------------------------------------------------------------- /docs/samples/reactCounter/build.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs-extra'); 3 | const fable = require('fable-compiler'); 4 | 5 | const BUILD_DIR = "public" 6 | const JS_DIR = "js" 7 | const DEST_FILE = "bundle.js" 8 | const PKG_JSON = "package.json" 9 | const PROJ_FILE = "sample.fsx" 10 | 11 | const fableconfig = { 12 | "babelPlugins": [ "transform-runtime" ], 13 | "projFile": PROJ_FILE, 14 | "rollup": { 15 | "dest": path.join(BUILD_DIR, JS_DIR, DEST_FILE), 16 | "external": ["react", "react-dom"], 17 | "globals": { 18 | "react": "React", 19 | "react-dom": "ReactDOM" 20 | } 21 | } 22 | }; 23 | 24 | const fableconfigDev = 25 | Object.assign({ 26 | "sourceMaps": true, 27 | "watch": true, 28 | "symbols": "DEV" 29 | }, fableconfig) 30 | 31 | const targets = { 32 | clean() { 33 | return fable.promisify(fs.remove, path.join(BUILD_DIR, JS_DIR)) 34 | }, 35 | build() { 36 | return this.clean() 37 | .then(_ => fable.compile(fableconfig)) 38 | }, 39 | dev() { 40 | return this.clean() 41 | .then(_ => fable.compile(fableconfigDev)) 42 | } 43 | } 44 | 45 | // As with FAKE scripts, run a default target if no one is specified 46 | targets[process.argv[2] || "build"]().catch(err => { 47 | console.log(err); 48 | process.exit(-1); 49 | }); 50 | -------------------------------------------------------------------------------- /docs/src/Pages/Index.fs: -------------------------------------------------------------------------------- 1 | namespace WebApp.Pages 2 | 3 | open Fable.Import 4 | open Fable.Arch 5 | open Fable.Arch.Html 6 | 7 | module Index = 8 | 9 | let markdownText = 10 | " 11 | # Fable-arch samples 12 | 13 | Fable-arch is a set of tools for building modern web applications inspired by the [elm architecture](http://guide.elm-lang.org/architecture/index.html). 14 | 15 | Fable-arch use [Fable](http://fable.io/) which allow you to write your code using F# and compile in JavaScript. 16 | 17 | It is implemented around a set of abstractions which makes it possible to implement custom renderers if there is a need. 18 | Fable-arch comes with a HTML Dsl and a renderer built on top of [virtual-dom](https://github.com/Matt-Esch/virtual-dom) and all 19 | the samples here are using those two tools. 20 | Hopefully the samples here show you how to get started and gives you some inspiration about how to build your application using Fable-arch. 21 | 22 | You can also contribute more examples by sending us a pull request. 23 | " 24 | 25 | let view () = 26 | div 27 | [ classy "section" ] 28 | [ div 29 | [ classy "container" ] 30 | [ div 31 | [ classy "content" 32 | property "innerHTML" (Marked.Globals.marked.parse(markdownText)) 33 | ] 34 | [] 35 | ] 36 | ] 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fable-Arch 2 | 3 | No more maintained. Please consider using [Elmish](https://elmish.github.io/elmish/) which offer the same architecture. 4 | ===== 5 | 6 | Framework for building applications based on the elm architecture. [Docs site](http://fable.io/fable-arch/) 7 | 8 | ## Installation 9 | 10 | ```sh 11 | $ npm install --save virtual-dom fable-core 12 | $ npm install --save-dev fable-arch 13 | ``` 14 | 15 | ## Usage 16 | 17 | ### In an F# project (.fsproj) 18 | 19 | ```xml 20 | 21 | 22 | 23 | 24 | node_modules\fable-arch\Fable.Arch.dll 25 | 26 | ``` 27 | 28 | ### In an F# script (.fsx) 29 | 30 | ```fsharp 31 | #r "node_modules/fable-core/Fable.Core.dll" 32 | #r "node_modules/fable-arch/Fable.Arch.dll" 33 | 34 | open Fable.Core 35 | open Fable.Import 36 | open Fable.Arch 37 | open Fable.Arch.App 38 | open Fable.Arch.App.AppApi 39 | open Fable.Arch.Html 40 | ``` 41 | 42 | ### Rollup 43 | 44 | If you are using Rollup as the bundler you need to tell it how to bundle virtual-dom. 45 | 46 | Example: 47 | 48 | ```json 49 | "rollup": { 50 | "dest": "public/bundle.js", 51 | "plugins": { 52 | "commonjs": { 53 | "namedExports": { 54 | "virtual-dom": [ "h", "create", "diff", "patch" ] 55 | } 56 | } 57 | } 58 | } 59 | ``` 60 | -------------------------------------------------------------------------------- /starter-kit/hmr/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fable-arch-hmr-starter-kit", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "fable-build": "node_modules/.bin/fable", 8 | "fable-watch": "npm run fable-build -- -w --symbols DEV_HMR", 9 | "wp-server": "node server.js", 10 | "dev": "node_modules/.bin/concurrently \"npm run fable-watch\" \"npm run wp-server\"", 11 | "build": "rimraf public && \"node_modules/.bin/webpack\"" 12 | }, 13 | "author": "Maxime Mangel ", 14 | "license": "ISC", 15 | "devDependencies": { 16 | "autoprefixer": "^6.4.0", 17 | "autoprefixer-stylus": "^0.9.4", 18 | "concurrently": "^2.2.0", 19 | "copy-webpack-plugin": "^3.0.1", 20 | "css-loader": "^0.23.1", 21 | "fable-compiler": "^0.5.6", 22 | "fable-import-virtualdom": "^0.6.7", 23 | "file-loader": "^0.9.0", 24 | "html-webpack-plugin": "^2.22.0", 25 | "postcss-loader": "^0.10.1", 26 | "poststylus": "^0.2.3", 27 | "source-map-loader": "^0.1.5", 28 | "style-loader": "^0.13.1", 29 | "stylus": "^0.54.5", 30 | "stylus-loader": "^2.3.1", 31 | "webpack": "^1.13.2", 32 | "webpack-dev-server": "^1.15.0", 33 | "webpack-merge": "^0.14.1", 34 | "rimraf": "^2.5.4", 35 | "extract-text-webpack-plugin": "^1.0.1" 36 | }, 37 | "dependencies": { 38 | "core-js": "^2.4.1", 39 | "fable-core": "^0.5.4", 40 | "virtual-dom": "^2.1.1" 41 | }, 42 | "engines": { 43 | "fable": "^0.5.6" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Fable.Arch/Fable.Arch.React.fs: -------------------------------------------------------------------------------- 1 | module Fable.Arch.React 2 | 3 | open Fable.Arch.App 4 | open Fable.Core 5 | open Fable.Core.JsInterop 6 | open Fable.Import.React 7 | 8 | open System.Diagnostics 9 | 10 | 11 | type MkView<'model> = ('model->unit) -> ('model->ReactElement) 12 | type [] Props<'model> = { 13 | main:MkView<'model> 14 | } 15 | 16 | module Components = 17 | let mutable internal mounted = false 18 | 19 | type App<'model>(props:Props<'model>) as this = 20 | inherit Component,obj>(props) 21 | do 22 | mounted <- false 23 | 24 | let safeState state = 25 | match mounted with 26 | | false -> this.setInitState state 27 | | _ -> this.setState state 28 | let view = props.main safeState 29 | member this.componentDidMount() = 30 | mounted <- true 31 | 32 | member this.render () = 33 | view(unbox<'model> this.state) 34 | 35 | let createRenderer viewFn initModel sel h v = 36 | let mutable setState = None 37 | let main s = 38 | setState <- Some s 39 | s initModel 40 | fun model -> viewFn model h 41 | 42 | let targetNode = 43 | match sel with 44 | | Query selector -> Fable.Import.Browser.document.body.querySelector(selector) :?> Fable.Import.Browser.HTMLElement 45 | | Node node -> node 46 | 47 | let comp = Fable.Helpers.React.com,_,_> {main = main} [] 48 | Fable.Import.ReactDom.render(comp,targetNode) 49 | 50 | fun hand vm -> 51 | (setState |> Option.get) vm -------------------------------------------------------------------------------- /docs/samples/navigation/README.md: -------------------------------------------------------------------------------- 1 | # Navigation sample 2 | 3 | This sample will reproduce the [counter sample](#/sample/counter?height=300) with support for Navigation. 4 | 5 | ## Usage 6 | 7 | In order to see the navigation in the address bar you need to click on "Open in tab". 8 | 9 | ## Helpers 10 | 11 | First we will need to write 3 functions. 12 | 13 | The first named `toUrl` convert the `Model` into a string (url). 14 | 15 | ```fsharp 16 | let toUrl count = sprintf "#/%i" count 17 | ``` 18 | 19 | The second named `urlParser` take the location value and extract the `Model` state from it. 20 | 21 | ```fsharp 22 | let urlParser location = 23 | location.Hash |> fromUrl 24 | ``` 25 | 26 | The third named `navigationUpdate` will be used by the Navigation module to reflect the url change into the model. 27 | 28 | ```fsharp 29 | let navigationUpdate model urlValue = 30 | { model with Value = urlValue }, [] 31 | ``` 32 | 33 | ## Update 34 | 35 | ```fsharp 36 | let update model action = 37 | match action with 38 | // Increment by 1 39 | | Increment -> 40 | { model with Value = model.Value + 1 } 41 | // Decrement by 1 42 | | Decrement -> 43 | { model with Value = model.Value - 1 } 44 | // Set the counter Value to 0 45 | | Reset -> 46 | { model with Value = 0 } 47 | // We return the new value and push a new state in the Navigation history 48 | |> (fun m -> m, [fun _ -> Navigation.pushState (toUrl m.Value)]) 49 | ``` 50 | 51 | ## Attach Navigation module 52 | 53 | We attach the navigation module by passing the `urlParser` and `navigationUpdate`. 54 | 55 | ```fsharp 56 | createApp Model.Initial view update Virtualdom.createRender 57 | |> withStartNodeSelector "#sample" 58 | |> withNavigation urlParser navigationUpdate 59 | |> start 60 | ``` -------------------------------------------------------------------------------- /docs/samples/subscriber/sample.fsx: -------------------------------------------------------------------------------- 1 | // If you are using the sample in standalone please switch the import lines 2 | // #r "node_modules/fable-core/Fable.Core.dll" 3 | // #r "node_modules/fable-arch/Fable.Arch.dll" 4 | // Imports for docs site mode 5 | #r "../../node_modules/fable-core/Fable.Core.dll" 6 | #r "../../node_modules/fable-arch/Fable.Arch.dll" 7 | 8 | open Fable.Arch 9 | open Fable.Arch.App 10 | open Fable.Arch.App.AppApi 11 | open Fable.Arch.Html 12 | open Fable.Core.JsInterop 13 | 14 | module VDom = 15 | let onInput x = onEvent "oninput" (fun e -> x (unbox e?target?value)) 16 | 17 | type Model = string 18 | 19 | type Actions = 20 | | ChangeStr of string 21 | 22 | let update model action = 23 | match action with 24 | | ChangeStr str -> str 25 | 26 | let view (model: Model) = 27 | div 28 | [ classy "columns is-flex-mobile" ] 29 | [ div [ classy "column" ] [] 30 | div 31 | [ classy "column is-narrow is-narrow-mobile" 32 | Style [ "width", "400px" ] 33 | ] 34 | [ label 35 | [ classy "label" ] 36 | [ text "Enter your name:" ] 37 | p 38 | [ classy "control" ] 39 | [ input 40 | [ classy "input" 41 | property "type" "text" 42 | property "placeholder" "Ex: Joe Doe" 43 | property "value" model 44 | VDom.onInput ChangeStr 45 | ] 46 | ] 47 | span 48 | [] 49 | [ text (sprintf "Hello %s" model) ] 50 | ] 51 | div [ classy "column" ] [] 52 | ] 53 | 54 | createSimpleApp "" view update Virtualdom.createRender 55 | |> withStartNodeSelector "#sample" 56 | |> withSubscriber (fun x -> Fable.Import.Browser.console.log("Event received: ", x)) 57 | |> start 58 | -------------------------------------------------------------------------------- /docs/WebApp.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.25420.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "project", "project", "{BF60BC93-E09B-4E5F-9D85-95A519479D54}" 7 | ProjectSection(SolutionItems) = preProject 8 | build.js = build.js 9 | index.html = index.html 10 | package.json = package.json 11 | EndProjectSection 12 | EndProject 13 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "public", "public", "{BDC2CBB4-EF50-4A9A-A738-0B2A82A621A3}" 14 | ProjectSection(SolutionItems) = preProject 15 | public\css\style.css = public\css\style.css 16 | EndProjectSection 17 | EndProject 18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scss", "scss", "{DF9E3093-65E8-4EB5-8870-21D1F3B554B8}" 19 | ProjectSection(SolutionItems) = preProject 20 | scss\main.sass = scss\main.sass 21 | EndProjectSection 22 | EndProject 23 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "WebApp", "src\WebApp.fsproj", "{861B39F3-785E-45AB-90B4-3CC720C1EC0B}" 24 | EndProject 25 | Global 26 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 27 | Debug|Any CPU = Debug|Any CPU 28 | Release|Any CPU = Release|Any CPU 29 | EndGlobalSection 30 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 31 | {861B39F3-785E-45AB-90B4-3CC720C1EC0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {861B39F3-785E-45AB-90B4-3CC720C1EC0B}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {861B39F3-785E-45AB-90B4-3CC720C1EC0B}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {861B39F3-785E-45AB-90B4-3CC720C1EC0B}.Release|Any CPU.Build.0 = Release|Any CPU 35 | EndGlobalSection 36 | GlobalSection(SolutionProperties) = preSolution 37 | HideSolutionNode = FALSE 38 | EndGlobalSection 39 | EndGlobal 40 | -------------------------------------------------------------------------------- /docs/samples/hello/README.md: -------------------------------------------------------------------------------- 1 | # Hello sample 2 | 3 | This sample is very basic in order to show you the basic structure of an application. 4 | 5 | In this sample, we want to ask the user to enter a name and then say hello to him. 6 | 7 | ## Model 8 | 9 | The first thing we defined is the model. This is used to store all the information used by our application. 10 | 11 | In our case, we simply defined a string alias 12 | 13 | ```fsharp 14 | type Model = string 15 | ``` 16 | 17 | ## Actions 18 | 19 | The actions are used to describe what can happen in your application. 20 | 21 | In our case, we have only one action taking a string as a value 22 | 23 | ```fsharp 24 | type Actions = 25 | | ChangeStr of string 26 | ``` 27 | 28 | ## Update 29 | 30 | The update function is where we handle all the logic of our application. 31 | 32 | In our case, we update the model with the new value transported by the `ChangeInput` action. 33 | 34 | ```fsharp 35 | let update model action = 36 | match action with 37 | | ChangeStr str -> str 38 | ``` 39 | 40 | ## View 41 | 42 | The view function is where we set up the VirtualDom used by the display. 43 | 44 | ```fsharp 45 | let view (model: Model) = 46 | div 47 | [] 48 | [ label 49 | [ classy "label" ] 50 | [ text "Enter your name:" ] 51 | p 52 | [ classy "control" ] 53 | [ input 54 | [ classy "input" 55 | property "type" "text" 56 | property "placeholder" "Ex: Joe Doe" 57 | property "value" model 58 | VDom.onInput ChangeStr 59 | ] 60 | ] 61 | span 62 | [] 63 | [ text (sprintf "Hello %s" model) ] 64 | ] 65 | ``` 66 | 67 | The function `VDom.onInput` is a simple helper to access the value from the onInput event. Below is its definition. 68 | 69 | ```fsharp 70 | module VDom = 71 | let onInput x = onEvent "oninput" (fun e -> x (unbox e?target?value)) 72 | ``` 73 | -------------------------------------------------------------------------------- /docs/samples/counter/README.md: -------------------------------------------------------------------------------- 1 | # Counter sample 2 | 3 | This sample will show you how to create a counter. 4 | 5 | ## Model 6 | 7 | The model will be a simple record holding a value. 8 | 9 | ```fsharp 10 | type Model = 11 | { Value: int 12 | } 13 | 14 | /// Static member giving back an init Model 15 | static member Initial = 16 | { Value = 0 } 17 | ``` 18 | 19 | ## Actions 20 | 21 | There are three actions for the counter: 22 | * Add 1 to the current value. 23 | * Substract 1 to the current value. 24 | * Reset the value to 0. 25 | 26 | ```fsharp 27 | type Actions = 28 | | Add 29 | | Sub 30 | | Reset 31 | ``` 32 | 33 | ## Update 34 | 35 | ```fsharp 36 | let update model action = 37 | match action with 38 | // Add 1 to the counter Value 39 | | Add -> 40 | { model with Value = model.Value + 1 } 41 | // Substract 1 to the counter Value 42 | | Sub -> 43 | { model with Value = model.Value - 1 } 44 | // Set the counter Value to 0 45 | | Reset -> 46 | { model with Value = 0 } 47 | ``` 48 | 49 | ## View 50 | 51 | For the view, we create an helper function `simpleButton` in order to make the final view cleaner and avoid to repeat ourself. 52 | 53 | ```fsharp 54 | let voidLinkAction<'T> : Attribute<'T> = property "href" "javascript:void(0)" 55 | let simpleButton txt action = 56 | div 57 | [ classy "column is-narrow" ] 58 | [ a 59 | [ classy "button" 60 | voidLinkAction 61 | onMouseClick(fun _ -> 62 | action 63 | ) 64 | ] 65 | [ text txt ] 66 | ] 67 | 68 | let view model = 69 | div 70 | [ classy "columns is-vcentered" ] 71 | [ 72 | div 73 | [ classy "column is-narrow" 74 | Style [ "width", "170px" ] 75 | ] 76 | [ text (sprintf "Counter value: %i" model.Value) ] 77 | simpleButton "+1" Add 78 | simpleButton "-1" Sub 79 | simpleButton "Reset" Reset 80 | ] 81 | ``` 82 | -------------------------------------------------------------------------------- /starter-kit/hmr/Source/Main.fsx: -------------------------------------------------------------------------------- 1 | #r "../node_modules/fable-core/Fable.Core.dll" 2 | #load "../node_modules/fable-import-virtualdom/Fable.Helpers.Virtualdom.fs" 3 | #load "Helpers.fsx" 4 | 5 | namespace App 6 | 7 | open Fable.Core 8 | open Fable.Core.JsInterop 9 | open Fable.Import 10 | open Fable.Import.Browser 11 | 12 | open Fable.Helpers.Virtualdom 13 | open Fable.Helpers.Virtualdom.App 14 | open Fable.Helpers.Virtualdom.Html 15 | 16 | open System 17 | open Helpers 18 | 19 | module Main = 20 | 21 | type Model = 22 | { Input: string } 23 | 24 | static member initial = 25 | #if DEV_HMR 26 | // This section is used to maintain state between HMR 27 | if isNotNull (unbox window?storage) then 28 | unbox window?storage 29 | else 30 | let model = { Input = "" } 31 | window?storage <- model 32 | model 33 | #else 34 | { Input = "" } 35 | #endif 36 | 37 | // Actions supported by the application 38 | type Actions = 39 | | ChangeInput of string 40 | 41 | let update (model: Model) action = 42 | let model', action' = 43 | match action with 44 | | ChangeInput s -> 45 | { model with Input = s } , [] 46 | 47 | #if DEV_HMR 48 | // Update the model in storage 49 | window?storage <- model' 50 | #endif 51 | 52 | model', action' 53 | 54 | /// Custom binding for inInput. This directly give the value 55 | let inline onInput x = onEvent "oninput" (fun e -> x (unbox e?target?value)) 56 | /// View 57 | let view model = 58 | div 59 | [] 60 | [ 61 | label 62 | [] 63 | [text "Enter name: "] 64 | input 65 | [ onInput ChangeInput ] 66 | br [] 67 | span 68 | [] 69 | [text (sprintf "Hello %s" model.Input)] 70 | ] 71 | 72 | let start node () = 73 | createApp Model.initial view update 74 | |> withStartNodeSelector node 75 | |> start renderer 76 | -------------------------------------------------------------------------------- /docs/samples/hello/sample.fsx: -------------------------------------------------------------------------------- 1 | // If you are using the sample in standalone please switch the import lines 2 | // #r "node_modules/fable-core/Fable.Core.dll" 3 | // #r "node_modules/fable-arch/Fable.Arch.dll" 4 | // Imports for docs site mode 5 | #r "../../node_modules/fable-core/Fable.Core.dll" 6 | #r "../../node_modules/fable-arch/Fable.Arch.dll" 7 | 8 | open Fable.Arch 9 | open Fable.Arch.App 10 | open Fable.Arch.App.AppApi 11 | open Fable.Arch.Html 12 | open Fable.Core.JsInterop 13 | 14 | module VDom = 15 | let onInput x = onEvent "oninput" (fun e -> x (unbox e?target?value)) 16 | 17 | type Model = string 18 | 19 | type Actions = 20 | | ChangeStr of string 21 | 22 | let update model action = 23 | match action with 24 | | ChangeStr str -> str 25 | 26 | let view (model: Model) = 27 | div 28 | [ classy "columns is-flex-mobile" ] 29 | [ div [ classy "column" ] [] 30 | div 31 | [ classy "column is-narrow is-narrow-mobile" 32 | Style [ "width", "400px" ] 33 | ] 34 | [ label 35 | [ classy "label" ] 36 | [ text "Enter your name:" ] 37 | p 38 | [ classy "control" ] 39 | [ input 40 | [ classy "input" 41 | property "type" "text" 42 | property "placeholder" "Ex: Joe Doe" 43 | property "value" model 44 | VDom.onInput ChangeStr 45 | ] 46 | ] 47 | span 48 | [] 49 | [ text (sprintf "Hello %s" model) ] 50 | ] 51 | div [ classy "column" ] [] 52 | ] 53 | 54 | // Using createSimpleApp instead of createApp since our 55 | // update function doesn't generate any actions. See 56 | // some of the other more advanced examples for how to 57 | // use createApp. In addition to the application functions 58 | // we also need to specify which renderer to use. 59 | createSimpleApp "" view update Virtualdom.createRender 60 | |> withStartNodeSelector "#sample" 61 | |> start 62 | -------------------------------------------------------------------------------- /docs/samples/counter/sample.fsx: -------------------------------------------------------------------------------- 1 | // If you are using the sample in standalone please switch the import lines 2 | // #r "node_modules/fable-core/Fable.Core.dll" 3 | // #r "node_modules/fable-arch/Fable.Arch.dll" 4 | // Imports for docs site mode 5 | #r "../../node_modules/fable-core/Fable.Core.dll" 6 | #r "../../node_modules/fable-arch/Fable.Arch.dll" 7 | 8 | open Fable.Core 9 | open Fable.Import 10 | open Fable.Import.Browser 11 | 12 | open Fable.Arch 13 | open Fable.Arch.App 14 | open Fable.Arch.Html 15 | 16 | open System 17 | 18 | type Actions = 19 | | Add 20 | | Sub 21 | | Reset 22 | 23 | type Model = 24 | { Value: int 25 | } 26 | 27 | /// Static member giving back an init Model 28 | static member Initial = 29 | { Value = 0 } 30 | 31 | let update model action = 32 | match action with 33 | // Add 1 to the counter Value 34 | | Add -> 35 | { model with Value = model.Value + 1 } 36 | // Substract 1 to the counter Value 37 | | Sub -> 38 | { model with Value = model.Value - 1 } 39 | // Set the counter Value to 0 40 | | Reset -> 41 | { model with Value = 0 } 42 | 43 | let voidLinkAction<'T> : Attribute<'T> = property "href" "javascript:void(0)" 44 | let simpleButton txt action = 45 | div 46 | [ classy "column is-narrow is-narrow-mobile" ] 47 | [ a 48 | [ classy "button" 49 | voidLinkAction 50 | onMouseClick(fun _ -> 51 | action 52 | ) 53 | ] 54 | [ text txt ] 55 | ] 56 | 57 | let view model = 58 | div 59 | [ classy "columns is-vcentered is-flex-mobile" ] 60 | [ // We add a column at the beginning and the end to force center of the view 61 | div [ classy "column" ] [] 62 | div 63 | [ classy "column is-narrow is-narrow-mobile" 64 | Style [ "width", "170px" ] 65 | ] 66 | [ text (sprintf "Counter value: %i" model.Value) ] 67 | simpleButton "+1" Add 68 | simpleButton "-1" Sub 69 | simpleButton "Reset" Reset 70 | div [ classy "column" ] [] 71 | ] 72 | 73 | 74 | createSimpleApp Model.Initial view update Virtualdom.createRender 75 | |> withStartNodeSelector "#sample" 76 | |> start -------------------------------------------------------------------------------- /docs/samples/reactCounter/sample.fsx: -------------------------------------------------------------------------------- 1 | // If you are using the sample in standalone please switch the import lines 2 | // #r "node_modules/fable-core/Fable.Core.dll" 3 | // #r "node_modules/fable-arch/Fable.Arch.dll" 4 | // Imports for docs site mode 5 | #r "../../node_modules/fable-core/Fable.Core.dll" 6 | #r "../../node_modules/fable-arch/Fable.Arch.dll" 7 | #r "../../node_modules/fable-react/Fable.React.dll" 8 | 9 | open Fable.Arch 10 | open Fable.Arch.App 11 | open Fable.Arch.App.AppApi 12 | open Fable.Arch.React 13 | open Fable.Core 14 | open Fable.Helpers.React 15 | open Fable.Helpers.React.Props 16 | 17 | type Actions = 18 | | Add 19 | | Sub 20 | | Reset 21 | 22 | type Model = 23 | { Value: int 24 | } 25 | 26 | /// Static member giving back an init Model 27 | static member Initial = 28 | { Value = 0 } 29 | 30 | let update model action = 31 | match action with 32 | // Add 1 to the counter Value 33 | | Add -> 34 | { model with Value = model.Value + 1 } 35 | // Substract 1 to the counter Value 36 | | Sub -> 37 | { model with Value = model.Value - 1 } 38 | // Set the counter Value to 0 39 | | Reset -> 40 | { model with Value = 0 } 41 | 42 | //let voidLinkAction<'T> : Attribute<'T> = property "href" "javascript:void(0)" 43 | let simpleButton txt action dispatch = 44 | let onClick msg = 45 | OnClick <| fun _ -> msg |> dispatch 46 | 47 | div 48 | [ ClassName "column is-narrow is-narrow-mobile" ] 49 | [ a 50 | [ ClassName "button" 51 | onClick action 52 | ] 53 | [ unbox txt ] 54 | ] 55 | 56 | let view model dispatch = 57 | div 58 | [ ClassName "columns is-vcentered is-flex-mobile" ] 59 | [ // We add a column at the beginning and the end to force center of the view 60 | div [ ClassName "column" ] [] 61 | div 62 | [ ClassName "column is-narrow is-narrow-mobile" 63 | Width (Case2 "170px") 64 | ] 65 | [ unbox (sprintf "Counter value: %i" model.Value) ] 66 | simpleButton "+1" Add dispatch 67 | simpleButton "-1" Sub dispatch 68 | simpleButton "Reset" Reset dispatch 69 | div [ ClassName "column" ] [] 70 | ] 71 | 72 | let createReactApp initModel view update = 73 | let renderer = createRenderer view initModel 74 | createSimpleApp initModel id update renderer 75 | 76 | createReactApp Model.Initial view update 77 | |> withStartNodeSelector "#sample" 78 | |> start 79 | -------------------------------------------------------------------------------- /docs/samples/clock/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Fable-arch - F# |> Babel 7 | 8 | 9 | 10 | 11 | 12 | 13 | 42 |
43 |
44 |
45 |
46 |
47 |

Clock

48 |

49 | A clock showing producer usage. 50 |

51 |
52 |
53 |
54 |
55 |
56 | 57 |

58 | 59 |
60 |
61 |
62 |
63 |
64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /docs/samples/template/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Fable-arch - F# |> Babel 7 | 8 | 9 | 10 | 11 | 12 | 13 | 42 |
43 |
44 |
45 |
46 |
47 |

Application title

48 |

49 | Short description 50 |

51 |
52 |
53 |
54 |
55 |
56 | 57 |

58 | 59 |
60 |
61 |
62 |
63 |
64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /docs/samples/hello/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Fable-arch - F# |> Babel 7 | 8 | 9 | 10 | 11 | 12 | 13 | 42 |
43 |
44 |
45 |
46 |
47 |

Hello

48 |

49 | A simple application showing inputs usage. 50 |

51 |
52 |
53 |
54 |
55 |
56 | 57 |

58 | 59 |
60 |
61 |
62 |
63 |
64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /docs/samples/echo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Fable-arch - F# |> Babel 7 | 8 | 9 | 10 | 11 | 12 | 13 | 42 |
43 |
44 |
45 |
46 |
47 |

Echo

48 |

49 | An echo application showing how to make ajax calls 50 |

51 |
52 |
53 |
54 |
55 |
56 | 57 |

58 | 59 |
60 |
61 |
62 |
63 |
64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /src/Fable.Arch/RouteParserTesting.fsx: -------------------------------------------------------------------------------- 1 | #r "./node_modules/fable-core/Fable.Core.dll" 2 | #load "./Fable.Arch.RouteParser.fs" 3 | 4 | module RouteParserTesting = 5 | open Fable.Arch.RouteParser.Parsing 6 | open Fable.Arch.RouteParser.RouteParser 7 | 8 | open System 9 | type Route = 10 | | Home 11 | | HomeStr of string 12 | | HomeStrInt of string*int 13 | | Home1 of Guid 14 | | Home2 of Guid*int 15 | | Home3 of Guid*int*int 16 | | Home4 of Guid*int*int*int 17 | | Home5 of Guid*int*int*int*int 18 | | User of Guid*int*float 19 | | Admin of Guid 20 | | NotFound of string 21 | let routes = [ 22 | runM Home (pStaticStr "home" |> (drop >> _end)) 23 | runM2 HomeStrInt (pStringTo '/' .>>. pint |> _end) 24 | runM1 HomeStr (pStringTo '/' |> _end) 25 | runM1 Home1 (pStaticStr "home" pguid |> _end) 26 | runM2 Home2 (pStaticStr "home" pguid pint |> _end) 27 | runM3 Home3 (pStaticStr "home" pguid pint pint |> _end) 28 | runM4 Home4 (pStaticStr "home" pguid pint pint pint |> _end) 29 | runM5 Home5 (pStaticStr "home" pguid pint pint pint pint |> _end) 30 | runM3 User (pStaticStr "user" pguid pint <./> pStaticStr "yolo" pfloat |> _end) 31 | runM2 (fun (g,_) -> Admin g) (pStaticStr "admin" pguid pint |> _end) 32 | runM1 NotFound pString 33 | ] 34 | let run() = 35 | [ 36 | "home" 37 | "this is a random string/567" 38 | "this is random string with / at the end" 39 | "30FF82ab-2861-43f5-8b68-3b52e2b3ddbc/123121" 40 | "home/30FF82ab-2861-43f5-8b68-3b52e2b3ddbc" 41 | "home/30FF82ab-2861-43f5-8b68-3b52e2b3ddbc/123121" 42 | "home/30FF82ab-2861-43f5-8b68-3b52e2b3ddbc/123121/123" 43 | "home/30FF82ab-2861-43f5-8b68-3b52e2b3ddbc/123121/234/345" 44 | "home/30FF82ab-2861-43f5-8b68-3b52e2b3ddbc/123121/1/323/232" 45 | "home/30FF82ab-2861-43f5-8b68-3b52e2b3ddbc/123121/2342/234234/23432" 46 | "user/30ff82ab-2861-43f5-8b68-3b52e2b3ddbc/123121/yolo/34.23" 47 | "admin/30FF82ab-2861-43f5-8b68-3b52e2b3ddbc/123121" 48 | "30FF82ab-2861-43f5-8b68-3b52e2b3ddbc/123121xxxz" 49 | ] |> List.iter (fun i -> printfn "%A" (choose routes i)) 50 | 51 | RouteParserTesting.run() -------------------------------------------------------------------------------- /docs/samples/counter/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Fable-arch - F# |> Babel 7 | 8 | 9 | 10 | 11 | 12 | 13 | 42 |
43 |
44 |
45 |
46 |
47 |

Counter

48 |

49 | A simple application showing how to support multiple actions. 50 |

51 |
52 |
53 |
54 |
55 |
56 | 57 |

58 | 59 |
60 |
61 |
62 |
63 |
64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /docs/samples/routing/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Fable-arch - F# |> Babel 7 | 8 | 9 | 10 | 11 | 12 | 13 | 42 |
43 |
44 |
45 |
46 |
47 |

Routing

48 |

49 | Application showing you how to use the Router provided by Fable-arch 50 |

51 |
52 |
53 |
54 |
55 |
56 | 57 |

58 | 59 |
60 |
61 |
62 |
63 |
64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /docs/samples/navigation/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Fable-arch - F# |> Babel 7 | 8 | 9 | 10 | 11 | 12 | 13 | 42 |
43 |
44 |
45 |
46 |
47 |

Navigation

48 |

49 | This application show you how to use the navigation feature of Fable-Arch 50 |

51 |
52 |
53 |
54 |
55 |
56 | 57 |

58 | 59 |
60 |
61 |
62 |
63 |
64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /docs/samples/nestedcounter/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Fable-arch - F# |> Babel 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 44 |
45 |
46 |
47 |
48 |
49 |

Nested counter

50 |

51 | A application showing how to use nested application. 52 |

53 |
54 |
55 |
56 |
57 |
58 | 59 |

60 | 61 |
62 |
63 |
64 |
65 |
66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /docs/samples/clock/sample.fsx: -------------------------------------------------------------------------------- 1 | // If you are using the sample in standalone please switch the import lines 2 | // #r "node_modules/fable-core/Fable.Core.dll" 3 | // #r "node_modules/fable-arch/Fable.Arch.dll" 4 | // Imports for docs site mode 5 | #r "../../node_modules/fable-core/Fable.Core.dll" 6 | #r "../../node_modules/fable-arch/Fable.Arch.dll" 7 | 8 | open Fable.Core 9 | open Fable.Import 10 | open Fable.Import.Browser 11 | 12 | open Fable.Arch 13 | open Fable.Arch.Html 14 | open Fable.Arch.App 15 | 16 | open System 17 | 18 | module Clock = 19 | 20 | type Actions = 21 | | Tick of DateTime 22 | 23 | /// A really simple type to Store our ModelChanged 24 | type Model = 25 | { Time: string // Time: HH:mm:ss 26 | Date: string } // Date: YYYY/MM/DD 27 | 28 | /// Static member giving back an init Model 29 | static member Initial = 30 | { Time = "00:00:00" 31 | Date = "01/01/1970" } 32 | 33 | 34 | /// Make sure that number have a minimal representation of 2 digits 35 | let normalizeNumber x = 36 | if x < 10 then 37 | sprintf "0%i" x 38 | else 39 | string x 40 | 41 | let update model action = 42 | let model_, action_ = 43 | match action with 44 | /// Tick are push by the producer 45 | | Tick datetime -> 46 | // Normalize the day and month to ensure a 2 digit representation 47 | let day = datetime.Day |> normalizeNumber 48 | let month = datetime.Month |> normalizeNumber 49 | // Create our date string 50 | let date = sprintf "%s/%s/%i" month day datetime.Year 51 | { model with 52 | Time = String.Format("{0:HH:mm:ss}", datetime) 53 | Date = date }, [] 54 | model_, action_ 55 | 56 | let view model = 57 | div 58 | [ classy "content has-text-centered" ] 59 | [ h1 60 | [ classy "is-marginless" ] 61 | [ text (sprintf "%s %s" model.Date model.Time )] 62 | ] 63 | 64 | let tickProducer push = 65 | window.setInterval((fun _ -> 66 | push(Tick DateTime.Now) 67 | null 68 | ), 1000) |> ignore 69 | // Force the first to push to have immediate effect 70 | // If we don't do that there is one second before the first push 71 | // and the view is rendered with the Model.init values 72 | push(Tick DateTime.Now) 73 | 74 | 75 | createApp Model.Initial view update Virtualdom.createRender 76 | |> withStartNodeSelector "#sample" 77 | |> withProducer tickProducer 78 | |> start -------------------------------------------------------------------------------- /docs/samples/subscriber/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Fable-arch - F# |> Babel 7 | 8 | 9 | 10 | 11 | 12 | 13 | 42 |
43 |
44 |
45 |
46 |
47 |

Subscriber

48 |

49 | This application will show you how to register a subscriber to your application. 50 |

51 |
52 |
53 |
54 |
55 |
56 | 57 |

58 | 59 |
60 |
61 |
62 |
63 |
64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /docs/samples/clock/README.md: -------------------------------------------------------------------------------- 1 | # Clock sample 2 | 3 | This sample will show you how to use producers. 4 | A producer, is used to push a message into your application from the outside world. 5 | In this sample, the producer is used to push a `Tick` actions every seconds. 6 | 7 | ## Model 8 | 9 | The model will store two values: 10 | * The `Time` string representation 11 | * The `Date` string representation 12 | 13 | ```fsharp 14 | /// A really simple type to Store our ModelChanged 15 | type Model = 16 | { Time: string // Time: HH:mm:ss 17 | Date: string } // Date: YYYY/MM/DD 18 | 19 | /// Static member giving back an init Model 20 | static member Initial = 21 | { Time = "00:00:00" 22 | Date = "01/01/1970" } 23 | ``` 24 | 25 | 26 | ## Actions 27 | 28 | There is only one action defined. This action is a `Tick` which will transport a `DateTime` value. 29 | 30 | ```fsharp 31 | type Actions = 32 | | Tick of DateTime 33 | ``` 34 | 35 | ## Update 36 | 37 | ```fsharp 38 | /// Make sure that number have a minimal representation of 2 digits 39 | let normalizeNumber x = 40 | if x < 10 then 41 | sprintf "0%i" x 42 | else 43 | string x 44 | 45 | let update model action = 46 | let model_, action_ = 47 | match action with 48 | /// Tick are push by the producer 49 | | Tick datetime -> 50 | // Normalize the day and month to ensure a 2 digit representation 51 | let day = datetime.Day |> normalizeNumber 52 | let month = datetime.Month |> normalizeNumber 53 | // Create our date string 54 | let date = sprintf "%s/%s/%i" month day datetime.Year 55 | { model with 56 | Time = String.Format("{0:HH:mm:ss}", datetime) 57 | Date = date }, [] 58 | model_, action_ 59 | ``` 60 | 61 | ## Producer 62 | 63 | Here is the producer used to send a `Tick` actions every seconds into the application. 64 | 65 | ```fsharp 66 | let tickProducer push = 67 | window.setInterval((fun _ -> 68 | push(Tick DateTime.Now) 69 | null 70 | ), 1000) |> ignore 71 | // Force the first to push to have immediate effect 72 | // If we don't do that there is one second before the first push 73 | // and the view is rendered with the Model.init values 74 | push(Tick DateTime.Now) 75 | ``` 76 | 77 | To a register a producer in your application you need to call `withProducer`. 78 | 79 | ```fsharp 80 | createApp Model.Initial view update Virtualdom.createRender 81 | |> withStartNodeSelector "#sample" 82 | |> withProducer tickProducer 83 | |> start 84 | ``` 85 | -------------------------------------------------------------------------------- /docs/samples/reactCounter/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Fable-arch - F# |> Babel 7 | 8 | 9 | 10 | 11 | 12 | 13 | 42 |
43 |
44 |
45 |
46 |
47 |

React counter

48 |

49 | Port of the counter sample using React 50 |

51 |
52 |
53 |
54 |
55 |
56 | 57 |

58 | 59 |
60 |
61 |
62 |
63 |
64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /docs/build.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs-extra'); 3 | const fable = require('fable-compiler'); 4 | const child_process = require('child_process'); 5 | 6 | const BUILD_DIR = "public" 7 | const JS_DIR = "js" 8 | const DEST_FILE = "bundle.js" 9 | const PKG_JSON = "package.json" 10 | const README = "README.md" 11 | const RELEASE_NOTES = "RELEASE_NOTES.md" 12 | const PROJ_FILE = "src/WebApp.fsproj" 13 | const SAMPLES_DIR = "samples" 14 | const SAMPLES_TEMPLATE = path.join(SAMPLES_DIR, "template") 15 | const GEN_DIR = "build" 16 | 17 | const fableconfig = { 18 | "babelPlugins": [ "transform-runtime" ], 19 | "projFile": PROJ_FILE, 20 | "rollup": { 21 | "dest": path.join(BUILD_DIR, JS_DIR, DEST_FILE), 22 | "plugins": { 23 | "commonjs": { 24 | "namedExports": { 25 | "virtual-dom": [ "h", "create", "diff", "patch" ] 26 | } 27 | } 28 | } 29 | } 30 | }; 31 | 32 | const fableconfigDev = 33 | Object.assign({ 34 | "sourceMaps": true, 35 | "watch": true, 36 | "symbols": "DEV" 37 | }, fableconfig) 38 | 39 | const targets = { 40 | clean() { 41 | return fable.promisify(fs.remove, path.join(BUILD_DIR, JS_DIR)) 42 | }, 43 | build() { 44 | return this.clean() 45 | .then(_ => fable.compile(fableconfig)) 46 | }, 47 | dev() { 48 | return this.clean() 49 | .then(_ => fable.compile(fableconfigDev)) 50 | }, 51 | createSample() { 52 | const destName = process.argv[3] 53 | 54 | if (destName === undefined){ 55 | console.log("Missing destination name"); 56 | console.log("Command: node build.js createSample [destName]"); 57 | process.exit(-1); 58 | } else { 59 | const destPath = path.join(SAMPLES_DIR , destName); 60 | return fable.promisify(fs.copy, SAMPLES_TEMPLATE, destPath) 61 | .then(_ => console.log("Sample created at: " + destPath)) 62 | } 63 | }, 64 | buildAllSamples() { 65 | return fable.promisify(fs.readdir, SAMPLES_DIR, (err, files) => { 66 | 67 | files.forEach(file => { 68 | if (file !== "template" && file !== ".DS_Store") { 69 | const localDir = path.join(SAMPLES_DIR, file) 70 | console.log(localDir) 71 | child_process.execSync("node build.js", { 72 | cwd: localDir 73 | }); 74 | } 75 | }); 76 | }); 77 | } 78 | } 79 | 80 | // As with FAKE scripts, run a default target if no one is specified 81 | targets[process.argv[2] || "build"]().catch(err => { 82 | console.log(err); 83 | process.exit(-1); 84 | }); 85 | -------------------------------------------------------------------------------- /docs/samples/calculator/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Fable-arch - F# |> Babel 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 44 |
45 |
46 |
47 |
48 |
49 |

Calculator

50 |

51 | A calculator application 52 |

53 |
54 |
55 |
56 |
57 |
58 | 59 |

60 | 61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /docs/samples/navigation/sample.fsx: -------------------------------------------------------------------------------- 1 | // If you are using the sample in standalone please switch the import lines 2 | // #r "node_modules/fable-core/Fable.Core.dll" 3 | // #r "node_modules/fable-arch/Fable.Arch.dll" 4 | // Imports for docs site mode 5 | #r "../../node_modules/fable-core/Fable.Core.dll" 6 | #r "../../node_modules/fable-arch/Fable.Arch.dll" 7 | 8 | open Fable.Arch 9 | open Fable.Arch.App 10 | open Fable.Arch.App.AppApi 11 | open Fable.Arch.Html 12 | open Fable.Arch.Navigation 13 | 14 | 15 | // Url handling 16 | let toUrl count = sprintf "#/%i" count 17 | 18 | let urlParser location = 19 | int (location.Hash.Substring(2)) 20 | 21 | // Model 22 | type Model = 23 | { Value: int 24 | } 25 | 26 | /// Static member giving back an init Model 27 | static member Initial = 28 | { Value = 0 } 29 | 30 | let initValue = Location.getLocation() |> urlParser 31 | 32 | type Actions = 33 | | Increment 34 | | Decrement 35 | | Reset 36 | 37 | let update model action = 38 | match action with 39 | // Increment by 1 40 | | Increment -> 41 | { model with Value = model.Value + 1 } 42 | // Decrement by 1 43 | | Decrement -> 44 | { model with Value = model.Value - 1 } 45 | // Set the counter Value to 0 46 | | Reset -> 47 | { model with Value = 0 } 48 | // We return the new value and push a new state in the Navigation history 49 | |> (fun m -> m, [fun _ -> Navigation.pushState (toUrl m.Value)]) 50 | 51 | // Update function used by the Navigation 52 | // This function is used to uddate the model when a Navigation event occured 53 | let navigationUpdate model urlValue = 54 | { model with Value = urlValue }, [] 55 | 56 | // View 57 | let voidLinkAction<'T> : Attribute<'T> = property "href" "javascript:void(0)" 58 | let simpleButton txt action = 59 | div 60 | [ classy "column is-narrow is-narrow-mobile" ] 61 | [ a 62 | [ classy "button" 63 | voidLinkAction 64 | onMouseClick(fun _ -> 65 | action 66 | ) 67 | ] 68 | [ text txt ] 69 | ] 70 | 71 | let view model = 72 | div 73 | [ classy "columns is-vcentered is-flex-mobile" ] 74 | [ // We add a column at the beginning and the end to force center of the view 75 | div [ classy "column" ] [] 76 | div 77 | [ classy "column is-narrow is-narrow-mobile" 78 | Style [ "width", "170px" ] 79 | ] 80 | [ text (sprintf "Counter value: %i" model.Value) ] 81 | simpleButton "+1" Increment 82 | simpleButton "-1" Decrement 83 | simpleButton "Reset" Reset 84 | div [ classy "column" ] [] 85 | ] 86 | 87 | createApp Model.Initial view update Virtualdom.createRender 88 | |> withStartNodeSelector "#sample" 89 | |> withNavigation urlParser navigationUpdate 90 | |> start 91 | -------------------------------------------------------------------------------- /docs/src/Navbar.fs: -------------------------------------------------------------------------------- 1 | namespace WebApp 2 | 3 | open Fable.Core 4 | open Fable.Import.Browser 5 | 6 | open Fable.Arch 7 | open Fable.Arch.App 8 | open Fable.Arch.Html 9 | 10 | open WebApp.Common 11 | 12 | module Navbar = 13 | 14 | type Model = 15 | { CurrentPage: Route 16 | } 17 | 18 | static member Initial (page) = 19 | { CurrentPage = page 20 | } 21 | 22 | static member CurrentPage_ = 23 | (fun r -> r.CurrentPage), (fun v r -> { r with CurrentPage = v }) 24 | 25 | type Actions 26 | = NoOp 27 | | NavigateTo of Route 28 | 29 | type NavLink = 30 | { Text: string 31 | Route: Route 32 | } 33 | 34 | static member Create (text, route) = 35 | { Text = text 36 | Route = route 37 | } 38 | 39 | let update model action = 40 | match action with 41 | | NoOp -> model, [] 42 | | NavigateTo route -> 43 | let message = 44 | [ fun _ -> 45 | let url = resolveRoutesToUrl route 46 | match url with 47 | | Some u -> location.hash <- u 48 | | None -> failwith "Cannot be reached. Route should always be resolve" 49 | ] 50 | model, message 51 | 52 | let navItem navLink currentPage = 53 | let class' = 54 | classBaseList 55 | "nav-item" 56 | [ "is-active", navLink.Route = currentPage 57 | ] 58 | a 59 | [ class' 60 | voidLinkAction 61 | onMouseClick(fun _ -> 62 | NavigateTo navLink.Route 63 | ) 64 | ] 65 | [ text navLink.Text ] 66 | 67 | let navButton id href faClass txt = 68 | a 69 | [ classy "button" 70 | property "id" id 71 | property "href" href 72 | property "target" "_blank" 73 | ] 74 | [ span 75 | [ classy "icon"] 76 | [ i 77 | [ classy (sprintf "fa %s" faClass) ] 78 | [] 79 | ] 80 | span 81 | [] 82 | [ text txt ] 83 | ] 84 | 85 | let navButtons = 86 | span 87 | [ classy "nav-item"] 88 | [ navButton "twitter" "https://twitter.com/FableCompiler" "fa-twitter" "Twitter" 89 | navButton "github" "https://github.com/fable-compiler/fable-arch" "fa-github" "Fork me" 90 | navButton "github" "https://gitter.im/fable-compiler/Fable" "fa-comments" "Gitter" 91 | ] 92 | 93 | let view model = 94 | nav 95 | [ classy "nav" ] 96 | [ div 97 | [ classy "nav-left" ] 98 | [ h1 99 | [ classy "nav-item is-brand title is-4" 100 | voidLinkAction 101 | ] 102 | [ text "Fable-Arch" 103 | ] 104 | ] 105 | navButtons 106 | ] 107 | -------------------------------------------------------------------------------- /docs/src/libs/Fable.Import.Marked.fs: -------------------------------------------------------------------------------- 1 | namespace Fable.Import 2 | 3 | open System 4 | open Fable.Core 5 | open Fable.Import.JS 6 | 7 | module Marked = 8 | 9 | type [] MarkedStatic = 10 | abstract Renderer: obj with get, set 11 | abstract Parser: obj with get, set 12 | [] abstract Invoke: src: string * callback: Function -> string 13 | [] abstract Invoke: src: string * ?options: MarkedOptions * ?callback: Function -> string 14 | abstract lexer: src: string * ?options: MarkedOptions -> ResizeArray 15 | abstract parse: src: string * callback: Function -> string 16 | abstract parse: src: string * ?options: MarkedOptions * ?callback: Function -> string 17 | abstract parser: src: ResizeArray * ?options: MarkedOptions -> string 18 | abstract setOptions: options: MarkedOptions -> MarkedStatic 19 | 20 | and [] MarkedRenderer = 21 | abstract code: code: string * language: string -> string 22 | abstract blockquote: quote: string -> string 23 | abstract html: html: string -> string 24 | abstract heading: text: string * level: float -> string 25 | abstract hr: unit -> string 26 | abstract list: body: string * ordered: bool -> string 27 | abstract listitem: text: string -> string 28 | abstract paragraph: text: string -> string 29 | abstract table: header: string * body: string -> string 30 | abstract tablerow: content: string -> string 31 | abstract tablecell: content: string * flags: obj -> string 32 | abstract strong: text: string -> string 33 | abstract em: text: string -> string 34 | abstract codespan: code: string -> string 35 | abstract br: unit -> string 36 | abstract del: text: string -> string 37 | abstract link: href: string * title: string * text: string -> string 38 | abstract image: href: string * title: string * text: string -> string 39 | abstract text: text: string -> string 40 | 41 | and [] MarkedParser = 42 | abstract parse: source: ResizeArray -> string 43 | 44 | and [] MarkedOptions = 45 | abstract renderer: MarkedRenderer option with get, set 46 | abstract gfm: bool option with get, set 47 | abstract tables: bool option with get, set 48 | abstract breaks: bool option with get, set 49 | abstract pedantic: bool option with get, set 50 | abstract sanitize: bool option with get, set 51 | abstract smartLists: bool option with get, set 52 | abstract silent: bool option with get, set 53 | abstract langPrefix: string option with get, set 54 | abstract smartypants: bool option with get, set 55 | abstract highlight: code: string * ?lang: string * ?callback: Function -> string 56 | 57 | type []Globals = 58 | [] static member marked : MarkedStatic = jsNative 59 | -------------------------------------------------------------------------------- /docs/public/css/prism.css: -------------------------------------------------------------------------------- 1 | /* http://prismjs.com/download.html?themes=prism */ 2 | /** 3 | * prism.js default theme for JavaScript, CSS and HTML 4 | * Based on dabblet (http://dabblet.com) 5 | * @author Lea Verou 6 | */ 7 | 8 | code[class*="language-"], 9 | pre[class*="language-"] { 10 | color: black; 11 | background: none; 12 | text-shadow: 0 1px white; 13 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 14 | text-align: left; 15 | white-space: pre; 16 | word-spacing: normal; 17 | word-break: normal; 18 | word-wrap: normal; 19 | line-height: 1.5; 20 | 21 | -moz-tab-size: 4; 22 | -o-tab-size: 4; 23 | tab-size: 4; 24 | 25 | -webkit-hyphens: none; 26 | -moz-hyphens: none; 27 | -ms-hyphens: none; 28 | hyphens: none; 29 | } 30 | 31 | pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, 32 | code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { 33 | text-shadow: none; 34 | background: #b3d4fc; 35 | } 36 | 37 | pre[class*="language-"]::selection, pre[class*="language-"] ::selection, 38 | code[class*="language-"]::selection, code[class*="language-"] ::selection { 39 | text-shadow: none; 40 | background: #b3d4fc; 41 | } 42 | 43 | @media print { 44 | code[class*="language-"], 45 | pre[class*="language-"] { 46 | text-shadow: none; 47 | } 48 | } 49 | 50 | /* Code blocks */ 51 | pre[class*="language-"] { 52 | padding: 1em; 53 | margin: .5em 0; 54 | overflow: auto; 55 | } 56 | 57 | :not(pre) > code[class*="language-"], 58 | pre[class*="language-"] { 59 | background: #f5f2f0; 60 | } 61 | 62 | /* Inline code */ 63 | :not(pre) > code[class*="language-"] { 64 | padding: .1em; 65 | border-radius: .3em; 66 | white-space: normal; 67 | } 68 | 69 | .token.comment, 70 | .token.prolog, 71 | .token.doctype, 72 | .token.cdata { 73 | color: slategray; 74 | } 75 | 76 | .token.punctuation { 77 | color: #999; 78 | } 79 | 80 | .namespace { 81 | opacity: .7; 82 | } 83 | 84 | .token.property, 85 | .token.tag, 86 | .token.boolean, 87 | .token.number, 88 | .token.constant, 89 | .token.symbol, 90 | .token.deleted { 91 | color: #905; 92 | } 93 | 94 | .token.selector, 95 | .token.attr-name, 96 | .token.string, 97 | .token.char, 98 | .token.builtin, 99 | .token.inserted { 100 | color: #690; 101 | } 102 | 103 | .token.operator, 104 | .token.entity, 105 | .token.url, 106 | .language-css .token.string, 107 | .style .token.string { 108 | color: #a67f59; 109 | background: hsla(0, 0%, 100%, .5); 110 | } 111 | 112 | .token.atrule, 113 | .token.attr-value, 114 | .token.keyword { 115 | color: #07a; 116 | } 117 | 118 | .token.function { 119 | color: #DD4A68; 120 | } 121 | 122 | .token.regex, 123 | .token.important, 124 | .token.variable { 125 | color: #e90; 126 | } 127 | 128 | .token.important, 129 | .token.bold { 130 | font-weight: bold; 131 | } 132 | .token.italic { 133 | font-style: italic; 134 | } 135 | 136 | .token.entity { 137 | cursor: help; 138 | } 139 | 140 | -------------------------------------------------------------------------------- /docs/src/Pages/Docs/Docs_Dispatcher.fs: -------------------------------------------------------------------------------- 1 | namespace WebApp.Pages.Docs 2 | 3 | open Fable.Core 4 | open Fable.Import.Browser 5 | 6 | open Fable.Arch 7 | open Fable.Arch.App 8 | open Fable.Arch.Html 9 | 10 | open WebApp 11 | open WebApp.Common 12 | 13 | module Dispatcher = 14 | 15 | type Model = 16 | { Viewer: Viewer.Model 17 | } 18 | 19 | static member Initial (currentPage: DocsApi.Route) = 20 | { Viewer = Viewer.Model.Initial 21 | } 22 | 23 | type Actions = 24 | | NoOp 25 | | ViewerActions of Viewer.Actions 26 | 27 | let update model action = 28 | match action with 29 | | NoOp -> 30 | model, [] 31 | | ViewerActions act -> 32 | let (res, action) = Viewer.update model.Viewer act 33 | let action' = mapActions ViewerActions action 34 | { model with Viewer = res}, action' 35 | 36 | 37 | type TileDocs = 38 | { Title: string 39 | SubTitle: string 40 | FileName: string 41 | } 42 | 43 | let tileDocs info = 44 | div 45 | [ classy "tile is-parent is-vertical" ] 46 | [ article 47 | [ classy "tile is-child notification" ] 48 | [ p 49 | [ classy "title" ] 50 | [ a 51 | [ voidLinkAction 52 | property "href" (DocGen.createDocURL info.FileName) 53 | ] 54 | [ text info.Title ] 55 | ] 56 | p 57 | [ classy "subtitle" ] 58 | [ text info.SubTitle ] 59 | ] 60 | ] 61 | 62 | let tileVertical tiles = 63 | div 64 | [ classy "tile is-vertical is-6" ] 65 | (tiles |> List.map tileDocs) 66 | 67 | let indexView = 68 | div 69 | [ classy "container" ] 70 | [ div 71 | [ classy "section" ] 72 | [ div 73 | [ classy "tile is-ancestor" ] 74 | [ tileVertical 75 | [ { Title = "Hot Module Replacement (HMR)" 76 | SubTitle = "Hot Module Reloading, or Replacement, is a feature where you inject update modules in a running application. 77 | This opens up the possibility to time travel in the application without losing context. 78 | It also makes it easier to try out changes in the functionality while retaining the state of the application." 79 | FileName = "hmr" 80 | } 81 | ] 82 | tileVertical 83 | [ { Title = "Subscriber" 84 | SubTitle = "A subscriber is a function attached to an application. The subscriber will be notified every time an event/message 85 | is handled by the application." 86 | FileName = "subscriber" 87 | } 88 | ] 89 | ] 90 | ] 91 | ] 92 | 93 | 94 | let view model subRoute = 95 | match subRoute with 96 | | DocsApi.Index -> indexView 97 | | DocsApi.Viewer _ -> Html.map ViewerActions (Viewer.view model.Viewer) 98 | -------------------------------------------------------------------------------- /starter-kit/hmr/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | var merge = require('webpack-merge'); 4 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | var CopyWebpackPlugin = require('copy-webpack-plugin'); 6 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 7 | var poststylus = require('poststylus'); 8 | 9 | var TARGET_ENV = process.env.npm_lifecycle_event === 'build' ? 'production' : 'development'; 10 | 11 | var commonConfig = { 12 | devtool: 'source-map', 13 | output: { 14 | path: path.join(__dirname, "public"), 15 | publicPath: "", 16 | filename: "[hash].js" 17 | }, 18 | module: { 19 | preLoaders: [ 20 | { 21 | test: /\.js$/, 22 | exclude: /node_modules/, 23 | loader: "source-map-loader" 24 | } 25 | ], 26 | loaders: [ 27 | { 28 | test: /\.(eot|ttf|woff|woff2|svg|png)$/, 29 | exclude: /node_modules/, 30 | loader: 'file-loader' 31 | } 32 | ] 33 | }, 34 | plugins: [ 35 | new HtmlWebpackPlugin({ 36 | template: 'Source/static/index.html', 37 | inject: 'body', 38 | filename: 'index.html' 39 | }), 40 | new CopyWebpackPlugin([ 41 | { 42 | from: 'Source/static/img/', 43 | to: 'img/' 44 | } 45 | ]) 46 | ], 47 | stylus: { 48 | use: [poststylus(['autoprefixer'])] 49 | } 50 | }; 51 | 52 | if ( TARGET_ENV === 'development' ) { 53 | console.log( 'Serving locally...'); 54 | module.exports = merge(commonConfig, { 55 | entry: [ 56 | './Source/static/index.js', 57 | 'webpack/hot/dev-server', 58 | 'webpack-dev-server/client?http://localhost:8080/', 59 | ], 60 | 61 | module: { 62 | loaders: [ 63 | { 64 | test: /\.styl$/, 65 | exclude: /node_modules/, 66 | loaders: [ 67 | 'style-loader', 68 | 'css-loader', 69 | 'stylus-loader' 70 | ] 71 | } 72 | ] 73 | }, 74 | 75 | plugins: [ 76 | new webpack.HotModuleReplacementPlugin(), 77 | ] 78 | 79 | }); 80 | } 81 | 82 | if ( TARGET_ENV === 'production' ) { 83 | console.log( 'Building for prod...'); 84 | 85 | module.exports = merge( commonConfig, { 86 | 87 | entry: path.join( __dirname, 'Source/static/index.js' ), 88 | 89 | module: { 90 | loaders: [ 91 | { 92 | test: /\.styl$/, 93 | loader: ExtractTextPlugin.extract( 'style-loader', [ 94 | 'css-loader', 95 | 'stylus-loader' 96 | ]) 97 | } 98 | ] 99 | }, 100 | 101 | plugins: [ 102 | new CopyWebpackPlugin([ 103 | { 104 | from: 'src/static/img/', 105 | to: 'static/img/' 106 | } 107 | ]), 108 | 109 | new webpack.optimize.OccurenceOrderPlugin(), 110 | 111 | // extract CSS into a separate file 112 | new ExtractTextPlugin( './[hash].css', { allChunks: true } ), 113 | 114 | // minify & mangle JS/CSS 115 | new webpack.optimize.UglifyJsPlugin({ 116 | minimize: true, 117 | compressor: { warnings: false } 118 | // mangle: true 119 | }) 120 | ] 121 | 122 | }); 123 | } 124 | -------------------------------------------------------------------------------- /docs/src/Pages/Docs/Docs_Viewer.fs: -------------------------------------------------------------------------------- 1 | namespace WebApp.Pages.Docs 2 | 3 | open Fable.Arch 4 | open Fable.Arch.App 5 | open Fable.Arch.Html 6 | open Fable.Core 7 | open Fable.Import 8 | open Fable.Import.Browser 9 | open Fable.PowerPack 10 | open Fable.PowerPack.Fetch 11 | open WebApp.Common 12 | open WebApp 13 | 14 | module Viewer = 15 | 16 | type State = 17 | | Available 18 | | Pending 19 | 20 | type DocHTML = 21 | { FileName: string 22 | Html: string 23 | State: State 24 | } 25 | 26 | static member Initial fileName = 27 | { FileName = fileName 28 | Html = "" 29 | State = Pending 30 | } 31 | 32 | type Model = 33 | { CurrentFile: string 34 | DocsHTML: DocHTML list 35 | } 36 | 37 | static member Initial = 38 | { CurrentFile = "" 39 | DocsHTML = [] 40 | } 41 | 42 | type Actions = 43 | | SetDoc of string 44 | | SetDocHTML of string * string 45 | 46 | let update model action = 47 | match action with 48 | | SetDoc fileName -> 49 | // Fetch the markdown content only if unknown doc entry 50 | let exist = 51 | model.DocsHTML 52 | |> List.exists(fun x -> x.FileName = fileName) 53 | if exist then 54 | { model with CurrentFile = fileName } , [] 55 | else 56 | let m' = 57 | { model with 58 | CurrentFile = fileName 59 | DocsHTML = (DocHTML.Initial fileName) :: model.DocsHTML 60 | } 61 | let message = 62 | [ fun h -> 63 | fetch (DocGen.createDocFilesDirectoryURL fileName) [] 64 | |> Promise.bind(fun res -> 65 | res.text() 66 | ) 67 | |> Promise.map(fun text -> 68 | h(SetDocHTML (fileName, Marked.Globals.marked.parse text)) 69 | ) 70 | |> ignore 71 | ] 72 | m', message 73 | | SetDocHTML (key, html) -> 74 | let docs = 75 | model.DocsHTML 76 | |> List.map(fun doc -> 77 | if doc.FileName = key then 78 | { doc with 79 | State = Available 80 | Html = html 81 | } 82 | else 83 | doc 84 | ) 85 | 86 | { model with DocsHTML = docs}, [] 87 | 88 | 89 | let view model = 90 | let doc = 91 | // Catch KeyNotFoundException which occured when the markdown 92 | // content have never been fetched yet 93 | try 94 | model.DocsHTML 95 | |> List.find(fun x -> 96 | x.FileName = model.CurrentFile 97 | ) 98 | |> Some 99 | with _ -> None 100 | 101 | let html = 102 | if doc.IsSome && doc.Value.State = Available then 103 | div 104 | [ property "innerHTML" doc.Value.Html ] 105 | [] 106 | else 107 | div 108 | [ classy "has-text-centered" ] 109 | [ i 110 | [ classy "fa fa-spinner fa-pulse fa-3x fa-fw" ] 111 | [] 112 | ] 113 | 114 | div 115 | [ classy "section" ] 116 | [ div 117 | [ classy "content" ] 118 | [ div 119 | [ classy "container" ] 120 | [ html ] 121 | ] 122 | ] 123 | -------------------------------------------------------------------------------- /docs/samples/routing/README.md: -------------------------------------------------------------------------------- 1 | # Routing Sample 2 | 3 | Application showing you how to use the Router provided by Fable-arch 4 | 5 | ## Usage 6 | 7 | In order to see the routing reflected in the address bar you need to click on "Open in tab". 8 | 9 | You can use the GUI to play with the sample and see the address bar been updated. 10 | 11 | You can also type directly into the address bar your operations. 12 | 13 | ## Routing preparation 14 | 15 | ### Pages 16 | 17 | First we need to describe all the pages available in our Application. 18 | 19 | ```fsharp 20 | type Page = 21 | | Index 22 | | Sum of int * int 23 | | Sub of int * int 24 | | Mul of int * int 25 | | Divide of int * int 26 | | Unknown of int * int 27 | ``` 28 | 29 | ### Route parser 30 | 31 | We will use `Fable.Arch.RouteParser` In order to parse the url address. 32 | In the next function we are creating a list mapping the url values to the `Page` type we created before. 33 | 34 | ```fsharp 35 | let routes = 36 | [ 37 | runM (NavigateTo Index) (pStaticStr "/" |> (drop >> _end)) 38 | runM1 (fun numbers -> NavigateTo (Unknown numbers)) (pStaticStr "/unknown" pint pint |> _end) 39 | runM1 (fun numbers -> NavigateTo (Sum numbers)) (pStaticStr "/sum" pint pint |> _end) 40 | runM1 (fun numbers -> NavigateTo (Sub numbers)) (pStaticStr "/sub" pint pint |> _end) 41 | runM1 (fun numbers -> NavigateTo (Mul numbers)) (pStaticStr "/mul" pint pint |> _end) 42 | runM1 (fun numbers -> NavigateTo (Divide numbers)) (pStaticStr "/divide" pint pint |> _end) 43 | ] 44 | ``` 45 | 46 | ### Convert Page into Url 47 | 48 | ```fsharp 49 | let resolveRoutesToUrl r = 50 | match r with 51 | | Index -> Some "/" 52 | | Sum (numA, numB) -> Some (sprintf "/sum/%i/%i" numA numB) 53 | | Sub (numA, numB) -> Some (sprintf "/sub/%i/%i" numA numB) 54 | | Mul (numA, numB) -> Some (sprintf "/mul/%i/%i" numA numB) 55 | | Divide(numA, numB) -> Some (sprintf "/divide/%i/%i" numA numB) 56 | | Unknown(numA, numB) -> Some (sprintf "/unknown/%i/%i" numA numB) 57 | ``` 58 | 59 | ### Helpers need by Fable.Arch.RouteParser 60 | 61 | ```fsharp 62 | let mapToRoute route = 63 | match route with 64 | | NavigateTo r -> 65 | resolveRoutesToUrl r 66 | | _ -> None 67 | 68 | let router = createRouter routes mapToRoute 69 | 70 | let locationHandler = 71 | { 72 | SubscribeToChange = 73 | ( fun h -> 74 | window.addEventListener_hashchange(fun _ -> 75 | h(location.hash.Substring 1) 76 | null 77 | ) 78 | ) 79 | PushChange = 80 | (fun s -> location.hash <- s) 81 | } 82 | 83 | let routerF m = router.Route m.Message 84 | ``` 85 | 86 | ### Linked to the app 87 | 88 | With attach our helpers to the application. 89 | 90 | ```fsharp 91 | createApp Model.Initial view update Virtualdom.createRender 92 | |> withStartNodeSelector "#sample" 93 | |> withProducer (routeProducer locationHandler router) 94 | |> withSubscriber (routeSubscriber locationHandler routerF) 95 | |> start 96 | ``` 97 | 98 | After you page has been loaded the state of your application is not sync with the address bar. 99 | To fix that we need to trigger and `hashchange` event manually. 100 | 101 | ```fsharp 102 | if location.hash = "" then 103 | location.hash <- "/" 104 | else 105 | // Else trigger hashchange to navigate to current route 106 | window.dispatchEvent(Event.Create("hashchange")) |> ignore 107 | ``` -------------------------------------------------------------------------------- /docs/src/Header.fs: -------------------------------------------------------------------------------- 1 | namespace WebApp 2 | 3 | open Fable.Core 4 | open Fable.Import.Browser 5 | 6 | open Fable.Arch 7 | open Fable.Arch.App 8 | open Fable.Arch.Html 9 | 10 | open WebApp.Common 11 | 12 | module Header = 13 | 14 | type Model = 15 | { CurrentPage: Route 16 | } 17 | 18 | static member Initial (page) = 19 | { CurrentPage = page 20 | } 21 | 22 | static member CurrentPage_ = 23 | (fun r -> r.CurrentPage), (fun v r -> { r with CurrentPage = v }) 24 | 25 | type Actions 26 | = NoOp 27 | | NavigateTo of Route 28 | 29 | type HeroLink = 30 | { Text: string 31 | Route: Route 32 | } 33 | 34 | static member Create (text, route) = 35 | { Text = text 36 | Route = route 37 | } 38 | 39 | let update model action = 40 | match action with 41 | | NoOp -> 42 | model, [] 43 | | NavigateTo route -> 44 | let message = 45 | [ fun h -> 46 | let url = resolveRoutesToUrl route 47 | match url with 48 | | Some u -> location.hash <- u 49 | | None -> failwith "Cannot be reached. Route should always be resolve" 50 | ] 51 | model, message 52 | 53 | let footerLinkItem menuLink currentPage = 54 | let isCurrentPage = 55 | match currentPage with 56 | | Index | About -> 57 | menuLink.Route = currentPage 58 | | Sample _ -> 59 | match menuLink.Route with 60 | | Sample _ -> true 61 | | _ -> false 62 | | Docs _ -> 63 | match menuLink.Route with 64 | | Docs _ -> true 65 | | _ -> false 66 | 67 | li 68 | [ classList 69 | [ "is-active", isCurrentPage 70 | ] 71 | ] 72 | [ a 73 | [ voidLinkAction 74 | onMouseClick (fun _ -> 75 | NavigateTo menuLink.Route 76 | )] 77 | [ text menuLink.Text] 78 | ] 79 | 80 | let footerLinks items currentPage = 81 | ul 82 | [] 83 | (items |> List.map(fun x -> footerLinkItem x currentPage)) 84 | 85 | let footer model = 86 | div 87 | [ classy "hero-foot" ] 88 | [ div 89 | [ classy "container" ] 90 | [ nav 91 | [ classy "tabs is-boxed" ] 92 | [ footerLinks 93 | [ HeroLink.Create("Home", Route.Index) 94 | HeroLink.Create("Docs", (Route.Docs DocsApi.Index)) 95 | HeroLink.Create("Samples", (Route.Sample SampleApi.Index)) 96 | HeroLink.Create("About", Route.About) 97 | ] 98 | model.CurrentPage 99 | ] 100 | ] 101 | ] 102 | 103 | let view model = 104 | section 105 | [ classy "hero is-primary" ] 106 | [ div 107 | [ classy "hero-body" ] 108 | [ div 109 | [ classy "container" ] 110 | [ div 111 | [ classy "columns is-vcentered" ] 112 | [ div 113 | [ classy "column" ] 114 | [ h1 115 | [ classy "title" ] 116 | [ text "Documentation"] 117 | h2 118 | [ classy "subtitle" 119 | property "innerHTML" "Everything you need to create a website using Fable Arch" 120 | ] 121 | [] 122 | ] 123 | ] 124 | ] 125 | ] 126 | footer model 127 | ] 128 | -------------------------------------------------------------------------------- /src/Fable.Arch/Fable.Arch.Navigation.fs: -------------------------------------------------------------------------------- 1 | module Fable.Arch.Navigation 2 | open Fable 3 | open Fable.Arch.App 4 | open Fable.Arch.App.AppApi 5 | open Fable.Arch.Html 6 | 7 | type Location = 8 | { 9 | Href: string 10 | Host: string 11 | Hostname: string 12 | Protocol: string 13 | Origin: string 14 | Port: string 15 | Pathname: string 16 | Search: string 17 | Hash: string 18 | } 19 | with 20 | static member getLocation() = 21 | let location = Import.Browser.document.location 22 | { 23 | Href = location.href 24 | Host = location.host 25 | Hostname = location.hostname 26 | Protocol = location.protocol 27 | Origin = location.origin 28 | Port = location.port 29 | Pathname = location.pathname 30 | Search = location.search 31 | Hash = location.hash 32 | } 33 | 34 | type NavigationAction<'TAction> = 35 | | Change of Location 36 | | Message of 'TAction 37 | 38 | type Parser<'TAction> = Location -> 'TAction 39 | 40 | let mapDownNavigationAction f = function 41 | | Change _ -> None 42 | | Message m -> f m |> Some 43 | 44 | let mapDownModelChanged mc = 45 | mc.Message 46 | |> mapDownNavigationAction (fun m -> 47 | { 48 | Message = m 49 | PreviousState = mc.PreviousState 50 | CurrentState = mc.CurrentState 51 | }) 52 | 53 | let pushState url = 54 | Import.Browser.history.pushState(null, "", url) 55 | 56 | let setState url = 57 | Import.Browser.history.replaceState(null, "", url) 58 | 59 | let go n = 60 | if n <> 0 61 | then 62 | Import.Browser.history.go(n) 63 | 64 | let withNavigation parser urlUpdate app = 65 | let update' model = function 66 | | Change location -> 67 | urlUpdate model (parser location) 68 | |> (fun (m, actions) -> m, actions |> List.map (mapAction Message)) 69 | | Message msg -> 70 | app.Update model msg 71 | |> (fun (m, actions) -> m, actions |> List.map (mapAction Message)) 72 | 73 | let popStateProducer h = 74 | let popstateHandler _ = 75 | let location = Location.getLocation() 76 | h (AppMessage.Message (Change (location))) 77 | null 78 | // Must be a lambda to compile 79 | Import.Browser.window.addEventListener_popstate(fun x -> popstateHandler x) 80 | 81 | let producers = 82 | let mappedProducers = app.Producers |> List.map (mapProducer (mapAppMessage Message)) 83 | popStateProducer::mappedProducers 84 | 85 | let subscribers = 86 | app.Subscribers |> List.map (mapSubscriber mapDownModelChanged mapDownNavigationAction) 87 | 88 | let initMessage = mapAction Message app.InitMessage 89 | 90 | let mapCreateRenderer createRenderer = 91 | let mapRenderer renderer = 92 | let renderer' handler view = 93 | renderer (Message >> handler) view 94 | renderer' 95 | 96 | let createRenderer' sel handler view = 97 | createRenderer sel (Message >> handler) view 98 | |> mapRenderer 99 | createRenderer' 100 | 101 | let createRenderer = mapCreateRenderer app.CreateRenderer 102 | { 103 | InitState = app.InitState 104 | View = app.View 105 | Update = update' 106 | InitMessage = initMessage 107 | CreateRenderer = createRenderer 108 | NodeSelector = app.NodeSelector 109 | Producers = producers 110 | Subscribers = subscribers 111 | } -------------------------------------------------------------------------------- /docs/doc_files/hmr.md: -------------------------------------------------------------------------------- 1 | # Hot Module Replacement 2 | 3 | According to webpack website: 4 | 5 | > “Hot Module Replacement” (HMR) is a feature to inject updated modules into the active runtime. 6 | > 7 | > It’s like LiveReload for every module. 8 | 9 | In this documentation we are going to see how HMR can be used with Fable-VirtualDom to dynamically update the application and maintain the state between realods. 10 | 11 | 12 | ## How to use in development mode ? 13 | 14 | First we need to install the npm dependencies and dev dependencies with: `npm install` 15 | 16 | Second we run `npm run dev` which have two effect: 17 | 18 | 1. Start fable compiler in watching mode 19 | 2. Start webpack development server for serving the files and activating the HMR 20 | 21 | Now you can access the website by navigating at [http://localhost:8080](http://localhost:8080) 22 | 23 | 24 | ## Architecture 25 | 26 | The file `Source/static/index.js` is used has a starting point by Webpack. 27 | 28 | ```js 29 | // Pull in desired css/stylus files 30 | require( '../styles/app.styl' ); 31 | 32 | // Call Entry point 33 | var Entry = require( '../../out/Entry' ); 34 | ``` 35 | 36 | The application start point is `Entry` module (js file). 37 | Fable is generating one *.js* file by *.fsx*. 38 | 39 | The `Entry.fsx` file is used to set up the HMR and start the VirtualDom application. 40 | 41 | ```fsharp 42 | let contentNode = "#app" 43 | 44 | #if DEV_HMR 45 | 46 | type IModule = 47 | abstract hot: obj with get, set 48 | 49 | let [] [] Module : IModule = failwith "JS only" 50 | 51 | let node = document.querySelector contentNode 52 | 53 | if isNotNull Module.hot then 54 | Module.hot?accept() |> ignore 55 | 56 | Module.hot?dispose(fun _ -> 57 | node.removeChild(node.firstChild) |> ignore 58 | ) |> ignore 59 | #endif 60 | 61 | App.Main.start contentNode () 62 | ``` 63 | 64 | The file `Main.fsx` is a Fable-VirtualDom application with some configuration to support the HMR. 65 | 66 | ```fsharp 67 | type Model = 68 | { Input: string } 69 | 70 | static member initial = 71 | #if DEV_HMR 72 | // This section is used to maintain state between HMR 73 | if isNotNull (unbox window?storage) then 74 | unbox window?storage 75 | else 76 | let model = { Input = "" } 77 | window?storage <- model 78 | model 79 | #else 80 | { Input = "" } 81 | #endif 82 | 83 | // Actions supported by the application 84 | type Actions = 85 | | ChangeInput of string 86 | 87 | let update (model: Model) action = 88 | let model', action' = 89 | match action with 90 | | ChangeInput s -> 91 | { model with Input = s } , [] 92 | 93 | #if DEV_HMR 94 | // Update the model in storage 95 | window?storage <- model' 96 | #endif 97 | 98 | model', action' 99 | ``` 100 | 101 | We used the `window?storage` to store the state of our application on each update and load the app from here if there is information in it at loading time. 102 | 103 | ## Known limitations 104 | 105 | There is some limitation with the actual way to maintain the state. All your application state stored in the model is working. 106 | 107 | But you will lose the focus on the current node (input, textarea, etc.) because it's not stored in your model. 108 | And Fable-VirtualDom is going to redraw your application on reload. 109 | 110 | ## Can I use it ? 111 | 112 | Definitely yes, I am actually using this template for all my Fable-VirtualDom projects. 113 | You just need to clone the repo and follow the [How to use in development mode ?](#How-to-use-in-development-mode) to start playing with this starter. 114 | 115 | ## Bonus 116 | 117 | The embedded webpack is also configure to generate production ready files. 118 | 119 | You just run the command `npm run build` and got the folder `public/` generated with all the files need for your client side. 120 | -------------------------------------------------------------------------------- /docs/samples/echo/README.md: -------------------------------------------------------------------------------- 1 | # Echo sample 2 | 3 | 4 | In this sample, we ask the user to enter a sentance and the server will send the same sentance uppercased. 5 | 6 | ## Model 7 | 8 | The first thing we defined is the model. This is used to store all the information used by our application. 9 | 10 | The model need to retain 3 informations: 11 | - User input string 12 | - Uppercased string 13 | - Status of the request 14 | 15 | ```fsharp 16 | type Status = 17 | | None 18 | | Pending 19 | | Done 20 | 21 | type Model = 22 | { InputValue: string 23 | ServerResponse: string 24 | Status: Status 25 | } 26 | 27 | static member Initial = 28 | { InputValue = "" 29 | ServerResponse = "" 30 | Status = None 31 | } 32 | ``` 33 | 34 | ## Actions 35 | 36 | There is 4 actions support by the application: 37 | 38 | ```fsharp 39 | type Actions = 40 | // User is typing in the textarea 41 | | ChangeInput of string 42 | // Send a request to the server 43 | | SendEcho 44 | // Updatet the ServerResponse value 45 | | ServerResponse of string 46 | // Update the status in the model 47 | | SetStatus of Status 48 | ``` 49 | 50 | ## Update 51 | 52 | ```fsharp 53 | // Update 54 | let update model action = 55 | match action with 56 | // On input change with update the model value 57 | | ChangeInput str -> 58 | { model with InputValue = str } ,[] 59 | // When we got back a server response, we update the value of the server and set the state to Done 60 | | ServerResponse str -> 61 | { model with 62 | ServerResponse = str 63 | Status = Done }, [] 64 | // Set the status in the model 65 | | SetStatus newStatus -> 66 | { model with Status = newStatus }, [] 67 | | SendEcho -> 68 | let message = 69 | [ fun h -> 70 | fakeAjax 71 | (fun data -> 72 | h (ServerResponse data) 73 | ) 74 | model.InputValue 75 | // We just sent an ajax request so make the state pending in the model 76 | h (SetStatus Pending) 77 | ] 78 | model, message 79 | ``` 80 | 81 | ## View 82 | 83 | ```fsharp 84 | // View 85 | let view model = 86 | // Choose what we want to display on output 87 | let resultArea = 88 | match model.Status with 89 | | None -> 90 | span 91 | [] 92 | [ text "" ] 93 | | Pending -> 94 | span 95 | [] 96 | [ text "Waiting Server response" ] 97 | | Done -> 98 | span 99 | [] 100 | [ text "The server response is:" 101 | br [] 102 | b 103 | [] 104 | [ text model.ServerResponse ] 105 | ] 106 | 107 | div 108 | [ classy "columns is-flex-mobile" ] 109 | [ div [ classy "column" ] [] 110 | div 111 | [ classy "column is-narrow is-narrow-mobile" 112 | Style [ "width", "400px" ] 113 | ] 114 | [ label 115 | [ classy "label" 116 | property "for" "input-area" 117 | ] 118 | [text "Enter a sentence: "] 119 | p 120 | [ classy "control" ] 121 | [ textarea 122 | [ 123 | property "id" "input-area" 124 | classy "textarea" 125 | onInput ChangeInput 126 | property "value" model.InputValue 127 | ] 128 | [] 129 | ] 130 | div 131 | [ classy "control" ] 132 | [ p 133 | [] 134 | [ button 135 | [ classy "button" 136 | onMouseClick (fun _ -> SendEcho) 137 | ] 138 | [ text "Uppercase by server" ] 139 | ] 140 | ] 141 | resultArea 142 | ] 143 | div [ classy "column" ] [] 144 | ] 145 | ``` 146 | -------------------------------------------------------------------------------- /docs/src/libs/Fable.Import.PrismJS.fs: -------------------------------------------------------------------------------- 1 | namespace Fable.Import 2 | open System 3 | open System.Text.RegularExpressions 4 | open Fable.Core 5 | open Fable.Import.Browser 6 | 7 | module PrismJS = 8 | type [] Prism = 9 | abstract util: Util with get, set 10 | abstract languages: Languages with get, set 11 | abstract plugins: obj with get, set 12 | abstract hooks: Hooks with get, set 13 | abstract highlightAll: async: bool * ?callback: Func -> unit 14 | abstract highlightElement: element: Element * async: bool * ?callback: Func -> unit 15 | abstract highlight: text: string * grammer: LanguageDefinition * ?language: LanguageDefinition -> string 16 | abstract tokenize: text: string * grammar: LanguageDefinition * ?language: LanguageDefinition -> ResizeArray 17 | abstract fileHighlight: unit -> unit 18 | 19 | and [] Environment = 20 | abstract element: Element option with get, set 21 | abstract language: LanguageDefinition option with get, set 22 | abstract grammer: obj option with get, set 23 | abstract code: obj option with get, set 24 | abstract highlightedCode: obj option with get, set 25 | abstract ``type``: string option with get, set 26 | abstract content: string option with get, set 27 | abstract tag: string option with get, set 28 | abstract classes: ResizeArray option with get, set 29 | abstract attributes: U2, obj> option with get, set 30 | abstract parent: Element option with get, set 31 | 32 | and [] Identifier = 33 | abstract value: float with get, set 34 | 35 | and [] Util = 36 | abstract encode: tokens: U3, string> -> U3, string> 37 | abstract ``type``: o: obj -> string 38 | abstract objId: obj: obj -> Identifier 39 | abstract clone: o: LanguageDefinition -> LanguageDefinition 40 | 41 | and [] LanguageDefinition = 42 | abstract keyword: U2 option with get, set 43 | abstract number: U2 option with get, set 44 | abstract ``function``: U2 option with get, set 45 | abstract string: U2 option with get, set 46 | abstract boolean: U2 option with get, set 47 | abstract operator: U2 option with get, set 48 | abstract punctuation: U2 option with get, set 49 | abstract atrule: U2 option with get, set 50 | abstract url: U2 option with get, set 51 | abstract selector: U2 option with get, set 52 | abstract property: U2 option with get, set 53 | abstract important: U2 option with get, set 54 | abstract style: U2 option with get, set 55 | abstract alias: string option with get, set 56 | abstract pattern: Regex option with get, set 57 | abstract lookbehind: bool option with get, set 58 | abstract inside: LanguageDefinition option with get, set 59 | abstract rest: ResizeArray option with get, set 60 | 61 | and [] Languages = 62 | //inherit ResizeArray 63 | abstract extend: id: string * redef: LanguageDefinition -> LanguageDefinition 64 | abstract insertBefore: inside: string * before: string * insert: LanguageDefinition * root: obj -> obj 65 | 66 | and [] Hooks = 67 | abstract all: ResizeArray>> with get, set 68 | abstract add: name: string * callback: Func -> unit 69 | abstract run: name: string * env: Environment -> unit 70 | 71 | and [] Token = 72 | abstract ``type``: string with get, set 73 | abstract content: U3, string> with get, set 74 | abstract alias: string with get, set 75 | abstract stringify: o: U2> * language: LanguageDefinition * parent: HTMLPreElement -> string 76 | 77 | type []Globals = 78 | [] static member Prism with get(): Prism = jsNative 79 | -------------------------------------------------------------------------------- /docs/samples/echo/sample.fsx: -------------------------------------------------------------------------------- 1 | // If you are using the sample in standalone please switch the import lines 2 | // #r "node_modules/fable-core/Fable.Core.dll" 3 | // #r "node_modules/fable-arch/Fable.Arch.dll" 4 | // Imports for docs site mode 5 | #r "../../node_modules/fable-core/Fable.Core.dll" 6 | #r "../../node_modules/fable-arch/Fable.Arch.dll" 7 | 8 | open Fable.Arch 9 | open Fable.Arch.App 10 | open Fable.Arch.App.AppApi 11 | open Fable.Arch.Html 12 | open Fable.Core.JsInterop 13 | open Fable.Import.Browser 14 | 15 | 16 | /// Helpers for working with input element from VirtualDom 17 | let inline onInput x = onEvent "oninput" (fun e -> x (unbox e?target?value)) 18 | 19 | /// Used to make a fake ajax calls. It emulates a server which return the given input uppercased after 1.5 seconds. 20 | let fakeAjax cb (data: string) = 21 | window.setTimeout((fun _ -> 22 | cb (data.ToUpper()) 23 | ) 24 | , 1500.) |> ignore 25 | 26 | /// DU used to discriminate the State of the request 27 | type Status = 28 | | None 29 | | Pending 30 | | Done 31 | 32 | // Model 33 | type Model = 34 | { InputValue: string 35 | ServerResponse: string 36 | Status: Status 37 | } 38 | 39 | static member Initial = 40 | { InputValue = "" 41 | ServerResponse = "" 42 | Status = None 43 | } 44 | 45 | type Actions = 46 | | ChangeInput of string 47 | | SendEcho 48 | | ServerResponse of string 49 | | SetStatus of Status 50 | 51 | // Update 52 | let update model action = 53 | match action with 54 | // On input change with update the model value 55 | | ChangeInput str -> 56 | { model with InputValue = str } ,[] 57 | // When we got back a server response, we update the value of the server and set the state to Done 58 | | ServerResponse str -> 59 | { model with 60 | ServerResponse = str 61 | Status = Done }, [] 62 | // Set the status in the model 63 | | SetStatus newStatus -> 64 | { model with Status = newStatus }, [] 65 | | SendEcho -> 66 | let message = 67 | [ fun h -> 68 | fakeAjax 69 | (fun data -> 70 | h (ServerResponse data) 71 | ) 72 | model.InputValue 73 | // We just sent an ajax request so make the state pending in the model 74 | h (SetStatus Pending) 75 | ] 76 | model, message 77 | 78 | // View 79 | let view model = 80 | // Choose what we want to display on output 81 | let resultArea = 82 | match model.Status with 83 | | None -> 84 | span 85 | [] 86 | [ text "" ] 87 | | Pending -> 88 | span 89 | [] 90 | [ text "Waiting Server response" ] 91 | | Done -> 92 | span 93 | [] 94 | [ text "The server response is:" 95 | br [] 96 | b 97 | [] 98 | [ text model.ServerResponse ] 99 | ] 100 | 101 | div 102 | [ classy "columns is-flex-mobile" ] 103 | [ div [ classy "column" ] [] 104 | div 105 | [ classy "column is-narrow is-narrow-mobile" 106 | Style [ "width", "400px" ] 107 | ] 108 | [ label 109 | [ classy "label" 110 | property "for" "input-area" 111 | ] 112 | [text "Enter a sentence: "] 113 | p 114 | [ classy "control" ] 115 | [ textarea 116 | [ 117 | property "id" "input-area" 118 | classy "textarea" 119 | onInput ChangeInput 120 | property "value" model.InputValue 121 | ] 122 | [] 123 | ] 124 | div 125 | [ classy "control" ] 126 | [ p 127 | [] 128 | [ button 129 | [ classy "button" 130 | onMouseClick (fun _ -> SendEcho) 131 | ] 132 | [ text "Uppercase by server" ] 133 | ] 134 | ] 135 | resultArea 136 | ] 137 | div [ classy "column" ] [] 138 | ] 139 | 140 | 141 | createApp Model.Initial view update Virtualdom.createRender 142 | |> withStartNodeSelector "#sample" 143 | |> start 144 | -------------------------------------------------------------------------------- /src/Fable.Arch/Fable.Arch.Virtualdom.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module Fable.Arch.Virtualdom 3 | 4 | open Fable.Core 5 | open Fable.Import.Browser 6 | open Fable.Core.JsInterop 7 | 8 | open Html 9 | open App 10 | 11 | [] 12 | let h(arg1: string, arg2: obj, arg3: obj[]): obj = failwith "JS only" 13 | 14 | [] 15 | let diff (tree1:obj) (tree2:obj): obj = failwith "JS only" 16 | 17 | [] 18 | let patch (node:obj) (patches:obj): Fable.Import.Browser.Node = failwith "JS only" 19 | 20 | [] 21 | let createElement (e:obj): Fable.Import.Browser.Node = failwith "JS only" 22 | 23 | let createTree<'T> (handler:'T -> unit) tag (attributes:Attribute<'T> list) children = 24 | let toAttrs (attrs:Attribute<'T> list) = 25 | let elAttributes = 26 | attrs 27 | |> List.map (function 28 | | Attribute (k,v) -> (k ==> v) |> Some 29 | | _ -> None) 30 | |> List.choose id 31 | |> (function | [] -> None | v -> Some ("attributes" ==> (createObj(v)))) 32 | let props = 33 | attrs 34 | |> List.filter (function | Attribute _ -> false | _ -> true) 35 | |> List.map (function 36 | | Attribute _ -> failwith "Shouldn't happen" 37 | | Style style -> "style" ==> createObj(unbox style) 38 | | Property (k,v) -> k ==> v 39 | | EventHandler(ev,f) -> ev ==> ((f >> handler) :> obj) 40 | | Hook (k,v) -> k ==> v 41 | ) 42 | 43 | match elAttributes with 44 | | None -> props 45 | | Some x -> x::props 46 | |> createObj 47 | let elem = h(tag, toAttrs attributes, List.toArray children) 48 | elem 49 | 50 | type RenderState = 51 | | NoRequest 52 | | PendingRequest 53 | | ExtraRequest 54 | 55 | type ViewState<'TMessage> = 56 | { 57 | CurrentTree: obj 58 | NextTree: obj 59 | Node: Node 60 | RenderState: RenderState 61 | } 62 | 63 | let rec renderSomething handler node = 64 | match node with 65 | | Element((tag,attrs), nodes) 66 | | Svg((tag,attrs), nodes) -> createTree handler tag attrs (nodes |> List.map (renderSomething handler)) 67 | | VoidElement (tag, attrs) -> createTree handler tag attrs [] 68 | | Text str -> box(string str) 69 | | WhiteSpace str -> box(string str) 70 | | VirtualNode(tag, props, childrens) -> h(tag, props, childrens) 71 | 72 | let render handler view viewState = 73 | let tree = renderSomething handler view 74 | {viewState with NextTree = tree} 75 | 76 | let createRender selector handler view = 77 | let node = 78 | match selector with 79 | | Query sel -> document.body.querySelector(sel) :?> HTMLElement 80 | | Node elem -> elem 81 | 82 | let tree = renderSomething handler view 83 | let vdomNode = createElement tree 84 | node.appendChild(vdomNode) |> ignore 85 | let mutable viewState = 86 | { 87 | CurrentTree = tree 88 | NextTree = tree 89 | Node = vdomNode 90 | RenderState = NoRequest 91 | } 92 | 93 | let raf cb = 94 | Fable.Import.Browser.window.requestAnimationFrame(fun fb -> cb()) 95 | 96 | let render' handler view = 97 | let viewState' = render handler view viewState 98 | viewState <- viewState' 99 | 100 | let rec callBack() = 101 | match viewState.RenderState with 102 | | PendingRequest -> 103 | raf callBack |> ignore 104 | viewState <- {viewState with RenderState = ExtraRequest} 105 | 106 | let patches = diff viewState.CurrentTree viewState.NextTree 107 | patch viewState.Node patches |> ignore 108 | viewState <- {viewState with CurrentTree = viewState.NextTree} 109 | | ExtraRequest -> 110 | viewState <- {viewState with RenderState = NoRequest} 111 | | NoRequest -> raise (exn "Shouldn't happen") 112 | 113 | match viewState.RenderState with 114 | | NoRequest -> 115 | raf callBack |> ignore 116 | | _ -> () 117 | viewState <- {viewState with RenderState = PendingRequest} 118 | 119 | render' 120 | -------------------------------------------------------------------------------- /docs/src/Common.fs: -------------------------------------------------------------------------------- 1 | namespace WebApp 2 | 3 | open Fable.Arch.Html 4 | 5 | module Common = 6 | 7 | [] 8 | module UserApi = 9 | type Route = 10 | | Index 11 | | Create 12 | | Edit of int 13 | | Show of int 14 | 15 | [] 16 | module DocsApi = 17 | type Route = 18 | | Index 19 | | Viewer of string 20 | 21 | [] 22 | module SampleApi = 23 | type Route = 24 | | Index 25 | | Viewer of string * int 26 | 27 | type Route = 28 | | Index 29 | | Docs of DocsApi.Route 30 | | Sample of SampleApi.Route 31 | | About 32 | 33 | let resolveRoutesToUrl r = 34 | match r with 35 | | Index -> Some "/" 36 | | Docs api -> 37 | match api with 38 | | DocsApi.Index -> Some "/docs" 39 | | DocsApi.Viewer fileName -> Some (sprintf "/docs?fileName=%s" fileName) 40 | | Sample api -> 41 | match api with 42 | | SampleApi.Index -> Some "/sample" 43 | | SampleApi.Viewer (fileName, height) -> Some (sprintf "/sample/%s?height=%i" fileName height) 44 | | About -> Some "/about" 45 | 46 | let voidLinkAction<'T> : Attribute<'T> = property "href" "javascript:void(0)" 47 | 48 | module VDom = 49 | 50 | [] 51 | module Types = 52 | 53 | type LabelInfo = 54 | { RefId: string 55 | Text: string 56 | } 57 | 58 | static member Create (refId, txt) = 59 | { RefId = refId 60 | Text = txt 61 | } 62 | 63 | member self.fRefId = 64 | sprintf "f%s" self.RefId 65 | 66 | type InputInfo<'Action> = 67 | { RefId: string 68 | Placeholder: string 69 | Value: string 70 | Action: string -> 'Action 71 | } 72 | 73 | static member Create (refId, placeholder, value, action) = 74 | { RefId = refId 75 | Placeholder = placeholder 76 | Value = value 77 | Action = action 78 | } 79 | 80 | member self.fRefId = 81 | sprintf "f%s" self.RefId 82 | 83 | member self.ToLabelInfo txt = 84 | { RefId = self.RefId 85 | Text = txt 86 | } 87 | 88 | module Html = 89 | 90 | open Fable.Core.JsInterop 91 | open Fable.Import 92 | 93 | let onInput x = onEvent "oninput" (fun e -> x (unbox e?target?value)) 94 | 95 | let controlLabel (info: Types.LabelInfo) = 96 | label 97 | [ classy "label" 98 | attribute "for" info.fRefId 99 | ] 100 | [ text info.Text ] 101 | 102 | let formInput<'Action> (info: Types.InputInfo<'Action>) = 103 | div 104 | [] 105 | [ 106 | controlLabel (info.ToLabelInfo "Firstname") 107 | p 108 | [ classy "control" ] 109 | [ input 110 | [ classy "input" 111 | attribute "id" info.fRefId 112 | attribute "type" "text" 113 | attribute "placeholder" info.Placeholder 114 | property "value" info.Value 115 | onInput (fun x -> info.Action x) 116 | ] 117 | ] 118 | ] 119 | 120 | let column<'T> : DomNode<'T> = div [ classy "column" ] [] 121 | 122 | let sampleView title sampleDemoView markdownText = 123 | let markdownHTML = 124 | if markdownText = "" then 125 | div 126 | [ classy "has-text-centered" ] 127 | [ i 128 | [ classy "fa fa-spinner fa-pulse fa-3x fa-fw" ] 129 | [] 130 | ] 131 | else 132 | div 133 | [ classy "content" 134 | property "innerHTML" markdownText 135 | ] 136 | [] 137 | 138 | div 139 | [ classy "section" ] 140 | [ div 141 | [ classy "content" ] 142 | [ h1 143 | [] 144 | [ text title ] 145 | ] 146 | div 147 | [ classy "columns" ] 148 | [ div 149 | [ classy "column is-half is-offset-one-quarter" ] 150 | [ sampleDemoView ] 151 | ] 152 | div 153 | [ classy "content" ] 154 | [ h1 155 | [] 156 | [ text "Explanations" ] 157 | markdownHTML 158 | ] 159 | ] 160 | -------------------------------------------------------------------------------- /docs/src/WebApp.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | 2.0 8 | 861b39f3-785e-45ab-90b4-3cc720c1ec0b 9 | Exe 10 | WebApp 11 | WebApp 12 | v4.5.2 13 | true 14 | 4.4.0.0 15 | WebApp 16 | 17 | 18 | true 19 | full 20 | false 21 | false 22 | bin\Debug\ 23 | TRACE;DEBUG;DEV 24 | 3 25 | AnyCPU 26 | bin\Debug\WebApp.XML 27 | true 28 | 29 | 30 | pdbonly 31 | true 32 | true 33 | bin\Release\ 34 | TRACE 35 | 3 36 | AnyCPU 37 | bin\Release\WebApp.XML 38 | true 39 | 40 | 41 | 11 42 | 43 | 44 | 45 | 46 | $(MSBuildExtensionsPath32)\..\Microsoft SDKs\F#\3.0\Framework\v4.0\Microsoft.FSharp.Targets 47 | 48 | 49 | 50 | 51 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\FSharp\Microsoft.FSharp.Targets 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | ..\node_modules\fable-arch\Fable.Arch.dll 75 | 76 | 77 | ..\node_modules\fable-core\Fable.Core.dll 78 | 79 | 80 | ..\node_modules\fable-powerpack\Fable.PowerPack.dll 81 | 82 | 83 | True 84 | 85 | 86 | 87 | 94 | -------------------------------------------------------------------------------- /docs/samples/nestedcounter/README.md: -------------------------------------------------------------------------------- 1 | # Nested counter sample 2 | 3 | This sample will show you how to create nested applications. Here we will used the counter sample as sub application. 4 | In this sample every thing preceded by `Counter.` is comming from the counter sample. 5 | Ex: `Counter.update` is the update function defined for the counter sample. 6 | 7 | ## Model 8 | 9 | This application will show a list of counter so the model consist of a list of counter with an *Id* associated. 10 | We also store in `NextId` the value we used for the next counter. 11 | 12 | ```fsharp 13 | type Model = 14 | { Counters: (int * Counter.Model) list 15 | NextId: int 16 | } 17 | 18 | static member Initial = 19 | { Counters = [] 20 | NextId = 0 21 | } 22 | ``` 23 | 24 | ## Actions 25 | 26 | There are four actions implemented: 27 | 28 | * Create a new counter 29 | * Delete the counter of a certain Id 30 | * Reset all the counter value to zero 31 | * Wrap the sub actions comming from the counter application. 32 | 33 | ```fsharp 34 | type Actions = 35 | | CreateCounter 36 | | DeleteCounter of int 37 | | ResetAll 38 | | CounterActions of int * Counter.Model * Counter.Actions 39 | ``` 40 | 41 | As you can see the definition for `CounterActions` is a tuple. 42 | 43 | * `int`: id of the counter on which we want to apply the actions 44 | * `Counter.Model`: current value of the counter. We pass it to simply the update function. 45 | * `Counter.Action`: action to apply 46 | 47 | ## Update 48 | 49 | ```fsharp 50 | let update model action = 51 | match action with 52 | // Create a new tuple composed of an unique Id and an initial counter record. 53 | // We also increment the `NextId` value for future usage. 54 | | CreateCounter -> 55 | { model with 56 | Counters = model.Counters @ [(model.NextId, Counter.Model.Initial)] 57 | NextId = model.NextId + 1 }, [] 58 | // In functional programming we work with immutable data to avoid side effect. 59 | // So to delete a specific counter, we build a new list minus the counter having is counterId equal to id 60 | | DeleteCounter id -> 61 | let counters = 62 | model.Counters 63 | |> List.filter(fun (counterId, _) -> 64 | counterId <> id 65 | ) 66 | { model with Counters = counters }, [] 67 | // Iterate over the counter list to trigger a Reset message for each. 68 | | ResetAll -> 69 | let message = 70 | [ fun h -> 71 | for (id, counter) in model.Counters do 72 | h(CounterActions (id, counter, Counter.Reset)) 73 | ] 74 | model, message 75 | // Apply actions of sub application 76 | | CounterActions (id, counter, act) -> 77 | // Apply sub application update 78 | let (res, action) = Counter.update counter act 79 | // Remap the new sub app actions 80 | let mActions = mapActions CounterActions action 81 | // Update the counter list with the new value 82 | let counters = 83 | model.Counters 84 | |> List.map(fun (counterId, counterModel) -> 85 | if counterId = id then 86 | (counterId, res) 87 | else 88 | (counterId, counterModel) 89 | ) 90 | { model with Counters = counters }, mActions 91 | ``` 92 | 93 | ## View 94 | 95 | For the view, the only special case is this line: 96 | 97 | ```fsharp 98 | div 99 | [] 100 | [ Html.map (fun act -> CounterActions (id, counter, act)) (Counter.sampleDemo counter) ] 101 | ``` 102 | 103 | Here we are using the `Html.map` function given by fable-arch. This functions will map the sub view message via the mapping function `(fun act -> CounterActions (id, counter, act))`. 104 | 105 | ```fsharp 106 | let simpleButton txt action = 107 | div 108 | [ classy "column is-narrow" ] 109 | [ a 110 | [ classy "button" 111 | VDom.voidLinkAction 112 | onMouseClick(fun _ -> 113 | action 114 | ) 115 | ] 116 | [ text txt ] 117 | ] 118 | 119 | let counterRow (id, counter) = 120 | div 121 | [ classy "columns is-vcentered" ] 122 | [ VDom.column 123 | div 124 | [ classy "column" ] 125 | [ 126 | a 127 | [ classy "button" 128 | VDom.voidLinkAction 129 | onMouseClick(fun _ -> 130 | (DeleteCounter id) 131 | ) 132 | ] 133 | [ i 134 | [ classy "fa fa-trash" ] 135 | [] 136 | ] 137 | ] 138 | div 139 | [] 140 | [ Html.map (fun act -> CounterActions (id, counter, act)) (Counter.view counter) ] 141 | VDom.column 142 | ] 143 | 144 | let view model = 145 | let countersView = 146 | model.Counters 147 | |> List.map(fun (id, counter) -> counterRow (id, counter)) 148 | div 149 | [] 150 | (div 151 | [ classy "columns is-vcentered" ] 152 | [ VDom.column 153 | simpleButton "Create a new counter" CreateCounter 154 | simpleButton "Reset all" ResetAll 155 | VDom.column 156 | ] :: countersView) 157 | ``` 158 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | npm/ 15 | [Dd]ebug/ 16 | [Dd]ebugPublic/ 17 | [Rr]elease/ 18 | [Rr]eleases/ 19 | x64/ 20 | x86/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | [Ll]og/ 25 | 26 | # Visual Studio 2015 cache/options directory 27 | .vs/ 28 | # Uncomment if you have tasks that create the project's static files in wwwroot 29 | #wwwroot/ 30 | 31 | # MSTest test Results 32 | [Tt]est[Rr]esult*/ 33 | [Bb]uild[Ll]og.* 34 | 35 | # NUNIT 36 | *.VisualState.xml 37 | TestResult.xml 38 | 39 | # Build Results of an ATL Project 40 | [Dd]ebugPS/ 41 | [Rr]eleasePS/ 42 | dlldata.c 43 | 44 | # DNX 45 | project.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | *.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.pfx 193 | *.publishsettings 194 | node_modules/ 195 | orleans.codegen.cs 196 | 197 | # Since there are multiple workflows, uncomment next line to ignore bower_components 198 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 199 | #bower_components/ 200 | 201 | # RIA/Silverlight projects 202 | Generated_Code/ 203 | 204 | # Backup & report files from converting an old project file 205 | # to a newer Visual Studio version. Backup files are not needed, 206 | # because we have git ;-) 207 | _UpgradeReport_Files/ 208 | Backup*/ 209 | UpgradeLog*.XML 210 | UpgradeLog*.htm 211 | 212 | # SQL Server files 213 | *.mdf 214 | *.ldf 215 | 216 | # Business Intelligence projects 217 | *.rdl.data 218 | *.bim.layout 219 | *.bim_*.settings 220 | 221 | # Microsoft Fakes 222 | FakesAssemblies/ 223 | 224 | # GhostDoc plugin setting file 225 | *.GhostDoc.xml 226 | 227 | # Node.js Tools for Visual Studio 228 | .ntvs_analysis.dat 229 | 230 | # Visual Studio 6 build log 231 | *.plg 232 | 233 | # Visual Studio 6 workspace options file 234 | *.opt 235 | 236 | # Visual Studio LightSwitch build output 237 | **/*.HTMLClient/GeneratedArtifacts 238 | **/*.DesktopClient/GeneratedArtifacts 239 | **/*.DesktopClient/ModelManifest.xml 240 | **/*.Server/GeneratedArtifacts 241 | **/*.Server/ModelManifest.xml 242 | _Pvt_Extensions 243 | 244 | # Paket dependency manager 245 | .paket/paket.exe 246 | paket-files/ 247 | 248 | # FAKE - F# Make 249 | .fake/ 250 | 251 | # JetBrains Rider 252 | .idea/ 253 | *.sln.iml 254 | 255 | **/static/js/* 256 | !**/static/js/.keep 257 | **/out/* 258 | deploy/ 259 | build/ 260 | .paket/paket.exe 261 | 262 | 263 | docs/public/js/ 264 | 265 | # Generated documentation folder 266 | docs/output/ 267 | 268 | # temp folder 269 | temp/ 270 | 271 | .DS_Store 272 | npm-debug\.log 273 | **/js/bundle.js 274 | **/js/bundle.js.map 275 | docs/public/css/style.css -------------------------------------------------------------------------------- /docs/samples/nestedcounter/sample.fsx: -------------------------------------------------------------------------------- 1 | // If you are using the sample in standalone please switch the import lines 2 | // #r "node_modules/fable-core/Fable.Core.dll" 3 | // #r "node_modules/fable-arch/Fable.Arch.dll" 4 | // Imports for docs site mode 5 | #r "../../node_modules/fable-core/Fable.Core.dll" 6 | #r "../../node_modules/fable-arch/Fable.Arch.dll" 7 | 8 | open Fable.Core 9 | open Fable.Import 10 | open Fable.Import.Browser 11 | 12 | open Fable.Arch 13 | open Fable.Arch.App 14 | open Fable.Arch.Html 15 | 16 | module VDom = 17 | let voidLinkAction<'T> : Attribute<'T> = property "href" "javascript:void(0)" 18 | let column<'T> : DomNode<'T> = div [ classy "column" ] [] 19 | 20 | module Counter = 21 | 22 | type Actions = 23 | | Add 24 | | Sub 25 | | Reset 26 | 27 | type Model = 28 | { Value: int 29 | } 30 | 31 | /// Static member giving back an init Model 32 | static member Initial = 33 | { Value = 0 } 34 | 35 | let update model action = 36 | match action with 37 | // Add 1 to the counter Value 38 | | Add -> 39 | { model with Value = model.Value + 1 }, [] 40 | // Substract 1 to the counter Value 41 | | Sub -> 42 | { model with Value = model.Value - 1 }, [] 43 | // Set the counter Value to 0 44 | | Reset -> 45 | { model with Value = 0 }, [] 46 | let simpleButton txt action = 47 | div 48 | [ classy "column is-narrow is-narrow-mobile" ] 49 | [ a 50 | [ classy "button" 51 | VDom.voidLinkAction 52 | onMouseClick(fun _ -> 53 | action 54 | ) 55 | ] 56 | [ text txt ] 57 | ] 58 | 59 | let view model = 60 | div 61 | [ classy "columns is-vcentered is-flex-mobile" ] 62 | [ 63 | div 64 | [ classy "column is-narrow is-narrow-mobile" 65 | Style [ "width", "170px" ] 66 | ] 67 | [ text (sprintf "Counter value: %i" model.Value) ] 68 | simpleButton "+1" Add 69 | simpleButton "-1" Sub 70 | simpleButton "Reset" Reset 71 | ] 72 | 73 | type Actions = 74 | | CreateCounter 75 | | DeleteCounter of int 76 | | ResetAll 77 | | CounterActions of int * Counter.Model * Counter.Actions 78 | 79 | type Model = 80 | { Counters: (int * Counter.Model) list 81 | NextId: int 82 | } 83 | 84 | static member Initial = 85 | { Counters = [] 86 | NextId = 0 87 | } 88 | 89 | let update model action = 90 | match action with 91 | // Create a new tuple composed of an unique Id and an initial counter record. 92 | // We also increment the `NextId` value for future usage. 93 | | CreateCounter -> 94 | { model with 95 | Counters = model.Counters @ [(model.NextId, Counter.Model.Initial)] 96 | NextId = model.NextId + 1 }, [] 97 | // In functional programming we work with immutable data to avoid side effect. 98 | // So to delete a specific counter, we build a new list minus the counter having is counterId equal to id 99 | | DeleteCounter id -> 100 | let counters = 101 | model.Counters 102 | |> List.filter(fun (counterId, _) -> 103 | counterId <> id 104 | ) 105 | { model with Counters = counters }, [] 106 | // Iterate over the counter list to trigger a Reset message for each. 107 | | ResetAll -> 108 | let message = 109 | [ fun h -> 110 | for (id, counter) in model.Counters do 111 | h(CounterActions (id, counter, Counter.Reset)) 112 | ] 113 | model, message 114 | // Apply actions of sub application 115 | | CounterActions (id, counter, act) -> 116 | // Apply sub application update 117 | let (res, action) = Counter.update counter act 118 | // Remap the new sub app actions 119 | let mActions = mapActions CounterActions action 120 | // Update the counter list with the new value 121 | let counters = 122 | model.Counters 123 | |> List.map(fun (counterId, counterModel) -> 124 | if counterId = id then 125 | (counterId, res) 126 | else 127 | (counterId, counterModel) 128 | ) 129 | { model with Counters = counters }, mActions 130 | 131 | let simpleButton txt action = 132 | div 133 | [ classy "column is-narrow" ] 134 | [ a 135 | [ classy "button" 136 | VDom.voidLinkAction 137 | onMouseClick(fun _ -> 138 | action 139 | ) 140 | ] 141 | [ text txt ] 142 | ] 143 | 144 | let counterRow (id, counter) = 145 | div 146 | [ classy "columns is-vcentered is-flex-mobile" ] 147 | [ VDom.column 148 | div 149 | [ classy "column is-narrow" ] 150 | [ 151 | a 152 | [ classy "button" 153 | VDom.voidLinkAction 154 | onMouseClick(fun _ -> 155 | (DeleteCounter id) 156 | ) 157 | ] 158 | [ i 159 | [ classy "fa fa-trash" ] 160 | [] 161 | ] 162 | ] 163 | div 164 | [] 165 | [ Html.map (fun act -> CounterActions (id, counter, act)) (Counter.view counter) ] 166 | VDom.column 167 | ] 168 | 169 | let view model = 170 | let countersView = 171 | model.Counters 172 | |> List.map(fun (id, counter) -> counterRow (id, counter)) 173 | div 174 | [] 175 | (div 176 | [ classy "columns is-vcentered is-flex-mobile" ] 177 | [ VDom.column 178 | simpleButton "Create a new counter" CreateCounter 179 | simpleButton "Reset all" ResetAll 180 | VDom.column 181 | ] :: countersView) 182 | 183 | createApp Model.Initial view update Virtualdom.createRender 184 | |> withStartNodeSelector "#sample" 185 | |> start 186 | -------------------------------------------------------------------------------- /docs/samples/calculator/sample.fsx: -------------------------------------------------------------------------------- 1 | // If you are using the sample in standalone please switch the import lines 2 | // #r "node_modules/fable-core/Fable.Core.dll" 3 | // #r "node_modules/fable-arch/Fable.Arch.dll" 4 | // Imports for docs site mode 5 | #r "../../node_modules/fable-core/Fable.Core.dll" 6 | #r "../../node_modules/fable-arch/Fable.Arch.dll" 7 | 8 | open Fable.Arch 9 | open Fable.Arch.App 10 | open Fable.Arch.App.AppApi 11 | open Fable.Arch.Html 12 | 13 | // Input of user, the buttons that he/she can click 14 | type Input = 15 | | Const of int 16 | | Plus 17 | | Minus 18 | | Times 19 | | Div 20 | | Clear 21 | | Equals 22 | // the model or state of the app 23 | // this is a list of the buttons the user has clicked so far 24 | type Model = 25 | | InputStack of Input list 26 | 27 | // The action is what gets dispatched/sent to the update function 28 | // then the update function gets both the current model/state 29 | // and the action dispatched and decides how the next state 30 | // should be computed 31 | type Actions = 32 | | PushInput of Input 33 | 34 | let (|Operation|_|) = function 35 | | Plus -> Some Plus 36 | | Minus -> Some Minus 37 | | Times -> Some Times 38 | | Div -> Some Div 39 | | _ -> None 40 | 41 | let concatInts x y = int (sprintf "%d%d" x y) 42 | 43 | let opString = function 44 | | Plus -> "+" 45 | | Minus -> "-" 46 | | Times -> "*" 47 | | Div -> "/" 48 | | Equals -> "=" 49 | | Clear -> "CE" 50 | | _ -> "" 51 | 52 | let inputString = function 53 | | Operation op -> opString op 54 | | Const n -> string n 55 | | _ -> "" 56 | 57 | let modelString (InputStack xs) = 58 | xs 59 | |> List.map inputString 60 | |> String.concat "" 61 | 62 | let solve (InputStack [Const x; Operation op; Const y]) = 63 | match op with 64 | | Plus -> x + y 65 | | Minus -> x - y 66 | | Times -> x * y 67 | | Div -> x / y 68 | | _ -> failwith "Will not happen" 69 | 70 | // The update function: contains the logic of how to compute the next state or model 71 | // based on the current state and the action dispatched by the User 72 | // update : Model -> Action -> Model 73 | let update (InputStack xs) (PushInput input) = 74 | if input = Clear then InputStack [] 75 | else 76 | match xs with 77 | | [] -> 78 | match input with 79 | | Operation op -> InputStack [] 80 | | Equals -> InputStack [] 81 | | _ -> InputStack [input] 82 | | [Const x] -> 83 | match input with 84 | | Const y -> InputStack [Const (concatInts x y)] 85 | | Operation op -> InputStack [Const x; op] 86 | | _ -> InputStack xs 87 | | [Const x; Operation op] -> 88 | match input with 89 | | Const y -> InputStack [Const x; op; Const y] // push Const y to stack 90 | | Operation otherOp -> InputStack [Const x; otherOp] // replace op with otheOp 91 | | _ -> InputStack xs // do nothing 92 | | [Const x; Operation op; Const y] -> 93 | match input with 94 | | Const y' -> InputStack [Const x; op; Const (concatInts y y')] 95 | | Equals -> InputStack [Const (solve (InputStack xs))] 96 | | Operation op -> 97 | let result = solve (InputStack xs) 98 | InputStack [Const result; op] 99 | | _ -> InputStack xs 100 | | _ -> InputStack xs 101 | 102 | let digitStyle = 103 | Style [ 104 | ("height", "50px") 105 | ("width", "55px") 106 | ("font-size","20px") 107 | ("cursor","pointer") 108 | ("padding", "15px") 109 | ("padding-top", "5px") 110 | ("margin","5px") 111 | ("text-align","center") 112 | ("line-height","40px") 113 | ("background-color","lightgreen") 114 | ("box-shadow", "0 0 3px black") 115 | ] 116 | 117 | let opButtonStyle = 118 | Style [ 119 | ("height", "50px") 120 | ("width", "55px") 121 | ("font-size","20px") 122 | ("padding", "15px") 123 | ("padding-top", "5px") 124 | ("text-align","center") 125 | ("line-height","40px") 126 | ("cursor","pointer") 127 | ("margin","5px") 128 | ("background-color","lightblue") 129 | ("box-shadow", "0 0 3px black") 130 | ] 131 | 132 | let view model = 133 | let digit n = 134 | div 135 | [ 136 | digitStyle 137 | onMouseClick (fun _ -> PushInput (Const n)) 138 | ] 139 | [ text (string n) ] 140 | 141 | let opBtn input = 142 | let content = 143 | match input with 144 | | Operation op -> opString op 145 | | Equals -> "=" 146 | | Clear -> "CE" 147 | | _ -> "" 148 | div 149 | [ 150 | opButtonStyle 151 | onMouseClick (fun _ -> PushInput input) 152 | ] 153 | [ text content ] 154 | 155 | let row xs = tr [] [ for x in xs -> td [] [x]] 156 | 157 | 158 | div 159 | [ Style [ ("width","320px"); ("border", "2px black solid"); ("border-radius", "15px"); ("padding", "10px")]] 160 | [ 161 | h1 [ Style [("font-size","24px");("padding-left", "20px"); ("height", "30px")] ] [ text (modelString model) ] 162 | br [] 163 | table 164 | [] 165 | [ 166 | row [digit 1; digit 2; digit 3; opBtn Plus] 167 | row [digit 4; digit 5; digit 6; opBtn Minus] 168 | row [digit 7; digit 8; digit 9; opBtn Times] 169 | row [opBtn Clear; digit 0; opBtn Equals; opBtn Div] 170 | ] 171 | ] 172 | 173 | // Using createSimpleApp instead of createApp since our 174 | // update function doesn't generate any actions. See 175 | // some of the other more advanced examples for how to 176 | // use createApp. In addition to the application functions 177 | // we also need to specify which renderer to use. 178 | createSimpleApp (InputStack []) view update Virtualdom.createRender 179 | |> withStartNodeSelector "#sample" 180 | |> start 181 | -------------------------------------------------------------------------------- /docs/src/Pages/Sample/Sample_Viewer.fs: -------------------------------------------------------------------------------- 1 | namespace WebApp.Pages.Sample 2 | 3 | open Fable.Arch 4 | open Fable.Arch.App 5 | open Fable.Arch.Html 6 | open Fable.Core 7 | open Fable.Import 8 | open Fable.Import.Browser 9 | open Fable.PowerPack 10 | open Fable.PowerPack.Fetch 11 | open WebApp.Common 12 | open WebApp 13 | 14 | module Viewer = 15 | 16 | type State = 17 | | Available 18 | | Pending 19 | 20 | type DocHTML = 21 | { SampleName: string 22 | Html: string 23 | State: State 24 | } 25 | 26 | static member Initial sampleName = 27 | { SampleName = sampleName 28 | Html = "" 29 | State = Pending 30 | } 31 | 32 | type Model = 33 | { CurrentFile: string 34 | IframeHeight: int 35 | DocsHTML: DocHTML list 36 | } 37 | 38 | static member Initial = 39 | { CurrentFile = "" 40 | DocsHTML = [] 41 | IframeHeight = 350 42 | } 43 | 44 | type Actions = 45 | | SetDoc of string 46 | | SetDocHTML of string * string 47 | | SetHeight of int 48 | 49 | let update model action = 50 | match action with 51 | | SetDoc (sampleName) -> 52 | // Fetch the markdown content only if unknown doc entry 53 | let exist = 54 | model.DocsHTML 55 | |> List.exists(fun x -> x.SampleName = sampleName) 56 | if exist then 57 | { model with CurrentFile = sampleName } , [] 58 | else 59 | let m' = 60 | { model with 61 | CurrentFile = sampleName 62 | DocsHTML = (DocHTML.Initial sampleName) :: model.DocsHTML 63 | } 64 | let message = 65 | [ fun h -> 66 | fetch (DocGen.createSampleReadmeURL sampleName) [] 67 | |> Promise.bind(fun res -> 68 | res.text() 69 | ) 70 | |> Promise.map( 71 | fun text -> 72 | h(SetDocHTML (sampleName, Marked.Globals.marked.parse text)) 73 | ) 74 | |> ignore 75 | fun h -> 76 | try 77 | let search = location.href.Split '?' 78 | let parameters = search.[1].Split '&' 79 | let heightParam = 80 | parameters 81 | |> Array.find(fun p -> p.Contains "height") 82 | let height = int (heightParam.Split '=').[1] 83 | h(SetHeight height) 84 | // If an error occured when extracting height from the parameters 85 | // We do nothing 86 | with 87 | | _ -> console.error "Error when extracting the iframe height parameter" 88 | ] 89 | m', message 90 | | SetDocHTML (key, html) -> 91 | let docs = 92 | model.DocsHTML 93 | |> List.map(fun doc -> 94 | if doc.SampleName = key then 95 | { doc with 96 | State = Available 97 | Html = html 98 | } 99 | else 100 | doc 101 | ) 102 | 103 | { model with DocsHTML = docs}, [] 104 | | SetHeight height -> 105 | { model with IframeHeight = height }, [] 106 | 107 | let iframe x = elem "iframe" x 108 | 109 | let view model = 110 | let doc = 111 | // Catch KeyNotFoundException which occured when the markdown 112 | // content have never been fetched yet 113 | try 114 | model.DocsHTML 115 | |> List.find(fun x -> 116 | x.SampleName = model.CurrentFile 117 | ) 118 | |> Some 119 | with _ -> None 120 | 121 | let html = 122 | if doc.IsSome && doc.Value.State = Available then 123 | div 124 | [ property "innerHTML" doc.Value.Html ] 125 | [] 126 | else 127 | div 128 | [ classy "has-text-centered" ] 129 | [ i 130 | [ classy "fa fa-spinner fa-pulse fa-3x fa-fw" ] 131 | [] 132 | ] 133 | 134 | div 135 | [ classy "section" ] 136 | [ div 137 | [ classy "content" ] 138 | [ div 139 | [ classy "container" ] 140 | [ h1 141 | [ classy "has-text-centered" ] 142 | [ text "Demo" ] 143 | div 144 | [ classy "columns" ] 145 | [ div 146 | [ classy "column is-half is-offset-one-quarter has-text-centered" ] 147 | [ a 148 | [ classy "button is-primary is-pulled-left" 149 | property "href" (DocGen.createSampleDirectoryURL model.CurrentFile) 150 | property "target" "_blank" 151 | ] 152 | [ text "Open in tab" ] 153 | a 154 | [ classy "button is-pulled-right" 155 | property "href" (DocGen.githubURL model.CurrentFile) 156 | property "target" "_blank" 157 | ] 158 | [ span 159 | [ classy "icon"] 160 | [ i 161 | [ classy "fa fa-github" ] 162 | [] 163 | ] 164 | span 165 | [] 166 | [ text "Go to source" ] 167 | ] 168 | br [] 169 | br [] 170 | iframe 171 | [ classy "sample-viewer" 172 | property "src" (DocGen.createSampleDirectoryURL model.CurrentFile) 173 | property "height" (string model.IframeHeight) 174 | ] 175 | [] 176 | ] 177 | ] 178 | div 179 | [ classy "content" ] 180 | [ h1 181 | [ classy "has-text-centered" ] 182 | [ text "Explanations" ] 183 | div 184 | [ classy "container" ] 185 | [ html ] 186 | ] 187 | ] 188 | ] 189 | ] 190 | -------------------------------------------------------------------------------- /docs/src/Pages/Sample/Sample_Dispatcher.fs: -------------------------------------------------------------------------------- 1 | namespace WebApp.Pages.Sample 2 | 3 | open Fable.Core 4 | open Fable.Import.Browser 5 | 6 | open Fable.Arch 7 | open Fable.Arch.App 8 | open Fable.Arch.Html 9 | 10 | open WebApp 11 | open WebApp.Common 12 | 13 | module Dispatcher = 14 | 15 | type Model = 16 | { 17 | Viewer: Viewer.Model 18 | } 19 | 20 | static member Initial (currentPage: SampleApi.Route) = 21 | { 22 | Viewer = Viewer.Model.Initial 23 | } 24 | 25 | type Actions = 26 | | NoOp 27 | | ViewerActions of Viewer.Actions 28 | 29 | let update model action = 30 | match action with 31 | | NoOp -> 32 | model, [] 33 | | ViewerActions act -> 34 | let (res, action) = Viewer.update model.Viewer act 35 | let action' = mapActions ViewerActions action 36 | { model with Viewer = res}, action' 37 | 38 | type TileDocs = 39 | { Title: string 40 | SubTitle: string 41 | FileName: string 42 | Height: int 43 | } 44 | 45 | type SectionsInfo = 46 | { 47 | Section1: TileDocs list 48 | Section2: TileDocs list 49 | Section3: TileDocs list 50 | } 51 | 52 | static member Initial = 53 | { 54 | Section1 = [] 55 | Section2 = [] 56 | Section3 = [] 57 | } 58 | 59 | let tileDocs info = 60 | let sampleURL = 61 | let sampleApi = SampleApi.Viewer (info.FileName, info.Height) 62 | match resolveRoutesToUrl (Sample sampleApi) with 63 | | Some url -> sprintf "#%s" url 64 | | None -> failwith "Uknown route" 65 | 66 | div 67 | [ classy "tile is-parent is-vertical" ] 68 | [ article 69 | [ classy "tile is-child notification" ] 70 | [ p 71 | [ classy "title" ] 72 | [ a 73 | [ voidLinkAction 74 | property "href" sampleURL 75 | ] 76 | [ text info.Title ] 77 | ] 78 | p 79 | [ classy "subtitle" ] 80 | [ text info.SubTitle ] 81 | ] 82 | ] 83 | 84 | let tileVertical tiles = 85 | div 86 | [ classy "tile is-vertical is-4" ] 87 | (tiles |> List.map tileDocs) 88 | let tileSection tiles = 89 | let rec divideTiles tiles index sectionsInfo = 90 | match tiles with 91 | | tile::trail -> 92 | let sectionsInfo' = 93 | match index % 3 with 94 | | 0 -> 95 | { sectionsInfo with 96 | Section1 = sectionsInfo.Section1 @ [tile] } 97 | | 1 -> 98 | { sectionsInfo with 99 | Section2 = sectionsInfo.Section2 @ [tile] } 100 | | 2 -> 101 | { sectionsInfo with 102 | Section3 = sectionsInfo.Section3 @ [tile] } 103 | | _ -> failwith "Should not happened" 104 | divideTiles trail (index + 1) sectionsInfo' 105 | | [] -> 106 | sectionsInfo 107 | 108 | let info = divideTiles tiles 0 SectionsInfo.Initial 109 | div 110 | [ classy "tile is-ancestor" ] 111 | [ tileVertical info.Section1 112 | tileVertical info.Section2 113 | tileVertical info.Section3 114 | ] 115 | 116 | let beginnerView = 117 | div 118 | [] 119 | [ 120 | h1 121 | [ classy "title" ] 122 | [ text "Beginner" ] 123 | tileSection 124 | [ { Title = "Hello World" 125 | SubTitle = "A simple application showing inputs usage" 126 | FileName = "hello" 127 | Height = 350 128 | } 129 | { Title = "Counter" 130 | SubTitle = "A simple application showing how to support multiple actions" 131 | FileName = "counter" 132 | Height = 300 133 | } 134 | { Title = "Nested counter" 135 | SubTitle = "A application showing how to use nested application" 136 | FileName = "nestedcounter" 137 | Height = 400 138 | } 139 | { Title = "Clock" 140 | SubTitle = "A clock showing producer usage" 141 | FileName = "clock" 142 | Height = 300 143 | } 144 | { Title = "Echo" 145 | SubTitle = "An echo application showing how to make ajax calls" 146 | FileName = "echo" 147 | Height = 500 148 | } 149 | { Title = "Subscriber" 150 | SubTitle = "This application show you how to register a subscriber to your application" 151 | FileName = "subscriber" 152 | Height = 350 153 | } 154 | { Title = "Navigation" 155 | SubTitle = "This application show you how to use the navigation feature of Fable-Arch" 156 | FileName = "navigation" 157 | Height = 350 158 | } 159 | { Title = "React" 160 | SubTitle = "Port of the counter sample using React" 161 | FileName = "reactCounter" 162 | Height = 300 163 | } 164 | ] 165 | ] 166 | 167 | let advancedView = 168 | div 169 | [] 170 | [ 171 | h1 172 | [ classy "title" ] 173 | [ text "Medium" ] 174 | tileSection 175 | [ { Title = "Calculator" 176 | SubTitle = "A calculator application" 177 | FileName = "calculator" 178 | Height = 600 179 | } 180 | { Title = "Routing" 181 | SubTitle = "This application show you how to use the navigation feature of Fable-Arch" 182 | FileName = "routing" 183 | Height = 350 184 | } 185 | ] 186 | ] 187 | 188 | let expertView = 189 | div 190 | [] 191 | [ 192 | h1 193 | [ classy "title" ] 194 | [ text "Advanced" ] 195 | tileSection 196 | [ 197 | ] 198 | div 199 | [] 200 | [ text "Nothing to show yet"] 201 | ] 202 | 203 | let indexView = 204 | div 205 | [ classy "container" ] 206 | [ div 207 | [ classy "section" ] 208 | [ 209 | beginnerView 210 | hr [] 211 | advancedView 212 | hr [] 213 | expertView 214 | ] 215 | ] 216 | 217 | let view model subRoute = 218 | match subRoute with 219 | | SampleApi.Index -> indexView 220 | | SampleApi.Viewer _ -> Html.map ViewerActions (Viewer.view model.Viewer) 221 | -------------------------------------------------------------------------------- /docs/samples/calculator/README.md: -------------------------------------------------------------------------------- 1 | # Calculator sample 2 | 3 | This sample is a simple calculator written using fable-arch. Created by [Zaid-Ajaj](https://github.com/Zaid-Ajaj). 4 | 5 | ## Model 6 | 7 | The first thing we do is define a *model*. Sometimes we call this the *state of the app*. The state is what we would like to keep track of when the user interacts with the app. 8 | 9 | In our case, the `Input` type represents what the user can click on in the calculator and `Model` represents what buttons the user had clicked so far. 10 | 11 | Not every click will be added to the model. It's up to the `update` function to decide how to compute the next state of the app once the user clicked something. 12 | 13 | You might ask, *How does the `update` function know what the user clicked if it only operates on the model?* It is the `view` function the sends or dispatches the actions to the `update` function. The `update` function computes the next state and returns the result to the `view`. The `view` function gets called and the UI gets re-rendered. 14 | 15 | ```fsharp 16 | type Input = 17 | | Const of int 18 | | Plus 19 | | Minus 20 | | Times 21 | | Div 22 | | Clear 23 | | Equals 24 | 25 | type Model = InputStack of Input list 26 | ``` 27 | ## Actions 28 | The action is what gets dispatched/sent to the update function, then the update function gets both the current model/state and the action dispatched and decides how the next state should be computed. 29 | ```fsharp 30 | type Actions = PushInput of Input 31 | ``` 32 | ### Helper functions 33 | Before we dive in the update function that has all the logic of the app, let us first define some helper functions that operate on our model and input. These should be self-explainatory for an F# developer. 34 | 35 | ```fsharp 36 | // lets you concat two ints, i.e. concatInts 11 22 -> 1122 37 | // concatInts : int -> int -> int 38 | let concatInts x y = int (sprintf "%d%d" x y) 39 | 40 | // Active pattern that matches with an operation 41 | Operation : Input -> Input option 42 | let (|Operation|_|) = function 43 | | Plus -> Some Plus 44 | | Minus -> Some Minus 45 | | Times -> Some Times 46 | | Div -> Some Div 47 | | _ -> None 48 | 49 | // when the model has the shape `[Const a; operation; Const b]`, we reduce that to `(operation) a b` 50 | // solve : Model -> int 51 | let solve (InputStack [Const x; Operation op; Const y]) = 52 | match op with 53 | | Plus -> x + y 54 | | Minus -> x - y 55 | | Times -> x * y 56 | | Div -> x / y 57 | | _ -> failwith "Will not happen" 58 | ``` 59 | 60 | ## Update 61 | The update function: contains the logic of how to compute the next state or model based on the current state and the action dispatched by the user. 62 | ```fsharp 63 | // update : Model -> Action -> Model 64 | let update (InputStack xs) (PushInput input) = 65 | if input = Clear then InputStack [] 66 | else 67 | match xs with 68 | | [] -> // model is empty 69 | match input with 70 | | Operation op -> InputStack [] // user clicks an operation -> model stays empty 71 | | Equals -> InputStack [] // user clicks = -> model stays empty 72 | | _ -> InputStack [input] // otherwise, add whatever input was clicked to model 73 | | [Const x] -> // model contains a number 74 | match input with 75 | | Const y -> InputStack [Const (concatInts x y)] // user clikced on digit -> concat the two 76 | | Operation op -> InputStack [Const x; op] // user clicked an operation -> add it to model 77 | | _ -> InputStack xs // otherwise -> return the model unchanged 78 | | [Const x; Operation op] -> // the model contains a number and an operation 79 | match input with 80 | | Const y -> InputStack [Const x; op; Const y] // user clicked another digit -> push the digit to model 81 | | Operation otherOp -> InputStack [Const x; otherOp] // user clicked another operation -> replace op with otheOp 82 | | _ -> InputStack xs // otherwise -> return model unchanged 83 | | [Const x; Operation op; Const y] -> // now model contains the shape we want to send to the "solve" function 84 | match input with 85 | | Const y' -> InputStack [Const x; op; Const (concatInts y y')] // clicked on digit -> concat it with Const y 86 | | Equals -> InputStack [Const (solve (InputStack xs))] // calculate result, reset model and push result to model 87 | | Operation op -> 88 | let result = solve (InputStack xs) 89 | InputStack [Const result; op] 90 | | _ -> InputStack xs 91 | | _ -> InputStack xs 92 | ``` 93 | ## View 94 | 95 | Now the view. The view depends on the current state and dispatches actions to the update function, there by getting a new state. At this point the view will rerender itself. 96 | 97 | ### Helper functions 98 | ```fsharp 99 | let opString = function 100 | | Plus -> "+" 101 | | Minus -> "-" 102 | | Times -> "*" 103 | | Div -> "/" 104 | | Equals -> "=" 105 | | Clear -> "CE" 106 | | _ -> "" 107 | 108 | let inputString = function 109 | | Operation op -> opString op 110 | | Const n -> string n 111 | | _ -> "" 112 | 113 | let modelString (InputStack xs) = 114 | xs 115 | |> List.map inputString 116 | |> String.concat "" 117 | ``` 118 | ### Styles for the buttons 119 | ```fsharp 120 | let digitStyle = 121 | Style [ 122 | ("height", "40px") 123 | ("width", "55px") 124 | ("font-size","24px") 125 | ("cursor","pointer") 126 | ("padding", "15px") 127 | ("margin","5px") 128 | ("text-align","center") 129 | ("vertical-align","middle") 130 | ("line-height","40px") 131 | ("background-color","lightgreen") 132 | ("box-shadow", "0 0 3px black") 133 | ] 134 | 135 | let opButtonStyle = 136 | Style [ 137 | ("height", "40px") 138 | ("width", "55px") 139 | ("font-size","24px") 140 | ("padding", "15px") 141 | ("text-align","center") 142 | ("vertical-align","middle") 143 | ("line-height","40px") 144 | ("cursor","pointer") 145 | ("margin","5px") 146 | ("background-color","lightblue") 147 | ("box-shadow", "0 0 3px black") 148 | ] 149 | ``` 150 | 151 | ## The `view` function 152 | ```fsharp 153 | let view model = 154 | let digit n = 155 | div 156 | [ 157 | digitStyle 158 | onMouseClick (fun _ -> PushInput (Const n)) 159 | ] 160 | [ text (string n) ] 161 | 162 | let opBtn input = 163 | let content = 164 | match input with 165 | | Operation op -> opString op 166 | | Equals -> "=" 167 | | Clear -> "CE" 168 | | _ -> "" 169 | div 170 | [ 171 | opButtonStyle 172 | onMouseClick (fun _ -> PushInput input) 173 | ] 174 | [ text content ] 175 | 176 | // table row 177 | let row xs = tr [] [ for x in xs -> td [] [x]] 178 | 179 | div 180 | [ Style [ ("width", "407px"); ("border", "2px black solid"); ("border-radius", "15px"); ("padding", "10px")]] 181 | [ 182 | h2 [ Style [("padding-left", "20px"); ("height", "30px")] ] [ text (modelString model) ] 183 | br [] 184 | table 185 | [] 186 | [ 187 | row [digit 1; digit 2; digit 3; opBtn Plus] 188 | row [digit 4; digit 5; digit 6; opBtn Minus] 189 | row [digit 7; digit 8; digit 9; opBtn Times] 190 | row [opBtn Clear; digit 0; opBtn Equals; opBtn Div] 191 | ] 192 | ] 193 | ``` 194 | 195 | # Create the app 196 | ```fsharp 197 | let initialState = InputStack [] 198 | createSimpleApp initialState view update Virtualdom.createRender 199 | |> withStartNodeSelector "#calc" 200 | |> start 201 | ``` 202 | -------------------------------------------------------------------------------- /src/Fable.Arch/Fable.Arch.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Fable.Arch 5 | Fable.Arch 6 | Fable.Arch 7 | Debug 8 | AnyCPU 9 | 2.0 10 | 1dc229e4-4873-4884-9e74-188610c74d11 11 | Library 12 | v4.5 13 | 4.0.0.1 14 | 15 | 16 | true 17 | Full 18 | false 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | 3 23 | 24 | 25 | PdbOnly 26 | true 27 | true 28 | bin\Release\ 29 | TRACE 30 | 3 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | $(MSBuildExtensionsPath32)\..\Microsoft SDKs\F#\3.0\Framework\v4.0\Microsoft.FSharp.Targets 58 | 59 | 60 | 61 | 62 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\FSharp\Microsoft.FSharp.Targets 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | ..\..\packages\FSharp.Core\lib\net20\FSharp.Core.dll 72 | True 73 | True 74 | 75 | 76 | 77 | 78 | 79 | 80 | ..\..\packages\FSharp.Core\lib\net40\FSharp.Core.dll 81 | True 82 | True 83 | 84 | 85 | 86 | 87 | 88 | 89 | ..\..\packages\FSharp.Core\lib\portable-net45+monoandroid10+monotouch10+xamarinios10\FSharp.Core.dll 90 | True 91 | True 92 | 93 | 94 | 95 | 96 | 97 | 98 | ..\..\packages\FSharp.Core\lib\portable-net45+netcore45\FSharp.Core.dll 99 | True 100 | True 101 | 102 | 103 | 104 | 105 | 106 | 107 | ..\..\packages\FSharp.Core\lib\portable-net45+netcore45+wp8\FSharp.Core.dll 108 | True 109 | True 110 | 111 | 112 | 113 | 114 | 115 | 116 | ..\..\packages\FSharp.Core\lib\portable-net45+netcore45+wpa81+wp8\FSharp.Core.dll 117 | True 118 | True 119 | 120 | 121 | 122 | 123 | 124 | 125 | ..\..\packages\FSharp.Core\lib\portable-net45+sl5+netcore45\FSharp.Core.dll 126 | True 127 | True 128 | 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /docs/samples/routing/sample.fsx: -------------------------------------------------------------------------------- 1 | // If you are using the sample in standalone please switch the import lines 2 | // #r "node_modules/fable-core/Fable.Core.dll" 3 | // #r "node_modules/fable-arch/Fable.Arch.dll" 4 | // Imports for docs site mode 5 | #r "../../node_modules/fable-core/Fable.Core.dll" 6 | #r "../../node_modules/fable-arch/Fable.Arch.dll" 7 | 8 | open Fable.Arch 9 | open Fable.Arch.App 10 | open Fable.Arch.App.AppApi 11 | open Fable.Arch.Html 12 | open Fable.Arch.RouteParser.Parsing 13 | open Fable.Arch.RouteParser.RouteParser 14 | open Fable.Core.JsInterop 15 | open Fable.Import.Browser 16 | open System.Text.RegularExpressions 17 | 18 | 19 | module VDom = 20 | open Fable.Core.JsInterop 21 | 22 | let onInput x = onEvent "oninput" (fun e -> x (unbox e?target?value)) 23 | 24 | type Actions = 25 | | NavigateTo of Page 26 | | ChangeNumberA of string 27 | | ChangeNumberB of string 28 | | ChangeOperator of Operator 29 | 30 | and Operator = 31 | | Sum 32 | | Sub 33 | | Mul 34 | | Divide 35 | | Unknown 36 | 37 | // Routing part 38 | and Page = 39 | | Index 40 | | Sum of int * int 41 | | Sub of int * int 42 | | Mul of int * int 43 | | Divide of int * int 44 | | Unknown of int * int 45 | 46 | let routes = 47 | [ 48 | runM (NavigateTo Index) (pStaticStr "/" |> (drop >> _end)) 49 | runM1 (fun numbers -> NavigateTo (Unknown numbers)) (pStaticStr "/unknown" pint pint |> _end) 50 | runM1 (fun numbers -> NavigateTo (Sum numbers)) (pStaticStr "/sum" pint pint |> _end) 51 | runM1 (fun numbers -> NavigateTo (Sub numbers)) (pStaticStr "/sub" pint pint |> _end) 52 | runM1 (fun numbers -> NavigateTo (Mul numbers)) (pStaticStr "/mul" pint pint |> _end) 53 | runM1 (fun numbers -> NavigateTo (Divide numbers)) (pStaticStr "/divide" pint pint |> _end) 54 | ] 55 | 56 | let resolveRoutesToUrl r = 57 | match r with 58 | | Index -> Some "/" 59 | | Sum (numA, numB) -> Some (sprintf "/sum/%i/%i" numA numB) 60 | | Sub (numA, numB) -> Some (sprintf "/sub/%i/%i" numA numB) 61 | | Mul (numA, numB) -> Some (sprintf "/mul/%i/%i" numA numB) 62 | | Divide(numA, numB) -> Some (sprintf "/divide/%i/%i" numA numB) 63 | | Unknown(numA, numB) -> Some (sprintf "/unknown/%i/%i" numA numB) 64 | 65 | let mapToRoute route = 66 | match route with 67 | | NavigateTo r -> 68 | resolveRoutesToUrl r 69 | | _ -> None 70 | 71 | let router = createRouter routes mapToRoute 72 | 73 | let locationHandler = 74 | { 75 | SubscribeToChange = 76 | ( fun h -> 77 | window.addEventListener_hashchange(fun _ -> 78 | h(location.hash.Substring 1) 79 | null 80 | ) 81 | ) 82 | PushChange = 83 | (fun s -> location.hash <- s) 84 | } 85 | 86 | let routerF m = router.Route m.Message 87 | 88 | type Model = 89 | { Operator: Operator 90 | NumberA: int 91 | NumberB: int 92 | Result: float 93 | } 94 | 95 | static member Initial = 96 | { 97 | Operator = Operator.Unknown 98 | NumberA = 0 99 | NumberB = 0 100 | Result = 0. 101 | } 102 | 103 | static member CreateFromPage page = 104 | match page with 105 | | Sum (numA, numB) -> 106 | { Operator = Operator.Sum 107 | NumberA = numA 108 | NumberB = numB 109 | Result = numA + numB |> float 110 | } 111 | | Sub (numA, numB) -> 112 | { Operator = Operator.Sub 113 | NumberA = numA 114 | NumberB = numB 115 | Result = numA - numB |> float 116 | } 117 | | Mul (numA, numB) -> 118 | { Operator = Operator.Mul 119 | NumberA = numA 120 | NumberB = numB 121 | Result = numA * numB |> float 122 | } 123 | | Divide (numA, numB) -> 124 | { Operator = Operator.Divide 125 | NumberA = numA 126 | NumberB = numB 127 | Result = float numA / float numB 128 | } 129 | | Unknown (numA, numB) -> 130 | { Operator = Operator.Unknown 131 | NumberA = numA 132 | NumberB = numB 133 | Result = Fable.Import.JS.NaN 134 | } 135 | | Index -> Model.Initial 136 | 137 | let isValidNumber input = 138 | Regex.Match(input, "(^\d+$){0,1}").Success 139 | 140 | let saveIntoURL model = 141 | [ fun _ -> 142 | match model.Operator with 143 | | Operator.Divide -> 144 | Page.Divide(model.NumberA, model.NumberB) 145 | | Operator.Sum -> 146 | Page.Sum(model.NumberA, model.NumberB) 147 | | Operator.Sub -> 148 | Page.Sub(model.NumberA, model.NumberB) 149 | | Operator.Mul -> 150 | Page.Mul(model.NumberA, model.NumberB) 151 | | Operator.Unknown -> 152 | Page.Unknown(model.NumberA, model.NumberB) 153 | |> resolveRoutesToUrl 154 | |> function 155 | | Some s -> locationHandler.PushChange (sprintf "#%s" s) 156 | | None -> () 157 | ] 158 | 159 | let update model action = 160 | match action with 161 | | ChangeOperator op -> 162 | let m = { model with Operator = op } 163 | m, saveIntoURL m 164 | | ChangeNumberA value -> 165 | if isValidNumber value then 166 | let m = { model with NumberA = int value } 167 | m, saveIntoURL m 168 | else 169 | model, [] 170 | | ChangeNumberB value -> 171 | if isValidNumber value then 172 | let m = { model with NumberB = int value } 173 | m, saveIntoURL m 174 | else 175 | model, [] 176 | | NavigateTo page -> 177 | Model.CreateFromPage page , [] 178 | 179 | 180 | type NumberAreaInfo = 181 | { 182 | OnInputAction: string -> Actions 183 | Value: int 184 | } 185 | 186 | let numberArea (areaInfo: NumberAreaInfo) = 187 | input 188 | [ classy "input" 189 | VDom.onInput areaInfo.OnInputAction 190 | property "type" "number" 191 | property "value" (string areaInfo.Value) 192 | Style [ "width", "100px"] 193 | ] 194 | 195 | let isSelected value ref = 196 | if value = ref then 197 | property "selected" "selected" 198 | else 199 | property "" "" 200 | 201 | let view model = 202 | div 203 | [ classy "columns is-flex-mobile" ] 204 | [ div [ classy "column" ] [] 205 | p 206 | [ classy "control has-addons"] 207 | [ 208 | numberArea { 209 | OnInputAction = (fun x -> ChangeNumberA x) 210 | Value = model.NumberA 211 | } 212 | div 213 | [ classy "select" ] 214 | [ select 215 | [ onChange (fun ev -> ChangeOperator (ofJson (unbox ev?target?value))) 216 | ] 217 | [ option 218 | [ property "disabled" "disabled" 219 | isSelected model.Operator Operator.Unknown 220 | property "value" "Unknown" 221 | ] 222 | [ text "" ] 223 | option 224 | [ isSelected model.Operator Operator.Sum 225 | property "value" (toJson Operator.Sum) 226 | ] 227 | [ text "+" ] 228 | option 229 | [ isSelected model.Operator Operator.Sub 230 | property "value" (toJson Operator.Sub) 231 | ] 232 | [ text "-" ] 233 | option 234 | [ isSelected model.Operator Operator.Mul 235 | property "value" (toJson Operator.Mul) 236 | ] 237 | [ text "*" ] 238 | option 239 | [ isSelected model.Operator Operator.Divide 240 | property "value" (toJson Operator.Divide) 241 | ] 242 | [ text "/" ] 243 | ] 244 | ] 245 | numberArea { 246 | OnInputAction = (fun x -> ChangeNumberB x) 247 | Value = model.NumberB 248 | } 249 | span 250 | [ classy "control is-vcentered" ] 251 | [ 252 | div 253 | [ classy "button is-primary" ] 254 | [ text (sprintf "= %.2f" model.Result ) ] 255 | ] 256 | ] 257 | div [ classy "column" ] [] 258 | ] 259 | 260 | // Using createSimpleApp instead of createApp since our 261 | // update function doesn't generate any actions. See 262 | // some of the other more advanced examples for how to 263 | // use createApp. In addition to the application functions 264 | // we also need to specify which renderer to use. 265 | createApp Model.Initial view update Virtualdom.createRender 266 | |> withStartNodeSelector "#sample" 267 | |> withProducer (routeProducer locationHandler router) 268 | |> withSubscriber (routeSubscriber locationHandler routerF) 269 | |> start 270 | 271 | if location.hash = "" then 272 | location.hash <- "/" 273 | else 274 | // Else trigger hashchange to navigate to current route 275 | window.dispatchEvent(Event.Create("hashchange")) |> ignore 276 | --------------------------------------------------------------------------------