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 +48/−32
- examples/compose-update/Main.hs +1/−1
- examples/router/Main.hs +4/−10
- ghc-src/Miso.hs +2/−0
- ghcjs-src/Miso.hs +11/−5
- ghcjs-src/Miso/Delegate.hs +8/−1
- ghcjs-src/Miso/Diff.hs +9/−4
- ghcjs-src/Miso/FFI.hs +0/−16
- ghcjs-src/Miso/Subscription/History.hs +17/−27
- ghcjs-src/Miso/Util.hs +0/−45
- jsbits/diff.js +245/−288
- miso.cabal +2/−3
- src/Miso/Html/Element.hs +8/−0
- src/Miso/Lens.hs +0/−34
- src/Miso/Router.hs +6/−20
- src/Miso/Util.hs +37/−0
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&hostname=irc.freenode.net&port=6697&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 []