A tasty Haskell front-end web framework
Miso is a small, production-ready, "isomorphic" Haskell front-end framework for quickly building highly interactive single-page web applications. It features a virtual-dom, recursive diffing / patching algorithm, attribute and property normalization, event delegation, event batching, SVG, Server-sent events, Websockets, type-safe servant-style routing and an extensible Subscription-based subsystem. Inspired by Elm, Redux and Bobril. Miso is pure by default, but side effects (like XHR
) can be introduced into the system via the Effect
data type. Miso makes heavy use of the GHCJS FFI and therefore has minimal dependencies. Miso can be considered a shallow embedded domain-specific language for modern web programming.
To get started quickly building applications, we recommend using the nix
package manager with miso's binary cache provided by cachix
. It is possible to use stack
to build GHCJS projects, but support for procuring GHCJS
has been removed as of stack 2.0. nix
is used to procure a working version of GHCJS
. If you're using cabal
we assume you have obtained GHCJS
by other means. All source code depicted below for the quick start app is available here.
To build the sample-app with nix
, execute the commands below:
# optional use of cache
nix-env -iA cachix -f https://cachix.org/api/v1/install
# optional use of cache
cachix use miso-haskell
git clone https://github.com/dmjio/miso
cd miso/sample-app
nix-build
open ./result/bin/app.jsexe/index.html
The above commands will add miso's binary cache to your nix installation (support for both Linux and OSX).
nix-build
will fetch the dependencies from miso's cache and build the sample application.
Nix
is a more powerful option for building web applications with miso
since it encompasses development workflow, configuration management, and deployment. The source code for haskell-miso.org
is an example of this.
If unfamiliar with nix
, we recommend @Gabriella439's "Nix and Haskell in production" guide.
To begin, make the following directory layout:
➜ mkdir app && touch app/{Main.hs,app.cabal,default.nix} && tree app
app
|-- Main.hs
|-- app.cabal
`-- default.nix
Add a cabal
file
➜ cat app/*.cabal
name: app
version: 0.1.0.0
synopsis: First miso app
category: Web
build-type: Simple
cabal-version: >=1.10
executable app
main-is: Main.hs
ghcjs-options:
-dedupe
build-depends: base, miso
default-language: Haskell2010
Write a default.nix
(this will fetch a recent version of miso
). miso
will provide you with a working nixpkgs
named pkgs
. callCabal2nix
will automatically produce a nix expression that builds your cabal file.
with (import (builtins.fetchGit {
url = "https://github.com/dmjio/miso";
ref = "refs/tags/1.8";
}) {});
pkgs.haskell.packages.ghcjs.callCabal2nix "app" ./. {}
Add the source from Sample Application to app/Main.hs
Build the project
nix-build
Open the result
open ./result/bin/app.jsexe/index.html
For development with nix
, it can be nice to have cabal
present for building. This command will make it available in your PATH
.
nix-env -iA cabal-install -f '<nixpkgs>'
To be put into a shell w/ GHCJS
and all the dependencies for this project present, use nix-shell
.
nix-shell -A env
To view the dependencies for your project, call ghcjs-pkg list
when inside the shell.
nix-shell -A env --run 'ghcjs-pkg list'
To build the project with cabal
after entering the nix-shell
nix-shell -A env --run 'cabal configure --ghcjs && cabal build'
For incremental development inside of the nix-shell
we recommend using a tool like entr
to automatically rebuild on file changes, or roll your own solution with inotify
.
ag -l | entr sh -c 'cabal build'
For constructing client and server applications, we recommend using one cabal
file with two executable sections, where the buildable
attribute set is contingent on the compiler. An example of this layout is here. For more info on how to use stack
with a client
/server
setup, see this link. For more information on how to use nix
with a client
/server
setup, see the nix scripts for https://haskell-miso.org.
-- | Haskell language pragma
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
-- | Haskell module declaration
module Main where
-- | Miso framework import
import Miso
import Miso.String
-- | Type synonym for an application model
type Model = Int
-- | Sum type for application events
data Action
= AddOne
| SubtractOne
| NoOp
| SayHelloWorld
deriving (Show, Eq)
-- | Entry point for a miso application
main :: IO ()
main = startApp App {..}
where
initialAction = SayHelloWorld -- initial action to be executed on application load
model = 0 -- initial model
update = updateModel -- update function
view = viewModel -- view function
events = defaultEvents -- default delegated events
subs = [] -- empty subscription list
mountPoint = Nothing -- mount point for application (Nothing defaults to 'body')
logLevel = Off -- used during prerendering to see if the VDOM and DOM are in sync (only applies to `miso` function)
-- | Updates model, optionally introduces side effects
updateModel :: Action -> Model -> Effect Action Model
updateModel action m =
case action of
AddOne
-> noEff (m + 1)
SubtractOne
-> noEff (m - 1)
NoOp
-> noEff m
SayHelloWorld
-> m <# do consoleLog "Hello World" >> pure NoOp
-- | Constructs a virtual DOM from a model
viewModel :: Model -> View Action
viewModel x = div_ [] [
button_ [ onClick AddOne ] [ text "+" ]
, text (ms x)
, button_ [ onClick SubtractOne ] [ text "-" ]
]
An alternative, more powerful interface for constructing miso
applications is using the Transition
interface.
Transition
is based on the StateT
monad transformer, and can be used to construct components. It also works
very nicely with lenses based on MonadState
(i.e. (.=)
, (%=)
,(+=)
,(-=)
).
-- | Haskell language pragma
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
-- | Haskell module declaration
module Main where
-- | Miso framework import
import Miso
import Miso.String
-- | Lens import
import Control.Lens
-- | Type synonym for an application model
data Model
= Model
{ _counter :: Int
} deriving (Show, Eq)
counter :: Lens' Model Int
counter = lens _counter $ \record field -> record { _counter = field }
-- | Sum type for application events
data Action
= AddOne
| SubtractOne
| NoOp
| SayHelloWorld
deriving (Show, Eq)
-- | Entry point for a miso application
main :: IO ()
main = startApp App {..}
where
initialAction = SayHelloWorld -- initial action to be executed on application load
model = Model 0 -- initial model
update = fromTransition . updateModel -- update function
view = viewModel -- view function
events = defaultEvents -- default delegated events
subs = [] -- empty subscription list
mountPoint = Nothing -- mount point for application (Nothing defaults to 'body')
logLevel = Off -- used during prerendering to see if the VDOM and DOM are in sync (only applies to `miso` function)
-- | Updates model, optionally introduces side effects
updateModel :: Action -> Transition Action Model ()
updateModel action =
case action of
AddOne
-> counter += 1
SubtractOne
-> counter -= 1
NoOp
-> pure ()
SayHelloWorld
-> scheduleIO_ (consoleLog "Hello World")
-- | Constructs a virtual DOM from a model
viewModel :: Model -> View Action
viewModel x = div_ [] [
button_ [ onClick AddOne ] [ text "+" ]
, text . ms $ x^.counter
, button_ [ onClick SubtractOne ] [ text "-" ]
]
It is possible to build miso
applications with ghcid
, jsaddle
that allow live reloading of your application in reponse to changes in application code. See the README in the sample-app-jsaddle
folder for more information.
Developing miso applications inside a Docker container is supported (allows applications to be built on Windows). See the README in the docker
folder for more information.
The easiest way to build the examples is with the nix
package manager
git clone https://github.com/dmjio/miso && cd miso && nix-build --arg examples true
This will build all examples and documentation into a folder named result
➜ miso git:(master) ✗ tree -d ./result/bin
./result/bin
|-- canvas2d.jsexe
|-- compose-update.jsexe
|-- file-reader.jsexe
|-- mario.jsexe
| `-- imgs
|-- mathml.jsexe
|-- router.jsexe
|-- simple.jsexe
|-- svg.jsexe
|-- tests.jsexe
|-- threejs.jsexe
|-- todo-mvc.jsexe
|-- websocket.jsexe
`-- xhr.jsexe
To see examples, we recommend hosting them with a webserver
cd result/bin/todo-mvc.jsexe && nix-shell -p python --run 'python -m SimpleHTTPServer'
Serving HTTP on 0.0.0.0 port 8000 ...
The core algorithmic component of miso is diff.js. It is responsible for all DOM manipulation that occurs in a miso application and has 100% code coverage. Tests and coverage made possible using jsdom and jest.
To run the tests and build the coverage report:
cd miso/tests
npm i
npm run test
## Or by using `yarn` instead of `npm`:
# yarn
# yarn test
Isomorphic javascript is a technique for increased SEO, code-sharing and perceived page load times. It works in two parts. First, the server sends a pre-rendered HTML body to the client's browser. Second, after the client javascript application loads, the pointers of the pre-rendered DOM are copied into the virtual DOM, and the application proceeds as normal. All subsequent page navigation is handled locally by the client, avoiding full-page postbacks as necessary.
The miso
function is used to perform the pointer-copying behavior client-side.
For more information on how miso
handles isomorphic javascript, we recommend this tutorial.
By default miso
uses a known-to-work, pinned version of nixpkgs
.
nix
users on a Linux or OSX distro can take advantage of a binary cache for faster builds. To use the binary cache follow the instructions on cachix.
cachix use miso-haskell
According to benchmarks, miso
is among the fastest functional programming web frameworks, second only to Elm.
Feel free to dive in! Open an issue or submit PRs.
See CONTRIBUTING for more info.
This project exists thanks to all the people who contribute. [Contribute].
Become a financial contributor and help us sustain our community. [Contribute]
Support this project with your organization. Your logo will show up here with a link to your website. [Contribute]
BSD3 © David Johnson