diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -45,13 +45,17 @@
 - [Examples](#examples)
   - [TodoMVC](#todomvc)
   - [Flatris](#flatris)
+  - [2048](#2048)
   - [Mario](#mario)
   - [Websocket](#websocket)
   - [SSE](#sse)
   - [XHR](#xhr)
   - [Router](#router)
   - [SVG](#svg)
+  - [Canvas 2D](#canvas-2d)
+  - [ThreeJS](#threejs)
   - [Simple](#simple)
+  - [File Reader](#file-reader)
 - [Haddocks](#haddocks)
   - [GHC](#ghc)
   - [GHCJS](#ghcjs)
@@ -88,14 +92,14 @@
 packages:
  - '.'
 extra-deps:
- - miso-0.4.0.0
+ - miso-0.7.2.0
 
 setup-info:
   ghcjs:
     source:
       ghcjs-0.2.0.9006020_ghc-7.10.3:
-	 url: http://ghcjs.tolysz.org/lts-6.20-9006020.tar.gz
-	 sha1: a6cea90cd8121eee3afb201183c6e9bd6bacd94a
+         url: http://ghcjs.tolysz.org/lts-6.20-9006020.tar.gz
+         sha1: a6cea90cd8121eee3afb201183c6e9bd6bacd94a
 ```
 
 Add a `cabal` file
@@ -291,6 +295,9 @@
 ### Flatris
   - [Link](https://flatris.haskell-miso.org/) / [Source](https://github.com/ptigwe/hs-flatris/)
 
+### 2048
+  - [Link](http://2048.haskell-miso.org/) / [Source](https://github.com/ptigwe/hs2048/)
+
 ### Mario
   - [Link](https://mario.haskell-miso.org/) / [Source](https://github.com/dmjio/miso/blob/master/examples/mario/Main.hs)
 
@@ -309,8 +316,17 @@
 ### SVG
   - [Link](https://svg.haskell-miso.org/) / [Source](https://github.com/dmjio/miso/blob/master/examples/svg/Main.hs)
 
+### Canvas 2D
+  - [Link](http://canvas.haskell-miso.org/) / [Source](https://github.com/dmjio/miso/blob/master/examples/canvas2d/Main.hs)
+
+### ThreeJS
+  - [Link](http://threejs.haskell-miso.org/) / [Source](https://github.com/dmjio/miso/blob/master/examples/three/Main.hs)
+
 ### Simple
   - [Link](https://simple.haskell-miso.org/) / [Source](https://github.com/dmjio/miso/blob/master/exe/Main.hs)
+
+### File Reader
+  - [Link](https://file-reader.haskell-miso.org/) / [Source](https://github.com/dmjio/miso/blob/master/examples/file-reader/Main.hs)
 
 ## Haddocks
 
diff --git a/examples/canvas2d/Main.hs b/examples/canvas2d/Main.hs
new file mode 100644
--- /dev/null
+++ b/examples/canvas2d/Main.hs
@@ -0,0 +1,94 @@
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE OverloadedStrings #-}
+module Main where
+
+import Control.Monad
+import GHCJS.Types
+import JavaScript.Web.Canvas
+
+import Miso
+import Miso.String
+
+type Model = (Double, Double)
+
+data Action
+  = NoOp
+  | GetTime
+  | SetTime Model
+
+main :: IO ()
+main = do
+  [sun, moon, earth] <- replicateM 3 newImage
+  setSrc sun "https://mdn.mozillademos.org/files/1456/Canvas_sun.png"
+  setSrc moon "https://mdn.mozillademos.org/files/1443/Canvas_moon.png"
+  setSrc earth "https://mdn.mozillademos.org/files/1429/Canvas_earth.png"
+  startApp App { initialAction = GetTime
+               , update = updateModel (sun,moon,earth)
+               , ..
+               }
+  where
+    view _ = canvas_ [ id_ "canvas"
+                     , width_ "300"
+                     , height_ "300"
+                     ] []
+    model  = (0.0, 0.0)
+    subs   = []
+    events = defaultEvents
+
+updateModel
+  :: (Image,Image,Image)
+  -> Action
+  -> Model
+  -> Effect Action Model
+updateModel _ NoOp m = noEff m
+updateModel _ GetTime m = m <# do
+  date <- newDate
+  (s,m') <- (,) <$> getSecs date <*> getMillis date
+  pure $ SetTime (s,m')
+updateModel (sun,moon,earth) (SetTime m@(secs,millis)) _ = m <# do
+  ctx <- getCtx
+  setGlobalCompositeOperation ctx
+  clearRect 0 0 300 300 ctx
+  fillStyle 0 0 0 0.6 ctx
+  strokeStyle 0 153 255 0.4 ctx
+  save ctx
+  translate 150 150 ctx
+  flip rotate ctx $ (((2 * pi) / 60) * secs) + (((2 * pi) / 60000) * millis)
+  translate 105 0 ctx
+  fillRect 0 (-12) 50 24 ctx
+  drawImage' earth (-12) (-12) ctx
+  save ctx
+  flip rotate ctx $ (((2 * pi) / 6) * secs) + (((2 * pi) / 6000) * millis)
+  translate 0 28.5 ctx
+  drawImage' moon (-3.5) (-3.5) ctx
+  replicateM_ 2 (restore ctx)
+  beginPath ctx
+  arc 150 150 105 0 (pi * 2) False ctx
+  stroke ctx
+  drawImage sun 0 0 300 300 ctx
+  pure GetTime
+
+foreign import javascript unsafe "$1.globalCompositeOperation = 'destination-over';"
+  setGlobalCompositeOperation :: Context -> IO ()
+
+foreign import javascript unsafe "$4.drawImage($1,$2,$3);"
+  drawImage' :: Image -> Double -> Double -> Context -> IO ()
+
+foreign import javascript unsafe "$r = document.getElementById('canvas').getContext('2d');"
+  getCtx :: IO Context
+
+foreign import javascript unsafe "$r = new Image();"
+  newImage :: IO Image
+
+foreign import javascript unsafe "$1.src = $2;"
+  setSrc :: Image -> MisoString -> IO ()
+
+foreign import javascript unsafe "$r = new Date();"
+  newDate :: IO JSVal
+
+foreign import javascript unsafe "$r = $1.getSeconds();"
+  getSecs :: JSVal -> IO Double
+
+foreign import javascript unsafe "$r = $1.getMilliseconds();"
+  getMillis :: JSVal -> IO Double
+
diff --git a/examples/file-reader/Main.hs b/examples/file-reader/Main.hs
new file mode 100644
--- /dev/null
+++ b/examples/file-reader/Main.hs
@@ -0,0 +1,94 @@
+{-# LANGUAGE DeriveGeneric       #-}
+{-# LANGUAGE OverloadedStrings   #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeOperators       #-}
+{-# LANGUAGE DataKinds           #-}
+{-# LANGUAGE RecordWildCards     #-}
+{-# LANGUAGE TypeFamilies        #-}
+module Main where
+
+import           Miso
+import           Miso.String
+import           Control.Concurrent.MVar
+
+import GHCJS.Types
+import GHCJS.Foreign.Callback
+
+-- | Model
+data Model
+  = Model
+  { info :: MisoString
+  } deriving (Eq, Show)
+
+-- | Action
+data Action
+  = ReadFile
+  | NoOp
+  | SetContent MisoString
+  deriving (Show, Eq)
+
+-- | Main entry point
+main :: IO ()
+main = do
+  startApp App { model = Model ""
+               , initialAction = NoOp
+               , ..
+               }
+    where
+      update = updateModel
+      events = defaultEvents
+      subs   = []
+      view   = viewModel
+
+-- | Update your model
+updateModel :: Action -> Model -> Effect Action Model
+updateModel ReadFile m = m <# do
+  fileReaderInput <- getElementById "fileReader"
+  file <- getFile fileReaderInput
+  reader <- newReader
+  mvar <- newEmptyMVar
+  setOnLoad reader =<< do
+    asyncCallback $ do
+      r <- getResult reader
+      putMVar mvar r
+  readText reader file
+  SetContent <$> readMVar mvar
+updateModel (SetContent c) m = noEff m { info = c }
+updateModel NoOp m = noEff m
+
+-- | View function, with routing
+viewModel :: Model -> View Action
+viewModel Model {..} = view
+  where
+    view = div_ [] [
+        "FileReader API example"
+      , input_ [ id_ "fileReader"
+             , type_ "file"
+             , onChange ReadFile
+             ] []
+      , div_ [] [ text info ]
+      ]
+
+foreign import javascript unsafe "console.log($1);"
+  consoleLog :: JSVal -> IO ()
+
+foreign import javascript unsafe "$r = new FileReader();"
+  newReader :: IO JSVal
+
+foreign import javascript unsafe "$r = document.getElementById($1);"
+  getElementById :: MisoString -> IO JSVal
+
+foreign import javascript unsafe "$r = $1.files[0];"
+  getFile :: JSVal -> IO JSVal
+
+foreign import javascript unsafe "$1.onload = $2;"
+  setOnLoad :: JSVal -> Callback (IO ()) -> IO ()
+
+foreign import javascript unsafe "$r = $1.result;"
+  getResult :: JSVal -> IO MisoString
+
+foreign import javascript unsafe "$1.readAsText($2);"
+  readText :: JSVal -> JSVal -> IO ()
+
+onChange :: action -> Attribute action
+onChange r = on "change" emptyDecoder (const r)
diff --git a/examples/three/Main.hs b/examples/three/Main.hs
new file mode 100644
--- /dev/null
+++ b/examples/three/Main.hs
@@ -0,0 +1,156 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE OverloadedLists   #-}
+{-# LANGUAGE RecordWildCards   #-}
+module Main where
+
+import Control.Monad
+import Data.IORef
+import GHCJS.Types
+
+import Miso
+import Miso.String
+
+data Action
+  = GetTime
+  | Init
+  | SetTime !Double
+
+withStats :: JSVal -> IO () -> IO ()
+withStats stats m = do
+  statsBegin stats >> m
+  statsEnd stats
+
+data Context = Context
+  { rotateCube :: IO ()
+  , renderScene :: IO ()
+  , stats :: JSVal
+  }
+
+initContext :: IORef Context -> IO ()
+initContext ref = do
+  canvas <- getElementById "canvas"
+  scene <- newScene
+  camera <- newCamera
+  renderer <- newRenderer canvas
+  setSize renderer
+  cube <- join $ newMesh
+    <$> newBoxGeometry 1 1 1
+    <*> newMeshBasicMaterial
+  addToScene scene cube
+  positionCamera camera 5
+  stats <- newStats
+  statsContainer <- getElementById "stats"
+  addStatsToDOM statsContainer stats
+  writeIORef ref Context {
+    stats = stats
+  , rotateCube = do
+      rotateX cube 0.1
+      rotateY cube 0.1
+  , renderScene =
+      render renderer scene camera
+  }
+
+main :: IO ()
+main = do
+  stats <- newStats
+  ref <- newIORef $ Context (pure ()) (pure ()) stats
+  m <- now
+  startApp App { model = m
+               , initialAction = Init
+               , update = updateModel ref
+               , ..
+               }
+    where
+      events = defaultEvents
+      view   = viewModel
+      subs   = []
+
+viewModel :: Double -> View action
+viewModel _ = div_ [] [
+    div_ [ id_ "stats"
+         , style_ [("position", "absolute")]
+         ] []
+  , canvas_ [ id_ "canvas"
+            , width_ "400"
+            , height_ "300"
+            ] []
+  ]
+
+updateModel
+  :: IORef Context
+  -> Action
+  -> Double
+  -> Effect Action Double
+updateModel ref Init m = m <# do
+  initContext ref
+  pure GetTime
+
+updateModel ref GetTime m = m <# do
+  Context {..} <- readIORef ref
+  withStats stats $ do
+    rotateCube
+    renderScene
+  SetTime <$> now
+
+updateModel _ (SetTime m) _ =
+  m <# pure GetTime
+
+foreign import javascript unsafe "$r = new Stats();"
+  newStats :: IO JSVal
+
+foreign import javascript unsafe "$1.begin();"
+  statsBegin :: JSVal -> IO ()
+
+foreign import javascript unsafe "$1.end();"
+  statsEnd :: JSVal -> IO ()
+
+foreign import javascript unsafe "$1.showPanel(0);"
+  showPanel :: JSVal -> IO ()
+
+foreign import javascript unsafe "$r = performance.now();"
+  now :: IO Double
+
+foreign import javascript unsafe "$r = new THREE.Scene();"
+  newScene :: IO JSVal
+
+foreign import javascript unsafe "$r = new THREE.BoxGeometry( $1, $2, $3 );"
+  newBoxGeometry :: Int -> Int -> Int -> IO JSVal
+
+foreign import javascript unsafe "$r = new THREE.PerspectiveCamera( 75, window.innerWidth/window.innerHeight, 0.1, 1000 );"
+  newCamera :: IO JSVal
+
+foreign import javascript unsafe "$r = new THREE.Mesh( $1, $2 );"
+  newMesh :: JSVal -> JSVal -> IO JSVal
+
+foreign import javascript unsafe "$r = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );"
+  newMeshBasicMaterial :: IO JSVal
+
+foreign import javascript unsafe "$r = new THREE.WebGLRenderer({canvas:$1, antialias : true});"
+  newRenderer :: JSVal -> IO JSVal
+
+foreign import javascript unsafe "$1.setSize( window.innerWidth, window.innerHeight );"
+  setSize :: JSVal -> IO ()
+
+foreign import javascript unsafe "$r = document.getElementById($1);"
+  getElementById :: MisoString -> IO JSVal
+
+foreign import javascript unsafe "$1.add($2);"
+  addToScene :: JSVal -> JSVal -> IO ()
+
+foreign import javascript unsafe "$1.position.z = $2;"
+  cameraZ :: JSVal -> Int -> IO ()
+
+foreign import javascript unsafe "$1.rotation.x += $2;"
+  rotateX :: JSVal -> Double -> IO ()
+
+foreign import javascript unsafe "$1.rotation.y += $2;"
+  rotateY :: JSVal -> Double -> IO ()
+
+foreign import javascript unsafe "$1.render($2, $3);"
+  render :: JSVal -> JSVal -> JSVal -> IO ()
+
+foreign import javascript unsafe "$1.position.z = $2;"
+  positionCamera :: JSVal -> Double -> IO ()
+
+foreign import javascript unsafe "$1.appendChild( $2.domElement );"
+  addStatsToDOM :: JSVal -> JSVal -> IO ()
diff --git a/ghcjs-src/Miso/Subscription/Keyboard.hs b/ghcjs-src/Miso/Subscription/Keyboard.hs
--- a/ghcjs-src/Miso/Subscription/Keyboard.hs
+++ b/ghcjs-src/Miso/Subscription/Keyboard.hs
@@ -16,7 +16,9 @@
     Arrows (..)
     -- * Subscriptions
   , arrowsSub
+  , directionSub
   , keyboardSub
+  , wasdSub
   ) where
 
 import           Data.IORef
@@ -40,25 +42,38 @@
  , arrowY :: !Int
  } deriving (Show, Eq)
 
--- | Helper function to convert keys currently pressed to `Arrow`
-toArrows :: Set Int -> Arrows
-toArrows set =
+-- | Helper function to convert keys currently pressed to `Arrow`, given a
+-- mapping for keys representing up, down, left and right respectively.
+toArrows :: ([Int], [Int], [Int], [Int]) -> Set Int -> Arrows
+toArrows (up, down, left, right) set =
   Arrows {
     arrowX =
-      case (S.member 37 set, S.member 39 set) of
+      case (check left, check right) of
         (True, False) -> -1
         (False, True) -> 1
         (_,_) -> 0
- ,  arrowY =
-      case (S.member 40 set, S.member 38 set) of
+  , arrowY =
+      case (check down, check up) of
         (True, False) -> -1
         (False, True) -> 1
         (_,_) -> 0
- }
+  }
+  where
+    check = any (`S.member` set)
 
 -- | Maps `Arrows` onto a Keyboard subscription
 arrowsSub :: (Arrows -> action) -> Sub action model
-arrowsSub = keyboardSub . (. toArrows)
+arrowsSub = directionSub ([38], [40], [37], [39])
+
+-- | Maps `WASD` onto a Keyboard subscription for directions
+wasdSub :: (Arrows -> action) -> Sub action model
+wasdSub = directionSub ([87], [83], [65], [68])
+
+-- | Maps a specified list of keys to directions (up, down, left, right)
+directionSub :: ([Int], [Int], [Int], [Int])
+             -> (Arrows -> action)
+             -> Sub action model
+directionSub dirs = keyboardSub . (. toArrows dirs)
 
 -- | Returns subscription for Keyboard
 keyboardSub :: (Set Int -> action) -> Sub action model
diff --git a/jsbits/delegate.js b/jsbits/delegate.js
--- a/jsbits/delegate.js
+++ b/jsbits/delegate.js
@@ -33,14 +33,18 @@
     /* stack.length == 1 */
     else {
 	if (obj.domRef === stack[0]) {
-          if (obj.events[event.type]) {
-	      var eventObj = obj.events[event.type],
-		  options = eventObj.options;
-              if (options.preventDefault) event.preventDefault();
-          eventObj.runEvent(event);
-	    if (!options.stopPropagation)
-	     propogateWhileAble (parentStack, event);
-          }
+	    var eventObj = obj.events[event.type];
+	    if (eventObj) {
+		var options = eventObj.options;
+		if (options.preventDefault)
+		    event.preventDefault();
+		eventObj.runEvent(event);
+		if (!options.stopPropagation)
+		    propogateWhileAble (parentStack, event);
+	    } else {
+		 /* still propagate to parent handlers even if event not defined */
+ 		 propogateWhileAble (parentStack, event);
+	      }
 	}
     }
 }
diff --git a/miso.cabal b/miso.cabal
--- a/miso.cabal
+++ b/miso.cabal
@@ -1,5 +1,5 @@
 name:                miso
-version:             0.7.2.0
+version:             0.7.3.0
 category:            Web, Miso, Data Structures
 license:             BSD3
 license-file:        LICENSE
@@ -53,6 +53,40 @@
     default-language:
       Haskell2010
 
+executable threejs
+  main-is:
+    Main.hs
+  if !impl(ghcjs) || !flag(examples)
+    buildable: False
+  else
+    hs-source-dirs:
+      examples/three
+    build-depends:
+      aeson,
+      base < 5,
+      ghcjs-base,
+      containers,
+      miso
+    default-language:
+      Haskell2010
+
+executable file-reader
+  main-is:
+    Main.hs
+  if !impl(ghcjs) || !flag(examples)
+    buildable: False
+  else
+    hs-source-dirs:
+      examples/file-reader
+    build-depends:
+      aeson,
+      base < 5,
+      containers,
+      ghcjs-base,
+      miso
+    default-language:
+      Haskell2010
+
 executable xhr
   main-is:
     Main.hs
@@ -65,6 +99,22 @@
       aeson,
       base < 5,
       containers,
+      ghcjs-base,
+      miso
+    default-language:
+      Haskell2010
+
+executable canvas2d
+  main-is:
+    Main.hs
+  if !impl(ghcjs) || !flag(examples)
+    buildable: False
+  else
+    hs-source-dirs:
+      examples/canvas2d
+    build-depends:
+      aeson,
+      base < 5,
       ghcjs-base,
       miso
     default-language:
