packages feed

miso 0.15.0.0 → 0.16.0.0

raw patch · 16 files changed

+398/−486 lines, 16 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- Miso.Lens: get :: Getting a s a -> s -> a
- Miso.Lens: makeLens :: (s -> a) -> (s -> b -> t) -> Lens s t a b
- Miso.Lens: set :: Lens s t a b -> b -> s -> t
- Miso.Lens: type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t
- Miso.Lens: type Getting r s a = (a -> Const r a) -> (s -> Const r s)
- Miso.Lens: type Lens' s a = Lens s s a a
- Miso.Router: class HasURI m
- Miso.Router: getURI :: HasURI m => m -> URI
- Miso.Router: lensURI :: HasURI m => Lens' m URI
- Miso.Router: makeLens :: (s -> a) -> (s -> b -> t) -> Lens s t a b
- Miso.Router: setURI :: HasURI m => URI -> m -> m
+ Miso.Html.Element: trKeyed_ :: Key -> [Attribute action] -> [View action] -> View action
+ Miso.Util: conditionalViews :: Bool -> [View action] -> [View action]
+ Miso.Util: withFoldable :: Foldable t => t a -> (a -> b) -> [b]
- Miso.Router: runRoute :: (HasURI m, HasRouter m layout) => Proxy layout -> RouteT m layout a -> m -> Either RoutingError a
+ Miso.Router: runRoute :: HasRouter m layout => Proxy layout -> RouteT m layout a -> (m -> URI) -> m -> Either RoutingError a

Files

README.md view
@@ -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
examples/compose-update/Main.hs view
@@ -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
examples/router/Main.hs view
@@ -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" ]
ghc-src/Miso.hs view
@@ -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
ghcjs-src/Miso.hs view
@@ -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 ()
ghcjs-src/Miso/Delegate.hs view
@@ -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 ()
ghcjs-src/Miso/Diff.hs view
@@ -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 ()
ghcjs-src/Miso/FFI.hs view
@@ -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.
ghcjs-src/Miso/Subscription/History.hs view
@@ -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 ()
− ghcjs-src/Miso/Util.hs
@@ -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 []
jsbits/diff.js view
@@ -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; }
miso.cabal view
@@ -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
src/Miso/Html/Element.hs view
@@ -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
− src/Miso/Lens.hs
@@ -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)
src/Miso/Router.hs view
@@ -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-----
+ src/Miso/Util.hs view
@@ -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 []