diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -8,22 +8,22 @@
 
 <p align="center">
   <a href="https://haskell-miso-slack.herokuapp.com">
-    <img src="https://img.shields.io/badge/slack-miso-E01563.svg?style=flat-square" alt="Miso Slack">
+	<img src="https://img.shields.io/badge/slack-miso-E01563.svg?style=flat-square" alt="Miso Slack">
   </a>
   <a href="http://hackage.haskell.org/package/miso">
-    <img src="https://img.shields.io/hackage/v/miso.svg?style=flat-square" alt="Hackage">
+	<img src="https://img.shields.io/hackage/v/miso.svg?style=flat-square" alt="Hackage">
   </a>
   <a href="https://haskell.org">
-    <img src="https://img.shields.io/badge/language-Haskell-green.svg?style=flat-square" alt="Haskell">
+	<img src="https://img.shields.io/badge/language-Haskell-green.svg?style=flat-square" alt="Haskell">
   </a>
   <a href="https://github.com/dmjio/miso/blob/master/LICENSE">
-    <img src="http://img.shields.io/badge/license-BSD3-brightgreen.svg?style=flat-square" alt="LICENSE">
+	<img src="http://img.shields.io/badge/license-BSD3-brightgreen.svg?style=flat-square" alt="LICENSE">
   </a>
   <a href="https://hydra.dmj.io">
-    <img src="https://img.shields.io/badge/build-Hydra-00BDFD.svg?style=flat-square" alt="Miso Hydra">
+	<img src="https://img.shields.io/badge/build-Hydra-00BDFD.svg?style=flat-square" alt="Miso Hydra">
   </a>
   <a href="https://www.irccloud.com/invite?channel=%23haskell-miso&amp;hostname=irc.freenode.net&amp;port=6697&amp;ssl=1">
-    <img src="https://img.shields.io/badge/irc-%23haskell--miso-1e72ff.svg?style=flat-square" alt="IRC #haskell-miso">
+	<img src="https://img.shields.io/badge/irc-%23haskell--miso-1e72ff.svg?style=flat-square" alt="IRC #haskell-miso">
   </a>
 </p>
 
@@ -58,6 +58,7 @@
   - [GHCJS](#ghcjs)
 - [Sample Application](#sample-application)
 - [Building examples](#building-examples)
+- [Coverage](#coverage)
 - [Isomorphic](#isomorphic)
 - [Pinning nixpkgs](#pinning-nixpkgs)
 - [Binary cache](#binary-cache)
@@ -95,9 +96,9 @@
 Note: It's important to ensure that you don't have a global `cabal-install` present on your system. This could cause build problems. If you see an error like this (below), try deleting your global `cabal-install`.
 
 ```bash
-exit status: 1                                
-stderr: solver must be one of: modular        
-CallStack (from HasCallStack):                
+exit status: 1
+stderr: solver must be one of: modular
+CallStack (from HasCallStack):
   error, called at libraries/Cabal/Cabal/Distribution/ReadE.hs:46:24 in Cabal-2.0.1.0:Distribution.ReadE
 ```
 
@@ -131,10 +132,10 @@
 
 setup-info:
   ghcjs:
-    source:
-      ghcjs-0.2.0.9006030_ghc-7.10.3:
-         url: http://ghcjs.tolysz.org/lts-6.30-9006030.tar.gz
-         sha1: 2371e2ffe9e8781808b7a04313e6a0065b64ee51
+	source:
+	  ghcjs-0.2.0.9006030_ghc-7.10.3:
+		 url: http://ghcjs.tolysz.org/lts-6.30-9006030.tar.gz
+		 sha1: 2371e2ffe9e8781808b7a04313e6a0065b64ee51
 ```
 
 Add a `cabal` file
@@ -255,18 +256,18 @@
 Write a `default.nix` (which calls `app.nix`), this fetches a recent version of `miso`.
 ```nix
 { pkgs ? import ((import <nixpkgs> {}).fetchFromGitHub {
-    owner = "NixOS";
-    repo = "nixpkgs";
-    rev = "a0aeb23";
-    sha256 = "04dgg0f2839c1kvlhc45hcksmjzr8a22q1bgfnrx71935ilxl33d";
+	owner = "NixOS";
+	repo = "nixpkgs";
+	rev = "a0aeb23";
+	sha256 = "04dgg0f2839c1kvlhc45hcksmjzr8a22q1bgfnrx71935ilxl33d";
   }){}
 }:
 let
   result = import (pkgs.fetchFromGitHub {
-    owner = "dmjio";
-    repo = "miso";
-    sha256 = "1l1gwzzqlvvcmg70jjrwc5ijv1vb6y5ljqkh7rxxq7hkyxpjyx9q";
-    rev = "95f6bc9b1ae6230b110358a82b6a573806f272c2";
+	owner = "dmjio";
+	repo = "miso";
+	sha256 = "1l1gwzzqlvvcmg70jjrwc5ijv1vb6y5ljqkh7rxxq7hkyxpjyx9q";
+	rev = "95f6bc9b1ae6230b110358a82b6a573806f272c2";
   }) {};
 in pkgs.haskell.packages.ghcjs.callPackage ./app.nix {
   miso = result.miso-ghcjs;
@@ -411,13 +412,13 @@
 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')
+	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')
 
 -- | Updates model, optionally introduces side effects
 updateModel :: Action -> Model -> Effect Action Model
@@ -476,6 +477,21 @@
 Serving HTTP on 0.0.0.0 port 8000 ...
 ```
 
+## Coverage
+
+The core algorithmic component of miso is [diff.js](https://github.com/dmjio/miso/blob/master/jsbits/diff.js). It is responsible for all DOM manipulation that occurs in a miso application and has [100% code coverage](http://coverage.haskell-miso.org). Tests and coverage made possible using [jsdom](https://github.com/jsdom/jsdom) and [jest](https://github.com/facebook/jest).
+
+To run the tests and build the coverage report:
+
+```bash
+cd miso/tests
+npm i
+npm run test
+## Or by using `yarn` instead of `npm`:
+# yarn
+# yarn test
+```
+
 ## Isomorphic
 
 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.
@@ -495,16 +511,16 @@
 
 ## Binary cache
 
-`nix` users on a Linux distro can take advantage of a [binary cache](https://cache.dmj.io/nix-cache-info) for faster builds. To use the binary cache simply append `https://cache.dmj.io/nix-cache-info` during all `nix-shell` and/or `nix-build` invocations.
+`nix` users on a Linux distro can take advantage of a [binary cache](https://hydra.dmj.io/nix-cache-info) for faster builds. To use the binary cache simply append `https://hydra.dmj.io/nix-cache-info` during all `nix-shell` and/or `nix-build` invocations.
 
 ```bash
-nix-build --option extra-binary-caches https://cache.dmj.io
+nix-build --option extra-binary-caches https://hydra.dmj.io
 ```
 
-Alternatively, add `https://cache.dmj.io` to your list of local binary caches in `nix.conf` (usually found in `/etc/nix/nix.conf`), and it will automatically be used on all invocations of `nix-build` and/or `nix-shell`.
+Alternatively, add `https://hydra.dmj.io` to your list of local binary caches in `nix.conf` (usually found in `/etc/nix/nix.conf`), and it will automatically be used on all invocations of `nix-build` and/or `nix-shell`.
 
 ```
-binary-caches = https://cache.dmj.io/ https://cache.nixos.org/
+binary-caches = https://hydra.dmj.io/ https://cache.nixos.org/
 ```
 
 ## Benchmarks
diff --git a/examples/compose-update/Main.hs b/examples/compose-update/Main.hs
--- a/examples/compose-update/Main.hs
+++ b/examples/compose-update/Main.hs
@@ -13,7 +13,6 @@
 import Data.Monoid
 
 import Miso
-import Miso.Lens
 import Miso.String
 
 -- In this slightly contrived example, our model consists of two
@@ -32,6 +31,7 @@
 -- utilities ourselves. We recommend that in your own applications,
 -- you depend on a lens library such as @lens@ or @microlens@ to get
 -- these definitions.
+type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t
 
 -- | You can find this under the same name in @lens@ and
 -- @microlens@. @lens@ also provides the infix operator '%%~' as a
diff --git a/examples/router/Main.hs b/examples/router/Main.hs
--- a/examples/router/Main.hs
+++ b/examples/router/Main.hs
@@ -22,13 +22,6 @@
     -- ^ current URI of application
   } deriving (Eq, Show)
 
--- | HasURI typeclass
-instance HasURI Model where
-  lensURI = makeLens getter setter
-    where
-      getter = uri
-      setter = \m u -> m { uri = u }
-
 -- | Action
 data Action
   = HandleURI URI
@@ -59,10 +52,11 @@
 
 -- | View function, with routing
 viewModel :: Model -> View Action
-viewModel model@Model {..} = view
+viewModel model = view
   where
-    view = either (const the404) id result
-    result = runRoute (Proxy :: Proxy API) handlers model
+    view =
+      either (const the404) id
+        $ runRoute (Proxy :: Proxy API) handlers uri model
     handlers = about :<|> home
     home (_ :: Model) = div_ [] [
         div_ [] [ text "home" ]
diff --git a/ghc-src/Miso.hs b/ghc-src/Miso.hs
--- a/ghc-src/Miso.hs
+++ b/ghc-src/Miso.hs
@@ -17,9 +17,11 @@
   , module Miso.Html
   , module Miso.Router
   , module Miso.TypeLevel
+  , module Miso.Util
   ) where
 
 import           Miso.Event
 import           Miso.Html
 import           Miso.Router
 import           Miso.TypeLevel
+import           Miso.Util
diff --git a/ghcjs-src/Miso.hs b/ghcjs-src/Miso.hs
--- a/ghcjs-src/Miso.hs
+++ b/ghcjs-src/Miso.hs
@@ -23,6 +23,7 @@
   , module Miso.Types
   , module Miso.Router
   , module Miso.Util
+  , module Miso.FFI
   ) where
 
 import           Control.Concurrent
@@ -31,6 +32,7 @@
 import           Data.List
 import           Data.Sequence                 ((|>))
 import qualified Data.Sequence                 as S
+import           GHCJS.Types (JSVal)
 import qualified JavaScript.Object.Internal    as OI
 import           JavaScript.Web.AnimationFrame
 
@@ -95,12 +97,11 @@
 
 -- | Runs an isomorphic miso application
 -- Assumes the pre-rendered DOM is already present
-miso :: (HasURI model, Eq model) => App model action -> IO ()
-miso app@App{..} = do
-  uri <- getCurrentURI
-  let modelWithUri = setURI uri model
+miso :: Eq model => (URI -> App model action) -> IO ()
+miso f = do
+  app@App {..} <- f <$> getCurrentURI
   common app model $ \writeEvent -> do
-    let initialView = view modelWithUri
+    let initialView = view model
     VTree (OI.Object iv) <- flip runView writeEvent initialView
     -- Initial diff can be bypassed, just copy DOM into VTree
     copyDOMIntoVTree iv
@@ -131,3 +132,8 @@
             void $ forkIO (eff sink)
 
 data Acc model = Acc !model !(IO ())
+
+-- | Copies DOM pointers into virtual dom
+-- entry point into isomorphic javascript
+foreign import javascript unsafe "copyDOMIntoVTree($1);"
+  copyDOMIntoVTree :: JSVal -> IO ()
diff --git a/ghcjs-src/Miso/Delegate.hs b/ghcjs-src/Miso/Delegate.hs
--- a/ghcjs-src/Miso/Delegate.hs
+++ b/ghcjs-src/Miso/Delegate.hs
@@ -13,7 +13,6 @@
 import qualified Data.Map                 as M
 import           Miso.Html.Internal
 import           Miso.String
-import           Miso.FFI
 import qualified JavaScript.Object.Internal as OI
 import           GHCJS.Foreign.Callback
 import           GHCJS.Marshal
@@ -32,3 +31,11 @@
     pure val
   delegateEvent mountPointElement evts getVTreeFromRef
 
+-- | Event delegation FFI, routes events received on body through the virtual dom
+-- Invokes event handler when found
+foreign import javascript unsafe "delegate($1, $2, $3);"
+  delegateEvent
+     :: JSVal               -- ^ mountPoint element
+     -> JSVal               -- ^ Events
+     -> Callback (IO JSVal) -- ^ Virtual DOM callback
+     -> IO ()
diff --git a/ghcjs-src/Miso/Diff.hs b/ghcjs-src/Miso/Diff.hs
--- a/ghcjs-src/Miso/Diff.hs
+++ b/ghcjs-src/Miso/Diff.hs
@@ -31,14 +31,15 @@
 -- | diffing / patching a given element
 diffElement :: JSVal -> Maybe VTree -> Maybe VTree -> IO ()
 diffElement mountEl current new = do
+  doc <- getDoc
   case (current, new) of
     (Nothing, Nothing) -> pure ()
     (Just (VTree current'), Just (VTree new')) -> do
-      diff' current' new' mountEl
+      diff' current' new' mountEl doc
     (Nothing, Just (VTree new')) -> do
-      diff' (Object jsNull) new' mountEl
+      diff' (Object jsNull) new' mountEl doc
     (Just (VTree current'), Nothing) -> do
-      diff' current' (Object jsNull) mountEl
+      diff' current' (Object jsNull) mountEl doc
 
 -- | return the configured mountPoint element or the body
 mountElement :: Maybe JSString -> IO JSVal
@@ -50,12 +51,16 @@
 foreign import javascript unsafe "$r = document.body;"
   getBody :: IO JSVal
 
+foreign import javascript unsafe "$r = document;"
+  getDoc :: IO JSVal
+
 foreign import javascript unsafe "$r = document.getElementById($1);"
   getElementById :: JSString -> IO JSVal
 
-foreign import javascript unsafe "diff($1, $2, $3);"
+foreign import javascript unsafe "diff($1, $2, $3, $4);"
   diff'
     :: Object -- ^ current object
     -> Object -- ^ new object
     -> JSVal  -- ^ parent node
+    -> JSVal  -- ^ document
     -> IO ()
diff --git a/ghcjs-src/Miso/FFI.hs b/ghcjs-src/Miso/FFI.hs
--- a/ghcjs-src/Miso/FFI.hs
+++ b/ghcjs-src/Miso/FFI.hs
@@ -20,10 +20,8 @@
    , consoleLog
    , stringify
    , parse
-   , copyDOMIntoVTree
    , item
    , jsvalToValue
-   , delegateEvent
    , clearBody
    ) where
 
@@ -122,20 +120,6 @@
 -- | Indexing into a JS object
 foreign import javascript unsafe "$r = $1[$2];"
   item :: JSVal -> JSString -> IO JSVal
-
--- | Copies DOM pointers into virtual dom
--- entry point into isomorphic javascript
-foreign import javascript unsafe "copyDOMIntoVTree($1);"
-  copyDOMIntoVTree :: JSVal -> IO ()
-
--- | Event delegation FFI, routes events received on body through the virtual dom
--- Invokes event handler when found
-foreign import javascript unsafe "delegate($1, $2, $3);"
-  delegateEvent
-     :: JSVal               -- ^ mountPoint element
-     -> JSVal               -- ^ Events
-     -> Callback (IO JSVal) -- ^ Virtual DOM callback
-     -> IO ()
 
 -- | Clear the document body. This is particularly useful to avoid
 -- creating multiple copies of your app when running in GHCJSi.
diff --git a/ghcjs-src/Miso/Subscription/History.hs b/ghcjs-src/Miso/Subscription/History.hs
--- a/ghcjs-src/Miso/Subscription/History.hs
+++ b/ghcjs-src/Miso/Subscription/History.hs
@@ -1,8 +1,8 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE TypeOperators     #-}
 {-# LANGUAGE DataKinds         #-}
+{-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE RecordWildCards   #-}
 {-# LANGUAGE TypeFamilies      #-}
+{-# LANGUAGE TypeOperators     #-}
 -----------------------------------------------------------------------------
 -- |
 -- Module      :  Miso.Subscription.History
@@ -23,14 +23,14 @@
   , URI (..)
   ) where
 
-import Control.Concurrent
-import Control.Monad
-import GHCJS.Foreign.Callback
-import Miso.Concurrent
-import Miso.Html.Internal     ( Sub )
-import Miso.String
-import Network.URI            hiding (path)
-import System.IO.Unsafe
+import           Control.Concurrent
+import           Control.Monad
+import           GHCJS.Foreign.Callback
+import           Miso.Concurrent
+import           Miso.Html.Internal     (Sub)
+import           Miso.String
+import           Network.URI            hiding (path)
+import           System.IO.Unsafe
 
 -- | Retrieves current URI of page
 getCurrentURI :: IO URI
@@ -41,11 +41,10 @@
 getURI :: IO URI
 {-# INLINE getURI #-}
 getURI = do
-  URI <$> do unpack <$> getProtocol
-      <*> pure Nothing
-      <*> do Prelude.drop 1 . unpack <$> getPathName
-      <*> do unpack <$> getSearch
-      <*> do unpack <$> getHash
+  href <- fromMisoString <$> getWindowLocationHref
+  case parseURI href of
+    Nothing  -> fail $ "Could not parse URI from window.location: " ++ href
+    Just uri -> return uri
 
 -- | Pushes a new URI onto the History stack
 pushURI :: URI -> IO ()
@@ -92,6 +91,9 @@
      asyncCallback $ do
       sink =<< f <$> getURI
 
+foreign import javascript safe "$r = window.location.href || '';"
+  getWindowLocationHref :: IO MisoString
+
 foreign import javascript unsafe "window.history.go($1);"
   go' :: Int -> IO ()
 
@@ -100,18 +102,6 @@
 
 foreign import javascript unsafe "window.history.forward();"
   forward' :: IO ()
-
-foreign import javascript unsafe "$r = window.location.pathname;"
-  getPathName :: IO JSString
-
-foreign import javascript unsafe "$r = window.location.search;"
-  getSearch :: IO JSString
-
-foreign import javascript unsafe "$r = window.location.hash;"
-  getHash :: IO JSString
-
-foreign import javascript unsafe "$r = window.location.protocol;"
-  getProtocol :: IO JSString
 
 foreign import javascript unsafe "window.addEventListener('popstate', $1);"
   onPopState :: Callback (IO ()) -> IO ()
diff --git a/ghcjs-src/Miso/Util.hs b/ghcjs-src/Miso/Util.hs
deleted file mode 100644
--- a/ghcjs-src/Miso/Util.hs
+++ /dev/null
@@ -1,45 +0,0 @@
------------------------------------------------------------------------------
--- |
--- Module      :  Miso.Util
--- Copyright   :  (C) 2016-2018 David M. Johnson
--- License     :  BSD3-style (see the file LICENSE)
--- Maintainer  :  David M. Johnson <djohnson.m@gmail.com>
--- Stability   :  experimental
--- Portability :  non-portable
-----------------------------------------------------------------------------
-module Miso.Util
-  ( now
-  , consoleLog
-  , jsvalToValue
-  , windowAddEventListener
-  , stringify
-  , parse
-
-  , withFoldable
-  , conditionalViews
-  ) where
-
-import Miso.FFI
-import Miso.Html.Internal
-import Data.Foldable
-
--- | Generic @map@ function, useful for creating @View@s from the elements of
--- some @Foldable@. Particularly handy for @Maybe@, as shown in the example
--- below.
---
--- @
--- view model =
---     div_ [] $
---      withFoldable (model ^. mSomeMaybeVal) $ \someVal ->
---         p_ [] [ text $ "Hey, look at this value: " <> ms (show someVal) ]
--- @
-withFoldable :: Foldable t => t a -> (a -> b) -> [b]
-withFoldable ta f = map f (toList ta)
-
--- | Hides the @View@s the condition is False. Shows them when the condition
--- is True.
-conditionalViews :: Bool -> [View action] -> [View action]
-conditionalViews condition views =
-    if condition
-    then views
-    else []
diff --git a/jsbits/diff.js b/jsbits/diff.js
--- a/jsbits/diff.js
+++ b/jsbits/diff.js
@@ -1,216 +1,203 @@
 /* virtual-dom diffing algorithm, applies patches as detected */
-function diff (currentObj, newObj, parent) {
-  if (!currentObj && !newObj) return;
-  else if (!currentObj && newObj) createNode (newObj, parent);
-  else if (currentObj && !newObj) destroyNode(currentObj, parent);
-  else {
-    if (currentObj.type === "vtext") {
-      if (newObj.type === "vnode") replaceTextWithElement (currentObj, newObj, parent);
-      else diffTextNodes (currentObj, newObj);
-    } else {
-      if (newObj.type === "vnode") diffVNodes (currentObj, newObj, parent);
-      else replaceElementWithText (currentObj, newObj, parent);
+function diff(currentObj, newObj, parent, doc) {
+    if (!currentObj && !newObj) return;
+    else if (!currentObj && newObj) createNode(newObj, parent, doc);
+    else if (currentObj && !newObj) destroyNode(currentObj, parent);
+    else {
+	if (currentObj.type === "vtext") {
+	    if (newObj.type === "vnode") replaceTextWithElement(currentObj, newObj, parent, doc);
+	    else diffTextNodes(currentObj, newObj);
+	} else {
+	    if (newObj.type === "vnode") diffVNodes(currentObj, newObj, parent, doc);
+	    else replaceElementWithText(currentObj, newObj, parent, doc);
+	}
     }
-  }
 }
 
-function diffTextNodes (c, n) {
-  if (c.text !== n.text) c.domRef.textContent = n.text;
-  n.domRef = c.domRef;
+function destroyNode(obj, parent) {
+    parent.removeChild(obj.domRef);
+    callDestroyedRecursive(obj);
 }
 
-function replaceElementWithText (c, n, parent) {
-  n.domRef = document.createTextNode (n.text);
-  parent.replaceChild (n.domRef, c.domRef);
-  callDestroyedRecursive(c);
+function callDestroyedRecursive(obj) {
+    callDestroyed(obj);
+    for (var i in obj.children)
+	callDestroyedRecursive(obj.children[i]);
 }
 
-function replaceTextWithElement (c, n, parent) {
-  createElement (n);
-  parent.replaceChild (n.domRef, c.domRef);
-  callCreated(n);
+function callDestroyed(obj) {
+    if (obj.onDestroyed) obj.onDestroyed();
 }
 
-function populate (c, n) {
-    if (!c) c = {
-	      props : null
-	    , css : null
-	    , children : []
-	    }
-    if (!n) n = {
-	      props : null
-	    , css : null
-	    , children : []
-	    }
-  diffProps (c.props, n.props, n.domRef, n.ns === "svg");
-  diffCss (c.css, n.css, n.domRef);
-  diffChildren (c.children, n.children, n.domRef);
+function diffTextNodes(c, n) {
+    if (c.text !== n.text) c.domRef.textContent = n.text;
+    n.domRef = c.domRef;
 }
 
-function diffVNodes (c, n, parent) {
-  if (c.tag === n.tag && n.key === c.key) {
-    n.domRef = c.domRef;
-    populate (c, n);
-  } else {
-    createElement(n);
-    parent.replaceChild (n.domRef, c.domRef);
+function replaceElementWithText(c, n, parent, doc) {
+    n.domRef = doc.createTextNode(n.text);
+    parent.replaceChild(n.domRef, c.domRef);
     callDestroyedRecursive(c);
+}
+
+function replaceTextWithElement(c, n, parent, doc) {
+    createElement(n, doc);
+    parent.replaceChild(n.domRef, c.domRef);
     callCreated(n);
-  }
 }
 
-function diffProps (cProps, nProps, node, isSvg) {
+function callCreated(obj) {
+    if (obj.onCreated) obj.onCreated();
+}
+
+function populate(c, n, doc) {
+    if (!c) c = {
+	props: null,
+	css: null,
+	children: []
+    }
+    diffProps(c.props, n.props, n.domRef, n.ns === "svg");
+    diffCss(c.css, n.css, n.domRef);
+    diffChildren(c.children, n.children, n.domRef, doc);
+}
+
+function diffVNodes(c, n, parent, doc) {
+    if (c.tag === n.tag && n.key === c.key) {
+	n.domRef = c.domRef;
+	populate(c, n, doc);
+    } else {
+	createElement(n, doc);
+	parent.replaceChild(n.domRef, c.domRef);
+	callDestroyedRecursive(c);
+	callCreated(n);
+    }
+}
+
+function diffProps(cProps, nProps, node, isSvg) {
     var result, newProp, domProp;
     /* Is current prop in new prop list? */
     for (var c in cProps) {
 	newProp = nProps[c];
-	 /* If current property no longer exists, remove it */
-	 if (!newProp) {
-	     /* current key is not in node, remove it from DOM, if SVG, remove attribute */
-	     if (isSvg || !(c in node))
-	       node.removeAttribute(c, cProps[c]);
-	     else
-	       node[c] = "";
-	  } else {
-	   /* Already on DOM from previous diff, continue */
-	   if (newProp === cProps[c]) continue;
-	   domProp = node[c];
-	   /* Value in new prop map, not in old, but already pre-populated on DOM, potentially from server-prerendering, continue */
-	   if (newProp === domProp) continue;
-	   if (isSvg) {
-	     if (c === "href")
-		node.setAttributeNS("http://www.w3.org/1999/xlink", "href", newProp);
-	     else
-		node.setAttribute(c, newProp);
+	/* If current property no longer exists, remove it */
+	if (!newProp) {
+	    /* current key is not in node, remove it from DOM, if SVG, remove attribute */
+	    if (isSvg || !(c in node))
+		node.removeAttribute(c, cProps[c]);
+	    else
+		node[c] = '';
+	} else {
+	    /* Already on DOM from previous diff, continue */
+	    if (newProp === cProps[c]) continue;
+	    domProp = node[c];
+	    if (isSvg) {
+		if (c === "href")
+		    node.setAttributeNS("http://www.w3.org/1999/xlink", "href", newProp);
+		else
+		    node.setAttribute(c, newProp);
 	    } else if (c in node && !(c === "list" || c === "form")) {
 		node[c] = newProp;
-	     } else {
-	       node.setAttribute(c, newProp);
-	   }
-       }
+	    } else {
+		node.setAttribute(c, newProp);
+	    }
+	}
     }
-      /* add remaining */
-      for (var n in nProps) {
-	  if (cProps && cProps[n]) continue;
-	  newProp = nProps[n];
-	  /* Only add new properties, skip (continue) if they already exist in current property map */
-	  if (isSvg) {
+    /* add remaining */
+    for (var n in nProps) {
+	if (cProps && cProps[n]) continue;
+	newProp = nProps[n];
+	/* Only add new properties, skip (continue) if they already exist in current property map */
+	if (isSvg) {
 	    if (n === "href")
-	       node.setAttributeNS("http://www.w3.org/1999/xlink", "href", newProp);
+		node.setAttributeNS("http://www.w3.org/1999/xlink", "href", newProp);
 	    else
-	       node.setAttribute(n, newProp);
-	  } else if (n in node && !(n === "list" || n === "form")) {
-	     node[n] = nProps[n];
-	  } else {
-	     node.setAttribute(n, newProp);
-	 }
-     }
+		node.setAttribute(n, newProp);
+	} else if (n in node && !(n === "list" || n === "form")) {
+	    node[n] = nProps[n];
+	} else {
+	    node.setAttribute(n, newProp);
+	}
+    }
 }
 
-function diffCss (cCss, nCss, node) {
+function diffCss(cCss, nCss, node) {
     var result;
     /* is current attribute in new attribute list? */
     for (var c in cCss) {
-     result = nCss[c];
-     if (!result) {
-	/* current key is not in node */
-       node.style[c] = null;
-     } else if (result !== cCss[c]) {
-	node.style[c] = result;
-     }
+	result = nCss[c];
+	if (!result) {
+	    /* current key is not in node */
+	    node.style[c] = null;
+	} else if (result !== cCss[c]) {
+	    node.style[c] = result;
+	}
     }
     /* add remaining */
     for (var n in nCss) {
-      if (cCss && cCss[n]) continue;
-      node.style[n] = nCss[n];
-  }
+	if (cCss && cCss[n]) continue;
+	node.style[n] = nCss[n];
+    }
 }
 
-function hasKey (cs) {
-    return cs && cs[0] && cs[0].key;
+function hasKeys(ns, cs) {
+    return ns.length > 0 && cs.length > 0 && ns[0].key !== null && cs[0].key !== null;
 }
 
-function diffChildren (cs, ns, parent) {
+function diffChildren(cs, ns, parent, doc) {
     var longest = ns.length > cs.length ? ns.length : cs.length;
-//    if (hasKey(cs)) syncChildren (cs, ns, parent);
-    for (var i = 0; i < longest; i++) diff (cs[i], ns[i], parent);
+    if (hasKeys(ns, cs)) {
+	syncChildren(cs, ns, parent, doc);
+    } else {
+	for (var i = 0; i < longest; i++)
+	    diff(cs[i], ns[i], parent, doc);
+    }
 }
 
-function createElement (obj) {
-    obj.domRef = obj.ns === "svg"
-	? document.createElementNS("http://www.w3.org/2000/svg", obj.tag)
-	: document.createElement(obj.tag);
-    populate (null, obj);
+function createElement(obj, doc) {
+    obj.domRef = obj.ns === "svg" ?
+	doc.createElementNS("http://www.w3.org/2000/svg", obj.tag) :
+	doc.createElement(obj.tag);
+    populate(null, obj, doc);
 }
 
-function createNode (obj, parent) {
-    if (obj.type === "vnode") createElement(obj);
-    else obj.domRef = document.createTextNode(obj.text);
+function createNode(obj, parent, doc) {
+    if (obj.type === "vnode") createElement(obj, doc);
+    else obj.domRef = doc.createTextNode(obj.text);
     parent.appendChild(obj.domRef);
     callCreated(obj);
 }
 
-function createNodeDontAppend (obj) {
-  if (obj.type === "vnode") createElement(obj);
-  else obj.domRef = document.createTextNode(obj.text);
-  return obj;
-}
-
-function destroyNode (obj, parent) {
-    parent.removeChild (obj.domRef);
-    callDestroyedRecursive(obj);
-}
-
-function callDestroyed (obj) {
-  if ("onDestroyed" in obj)
-    obj.onDestroyed();
-}
-
-function callDestroyedRecursive (obj) {
-  callDestroyed(obj);
-  for (var i in obj.children)
-    callDestroyedRecursive(obj.children[i]);
-}
-
-function callCreated (obj) {
-  if ("onCreated" in obj)
-    obj.onCreated();
-}
-
 /* Child reconciliation algorithm, inspired by kivi and Bobril */
-function syncChildren (os, ns, parent) {
-    var oldFirstIndex = 0
-    , newFirstIndex = 0
-    , oldLastIndex = os.length - 1
-    , newLastIndex = ns.length - 1
-    , nFirst
-    , nLast
-    , oFirst
-    , oLast
-    , LIS
-    , temp;
+function syncChildren(os, ns, parent, doc) {
+    var oldFirstIndex = 0,
+	newFirstIndex = 0,
+	oldLastIndex = os.length - 1,
+	newLastIndex = ns.length - 1,
+	nFirst, nLast, tmp, found, node;
     for (;;) {
-	 /* check base case, first > last for both new and old
-	   [ ] -- old children empty (fully-swapped)
-	   [ ] -- new children empty (fully-swapped)
-	 */
-	if (newFirstIndex > newLastIndex && oldFirstIndex > oldLastIndex) break;
-	  nFirst = ns[newFirstIndex];
-	  nLast  = ns[newLastIndex];
-	  oFirst = os[oldFirstIndex];
-	  oLast  = os[oldLastIndex];
+	/* check base case, first > last for both new and old
+	  [ ] -- old children empty (fully-swapped)
+	  [ ] -- new children empty (fully-swapped)
+	*/
+	if (newFirstIndex > newLastIndex && oldFirstIndex > oldLastIndex) {
+	    break;
+	}
+
+	/* Initialize */
+	nFirst = ns[newFirstIndex];
+	nLast = ns[newLastIndex];
+	oFirst = os[oldFirstIndex];
+	oLast = os[oldLastIndex];
 	/* No more old nodes, create and insert all remaining nodes
 	   -> [ ] <- old children
 	   -> [ a b c ] <- new children
 	*/
 	if (oldFirstIndex > oldLastIndex) {
-	    var node = createNodeDontAppend(nFirst);
+	    diff(null, nFirst, parent, doc);
 	    /* insertBefore's semantics will append a node if the second argument provided is `null` or `undefined`.
 	       Otherwise, it will insert node.domRef before oLast.domRef. */
-	    parent.insertBefore(node.domRef, oLast ? oLast.domRef : null);
+	    parent.insertBefore(nFirst.domRef, oLast.domRef);
 	    os.splice(newFirstIndex, 0, nFirst);
 	    newFirstIndex++;
-	    continue;
 	}
 	/* No more new nodes, delete all remaining nodes in old list
 	   -> [ a b c ] <- old children
@@ -219,9 +206,9 @@
 	else if (newFirstIndex > newLastIndex) {
 	    tmp = oldLastIndex - oldFirstIndex;
 	    while (tmp >= 0) {
-	      parent.removeChild(os[oldFirstIndex].domRef);
-	      os.splice(oldFirstIndex, 1);
-	      tmp--;
+		parent.removeChild(os[oldFirstIndex].domRef);
+		os.splice(oldFirstIndex, 1);
+		tmp--;
 	    }
 	    break;
 	}
@@ -231,157 +218,127 @@
 	   check if nFirst and oFirst align, if so, check nLast and oLast
 	*/
 	else if (oFirst.key === nFirst.key) {
-	    newFirstIndex++;
-	    oldFirstIndex++;
-	    continue;
+	    diff(os[oldFirstIndex++], ns[newFirstIndex++], parent, doc);
 	} else if (oLast.key === nLast.key) {
-	    newLastIndex--;
-	    oldLastIndex--;
-	    continue;
+	    diff(os[oldLastIndex--], ns[newLastIndex--], parent, doc);
 	}
 	/* flip-flop case, nodes have been swapped, in some way or another
 	   both could have been swapped.
 	   -> [ a b c ] <- old children
-	   -> [ c b a ] <- new children */
+	   -> [ c b a ] <- new children
+	*/
 	else if (oFirst.key === nLast.key && nFirst.key === oLast.key) {
-	    var nextSib = oFirst.domRef.nextSibling;
-	    parent.insertBefore(oFirst.domRef, oLast.domRef);
-	    parent.insertBefore(nextSib, oLast.domRef);
+	    swapDomRefs(node, oFirst.domRef, oLast.domRef, parent);
+	    swap(os, oldFirstIndex, oldLastIndex);
+	    diff(os[oldFirstIndex++], ns[newFirstIndex++], parent, doc);
+	    diff(os[oldLastIndex--], ns[newLastIndex--], parent, doc);
+	}
+	/* Or just one could be swapped (d's align here)
+	       This is top left and bottom right match case.
+	       We move d to end of list, mutate old vdom to reflect the change
+	       We then continue without affecting indexes, hoping to land in a better case
+	       -> [ d a b ] <- old children
+	       -> [ a b d ] <- new children
+	       becomes
+	       -> [ a b d ] <- old children
+	       -> [ a b d ] <- new children
+	       and now we happy path
+	   */
+	else if (oFirst.key === nLast.key) {
+	    /* insertAfter */
+	    parent.insertBefore(oFirst.domRef, oLast.domRef.nextSibling);
+	    /* swap positions in old vdom */
+	    os.splice(oldLastIndex,0,os.splice(oldFirstIndex,1)[0]);
+	    diff(os[oldLastIndex--], ns[newLastIndex--], parent, doc);
+	}
+	/* This is top right and bottom lefts match case.
+	   We move d to end of list, mutate old vdom to reflect the change
+	   -> [ b a d ] <- old children
+	   -> [ d b a ] <- new children
+	   becomes
+	   -> [ d b a ] <- old children
+	   -> [ d b a ] <- new children
+	   and now we happy path
+	*/
+	else if (oLast.key === nFirst.key) {
+	    /* insertAfter */
+	    parent.insertBefore(oLast.domRef, oFirst.domRef);
+	    /* swap positions in old vdom */
+	    os.splice(oldFirstIndex,0, os.splice(oldLastIndex,1)[0]);
+	    diff(os[oldFirstIndex++], nFirst, parent, doc);
 	    newFirstIndex++;
-	    oldFirstIndex++;
-	    oldLastIndex--;
-	    newLastIndex--;
-	    continue;
 	}
 
-	/* or just one could be swapped (d's align here)
-	   -> [ d b g ] <- old children
-	   -> [ a k d ] <- new children
-	   on either side (e's align here)
-	   -> [ e b c ] <- old children
-	   -> [ b c e ] <- new children */
-
-	/* For now, the above case is handled in the "you're screwed case" below. */
-
 	/* The "you're screwed" case, nothing aligns, pull the ripcord, do something more fancy
 	   This can happen when the list is sorted, for example.
 	   -> [ a e c ] <- old children
 	   -> [ b e d ] <- new children
 	*/
-
 	else {
-	    var P = [], I = {}, i = 0, nLen = newLastIndex - newFirstIndex, oLen = oldLastIndex - oldFirstIndex,
-		foundKey, last = -1, moved = false, newNodeIndex, removedNodes = 0;
-	    /* Create array with length of new children list */
-	    /* -1 means a new node should be inserted */
-	    for (i = nLen; i > 0; i--) P.append(-1);
-	    /* Create index I that maps keys with node positions of the remaining nodes from the new children */
-	    for (i = newFirstIndex; i <= newLastIndex; i++) I[ns[i].key] = i;
-	    /* Iterate over old nodes with Index, check if we can find node with same key in index */
-	    for (i = oldFirstIndex; i <= oldLastIndex; i++) {
-		node = os[i]; /* This will always return a match in this loop */
-		newNodeIndex = I[node.key];
-		/* If old node doesn't exist in new node list, remove it */
-		if (!newNodeIndex) {
-		    parent.removeChild(node.domRef);
-		    removedNodes++;
+	    /* final case, perform linear search to check if new key exists in old map, decide what to do from there */
+	    found = false;
+	    tmp = oldFirstIndex;
+	    while (tmp <= oldLastIndex) {
+		if (os[tmp].key === nFirst.key) {
+		    found = true;
+		    node = os[tmp];
+		    break;
 		}
-		/* Found new node in index map */
-		else {
-		    /* Assign position of the node in the old children list to the positions array. */
-		    P[newNodeIndex] = i;
-		    /* When assigning positions in the positions array, we also keep the last seen node position of the new children
-		       list. If the last seen position is larger than current position of the node at the new list, then we are switching
-		       `moved` flag to `true`. */
+		tmp++;
+	    }
+   	        /* If new key was found in old map this means it was moved, hypothetically as below
+		   -> [ a e b c ] <- old children
+		   -> [ b e a j ] <- new children
+			^
+		   In the above case 'b' has been moved, so we need to insert 'b' before 'a' in both vDOM and DOM
+		   We also increase oldFirstIndex and newFirstIndex.
 
-		    /* First check if last seen node position is larger than current node position */
-		    if (last > newNodeIndex) moved = true;
-		    /* Update last seen */
-		    last = newNodeIndex;
-		}
+		   This results in new list below w/ updated index position
+		   -> [ b a e c ] <- old children
+		   -> [ b e a j ] <- new children
+			  ^
+		*/
+	    if (found) {
+		/* Move item to correct position */
+		os.splice(oldFirstIndex,0, os.splice(tmp,1)[0]);
+  		/* Swap DOM references */
+		parent.insertBefore(node.domRef, os[oldFirstIndex].domRef);
+		/* optionally perform `diff` here */
+		diff(os[oldFirstIndex++], nFirst, parent, doc);
+		/* increment counters */
+		newFirstIndex++;
 	    }
-	    /* If `moved` flag is on, or if the length of the old children list minus the number of
-	       removed nodes isn't equal to the length of the new children list. Then go to the next step */
-	    if (moved /* DMJ: not sure we need this predicate ? ===> */ || (oLen - removedNodes !== nLen)) {
-		/* Find minimum number of moves if `moved` flag is on, or insert new nodes if the length is changed. */
-		LIS = lis(P);
-		lisIndex = LIS.length - 1;
-		while (nLen > -1) {
-		    if (P[lisIndex] === nLen) {
-			listIndex--;
-			continue;
-		    }
-		    else if (LIS[lisIndex] !== nLen) {
-			/* node has moved */
-			ns[nLen].domRef = os[P[nLen]].domRef;
-			parent.insertBefore(ns[nLen].domRef, os[nLen].domRef);
-		    }
-		    nLen--;
-		}
-	    } else if (!moved) {
-		/* When moved flag is off, we don't need to find LIS, and we just
-		   iterate over the new children list and check its
-		   current position in the positions array, if it is `-1`,
-		   then we insert new node. */
-		for (i = newFirstIndex; i <= newLastIndex; i++) {
-		    if (P[i] === -1) {
-			createNodeDontAppend(ns[i]);
-			/* Replace whatever is at current */
-			/* parent.replaceChild(ns[i].domRef, parent.children[i]); ... this doesn't seem right to me... */
-			parent.insertBefore(ns[i].domRef, parent.children[i+1]);
-		    }
-		}
+	    /* If new key was *not* found in the old map this means it must now be created, example below
+		   -> [ a e d c ] <- old children
+		   -> [ b e a j ] <- new children
+			^
+
+		   In the above case 'b' does not exist in the old map, so we create a new element and DOM reference.
+		   We then insertBefore in both vDOM and DOM.
+
+		   -> [ b a e d c ] <- old children
+		   -> [ b e a j   ] <- new children
+			  ^
+	       */
+	    else {
+		createElement(nFirst, doc);
+		parent.insertBefore(nFirst.domRef, oFirst.domRef);
+		os.splice(oldFirstIndex++, 0, nFirst);
+		newFirstIndex++;
+		oldLastIndex++;
 	    }
 	}
     }
 }
 
-
-/* Thanks Boris :) */
-function lis(a) {
-  var p = a.slice(0);
-  var result = [], u, v, il, c, j;
-  result.push(0);
-
-  for (var i = 0, il = a.length; i < il; i++) {
-    if (a[i] === -1) {
-      continue;
-    }
-
-    j = result[result.length - 1];
-    if (a[j] < a[i]) {
-      p[i] = j;
-      result.push(i);
-      continue;
-    }
-
-    u = 0;
-    v = result.length - 1;
-
-    while (u < v) {
-      c = ((u + v) / 2) | 0;
-      if (a[result[c]] < a[i]) {
-	u = c + 1;
-      } else {
-	v = c;
-      }
-    }
-
-    if (a[i] < a[result[u]]) {
-      if (u > 0) {
-	p[i] = result[u - 1];
-      }
-      result[u] = i;
-    }
-  }
-
-  u = result.length;
-  v = result[u - 1];
-
-  while (u-- > 0) {
-    result[u] = v;
-    v = p[v];
-  }
+function swapDomRefs(tmp,a,b,p) {
+  tmp = a.nextSibling;
+  p.insertBefore(a,b);
+  p.insertBefore(b,tmp);
+}
 
-  return result;
+function swap(os,l,r) {
+  var k = os[l];
+  os[l] = os[r];
+  os[r] = k;
 }
diff --git a/miso.cabal b/miso.cabal
--- a/miso.cabal
+++ b/miso.cabal
@@ -1,5 +1,5 @@
 name:                miso
-version:             0.15.0.0
+version:             0.16.0.0
 category:            Web, Miso, Data Structures
 license:             BSD3
 license-file:        LICENSE
@@ -250,11 +250,11 @@
     Haskell2010
   exposed-modules:
     Miso
+    Miso.Util
     Miso.Html
     Miso.Html.Element
     Miso.Html.Event
     Miso.Html.Property
-    Miso.Lens
     Miso.Event
     Miso.Event.Decoder
     Miso.Event.Types
@@ -311,7 +311,6 @@
       Miso.Subscription.Window
       Miso.Subscription.SSE
       Miso.Types
-      Miso.Util
     other-modules:
       Miso.Diff
       Miso.FFI
diff --git a/src/Miso/Html/Element.hs b/src/Miso/Html/Element.hs
--- a/src/Miso/Html/Element.hs
+++ b/src/Miso/Html/Element.hs
@@ -81,6 +81,7 @@
     , thead_
     , tfoot_
     , tr_
+    , trKeyed_
     , td_
     , th_
     -- * Less common elements
@@ -162,6 +163,13 @@
 -- | https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tr
 tr_ :: [Attribute action] -> [View action] -> View action
 tr_  = nodeHtml "tr"
+
+-- | Contains `Key`, inteded to be used for child replacement patch
+--
+-- <https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tr>
+
+trKeyed_ :: Key -> [Attribute action] -> [View action] -> View action
+trKeyed_ = node HTML "tr" . pure
 
 -- | https://developer.mozilla.org/en-US/docs/Web/HTML/Element/th
 th_ :: [Attribute action] -> [View action] -> View action
diff --git a/src/Miso/Lens.hs b/src/Miso/Lens.hs
deleted file mode 100644
--- a/src/Miso/Lens.hs
+++ /dev/null
@@ -1,34 +0,0 @@
-{-# LANGUAGE RankNTypes #-}
------------------------------------------------------------------------------
--- |
--- Module      :  Miso.Lens
--- Copyright   :  (C) 2016-2018 David M. Johnson
--- License     :  BSD3-style (see the file LICENSE)
--- Maintainer  :  David M. Johnson <djohnson.m@gmail.com>
--- Stability   :  experimental
--- Portability :  non-portable
-----------------------------------------------------------------------------
-module Miso.Lens
-  ( Lens
-  , Lens'
-  , Getting
-  , get
-  , set
-  , makeLens
-  ) where
-
-import Data.Functor.Identity
-import Control.Applicative
-
-type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t
-type Lens' s a = Lens s s a a
-type Getting r s a = (a -> Const r a) -> (s -> Const r s)
-
-get :: Getting a s a -> s -> a
-get l = \ s -> getConst (l Const s)
-
-set :: Lens s t a b -> b -> s -> t
-set l b = \s -> runIdentity (l (\ _ -> pure b) s)
-
-makeLens :: (s -> a) -> (s -> b -> t) -> Lens s t a b
-makeLens get' upd = \ f s -> upd s `fmap` f (get' s)
diff --git a/src/Miso/Router.hs b/src/Miso/Router.hs
--- a/src/Miso/Router.hs
+++ b/src/Miso/Router.hs
@@ -24,10 +24,6 @@
 module Miso.Router
   ( runRoute
   , RoutingError (..)
-  , HasURI (..)
-  , getURI
-  , setURI
-  , makeLens
   ) where
 
 import qualified Data.ByteString.Char8 as BS
@@ -42,7 +38,6 @@
 import           Web.HttpApiData
 
 import           Miso.Html             hiding (text)
-import           Miso.Lens
 
 -- | Router terminator.
 -- The 'HasRouter' instance for 'View' finalizes the router.
@@ -145,12 +140,16 @@
 -- All handlers must, in the end, return @m a@.
 -- 'routeLoc' will choose a route and return its result.
 runRoute
-  :: (HasURI m, HasRouter m layout)
+  :: HasRouter m layout
   => Proxy layout
   -> RouteT m layout a
+  -> (m -> URI)
   -> m
   -> Either RoutingError a
-runRoute layout page m = runRouteLoc (uriToLocation (getURI m)) layout page m
+runRoute layout page getURI m =
+  runRouteLoc (uriToLocation uri) layout page m
+    where
+      uri = getURI m
 
 -- | Use a computed 'Router' to route a 'Location'.
 routeLoc :: Location -> Router a -> m -> Either RoutingError a
@@ -195,16 +194,3 @@
   { locPath = decodePathSegments $ BS.pack (uriPath uri)
   , locQuery = parseQuery $ BS.pack (uriQuery uri)
   }
-
-class HasURI m where lensURI :: Lens' m URI
-
-getURI :: HasURI m => m -> URI
-getURI = get lensURI
-
-setURI :: HasURI m => URI -> m -> m
-setURI m u = set lensURI m u
-
-
-
-
-
diff --git a/src/Miso/Util.hs b/src/Miso/Util.hs
new file mode 100644
--- /dev/null
+++ b/src/Miso/Util.hs
@@ -0,0 +1,37 @@
+-----------------------------------------------------------------------------
+-- |
+-- Module      :  Miso.Util
+-- Copyright   :  (C) 2016-2018 David M. Johnson
+-- License     :  BSD3-style (see the file LICENSE)
+-- Maintainer  :  David M. Johnson <djohnson.m@gmail.com>
+-- Stability   :  experimental
+-- Portability :  non-portable
+----------------------------------------------------------------------------
+module Miso.Util
+  ( withFoldable
+  , conditionalViews
+  ) where
+
+import Data.Foldable
+import Miso.Html (View)
+
+-- | Generic @map@ function, useful for creating @View@s from the elements of
+-- some @Foldable@. Particularly handy for @Maybe@, as shown in the example
+-- below.
+--
+-- @
+-- view model =
+--     div_ [] $
+--      withFoldable (model ^. mSomeMaybeVal) $ \someVal ->
+--         p_ [] [ text $ "Hey, look at this value: " <> ms (show someVal) ]
+-- @
+withFoldable :: Foldable t => t a -> (a -> b) -> [b]
+withFoldable ta f = map f (toList ta)
+
+-- | Hides the @View@s the condition is False. Shows them when the condition
+-- is True.
+conditionalViews :: Bool -> [View action] -> [View action]
+conditionalViews condition views =
+    if condition
+    then views
+    else []
