on develop help sign in
Merge pull request #300 from reflex-frp/release/0.5

Release/0.5

commitf8b870722a2497c1f8ca8bd9ffaceef4b78ccacc authorAli Abrar <aliabrar@gmail.com> dateFri 22 Mar 2019, 19:30 (8 years ago) parenta851884646a98a
25 files changed+3830−750
chrome-test-utils/LICENSE+12 chrome-test-utils/chrome-test-utils.cabal+30 chrome-test-utils/src/Test/Util/ChromeFlags.hs+17 chrome-test-utils/src/Test/Util/UnshareNetwork.hs+12 default.nix+1 reflex-dom-core/ChangeLog.md+7 reflex-dom-core/default.nix+29−13 reflex-dom-core/reflex-dom-core.cabal+55−7 reflex-dom-core/src/Reflex/Dom/Builder/Class.hs+53−3 reflex-dom-core/src/Reflex/Dom/Builder/Immediate.hs+1643−562 reflex-dom-core/src/Reflex/Dom/Builder/Static.hs+72−4 reflex-dom-core/src/Reflex/Dom/Core.hs+1 reflex-dom-core/src/Reflex/Dom/Location.hs+39 reflex-dom-core/src/Reflex/Dom/Main.hs+129−17 reflex-dom-core/src/Reflex/Dom/Prerender.hs+184−50 reflex-dom-core/src/Reflex/Dom/Specializations.hs−62 reflex-dom-core/src/Reflex/Dom/Widget/Basic.hs+15 reflex-dom-core/src/Reflex/Dom/Xhr/FormData.hs+47 reflex-dom-core/test/gc.hs+12−11 reflex-dom-core/test/hydration.hs+1428 reflex-dom/ChangeLog.md+6 reflex-dom/benchmarks/krausest.hs+7−7 reflex-dom/reflex-dom.cabal+11−9 reflex-dom/src/Reflex/Dom.hs+2−1 reflex-dom/src/Reflex/Dom/Internal.hs+18−4
chrome-test-utils/LICENSEnew file+12
@@ -0,0 +1,12 @@
1+Copyright (c) 2019, Obsidian Systems LLC
2+All rights reserved.
3+
4+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5+
6+1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7+
8+2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9+
10+3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
11+
12+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
chrome-test-utils/chrome-test-utils.cabalnew file+30
@@ -0,0 +1,30 @@
1+Name: chrome-test-utils
2+Version: 0.4
3+Synopsis: Utilities to write headless Chromium tests
4+License: BSD3
5+License-file: LICENSE
6+Author: Obsidian Systems LLC
7+Maintainer: maintainer@obsidian.systems
8+Stability: Experimental
9+Build-type: Simple
10+Cabal-version: >=1.9.2
11+
12+library
13+ hs-source-dirs: src
14+ ghc-options: -Wall -fwarn-tabs -funbox-strict-fields -O2 -ferror-spans
15+ build-depends: base
16+ , linux-namespaces
17+ , process
18+ , temporary
19+ , text
20+ , unix
21+ exposed-modules:
22+ Test.Util.UnshareNetwork
23+ Test.Util.ChromeFlags
24+ if !os(linux) || !arch(x86_64)
25+ buildable: False
26+
27+source-repository head
28+ type: git
29+ location: https://github.com/reflex-frp/reflex-dom
30+ subdir: chrome-test-utils
chrome-test-utils/src/Test/Util/ChromeFlags.hsnew file+17
@@ -0,0 +1,17 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+module Test.Util.ChromeFlags where
3+
4+import Data.Semigroup
5+import Data.Text
6+import System.IO.Temp
7+
8+withSandboxedChromeFlags :: Bool -> ([Text] -> IO a) -> IO a
9+withSandboxedChromeFlags headless action =
10+ withSystemTempDirectory "reflex-dom-core_test" $ \tmp ->
11+ action
12+ [ if headless then "--headless" else "--auto-open-devtools-for-tabs"
13+ , "--disable-gpu"
14+ , "--no-sandbox"
15+ , "--remote-debugging-port=9222"
16+ , "--user-data-dir=" <> pack tmp
17+ ]
chrome-test-utils/src/Test/Util/UnshareNetwork.hsnew file+12
@@ -0,0 +1,12 @@
1+module Test.Util.UnshareNetwork where
2+
3+import System.Linux.Namespaces
4+import System.Posix
5+import System.Process
6+
7+unshareNetork :: IO ()
8+unshareNetork = do
9+ uid <- getEffectiveUserID
10+ unshare [User, Network]
11+ writeUserMappings Nothing [UserMapping 0 uid 1]
12+ callCommand "ip link set lo up ; ip addr"
default.nix+1
@@ -3,4 +3,5 @@ haskellPackages: nixpkgs: {
33 reflex-dom-core = haskellPackages.callPackage ./reflex-dom-core {
44 inherit (nixpkgs) iproute chromium;
55 };
6+ chrome-test-utils = haskellPackages.callCabal2nix "chrome-test-utils" ./chrome-test-utils {};
67}
reflex-dom-core/ChangeLog.mdnew file+7
@@ -0,0 +1,7 @@
1+# Revision history for reflex-dom-core
2+
3+## 0.5
4+
5+* Add HydrationDomBuilderT to support hydration of statically rendered DOM nodes. See the note at the top of Reflex.Dom.Builder.Immediate.
6+* As a result of the hydration changes, the Prerender class has changed, the type of `prerender` has changed and it is now a class method.
7+* Add the Reflex.Dom.Xhr.FormData module to make posting formdata over xhr more convenient.
reflex-dom-core/default.nix+29−13
@@ -1,18 +1,26 @@
1−{ mkDerivation, ghc, aeson, base, bifunctors, bimap, blaze-builder
2−, bytestring, constraints, containers, contravariant, data-default
3−, dependent-map, dependent-sum, dependent-sum-template, directory
4−, exception-transformers, ghcjs-dom, hlint, jsaddle, keycode, lens
5−, monad-control, mtl, primitive, ref-tf, reflex, semigroups, stdenv
6−, stm, template-haskell, temporary, text, these, transformers
7−, unix, zenc, hashable, chromium, process, jsaddle-warp
8−, linux-namespaces, iproute, network-uri, fontconfig
1+{ mkDerivation, aeson, base, bifunctors, bimap, blaze-builder
2+, bytestring, chrome-test-utils, constraints, constraints-extras, containers
3+, contravariant, data-default, dependent-map, dependent-sum
4+, dependent-sum-template, directory, exception-transformers
5+, exceptions, filepath, ghcjs-dom, hlint, hspec, hspec-webdriver, http-types, HUnit
6+, jsaddle, jsaddle-warp, keycode, lens, lifted-base
7+, monad-control, mtl, network, network-uri, primitive, process
8+, random, ref-tf, reflex, semigroups, silently, stdenv, stm
9+, template-haskell, temporary, text, these, transformers, unix, wai
10+, wai-websockets, warp, webdriver, websockets, zenc
11+, ghc, iproute, chromium, hashable, selenium-server-standalone, which, fontconfig
912}:
1013let addGcTestDepends = drv: if (stdenv.system != "x86_64-linux" || stdenv.hostPlatform != stdenv.buildPlatform || (ghc.isGhcjs or false)) then drv else drv // {
1114 # The headless browser run as part of gc tests would hang/crash without this
1215 preCheck = ''
1316 export FONTCONFIG_PATH=${fontconfig.out}/etc/fonts
1417 '';
15− testHaskellDepends = (drv.testHaskellDepends or []) ++ [ temporary jsaddle-warp process linux-namespaces ];
18+ testHaskellDepends = (drv.testHaskellDepends or []) ++ [
19+ temporary
20+ jsaddle-warp
21+ process
22+ chrome-test-utils
23+ ];
1624 testSystemDepends = (drv.testSystemDepends or []) ++ [ chromium iproute ];
1725 };
1826in mkDerivation (addGcTestDepends {
@@ -23,9 +31,9 @@ in mkDerivation (addGcTestDepends {
2331 aeson base bifunctors bimap blaze-builder bytestring constraints
2432 containers contravariant data-default dependent-map dependent-sum
2533 dependent-sum-template directory exception-transformers ghcjs-dom
26− jsaddle keycode lens monad-control mtl primitive ref-tf
27− reflex semigroups stm template-haskell text these transformers
28− unix zenc network-uri
34+ jsaddle keycode lens monad-control mtl network-uri primitive random
35+ ref-tf reflex semigroups stm template-haskell text these
36+ transformers unix zenc
2937 ] ++ (if ghc.isGhcjs or false then [
3038 hashable
3139 ] else []);
@@ -41,7 +49,15 @@ in mkDerivation (addGcTestDepends {
4149 # Show some output while running tests, so we might notice what's wrong
4250 testTarget = "--show-details=streaming";
4351
44− testHaskellDepends = [ base hlint ];
52+ testSystemDepends = [ selenium-server-standalone which ];
53+ testHaskellDepends = [
54+ aeson base bytestring constraints constraints-extras containers
55+ dependent-map dependent-sum dependent-sum-template directory
56+ exceptions filepath ghcjs-dom hlint hspec hspec-webdriver http-types HUnit jsaddle
57+ jsaddle-warp lens lifted-base network process random reflex
58+ silently temporary text unix wai wai-websockets warp webdriver
59+ websockets
60+ ];
4561 description = "Functional Reactive Web Apps with Reflex";
4662 license = stdenv.lib.licenses.bsd3;
4763})
reflex-dom-core/reflex-dom-core.cabal+55−7
@@ -1,5 +1,5 @@
11Name: reflex-dom-core
2−Version: 0.4
2+Version: 0.5
33Synopsis: Functional Reactive Web Apps with Reflex
44Description: Reflex-DOM is a Functional Reactive web framework based on the Reflex FRP engine
55License: BSD3
@@ -17,6 +17,7 @@ extra-source-files: src-ghc/Foreign/JavaScript/Internal/Utils.hs
1717 src/Reflex/Dom/WebSocket/Foreign.hs
1818 src/Reflex/Dom/Xhr/ResponseType.hs
1919 src/Reflex/Dom/Xhr/Exception.hs
20+ ChangeLog.md
2021
2122flag use-template-haskell
2223 description: Use template haskell to generate lenses
@@ -64,8 +65,9 @@ library
6465 monad-control >= 1.0.1 && < 1.1,
6566 mtl >= 2.1 && < 2.3,
6667 primitive >= 0.5 && < 0.7,
68+ random,
6769 ref-tf == 0.4.*,
68− reflex == 0.5.*,
70+ reflex == 0.6.*,
6971 semigroups >= 0.16 && < 0.19,
7072 stm == 2.4.*,
7173 text == 1.2.*,
@@ -100,7 +102,6 @@ library
100102 Reflex.Dom.Modals.Class
101103 Reflex.Dom.Old
102104 Reflex.Dom.Prerender
103− Reflex.Dom.Specializations
104105 Reflex.Dom.Time
105106 Reflex.Dom.WebSocket
106107 Reflex.Dom.WebSocket.Query
@@ -110,6 +111,7 @@ library
110111 Reflex.Dom.Widget.Lazy
111112 Reflex.Dom.Widget.Resize
112113 Reflex.Dom.Xhr
114+ Reflex.Dom.Xhr.FormData
113115 other-modules:
114116 Foreign.JavaScript.Internal.Utils
115117 Reflex.Dom.WebSocket.Foreign
@@ -148,16 +150,62 @@ test-suite hlint
148150 main-is: hlint.hs
149151 type: exitcode-stdio-1.0
150152
151−test-suite gc
153+test-suite hydration
152154 build-depends: base
155+ , aeson
156+ , bytestring
157+ , chrome-test-utils
158+ , constraints
159+ , constraints-extras
160+ , containers
161+ , dependent-map
162+ , dependent-sum
163+ , dependent-sum-template
164+ , directory
165+ , exceptions
166+ , filepath
167+ , ghcjs-dom
168+ , hspec
169+ , hspec-webdriver
170+ , http-types
171+ , HUnit
172+ , jsaddle
173+ , jsaddle-warp
174+ , lens
175+ , lifted-base
176+ , network
177+ , random
178+ , ref-tf
153179 , reflex
154180 , reflex-dom-core
181+ , process
182+ , silently
183+ , temporary
184+ , text
185+ , wai
186+ , wai-websockets
187+ , warp
188+ , webdriver
189+ , websockets
190+ hs-source-dirs: test
191+ ghc-options: -rtsopts -with-rtsopts=-T -Wall -fwarn-tabs -funbox-strict-fields -O2 -ferror-spans
192+ ghc-prof-options: -fprof-auto -optP-DPROFILING
193+ main-is: hydration.hs
194+ type: exitcode-stdio-1.0
195+ if !os(linux) || !arch(x86_64) || flag(profile-reflex)
196+ buildable: False
197+
198+-- broken test on base 4.11 & ghc 8.4.3
199+-- needs to be updated for changes in GHC.Stats
200+test-suite gc
201+ build-depends: base
202+ , chrome-test-utils
155203 , jsaddle
156204 , jsaddle-warp
157− , linux-namespaces
158205 , process
159− , temporary
160− , unix
206+ , reflex
207+ , reflex-dom-core
208+ , text
161209 hs-source-dirs: test
162210 ghc-options: -rtsopts -with-rtsopts=-T -Wall -fwarn-tabs -funbox-strict-fields -O2 -ferror-spans
163211 ghc-prof-options: -fprof-auto -optP-DPROFILING
reflex-dom-core/src/Reflex/Dom/Builder/Class.hs+53−3
@@ -35,6 +35,7 @@ import Reflex.Dom.Builder.Class.Events
3535#ifdef USE_TEMPLATE_HASKELL
3636import Reflex.Dom.Builder.Class.TH
3737#endif
38+import Reflex.BehaviorWriter.Base
3839import Reflex.DynamicWriter.Base
3940import Reflex.EventWriter.Base
4041import Reflex.NotReady.Class
@@ -61,13 +62,14 @@ import Data.String
6162import Data.Text (Text)
6263import Data.Type.Coercion
6364import GHCJS.DOM.Types (JSM)
65+import qualified GHCJS.DOM.Types as DOM
6466
6567class Default (EventSpec d EventResult) => DomSpace d where
6668 type EventSpec d :: (EventTag -> *) -> *
6769 type RawDocument d :: *
6870 type RawTextNode d :: *
71+ type RawCommentNode d :: *
6972 type RawElement d :: *
70− type RawFile d :: *
7173 type RawInputElement d :: *
7274 type RawTextAreaElement d :: *
7375 type RawSelectElement d :: *
@@ -86,6 +88,15 @@ class (Monad m, Reflex t, DomSpace (DomBuilderSpace m), NotReady t m, Adjustable
8688 => TextNodeConfig t -> m (TextNode (DomBuilderSpace m) t)
8789 textNode = lift . textNode
8890 {-# INLINABLE textNode #-}
91+ commentNode :: CommentNodeConfig t -> m (CommentNode (DomBuilderSpace m) t)
92+ default commentNode :: ( MonadTrans f
93+ , m ~ f m'
94+ , DomBuilderSpace m' ~ DomBuilderSpace m
95+ , DomBuilder t m'
96+ )
97+ => CommentNodeConfig t -> m (CommentNode (DomBuilderSpace m) t)
98+ commentNode = lift . commentNode
99+ {-# INLINABLE commentNode #-}
89100 element :: Text -> ElementConfig er t (DomBuilderSpace m) -> m a -> m (Element er (DomBuilderSpace m) t, a)
90101 default element :: ( MonadTransControl f
91102 , StT f a ~ a
@@ -175,6 +186,28 @@ newtype TextNode d t = TextNode
175186 { _textNode_raw :: RawTextNode d
176187 }
177188
189+data CommentNodeConfig t
190+ = CommentNodeConfig { _commentNodeConfig_initialContents :: {-# UNPACK #-} !Text
191+ , _commentNodeConfig_setContents :: !(Maybe (Event t Text))
192+ }
193+
194+#ifndef USE_TEMPLATE_HASKELL
195+commentNodeConfig_initialContents :: Lens' (CommentNodeConfig t) Text
196+commentNodeConfig_initialContents f (CommentNodeConfig a b) = (\a' -> CommentNodeConfig a' b) <$> f a
197+{-# INLINE commentNodeConfig_initialContents #-}
198+#endif
199+
200+instance (Reflex t) => Default (CommentNodeConfig t) where
201+ {-# INLINABLE def #-}
202+ def = CommentNodeConfig
203+ { _commentNodeConfig_initialContents = mempty
204+ , _commentNodeConfig_setContents = Nothing
205+ }
206+
207+newtype CommentNode d t = CommentNode
208+ { _commentNode_raw :: RawCommentNode d
209+ }
210+
178211data AttributeName = AttributeName !(Maybe Namespace) !Text deriving (Show, Read, Eq, Ord)
179212
180213mapKeysToAttributeName :: Map Text v -> Map AttributeName v
@@ -291,7 +324,7 @@ data InputElement er d t
291324 , _inputElement_hasFocus :: Dynamic t Bool
292325 , _inputElement_element :: Element er d t
293326 , _inputElement_raw :: RawInputElement d
294− , _inputElement_files :: Dynamic t [RawFile d]
327+ , _inputElement_files :: Dynamic t [DOM.File]
295328 }
296329
297330data TextAreaElementConfig er t m
@@ -393,6 +426,7 @@ data SelectElement er d t = SelectElement
393426#ifdef USE_TEMPLATE_HASKELL
394427concat <$> mapM (uncurry makeLensesWithoutField)
395428 [ (["_textNodeConfig_setContents"], ''TextNodeConfig)
429+ , (["_commentNodeConfig_setContents"], ''CommentNodeConfig)
396430 , ([ "_inputElementConfig_setValue"
397431 , "_inputElementConfig_setChecked" ], ''InputElementConfig)
398432 , (["_rawElementConfig_modifyAttributes"], ''RawElementConfig)
@@ -409,6 +443,13 @@ textNodeConfig_setContents =
409443 setter t e = t { _textNodeConfig_setContents = Just e }
410444 in lens getter setter
411445
446+-- | This lens is technically illegal. The implementation of 'CommentNodeConfig' uses a 'Maybe' under the hood for efficiency reasons. However, always interacting with 'CommentNodeConfig' via lenses will always behave correctly, and if you pattern match on it, you should always treat 'Nothing' as 'never'.
447+commentNodeConfig_setContents :: Reflex t => Lens (CommentNodeConfig t) (CommentNodeConfig t) (Event t Text) (Event t Text)
448+commentNodeConfig_setContents =
449+ let getter = fromMaybe never . _commentNodeConfig_setContents
450+ setter t e = t { _commentNodeConfig_setContents = Just e }
451+ in lens getter setter
452+
412453-- | This lens is technically illegal. The implementation of 'InputElementConfig' uses a 'Maybe' under the hood for efficiency reasons. However, always interacting with 'InputElementConfig' via lenses will always behave correctly, and if you pattern match on it, you should always treat 'Nothing' as 'never'.
413454inputElementConfig_setValue :: Reflex t => Lens' (InputElementConfig er t m) (Event t Text)
414455inputElementConfig_setValue =
@@ -521,6 +562,7 @@ instance (MountableDomBuilder t m, PerformEvent t m, MonadFix m, MonadHold t m)
521562instance (DomBuilder t m, Monoid w, MonadHold t m, MonadFix m) => DomBuilder t (DynamicWriterT t w m) where
522563 type DomBuilderSpace (DynamicWriterT t w m) = DomBuilderSpace m
523564 textNode = liftTextNode
565+ commentNode = liftCommentNode
524566 element elementTag cfg (DynamicWriterT child) = DynamicWriterT $ do
525567 s <- get
526568 (el, (a, newS)) <- lift $ element elementTag cfg $ runStateT child s
@@ -539,6 +581,7 @@ instance (DomBuilder t m, Monoid w, MonadHold t m, MonadFix m) => DomBuilder t (
539581instance (DomBuilder t m, MonadHold t m, MonadFix m) => DomBuilder t (RequesterT t request response m) where
540582 type DomBuilderSpace (RequesterT t request response m) = DomBuilderSpace m
541583 textNode = liftTextNode
584+ commentNode = liftCommentNode
542585 element elementTag cfg (RequesterT child) = RequesterT $ do
543586 r <- ask
544587 old <- get
@@ -559,6 +602,7 @@ instance (DomBuilder t m, MonadHold t m, MonadFix m) => DomBuilder t (RequesterT
559602instance (DomBuilder t m, MonadHold t m, MonadFix m, Semigroup w) => DomBuilder t (EventWriterT t w m) where
560603 type DomBuilderSpace (EventWriterT t w m) = DomBuilderSpace m
561604 textNode = liftTextNode
605+ commentNode = liftCommentNode
562606 element elementTag cfg (EventWriterT child) = EventWriterT $ do
563607 old <- get
564608 (el, (a, new)) <- lift $ element elementTag cfg $ runStateT child old
@@ -574,9 +618,10 @@ instance (DomBuilder t m, MonadHold t m, MonadFix m, Semigroup w) => DomBuilder
574618 placeRawElement = lift . placeRawElement
575619 wrapRawElement e = lift . wrapRawElement e
576620
577−instance (DomBuilder t m, MonadFix m, MonadHold t m, Group q, Query q, Additive q) => DomBuilder t (QueryT t q m) where
621+instance (DomBuilder t m, MonadFix m, MonadHold t m, Group q, Query q, Additive q, Eq q) => DomBuilder t (QueryT t q m) where
578622 type DomBuilderSpace (QueryT t q m) = DomBuilderSpace m
579623 textNode = liftTextNode
624+ commentNode = liftCommentNode
580625 element elementTag cfg (QueryT child) = QueryT $ do
581626 s <- get
582627 (e, (a, newS)) <- lift $ element elementTag cfg $ runStateT child s
@@ -645,6 +690,9 @@ liftWithStateless a = liftWith $ \run -> a $ fmap (fromStT (Proxy :: Proxy t)) .
645690liftTextNode :: (MonadTrans f, DomBuilder t m) => TextNodeConfig t -> f m (TextNode (DomBuilderSpace m) t)
646691liftTextNode = lift . textNode
647692
693+liftCommentNode :: (MonadTrans f, DomBuilder t m) => CommentNodeConfig t -> f m (CommentNode (DomBuilderSpace m) t)
694+liftCommentNode = lift . commentNode
695+
648696liftElement :: LiftDomBuilder t f m => Text -> ElementConfig er t (DomBuilderSpace m) -> f m a -> f m (Element er (DomBuilderSpace m) t, a)
649697liftElement elementTag cfg child = liftWithStateless $ \run -> element elementTag cfg $ run child
650698
@@ -668,7 +716,9 @@ instance DomRenderHook t m => DomRenderHook t (Lazy.StateT e m) where
668716 requestDomAction = lift . requestDomAction
669717 requestDomAction_ = lift . requestDomAction_
670718
719+deriving instance DomRenderHook t m => DomRenderHook t (BehaviorWriterT t w m)
671720deriving instance DomRenderHook t m => DomRenderHook t (EventWriterT t w m)
721+deriving instance DomRenderHook t m => DomRenderHook t (DynamicWriterT t w m)
672722deriving instance DomRenderHook t m => DomRenderHook t (RequesterT t req rsp m)
673723deriving instance DomRenderHook t m => DomRenderHook t (PostBuildT t m)
674724deriving instance DomRenderHook t m => DomRenderHook t (QueryT t q m)
reflex-dom-core/src/Reflex/Dom/Builder/Immediate.hs+1643−562
@@ -2,7 +2,8 @@
22{-# LANGUAGE CPP #-}
33{-# LANGUAGE ConstraintKinds #-}
44{-# LANGUAGE DataKinds #-}
5−{-# LANGUAGE DefaultSignatures #-}
5+{-# LANGUAGE DeriveFunctor #-}
6+{-# LANGUAGE DeriveTraversable #-}
67{-# LANGUAGE EmptyDataDecls #-}
78{-# LANGUAGE FlexibleContexts #-}
89{-# LANGUAGE FlexibleInstances #-}
@@ -11,14 +12,15 @@
1112{-# LANGUAGE LambdaCase #-}
1213{-# LANGUAGE MultiParamTypeClasses #-}
1314{-# LANGUAGE OverloadedStrings #-}
15+{-# LANGUAGE PatternGuards #-}
1416{-# LANGUAGE PolyKinds #-}
1517{-# LANGUAGE RankNTypes #-}
1618{-# LANGUAGE RecursiveDo #-}
1719{-# LANGUAGE ScopedTypeVariables #-}
18−{-# LANGUAGE StandaloneDeriving #-}
1920#ifdef USE_TEMPLATE_HASKELL
2021{-# LANGUAGE TemplateHaskell #-}
2122#endif
23+{-# LANGUAGE TupleSections #-}
2224{-# LANGUAGE TypeFamilies #-}
2325{-# LANGUAGE TypeOperators #-}
2426{-# LANGUAGE UndecidableInstances #-}
@@ -26,184 +28,346 @@
2628{-# LANGUAGE ForeignFunctionInterface #-}
2729{-# LANGUAGE JavaScriptFFI #-}
2830#endif
31+-- | This is a builder to be used on the client side. It can be run in two modes:
32+--
33+-- 1. in "hydration mode", reusing DOM nodes already in the page (as produced
34+-- by 'Reflex.Dom.Builder.Static.renderStatic')
35+-- 2. in "immediate mode", creating and appending DOM nodes as required
36+--
37+-- In "hydration mode", the preexisting DOM __must contain__ what the builder
38+-- will expect at switchover time (the time at which parity with the static
39+-- renderer is reached, and the time after which the page is "live").
40+--
41+-- For example, displaying the current time as text should be done inside
42+-- 'Reflex.Dom.Prerender.prerender' to ensure that we don't attempt to hydrate the incorrect text.
43+-- The server will prerender a text node with time A, and the client will expect
44+-- a text node with time B. Barring a miracle, time A and time B will not match,
45+-- and hydration will fail.
2946module Reflex.Dom.Builder.Immediate
30− ( EventTriggerRef (..)
31− , ImmediateDomBuilderEnv (..)
32− , ImmediateDomBuilderT (..)
33− , runImmediateDomBuilderT
34− , askParent
35− , askEvents
36− , append
37− , textNodeInternal
38− , deleteBetweenExclusive
39− , extractBetweenExclusive
40− , deleteUpTo
41− , extractUpTo
42− , SupportsImmediateDomBuilder
43− , collectUpTo
44− , collectUpToGivenParent
45− , EventFilterTriggerRef (..)
46− , wrap
47− , makeElement
48− , GhcjsDomHandler (..)
49− , GhcjsDomHandler1 (..)
50− , GhcjsDomEvent (..)
51− , GhcjsDomSpace
52− , GhcjsEventFilter (..)
53− , Pair1 (..)
54− , Maybe1 (..)
55− , GhcjsEventSpec (..)
56− , HasDocument (..)
57− , ghcjsEventSpec_filters
58− , ghcjsEventSpec_handler
59− , GhcjsEventHandler (..)
60−#ifndef USE_TEMPLATE_HASKELL
61− , phantom2
62−#endif
63− , drawChildUpdate
64− , ChildReadyState (..)
65− , ChildReadyStateInt (..)
66− , mkHasFocus
67− , insertBefore
68− , EventType
69− , defaultDomEventHandler
70− , defaultDomWindowEventHandler
71− , withIsEvent
72− , showEventName
73− , elementOnEventName
74− , windowOnEventName
75− , wrapDomEvent
76− , subscribeDomEvent
77− , wrapDomEventMaybe
78− , wrapDomEventsMaybe
79− , getKeyEvent
80− , getMouseEventCoords
81− , getTouchEvent
82− , WindowConfig (..)
83− , Window (..)
84− , wrapWindow
85− -- * Internal
86− , traverseDMapWithKeyWithAdjust'
87− , hoistTraverseWithKeyWithAdjust
88− , traverseIntMapWithKeyWithAdjust'
89− , hoistTraverseIntMapWithKeyWithAdjust
90− ) where
91−
92−import Foreign.JavaScript.TH
93−import Reflex.Adjustable.Class
94−import Reflex.Class as Reflex
95−import Reflex.Dom.Builder.Class
96−import Reflex.Dynamic
97−import Reflex.Host.Class
98−import qualified Reflex.Patch.DMap as PatchDMap
99−import qualified Reflex.Patch.DMapWithMove as PatchDMapWithMove
100−import Reflex.PerformEvent.Class
101−import Reflex.PostBuild.Class
102−import Reflex.TriggerEvent.Base hiding (askEvents)
103−import qualified Reflex.TriggerEvent.Base as TriggerEventT (askEvents)
104−import Reflex.TriggerEvent.Class
47+ ( HydrationDomBuilderT (..)
48+ , HydrationDomBuilderEnv (..)
49+ , HydrationMode (..)
50+ , HydrationRunnerT (..)
51+ , runHydrationRunnerT
52+ , ImmediateDomBuilderT
53+ , runHydrationDomBuilderT
54+ , getHydrationMode
55+ , addHydrationStep
56+ , addHydrationStepWithSetup
57+ , setPreviousNode
58+ , insertAfterPreviousNode
59+ , hydrateComment
60+ , askParent
61+ , askEvents
62+ , append
63+ , textNodeInternal
64+ , removeSubsequentNodes
65+ , deleteBetweenExclusive
66+ , extractBetweenExclusive
67+ , deleteUpTo
68+ , extractUpTo
69+ , SupportsHydrationDomBuilder
70+ , collectUpTo
71+ , collectUpToGivenParent
72+ , EventTriggerRef (..)
73+ , EventFilterTriggerRef (..)
74+ , wrap
75+ , elementInternal
76+ , HydrationDomSpace
77+ , GhcjsDomSpace
78+ , GhcjsDomHandler (..)
79+ , GhcjsDomHandler1 (..)
80+ , GhcjsDomEvent (..)
81+ , GhcjsEventFilter (..)
82+ , Pair1 (..)
83+ , Maybe1 (..)
84+ , GhcjsEventSpec (..)
85+ , HasDocument (..)
86+ , ghcjsEventSpec_filters
87+ , ghcjsEventSpec_handler
88+ , GhcjsEventHandler (..)
89+ , drawChildUpdate
90+ , ChildReadyState (..)
91+ , mkHasFocus
92+ , insertBefore
93+ , EventType
94+ , defaultDomEventHandler
95+ , defaultDomWindowEventHandler
96+ , withIsEvent
97+ , showEventName
98+ , elementOnEventName
99+ , windowOnEventName
100+ , wrapDomEvent
101+ , subscribeDomEvent
102+ , wrapDomEventMaybe
103+ , wrapDomEventsMaybe
104+ , getKeyEvent
105+ , getMouseEventCoords
106+ , getTouchEvent
107+ , WindowConfig (..)
108+ , Window (..)
109+ , wrapWindow
110+ -- * Internal
111+ , traverseDMapWithKeyWithAdjust'
112+ , hoistTraverseWithKeyWithAdjust
113+ , traverseIntMapWithKeyWithAdjust'
114+ , hoistTraverseIntMapWithKeyWithAdjust
115+ ) where
105116
106117import Control.Concurrent
107−import Control.Lens hiding (element, ix)
118+import Control.Exception (bracketOnError)
119+import Control.Lens (Identity(..), imapM_, iforM_, (^.), makeLenses)
108120import Control.Monad.Exception
109121import Control.Monad.Primitive
110122import Control.Monad.Reader
111123import Control.Monad.Ref
112−#ifndef USE_TEMPLATE_HASKELL
113−import Data.Functor.Contravariant (phantom)
114−#endif
124+import Control.Monad.State.Strict (StateT, mapStateT, get, modify', gets, runStateT)
115125import Data.Bitraversable
116126import Data.Default
117127import Data.Dependent.Map (DMap)
118−import qualified Data.Dependent.Map as DMap
119128import Data.Dependent.Sum
129+import Data.FastMutableIntMap (PatchIntMap (..))
130+import Data.Foldable (for_, traverse_)
120131import Data.Functor.Compose
121132import Data.Functor.Constant
122133import Data.Functor.Misc
123134import Data.Functor.Product
124135import Data.IORef
125−import qualified Data.Map as Map
136+import Data.IntMap.Strict (IntMap)
126137import Data.Maybe
127−import Data.Monoid hiding (Product)
128−import Data.Some (Some)
129−import qualified Data.Some as Some
138+import Data.Monoid ((<>))
139+import Data.Some (Some(..))
130140import Data.Text (Text)
141+import Foreign.JavaScript.Internal.Utils
142+import Foreign.JavaScript.TH
143+import GHCJS.DOM.Document (Document, createDocumentFragment, createElement, createElementNS, createTextNode, createComment)
144+import GHCJS.DOM.Element (getScrollTop, removeAttribute, removeAttributeNS, setAttribute, setAttributeNS, hasAttribute, hasAttributeNS)
145+import GHCJS.DOM.EventM (EventM, event, on)
146+import GHCJS.DOM.KeyboardEvent as KeyboardEvent
147+import GHCJS.DOM.MouseEvent
148+import GHCJS.DOM.Node (appendChild_, getOwnerDocumentUnchecked, getParentNodeUnchecked, setNodeValue, toNode)
149+import GHCJS.DOM.Types (liftJSM, askJSM, runJSM, JSM, MonadJSM, FocusEvent, IsElement, IsEvent, IsNode, KeyboardEvent, Node, TouchEvent, WheelEvent, uncheckedCastTo, ClipboardEvent)
150+import GHCJS.DOM.UIEvent
151+import Language.Javascript.JSaddle (call, eval)
152+import Reflex.Adjustable.Class
153+import Reflex.Class as Reflex
154+import Reflex.Dom.Builder.Class
155+import Reflex.Dynamic
156+import Reflex.Host.Class
157+import Reflex.Patch.DMapWithMove (PatchDMapWithMove(..))
158+import Reflex.Patch.MapWithMove (PatchMapWithMove(..))
159+import Reflex.PerformEvent.Base (PerformEventT)
160+import Reflex.PerformEvent.Class
161+import Reflex.PostBuild.Base (PostBuildT)
162+import Reflex.PostBuild.Class
163+#ifdef PROFILE_REFLEX
164+import Reflex.Profiled
165+#endif
166+import Reflex.Requester.Base
167+import Reflex.Requester.Class
168+import Reflex.Spider (Spider, SpiderHost, Global)
169+import Reflex.TriggerEvent.Base hiding (askEvents)
170+import Reflex.TriggerEvent.Class
171+
172+import qualified Data.Dependent.Map as DMap
173+import qualified Data.FastMutableIntMap as FastMutableIntMap
174+import qualified Data.IntMap.Strict as IntMap
175+import qualified Data.Map as Map
176+import qualified Data.Text as T
131177import qualified GHCJS.DOM as DOM
132−import GHCJS.DOM.Document (Document, createDocumentFragment, createElement, createElementNS, createTextNode)
133−import GHCJS.DOM.Element (getScrollTop, removeAttribute, removeAttributeNS, setAttribute, setAttributeNS)
178+import qualified GHCJS.DOM.DocumentAndElementEventHandlers as Events
179+import qualified GHCJS.DOM.DocumentOrShadowRoot as Document
134180import qualified GHCJS.DOM.Element as Element
135181import qualified GHCJS.DOM.Event as Event
136−import qualified GHCJS.DOM.GlobalEventHandlers as Events
137−import qualified GHCJS.DOM.DocumentAndElementEventHandlers as Events
138−import GHCJS.DOM.EventM (EventM, event, on)
139182import qualified GHCJS.DOM.EventM as DOM
140183import qualified GHCJS.DOM.FileList as FileList
184+import qualified GHCJS.DOM.GlobalEventHandlers as Events
141185import qualified GHCJS.DOM.HTMLInputElement as Input
142186import qualified GHCJS.DOM.HTMLSelectElement as Select
143187import qualified GHCJS.DOM.HTMLTextAreaElement as TextArea
144−import GHCJS.DOM.MouseEvent
188+import qualified GHCJS.DOM.Node as Node
189+import qualified GHCJS.DOM.Text as DOM
145190import qualified GHCJS.DOM.Touch as Touch
146191import qualified GHCJS.DOM.TouchEvent as TouchEvent
147192import qualified GHCJS.DOM.TouchList as TouchList
148−import GHCJS.DOM.Node (appendChild_, getOwnerDocumentUnchecked, getParentNodeUnchecked, setNodeValue, toNode)
149−import qualified GHCJS.DOM.Node as DOM (insertBefore_)
150−import GHCJS.DOM.Types
151− (liftJSM, askJSM, runJSM, JSM, MonadJSM,
152− FocusEvent, IsElement, IsEvent, IsNode, KeyboardEvent, Node,
153− ToDOMString, TouchEvent, WheelEvent, uncheckedCastTo, ClipboardEvent)
154193import qualified GHCJS.DOM.Types as DOM
155−import GHCJS.DOM.UIEvent
156−import GHCJS.DOM.KeyboardEvent as KeyboardEvent
157194import qualified GHCJS.DOM.Window as Window
158−import Language.Javascript.JSaddle (call, eval)
159−import Data.IntMap.Strict (IntMap)
160−import qualified Data.IntMap.Strict as IntMap
161−import Data.FastMutableIntMap (PatchIntMap (..))
162−import qualified Data.FastMutableIntMap as FastMutableIntMap
195+import qualified Reflex.Patch.DMap as PatchDMap
196+import qualified Reflex.Patch.DMapWithMove as PatchDMapWithMove
197+import qualified Reflex.Patch.MapWithMove as PatchMapWithMove
198+import qualified Reflex.TriggerEvent.Base as TriggerEventT (askEvents)
163199
164−import Reflex.Requester.Base
165−import Reflex.Requester.Class
166−import Foreign.JavaScript.Internal.Utils
200+#ifndef USE_TEMPLATE_HASKELL
201+import Data.Functor.Contravariant (phantom)
202+import Control.Lens (Lens', Getter)
203+#endif
167204
168205#ifndef ghcjs_HOST_OS
169206import GHCJS.DOM.Types (MonadJSM (..))
170207
171−instance MonadJSM m => MonadJSM (ImmediateDomBuilderT t m) where
172− liftJSM' = ImmediateDomBuilderT . liftJSM'
208+instance MonadJSM m => MonadJSM (HydrationRunnerT t m) where
209+ {-# INLINABLE liftJSM' #-}
210+ liftJSM' = lift . liftJSM'
211+
212+instance MonadJSM m => MonadJSM (HydrationDomBuilderT s t m) where
213+ {-# INLINABLE liftJSM' #-}
214+ liftJSM' = lift . liftJSM'
215+
216+instance MonadJSM m => MonadJSM (DomRenderHookT t m) where
217+ {-# INLINABLE liftJSM' #-}
218+ liftJSM' = lift . liftJSM'
173219#endif
174220
175−data ImmediateDomBuilderEnv t
176− = ImmediateDomBuilderEnv { _immediateDomBuilderEnv_document :: {-# UNPACK #-} !Document
177− , _immediateDomBuilderEnv_parent :: {-# UNPACK #-} !Node
178− , _immediateDomBuilderEnv_unreadyChildren :: {-# UNPACK #-} !(IORef Word) -- Number of children who still aren't fully rendered
179− , _immediateDomBuilderEnv_commitAction :: !(JSM ()) -- Action to take when all children are ready --TODO: we should probably get rid of this once we invoke it
180− }
221+data HydrationDomBuilderEnv t m = HydrationDomBuilderEnv
222+ { _hydrationDomBuilderEnv_document :: {-# UNPACK #-} !Document
223+ -- ^ Reference to the document
224+ , _hydrationDomBuilderEnv_parent :: !(Either Node (IORef Node))
225+ -- ^ This is in an IORef because in the time up to hydration we can't actually know what the
226+ -- parent is - we populate this reference during the DOM traversal at hydration time
227+ , _hydrationDomBuilderEnv_unreadyChildren :: {-# UNPACK #-} !(IORef Word)
228+ -- ^ Number of children who still aren't fully rendered
229+ , _hydrationDomBuilderEnv_commitAction :: !(JSM ())
230+ -- ^ Action to take when all children are ready --TODO: we should probably get rid of this once we invoke it
231+ , _hydrationDomBuilderEnv_hydrationMode :: {-# UNPACK #-} !(IORef HydrationMode)
232+ -- ^ In hydration mode? Should be switched to `HydrationMode_Immediate` after hydration is finished
233+ , _hydrationDomBuilderEnv_switchover :: !(Event t ())
234+ , _hydrationDomBuilderEnv_delayed :: {-# UNPACK #-} !(IORef (HydrationRunnerT t m ()))
235+ }
181236
182−newtype ImmediateDomBuilderT t m a = ImmediateDomBuilderT { unImmediateDomBuilderT :: ReaderT (ImmediateDomBuilderEnv t) (RequesterT t JSM Identity (TriggerEventT t m)) a }
237+-- | A monad for DomBuilder which just gets the results of children and pushes
238+-- work into an action that is delayed until after postBuild (to match the
239+-- static builder). The action runs in 'HydrationRunnerT', which performs the
240+-- DOM takeover and sets up the events, after which point this monad will
241+-- continue in the vein of 'ImmediateDomBuilderT'.
242+newtype HydrationDomBuilderT s t m a = HydrationDomBuilderT { unHydrationDomBuilderT :: ReaderT (HydrationDomBuilderEnv t m) (DomRenderHookT t m) a }
183243 deriving (Functor, Applicative, Monad, MonadFix, MonadIO, MonadException
184244#if MIN_VERSION_base(4,9,1)
185245 , MonadAsyncException
186246#endif
187247 )
188248
189−instance PrimMonad m => PrimMonad (ImmediateDomBuilderT x m) where
190− type PrimState (ImmediateDomBuilderT x m) = PrimState m
249+instance PrimMonad m => PrimMonad (HydrationDomBuilderT s t m) where
250+ type PrimState (HydrationDomBuilderT s t m) = PrimState m
191251 primitive = lift . primitive
192252
193−instance MonadTrans (ImmediateDomBuilderT t) where
194− lift = ImmediateDomBuilderT . lift . lift . lift
253+instance MonadTrans (HydrationDomBuilderT s t) where
254+ lift = HydrationDomBuilderT . lift . lift
255+
256+instance (Reflex t, MonadFix m) => DomRenderHook t (HydrationDomBuilderT s t m) where
257+ withRenderHook hook = HydrationDomBuilderT . mapReaderT (withRenderHook hook) . unHydrationDomBuilderT
258+ requestDomAction = HydrationDomBuilderT . lift . requestDomAction
259+ requestDomAction_ = HydrationDomBuilderT . lift . requestDomAction_
195260
196−instance (Reflex t, MonadFix m) => DomRenderHook t (ImmediateDomBuilderT t m) where
197− withRenderHook hook (ImmediateDomBuilderT a) = do
198− e <- ImmediateDomBuilderT ask
199− ImmediateDomBuilderT $ lift $ withRequesting $ \rsp -> do
200− (x, req) <- lift $ runRequesterT (runReaderT a e) $ runIdentity <$> rsp
261+-- | The monad which performs the delayed actions to reuse prerendered nodes and set up events.
262+-- State contains reference to the previous node sibling, if any, and the reader contains reference to the parent node.
263+newtype HydrationRunnerT t m a = HydrationRunnerT { unHydrationRunnerT :: StateT HydrationState (ReaderT Node (DomRenderHookT t m)) a }
264+ deriving (Functor, Applicative, Monad, MonadFix, MonadIO, MonadException
265+#if MIN_VERSION_base(4,9,1)
266+ , MonadAsyncException
267+#endif
268+ )
269+
270+data HydrationState = HydrationState
271+ { _hydrationState_previousNode :: !(Maybe Node)
272+ , _hydrationState_failed :: !Bool
273+ }
274+
275+{-# INLINABLE localRunner #-}
276+localRunner :: (MonadJSM m, Monad m) => HydrationRunnerT t m a -> Maybe Node -> Node -> HydrationRunnerT t m a
277+localRunner (HydrationRunnerT m) s parent = do
278+ s0 <- HydrationRunnerT get
279+ (a, s') <- HydrationRunnerT $ lift $ local (\_ -> parent) $ runStateT m (s0 { _hydrationState_previousNode = s })
280+ traverse_ removeSubsequentNodes $ _hydrationState_previousNode s'
281+ HydrationRunnerT $ modify' $ \hs -> hs { _hydrationState_failed = _hydrationState_failed s' }
282+ pure a
283+
284+{-# INLINABLE runHydrationRunnerT #-}
285+runHydrationRunnerT
286+ :: (MonadRef m, Ref m ~ IORef, Monad m, PerformEvent t m, MonadFix m, MonadReflexCreateTrigger t m, MonadJSM m, MonadJSM (Performable m))
287+ => HydrationRunnerT t m a -> Maybe Node -> Node -> Chan [DSum (EventTriggerRef t) TriggerInvocation] -> m a
288+runHydrationRunnerT (HydrationRunnerT m) s parent events = flip runDomRenderHookT events $ flip runReaderT parent $ do
289+ (a, s') <- runStateT m (HydrationState s False)
290+ traverse_ removeSubsequentNodes $ _hydrationState_previousNode s'
291+ when (_hydrationState_failed s') $ liftIO $ putStrLn "reflex-dom warning: hydration failed: the DOM was not as expected at switchover time. This may be due to invalid HTML which the browser has altered upon parsing, some external JS altering the DOM, or the page being served from an outdated cache."
292+ pure a
293+
294+instance MonadReflexCreateTrigger t m => MonadReflexCreateTrigger t (HydrationRunnerT t m) where
295+ {-# INLINABLE newEventWithTrigger #-}
296+ newEventWithTrigger = lift . newEventWithTrigger
297+ {-# INLINABLE newFanEventWithTrigger #-}
298+ newFanEventWithTrigger f = lift $ newFanEventWithTrigger f
299+
300+instance MonadTrans (HydrationRunnerT t) where
301+ {-# INLINABLE lift #-}
302+ lift = HydrationRunnerT . lift . lift . lift
303+
304+instance MonadSample t m => MonadSample t (HydrationRunnerT t m) where
305+ {-# INLINABLE sample #-}
306+ sample = lift . sample
307+
308+instance (Reflex t, MonadFix m) => DomRenderHook t (HydrationRunnerT t m) where
309+ withRenderHook hook = HydrationRunnerT . mapStateT (mapReaderT (withRenderHook hook)) . unHydrationRunnerT
310+ requestDomAction = HydrationRunnerT . lift . lift . requestDomAction
311+ requestDomAction_ = HydrationRunnerT . lift . lift . requestDomAction_
312+
313+-- | Add a hydration step which depends on some computation that should only be
314+-- done *before* the switchover to immediate mode - this is most likely some
315+-- form of 'hold' which we want to remove after hydration is done
316+{-# INLINABLE addHydrationStepWithSetup #-}
317+addHydrationStepWithSetup :: (Adjustable t m, MonadIO m) => m a -> (a -> HydrationRunnerT t m ()) -> HydrationDomBuilderT s t m ()
318+addHydrationStepWithSetup setup f = getHydrationMode >>= \case
319+ HydrationMode_Immediate -> pure ()
320+ HydrationMode_Hydrating -> do
321+ switchover <- HydrationDomBuilderT $ asks _hydrationDomBuilderEnv_switchover
322+ (s, _) <- lift $ runWithReplace setup $ return () <$ switchover
323+ addHydrationStep (f s)
324+
325+-- | Add a hydration step
326+{-# INLINABLE addHydrationStep #-}
327+addHydrationStep :: MonadIO m => HydrationRunnerT t m () -> HydrationDomBuilderT s t m ()
328+addHydrationStep m = do
329+ delayedRef <- HydrationDomBuilderT $ asks _hydrationDomBuilderEnv_delayed
330+ liftIO $ modifyIORef' delayedRef (>> m)
331+
332+-- | Shared behavior for HydrationDomBuilderT and HydrationRunnerT
333+newtype DomRenderHookT t m a = DomRenderHookT { unDomRenderHookT :: RequesterT t JSM Identity (TriggerEventT t m) a }
334+ deriving (Functor, Applicative, Monad, MonadFix, MonadIO, MonadException
335+#if MIN_VERSION_base(4,9,1)
336+ , MonadAsyncException
337+#endif
338+ )
339+
340+{-# INLINABLE runDomRenderHookT #-}
341+runDomRenderHookT
342+ :: (MonadFix m, PerformEvent t m, MonadReflexCreateTrigger t m, MonadJSM m, MonadJSM (Performable m), MonadRef m, Ref m ~ IORef)
343+ => DomRenderHookT t m a
344+ -> Chan [DSum (EventTriggerRef t) TriggerInvocation]
345+ -> m a
346+runDomRenderHookT (DomRenderHookT a) events = do
347+ flip runTriggerEventT events $ do
348+ rec (result, req) <- runRequesterT a rsp
349+ rsp <- performEventAsync $ ffor req $ \rm f -> liftJSM $ runInAnimationFrame f $
350+ traverseRequesterData (\r -> Identity <$> r) rm
351+ return result
352+ where
353+ runInAnimationFrame f x = void . DOM.inAnimationFrame' $ \_ -> do
354+ v <- synchronously x
355+ void . liftIO $ f v
356+
357+instance MonadTrans (DomRenderHookT t) where
358+ {-# INLINABLE lift #-}
359+ lift = DomRenderHookT . lift . lift
360+
361+instance (Reflex t, MonadFix m) => DomRenderHook t (DomRenderHookT t m) where
362+ withRenderHook hook (DomRenderHookT a) = do
363+ DomRenderHookT $ withRequesting $ \rsp -> do
364+ (x, req) <- lift $ runRequesterT a $ runIdentity <$> rsp
201365 return (ffor req $ \rm -> hook $ traverseRequesterData (\r -> Identity <$> r) rm, x)
202− requestDomAction = ImmediateDomBuilderT . lift . requestingIdentity
203− requestDomAction_ = ImmediateDomBuilderT . lift . requesting_
366+ requestDomAction = DomRenderHookT . requestingIdentity
367+ requestDomAction_ = DomRenderHookT . requesting_
204368
205−{-# INLINABLE runImmediateDomBuilderT #-}
206−runImmediateDomBuilderT
369+{-# INLINABLE runHydrationDomBuilderT #-}
370+runHydrationDomBuilderT
207371 :: ( MonadFix m
208372 , PerformEvent t m
209373 , MonadReflexCreateTrigger t m
@@ -212,50 +376,76 @@ runImmediateDomBuilderT
212376 , MonadRef m
213377 , Ref m ~ IORef
214378 )
215− => ImmediateDomBuilderT t m a
216− -> ImmediateDomBuilderEnv t
379+ => HydrationDomBuilderT s t m a
380+ -> HydrationDomBuilderEnv t m
217381 -> Chan [DSum (EventTriggerRef t) TriggerInvocation]
218382 -> m a
219−runImmediateDomBuilderT (ImmediateDomBuilderT a) env eventChan =
220− flip runTriggerEventT eventChan $ do
221− rec (x, req) <- runRequesterT (runReaderT a env) rsp
222− rsp <- performEventAsync $ ffor req $ \rm f -> liftJSM $ runInAnimationFrame f $
223− traverseRequesterData (\r -> Identity <$> r) rm
224− return x
225− where
226− runInAnimationFrame f x = void . DOM.inAnimationFrame' $ \_ -> do
227− v <- synchronously x
228− void . liftIO $ f v
383+runHydrationDomBuilderT (HydrationDomBuilderT a) env = runDomRenderHookT (runReaderT a env)
229384
230−instance Monad m => HasDocument (ImmediateDomBuilderT t m) where
385+instance (RawDocument (DomBuilderSpace (HydrationDomBuilderT s t m)) ~ Document, Monad m) => HasDocument (HydrationDomBuilderT s t m) where
231386 {-# INLINABLE askDocument #-}
232− askDocument = ImmediateDomBuilderT $ asks _immediateDomBuilderEnv_document
387+ askDocument = HydrationDomBuilderT $ asks _hydrationDomBuilderEnv_document
233388
234389{-# INLINABLE askParent #-}
235−askParent :: Monad m => ImmediateDomBuilderT t m Node
236−askParent = ImmediateDomBuilderT $ asks _immediateDomBuilderEnv_parent
390+askParent :: Monad m => HydrationRunnerT t m DOM.Node
391+askParent = HydrationRunnerT ask
392+
393+{-# INLINABLE getParent #-}
394+getParent :: MonadIO m => HydrationDomBuilderT s t m DOM.Node
395+getParent = either pure (liftIO . readIORef) =<< HydrationDomBuilderT (asks _hydrationDomBuilderEnv_parent)
237396
238397{-# INLINABLE askEvents #-}
239−askEvents :: Monad m => ImmediateDomBuilderT t m (Chan [DSum (EventTriggerRef t) TriggerInvocation])
240−askEvents = ImmediateDomBuilderT . lift . lift $ TriggerEventT.askEvents
398+askEvents :: Monad m => HydrationDomBuilderT s t m (Chan [DSum (EventTriggerRef t) TriggerInvocation])
399+askEvents = HydrationDomBuilderT . lift . DomRenderHookT . lift $ TriggerEventT.askEvents
241400
242−localEnv :: Monad m => (ImmediateDomBuilderEnv t -> ImmediateDomBuilderEnv t) -> ImmediateDomBuilderT t m a -> ImmediateDomBuilderT t m a
243−localEnv f = ImmediateDomBuilderT . local f . unImmediateDomBuilderT
401+{-# INLINABLE localEnv #-}
402+localEnv :: Monad m => (HydrationDomBuilderEnv t m -> HydrationDomBuilderEnv t m) -> HydrationDomBuilderT s t m a -> HydrationDomBuilderT s t m a
403+localEnv f = HydrationDomBuilderT . local (f $!) . unHydrationDomBuilderT
244404
245405{-# INLINABLE append #-}
246−append :: MonadJSM m => DOM.Node -> ImmediateDomBuilderT t m ()
406+append :: MonadJSM m => DOM.Node -> HydrationDomBuilderT s t m ()
247407append n = do
248− p <- askParent
408+ p <- getParent
249409 liftJSM $ appendChild_ p n
250410 return ()
251411
252−{-# INLINABLE textNodeInternal #-}
253−textNodeInternal :: (MonadJSM m, ToDOMString contents) => contents -> ImmediateDomBuilderT t m DOM.Text
254−textNodeInternal !t = do
255− doc <- askDocument
256− n <- liftJSM $ createTextNode doc t
257− append $ toNode n
258− return n
412+{-# SPECIALIZE append
413+ :: DOM.Node
414+ -> HydrationDomBuilderT s Spider HydrationM ()
415+ #-}
416+
417+data HydrationMode
418+ = HydrationMode_Hydrating
419+ -- ^ The time from initial load to parity with static builder
420+ | HydrationMode_Immediate
421+ -- ^ After hydration
422+ deriving (Eq, Ord, Show)
423+
424+{-# INLINABLE getPreviousNode #-}
425+getPreviousNode :: Monad m => HydrationRunnerT t m (Maybe DOM.Node)
426+getPreviousNode = HydrationRunnerT $ gets _hydrationState_previousNode
427+
428+{-# INLINABLE setPreviousNode #-}
429+setPreviousNode :: Monad m => Maybe DOM.Node -> HydrationRunnerT t m ()
430+setPreviousNode n = HydrationRunnerT $ modify' (\hs -> hs { _hydrationState_previousNode = n })
431+
432+{-# INLINABLE askUnreadyChildren #-}
433+askUnreadyChildren :: Monad m => HydrationDomBuilderT s t m (IORef Word)
434+askUnreadyChildren = HydrationDomBuilderT $ asks _hydrationDomBuilderEnv_unreadyChildren
435+
436+{-# INLINABLE askCommitAction #-}
437+askCommitAction :: Monad m => HydrationDomBuilderT s t m (JSM ())
438+askCommitAction = HydrationDomBuilderT $ asks _hydrationDomBuilderEnv_commitAction
439+
440+{-# INLINABLE getHydrationMode #-}
441+getHydrationMode :: MonadIO m => HydrationDomBuilderT s t m HydrationMode
442+getHydrationMode = liftIO . readIORef =<< HydrationDomBuilderT (asks _hydrationDomBuilderEnv_hydrationMode)
443+
444+-- | Remove all nodes after given node
445+removeSubsequentNodes :: (MonadJSM m, IsNode n) => n -> m ()
446+removeSubsequentNodes n = liftJSM $ do
447+ f <- eval ("(function(n) { while (n.nextSibling) { (n.parentNode).removeChild(n.nextSibling); }; })" :: Text)
448+ void $ call f f [n]
259449
260450-- | s and e must both be children of the same node and s must precede e;
261451-- all nodes between s and e will be removed, but s and e will not be removed
@@ -293,7 +483,7 @@ extractUpTo df s e = liftJSM $ do
293483 void $ call f f (df, s, e)
294484#endif
295485
296−type SupportsImmediateDomBuilder t m = (Reflex t, MonadJSM m, MonadHold t m, MonadFix m, MonadReflexCreateTrigger t m, MonadRef m, Ref m ~ Ref JSM, Adjustable t m, PrimMonad m)
486+type SupportsHydrationDomBuilder t m = (Reflex t, MonadJSM m, MonadHold t m, MonadFix m, MonadReflexCreateTrigger t m, MonadRef m, Ref m ~ Ref JSM, Adjustable t m, PrimMonad m, PerformEvent t m, MonadJSM (Performable m))
297487
298488{-# INLINABLE collectUpTo #-}
299489collectUpTo :: (MonadJSM m, IsNode start, IsNode end) => start -> end -> m DOM.DocumentFragment
@@ -311,10 +501,15 @@ collectUpToGivenParent currentParent s e = do
311501
312502newtype EventFilterTriggerRef t er (en :: EventTag) = EventFilterTriggerRef (IORef (Maybe (EventTrigger t (er en))))
313503
314−{-# INLINABLE wrap #-}
315−wrap :: forall m er t. (Reflex t, MonadFix m, MonadJSM m, MonadReflexCreateTrigger t m) => RawElement GhcjsDomSpace -> RawElementConfig er t GhcjsDomSpace -> ImmediateDomBuilderT t m (Element er GhcjsDomSpace t)
316−wrap e cfg = do
317− events <- askEvents
504+-- | This 'wrap' is only partial: it doesn't create the 'EventSelector' itself
505+{-# INLINE wrap #-}
506+wrap
507+ :: forall s m er t. (Reflex t, MonadJSM m, MonadReflexCreateTrigger t m, DomRenderHook t m, EventSpec s ~ GhcjsEventSpec)
508+ => Chan [DSum (EventTriggerRef t) TriggerInvocation]
509+ -> DOM.Element
510+ -> RawElementConfig er t s
511+ -> m (DMap EventName (EventFilterTriggerRef t er))
512+wrap events e cfg = do
318513 forM_ (_rawElementConfig_modifyAttributes cfg) $ \modifyAttrs -> requestDomAction_ $ ffor modifyAttrs $ imapM_ $ \(AttributeName mAttrNamespace n) mv -> case mAttrNamespace of
319514 Nothing -> maybe (removeAttribute e n) (setAttribute e n) mv
320515 Just ns -> maybe (removeAttributeNS e (Just ns) n) (setAttributeNS e (Just ns) n) mv
@@ -331,46 +526,74 @@ wrap e cfg = do
331526 mv <- liftJSM k --TODO: Only do this when the event is subscribed
332527 liftIO $ forM_ mv $ \v -> writeChan events [EventTriggerRef triggerRef :=> TriggerInvocation v (return ())]
333528 return $ en :=> EventFilterTriggerRef triggerRef
334− es <- do
335− let h :: GhcjsEventHandler er
336− !h = _ghcjsEventSpec_handler $ _rawElementConfig_eventSpec cfg -- Note: this needs to be done strictly and outside of the newFanEventWithTrigger, so that the newFanEventWithTrigger doesn't retain the entire cfg, which can cause a cyclic dependency that the GC won't be able to clean up
337− ctx <- askJSM
338− newFanEventWithTrigger $ \(WrapArg en) t ->
339− case DMap.lookup en eventTriggerRefs of
340− Just (EventFilterTriggerRef r) -> do
341− writeIORef r $ Just t
342− return $ do
343− writeIORef r Nothing
344− Nothing -> (`runJSM` ctx) <$> (`runJSM` ctx) (elementOnEventName en e $ do
345− evt <- DOM.event
346− mv <- lift $ unGhcjsEventHandler h (en, GhcjsDomEvent evt)
347− case mv of
348− Nothing -> return ()
349− Just v -> liftIO $ do
350− --TODO: I don't think this is quite right: if a new trigger is created between when this is enqueued and when it fires, this may not work quite right
351− ref <- newIORef $ Just t
352− writeChan events [EventTriggerRef ref :=> TriggerInvocation v (return ())])
353− return $ Element
354− { _element_events = es
355− , _element_raw = e
356− }
357−
358−{-# INLINABLE makeElement #-}
359−makeElement :: forall er t m a. (MonadJSM m, MonadFix m, MonadReflexCreateTrigger t m, Adjustable t m) => Text -> ElementConfig er t GhcjsDomSpace -> ImmediateDomBuilderT t m a -> ImmediateDomBuilderT t m ((Element er GhcjsDomSpace t, a), DOM.Element)
360−makeElement elementTag cfg child = do
361− doc <- askDocument
362− e <- liftJSM $ uncheckedCastTo DOM.Element <$> case cfg ^. namespace of
363− Nothing -> createElement doc elementTag
364− Just ens -> createElementNS doc (Just ens) elementTag
365− ImmediateDomBuilderT $ iforM_ (cfg ^. initialAttributes) $ \(AttributeName mAttrNamespace n) v -> case mAttrNamespace of
366− Nothing -> lift $ setAttribute e n v
367− Just ans -> lift $ setAttributeNS e (Just ans) n v
368− result <- flip localEnv child $ \env -> env
369− { _immediateDomBuilderEnv_parent = toNode e
370− }
371− append $ toNode e
372− wrapped <- wrap e $ extractRawElementConfig cfg
373− return ((wrapped, result), e)
529+ return eventTriggerRefs
530+
531+{-# SPECIALIZE wrap
532+ :: Chan [DSum (EventTriggerRef DomTimeline) TriggerInvocation]
533+ -> DOM.Element
534+ -> RawElementConfig er DomTimeline HydrationDomSpace
535+ -> HydrationDomBuilderT HydrationDomSpace DomTimeline HydrationM (DMap EventName (EventFilterTriggerRef DomTimeline er))
536+ #-}
537+
538+{-# SPECIALIZE wrap
539+ :: Chan [DSum (EventTriggerRef DomTimeline) TriggerInvocation]
540+ -> DOM.Element
541+ -> RawElementConfig er DomTimeline GhcjsDomSpace
542+ -> HydrationDomBuilderT GhcjsDomSpace DomTimeline HydrationM (DMap EventName (EventFilterTriggerRef DomTimeline er))
543+ #-}
544+
545+{-# INLINE triggerBody #-}
546+triggerBody
547+ :: forall s er t x. EventSpec s ~ GhcjsEventSpec
548+ => DOM.JSContextRef
549+ -> RawElementConfig er t s
550+ -> Chan [DSum (EventTriggerRef t) TriggerInvocation]
551+ -> DMap EventName (EventFilterTriggerRef t er)
552+ -> DOM.Element
553+ -> WrapArg er EventName x
554+ -> EventTrigger t x
555+ -> IO (IO ())
556+triggerBody ctx cfg events eventTriggerRefs e (WrapArg en) t = case DMap.lookup en eventTriggerRefs of
557+ Just (EventFilterTriggerRef r) -> do
558+ writeIORef r $ Just t
559+ return $ do
560+ writeIORef r Nothing
561+ Nothing -> (`runJSM` ctx) <$> (`runJSM` ctx) (elementOnEventName en e $ do
562+ evt <- DOM.event
563+ mv <- lift $ unGhcjsEventHandler handler (en, GhcjsDomEvent evt)
564+ case mv of
565+ Nothing -> return ()
566+ Just v -> liftIO $ do
567+ --TODO: I don't think this is quite right: if a new trigger is created between when this is enqueued and when it fires, this may not work quite right
568+ ref <- newIORef $ Just t
569+ writeChan events [EventTriggerRef ref :=> TriggerInvocation v (return ())])
570+ where
571+ -- Note: this needs to be done strictly and outside of the newFanEventWithTrigger, so that the newFanEventWithTrigger doesn't
572+ -- retain the entire cfg, which can cause a cyclic dependency that the GC won't be able to clean up
573+ handler :: GhcjsEventHandler er
574+ !handler = _ghcjsEventSpec_handler $ _rawElementConfig_eventSpec cfg
575+
576+{-# SPECIALIZE triggerBody
577+ :: DOM.JSContextRef
578+ -> RawElementConfig er DomTimeline HydrationDomSpace
579+ -> Chan [DSum (EventTriggerRef DomTimeline) TriggerInvocation]
580+ -> DMap EventName (EventFilterTriggerRef DomTimeline er)
581+ -> DOM.Element
582+ -> WrapArg er EventName x
583+ -> EventTrigger DomTimeline x
584+ -> IO (IO ())
585+ #-}
586+
587+{-# SPECIALIZE triggerBody
588+ :: DOM.JSContextRef
589+ -> RawElementConfig er DomTimeline GhcjsDomSpace
590+ -> Chan [DSum (EventTriggerRef DomTimeline) TriggerInvocation]
591+ -> DMap EventName (EventFilterTriggerRef DomTimeline er)
592+ -> DOM.Element
593+ -> WrapArg er EventName x
594+ -> EventTrigger DomTimeline x
595+ -> IO (IO ())
596+ #-}
374597
375598newtype GhcjsDomHandler a b = GhcjsDomHandler { unGhcjsDomHandler :: a -> JSM b }
376599
@@ -384,8 +607,8 @@ instance DomSpace GhcjsDomSpace where
384607 type EventSpec GhcjsDomSpace = GhcjsEventSpec
385608 type RawDocument GhcjsDomSpace = DOM.Document
386609 type RawTextNode GhcjsDomSpace = DOM.Text
610+ type RawCommentNode GhcjsDomSpace = DOM.Comment
387611 type RawElement GhcjsDomSpace = DOM.Element
388− type RawFile GhcjsDomSpace = DOM.File
389612 type RawInputElement GhcjsDomSpace = DOM.HTMLInputElement
390613 type RawTextAreaElement GhcjsDomSpace = DOM.HTMLTextAreaElement
391614 type RawSelectElement GhcjsDomSpace = DOM.HTMLSelectElement
@@ -417,15 +640,11 @@ data GhcjsEventSpec er = GhcjsEventSpec
417640newtype GhcjsEventHandler er = GhcjsEventHandler { unGhcjsEventHandler :: forall en. (EventName en, GhcjsDomEvent en) -> JSM (Maybe (er en)) }
418641
419642#ifndef USE_TEMPLATE_HASKELL
420−phantom2 :: (Functor f, Contravariant f) => f a -> f b
421−phantom2 = phantom
422−{-# INLINE phantom2 #-}
423−
424643ghcjsEventSpec_filters :: forall er . Lens' (GhcjsEventSpec er) (DMap EventName (GhcjsEventFilter er))
425644ghcjsEventSpec_filters f (GhcjsEventSpec a b) = (\a' -> GhcjsEventSpec a' b) <$> f a
426645{-# INLINE ghcjsEventSpec_filters #-}
427646ghcjsEventSpec_handler :: forall er en . Getter (GhcjsEventSpec er) ((EventName en, GhcjsDomEvent en) -> JSM (Maybe (er en)))
428−ghcjsEventSpec_handler f (GhcjsEventSpec _ (GhcjsEventHandler b)) = phantom2 (f b)
647+ghcjsEventSpec_handler f (GhcjsEventSpec _ (GhcjsEventHandler b)) = phantom (f b)
429648{-# INLINE ghcjsEventSpec_handler #-}
430649#endif
431650
@@ -438,137 +657,746 @@ instance er ~ EventResult => Default (GhcjsEventSpec er) where
438657 runReaderT (defaultDomEventHandler e en) evt
439658 }
440659
441−instance SupportsImmediateDomBuilder t m => NotReady t (ImmediateDomBuilderT t m) where
660+{-# INLINE makeElement #-}
661+makeElement :: MonadJSM m => Document -> Text -> ElementConfig er t s -> m DOM.Element
662+makeElement doc elementTag cfg = do
663+ e <- uncheckedCastTo DOM.Element <$> case cfg ^. namespace of
664+ Nothing -> createElement doc elementTag
665+ Just ens -> createElementNS doc (Just ens) elementTag
666+ iforM_ (cfg ^. initialAttributes) $ \(AttributeName mAttrNamespace n) v -> case mAttrNamespace of
667+ Nothing -> setAttribute e n v
668+ Just ans -> setAttributeNS e (Just ans) n v
669+ pure e
670+
671+{-# INLINE elementImmediate #-}
672+elementImmediate
673+ :: ( RawDocument (DomBuilderSpace (HydrationDomBuilderT s t m)) ~ Document, EventSpec s ~ GhcjsEventSpec
674+ , MonadJSM m, Reflex t, MonadReflexCreateTrigger t m, MonadFix m )
675+ => Text
676+ -> ElementConfig er t s
677+ -> HydrationDomBuilderT s t m a
678+ -> HydrationDomBuilderT s t m (Element er GhcjsDomSpace t, a)
679+elementImmediate elementTag cfg child = do
680+ doc <- askDocument
681+ ctx <- askJSM
682+ events <- askEvents
683+ parent <- getParent
684+ e <- makeElement doc elementTag cfg
685+ appendChild_ parent e
686+ -- Run the child builder with updated parent and previous sibling references
687+ result <- localEnv (\env -> env { _hydrationDomBuilderEnv_parent = Left $ toNode e }) child
688+ let rawCfg = extractRawElementConfig cfg
689+ eventTriggerRefs <- wrap events e rawCfg
690+ es <- newFanEventWithTrigger $ triggerBody ctx rawCfg events eventTriggerRefs e
691+ return (Element es e, result)
692+
693+{-# SPECIALIZE elementImmediate
694+ :: Text
695+ -> ElementConfig er DomTimeline HydrationDomSpace
696+ -> HydrationDomBuilderT HydrationDomSpace DomTimeline HydrationM a
697+ -> HydrationDomBuilderT HydrationDomSpace DomTimeline HydrationM (Element er GhcjsDomSpace DomTimeline, a)
698+ #-}
699+
700+{-# SPECIALIZE elementImmediate
701+ :: Text
702+ -> ElementConfig er DomTimeline GhcjsDomSpace
703+ -> HydrationDomBuilderT GhcjsDomSpace DomTimeline HydrationM a
704+ -> HydrationDomBuilderT GhcjsDomSpace DomTimeline HydrationM (Element er GhcjsDomSpace DomTimeline, a)
705+ #-}
706+
707+-- For specialisation
708+
709+-- | The Reflex timeline for interacting with the DOM
710+type DomTimeline =
711+#ifdef PROFILE_REFLEX
712+ ProfiledTimeline
713+#endif
714+ Spider
715+
716+-- | The ReflexHost the DOM lives in
717+type DomHost =
718+#ifdef PROFILE_REFLEX
719+ ProfiledM
720+#endif
721+ (SpiderHost Global)
722+
723+type DomCoreWidget x = PostBuildT DomTimeline (WithJSContextSingleton x (PerformEventT DomTimeline DomHost))
724+type HydrationM = DomCoreWidget ()
725+
726+{-# INLINE elementInternal #-}
727+elementInternal
728+ :: (MonadJSM m, Reflex t, MonadReflexCreateTrigger t m, MonadFix m)
729+ => Text
730+ -> ElementConfig er t HydrationDomSpace
731+ -> HydrationDomBuilderT HydrationDomSpace t m a
732+ -> HydrationDomBuilderT HydrationDomSpace t m (Element er HydrationDomSpace t, a)
733+elementInternal elementTag cfg child = getHydrationMode >>= \case
734+ HydrationMode_Immediate -> do
735+ (Element es _, result) <- elementImmediate elementTag cfg child
736+ return (Element es (), result)
737+ HydrationMode_Hydrating -> fst <$> hydrateElement elementTag cfg child
738+
739+{-# SPECIALIZE elementInternal
740+ :: Text
741+ -> ElementConfig er DomTimeline HydrationDomSpace
742+ -> HydrationDomBuilderT HydrationDomSpace DomTimeline HydrationM a
743+ -> HydrationDomBuilderT HydrationDomSpace DomTimeline HydrationM (Element er HydrationDomSpace DomTimeline, a)
744+ #-}
745+
746+{-# INLINE hydrateElement #-}
747+hydrateElement
748+ :: forall er t m a. (MonadJSM m, Reflex t, MonadReflexCreateTrigger t m, MonadFix m)
749+ => Text
750+ -> ElementConfig er t HydrationDomSpace
751+ -> HydrationDomBuilderT HydrationDomSpace t m a
752+ -> HydrationDomBuilderT HydrationDomSpace t m ((Element er HydrationDomSpace t, a), IORef DOM.Element)
753+hydrateElement elementTag cfg child = do
754+ ctx <- askJSM
755+ events <- askEvents
756+ -- Schedule everything for after postBuild, except for getting the result itself
757+ parentRef <- liftIO $ newIORef $ error "Parent not yet initialized"
758+ e' <- liftIO $ newIORef $ error "hydrateElement: Element not yet initialized"
759+ env <- HydrationDomBuilderT ask
760+ childDelayedRef <- liftIO $ newIORef $ pure ()
761+ let env' = env
762+ { _hydrationDomBuilderEnv_parent = Right parentRef
763+ , _hydrationDomBuilderEnv_delayed = childDelayedRef
764+ }
765+ result <- HydrationDomBuilderT $ lift $ runReaderT (unHydrationDomBuilderT child) env'
766+ wrapResult <- liftIO newEmptyMVar
767+ let ssrAttr = "data-ssr" :: DOM.JSString
768+ hasSSRAttribute :: DOM.Element -> HydrationRunnerT t m Bool
769+ hasSSRAttribute e = case cfg ^. namespace of
770+ Nothing -> hasAttribute e ssrAttr <* removeAttribute e ssrAttr
771+ Just ns -> hasAttributeNS e (Just ns) ssrAttr <* removeAttributeNS e (Just ns) ssrAttr
772+ childDom <- liftIO $ readIORef childDelayedRef
773+ let rawCfg = extractRawElementConfig cfg
774+ doc <- askDocument
775+ addHydrationStep $ do
776+ parent <- askParent
777+ lastHydrationNode <- getPreviousNode
778+ let go mLastNode = maybe (Node.getFirstChild parent) Node.getNextSibling mLastNode >>= \case
779+ Nothing -> do -- ran out of nodes, create the element
780+ HydrationRunnerT $ modify' $ \s -> s { _hydrationState_failed = True }
781+ e <- makeElement doc elementTag cfg
782+ insertAfterPreviousNode e
783+ pure e
784+ Just node -> DOM.castTo DOM.Element node >>= \case
785+ Nothing -> go (Just node) -- this node is not an element, skip
786+ Just e -> hasSSRAttribute e >>= \case
787+ False -> go (Just node) -- this element was not added by the static renderer, skip
788+ True -> do
789+ t <- Element.getTagName e
790+ -- TODO: check attributes?
791+ if T.toCaseFold elementTag == T.toCaseFold t
792+ then pure e
793+ -- we came to some other statically rendered element, so something has gone wrong
794+ else do
795+ HydrationRunnerT $ modify' $ \s -> s { _hydrationState_failed = True }
796+ n <- makeElement doc elementTag cfg
797+ insertAfterPreviousNode n
798+ pure n
799+ e <- go lastHydrationNode
800+ setPreviousNode $ Just $ toNode e
801+ -- Update the parent node used by the children
802+ liftIO $ writeIORef parentRef $ toNode e
803+ liftIO $ writeIORef e' e
804+ -- Setup events, store the result so we can wait on it later
805+ refs <- wrap events e rawCfg
806+ liftIO $ putMVar wrapResult (e, refs)
807+ localRunner childDom Nothing $ toNode e
808+ -- We need the EventSelector to switch to the real event handler after activation
809+ es <- newFanEventWithTrigger $ \(WrapArg en) t -> do
810+ cleanup <- newEmptyMVar
811+ threadId <- forkIO $ do
812+ -- Wait on the data we need from the delayed action
813+ (e, eventTriggerRefs) <- readMVar wrapResult
814+ bracketOnError
815+ -- Run the setup, acquiring the cleanup action
816+ (triggerBody ctx rawCfg events eventTriggerRefs e (WrapArg en) t)
817+ -- Run the cleanup, if we have it - but only when an exception is
818+ -- raised (we might get killed between acquiring the cleanup action
819+ -- from 'triggerBody' and putting it in the MVar)
820+ id
821+ -- Try to put this action into the cleanup MVar
822+ (putMVar cleanup)
823+ pure $ do
824+ tryReadMVar cleanup >>= \case
825+ Nothing -> killThread threadId
826+ Just c -> c
827+ return ((Element es (), result), e')
828+
829+{-# SPECIALIZE hydrateElement
830+ :: Text
831+ -> ElementConfig er DomTimeline HydrationDomSpace
832+ -> HydrationDomBuilderT HydrationDomSpace DomTimeline HydrationM a
833+ -> HydrationDomBuilderT HydrationDomSpace DomTimeline HydrationM ((Element er HydrationDomSpace DomTimeline, a), IORef DOM.Element)
834+ #-}
835+
836+{-# INLINE inputElementImmediate #-}
837+inputElementImmediate
838+ :: ( RawDocument (DomBuilderSpace (HydrationDomBuilderT s t m)) ~ Document, EventSpec s ~ GhcjsEventSpec
839+ , MonadJSM m, Reflex t, MonadReflexCreateTrigger t m, MonadFix m, MonadHold t m
840+ , MonadRef m, Ref m ~ IORef )
841+ => InputElementConfig er t s -> HydrationDomBuilderT s t m (InputElement er GhcjsDomSpace t)
842+inputElementImmediate cfg = do
843+ (e@(Element eventSelector domElement), ()) <- elementImmediate "input" (_inputElementConfig_elementConfig cfg) $ return ()
844+ let domInputElement = uncheckedCastTo DOM.HTMLInputElement domElement
845+ Input.setValue domInputElement $ cfg ^. inputElementConfig_initialValue
846+ v0 <- Input.getValue domInputElement
847+ let getMyValue = Input.getValue domInputElement
848+ valueChangedByUI <- requestDomAction $ liftJSM getMyValue <$ Reflex.select eventSelector (WrapArg Input)
849+ valueChangedBySetValue <- case _inputElementConfig_setValue cfg of
850+ Nothing -> return never
851+ Just eSetValue -> requestDomAction $ ffor eSetValue $ \v' -> do
852+ Input.setValue domInputElement v'
853+ getMyValue -- We get the value after setting it in case the browser has mucked with it somehow
854+ v <- holdDyn v0 $ leftmost
855+ [ valueChangedBySetValue
856+ , valueChangedByUI
857+ ]
858+ Input.setChecked domInputElement $ _inputElementConfig_initialChecked cfg
859+ checkedChangedByUI <- wrapDomEvent domInputElement (`on` Events.click) $ do
860+ Input.getChecked domInputElement
861+ checkedChangedBySetChecked <- case _inputElementConfig_setChecked cfg of
862+ Nothing -> return never
863+ Just eNewchecked -> requestDomAction $ ffor eNewchecked $ \newChecked -> do
864+ oldChecked <- Input.getChecked domInputElement
865+ Input.setChecked domInputElement newChecked
866+ return $ if newChecked /= oldChecked
867+ then Just newChecked
868+ else Nothing
869+ c <- holdDyn (_inputElementConfig_initialChecked cfg) $ leftmost
870+ [ fmapMaybe id checkedChangedBySetChecked
871+ , checkedChangedByUI
872+ ]
873+ hasFocus <- mkHasFocus e
874+ files <- holdDyn mempty <=< wrapDomEvent domInputElement (`on` Events.change) $ do
875+ mfiles <- Input.getFiles domInputElement
876+ let getMyFiles xs = fmap catMaybes . mapM (FileList.item xs) . flip take [0..] . fromIntegral =<< FileList.getLength xs
877+ maybe (return []) getMyFiles mfiles
878+ checked <- holdUniqDyn c
879+ return $ InputElement
880+ { _inputElement_value = v
881+ , _inputElement_checked = checked
882+ , _inputElement_checkedChange = checkedChangedByUI
883+ , _inputElement_input = valueChangedByUI
884+ , _inputElement_hasFocus = hasFocus
885+ , _inputElement_element = e
886+ , _inputElement_raw = domInputElement
887+ , _inputElement_files = files
888+ }
889+
890+{-# INLINE inputElementInternal #-}
891+inputElementInternal
892+ :: ( MonadJSM m, Reflex t, MonadReflexCreateTrigger t m, MonadFix m, MonadHold t m
893+ , MonadRef m, Ref m ~ IORef )
894+ => InputElementConfig er t HydrationDomSpace -> HydrationDomBuilderT HydrationDomSpace t m (InputElement er HydrationDomSpace t)
895+inputElementInternal cfg = getHydrationMode >>= \case
896+ HydrationMode_Immediate -> ffor (inputElementImmediate cfg) $ \result -> result
897+ { _inputElement_element = Element (_element_events $ _inputElement_element result) ()
898+ , _inputElement_raw = ()
899+ }
900+ HydrationMode_Hydrating -> do
901+ ((e, _), domElementRef) <- hydrateElement "input" (cfg ^. inputElementConfig_elementConfig) $ return ()
902+ (valueChangedByUI, triggerChangeByUI) <- newTriggerEvent
903+ (valueChangedBySetValue, triggerChangeBySetValue) <- newTriggerEvent
904+ (focusChange, triggerFocusChange) <- newTriggerEvent
905+ (checkedChangedByUI, triggerCheckedChangedByUI) <- newTriggerEvent
906+ (checkedChangedBySetChecked, triggerCheckedChangedBySetChecked) <- newTriggerEvent
907+ (fileChange, triggerFileChange) <- newTriggerEvent
908+ doc <- askDocument
909+ -- Expected initial value from config
910+ let v0 = _inputElementConfig_initialValue cfg
911+ addHydrationStep $ do
912+ domElement <- liftIO $ readIORef domElementRef
913+ let domInputElement = uncheckedCastTo DOM.HTMLInputElement domElement
914+ getValue = Input.getValue domInputElement
915+ -- The browser might have messed with the value, or the user could have
916+ -- altered it before activation, so we set it if it isn't what we expect
917+ liftJSM getValue >>= \v0' -> do
918+ when (v0' /= v0) $ liftIO $ triggerChangeByUI v0'
919+ -- Watch for user interaction and trigger event accordingly
920+ requestDomAction_ $ (liftJSM getValue >>= liftIO . triggerChangeByUI) <$ Reflex.select (_element_events e) (WrapArg Input)
921+ for_ (_inputElementConfig_setValue cfg) $ \eSetValue ->
922+ requestDomAction_ $ ffor eSetValue $ \v' -> do
923+ Input.setValue domInputElement v'
924+ v <- getValue -- We get the value after setting it in case the browser has mucked with it somehow
925+ liftIO $ triggerChangeBySetValue v
926+ let focusChange' = leftmost
927+ [ False <$ Reflex.select (_element_events e) (WrapArg Blur)
928+ , True <$ Reflex.select (_element_events e) (WrapArg Focus)
929+ ]
930+ liftIO . triggerFocusChange =<< Node.isSameNode (toNode domElement) . fmap toNode =<< Document.getActiveElement doc
931+ requestDomAction_ $ liftIO . triggerFocusChange <$> focusChange'
932+ Input.setChecked domInputElement $ _inputElementConfig_initialChecked cfg
933+ _ <- liftJSM $ domInputElement `on` Events.click $ do
934+ liftIO . triggerCheckedChangedByUI =<< Input.getChecked domInputElement
935+ for_ (_inputElementConfig_setChecked cfg) $ \eNewchecked ->
936+ requestDomAction $ ffor eNewchecked $ \newChecked -> do
937+ oldChecked <- Input.getChecked domInputElement
938+ Input.setChecked domInputElement newChecked
939+ when (newChecked /= oldChecked) $ liftIO $ triggerCheckedChangedBySetChecked newChecked
940+ _ <- liftJSM $ domInputElement `on` Events.change $ do
941+ mfiles <- Input.getFiles domInputElement
942+ let getMyFiles xs = fmap catMaybes . mapM (FileList.item xs) . flip take [0..] . fromIntegral =<< FileList.getLength xs
943+ liftIO . triggerFileChange =<< maybe (return []) getMyFiles mfiles
944+ return ()
945+ checked' <- holdDyn (_inputElementConfig_initialChecked cfg) $ leftmost
946+ [ checkedChangedBySetChecked
947+ , checkedChangedByUI
948+ ]
949+ checked <- holdUniqDyn checked'
950+ let initialFocus = False -- Assume it isn't focused, but we update the actual focus state at switchover
951+ hasFocus <- holdUniqDyn =<< holdDyn initialFocus focusChange
952+ v <- holdDyn v0 $ leftmost
953+ [ valueChangedBySetValue
954+ , valueChangedByUI
955+ ]
956+ files <- holdDyn mempty fileChange
957+ return $ InputElement
958+ { _inputElement_value = v
959+ , _inputElement_checked = checked
960+ , _inputElement_checkedChange = checkedChangedByUI
961+ , _inputElement_input = valueChangedByUI
962+ , _inputElement_hasFocus = hasFocus
963+ , _inputElement_element = e
964+ , _inputElement_raw = ()
965+ , _inputElement_files = files
966+ }
967+
968+{-# INLINE textAreaElementImmediate #-}
969+textAreaElementImmediate
970+ :: ( RawDocument (DomBuilderSpace (HydrationDomBuilderT s t m)) ~ Document, EventSpec s ~ GhcjsEventSpec
971+ , MonadJSM m, Reflex t, MonadReflexCreateTrigger t m, MonadFix m, MonadHold t m
972+ , MonadRef m, Ref m ~ IORef )
973+ => TextAreaElementConfig er t s -> HydrationDomBuilderT s t m (TextAreaElement er GhcjsDomSpace t)
974+textAreaElementImmediate cfg = do
975+ (e@(Element eventSelector domElement), _) <- elementImmediate "textarea" (cfg ^. textAreaElementConfig_elementConfig) $ return ()
976+ let domTextAreaElement = uncheckedCastTo DOM.HTMLTextAreaElement domElement
977+ TextArea.setValue domTextAreaElement $ cfg ^. textAreaElementConfig_initialValue
978+ v0 <- TextArea.getValue domTextAreaElement
979+ let getMyValue = TextArea.getValue domTextAreaElement
980+ valueChangedByUI <- requestDomAction $ liftJSM getMyValue <$ Reflex.select eventSelector (WrapArg Input)
981+ valueChangedBySetValue <- case _textAreaElementConfig_setValue cfg of
982+ Nothing -> return never
983+ Just eSetValue -> requestDomAction $ ffor eSetValue $ \v' -> do
984+ TextArea.setValue domTextAreaElement v'
985+ getMyValue -- We get the value after setting it in case the browser has mucked with it somehow
986+ v <- holdDyn v0 $ leftmost
987+ [ valueChangedBySetValue
988+ , valueChangedByUI
989+ ]
990+ hasFocus <- mkHasFocus e
991+ return $ TextAreaElement
992+ { _textAreaElement_value = v
993+ , _textAreaElement_input = valueChangedByUI
994+ , _textAreaElement_hasFocus = hasFocus
995+ , _textAreaElement_element = e
996+ , _textAreaElement_raw = domTextAreaElement
997+ }
998+
999+{-# INLINE textAreaElementInternal #-}
1000+textAreaElementInternal
1001+ :: ( MonadJSM m, Reflex t, MonadReflexCreateTrigger t m, MonadFix m, MonadHold t m
1002+ , MonadRef m, Ref m ~ IORef )
1003+ => TextAreaElementConfig er t HydrationDomSpace -> HydrationDomBuilderT HydrationDomSpace t m (TextAreaElement er HydrationDomSpace t)
1004+textAreaElementInternal cfg = getHydrationMode >>= \case
1005+ HydrationMode_Immediate -> ffor (textAreaElementImmediate cfg) $ \result -> result
1006+ { _textAreaElement_element = Element (_element_events $ _textAreaElement_element result) ()
1007+ , _textAreaElement_raw = ()
1008+ }
1009+ HydrationMode_Hydrating -> do
1010+ ((e, _), domElementRef) <- hydrateElement "textarea" (cfg ^. textAreaElementConfig_elementConfig) $ return ()
1011+ (valueChangedByUI, triggerChangeByUI) <- newTriggerEvent
1012+ (valueChangedBySetValue, triggerChangeBySetValue) <- newTriggerEvent
1013+ (focusChange, triggerFocusChange) <- newTriggerEvent
1014+ doc <- askDocument
1015+ -- Expected initial value from config
1016+ let v0 = _textAreaElementConfig_initialValue cfg
1017+ addHydrationStep $ do
1018+ domElement <- liftIO $ readIORef domElementRef
1019+ let domTextAreaElement = uncheckedCastTo DOM.HTMLTextAreaElement domElement
1020+ getValue = TextArea.getValue domTextAreaElement
1021+ -- The browser might have messed with the value, or the user could have
1022+ -- altered it before activation, so we set it if it isn't what we expect
1023+ liftJSM getValue >>= \v0' -> do
1024+ when (v0' /= v0) $ liftIO $ triggerChangeByUI v0'
1025+ -- Watch for user interaction and trigger event accordingly
1026+ requestDomAction_ $ (liftJSM getValue >>= liftIO . triggerChangeByUI) <$ Reflex.select (_element_events e) (WrapArg Input)
1027+ for_ (_textAreaElementConfig_setValue cfg) $ \eSetValue ->
1028+ requestDomAction_ $ ffor eSetValue $ \v' -> do
1029+ TextArea.setValue domTextAreaElement v'
1030+ v <- getValue -- We get the value after setting it in case the browser has mucked with it somehow
1031+ liftIO $ triggerChangeBySetValue v
1032+ let focusChange' = leftmost
1033+ [ False <$ Reflex.select (_element_events e) (WrapArg Blur)
1034+ , True <$ Reflex.select (_element_events e) (WrapArg Focus)
1035+ ]
1036+ liftIO . triggerFocusChange =<< Node.isSameNode (toNode domElement) . fmap toNode =<< Document.getActiveElement doc
1037+ requestDomAction_ $ liftIO . triggerFocusChange <$> focusChange'
1038+ let initialFocus = False -- Assume it isn't focused, but we update the actual focus state at switchover
1039+ hasFocus <- holdUniqDyn =<< holdDyn initialFocus focusChange
1040+ v <- holdDyn v0 $ leftmost
1041+ [ valueChangedBySetValue
1042+ , valueChangedByUI
1043+ ]
1044+ return $ TextAreaElement
1045+ { _textAreaElement_value = v
1046+ , _textAreaElement_input = valueChangedByUI
1047+ , _textAreaElement_hasFocus = hasFocus
1048+ , _textAreaElement_element = e
1049+ , _textAreaElement_raw = ()
1050+ }
1051+
1052+{-# INLINE selectElementImmediate #-}
1053+selectElementImmediate
1054+ :: ( EventSpec s ~ GhcjsEventSpec, RawDocument (DomBuilderSpace (HydrationDomBuilderT s t m)) ~ Document
1055+ , MonadJSM m, Reflex t, MonadReflexCreateTrigger t m, MonadFix m, MonadHold t m )
1056+ => SelectElementConfig er t s
1057+ -> HydrationDomBuilderT s t m a
1058+ -> HydrationDomBuilderT s t m (SelectElement er GhcjsDomSpace t, a)
1059+selectElementImmediate cfg child = do
1060+ (e@(Element eventSelector domElement), result) <- elementImmediate "select" (cfg ^. selectElementConfig_elementConfig) child
1061+ let domSelectElement = uncheckedCastTo DOM.HTMLSelectElement domElement
1062+ Select.setValue domSelectElement $ cfg ^. selectElementConfig_initialValue
1063+ v0 <- Select.getValue domSelectElement
1064+ let getMyValue = Select.getValue domSelectElement
1065+ valueChangedByUI <- requestDomAction $ liftJSM getMyValue <$ Reflex.select eventSelector (WrapArg Change)
1066+ valueChangedBySetValue <- case _selectElementConfig_setValue cfg of
1067+ Nothing -> return never
1068+ Just eSetValue -> requestDomAction $ ffor eSetValue $ \v' -> do
1069+ Select.setValue domSelectElement v'
1070+ getMyValue -- We get the value after setting it in case the browser has mucked with it somehow
1071+ v <- holdDyn v0 $ leftmost
1072+ [ valueChangedBySetValue
1073+ , valueChangedByUI
1074+ ]
1075+ hasFocus <- mkHasFocus e
1076+ let wrapped = SelectElement
1077+ { _selectElement_value = v
1078+ , _selectElement_change = valueChangedByUI
1079+ , _selectElement_hasFocus = hasFocus
1080+ , _selectElement_element = e
1081+ , _selectElement_raw = domSelectElement
1082+ }
1083+ return (wrapped, result)
1084+
1085+{-# INLINE selectElementInternal #-}
1086+selectElementInternal
1087+ :: ( MonadJSM m, Reflex t, MonadReflexCreateTrigger t m, MonadFix m, MonadHold t m
1088+ , MonadRef m, Ref m ~ IORef )
1089+ => SelectElementConfig er t HydrationDomSpace
1090+ -> HydrationDomBuilderT HydrationDomSpace t m a
1091+ -> HydrationDomBuilderT HydrationDomSpace t m (SelectElement er HydrationDomSpace t, a)
1092+selectElementInternal cfg child = getHydrationMode >>= \case
1093+ HydrationMode_Immediate -> ffor (selectElementImmediate cfg child) $ \(e, result) -> (e
1094+ { _selectElement_element = Element (_element_events $ _selectElement_element e) ()
1095+ , _selectElement_raw = ()
1096+ }, result)
1097+ HydrationMode_Hydrating -> do
1098+ ((e, result), domElementRef) <- hydrateElement "select" (cfg ^. selectElementConfig_elementConfig) child
1099+ (valueChangedByUI, triggerChangeByUI) <- newTriggerEvent
1100+ (valueChangedBySetValue, triggerChangeBySetValue) <- newTriggerEvent
1101+ (focusChange, triggerFocusChange) <- newTriggerEvent
1102+ doc <- askDocument
1103+ -- Expected initial value from config
1104+ let v0 = _selectElementConfig_initialValue cfg
1105+ addHydrationStep $ do
1106+ domElement <- liftIO $ readIORef domElementRef
1107+ let domSelectElement = uncheckedCastTo DOM.HTMLSelectElement domElement
1108+ getValue = Select.getValue domSelectElement
1109+ -- The browser might have messed with the value, or the user could have
1110+ -- altered it before activation, so we set it if it isn't what we expect
1111+ liftJSM getValue >>= \v0' -> do
1112+ when (v0' /= v0) $ liftIO $ triggerChangeByUI v0'
1113+ -- Watch for user interaction and trigger event accordingly
1114+ requestDomAction_ $ (liftJSM getValue >>= liftIO . triggerChangeByUI) <$ Reflex.select (_element_events e) (WrapArg Change)
1115+ for_ (_selectElementConfig_setValue cfg) $ \eSetValue ->
1116+ requestDomAction_ $ ffor eSetValue $ \v' -> do
1117+ Select.setValue domSelectElement v'
1118+ v <- getValue -- We get the value after setting it in case the browser has mucked with it somehow
1119+ liftIO $ triggerChangeBySetValue v
1120+ let focusChange' = leftmost
1121+ [ False <$ Reflex.select (_element_events e) (WrapArg Blur)
1122+ , True <$ Reflex.select (_element_events e) (WrapArg Focus)
1123+ ]
1124+ liftIO . triggerFocusChange =<< Node.isSameNode (toNode domElement) . fmap toNode =<< Document.getActiveElement doc
1125+ requestDomAction_ $ liftIO . triggerFocusChange <$> focusChange'
1126+ let initialFocus = False -- Assume it isn't focused, but we update the actual focus state at switchover
1127+ hasFocus <- holdUniqDyn =<< holdDyn initialFocus focusChange
1128+ v <- holdDyn v0 $ leftmost
1129+ [ valueChangedBySetValue
1130+ , valueChangedByUI
1131+ ]
1132+ return $ (,result) $ SelectElement
1133+ { _selectElement_value = v
1134+ , _selectElement_change = valueChangedByUI
1135+ , _selectElement_hasFocus = hasFocus
1136+ , _selectElement_element = e
1137+ , _selectElement_raw = ()
1138+ }
1139+
1140+{-# INLINE textNodeImmediate #-}
1141+textNodeImmediate
1142+ :: (RawDocument (DomBuilderSpace (HydrationDomBuilderT s t m)) ~ Document, MonadJSM m, Reflex t, MonadFix m)
1143+ => TextNodeConfig t -> HydrationDomBuilderT s t m DOM.Text
1144+textNodeImmediate (TextNodeConfig !t mSetContents) = do
1145+ p <- getParent
1146+ doc <- askDocument
1147+ n <- createTextNode doc t
1148+ appendChild_ p n
1149+ mapM_ (requestDomAction_ . fmap (setNodeValue n . Just)) mSetContents
1150+ pure n
1151+
1152+{-# SPECIALIZE textNodeImmediate
1153+ :: TextNodeConfig DomTimeline
1154+ -> HydrationDomBuilderT HydrationDomSpace DomTimeline HydrationM DOM.Text
1155+ #-}
1156+
1157+{-# SPECIALIZE textNodeImmediate
1158+ :: TextNodeConfig DomTimeline
1159+ -> HydrationDomBuilderT GhcjsDomSpace DomTimeline HydrationM DOM.Text
1160+ #-}
1161+
1162+{-# INLINE textNodeInternal #-}
1163+textNodeInternal
1164+ :: (Adjustable t m, MonadHold t m, MonadJSM m, MonadFix m, Reflex t)
1165+ => TextNodeConfig t -> HydrationDomBuilderT HydrationDomSpace t m (TextNode HydrationDomSpace t)
1166+textNodeInternal tc@(TextNodeConfig !t mSetContents) = do
1167+ doc <- askDocument
1168+ getHydrationMode >>= \case
1169+ HydrationMode_Immediate -> void $ textNodeImmediate tc
1170+ HydrationMode_Hydrating -> addHydrationStepWithSetup (maybe (pure $ pure t) (hold t) mSetContents) $ \currentText -> do
1171+ n <- hydrateTextNode doc =<< sample currentText
1172+ mapM_ (requestDomAction_ . fmap (setNodeValue n . Just)) mSetContents
1173+ pure $ TextNode ()
1174+
1175+{-# SPECIALIZE textNodeInternal
1176+ :: TextNodeConfig DomTimeline
1177+ -> HydrationDomBuilderT HydrationDomSpace DomTimeline HydrationM (TextNode HydrationDomSpace DomTimeline)
1178+ #-}
1179+
1180+-- | The static builder mashes adjacent text nodes into one node: we check the
1181+-- text content of each node we come to, comparing it to the content we
1182+-- expect. We also have a special case for empty text nodes - we always create
1183+-- the and add them after the previous node reference.
1184+{-# INLINE hydrateTextNode #-}
1185+hydrateTextNode :: MonadJSM m => Document -> Text -> HydrationRunnerT t m DOM.Text
1186+hydrateTextNode doc t@"" = do
1187+ tn <- createTextNode doc t
1188+ insertAfterPreviousNode tn
1189+ pure tn
1190+hydrateTextNode doc t = do
1191+ n <- join $ go <$> askParent <*> getPreviousNode
1192+ setPreviousNode $ Just $ toNode n
1193+ return n
1194+ where
1195+ go parent mLastNode = maybe (Node.getFirstChild parent) Node.getNextSibling mLastNode >>= \case
1196+ Nothing -> do
1197+ HydrationRunnerT $ modify' $ \s -> s { _hydrationState_failed = True }
1198+ n <- createTextNode doc t
1199+ insertAfterPreviousNode n
1200+ pure n
1201+ Just node -> DOM.castTo DOM.Text node >>= \case
1202+ Nothing -> go parent $ Just node
1203+ Just originalNode -> do
1204+ originalText <- Node.getTextContentUnchecked originalNode
1205+ case T.stripPrefix t originalText of
1206+ Just "" -> return originalNode
1207+ Just _ -> do
1208+ -- If we have the right prefix, we split the text node into a node containing the
1209+ -- required text and a subsequent sibling node containing the rest of the text.
1210+ DOM.splitText_ originalNode $ fromIntegral $ T.length t
1211+ return originalNode
1212+ Nothing -> do
1213+ HydrationRunnerT $ modify' $ \s -> s { _hydrationState_failed = True }
1214+ n <- createTextNode doc t
1215+ insertAfterPreviousNode n
1216+ pure n
1217+
1218+{-# INLINE commentNodeImmediate #-}
1219+commentNodeImmediate
1220+ :: (RawDocument (DomBuilderSpace (HydrationDomBuilderT s t m)) ~ Document, MonadJSM m, Reflex t, MonadFix m)
1221+ => CommentNodeConfig t -> HydrationDomBuilderT s t m DOM.Comment
1222+commentNodeImmediate (CommentNodeConfig !t mSetContents) = do
1223+ p <- getParent
1224+ doc <- askDocument
1225+ n <- createComment doc t
1226+ appendChild_ p n
1227+ mapM_ (requestDomAction_ . fmap (setNodeValue n . Just)) mSetContents
1228+ pure n
1229+
1230+{-# INLINE commentNodeInternal #-}
1231+commentNodeInternal
1232+ :: (Ref m ~ IORef, MonadRef m, PerformEvent t m, MonadReflexCreateTrigger t m, MonadJSM (Performable m), MonadJSM m, MonadFix m, Reflex t, Adjustable t m, MonadHold t m, MonadSample t m)
1233+ => CommentNodeConfig t -> HydrationDomBuilderT HydrationDomSpace t m (CommentNode HydrationDomSpace t)
1234+commentNodeInternal tc@(CommentNodeConfig t0 mSetContents) = do
1235+ doc <- askDocument
1236+ getHydrationMode >>= \case
1237+ HydrationMode_Immediate -> void $ commentNodeInternal tc
1238+ HydrationMode_Hydrating -> addHydrationStepWithSetup (maybe (pure $ pure t0) (hold t0) mSetContents) $ \bt -> do
1239+ t <- sample bt
1240+ void $ hydrateComment doc t mSetContents
1241+ pure $ CommentNode ()
1242+
1243+{-# INLINE hydrateComment #-}
1244+hydrateComment :: (MonadJSM m, Reflex t, MonadFix m) => Document -> Text -> Maybe (Event t Text) -> HydrationRunnerT t m DOM.Comment
1245+hydrateComment doc t mSetContents = do
1246+ parent <- askParent
1247+ let go mLastNode = maybe (Node.getFirstChild parent) Node.getNextSibling mLastNode >>= \case
1248+ Nothing -> do
1249+ c <- createComment doc t
1250+ insertAfterPreviousNode c
1251+ pure c
1252+ Just node -> DOM.castTo DOM.Comment node >>= \case
1253+ Nothing -> go (Just node)
1254+ Just c -> do
1255+ t' <- Node.getTextContentUnchecked c
1256+ if t == t'
1257+ then pure c
1258+ else do
1259+ c' <- createComment doc t
1260+ insertAfterPreviousNode c'
1261+ pure c'
1262+ n <- go =<< getPreviousNode
1263+ setPreviousNode $ Just $ toNode n
1264+ mapM_ (requestDomAction_ . fmap (setNodeValue n . Just)) mSetContents
1265+ pure n
1266+
1267+-- | We leave markers in the static builder as comments, and rip these comments
1268+-- out at hydration time, replacing them with empty text nodes.
1269+{-# INLINABLE skipToAndReplaceComment #-}
1270+skipToAndReplaceComment
1271+ :: (MonadJSM m, Reflex t, MonadFix m, Adjustable t m, MonadHold t m, RawDocument (DomBuilderSpace (HydrationDomBuilderT s t m)) ~ Document)
1272+ => Text
1273+ -> IORef Text
1274+ -> HydrationDomBuilderT s t m (HydrationRunnerT t m (), IORef DOM.Text, IORef Text)
1275+skipToAndReplaceComment prefix key0Ref = getHydrationMode >>= \case
1276+ HydrationMode_Immediate -> do
1277+ -- If we're in immediate mode, we don't try to replace an existing comment,
1278+ -- and just return a dummy key
1279+ t <- textNodeImmediate $ TextNodeConfig ("" :: Text) Nothing
1280+ append $ toNode t
1281+ textNodeRef <- liftIO $ newIORef t
1282+ keyRef <- liftIO $ newIORef ""
1283+ pure (pure (), textNodeRef, keyRef)
1284+ HydrationMode_Hydrating -> do
1285+ doc <- askDocument
1286+ textNodeRef <- liftIO $ newIORef $ error "textNodeRef not yet initialized"
1287+ keyRef <- liftIO $ newIORef $ error "keyRef not yet initialized"
1288+ let go key0 mLastNode = do
1289+ parent <- askParent
1290+ node <- maybe (Node.getFirstChildUnchecked parent) Node.getNextSiblingUnchecked mLastNode
1291+ DOM.castTo DOM.Comment node >>= \case
1292+ Just comment -> do
1293+ commentText <- Node.getTextContentUnchecked comment
1294+ case T.stripPrefix (prefix <> key0) commentText of
1295+ Just key -> do
1296+ -- Replace the comment with an (invisible) text node
1297+ tn <- createTextNode doc ("" :: Text)
1298+ Node.replaceChild_ parent tn comment
1299+ pure (tn, key)
1300+ Nothing -> do
1301+ go key0 (Just node)
1302+ Nothing -> do
1303+ go key0 (Just node)
1304+ switchComment = do
1305+ key0 <- liftIO $ readIORef key0Ref
1306+ (tn, key) <- go key0 =<< getPreviousNode
1307+ setPreviousNode $ Just $ toNode tn
1308+ liftIO $ do
1309+ writeIORef textNodeRef tn
1310+ writeIORef keyRef key
1311+ pure (switchComment, textNodeRef, keyRef)
1312+
1313+{-# INLINABLE skipToReplaceStart #-}
1314+skipToReplaceStart :: (MonadJSM m, Reflex t, MonadFix m, Adjustable t m, MonadHold t m, RawDocument (DomBuilderSpace (HydrationDomBuilderT s t m)) ~ Document) => HydrationDomBuilderT s t m (HydrationRunnerT t m (), IORef DOM.Text, IORef Text)
1315+skipToReplaceStart = skipToAndReplaceComment "replace-start" =<< liftIO (newIORef "")
1316+
1317+{-# INLINABLE skipToReplaceEnd #-}
1318+skipToReplaceEnd :: (MonadJSM m, Reflex t, MonadFix m, Adjustable t m, MonadHold t m, RawDocument (DomBuilderSpace (HydrationDomBuilderT s t m)) ~ Document) => IORef Text -> HydrationDomBuilderT s t m (HydrationRunnerT t m (), IORef DOM.Text)
1319+skipToReplaceEnd key = fmap (\(m,e,_) -> (m,e)) $ skipToAndReplaceComment "replace-end" key
1320+
1321+instance SupportsHydrationDomBuilder t m => NotReady t (HydrationDomBuilderT s t m) where
4421322 notReadyUntil e = do
4431323 eOnce <- headE e
444− env <- ImmediateDomBuilderT ask
445− let unreadyChildren = _immediateDomBuilderEnv_unreadyChildren env
1324+ unreadyChildren <- askUnreadyChildren
1325+ commitAction <- askCommitAction
4461326 liftIO $ modifyIORef' unreadyChildren succ
4471327 let ready = do
4481328 old <- liftIO $ readIORef unreadyChildren
4491329 let new = pred old
4501330 liftIO $ writeIORef unreadyChildren $! new
451− when (new == 0) $ _immediateDomBuilderEnv_commitAction env
1331+ when (new == 0) commitAction
4521332 requestDomAction_ $ ready <$ eOnce
4531333 notReady = do
454− env <- ImmediateDomBuilderT ask
455− let unreadyChildren = _immediateDomBuilderEnv_unreadyChildren env
1334+ unreadyChildren <- askUnreadyChildren
4561335 liftIO $ modifyIORef' unreadyChildren succ
4571336
458−instance SupportsImmediateDomBuilder t m => DomBuilder t (ImmediateDomBuilderT t m) where
459− type DomBuilderSpace (ImmediateDomBuilderT t m) = GhcjsDomSpace
1337+data HydrationDomSpace
1338+
1339+instance DomSpace HydrationDomSpace where
1340+ type EventSpec HydrationDomSpace = GhcjsEventSpec
1341+ type RawDocument HydrationDomSpace = DOM.Document
1342+ type RawTextNode HydrationDomSpace = ()
1343+ type RawCommentNode HydrationDomSpace = ()
1344+ type RawElement HydrationDomSpace = ()
1345+ type RawInputElement HydrationDomSpace = ()
1346+ type RawTextAreaElement HydrationDomSpace = ()
1347+ type RawSelectElement HydrationDomSpace = ()
1348+ addEventSpecFlags _ en f es = es
1349+ { _ghcjsEventSpec_filters =
1350+ let f' = Just . GhcjsEventFilter . \case
1351+ Nothing -> \evt -> do
1352+ mEventResult <- unGhcjsEventHandler (_ghcjsEventSpec_handler es) (en, evt)
1353+ return (f mEventResult, return mEventResult)
1354+ Just (GhcjsEventFilter oldFilter) -> \evt -> do
1355+ (oldFlags, oldContinuation) <- oldFilter evt
1356+ mEventResult <- oldContinuation
1357+ let newFlags = oldFlags <> f mEventResult
1358+ return (newFlags, return mEventResult)
1359+ in DMap.alter f' en $ _ghcjsEventSpec_filters es
1360+ }
1361+
1362+instance SupportsHydrationDomBuilder t m => DomBuilder t (HydrationDomBuilderT HydrationDomSpace t m) where
1363+ type DomBuilderSpace (HydrationDomBuilderT HydrationDomSpace t m) = HydrationDomSpace
1364+ {-# INLINABLE element #-}
1365+ element = elementInternal
4601366 {-# INLINABLE textNode #-}
461− textNode (TextNodeConfig initialContents mSetContents) = do
462− n <- textNodeInternal initialContents
463− mapM_ (requestDomAction_ . fmap (setNodeValue n . Just)) mSetContents
464− return $ TextNode n
1367+ textNode = textNodeInternal
1368+ {-# INLINABLE commentNode #-}
1369+ commentNode = commentNodeInternal
1370+ {-# INLINABLE inputElement #-}
1371+ inputElement = inputElementInternal
1372+ {-# INLINABLE textAreaElement #-}
1373+ textAreaElement = textAreaElementInternal
1374+ {-# INLINABLE selectElement #-}
1375+ selectElement = selectElementInternal
1376+ placeRawElement () = pure ()
1377+ wrapRawElement () _cfg = pure $ Element (EventSelector $ const never) ()
1378+
1379+instance SupportsHydrationDomBuilder t m => DomBuilder t (HydrationDomBuilderT GhcjsDomSpace t m) where
1380+ type DomBuilderSpace (HydrationDomBuilderT GhcjsDomSpace t m) = GhcjsDomSpace
4651381 {-# INLINABLE element #-}
466− element elementTag cfg child = fst <$> makeElement elementTag cfg child
1382+ element = elementImmediate
1383+ {-# INLINABLE textNode #-}
1384+ textNode = fmap TextNode . textNodeImmediate
1385+ {-# INLINABLE commentNode #-}
1386+ commentNode = fmap CommentNode . commentNodeImmediate
4671387 {-# INLINABLE inputElement #-}
468− inputElement cfg = do
469− ((e, _), domElement) <- makeElement "input" (cfg ^. inputElementConfig_elementConfig) $ return ()
470− let domInputElement = uncheckedCastTo DOM.HTMLInputElement domElement
471− Input.setValue domInputElement $ cfg ^. inputElementConfig_initialValue
472− v0 <- Input.getValue domInputElement
473− let getMyValue = Input.getValue domInputElement
474− valueChangedByUI <- requestDomAction $ liftJSM getMyValue <$ Reflex.select (_element_events e) (WrapArg Input)
475− valueChangedBySetValue <- case _inputElementConfig_setValue cfg of
476− Nothing -> return never
477− Just eSetValue -> requestDomAction $ ffor eSetValue $ \v' -> do
478− Input.setValue domInputElement v'
479− getMyValue -- We get the value after setting it in case the browser has mucked with it somehow
480− v <- holdDyn v0 $ leftmost
481− [ valueChangedBySetValue
482− , valueChangedByUI
483− ]
484− Input.setChecked domInputElement $ _inputElementConfig_initialChecked cfg
485− checkedChangedByUI <- wrapDomEvent domInputElement (`on` Events.click) $ do
486− Input.getChecked domInputElement
487− checkedChangedBySetChecked <- case _inputElementConfig_setChecked cfg of
488− Nothing -> return never
489− Just eNewchecked -> requestDomAction $ ffor eNewchecked $ \newChecked -> do
490− oldChecked <- Input.getChecked domInputElement
491− Input.setChecked domInputElement newChecked
492− return $ if newChecked /= oldChecked
493− then Just newChecked
494− else Nothing
495− c <- holdDyn (_inputElementConfig_initialChecked cfg) $ leftmost
496− [ fmapMaybe id checkedChangedBySetChecked
497− , checkedChangedByUI
498− ]
499− let initialFocus = False --TODO: Is this correct?
500− hasFocus <- holdDyn initialFocus $ leftmost
501− [ False <$ Reflex.select (_element_events e) (WrapArg Blur)
502− , True <$ Reflex.select (_element_events e) (WrapArg Focus)
503− ]
504− files <- holdDyn mempty <=< wrapDomEvent domInputElement (`on` Events.change) $ do
505− mfiles <- Input.getFiles domInputElement
506− let getMyFiles xs = fmap catMaybes . mapM (FileList.item xs) . flip take [0..] . fromIntegral =<< FileList.getLength xs
507− maybe (return []) getMyFiles mfiles
508− checked <- holdUniqDyn c
509− return $ InputElement
510− { _inputElement_value = v
511− , _inputElement_checked = checked
512− , _inputElement_checkedChange = checkedChangedByUI
513− , _inputElement_input = valueChangedByUI
514− , _inputElement_hasFocus = hasFocus
515− , _inputElement_element = e
516− , _inputElement_raw = domInputElement
517− , _inputElement_files = files
518− }
1388+ inputElement = inputElementImmediate
5191389 {-# INLINABLE textAreaElement #-}
520− textAreaElement cfg = do --TODO
521− ((e, _), domElement) <- makeElement "textarea" (cfg ^. textAreaElementConfig_elementConfig) $ return ()
522− let domTextAreaElement = uncheckedCastTo DOM.HTMLTextAreaElement domElement
523− TextArea.setValue domTextAreaElement $ cfg ^. textAreaElementConfig_initialValue
524− v0 <- TextArea.getValue domTextAreaElement
525− let getMyValue = TextArea.getValue domTextAreaElement
526− valueChangedByUI <- requestDomAction $ liftJSM getMyValue <$ Reflex.select (_element_events e) (WrapArg Input)
527− valueChangedBySetValue <- case _textAreaElementConfig_setValue cfg of
528− Nothing -> return never
529− Just eSetValue -> requestDomAction $ ffor eSetValue $ \v' -> do
530− TextArea.setValue domTextAreaElement v'
531− getMyValue -- We get the value after setting it in case the browser has mucked with it somehow
532− v <- holdDyn v0 $ leftmost
533− [ valueChangedBySetValue
534− , valueChangedByUI
535− ]
536− hasFocus <- mkHasFocus e
537− return $ TextAreaElement
538− { _textAreaElement_value = v
539− , _textAreaElement_input = valueChangedByUI
540− , _textAreaElement_hasFocus = hasFocus
541− , _textAreaElement_element = e
542− , _textAreaElement_raw = domTextAreaElement
543− }
1390+ textAreaElement = textAreaElementImmediate
5441391 {-# INLINABLE selectElement #-}
545− selectElement cfg child = do
546− ((e, result), domElement) <- makeElement "select" (cfg ^. selectElementConfig_elementConfig) child
547− let domSelectElement = uncheckedCastTo DOM.HTMLSelectElement domElement
548− Select.setValue domSelectElement $ cfg ^. selectElementConfig_initialValue
549− v0 <- Select.getValue domSelectElement
550− let getMyValue = Select.getValue domSelectElement
551− valueChangedByUI <- requestDomAction $ liftJSM getMyValue <$ Reflex.select (_element_events e) (WrapArg Change)
552− valueChangedBySetValue <- case _selectElementConfig_setValue cfg of
553− Nothing -> return never
554− Just eSetValue -> requestDomAction $ ffor eSetValue $ \v' -> do
555− Select.setValue domSelectElement v'
556− getMyValue -- We get the value after setting it in case the browser has mucked with it somehow
557− v <- holdDyn v0 $ leftmost
558− [ valueChangedBySetValue
559− , valueChangedByUI
560− ]
561− hasFocus <- mkHasFocus e
562− let wrapped = SelectElement
563− { _selectElement_value = v
564− , _selectElement_change = valueChangedByUI
565− , _selectElement_hasFocus = hasFocus
566− , _selectElement_element = e
567− , _selectElement_raw = domSelectElement
568− }
569− return (wrapped, result)
1392+ selectElement = selectElementImmediate
5701393 placeRawElement = append . toNode
571− wrapRawElement = wrap
1394+ wrapRawElement e rawCfg = do
1395+ events <- askEvents
1396+ ctx <- askJSM
1397+ eventTriggerRefs <- wrap events e rawCfg
1398+ es <- newFanEventWithTrigger $ triggerBody ctx rawCfg events eventTriggerRefs e
1399+ pure $ Element es e
5721400
5731401data FragmentState
5741402 = FragmentState_Unmounted
@@ -588,21 +1416,21 @@ extractFragment fragment = do
5881416 extractBetweenExclusive (_immediateDomFragment_document fragment) before after
5891417 liftIO $ writeIORef (_immediateDomFragment_state fragment) FragmentState_Unmounted
5901418
591−instance SupportsImmediateDomBuilder t m => MountableDomBuilder t (ImmediateDomBuilderT t m) where
592− type DomFragment (ImmediateDomBuilderT t m) = ImmediateDomFragment
1419+instance SupportsHydrationDomBuilder t m => MountableDomBuilder t (HydrationDomBuilderT GhcjsDomSpace t m) where
1420+ type DomFragment (HydrationDomBuilderT GhcjsDomSpace t m) = ImmediateDomFragment
5931421 buildDomFragment w = do
5941422 df <- createDocumentFragment =<< askDocument
5951423 result <- flip localEnv w $ \env -> env
596− { _immediateDomBuilderEnv_parent = toNode df
1424+ { _hydrationDomBuilderEnv_parent = Left $ toNode df
5971425 }
5981426 state <- liftIO $ newIORef FragmentState_Unmounted
5991427 return (ImmediateDomFragment df state, result)
6001428 mountDomFragment fragment setFragment = do
601− parent <- askParent
1429+ parent <- getParent
6021430 extractFragment fragment
603− before <- textNodeInternal ("" :: Text)
1431+ before <- textNodeImmediate $ TextNodeConfig ("" :: Text) Nothing
6041432 appendChild_ parent $ _immediateDomFragment_document fragment
605− after <- textNodeInternal ("" :: Text)
1433+ after <- textNodeImmediate $ TextNodeConfig ("" :: Text) Nothing
6061434 xs <- foldDyn (\new (previous, _) -> (new, Just previous)) (fragment, Nothing) setFragment
6071435 requestDomAction_ $ ffor (updated xs) $ \(childFragment, Just previousFragment) -> do
6081436 extractFragment previousFragment
@@ -611,12 +1439,19 @@ instance SupportsImmediateDomBuilder t m => MountableDomBuilder t (ImmediateDomB
6111439 liftIO $ writeIORef (_immediateDomFragment_state childFragment) $ FragmentState_Mounted (before, after)
6121440 liftIO $ writeIORef (_immediateDomFragment_state fragment) $ FragmentState_Mounted (before, after)
6131441
614−instance (Reflex t, Adjustable t m, MonadJSM m, MonadHold t m, MonadFix m, PrimMonad m) => Adjustable t (ImmediateDomBuilderT t m) where
1442+instance (Reflex t, Monad m, Adjustable t m, MonadHold t m, MonadFix m) => Adjustable t (DomRenderHookT t m) where
1443+ runWithReplace a0 a' = DomRenderHookT $ runWithReplace (unDomRenderHookT a0) (fmapCheap unDomRenderHookT a')
1444+ traverseIntMapWithKeyWithAdjust f m = DomRenderHookT . traverseIntMapWithKeyWithAdjust (\k -> unDomRenderHookT . f k) m
1445+ traverseDMapWithKeyWithAdjust f m = DomRenderHookT . traverseDMapWithKeyWithAdjust (\k -> unDomRenderHookT . f k) m
1446+ traverseDMapWithKeyWithAdjustWithMove f m = DomRenderHookT . traverseDMapWithKeyWithAdjustWithMove (\k -> unDomRenderHookT . f k) m
1447+
1448+instance (Adjustable t m, MonadJSM m, MonadHold t m, MonadFix m, PrimMonad m, RawDocument (DomBuilderSpace (HydrationDomBuilderT s t m)) ~ Document) => Adjustable t (HydrationDomBuilderT s t m) where
6151449 {-# INLINABLE runWithReplace #-}
6161450 runWithReplace a0 a' = do
617− initialEnv <- ImmediateDomBuilderT ask
618− before <- textNodeInternal ("" :: Text)
619− let parentUnreadyChildren = _immediateDomBuilderEnv_unreadyChildren initialEnv
1451+ initialEnv <- HydrationDomBuilderT ask
1452+ let hydrating = _hydrationDomBuilderEnv_hydrationMode initialEnv
1453+ (hydrateStart, before, beforeKey) <- skipToReplaceStart
1454+ let parentUnreadyChildren = _hydrationDomBuilderEnv_unreadyChildren initialEnv
6201455 haveEverBeenReady <- liftIO $ newIORef False
6211456 currentCohort <- liftIO $ newIORef (-1 :: Int) -- Equal to the cohort currently in the DOM
6221457 let myCommitAction = do
@@ -627,79 +1462,117 @@ instance (Reflex t, Adjustable t m, MonadJSM m, MonadHold t m, MonadFix m, PrimM
6271462 old <- liftIO $ readIORef parentUnreadyChildren
6281463 let new = pred old
6291464 liftIO $ writeIORef parentUnreadyChildren $! new
630− when (new == 0) $ _immediateDomBuilderEnv_commitAction initialEnv
1465+ when (new == 0) $ _hydrationDomBuilderEnv_commitAction initialEnv
6311466 -- We draw 'after' in this roundabout way to avoid using MonadFix
6321467 doc <- askDocument
633− after <- createTextNode doc ("" :: Text)
1468+ parent <- getParent
1469+ (hydrateEnd, after) <- skipToReplaceEnd beforeKey
6341470 let drawInitialChild = do
1471+ h <- liftIO $ readIORef hydrating
1472+ p' <- case h of
1473+ HydrationMode_Hydrating -> pure parent
1474+ HydrationMode_Immediate -> toNode <$> createDocumentFragment doc
6351475 unreadyChildren <- liftIO $ newIORef 0
636− let f = do
637− result <- a0
638− append $ toNode after
639− return result
640− result <- runReaderT (unImmediateDomBuilderT f) $ initialEnv
641− { _immediateDomBuilderEnv_unreadyChildren = unreadyChildren
642− , _immediateDomBuilderEnv_commitAction = myCommitAction
1476+ let a0' = case h of
1477+ HydrationMode_Hydrating -> a0
1478+ HydrationMode_Immediate -> do
1479+ a <- a0
1480+ insertBefore p' =<< liftIO (readIORef after)
1481+ pure a
1482+ delayed <- case h of
1483+ HydrationMode_Hydrating -> liftIO $ newIORef $ pure ()
1484+ HydrationMode_Immediate -> pure $ _hydrationDomBuilderEnv_delayed initialEnv
1485+ result <- runReaderT (unHydrationDomBuilderT a0') initialEnv
1486+ { _hydrationDomBuilderEnv_unreadyChildren = unreadyChildren
1487+ , _hydrationDomBuilderEnv_commitAction = myCommitAction
1488+ , _hydrationDomBuilderEnv_parent = Left p'
1489+ , _hydrationDomBuilderEnv_delayed = delayed
6431490 }
1491+ dom <- case h of
1492+ HydrationMode_Hydrating -> liftIO $ readIORef delayed
1493+ HydrationMode_Immediate -> pure $ pure ()
6441494 liftIO $ readIORef unreadyChildren >>= \case
6451495 0 -> writeIORef haveEverBeenReady True
6461496 _ -> modifyIORef' parentUnreadyChildren succ
647− return result
1497+ return (dom, result)
6481498 a'' <- numberOccurrences a'
649− (result0, child') <- ImmediateDomBuilderT $ lift $ runWithReplace drawInitialChild $ ffor a'' $ \(cohortId, child) -> do
650− df <- createDocumentFragment doc
1499+ ((hydrate0, result0), child') <- HydrationDomBuilderT $ lift $ runWithReplace drawInitialChild $ ffor a'' $ \(cohortId, child) -> do
1500+ h <- liftIO $ readIORef hydrating
1501+ p' <- case h of
1502+ HydrationMode_Hydrating -> pure parent
1503+ HydrationMode_Immediate -> toNode <$> createDocumentFragment doc
6511504 unreadyChildren <- liftIO $ newIORef 0
6521505 let commitAction = do
6531506 c <- liftIO $ readIORef currentCohort
6541507 when (c <= cohortId) $ do -- If a newer cohort has already been committed, just ignore this
655− deleteBetweenExclusive before after
656− insertBefore df after
1508+ !before' <- liftIO $ readIORef before
1509+ !after' <- liftIO $ readIORef after
1510+ deleteBetweenExclusive before' after'
1511+ insertBefore p' after'
6571512 liftIO $ writeIORef currentCohort cohortId
6581513 myCommitAction
659− result <- runReaderT (unImmediateDomBuilderT child) $ initialEnv
660− { _immediateDomBuilderEnv_parent = toNode df
661− , _immediateDomBuilderEnv_unreadyChildren = unreadyChildren
662− , _immediateDomBuilderEnv_commitAction = commitAction
663− }
1514+ delayed <- case h of
1515+ HydrationMode_Hydrating -> liftIO $ newIORef $ pure ()
1516+ HydrationMode_Immediate -> pure $ _hydrationDomBuilderEnv_delayed initialEnv
1517+ result <- runReaderT (unHydrationDomBuilderT child) $ initialEnv
1518+ { _hydrationDomBuilderEnv_unreadyChildren = unreadyChildren
1519+ , _hydrationDomBuilderEnv_commitAction = case h of
1520+ HydrationMode_Hydrating -> myCommitAction
1521+ HydrationMode_Immediate -> commitAction
1522+ , _hydrationDomBuilderEnv_parent = Left p'
1523+ , _hydrationDomBuilderEnv_delayed = delayed
1524+ }
1525+ dom <- case h of
1526+ HydrationMode_Hydrating -> liftIO $ readIORef delayed
1527+ HydrationMode_Immediate -> pure $ pure ()
6641528 uc <- liftIO $ readIORef unreadyChildren
6651529 let commitActionToRunNow = if uc == 0
666− then Just commitAction
1530+ then Just $ commitAction
6671531 else Nothing -- A child will run it when unreadyChildren is decremented to 0
668− return (commitActionToRunNow, result)
669− requestDomAction_ $ fmapMaybe fst child'
1532+ actions = case h of
1533+ HydrationMode_Hydrating -> Left dom
1534+ HydrationMode_Immediate -> Right commitActionToRunNow
1535+ return (actions, result)
1536+ let (hydrate', commitAction) = fanEither $ fmap fst child'
1537+ addHydrationStepWithSetup (hold hydrate0 hydrate') $ \contents -> do
1538+ hydrateStart
1539+ join $ sample contents
1540+ hydrateEnd
1541+ requestDomAction_ $ fmapMaybe id commitAction
6701542 return (result0, snd <$> child')
1543+
6711544 {-# INLINABLE traverseIntMapWithKeyWithAdjust #-}
6721545 traverseIntMapWithKeyWithAdjust = traverseIntMapWithKeyWithAdjust'
6731546 {-# INLINABLE traverseDMapWithKeyWithAdjust #-}
6741547 traverseDMapWithKeyWithAdjust = traverseDMapWithKeyWithAdjust'
6751548 {-# INLINABLE traverseDMapWithKeyWithAdjustWithMove #-}
6761549 traverseDMapWithKeyWithAdjustWithMove = do
677− let updateChildUnreadiness (p :: PatchDMapWithMove k (Compose ((,,,) DOM.DocumentFragment DOM.Text (IORef (ChildReadyState k))) v')) old = do
678− let new :: forall a. k a -> PatchDMapWithMove.NodeInfo k (Compose ((,,,) DOM.DocumentFragment DOM.Text (IORef (ChildReadyState k))) v') a -> IO (PatchDMapWithMove.NodeInfo k (Constant (IORef (ChildReadyState k))) a)
1550+ let updateChildUnreadiness (p :: PatchDMapWithMove k (Compose (TraverseChild t m (Some k)) v')) old = do
1551+ let new :: forall a. k a -> PatchDMapWithMove.NodeInfo k (Compose (TraverseChild t m (Some k)) v') a -> IO (PatchDMapWithMove.NodeInfo k (Constant (IORef (ChildReadyState (Some k)))) a)
6791552 new k = PatchDMapWithMove.nodeInfoMapFromM $ \case
680− PatchDMapWithMove.From_Insert (Compose (_, _, sRef, _)) -> do
681− readIORef sRef >>= \case
1553+ PatchDMapWithMove.From_Insert (Compose (TraverseChild (Left _hydration) _)) -> return PatchDMapWithMove.From_Delete
1554+ PatchDMapWithMove.From_Insert (Compose (TraverseChild (Right immediate) _)) -> do
1555+ readIORef (_traverseChildImmediate_childReadyState immediate) >>= \case
6821556 ChildReadyState_Ready -> return PatchDMapWithMove.From_Delete
6831557 ChildReadyState_Unready _ -> do
684− writeIORef sRef $ ChildReadyState_Unready $ Just $ Some.This k
685− return $ PatchDMapWithMove.From_Insert $ Constant sRef
1558+ writeIORef (_traverseChildImmediate_childReadyState immediate) $ ChildReadyState_Unready $ Just $ This k
1559+ return $ PatchDMapWithMove.From_Insert $ Constant (_traverseChildImmediate_childReadyState immediate)
6861560 PatchDMapWithMove.From_Delete -> return PatchDMapWithMove.From_Delete
6871561 PatchDMapWithMove.From_Move fromKey -> return $ PatchDMapWithMove.From_Move fromKey
688− deleteOrMove :: forall a. k a -> Product (Constant (IORef (ChildReadyState k))) (ComposeMaybe k) a -> IO (Constant () a)
1562+ deleteOrMove :: forall a. k a -> Product (Constant (IORef (ChildReadyState (Some k)))) (ComposeMaybe k) a -> IO (Constant () a)
6891563 deleteOrMove _ (Pair (Constant sRef) (ComposeMaybe mToKey)) = do
690− writeIORef sRef $ ChildReadyState_Unready $ Some.This <$> mToKey -- This will be Nothing if deleting, and Just if moving, so it works out in both cases
1564+ writeIORef sRef $ ChildReadyState_Unready $ This <$> mToKey -- This will be Nothing if deleting, and Just if moving, so it works out in both cases
6911565 return $ Constant ()
6921566 p' <- fmap unsafePatchDMapWithMove $ DMap.traverseWithKey new $ unPatchDMapWithMove p
6931567 _ <- DMap.traverseWithKey deleteOrMove $ PatchDMapWithMove.getDeletionsAndMoves p old
6941568 return $ applyAlways p' old
695− hoistTraverseWithKeyWithAdjust traverseDMapWithKeyWithAdjustWithMove mapPatchDMapWithMove updateChildUnreadiness $ \placeholders lastPlaceholderRef (p_ :: PatchDMapWithMove k (Compose ((,,,) DOM.DocumentFragment DOM.Text (IORef (ChildReadyState k))) v')) -> do
1569+ hoistTraverseWithKeyWithAdjust traverseDMapWithKeyWithAdjustWithMove mapPatchDMapWithMove updateChildUnreadiness $ \placeholders lastPlaceholder (p_ :: PatchDMapWithMove k (Compose (TraverseChild t m (Some k)) v')) -> do
6961570 let p = unPatchDMapWithMove p_
6971571 phsBefore <- liftIO $ readIORef placeholders
698− lastPlaceholder <- liftIO $ readIORef lastPlaceholderRef
699− let collectIfMoved :: forall a. k a -> PatchDMapWithMove.NodeInfo k (Compose ((,,,) DOM.DocumentFragment DOM.Text (IORef (ChildReadyState k))) v') a -> JSM (Constant (Maybe DOM.DocumentFragment) a)
1572+ let collectIfMoved :: forall a. k a -> PatchDMapWithMove.NodeInfo k (Compose (TraverseChild t m (Some k)) v') a -> JSM (Constant (Maybe DOM.DocumentFragment) a)
7001573 collectIfMoved k e = do
701− let mThisPlaceholder = Map.lookup (Some.This k) phsBefore -- Will be Nothing if this element wasn't present before
702− nextPlaceholder = maybe lastPlaceholder snd $ Map.lookupGT (Some.This k) phsBefore
1574+ let mThisPlaceholder = Map.lookup (This k) phsBefore -- Will be Nothing if this element wasn't present before
1575+ nextPlaceholder = maybe lastPlaceholder snd $ Map.lookupGT (This k) phsBefore
7031576 case isJust $ getComposeMaybe $ PatchDMapWithMove._nodeInfo_to e of
7041577 False -> do
7051578 mapM_ (`deleteUpTo` nextPlaceholder) mThisPlaceholder
@@ -707,13 +1580,22 @@ instance (Reflex t, Adjustable t m, MonadJSM m, MonadHold t m, MonadFix m, PrimM
7071580 True -> do
7081581 Constant <$> mapM (`collectUpTo` nextPlaceholder) mThisPlaceholder
7091582 collected <- DMap.traverseWithKey collectIfMoved p
710− let !phsAfter = fromMaybe phsBefore $ apply (weakenPatchDMapWithMoveWith (\(Compose (_, ph, _, _)) -> ph) p_) phsBefore --TODO: Don't recompute this
711− let placeFragment :: forall a. k a -> PatchDMapWithMove.NodeInfo k (Compose ((,,,) DOM.DocumentFragment DOM.Text (IORef (ChildReadyState k))) v') a -> JSM (Constant () a)
1583+ let !phsAfter = fromMaybe phsBefore $ apply filtered phsBefore
1584+ weakened :: PatchMapWithMove (Some k) (Either (TraverseChildHydration t m) (TraverseChildImmediate (Some k)))
1585+ weakened = weakenPatchDMapWithMoveWith (_traverseChild_mode . getCompose) p_
1586+ filtered :: PatchMapWithMove (Some k) DOM.Text
1587+ filtered = PatchMapWithMove $ flip Map.mapMaybe (unPatchMapWithMove weakened) $ \(PatchMapWithMove.NodeInfo from to) -> flip PatchMapWithMove.NodeInfo to <$> case from of
1588+ PatchMapWithMove.From_Insert (Left _hydration) -> Nothing
1589+ PatchMapWithMove.From_Insert (Right immediate) -> Just $ PatchMapWithMove.From_Insert $ _traverseChildImmediate_placeholder immediate
1590+ PatchMapWithMove.From_Delete -> Just $ PatchMapWithMove.From_Delete
1591+ PatchMapWithMove.From_Move k -> Just $ PatchMapWithMove.From_Move k
1592+ let placeFragment :: forall a. k a -> PatchDMapWithMove.NodeInfo k (Compose (TraverseChild t m (Some k)) v') a -> JSM (Constant () a)
7121593 placeFragment k e = do
713− let nextPlaceholder = maybe lastPlaceholder snd $ Map.lookupGT (Some.This k) phsAfter
1594+ let nextPlaceholder = maybe lastPlaceholder snd $ Map.lookupGT (This k) phsAfter
7141595 case PatchDMapWithMove._nodeInfo_from e of
715− PatchDMapWithMove.From_Insert (Compose (df, _, _, _)) -> do
716− df `insertBefore` nextPlaceholder
1596+ PatchDMapWithMove.From_Insert (Compose (TraverseChild x _)) -> case x of
1597+ Left _ -> pure ()
1598+ Right immediate -> _traverseChildImmediate_fragment immediate `insertBefore` nextPlaceholder
7171599 PatchDMapWithMove.From_Delete -> do
7181600 return ()
7191601 PatchDMapWithMove.From_Move fromKey -> do
@@ -724,105 +1606,166 @@ instance (Reflex t, Adjustable t m, MonadJSM m, MonadHold t m, MonadFix m, PrimM
7241606 liftIO $ writeIORef placeholders $! phsAfter
7251607
7261608{-# INLINABLE traverseDMapWithKeyWithAdjust' #-}
727−traverseDMapWithKeyWithAdjust' :: forall t m (k :: * -> *) v v'. (Adjustable t m, MonadHold t m, MonadFix m, MonadIO m, MonadJSM m, PrimMonad m, DMap.GCompare k) => (forall a. k a -> v a -> ImmediateDomBuilderT t m (v' a)) -> DMap k v -> Event t (PatchDMap k v) -> ImmediateDomBuilderT t m (DMap k v', Event t (PatchDMap k v'))
1609+traverseDMapWithKeyWithAdjust'
1610+ :: forall s t m (k :: * -> *) v v'. (Adjustable t m, MonadHold t m, MonadFix m, MonadJSM m, PrimMonad m, DMap.GCompare k, RawDocument (DomBuilderSpace (HydrationDomBuilderT s t m)) ~ Document)
1611+ => (forall a. k a -> v a -> HydrationDomBuilderT s t m (v' a))
1612+ -> DMap k v
1613+ -> Event t (PatchDMap k v)
1614+ -> HydrationDomBuilderT s t m (DMap k v', Event t (PatchDMap k v'))
7281615traverseDMapWithKeyWithAdjust' = do
729− let updateChildUnreadiness (p :: PatchDMap k (Compose ((,,,) DOM.DocumentFragment DOM.Text (IORef (ChildReadyState k))) v')) old = do
730− let new :: forall a. k a -> ComposeMaybe (Compose ((,,,) DOM.DocumentFragment DOM.Text (IORef (ChildReadyState k))) v') a -> IO (ComposeMaybe (Constant (IORef (ChildReadyState k))) a)
1616+ let updateChildUnreadiness (p :: PatchDMap k (Compose (TraverseChild t m (Some k)) v')) old = do
1617+ let new :: forall a. k a -> ComposeMaybe (Compose (TraverseChild t m (Some k)) v') a -> IO (ComposeMaybe (Constant (IORef (ChildReadyState (Some k)))) a)
7311618 new k (ComposeMaybe m) = ComposeMaybe <$> case m of
7321619 Nothing -> return Nothing
733− Just (Compose (_, _, sRef, _)) -> do
734− readIORef sRef >>= \case
1620+ Just (Compose (TraverseChild (Left _hydration) _)) -> pure Nothing
1621+ Just (Compose (TraverseChild (Right immediate) _)) -> do
1622+ readIORef (_traverseChildImmediate_childReadyState immediate) >>= \case
7351623 ChildReadyState_Ready -> return Nothing -- Delete this child, since it's ready
7361624 ChildReadyState_Unready _ -> do
737− writeIORef sRef $ ChildReadyState_Unready $ Just $ Some.This k
738− return $ Just $ Constant sRef
1625+ writeIORef (_traverseChildImmediate_childReadyState immediate) $ ChildReadyState_Unready $ Just $ This k
1626+ return $ Just $ Constant (_traverseChildImmediate_childReadyState immediate)
7391627 delete _ (Constant sRef) = do
7401628 writeIORef sRef $ ChildReadyState_Unready Nothing
7411629 return $ Constant ()
7421630 p' <- fmap PatchDMap $ DMap.traverseWithKey new $ unPatchDMap p
7431631 _ <- DMap.traverseWithKey delete $ PatchDMap.getDeletions p old
7441632 return $ applyAlways p' old
745− hoistTraverseWithKeyWithAdjust traverseDMapWithKeyWithAdjust mapPatchDMap updateChildUnreadiness $ \placeholders lastPlaceholderRef (PatchDMap p) -> do
1633+ hoistTraverseWithKeyWithAdjust traverseDMapWithKeyWithAdjust mapPatchDMap updateChildUnreadiness $ \placeholders lastPlaceholder (PatchDMap patch) -> do
7461634 phs <- liftIO $ readIORef placeholders
747− forM_ (DMap.toList p) $ \(k :=> ComposeMaybe mv) -> do
748− lastPlaceholder <- liftIO $ readIORef lastPlaceholderRef
749− let nextPlaceholder = maybe lastPlaceholder snd $ Map.lookupGT (Some.This k) phs
750− forM_ (Map.lookup (Some.This k) phs) $ \thisPlaceholder -> thisPlaceholder `deleteUpTo` nextPlaceholder
751− forM_ mv $ \(Compose (df, _, _, _)) -> df `insertBefore` nextPlaceholder
752− liftIO $ writeIORef placeholders $! fromMaybe phs $ apply (weakenPatchDMapWith (\(Compose (_, ph, _, _)) -> ph) $ PatchDMap p) phs
1635+ forM_ (DMap.toList patch) $ \(k :=> ComposeMaybe mv) -> do
1636+ let nextPlaceholder = maybe lastPlaceholder snd $ Map.lookupGT (This k) phs
1637+ -- Delete old node
1638+ forM_ (Map.lookup (This k) phs) $ \thisPlaceholder -> do
1639+ thisPlaceholder `deleteUpTo` nextPlaceholder
1640+ -- Insert new node
1641+ forM_ mv $ \(Compose (TraverseChild e _)) -> case e of
1642+ Left _hydration -> pure ()
1643+ Right immediate -> do
1644+ _traverseChildImmediate_fragment immediate `insertBefore` nextPlaceholder
1645+ let weakened :: PatchMap (Some k) (Either (TraverseChildHydration t m) (TraverseChildImmediate (Some k)))
1646+ weakened = weakenPatchDMapWith (_traverseChild_mode . getCompose) $ PatchDMap patch
1647+ filtered :: PatchMap (Some k) DOM.Text
1648+ filtered = PatchMap $ flip Map.mapMaybe (unPatchMap weakened) $ \case
1649+ Nothing -> Just Nothing -- deletion
1650+ Just (Left _) -> Nothing
1651+ Just (Right immediate) -> Just $ Just $ _traverseChildImmediate_placeholder immediate
1652+ liftIO $ writeIORef placeholders $! fromMaybe phs $ apply filtered phs
7531653
7541654{-# INLINABLE traverseIntMapWithKeyWithAdjust' #-}
755−traverseIntMapWithKeyWithAdjust' :: forall t m v v'. (Adjustable t m, MonadHold t m, MonadFix m, MonadIO m, MonadJSM m, PrimMonad m) => (IntMap.Key -> v -> ImmediateDomBuilderT t m v') -> IntMap v -> Event t (PatchIntMap v) -> ImmediateDomBuilderT t m (IntMap v', Event t (PatchIntMap v'))
1655+traverseIntMapWithKeyWithAdjust'
1656+ :: forall s t m v v'. (Adjustable t m, MonadJSM m, MonadFix m, PrimMonad m, MonadHold t m, RawDocument (DomBuilderSpace (HydrationDomBuilderT s t m)) ~ Document)
1657+ => (IntMap.Key -> v -> HydrationDomBuilderT s t m v')
1658+ -> IntMap v
1659+ -> Event t (PatchIntMap v)
1660+ -> HydrationDomBuilderT s t m (IntMap v', Event t (PatchIntMap v'))
7561661traverseIntMapWithKeyWithAdjust' = do
757− let updateChildUnreadiness (p@(PatchIntMap pInner) :: PatchIntMap (DOM.DocumentFragment, DOM.Text, IORef ChildReadyStateInt, v')) old = do
758− let new :: IntMap.Key -> Maybe (DOM.DocumentFragment, DOM.Text, IORef ChildReadyStateInt, v') -> IO (Maybe (IORef ChildReadyStateInt))
1662+ let updateChildUnreadiness (p@(PatchIntMap pInner) :: PatchIntMap (TraverseChild t m Int v')) old = do
1663+ let new :: IntMap.Key -> Maybe (TraverseChild t m Int v') -> IO (Maybe (IORef (ChildReadyState Int)))
7591664 new k m = case m of
7601665 Nothing -> return Nothing
761− Just (_, _, sRef, _) -> do
1666+ Just (TraverseChild (Left _hydration) _) -> pure Nothing
1667+ Just (TraverseChild (Right immediate) _) -> do
1668+ let sRef = _traverseChildImmediate_childReadyState immediate
7621669 readIORef sRef >>= \case
763− ChildReadyStateInt_Ready -> return Nothing -- Delete this child, since it's ready
764− ChildReadyStateInt_Unready _ -> do
765− writeIORef sRef $ ChildReadyStateInt_Unready $ Just k
1670+ ChildReadyState_Ready -> return Nothing -- Delete this child, since it's ready
1671+ ChildReadyState_Unready _ -> do
1672+ writeIORef sRef $ ChildReadyState_Unready $ Just k
7661673 return $ Just sRef
7671674 delete _ sRef = do
768− writeIORef sRef $ ChildReadyStateInt_Unready Nothing
1675+ writeIORef sRef $ ChildReadyState_Unready Nothing
7691676 return ()
7701677 p' <- PatchIntMap <$> IntMap.traverseWithKey new pInner
7711678 _ <- IntMap.traverseWithKey delete $ FastMutableIntMap.getDeletions p old
7721679 return $ applyAlways p' old
773− hoistTraverseIntMapWithKeyWithAdjust traverseIntMapWithKeyWithAdjust updateChildUnreadiness $ \placeholders lastPlaceholderRef (PatchIntMap p) -> do
1680+ hoistTraverseIntMapWithKeyWithAdjust traverseIntMapWithKeyWithAdjust updateChildUnreadiness $ \placeholders lastPlaceholder (PatchIntMap p) -> do
7741681 phs <- liftIO $ readIORef placeholders
7751682 forM_ (IntMap.toList p) $ \(k, mv) -> do
776− lastPlaceholder <- liftIO $ readIORef lastPlaceholderRef
7771683 let nextPlaceholder = maybe lastPlaceholder snd $ IntMap.lookupGT k phs
1684+ -- Delete old node
7781685 forM_ (IntMap.lookup k phs) $ \thisPlaceholder -> thisPlaceholder `deleteUpTo` nextPlaceholder
779− forM_ mv $ \(df, _, _, _) -> df `insertBefore` nextPlaceholder
780− liftIO $ writeIORef placeholders $! fromMaybe phs $ apply ((\(_, ph, _, _) -> ph) <$> PatchIntMap p) phs
1686+ -- Insert new node
1687+ forM_ mv $ \(TraverseChild e _) -> case e of
1688+ Left _hydration -> pure ()
1689+ Right immediate -> do
1690+ _traverseChildImmediate_fragment immediate `insertBefore` nextPlaceholder
1691+ let filtered :: PatchIntMap DOM.Text
1692+ filtered = PatchIntMap $ flip IntMap.mapMaybe p $ \case
1693+ Nothing -> Just Nothing -- deletion
1694+ Just tc
1695+ | Right immediate <- _traverseChild_mode tc -> Just $ Just $ _traverseChildImmediate_placeholder immediate
1696+ | otherwise -> Nothing
1697+ liftIO $ writeIORef placeholders $! fromMaybe phs $ apply filtered phs
1698+
1699+{-# SPECIALIZE traverseIntMapWithKeyWithAdjust'
1700+ :: (IntMap.Key -> v -> HydrationDomBuilderT GhcjsDomSpace DomTimeline HydrationM v')
1701+ -> IntMap v
1702+ -> Event DomTimeline (PatchIntMap v)
1703+ -> HydrationDomBuilderT GhcjsDomSpace DomTimeline HydrationM (IntMap v', Event DomTimeline (PatchIntMap v'))
1704+ #-}
7811705
782−#if MIN_VERSION_base(4,9,0)
783−data ChildReadyState k
784−#else
785−data ChildReadyState (k :: * -> *)
786−#endif
1706+{-# SPECIALIZE traverseIntMapWithKeyWithAdjust'
1707+ :: (IntMap.Key -> v -> HydrationDomBuilderT HydrationDomSpace DomTimeline HydrationM v')
1708+ -> IntMap v
1709+ -> Event DomTimeline (PatchIntMap v)
1710+ -> HydrationDomBuilderT HydrationDomSpace DomTimeline HydrationM (IntMap v', Event DomTimeline (PatchIntMap v'))
1711+ #-}
1712+
1713+data ChildReadyState a
7871714 = ChildReadyState_Ready
788− | ChildReadyState_Unready !(Maybe (Some k))
1715+ | ChildReadyState_Unready !(Maybe a)
7891716 deriving (Show, Read, Eq, Ord)
7901717
791−data ChildReadyStateInt
792− = ChildReadyStateInt_Ready
793− | ChildReadyStateInt_Unready !(Maybe Int)
794− deriving (Show, Read, Eq, Ord)
1718+insertAfterPreviousNode :: (Monad m, MonadJSM m) => DOM.IsNode node => node -> HydrationRunnerT t m ()
1719+insertAfterPreviousNode node = do
1720+ parent <- askParent
1721+ nextNode <- maybe (Node.getFirstChild parent) Node.getNextSibling =<< getPreviousNode
1722+ Node.insertBefore_ parent node nextNode
1723+ setPreviousNode $ Just $ toNode node
7951724
796−{-# INLINE hoistTraverseIntMapWithKeyWithAdjust #-}
797−hoistTraverseIntMapWithKeyWithAdjust :: forall v v' t m p.
1725+{-# INLINABLE hoistTraverseWithKeyWithAdjust #-}
1726+hoistTraverseWithKeyWithAdjust
1727+ ::
7981728 ( Adjustable t m
1729+ , MonadHold t m
1730+ , DMap.GCompare k
7991731 , MonadIO m
8001732 , MonadJSM m
801− , MonadFix m
8021733 , PrimMonad m
803− , Monoid (p (DOM.DocumentFragment, DOM.Text, IORef ChildReadyStateInt, v'))
804− , Functor p
1734+ , MonadFix m
1735+ , Patch (p k v)
1736+ , Patch (p k (Constant Int))
1737+ , PatchTarget (p k (Constant Int)) ~ DMap k (Constant Int)
1738+ , Patch (p k (Compose (TraverseChild t m (Some k)) v'))
1739+ , PatchTarget (p k (Compose (TraverseChild t m (Some k)) v')) ~ DMap k (Compose (TraverseChild t m (Some k)) v')
1740+ , Monoid (p k (Compose (TraverseChild t m (Some k)) v'))
1741+ , RawDocument (DomBuilderSpace (HydrationDomBuilderT s t m)) ~ Document
8051742 )
806− => ( (IntMap.Key -> v -> RequesterT t JSM Identity (TriggerEventT t m) (DOM.DocumentFragment, DOM.Text, IORef ChildReadyStateInt, v'))
807− -> IntMap v
808− -> Event t (p v)
809− -> RequesterT t JSM Identity (TriggerEventT t m) (IntMap (DOM.DocumentFragment, DOM.Text, IORef ChildReadyStateInt, v'), Event t (p (DOM.DocumentFragment, DOM.Text, IORef ChildReadyStateInt, v')))
810− ) -- ^ The base monad's traversal
811− -> (p (DOM.DocumentFragment, DOM.Text, IORef ChildReadyStateInt, v') -> IntMap (IORef ChildReadyStateInt) -> IO (IntMap (IORef ChildReadyStateInt))) -- ^ Given a patch for the children DOM elements, produce a patch for the childrens' unreadiness state
812− -> (IORef (IntMap DOM.Text) -> IORef DOM.Text -> p (DOM.DocumentFragment, DOM.Text, IORef ChildReadyStateInt, v') -> JSM ()) -- ^ Apply a patch to the DOM
813− -> (IntMap.Key -> v -> ImmediateDomBuilderT t m v')
814− -> IntMap v
815− -> Event t (p v)
816− -> ImmediateDomBuilderT t m (IntMap v', Event t (p v'))
817−hoistTraverseIntMapWithKeyWithAdjust base updateChildUnreadiness applyDomUpdate_ f dm0 dm' = do
818− initialEnv <- ImmediateDomBuilderT ask
819− let parentUnreadyChildren = _immediateDomBuilderEnv_unreadyChildren initialEnv
820− pendingChange :: IORef (IntMap (IORef ChildReadyStateInt), p (DOM.DocumentFragment, DOM.Text, IORef ChildReadyStateInt, v')) <- liftIO $ newIORef mempty
1743+ => (forall vv vv'.
1744+ (forall a. k a -> vv a -> DomRenderHookT t m (vv' a))
1745+ -> DMap k vv
1746+ -> Event t (p k vv)
1747+ -> DomRenderHookT t m (DMap k vv', Event t (p k vv')))
1748+ -- ^ The base monad's traversal
1749+ -> (forall vv vv'. (forall a. vv a -> vv' a) -> p k vv -> p k vv')
1750+ -- ^ A way of mapping over the patch type
1751+ -> (p k (Compose (TraverseChild t m (Some k)) v') -> DMap k (Constant (IORef (ChildReadyState (Some k)))) -> IO (DMap k (Constant (IORef (ChildReadyState (Some k))))))
1752+ -- ^ Given a patch for the children DOM elements, produce a patch for the childrens' unreadiness state
1753+ -> (IORef (Map.Map (Some k) DOM.Text) -> DOM.Text -> p k (Compose (TraverseChild t m (Some k)) v') -> JSM ())
1754+ -- ^ Apply a patch to the DOM
1755+ -> (forall a. k a -> v a -> HydrationDomBuilderT s t m (v' a))
1756+ -> DMap k v
1757+ -> Event t (p k v)
1758+ -> HydrationDomBuilderT s t m (DMap k v', Event t (p k v'))
1759+hoistTraverseWithKeyWithAdjust base mapPatch updateChildUnreadiness applyDomUpdate_ f dm0 dm' = do
1760+ doc <- askDocument
1761+ initialEnv <- HydrationDomBuilderT ask
1762+ let parentUnreadyChildren = _hydrationDomBuilderEnv_unreadyChildren initialEnv
1763+ pendingChange :: IORef (DMap k (Constant (IORef (ChildReadyState (Some k)))), p k (Compose (TraverseChild t m (Some k)) v')) <- liftIO $ newIORef mempty
8211764 haveEverBeenReady <- liftIO $ newIORef False
822− placeholders <- liftIO $ newIORef $ error "placeholders not yet initialized"
823− lastPlaceholderRef <- liftIO $ newIORef $ error "lastPlaceholderRef not yet initialized"
1765+ placeholders <- liftIO $ newIORef Map.empty
1766+ lastPlaceholder <- createTextNode doc ("" :: Text)
8241767 let applyDomUpdate p = do
825− applyDomUpdate_ placeholders lastPlaceholderRef p
1768+ applyDomUpdate_ placeholders lastPlaceholder p
8261769 markSelfReady
8271770 liftIO $ writeIORef pendingChange $! mempty
8281771 markSelfReady = do
@@ -833,86 +1776,105 @@ hoistTraverseIntMapWithKeyWithAdjust base updateChildUnreadiness applyDomUpdate_
8331776 old <- liftIO $ readIORef parentUnreadyChildren
8341777 let new = pred old
8351778 liftIO $ writeIORef parentUnreadyChildren $! new
836− when (new == 0) $ _immediateDomBuilderEnv_commitAction initialEnv
837− markChildReady :: IORef ChildReadyStateInt -> JSM ()
1779+ when (new == 0) $ _hydrationDomBuilderEnv_commitAction initialEnv
1780+ markChildReady :: IORef (ChildReadyState (Some k)) -> JSM ()
8381781 markChildReady childReadyState = do
8391782 liftIO (readIORef childReadyState) >>= \case
840− ChildReadyStateInt_Ready -> return ()
841− ChildReadyStateInt_Unready countedAt -> do
842− liftIO $ writeIORef childReadyState ChildReadyStateInt_Ready
1783+ ChildReadyState_Ready -> return ()
1784+ ChildReadyState_Unready countedAt -> do
1785+ liftIO $ writeIORef childReadyState ChildReadyState_Ready
8431786 case countedAt of
8441787 Nothing -> return ()
845− Just k -> do -- This child has been counted as unready, so we need to remove it from the unready set
1788+ Just (This k) -> do -- This child has been counted as unready, so we need to remove it from the unready set
8461789 (oldUnready, p) <- liftIO $ readIORef pendingChange
847− when (not $ IntMap.null oldUnready) $ do -- This shouldn't actually ever be null
848− let newUnready = IntMap.delete k oldUnready
1790+ when (not $ DMap.null oldUnready) $ do -- This shouldn't actually ever be null
1791+ let newUnready = DMap.delete k oldUnready
8491792 liftIO $ writeIORef pendingChange (newUnready, p)
850− when (IntMap.null newUnready) $ do
1793+ when (DMap.null newUnready) $ do
8511794 applyDomUpdate p
852− (children0, children') <- ImmediateDomBuilderT $ lift $ base (\k v -> drawChildUpdateInt initialEnv markChildReady $ f k v) dm0 dm'
853− let processChild k (_, _, sRef, _) = do
854− readIORef sRef >>= \case
855− ChildReadyStateInt_Ready -> return Nothing
856− ChildReadyStateInt_Unready _ -> do
857− writeIORef sRef $ ChildReadyStateInt_Unready $ Just k
858− return $ Just sRef
859− initialUnready <- liftIO $ IntMap.mapMaybe id <$> IntMap.traverseWithKey processChild children0
860− liftIO $ if IntMap.null initialUnready
1795+ (children0 :: DMap k (Compose (TraverseChild t m (Some k)) v'), children' :: Event t (p k (Compose (TraverseChild t m (Some k)) v')))
1796+ <- HydrationDomBuilderT $ lift $ base (\k v -> drawChildUpdate initialEnv markChildReady $ f k v) dm0 dm'
1797+ let processChild k (Compose (TraverseChild e _)) = case e of
1798+ Left _ -> pure $ ComposeMaybe Nothing
1799+ Right immediate -> ComposeMaybe <$> do
1800+ readIORef (_traverseChildImmediate_childReadyState immediate) >>= \case
1801+ ChildReadyState_Ready -> return Nothing
1802+ ChildReadyState_Unready _ -> do
1803+ writeIORef (_traverseChildImmediate_childReadyState immediate) $ ChildReadyState_Unready $ Just $ This k
1804+ return $ Just $ Constant (_traverseChildImmediate_childReadyState immediate)
1805+ initialUnready <- liftIO $ DMap.mapMaybeWithKey (\_ -> getComposeMaybe) <$> DMap.traverseWithKey processChild children0
1806+ liftIO $ if DMap.null initialUnready
8611807 then writeIORef haveEverBeenReady True
8621808 else do
8631809 modifyIORef' parentUnreadyChildren succ
8641810 writeIORef pendingChange (initialUnready, mempty) -- The patch is always empty because it got applied implicitly when we ran the children the first time
865− let result0 = IntMap.map (\(_, _, _, v) -> v) children0
866− placeholders0 = fmap (\(_, ph, _, _) -> ph) children0
867− result' = ffor children' $ fmap $ \(_, _, _, r) -> r
868− liftIO $ writeIORef placeholders $! placeholders0
869− _ <- IntMap.traverseWithKey (\_ (df, _, _, _) -> void $ append $ toNode df) children0
870− liftIO . writeIORef lastPlaceholderRef =<< textNodeInternal ("" :: Text)
1811+ getHydrationMode >>= \case
1812+ HydrationMode_Hydrating -> addHydrationStepWithSetup (holdIncremental children0 children') $ \children -> do
1813+ dm :: DMap k (Compose (TraverseChild t m (Some k)) v') <- sample $ currentIncremental children
1814+ phs <- traverse id $ weakenDMapWith (either _traverseChildHydration_delayed (pure . _traverseChildImmediate_placeholder) . _traverseChild_mode . getCompose) dm
1815+ liftIO $ writeIORef placeholders $! phs
1816+ insertAfterPreviousNode lastPlaceholder
1817+ HydrationMode_Immediate -> do
1818+ let activate i = do
1819+ append $ toNode $ _traverseChildImmediate_fragment i
1820+ pure $ _traverseChildImmediate_placeholder i
1821+ phs <- traverse id $ weakenDMapWith (either (error "impossible") activate . _traverseChild_mode . getCompose) children0
1822+ liftIO $ writeIORef placeholders $! phs
1823+ append $ toNode lastPlaceholder
8711824 requestDomAction_ $ ffor children' $ \p -> do
8721825 (oldUnready, oldP) <- liftIO $ readIORef pendingChange
8731826 newUnready <- liftIO $ updateChildUnreadiness p oldUnready
8741827 let !newP = p <> oldP
8751828 liftIO $ writeIORef pendingChange (newUnready, newP)
876− when (IntMap.null newUnready) $ do
1829+ when (DMap.null newUnready) $ do
8771830 applyDomUpdate newP
1831+ let result0 = DMap.map (_traverseChild_result . getCompose) children0
1832+ result' = ffor children' $ mapPatch $ _traverseChild_result . getCompose
8781833 return (result0, result')
8791834
880−{-# INLINABLE hoistTraverseWithKeyWithAdjust #-}
881−hoistTraverseWithKeyWithAdjust :: forall (k :: * -> *) v v' t m p.
1835+{-# INLINE hoistTraverseIntMapWithKeyWithAdjust #-}
1836+hoistTraverseIntMapWithKeyWithAdjust ::
8821837 ( Adjustable t m
8831838 , MonadHold t m
884− , DMap.GCompare k
885− , MonadIO m
8861839 , MonadJSM m
887− , PrimMonad m
8881840 , MonadFix m
889− , Patch (p k v)
890− , PatchTarget (p k (Constant Int)) ~ DMap k (Constant Int)
891− , Monoid (p k (Compose ((,,,) DOM.DocumentFragment DOM.Text (IORef (ChildReadyState k))) v'))
892− , Patch (p k (Constant Int))
1841+ , PrimMonad m
1842+ , Monoid (p (TraverseChild t m Int v'))
1843+ , Functor p
1844+ , PatchTarget (p (HydrationRunnerT t m ())) ~ IntMap (HydrationRunnerT t m ())
1845+ , PatchTarget (p (TraverseChild t m Int v')) ~ IntMap (TraverseChild t m Int v')
1846+ , Patch (p (HydrationRunnerT t m ()))
1847+ , Patch (p (TraverseChild t m Int v'))
1848+ , RawDocument (DomBuilderSpace (HydrationDomBuilderT s t m)) ~ Document
8931849 )
894− => (forall vv vv'.
895− (forall a. k a -> vv a -> RequesterT t JSM Identity (TriggerEventT t m) (vv' a))
896− -> DMap k vv
897− -> Event t (p k vv)
898− -> RequesterT t JSM Identity (TriggerEventT t m) (DMap k vv', Event t (p k vv'))
899− ) -- ^ The base monad's traversal
900− -> (forall vv vv'. (forall a. vv a -> vv' a) -> p k vv -> p k vv') -- ^ A way of mapping over the patch type
901− -> (p k (Compose ((,,,) DOM.DocumentFragment DOM.Text (IORef (ChildReadyState k))) v') -> DMap k (Constant (IORef (ChildReadyState k))) -> IO (DMap k (Constant (IORef (ChildReadyState k))))) -- ^ Given a patch for the children DOM elements, produce a patch for the childrens' unreadiness state
902− -> (IORef (Map.Map (Some.Some k) DOM.Text) -> IORef DOM.Text -> p k (Compose ((,,,) DOM.DocumentFragment DOM.Text (IORef (ChildReadyState k))) v') -> JSM ()) -- ^ Apply a patch to the DOM
903− -> (forall a. k a -> v a -> ImmediateDomBuilderT t m (v' a))
904− -> DMap k v
905− -> Event t (p k v)
906− -> ImmediateDomBuilderT t m (DMap k v', Event t (p k v'))
907−hoistTraverseWithKeyWithAdjust base mapPatch updateChildUnreadiness applyDomUpdate_ (f :: forall a. k a -> v a -> ImmediateDomBuilderT t m (v' a)) (dm0 :: DMap k v) dm' = do
908− initialEnv <- ImmediateDomBuilderT ask
909− let parentUnreadyChildren = _immediateDomBuilderEnv_unreadyChildren initialEnv
910− pendingChange :: IORef (DMap k (Constant (IORef (ChildReadyState k))), p k (Compose ((,,,) DOM.DocumentFragment DOM.Text (IORef (ChildReadyState k))) v')) <- liftIO $ newIORef mempty
1850+ => ((IntMap.Key -> v -> DomRenderHookT t m (TraverseChild t m Int v'))
1851+ -> IntMap v
1852+ -> Event t (p v)
1853+ -> DomRenderHookT t m (IntMap (TraverseChild t m Int v'), Event t (p (TraverseChild t m Int v'))))
1854+ -- ^ The base monad's traversal
1855+ -> (p (TraverseChild t m Int v')
1856+ -> IntMap (IORef (ChildReadyState Int))
1857+ -> IO (IntMap (IORef (ChildReadyState Int))))
1858+ -- ^ Given a patch for the children DOM elements, produce a patch for the childrens' unreadiness state
1859+ -> (IORef (IntMap DOM.Text)
1860+ -> DOM.Text
1861+ -> p (TraverseChild t m Int v')
1862+ -> JSM ())
1863+ -- ^ Apply a patch to the DOM
1864+ -> (IntMap.Key -> v -> HydrationDomBuilderT s t m v')
1865+ -> IntMap v
1866+ -> Event t (p v)
1867+ -> HydrationDomBuilderT s t m (IntMap v', Event t (p v'))
1868+hoistTraverseIntMapWithKeyWithAdjust base updateChildUnreadiness applyDomUpdate_ f dm0 dm' = do
1869+ doc <- askDocument
1870+ initialEnv <- HydrationDomBuilderT ask
1871+ let parentUnreadyChildren = _hydrationDomBuilderEnv_unreadyChildren initialEnv
1872+ pendingChange :: IORef (IntMap (IORef (ChildReadyState Int)), p (TraverseChild t m Int v')) <- liftIO $ newIORef mempty
9111873 haveEverBeenReady <- liftIO $ newIORef False
912− placeholders <- liftIO $ newIORef $ error "placeholders not yet initialized"
913− lastPlaceholderRef <- liftIO $ newIORef $ error "lastPlaceholderRef not yet initialized"
1874+ placeholders <- liftIO $ newIORef IntMap.empty
1875+ lastPlaceholder <- createTextNode doc ("" :: Text)
9141876 let applyDomUpdate p = do
915− applyDomUpdate_ placeholders lastPlaceholderRef p
1877+ applyDomUpdate_ placeholders lastPlaceholder p
9161878 markSelfReady
9171879 liftIO $ writeIORef pendingChange $! mempty
9181880 markSelfReady = do
@@ -923,8 +1885,8 @@ hoistTraverseWithKeyWithAdjust base mapPatch updateChildUnreadiness applyDomUpda
9231885 old <- liftIO $ readIORef parentUnreadyChildren
9241886 let new = pred old
9251887 liftIO $ writeIORef parentUnreadyChildren $! new
926− when (new == 0) $ _immediateDomBuilderEnv_commitAction initialEnv
927− markChildReady :: IORef (ChildReadyState k) -> JSM ()
1888+ when (new == 0) $ _hydrationDomBuilderEnv_commitAction initialEnv
1889+ markChildReady :: IORef (ChildReadyState Int) -> JSM ()
9281890 markChildReady childReadyState = do
9291891 liftIO (readIORef childReadyState) >>= \case
9301892 ChildReadyState_Ready -> return ()
@@ -932,82 +1894,191 @@ hoistTraverseWithKeyWithAdjust base mapPatch updateChildUnreadiness applyDomUpda
9321894 liftIO $ writeIORef childReadyState ChildReadyState_Ready
9331895 case countedAt of
9341896 Nothing -> return ()
935− Just (Some.This k) -> do -- This child has been counted as unready, so we need to remove it from the unready set
1897+ Just k -> do -- This child has been counted as unready, so we need to remove it from the unready set
9361898 (oldUnready, p) <- liftIO $ readIORef pendingChange
937− when (not $ DMap.null oldUnready) $ do -- This shouldn't actually ever be null
938− let newUnready = DMap.delete k oldUnready
1899+ when (not $ IntMap.null oldUnready) $ do -- This shouldn't actually ever be null
1900+ let newUnready = IntMap.delete k oldUnready
9391901 liftIO $ writeIORef pendingChange (newUnready, p)
940− when (DMap.null newUnready) $ do
1902+ when (IntMap.null newUnready) $ do
9411903 applyDomUpdate p
942− (children0, children') <- ImmediateDomBuilderT $ lift $ base (\k v -> drawChildUpdate initialEnv markChildReady $ f k v) dm0 dm'
943− let processChild k (Compose (_, _, sRef, _)) = ComposeMaybe <$> do
944− readIORef sRef >>= \case
945− ChildReadyState_Ready -> return Nothing
946− ChildReadyState_Unready _ -> do
947− writeIORef sRef $ ChildReadyState_Unready $ Just $ Some.This k
948− return $ Just $ Constant sRef
949− initialUnready <- liftIO $ DMap.mapMaybeWithKey (\_ -> getComposeMaybe) <$> DMap.traverseWithKey processChild children0
950− liftIO $ if DMap.null initialUnready
1904+ (children0 :: IntMap (TraverseChild t m Int v'), children' :: Event t (p (TraverseChild t m Int v')))
1905+ <- HydrationDomBuilderT $ lift $ base (\k v -> drawChildUpdateInt initialEnv markChildReady $ f k v) dm0 dm'
1906+ let processChild k (TraverseChild e _) = case e of
1907+ Left _ -> pure Nothing
1908+ Right immediate -> do
1909+ readIORef (_traverseChildImmediate_childReadyState immediate) >>= \case
1910+ ChildReadyState_Ready -> return Nothing
1911+ ChildReadyState_Unready _ -> do
1912+ writeIORef (_traverseChildImmediate_childReadyState immediate) $ ChildReadyState_Unready $ Just k
1913+ return $ Just (_traverseChildImmediate_childReadyState immediate)
1914+ initialUnready <- liftIO $ IntMap.mapMaybe id <$> IntMap.traverseWithKey processChild children0
1915+ liftIO $ if IntMap.null initialUnready
9511916 then writeIORef haveEverBeenReady True
9521917 else do
9531918 modifyIORef' parentUnreadyChildren succ
9541919 writeIORef pendingChange (initialUnready, mempty) -- The patch is always empty because it got applied implicitly when we ran the children the first time
955− let result0 = DMap.map (\(Compose (_, _, _, v)) -> v) children0
956− placeholders0 = weakenDMapWith (\(Compose (_, ph, _, _)) -> ph) children0
957− result' = ffor children' $ mapPatch $ \(Compose (_, _, _, r)) -> r
958− liftIO $ writeIORef placeholders $! placeholders0
959− _ <- DMap.traverseWithKey (\_ (Compose (df, _, _, _)) -> Constant () <$ append (toNode df)) children0
960− liftIO . writeIORef lastPlaceholderRef =<< textNodeInternal ("" :: Text)
1920+ getHydrationMode >>= \case
1921+ HydrationMode_Hydrating -> addHydrationStepWithSetup (holdIncremental children0 children') $ \children -> do
1922+ dm :: IntMap (TraverseChild t m Int v') <- sample $ currentIncremental children
1923+ phs <- traverse (either _traverseChildHydration_delayed (pure . _traverseChildImmediate_placeholder) . _traverseChild_mode) dm
1924+ liftIO $ writeIORef placeholders $! phs
1925+ insertAfterPreviousNode lastPlaceholder
1926+ HydrationMode_Immediate -> do
1927+ let activate i = do
1928+ append $ toNode $ _traverseChildImmediate_fragment i
1929+ pure $ _traverseChildImmediate_placeholder i
1930+ phs <- traverse (either (error "impossible") activate . _traverseChild_mode) children0
1931+ liftIO $ writeIORef placeholders $! phs
1932+ append $ toNode lastPlaceholder
9611933 requestDomAction_ $ ffor children' $ \p -> do
9621934 (oldUnready, oldP) <- liftIO $ readIORef pendingChange
9631935 newUnready <- liftIO $ updateChildUnreadiness p oldUnready
9641936 let !newP = p <> oldP
9651937 liftIO $ writeIORef pendingChange (newUnready, newP)
966− when (DMap.null newUnready) $ do
1938+ when (IntMap.null newUnready) $ do
9671939 applyDomUpdate newP
1940+ let result0 = IntMap.map _traverseChild_result children0
1941+ result' = ffor children' $ fmap $ _traverseChild_result
9681942 return (result0, result')
9691943
1944+{-# SPECIALIZE hoistTraverseIntMapWithKeyWithAdjust
1945+ :: ((IntMap.Key -> v -> DomRenderHookT DomTimeline HydrationM (TraverseChild DomTimeline HydrationM Int v'))
1946+ -> IntMap v
1947+ -> Event DomTimeline (PatchIntMap v)
1948+ -> DomRenderHookT DomTimeline HydrationM (IntMap (TraverseChild DomTimeline HydrationM Int v'), Event DomTimeline (PatchIntMap (TraverseChild DomTimeline HydrationM Int v'))))
1949+ -> (PatchIntMap (TraverseChild DomTimeline HydrationM Int v')
1950+ -> IntMap (IORef (ChildReadyState Int))
1951+ -> IO (IntMap (IORef (ChildReadyState Int))))
1952+ -> (IORef (IntMap DOM.Text)
1953+ -> DOM.Text
1954+ -> PatchIntMap (TraverseChild DomTimeline HydrationM Int v')
1955+ -> JSM ())
1956+ -> (IntMap.Key -> v -> HydrationDomBuilderT HydrationDomSpace DomTimeline HydrationM v')
1957+ -> IntMap v
1958+ -> Event DomTimeline (PatchIntMap v)
1959+ -> HydrationDomBuilderT HydrationDomSpace DomTimeline HydrationM (IntMap v', Event DomTimeline (PatchIntMap v'))
1960+ #-}
1961+
1962+{-# SPECIALIZE hoistTraverseIntMapWithKeyWithAdjust
1963+ :: ((IntMap.Key -> v -> DomRenderHookT DomTimeline HydrationM (TraverseChild DomTimeline HydrationM Int v'))
1964+ -> IntMap v
1965+ -> Event DomTimeline (PatchIntMap v)
1966+ -> DomRenderHookT DomTimeline HydrationM (IntMap (TraverseChild DomTimeline HydrationM Int v'), Event DomTimeline (PatchIntMap (TraverseChild DomTimeline HydrationM Int v'))))
1967+ -> (PatchIntMap (TraverseChild DomTimeline HydrationM Int v')
1968+ -> IntMap (IORef (ChildReadyState Int))
1969+ -> IO (IntMap (IORef (ChildReadyState Int))))
1970+ -> (IORef (IntMap DOM.Text)
1971+ -> DOM.Text
1972+ -> PatchIntMap (TraverseChild DomTimeline HydrationM Int v')
1973+ -> JSM ())
1974+ -> (IntMap.Key -> v -> HydrationDomBuilderT GhcjsDomSpace DomTimeline HydrationM v')
1975+ -> IntMap v
1976+ -> Event DomTimeline (PatchIntMap v)
1977+ -> HydrationDomBuilderT GhcjsDomSpace DomTimeline HydrationM (IntMap v', Event DomTimeline (PatchIntMap v'))
1978+ #-}
1979+
1980+data TraverseChildImmediate k = TraverseChildImmediate
1981+ { _traverseChildImmediate_fragment :: {-# UNPACK #-} !DOM.DocumentFragment
1982+ -- ^ Child is appended to this fragment
1983+ , _traverseChildImmediate_placeholder :: {-# UNPACK #-} !DOM.Text
1984+ -- ^ Placeholder reference
1985+ , _traverseChildImmediate_childReadyState :: {-# UNPACK #-} !(IORef (ChildReadyState k))
1986+ }
1987+
1988+newtype TraverseChildHydration t m = TraverseChildHydration
1989+ { _traverseChildHydration_delayed :: HydrationRunnerT t m DOM.Text
1990+ -- ^ Action to run at switchover, returns the placeholder
1991+ }
1992+
1993+data TraverseChild t m k a = TraverseChild
1994+ { _traverseChild_mode :: !(Either (TraverseChildHydration t m) (TraverseChildImmediate k))
1995+ , _traverseChild_result :: !a
1996+ } deriving Functor
1997+
9701998{-# INLINABLE drawChildUpdate #-}
971−drawChildUpdate :: (MonadIO m, MonadJSM m)
972− => ImmediateDomBuilderEnv t
1999+drawChildUpdate :: (MonadJSM m, Reflex t)
2000+ => HydrationDomBuilderEnv t m
9732001 -> (IORef (ChildReadyState k) -> JSM ()) -- This will NOT be called if the child is ready at initialization time; instead, the ChildReadyState return value will be ChildReadyState_Ready
974− -> ImmediateDomBuilderT t m (v' a)
975− -> RequesterT t JSM Identity (TriggerEventT t m) (Compose ((,,,) DOM.DocumentFragment DOM.Text (IORef (ChildReadyState k))) v' a)
2002+ -> HydrationDomBuilderT s t m (f a)
2003+ -> DomRenderHookT t m (Compose (TraverseChild t m k) f a)
9762004drawChildUpdate initialEnv markReady child = do
977− childReadyState <- liftIO $ newIORef $ ChildReadyState_Unready Nothing
2005+ let doc = _hydrationDomBuilderEnv_document initialEnv
9782006 unreadyChildren <- liftIO $ newIORef 0
979− df <- createDocumentFragment $ _immediateDomBuilderEnv_document initialEnv
980− (placeholder, result) <- runReaderT (unImmediateDomBuilderT $ (,) <$> textNodeInternal ("" :: Text) <*> child) $ initialEnv
981− { _immediateDomBuilderEnv_parent = toNode df
982− , _immediateDomBuilderEnv_unreadyChildren = unreadyChildren
983− , _immediateDomBuilderEnv_commitAction = markReady childReadyState
984− }
985− u <- liftIO $ readIORef unreadyChildren
986− when (u == 0) $ liftIO $ writeIORef childReadyState ChildReadyState_Ready
987− return $ Compose (df, placeholder, childReadyState, result)
2007+ liftIO (readIORef $ _hydrationDomBuilderEnv_hydrationMode initialEnv) >>= \case
2008+ HydrationMode_Hydrating -> do
2009+ childDelayedRef <- liftIO $ newIORef $ pure ()
2010+ result <- runReaderT (unHydrationDomBuilderT child) initialEnv
2011+ { _hydrationDomBuilderEnv_unreadyChildren = unreadyChildren
2012+ , _hydrationDomBuilderEnv_delayed = childDelayedRef
2013+ }
2014+ childDelayed <- liftIO $ readIORef childDelayedRef
2015+ return $ Compose $ TraverseChild
2016+ { _traverseChild_result = result
2017+ , _traverseChild_mode = Left TraverseChildHydration
2018+ { _traverseChildHydration_delayed = do
2019+ placeholder <- createTextNode doc ("" :: Text)
2020+ insertAfterPreviousNode placeholder
2021+ childDelayed
2022+ pure placeholder
2023+ }
2024+ }
2025+ HydrationMode_Immediate -> do
2026+ childReadyState <- liftIO $ newIORef $ ChildReadyState_Unready Nothing
2027+ df <- createDocumentFragment doc
2028+ placeholder <- createTextNode doc ("" :: Text)
2029+ Node.appendChild_ df placeholder
2030+ result <- runReaderT (unHydrationDomBuilderT child) initialEnv
2031+ { _hydrationDomBuilderEnv_parent = Left $ toNode df
2032+ , _hydrationDomBuilderEnv_unreadyChildren = unreadyChildren
2033+ , _hydrationDomBuilderEnv_commitAction = markReady childReadyState
2034+ }
2035+ u <- liftIO $ readIORef unreadyChildren
2036+ when (u == 0) $ liftIO $ writeIORef childReadyState ChildReadyState_Ready
2037+ return $ Compose $ TraverseChild
2038+ { _traverseChild_result = result
2039+ , _traverseChild_mode = Right TraverseChildImmediate
2040+ { _traverseChildImmediate_fragment = df
2041+ , _traverseChildImmediate_placeholder = placeholder
2042+ , _traverseChildImmediate_childReadyState = childReadyState
2043+ }
2044+ }
9882045
989−{-# INLINABLE drawChildUpdateInt #-}
990−drawChildUpdateInt :: (MonadIO m, MonadJSM m)
991− => ImmediateDomBuilderEnv t
992− -> (IORef ChildReadyStateInt -> JSM ()) -- This will NOT be called if the child is ready at initialization time; instead, the ChildReadyState return value will be ChildReadyState_Ready
993− -> ImmediateDomBuilderT t m v'
994− -> RequesterT t JSM Identity (TriggerEventT t m) (DOM.DocumentFragment, DOM.Text, IORef ChildReadyStateInt, v')
995−drawChildUpdateInt initialEnv markReady child = do
996− childReadyState <- liftIO $ newIORef $ ChildReadyStateInt_Unready Nothing
997− unreadyChildren <- liftIO $ newIORef 0
998− df <- createDocumentFragment $ _immediateDomBuilderEnv_document initialEnv
999− (placeholder, result) <- runReaderT (unImmediateDomBuilderT $ (,) <$> textNodeInternal ("" :: Text) <*> child) $ initialEnv
1000− { _immediateDomBuilderEnv_parent = toNode df
1001− , _immediateDomBuilderEnv_unreadyChildren = unreadyChildren
1002− , _immediateDomBuilderEnv_commitAction = markReady childReadyState
1003− }
1004− u <- liftIO $ readIORef unreadyChildren
1005− when (u == 0) $ liftIO $ writeIORef childReadyState ChildReadyStateInt_Ready
1006− return (df, placeholder, childReadyState, result)
2046+{-# SPECIALIZE drawChildUpdate
2047+ :: HydrationDomBuilderEnv DomTimeline HydrationM
2048+ -> (IORef (ChildReadyState Int) -> JSM ())
2049+ -> HydrationDomBuilderT s DomTimeline HydrationM (Identity a)
2050+ -> DomRenderHookT DomTimeline HydrationM (Compose (TraverseChild DomTimeline HydrationM Int) Identity a)
2051+ #-}
2052+
2053+{-# SPECIALIZE drawChildUpdate
2054+ :: HydrationDomBuilderEnv DomTimeline HydrationM
2055+ -> (IORef (ChildReadyState (Some k)) -> JSM ())
2056+ -> HydrationDomBuilderT s DomTimeline HydrationM (f a)
2057+ -> DomRenderHookT DomTimeline HydrationM (Compose (TraverseChild DomTimeline HydrationM (Some k)) f a)
2058+ #-}
10072059
1008−mkHasFocus :: (MonadHold t m, Reflex t) => Element er d t -> m (Dynamic t Bool)
2060+{-# INLINABLE drawChildUpdateInt #-}
2061+drawChildUpdateInt :: (MonadIO m, MonadJSM m, Reflex t)
2062+ => HydrationDomBuilderEnv t m
2063+ -> (IORef (ChildReadyState k) -> JSM ())
2064+ -> HydrationDomBuilderT s t m v
2065+ -> DomRenderHookT t m (TraverseChild t m k v)
2066+drawChildUpdateInt env mark m = fmap runIdentity . getCompose <$> drawChildUpdate env mark (Identity <$> m)
2067+
2068+{-# SPECIALIZE drawChildUpdateInt
2069+ :: HydrationDomBuilderEnv DomTimeline HydrationM
2070+ -> (IORef (ChildReadyState k) -> JSM ())
2071+ -> HydrationDomBuilderT s DomTimeline HydrationM v
2072+ -> DomRenderHookT DomTimeline HydrationM (TraverseChild DomTimeline HydrationM k v)
2073+ #-}
2074+
2075+{-# INLINE mkHasFocus #-}
2076+mkHasFocus
2077+ :: (HasDocument m, MonadJSM m, IsNode (RawElement d), MonadHold t m, Reflex t, DOM.IsDocumentOrShadowRoot (RawDocument (DomBuilderSpace m)))
2078+ => Element er d t -> m (Dynamic t Bool)
10092079mkHasFocus e = do
1010− let initialFocus = False --TODO: Actually get the initial focus of the element
2080+ doc <- askDocument
2081+ initialFocus <- Node.isSameNode (toNode $ _element_raw e) . fmap toNode =<< Document.getActiveElement doc
10112082 holdDyn initialFocus $ leftmost
10122083 [ False <$ Reflex.select (_element_events e) (WrapArg Blur)
10132084 , True <$ Reflex.select (_element_events e) (WrapArg Focus)
@@ -1016,39 +2087,49 @@ mkHasFocus e = do
10162087insertBefore :: (MonadJSM m, IsNode new, IsNode existing) => new -> existing -> m ()
10172088insertBefore new existing = do
10182089 p <- getParentNodeUnchecked existing
1019− DOM.insertBefore_ p new (Just existing) -- If there's no parent, that means we've been removed from the DOM; this should not happen if the we're removing ourselves from the performEvent properly
2090+ Node.insertBefore_ p new (Just existing) -- If there's no parent, that means we've been removed from the DOM; this should not happen if the we're removing ourselves from the performEvent properly
10202091
1021−instance PerformEvent t m => PerformEvent t (ImmediateDomBuilderT t m) where
1022− type Performable (ImmediateDomBuilderT t m) = Performable m
2092+type ImmediateDomBuilderT = HydrationDomBuilderT GhcjsDomSpace
2093+
2094+instance PerformEvent t m => PerformEvent t (HydrationDomBuilderT s t m) where
2095+ type Performable (HydrationDomBuilderT s t m) = Performable m
10232096 {-# INLINABLE performEvent_ #-}
10242097 performEvent_ e = lift $ performEvent_ e
10252098 {-# INLINABLE performEvent #-}
10262099 performEvent e = lift $ performEvent e
10272100
1028−instance PostBuild t m => PostBuild t (ImmediateDomBuilderT t m) where
2101+instance PostBuild t m => PostBuild t (HydrationDomBuilderT s t m) where
10292102 {-# INLINABLE getPostBuild #-}
10302103 getPostBuild = lift getPostBuild
10312104
1032−instance MonadReflexCreateTrigger t m => MonadReflexCreateTrigger t (ImmediateDomBuilderT t m) where
2105+instance MonadReflexCreateTrigger t m => MonadReflexCreateTrigger t (HydrationDomBuilderT s t m) where
10332106 {-# INLINABLE newEventWithTrigger #-}
10342107 newEventWithTrigger = lift . newEventWithTrigger
10352108 {-# INLINABLE newFanEventWithTrigger #-}
10362109 newFanEventWithTrigger f = lift $ newFanEventWithTrigger f
10372110
1038−instance (Monad m, MonadRef m, Ref m ~ Ref IO, MonadReflexCreateTrigger t m) => TriggerEvent t (ImmediateDomBuilderT t m) where
2111+instance (Monad m, MonadRef m, Ref m ~ Ref IO, MonadReflexCreateTrigger t m) => TriggerEvent t (HydrationDomBuilderT s t m) where
2112+ {-# INLINABLE newTriggerEvent #-}
2113+ newTriggerEvent = HydrationDomBuilderT . lift $ newTriggerEvent
2114+ {-# INLINABLE newTriggerEventWithOnComplete #-}
2115+ newTriggerEventWithOnComplete = HydrationDomBuilderT . lift $ newTriggerEventWithOnComplete
2116+ {-# INLINABLE newEventWithLazyTriggerWithOnComplete #-}
2117+ newEventWithLazyTriggerWithOnComplete f = HydrationDomBuilderT . lift $ newEventWithLazyTriggerWithOnComplete f
2118+
2119+instance (Monad m, MonadRef m, Ref m ~ Ref IO, MonadReflexCreateTrigger t m) => TriggerEvent t (DomRenderHookT t m) where
10392120 {-# INLINABLE newTriggerEvent #-}
1040− newTriggerEvent = ImmediateDomBuilderT . lift . lift $ newTriggerEvent
2121+ newTriggerEvent = DomRenderHookT . lift $ newTriggerEvent
10412122 {-# INLINABLE newTriggerEventWithOnComplete #-}
1042− newTriggerEventWithOnComplete = ImmediateDomBuilderT . lift . lift $ newTriggerEventWithOnComplete
2123+ newTriggerEventWithOnComplete = DomRenderHookT . lift $ newTriggerEventWithOnComplete
10432124 {-# INLINABLE newEventWithLazyTriggerWithOnComplete #-}
1044− newEventWithLazyTriggerWithOnComplete f = ImmediateDomBuilderT . lift . lift $ newEventWithLazyTriggerWithOnComplete f
2125+ newEventWithLazyTriggerWithOnComplete f = DomRenderHookT . lift $ newEventWithLazyTriggerWithOnComplete f
10452126
1046−instance HasJSContext m => HasJSContext (ImmediateDomBuilderT t m) where
1047− type JSContextPhantom (ImmediateDomBuilderT t m) = JSContextPhantom m
2127+instance HasJSContext m => HasJSContext (HydrationDomBuilderT s t m) where
2128+ type JSContextPhantom (HydrationDomBuilderT s t m) = JSContextPhantom m
10482129 askJSContext = lift askJSContext
10492130
1050−instance MonadRef m => MonadRef (ImmediateDomBuilderT t m) where
1051− type Ref (ImmediateDomBuilderT t m) = Ref m
2131+instance MonadRef m => MonadRef (HydrationDomBuilderT s t m) where
2132+ type Ref (HydrationDomBuilderT s t m) = Ref m
10522133 {-# INLINABLE newRef #-}
10532134 newRef = lift . newRef
10542135 {-# INLINABLE readRef #-}
@@ -1056,12 +2137,12 @@ instance MonadRef m => MonadRef (ImmediateDomBuilderT t m) where
10562137 {-# INLINABLE writeRef #-}
10572138 writeRef r = lift . writeRef r
10582139
1059−instance MonadAtomicRef m => MonadAtomicRef (ImmediateDomBuilderT t m) where
2140+instance MonadAtomicRef m => MonadAtomicRef (HydrationDomBuilderT s t m) where
10602141 {-# INLINABLE atomicModifyRef #-}
10612142 atomicModifyRef r = lift . atomicModifyRef r
10622143
1063−instance (HasJS x m, ReflexHost t) => HasJS x (ImmediateDomBuilderT t m) where
1064− type JSX (ImmediateDomBuilderT t m) = JSX m
2144+instance (HasJS x m, ReflexHost t) => HasJS x (HydrationDomBuilderT s t m) where
2145+ type JSX (HydrationDomBuilderT s t m) = JSX m
10652146 liftJS = lift . liftJS
10662147
10672148type family EventType en where
@@ -1519,11 +2600,11 @@ getTouchEvent = do
15192600 , _touchEventResult_touches = touches
15202601 }
15212602
1522−instance MonadSample t m => MonadSample t (ImmediateDomBuilderT t m) where
2603+instance MonadSample t m => MonadSample t (HydrationDomBuilderT s t m) where
15232604 {-# INLINABLE sample #-}
15242605 sample = lift . sample
15252606
1526−instance MonadHold t m => MonadHold t (ImmediateDomBuilderT t m) where
2607+instance MonadHold t m => MonadHold t (HydrationDomBuilderT s t m) where
15272608 {-# INLINABLE hold #-}
15282609 hold v0 v' = lift $ hold v0 v'
15292610 {-# INLINABLE holdDyn #-}
@@ -1545,7 +2626,7 @@ data Window t = Window
15452626 , _window_raw :: DOM.Window
15462627 }
15472628
1548−wrapWindow :: (MonadJSM m, MonadReflexCreateTrigger t m) => DOM.Window -> WindowConfig t -> ImmediateDomBuilderT t m (Window t)
2629+wrapWindow :: (MonadJSM m, MonadReflexCreateTrigger t m) => DOM.Window -> WindowConfig t -> HydrationDomBuilderT GhcjsDomSpace t m (Window t)
15492630wrapWindow wv _ = do
15502631 events <- wrapDomEventsMaybe wv (defaultDomWindowEventHandler wv) windowOnEventName
15512632 return $ Window
reflex-dom-core/src/Reflex/Dom/Builder/Static.hs+72−4
@@ -15,6 +15,7 @@
1515{-# LANGUAGE UndecidableInstances #-}
1616module Reflex.Dom.Builder.Static where
1717
18+import Data.IORef (IORef)
1819import Blaze.ByteString.Builder.Html.Utf8
1920import Control.Lens hiding (element)
2021import Control.Monad.Exception
@@ -32,11 +33,14 @@ import qualified Data.Dependent.Map as DMap
3233import Data.Dependent.Sum (DSum (..))
3334import Data.Functor.Compose
3435import Data.Functor.Constant
36+import Data.IntMap (IntMap)
37+import qualified Data.IntMap as IntMap
3538import qualified Data.Map as Map
3639import Data.Map.Misc (applyMap)
37−import Data.Monoid
40+import Data.Monoid ((<>))
3841import qualified Data.Set as Set
3942import Data.Text (Text)
43+import qualified Data.Text as T
4044import Data.Text.Encoding
4145import Data.Tuple
4246import GHC.Generics
@@ -49,7 +53,9 @@ import Reflex.Host.Class
4953import Reflex.PerformEvent.Base
5054import Reflex.PerformEvent.Class
5155import Reflex.PostBuild.Base
56+import Reflex.PostBuild.Class
5257import Reflex.TriggerEvent.Class
58+import System.Random (randomRIO)
5359
5460data StaticDomBuilderEnv t = StaticDomBuilderEnv
5561 { _staticDomBuilderEnv_shouldEscape :: Bool
@@ -77,6 +83,10 @@ runStaticDomBuilderT (StaticDomBuilderT a) e = do
7783 (result, a') <- runStateT (runReaderT a e) []
7884 return (result, mconcat $ reverse a')
7985
86+instance PostBuild t m => PostBuild t (StaticDomBuilderT t m) where
87+ {-# INLINABLE getPostBuild #-}
88+ getPostBuild = lift getPostBuild
89+
8090instance MonadReflexCreateTrigger t m => MonadReflexCreateTrigger t (StaticDomBuilderT t m) where
8191 {-# INLINABLE newEventWithTrigger #-}
8292 newEventWithTrigger = lift . newEventWithTrigger
@@ -141,8 +151,8 @@ instance DomSpace StaticDomSpace where
141151 type EventSpec StaticDomSpace = StaticEventSpec
142152 type RawDocument StaticDomSpace = ()
143153 type RawTextNode StaticDomSpace = ()
154+ type RawCommentNode StaticDomSpace = ()
144155 type RawElement StaticDomSpace = ()
145− type RawFile StaticDomSpace = ()
146156 type RawInputElement StaticDomSpace = ()
147157 type RawTextAreaElement StaticDomSpace = ()
148158 type RawSelectElement StaticDomSpace = ()
@@ -151,21 +161,70 @@ instance DomSpace StaticDomSpace where
151161instance (SupportsStaticDomBuilder t m, Monad m) => HasDocument (StaticDomBuilderT t m) where
152162 askDocument = pure ()
153163
154−instance (Reflex t, Adjustable t m, MonadHold t m) => Adjustable t (StaticDomBuilderT t m) where
164+instance (Reflex t, Adjustable t m, MonadHold t m, SupportsStaticDomBuilder t m) => Adjustable t (StaticDomBuilderT t m) where
155165 runWithReplace a0 a' = do
156166 e <- StaticDomBuilderT ask
167+ key <- replaceStart
157168 (result0, result') <- lift $ runWithReplace (runStaticDomBuilderT a0 e) (flip runStaticDomBuilderT e <$> a')
158169 o <- hold (snd result0) $ fmapCheap snd result'
159170 StaticDomBuilderT $ modify $ (:) $ join o
171+ replaceEnd key
160172 return (fst result0, fmapCheap fst result')
173+ traverseIntMapWithKeyWithAdjust = hoistIntMapWithKeyWithAdjust traverseIntMapWithKeyWithAdjust
161174 traverseDMapWithKeyWithAdjust = hoistDMapWithKeyWithAdjust traverseDMapWithKeyWithAdjust mapPatchDMap
162175 traverseDMapWithKeyWithAdjustWithMove = hoistDMapWithKeyWithAdjust traverseDMapWithKeyWithAdjustWithMove mapPatchDMapWithMove
163176
177+-- TODO remove this?
178+replaceStart :: (DomBuilder t m, MonadIO m) => m Text
179+replaceStart = do
180+ str <- liftIO $ replicateM 8 $ randomRIO ('a', 'z')
181+ let key = "-" <> T.pack str
182+ _ <- commentNode $ def { _commentNodeConfig_initialContents = "replace-start" <> key }
183+ pure key
184+
185+replaceEnd :: DomBuilder t m => Text -> m ()
186+replaceEnd key = void $ commentNode $ def { _commentNodeConfig_initialContents = "replace-end" <> key }
187+
188+hoistIntMapWithKeyWithAdjust :: forall t m p a b.
189+ ( Adjustable t m
190+ , MonadHold t m
191+ , Patch (p a)
192+ , Functor p
193+ , Patch (p (Behavior t Builder))
194+ , PatchTarget (p (Behavior t Builder)) ~ IntMap (Behavior t Builder)
195+ , Ref m ~ IORef, MonadIO m, MonadFix m, PerformEvent t m, MonadReflexCreateTrigger t m, MonadRef m -- TODO remove
196+ )
197+ => (forall x. (IntMap.Key -> a -> m x)
198+ -> IntMap a
199+ -> Event t (p a)
200+ -> m (IntMap x, Event t (p x))
201+ ) -- ^ The base monad's traversal
202+ -> (IntMap.Key -> a -> StaticDomBuilderT t m b)
203+ -> IntMap a
204+ -> Event t (p a)
205+ -> StaticDomBuilderT t m (IntMap b, Event t (p b))
206+hoistIntMapWithKeyWithAdjust base f im0 im' = do
207+ e <- StaticDomBuilderT ask
208+ (children0, children') <- lift $ base (\k v -> runStaticDomBuilderT (f k v) e) im0 im'
209+ let result0 = IntMap.map fst children0
210+ result' = (fmap . fmap) fst children'
211+ outputs0 :: IntMap (Behavior t Builder)
212+ outputs0 = IntMap.map snd children0
213+ outputs' :: Event t (p (Behavior t Builder))
214+ outputs' = (fmap . fmap) snd children'
215+ outputs <- holdIncremental outputs0 outputs'
216+ StaticDomBuilderT $ modify $ (:) $ pull $ do
217+ os <- sample $ currentIncremental outputs
218+ fmap mconcat $ forM (IntMap.toList os) $ \(_, o) -> do
219+ sample o
220+ return (result0, result')
221+
164222hoistDMapWithKeyWithAdjust :: forall (k :: * -> *) v v' t m p.
165223 ( Adjustable t m
166224 , MonadHold t m
167225 , PatchTarget (p k (Constant (Behavior t Builder))) ~ DMap k (Constant (Behavior t Builder))
168226 , Patch (p k (Constant (Behavior t Builder)))
227+ , Ref m ~ IORef, MonadIO m, MonadFix m, PerformEvent t m, MonadReflexCreateTrigger t m, MonadRef m -- TODO remove
169228 )
170229 => (forall vv vv'.
171230 (forall a. k a -> vv a -> m (vv' a))
@@ -210,6 +269,15 @@ instance SupportsStaticDomBuilder t m => DomBuilder t (StaticDomBuilderT t m) wh
210269 Nothing -> return (pure (escape initialContents))
211270 Just setContents -> hold (escape initialContents) $ fmapCheap escape setContents --Only because it doesn't get optimized when profiling is on
212271 return $ TextNode ()
272+ {-# INLINABLE commentNode #-}
273+ commentNode (CommentNodeConfig initialContents mSetContents) = StaticDomBuilderT $ do
274+ --TODO: Do not escape quotation marks; see https://stackoverflow.com/questions/25612166/what-characters-must-be-escaped-in-html-5
275+ shouldEscape <- asks _staticDomBuilderEnv_shouldEscape
276+ let escape = if shouldEscape then fromHtmlEscapedText else byteString . encodeUtf8
277+ modify . (:) =<< (\c -> "<!--" <> c <> "-->") <$> case mSetContents of
278+ Nothing -> return (pure (escape initialContents))
279+ Just setContents -> hold (escape initialContents) $ fmapCheap escape setContents --Only because it doesn't get optimized when profiling is on
280+ return $ CommentNode ()
213281 {-# INLINABLE element #-}
214282 element elementTag cfg child = do
215283 -- https://www.w3.org/TR/html-markup/syntax.html#syntax-elements
@@ -220,7 +288,7 @@ instance SupportsStaticDomBuilder t m => DomBuilder t (StaticDomBuilderT t m) wh
220288 StaticDomBuilderT $ do
221289 let shouldEscape = elementTag `Set.notMember` noEscapeElements
222290 (result, innerHtml) <- lift $ lift $ runStaticDomBuilderT child $ StaticDomBuilderEnv shouldEscape Nothing
223− attrs0 <- foldDyn applyMap (cfg ^. initialAttributes) (cfg ^. modifyAttributes)
291+ attrs0 <- foldDyn applyMap (Map.insert "data-ssr" "" $ cfg ^. initialAttributes) (cfg ^. modifyAttributes)
224292 selectValue <- asks _staticDomBuilderEnv_selectValue
225293 let addSelectedAttr attrs sel = case Map.lookup "value" attrs of
226294 Just v | v == sel -> attrs <> Map.singleton "selected" ""
reflex-dom-core/src/Reflex/Dom/Core.hs+1
@@ -18,3 +18,4 @@ import Reflex.Dom.Prerender as X
1818import Reflex.Dom.WebSocket as X
1919import Reflex.Dom.Widget as X
2020import Reflex.Dom.Xhr as X
21+import Reflex.Dom.Xhr.FormData as X
reflex-dom-core/src/Reflex/Dom/Location.hs+39
@@ -12,6 +12,7 @@ module Reflex.Dom.Location
1212 , getLocationProtocol
1313 , getLocationUrl
1414 , manageHistory
15+ , manageHistory'
1516 , HistoryCommand (..)
1617 , HistoryStateUpdate (..)
1718 , HistoryItem (..)
@@ -23,8 +24,11 @@ import Reflex.Dom.Builder.Immediate (wrapDomEvent)
2324
2425import Control.Lens ((^.))
2526import Control.Monad ((>=>))
27+import Control.Monad.Fix (MonadFix)
28+import Data.Align (align)
2629import Data.Monoid
2730import Data.Text (Text)
31+import Data.These (These(..))
2832import qualified GHCJS.DOM as DOM
2933import qualified GHCJS.DOM.EventM as DOM
3034import qualified GHCJS.DOM.Location as Location
@@ -159,3 +163,38 @@ manageHistory runCmd = do
159163 <*> getLocationUri location
160164 holdDyn item0 $ leftmost [itemSetInternal, itemSetExternal]
161165--TODO: Handle title setting better
166+
167+manageHistory'
168+ :: (MonadFix m, MonadJSM m, TriggerEvent t m, MonadHold t m, PerformEvent t m, MonadJSM (Performable m))
169+ => Event t ()
170+ -- ^ Don't do anything until this event has fired
171+ -> Event t HistoryCommand
172+ -> m (Dynamic t HistoryItem)
173+manageHistory' switchover runCmd = do
174+ window <- DOM.currentWindowUnchecked
175+ location <- Window.getLocation window
176+ history <- Window.getHistory window
177+ let getCurrentHistoryItem = HistoryItem
178+ <$> History.getState history
179+ <*> getLocationUri location
180+ item0 <- liftJSM getCurrentHistoryItem
181+ itemSetExternal' <- wrapDomEvent window (`DOM.on` DOM.popState) $ do
182+ e <- DOM.event
183+ HistoryItem
184+ <$> (SerializedScriptValue <$> PopStateEvent.getState e)
185+ <*> getLocationUri location
186+ let f :: (Bool, Maybe a) -> These a () -> (Maybe (Bool, Maybe a), Maybe a)
187+ f (switched, acc) = \case
188+ This change
189+ | switched -> (Nothing, Just change)
190+ | otherwise -> (Just (switched, Just change), Nothing)
191+ That () -> (Just (True, Nothing), acc)
192+ These change () -> (Just (True, Nothing), Just change)
193+ -- Accumulate the events before switchover
194+ (_, cmd') <- mapAccumMaybeB f (False, Nothing) $ align (leftmost [Left <$> runCmd, Right <$> itemSetExternal']) switchover
195+ let (itemSetInternal', itemSetExternal) = fanEither cmd'
196+ itemSetInternal <- performEvent $ ffor itemSetInternal' $ \cmd -> liftJSM $ do
197+ runHistoryCommand history cmd
198+ getCurrentHistoryItem
199+ holdDyn item0 $ leftmost [itemSetInternal, itemSetExternal]
200+--TODO: Handle title setting better
reflex-dom-core/src/Reflex/Dom/Main.hs+129−17
@@ -17,6 +17,8 @@ module Reflex.Dom.Main where
1717
1818import Prelude hiding (concat, mapM, mapM_, sequence, sequence_)
1919
20+import Reflex.Adjustable.Class
21+import Reflex.Class
2022import Reflex.Dom.Builder.Immediate
2123import Reflex.Dom.Class
2224import Reflex.Host.Class
@@ -24,8 +26,10 @@ import Reflex.PerformEvent.Base
2426import Reflex.PostBuild.Base
2527import Reflex.Spider (Global, Spider, SpiderHost, runSpiderHost)
2628import Reflex.TriggerEvent.Base
27−
28−import Reflex.Dom.Specializations () -- For SPECIALIZATION pragmas, which are always re-exported
29+import Reflex.TriggerEvent.Class
30+#ifdef PROFILE_REFLEX
31+import Reflex.Profiled
32+#endif
2933
3034import Control.Concurrent
3135import Control.Lens
@@ -34,6 +38,7 @@ import Control.Monad.Reader hiding (forM, forM_, mapM, mapM_, sequence, sequence
3438import Control.Monad.Ref
3539import Data.ByteString (ByteString)
3640import Data.Dependent.Sum (DSum (..))
41+import Data.Foldable (for_)
3742import Data.IORef
3843import Data.Maybe
3944import Data.Monoid ((<>))
@@ -52,6 +57,104 @@ import qualified GHCJS.DOM.Types as DOM
5257import Reflex.Profiled
5358#endif
5459
60+{-# INLINE mainHydrationWidgetWithHead #-}
61+mainHydrationWidgetWithHead :: (forall x. HydrationWidget x ()) -> (forall x. HydrationWidget x ()) -> JSM ()
62+mainHydrationWidgetWithHead = mainHydrationWidgetWithHead'
63+
64+{-# INLINABLE mainHydrationWidgetWithHead' #-}
65+-- | Warning: `mainHydrationWidgetWithHead'` is provided only as performance tweak. It is expected to disappear in future releases.
66+mainHydrationWidgetWithHead' :: HydrationWidget () () -> HydrationWidget () () -> JSM ()
67+mainHydrationWidgetWithHead' = mainHydrationWidgetWithSwitchoverAction' (pure ())
68+
69+{-# INLINE mainHydrationWidgetWithSwitchoverAction #-}
70+mainHydrationWidgetWithSwitchoverAction :: JSM () -> (forall x. HydrationWidget x ()) -> (forall x. HydrationWidget x ()) -> JSM ()
71+mainHydrationWidgetWithSwitchoverAction = mainHydrationWidgetWithSwitchoverAction'
72+
73+{-# INLINABLE mainHydrationWidgetWithSwitchoverAction' #-}
74+-- | Warning: `mainHydrationWidgetWithSwitchoverAction'` is provided only as performance tweak. It is expected to disappear in future releases.
75+mainHydrationWidgetWithSwitchoverAction' :: JSM () -> HydrationWidget () () -> HydrationWidget () () -> JSM ()
76+mainHydrationWidgetWithSwitchoverAction' switchoverAction head' body = do
77+ runHydrationWidgetWithHeadAndBody switchoverAction $ \appendHead appendBody -> do
78+ appendHead head'
79+ appendBody body
80+
81+{-# INLINABLE attachHydrationWidget #-}
82+attachHydrationWidget
83+ :: JSM ()
84+ -> JSContextSingleton ()
85+ -> ( Event DomTimeline ()
86+ -> IORef HydrationMode
87+ -> Maybe (IORef [(Node, HydrationRunnerT DomTimeline (DomCoreWidget ()) ())])
88+ -> EventChannel
89+ -> PerformEventT DomTimeline DomHost (a, IORef (Maybe (EventTrigger DomTimeline ())))
90+ )
91+ -> IO (a, FireCommand DomTimeline DomHost)
92+attachHydrationWidget switchoverAction jsSing w = do
93+ hydrationMode <- liftIO $ newIORef HydrationMode_Hydrating
94+ rootNodesRef <- liftIO $ newIORef []
95+ events <- newChan
96+ runDomHost $ flip runTriggerEventT events $ mdo
97+ (syncEvent, fireSync) <- newTriggerEvent
98+ ((result, postBuildTriggerRef), fc@(FireCommand fire)) <- lift $ hostPerformEventT $ do
99+ a <- w syncEvent hydrationMode (Just rootNodesRef) events
100+ _ <- runWithReplace (return ()) $ delayedAction <$ syncEvent
101+ pure a
102+ mPostBuildTrigger <- readRef postBuildTriggerRef
103+ lift $ forM_ mPostBuildTrigger $ \postBuildTrigger -> fire [postBuildTrigger :=> Identity ()] $ return ()
104+ liftIO $ fireSync ()
105+ rootNodes <- liftIO $ readIORef rootNodesRef
106+ let delayedAction = do
107+ for_ (reverse rootNodes) $ \(rootNode, runner) -> do
108+ let hydrate = runHydrationRunnerT runner Nothing rootNode events
109+ void $ runWithJSContextSingleton (runPostBuildT hydrate never) jsSing
110+ liftIO $ writeIORef hydrationMode HydrationMode_Immediate
111+ runWithJSContextSingleton (DOM.liftJSM switchoverAction) jsSing
112+ pure (result, fc)
113+
114+type HydrationWidget x a = HydrationDomBuilderT HydrationDomSpace DomTimeline (DomCoreWidget x) a
115+
116+-- | A widget that isn't attached to any particular part of the DOM hierarchy
117+type FloatingWidget x = TriggerEventT DomTimeline (DomCoreWidget x)
118+
119+type DomCoreWidget x = PostBuildT DomTimeline (WithJSContextSingleton x (PerformEventT DomTimeline DomHost))
120+
121+{-# INLINABLE runHydrationWidgetWithHeadAndBody #-}
122+runHydrationWidgetWithHeadAndBody
123+ :: JSM ()
124+ -> ( (forall c. HydrationWidget () c -> FloatingWidget () c) -- "Append to head"
125+ -> (forall c. HydrationWidget () c -> FloatingWidget () c) -- "Append to body"
126+ -> FloatingWidget () ()
127+ )
128+ -> JSM ()
129+runHydrationWidgetWithHeadAndBody switchoverAction app = withJSContextSingletonMono $ \jsSing -> do
130+ globalDoc <- currentDocumentUnchecked
131+ headElement <- getHeadUnchecked globalDoc
132+ bodyElement <- getBodyUnchecked globalDoc
133+ (events, fc) <- liftIO . attachHydrationWidget switchoverAction jsSing $ \switchover hydrationMode hydrationResult events -> do
134+ (postBuild, postBuildTriggerRef) <- newEventWithTriggerRef
135+ let hydrateDom :: DOM.Node -> HydrationWidget () c -> FloatingWidget () c
136+ hydrateDom n w = do
137+ delayed <- liftIO $ newIORef $ pure ()
138+ unreadyChildren <- liftIO $ newIORef 0
139+ lift $ do
140+ let builderEnv = HydrationDomBuilderEnv
141+ { _hydrationDomBuilderEnv_document = globalDoc
142+ , _hydrationDomBuilderEnv_parent = Left $ toNode n
143+ , _hydrationDomBuilderEnv_unreadyChildren = unreadyChildren
144+ , _hydrationDomBuilderEnv_commitAction = pure ()
145+ , _hydrationDomBuilderEnv_hydrationMode = hydrationMode
146+ , _hydrationDomBuilderEnv_switchover = switchover
147+ , _hydrationDomBuilderEnv_delayed = delayed
148+ }
149+ a <- runHydrationDomBuilderT w builderEnv events
150+ forM_ hydrationResult $ \hr -> do
151+ res <- liftIO $ readIORef delayed
152+ liftIO $ modifyIORef' hr ((n, res) :)
153+ pure a
154+ runWithJSContextSingleton (runPostBuildT (runTriggerEventT (app (hydrateDom $ toNode headElement) (hydrateDom $ toNode bodyElement)) events) postBuild) jsSing
155+ return (events, postBuildTriggerRef)
156+ liftIO $ processAsyncEvents events fc
157+
55158{-# INLINE mainWidget #-}
56159mainWidget :: (forall x. Widget x ()) -> JSM ()
57160mainWidget = mainWidget'
@@ -103,7 +206,7 @@ runDomHost = runSpiderHost
103206 . runProfiledM
104207#endif
105208
106−type Widget x = PostBuildT DomTimeline (ImmediateDomBuilderT DomTimeline (WithJSContextSingleton x (PerformEventT DomTimeline DomHost))) --TODO: Make this more abstract --TODO: Put the WithJSContext underneath PerformEventT - I think this would perform better because it could avoid fmapping over every performEvent
209+type Widget x = ImmediateDomBuilderT DomTimeline (DomCoreWidget x)
107210
108211{-# INLINABLE attachWidget #-}
109212attachWidget :: DOM.IsElement e => e -> JSContextSingleton x -> Widget x a -> JSM a
@@ -117,19 +220,24 @@ mainWidgetWithHead' widgets = withJSContextSingletonMono $ \jsSing -> do
117220 headFragment <- createDocumentFragment doc
118221 bodyElement <- getBodyUnchecked doc
119222 bodyFragment <- createDocumentFragment doc
223+ hydrationMode <- liftIO $ newIORef HydrationMode_Immediate
120224 (events, fc) <- liftIO . attachWidget'' $ \events -> do
121225 let (headWidget, bodyWidget) = widgets
122226 (postBuild, postBuildTriggerRef) <- newEventWithTriggerRef
123227 let go :: forall c. Widget () c -> DOM.DocumentFragment -> PerformEventT DomTimeline DomHost c
124228 go w df = do
125229 unreadyChildren <- liftIO $ newIORef 0
126− let builderEnv = ImmediateDomBuilderEnv
127− { _immediateDomBuilderEnv_document = toDocument doc
128− , _immediateDomBuilderEnv_parent = toNode df
129− , _immediateDomBuilderEnv_unreadyChildren = unreadyChildren
130− , _immediateDomBuilderEnv_commitAction = return () --TODO
230+ delayed <- liftIO $ newIORef $ pure ()
231+ let builderEnv = HydrationDomBuilderEnv
232+ { _hydrationDomBuilderEnv_document = toDocument doc
233+ , _hydrationDomBuilderEnv_parent = Left $ toNode df
234+ , _hydrationDomBuilderEnv_unreadyChildren = unreadyChildren
235+ , _hydrationDomBuilderEnv_commitAction = return () --TODO
236+ , _hydrationDomBuilderEnv_switchover = never
237+ , _hydrationDomBuilderEnv_delayed = delayed
238+ , _hydrationDomBuilderEnv_hydrationMode = hydrationMode
131239 }
132− runWithJSContextSingleton (runImmediateDomBuilderT (runPostBuildT w postBuild) builderEnv events) jsSing
240+ runWithJSContextSingleton (runPostBuildT (runHydrationDomBuilderT w builderEnv events) postBuild) jsSing
133241 rec b <- go (headWidget a) headFragment
134242 a <- go (bodyWidget b) bodyFragment
135243 return (events, postBuildTriggerRef)
@@ -148,16 +256,21 @@ attachWidget' :: DOM.IsElement e => e -> JSContextSingleton x -> Widget x a -> J
148256attachWidget' rootElement jsSing w = do
149257 doc <- getOwnerDocumentUnchecked rootElement
150258 df <- createDocumentFragment doc
259+ hydrationMode <- liftIO $ newIORef HydrationMode_Immediate
151260 ((a, events), fc) <- liftIO . attachWidget'' $ \events -> do
152261 (postBuild, postBuildTriggerRef) <- newEventWithTriggerRef
153262 unreadyChildren <- liftIO $ newIORef 0
154− let builderEnv = ImmediateDomBuilderEnv
155− { _immediateDomBuilderEnv_document = toDocument doc
156− , _immediateDomBuilderEnv_parent = toNode df
157− , _immediateDomBuilderEnv_unreadyChildren = unreadyChildren
158− , _immediateDomBuilderEnv_commitAction = return () --TODO
263+ delayed <- liftIO $ newIORef $ pure ()
264+ let builderEnv = HydrationDomBuilderEnv
265+ { _hydrationDomBuilderEnv_document = toDocument doc
266+ , _hydrationDomBuilderEnv_parent = Left $ toNode df
267+ , _hydrationDomBuilderEnv_unreadyChildren = unreadyChildren
268+ , _hydrationDomBuilderEnv_commitAction = return () --TODO
269+ , _hydrationDomBuilderEnv_switchover = never
270+ , _hydrationDomBuilderEnv_delayed = delayed
271+ , _hydrationDomBuilderEnv_hydrationMode = hydrationMode
159272 }
160− a <- runWithJSContextSingleton (runImmediateDomBuilderT (runPostBuildT w postBuild) builderEnv events) jsSing
273+ a <- runWithJSContextSingleton (runPostBuildT (runHydrationDomBuilderT w builderEnv events) postBuild) jsSing
161274 return ((a, events), postBuildTriggerRef)
162275 replaceElementContents rootElement df
163276 liftIO $ processAsyncEvents events fc
@@ -186,7 +299,6 @@ processAsyncEvents events (FireCommand fire) = void $ forkIO $ forever $ do
186299 liftIO $ forM_ ers $ \(_ :=> TriggerInvocation _ cb) -> cb
187300 return ()
188301
189−
190302-- | Run a reflex-dom application inside of an existing DOM element with the given ID
191303mainWidgetInElementById :: Text -> (forall x. Widget x ()) -> JSM ()
192304mainWidgetInElementById eid w = withJSContextSingleton $ \jsSing -> do
@@ -208,7 +320,7 @@ runApp' app = withJSContextSingleton $ \jsSing -> do
208320 body <- getBodyUnchecked doc
209321 win <- getDefaultViewUnchecked doc
210322 rec o <- attachWidget body jsSing $ do
211− w <- lift $ wrapWindow win $ _appOutput_windowConfig o
323+ w <- wrapWindow win $ _appOutput_windowConfig o
212324 app $ AppInput
213325 { _appInput_window = w
214326 }
reflex-dom-core/src/Reflex/Dom/Prerender.hs+184−50
@@ -3,86 +3,220 @@
33{-# LANGUAGE FlexibleContexts #-}
44{-# LANGUAGE FlexibleInstances #-}
55{-# LANGUAGE FunctionalDependencies #-}
6+{-# LANGUAGE LambdaCase #-}
7+{-# LANGUAGE OverloadedStrings #-}
68{-# LANGUAGE MultiParamTypeClasses #-}
79{-# LANGUAGE Rank2Types #-}
10+{-# LANGUAGE RecursiveDo #-}
811{-# LANGUAGE ScopedTypeVariables #-}
912{-# LANGUAGE TypeFamilies #-}
1013{-# LANGUAGE TypeSynonymInstances #-}
1114{-# LANGUAGE UndecidableInstances #-}
15+
16+-- | Render the first widget on the server, and the second on the client.
1217module Reflex.Dom.Prerender
1318 ( Prerender (..)
14− , prerender
19+ , prerender_
1520 , PrerenderClientConstraint
21+ , PrerenderBaseConstraints
1622 ) where
1723
24+import Control.Monad.Primitive (PrimMonad)
1825import Control.Monad.Reader
19−import Data.Constraint
26+import Control.Monad.Ref (MonadRef(..))
27+import Data.IORef (IORef, newIORef)
28+import Data.Semigroup (Semigroup)
29+import Data.Text (Text)
2030import Foreign.JavaScript.TH
2131import GHCJS.DOM.Types (MonadJSM)
22−import Reflex
32+import Reflex hiding (askEvents)
2333import Reflex.Dom.Builder.Class
24−import Reflex.Dom.Builder.InputDisabled
2534import Reflex.Dom.Builder.Immediate
35+import Reflex.Dom.Builder.InputDisabled
2636import Reflex.Dom.Builder.Static
2737import Reflex.Host.Class
38+import Reflex.Requester.Base
39+import Data.IntMap.Strict (IntMap)
40+import qualified Data.IntMap.Strict as IntMap
2841
29−type PrerenderClientConstraint js m =
30− ( HasJS js m
31− , HasJS js (Performable m)
32− , MonadJSM m
33− , MonadJSM (Performable m)
42+import qualified GHCJS.DOM.Document as Document
43+import qualified GHCJS.DOM.Node as Node
44+import qualified GHCJS.DOM.Types as DOM
45+
46+type PrerenderClientConstraint js t m =
47+ ( DomBuilder t m
48+ , DomBuilderSpace m ~ GhcjsDomSpace
49+ , DomRenderHook t m
50+ , HasDocument m
51+ , TriggerEvent t m
52+ , Prerender js t m
53+ , PrerenderBaseConstraints js t m
54+ )
55+
56+type PrerenderBaseConstraints js t m =
57+ ( HasJSContext (Performable m)
3458 , HasJSContext m
35− , HasJSContext (Performable m)
3659 , MonadFix m
37− , MonadFix (Performable m)
38− , DomBuilderSpace m ~ GhcjsDomSpace
60+ , MonadHold t m
61+ , MonadJSM (Performable m)
62+ , MonadJSM m
63+ , MonadRef (Performable m)
64+ , MonadRef m
65+ , MonadReflexCreateTrigger t m
66+ , MonadSample t (Performable m)
67+ , PerformEvent t m
68+ , PostBuild t m
69+ , PrimMonad m
70+ , Ref (Performable m) ~ IORef
71+ , Ref m ~ IORef
72+ , HasJS js m
73+ , HasJS js (Performable m)
3974 )
4075
41−class Prerender js m | m -> js where
42− prerenderClientDict :: Maybe (Dict (PrerenderClientConstraint js m))
43−
44−-- | Draw one widget when prerendering (e.g. server-side) and another when the
45−-- widget is fully instantiated. In a given execution of this function, there
46−-- will be exactly one invocation of exactly one of the arguments.
47−prerender :: forall js m a. Prerender js m => m a -> (PrerenderClientConstraint js m => m a) -> m a
48−prerender server client = case prerenderClientDict :: Maybe (Dict (PrerenderClientConstraint js m)) of
49− Nothing -> server
50− Just Dict -> client
51−
52−instance ( HasJS js m
53− , HasJS js (Performable m)
54− , HasJSContext m
55− , HasJSContext (Performable m)
56− , MonadJSM m
57− , MonadJSM (Performable m)
58− , MonadFix m
59− , MonadFix (Performable m)
60− , ReflexHost t
61− ) => Prerender js (ImmediateDomBuilderT t m) where
62− prerenderClientDict = Just Dict
76+-- | Render the first widget on the server, and the second on the client. The
77+-- hydration builder will run *both* widgets.
78+prerender_
79+ :: (Functor m, Reflex t, Prerender js t m)
80+ => m () -> ((PrerenderClientConstraint js t (Client m)) => Client m ()) -> m ()
81+prerender_ server client = void $ prerender server client
82+
83+class Prerender js t m | m -> t js where
84+ -- | Monad in which the client widget is built
85+ type Client m :: * -> *
86+ -- | Render the first widget on the server, and the second on the client. The
87+ -- hydration builder will run *both* widgets, updating the result dynamic at
88+ -- switchover time.
89+ prerender :: m a -> ((PrerenderClientConstraint js t (Client m)) => Client m a) -> m (Dynamic t a)
90+
91+instance (ReflexHost t, Adjustable t m, PrerenderBaseConstraints js t m) => Prerender js t (HydrationDomBuilderT GhcjsDomSpace t m) where
92+ type Client (HydrationDomBuilderT GhcjsDomSpace t m) = HydrationDomBuilderT GhcjsDomSpace t m
93+ prerender _ client = pure <$> client
94+
95+instance (Adjustable t m, PrerenderBaseConstraints js t m, ReflexHost t) => Prerender js t (HydrationDomBuilderT HydrationDomSpace t m) where
96+ -- | PostBuildT is needed here because we delay running the client builder
97+ -- until after switchover, at which point the postBuild of @m@ has already fired
98+ type Client (HydrationDomBuilderT HydrationDomSpace t m) = PostBuildT t (HydrationDomBuilderT GhcjsDomSpace t m)
99+ -- | Runs the server widget up until switchover, then replaces it with the
100+ -- client widget.
101+ prerender server client = do
102+ env <- HydrationDomBuilderT ask
103+ events <- askEvents
104+ doc <- askDocument
105+ serverDf <- Document.createDocumentFragment doc -- server dom should not be mounted in the window's doc in hydration
106+ df <- Document.createDocumentFragment doc
107+ unreadyChildren <- HydrationDomBuilderT $ asks _hydrationDomBuilderEnv_unreadyChildren
108+ immediateMode <- liftIO $ newIORef HydrationMode_Immediate
109+ delayed <- liftIO $ newIORef $ pure ()
110+ let clientEnv = env
111+ { _hydrationDomBuilderEnv_parent = Left $ DOM.toNode df
112+ , _hydrationDomBuilderEnv_hydrationMode = immediateMode
113+ }
114+ serverEnv = HydrationDomBuilderEnv
115+ { _hydrationDomBuilderEnv_document = doc
116+ , _hydrationDomBuilderEnv_parent = Left $ DOM.toNode serverDf
117+ , _hydrationDomBuilderEnv_unreadyChildren = unreadyChildren
118+ , _hydrationDomBuilderEnv_commitAction = pure ()
119+ , _hydrationDomBuilderEnv_delayed = delayed
120+ , _hydrationDomBuilderEnv_hydrationMode = immediateMode
121+ , _hydrationDomBuilderEnv_switchover = never
122+ }
123+ a0 <- lift $ runHydrationDomBuilderT server serverEnv events
124+ (a', trigger) <- newTriggerEvent
125+ getHydrationMode >>= \case
126+ HydrationMode_Immediate -> do
127+ liftIO . trigger <=< lift $ runHydrationDomBuilderT (runPostBuildT client $ void a') clientEnv events
128+ append $ DOM.toNode df
129+ HydrationMode_Hydrating -> addHydrationStep $ do
130+ liftIO . trigger <=< lift $ runHydrationDomBuilderT (runPostBuildT client $ void a') clientEnv events
131+ insertBefore df =<< deleteToPrerenderEnd doc
132+ holdDyn a0 a'
63133
64134data NoJavaScript -- This type should never have a HasJS instance
65135
66−instance js ~ NoJavaScript => Prerender js (StaticDomBuilderT t m) where
67− prerenderClientDict = Nothing
136+instance (js ~ NoJavaScript, SupportsStaticDomBuilder t m) => Prerender js t (StaticDomBuilderT t m) where
137+ type Client (StaticDomBuilderT t m) = HydrationDomBuilderT GhcjsDomSpace t m
138+ prerender server _ = do
139+ _ <- commentNode $ CommentNodeConfig startMarker Nothing
140+ a <- server
141+ _ <- commentNode $ CommentNodeConfig endMarker Nothing
142+ pure $ pure a
143+
144+instance (Prerender js t m, Monad m) => Prerender js t (ReaderT r m) where
145+ type Client (ReaderT r m) = ReaderT r (Client m)
146+ prerender server client = do
147+ r <- ask
148+ lift $ prerender (runReaderT server r) (runReaderT client r)
149+
150+instance (Prerender js t m, Monad m, Reflex t, MonadFix m, Monoid w) => Prerender js t (DynamicWriterT t w m) where
151+ type Client (DynamicWriterT t w m) = DynamicWriterT t w (Client m)
152+ prerender server client = do
153+ x <- lift $ prerender (runDynamicWriterT server) (runDynamicWriterT client)
154+ let (a, w') = splitDynPure x
155+ w = join w'
156+ tellDyn w
157+ pure a
68158
69−instance (Prerender js m, ReflexHost t) => Prerender js (PostBuildT t m) where
70− prerenderClientDict = fmap (\Dict -> Dict) (prerenderClientDict :: Maybe (Dict (PrerenderClientConstraint js m)))
159+instance (Prerender js t m, Monad m, Reflex t, Semigroup w) => Prerender js t (EventWriterT t w m) where
160+ type Client (EventWriterT t w m) = EventWriterT t w (Client m)
161+ prerender server client = do
162+ x <- lift $ prerender (runEventWriterT server) (runEventWriterT client)
163+ let (a, w') = splitDynPure x
164+ w = switch $ current w'
165+ tellEvent w
166+ pure a
71167
72−instance Prerender js m => Prerender js (DynamicWriterT t w m) where
73− prerenderClientDict = fmap (\Dict -> Dict) (prerenderClientDict :: Maybe (Dict (PrerenderClientConstraint js m)))
168+instance (Prerender js t m, MonadFix m, Reflex t) => Prerender js t (RequesterT t request response m) where
169+ type Client (RequesterT t request response m) = RequesterT t request response (Client m)
170+ prerender server client = mdo
171+ let fannedResponses = fanInt responses
172+ withFannedResponses :: forall m' a. Monad m' => RequesterT t request response m' a -> Int -> m' (a, Event t (IntMap (RequesterData request)))
173+ withFannedResponses w selector = do
174+ (x, e) <- runRequesterT w (selectInt fannedResponses selector)
175+ pure (x, fmapCheap (IntMap.singleton selector) e)
176+ (result, requestsDyn) <- fmap splitDynPure $ lift $ prerender (withFannedResponses server 0) (withFannedResponses client 1)
177+ responses <- fmap (fmapCheap unMultiEntry) $ requesting' $ fmapCheap multiEntry $ switch $ current requestsDyn
178+ pure result
74179
75−instance Prerender js m => Prerender js (EventWriterT t w m) where
76− prerenderClientDict = fmap (\Dict -> Dict) (prerenderClientDict :: Maybe (Dict (PrerenderClientConstraint js m)))
180+instance (Prerender js t m, Monad m, Reflex t, MonadFix m, Group q, Additive q, Query q, Eq q) => Prerender js t (QueryT t q m) where
181+ type Client (QueryT t q m) = QueryT t q (Client m)
182+ prerender server client = mdo
183+ result <- queryDyn query
184+ x <- lift $ prerender (runQueryT server result) (runQueryT client result)
185+ let (a, inc) = splitDynPure x
186+ query = incrementalToDynamic =<< inc -- Can we avoid the incrementalToDynamic?
187+ pure a
77188
78−instance Prerender js m => Prerender js (ReaderT w m) where
79− prerenderClientDict = fmap (\Dict -> Dict) (prerenderClientDict :: Maybe (Dict (PrerenderClientConstraint js m)))
189+instance (Prerender js t m, Monad m) => Prerender js t (InputDisabledT m) where
190+ type Client (InputDisabledT m) = Client m
191+ prerender (InputDisabledT server) client = InputDisabledT $ prerender server client
80192
81−instance Prerender js m => Prerender js (RequesterT t request response m) where
82− prerenderClientDict = fmap (\Dict -> Dict) (prerenderClientDict :: Maybe (Dict (PrerenderClientConstraint js m)))
193+instance (Prerender js t m, Monad m, ReflexHost t) => Prerender js t (PostBuildT t m) where
194+ type Client (PostBuildT t m) = PostBuildT t (Client m)
195+ prerender server client = PostBuildT $ do
196+ pb <- ask
197+ lift $ prerender (runPostBuildT server pb) (runPostBuildT client pb)
83198
84−instance Prerender js m => Prerender js (QueryT t q m) where
85− prerenderClientDict = fmap (\Dict -> Dict) (prerenderClientDict :: Maybe (Dict (PrerenderClientConstraint js m)))
199+startMarker, endMarker :: Text
200+startMarker = "prerender/start"
201+endMarker = "prerender/end"
86202
87−instance Prerender js m => Prerender js (InputDisabledT m) where
88− prerenderClientDict = fmap (\Dict -> Dict) (prerenderClientDict :: Maybe (Dict (PrerenderClientConstraint js m)))
203+deleteToPrerenderEnd :: (MonadIO m, MonadJSM m, Reflex t, MonadFix m) => DOM.Document -> HydrationRunnerT t m DOM.Comment
204+deleteToPrerenderEnd doc = do
205+ startNode <- hydrateComment doc startMarker Nothing
206+ let go (n :: Int) lastNode = Node.getNextSibling lastNode >>= \case
207+ Nothing -> do
208+ c <- Document.createComment doc endMarker
209+ insertAfterPreviousNode c
210+ pure c
211+ Just node -> DOM.castTo DOM.Comment node >>= \case
212+ Nothing -> go n node
213+ Just c -> Node.getTextContentUnchecked c >>= \case
214+ t | t == startMarker -> go (succ n) node
215+ | t == endMarker -> case n of
216+ 0 -> pure c
217+ _ -> go (pred n) node
218+ | otherwise -> go n node
219+ endNode <- go 0 $ DOM.toNode startNode
220+ deleteBetweenExclusive startNode endNode
221+ setPreviousNode $ Just $ DOM.toNode endNode
222+ pure endNode
reflex-dom-core/src/Reflex/Dom/Specializations.hsdeleted−62
@@ -1,62 +0,0 @@
1−{-# OPTIONS_HADDOCK hide #-}
2−{-# LANGUAGE FlexibleContexts #-}
3−{-# LANGUAGE GADTs #-}
4−{-# LANGUAGE Rank2Types #-}
5−{-# LANGUAGE ScopedTypeVariables #-}
6−module Reflex.Dom.Specializations where
7−
8−import Data.Text (Text)
9−import Reflex.Spider
10−import Reflex.Class
11−import Foreign.JavaScript.TH
12−import Reflex.PerformEvent.Base
13−import Reflex.PostBuild.Base
14−import Reflex.Requester.Base
15−import Reflex.TriggerEvent.Base
16−import Reflex.Dom.Builder.Class
17−import Reflex.Dom.Builder.Immediate
18−import Data.Functor.Identity
19−import Data.Functor.Compose
20−import Data.IORef
21−import GHCJS.DOM.Types (JSM)
22−import Data.IntMap.Strict (IntMap)
23−import qualified Data.IntMap.Strict as IntMap
24−
25−import qualified GHCJS.DOM.Element as DOM
26−import qualified GHCJS.DOM.Types as DOM
27−
28−{-# SPECIALIZE makeElement :: forall er a. Text -> ElementConfig er (SpiderTimeline Global) GhcjsDomSpace -> ImmediateDomBuilderT (SpiderTimeline Global) (WithJSContextSingleton () (PerformEventT Spider (SpiderHost Global))) a -> ImmediateDomBuilderT (SpiderTimeline Global) (WithJSContextSingleton () (PerformEventT Spider (SpiderHost Global))) ((Element er GhcjsDomSpace (SpiderTimeline Global), a), DOM.Element) #-}
29−
30−{-# SPECIALIZE wrap :: forall er. RawElement GhcjsDomSpace -> RawElementConfig er (SpiderTimeline Global) GhcjsDomSpace -> ImmediateDomBuilderT (SpiderTimeline Global) (WithJSContextSingleton () (PerformEventT Spider (SpiderHost Global))) (Element er GhcjsDomSpace (SpiderTimeline Global)) #-}
31−
32−{-# SPECIALIZE mapIntMapWithAdjustImpl :: forall v v'.
33− ( (IntMap.Key -> (Event (SpiderTimeline Global) (), v) -> (ImmediateDomBuilderT Spider (WithJSContextSingleton () (PerformEventT Spider (SpiderHost Global)))) v')
34− -> IntMap (Event (SpiderTimeline Global) (), v)
35− -> Event (SpiderTimeline Global) (PatchIntMap (Event (SpiderTimeline Global) (), v))
36− -> (ImmediateDomBuilderT Spider (WithJSContextSingleton () (PerformEventT Spider (SpiderHost Global)))) (IntMap v', Event (SpiderTimeline Global) (PatchIntMap v'))
37− )
38− -> (IntMap.Key -> v -> PostBuildT (SpiderTimeline Global) (ImmediateDomBuilderT Spider (WithJSContextSingleton () (PerformEventT Spider (SpiderHost Global)))) v')
39− -> IntMap v
40− -> Event (SpiderTimeline Global) (PatchIntMap v)
41− -> PostBuildT (SpiderTimeline Global) (ImmediateDomBuilderT Spider (WithJSContextSingleton () (PerformEventT Spider (SpiderHost Global)))) (IntMap v', Event (SpiderTimeline Global) (PatchIntMap v'))
42− #-}
43−
44−{-# SPECIALIZE append :: DOM.Node -> ImmediateDomBuilderT (SpiderTimeline Global) (WithJSContextSingleton () (PerformEventT Spider (SpiderHost Global))) () #-}
45−
46−{-# SPECIALIZE drawChildUpdate :: forall k v' a. ImmediateDomBuilderEnv (SpiderTimeline Global) -> (IORef (ChildReadyState k) -> JSM ()) -> ImmediateDomBuilderT (SpiderTimeline Global) (WithJSContextSingleton () (PerformEventT Spider (SpiderHost Global))) (v' a) -> RequesterT (SpiderTimeline Global) JSM Identity (TriggerEventT (SpiderTimeline Global) (WithJSContextSingleton () (PerformEventT Spider (SpiderHost Global)))) (Compose ((,,,) DOM.DocumentFragment DOM.Text (IORef (ChildReadyState k))) v' a) #-}
47−
48−{-# SPECIALIZE traverseIntMapWithKeyWithAdjust' :: forall v v'. (IntMap.Key -> v -> ImmediateDomBuilderT (SpiderTimeline Global) (WithJSContextSingleton () (PerformEventT Spider (SpiderHost Global))) v') -> IntMap v -> Event (SpiderTimeline Global) (PatchIntMap v) -> ImmediateDomBuilderT (SpiderTimeline Global) (WithJSContextSingleton () (PerformEventT Spider (SpiderHost Global))) (IntMap v', Event (SpiderTimeline Global) (PatchIntMap v')) #-}
49−
50−{-# SPECIALIZE hoistTraverseIntMapWithKeyWithAdjust :: forall v v'.
51− ( (IntMap.Key -> v -> RequesterT (SpiderTimeline Global) JSM Identity (TriggerEventT (SpiderTimeline Global) (WithJSContextSingleton () (PerformEventT Spider (SpiderHost Global)))) (DOM.DocumentFragment, DOM.Text, IORef ChildReadyStateInt, v'))
52− -> IntMap v
53− -> Event (SpiderTimeline Global) (PatchIntMap v)
54− -> RequesterT (SpiderTimeline Global) JSM Identity (TriggerEventT (SpiderTimeline Global) (WithJSContextSingleton () (PerformEventT Spider (SpiderHost Global)))) (IntMap (DOM.DocumentFragment, DOM.Text, IORef ChildReadyStateInt, v'), Event (SpiderTimeline Global) (PatchIntMap (DOM.DocumentFragment, DOM.Text, IORef ChildReadyStateInt, v')))
55− )
56− -> (PatchIntMap (DOM.DocumentFragment, DOM.Text, IORef ChildReadyStateInt, v') -> IntMap (IORef ChildReadyStateInt) -> IO (IntMap (IORef ChildReadyStateInt)))
57− -> (IORef (IntMap DOM.Text) -> IORef DOM.Text -> PatchIntMap (DOM.DocumentFragment, DOM.Text, IORef ChildReadyStateInt, v') -> JSM ())
58− -> (IntMap.Key -> v -> ImmediateDomBuilderT (SpiderTimeline Global) (WithJSContextSingleton () (PerformEventT Spider (SpiderHost Global))) v')
59− -> IntMap v
60− -> Event (SpiderTimeline Global) (PatchIntMap v)
61− -> ImmediateDomBuilderT (SpiderTimeline Global) (WithJSContextSingleton () (PerformEventT Spider (SpiderHost Global))) (IntMap v', Event (SpiderTimeline Global) (PatchIntMap v'))
62− #-}
reflex-dom-core/src/Reflex/Dom/Widget/Basic.hs+15
@@ -14,6 +14,8 @@ module Reflex.Dom.Widget.Basic
1414 -- * Displaying Values
1515 text
1616 , dynText
17+ , comment
18+ , dynComment
1719 , display
1820 , button
1921 , dyn
@@ -119,6 +121,19 @@ dynText t = do
119121 ]
120122 notReadyUntil postBuild
121123
124+comment :: DomBuilder t m => Text -> m ()
125+comment t = void $ commentNode $ def & commentNodeConfig_initialContents .~ t
126+
127+{-# INLINABLE dynComment #-}
128+dynComment :: forall t m. (PostBuild t m, DomBuilder t m) => Dynamic t Text -> m ()
129+dynComment t = do
130+ postBuild <- getPostBuild
131+ void $ commentNode $ (def :: CommentNodeConfig t) & commentNodeConfig_setContents .~ leftmost
132+ [ updated t
133+ , tag (current t) postBuild
134+ ]
135+ notReadyUntil postBuild
136+
122137display :: (PostBuild t m, DomBuilder t m, Show a) => Dynamic t a -> m ()
123138display = dynText . fmap (T.pack . show)
124139
reflex-dom-core/src/Reflex/Dom/Xhr/FormData.hsnew file+47
@@ -0,0 +1,47 @@
1+{-# LANGUAGE FlexibleContexts #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Reflex.Dom.Xhr.FormData
4+ ( postForms
5+ , FormValue (..)
6+ , fileToFormValue
7+ )
8+ where
9+
10+import Control.Lens
11+import Data.Default
12+import Data.Map (Map)
13+import Data.Text (Text)
14+import Data.Traversable
15+import qualified GHCJS.DOM.FormData as FD
16+import Foreign.JavaScript.TH
17+import GHCJS.DOM.File (getName)
18+import GHCJS.DOM.Types (File, IsBlob)
19+import Language.Javascript.JSaddle.Monad (MonadJSM, liftJSM)
20+import Reflex
21+import Reflex.Dom.Xhr
22+
23+-- | A FormData value may be a blob/file or a string. The file can optionally be provided with filename.
24+data FormValue blob = FormValue_Text Text
25+ | FormValue_File blob (Maybe Text) -- maybe filename
26+
27+-- | Performs a POST request with the provided FormData payload
28+postForms
29+ :: ( IsBlob blob, HasJSContext (Performable m), MonadJSM (Performable m)
30+ , PerformEvent t m, TriggerEvent t m
31+ , Traversable f)
32+ => Text -- ^ The target url
33+ -> Event t (f (Map Text (FormValue blob))) -- ^ Maps of text keys and values that will be sent as "FormData"
34+ -> m (Event t (f XhrResponse))
35+postForms url payload = do
36+ performMkRequestsAsync $ ffor payload $ \fs -> for fs $ \u -> liftJSM $ do
37+ fd <- FD.newFormData Nothing
38+ iforM_ u $ \k v -> case v of
39+ FormValue_Text t -> FD.append fd k t
40+ FormValue_File b fn -> FD.appendBlob fd k b fn
41+ return $ xhrRequest "POST" url $ def & xhrRequestConfig_sendData .~ fd
42+
43+-- | Converts a File (e.g., the output of a 'FileInput') into a 'FormValue'. The filename will be included if it is available.
44+fileToFormValue :: MonadJSM m => File -> m (FormValue File)
45+fileToFormValue f = do
46+ fn <- getName f
47+ return $ FormValue_File f $ Just fn
reflex-dom-core/test/gc.hs+12−11
@@ -8,16 +8,17 @@ import Control.Exception
88import Control.Monad
99import Control.Monad.IO.Class
1010import Data.Int
11+import Data.Text as T
1112import Language.Javascript.JSaddle.Warp
1213import Reflex.Dom.Core
1314import Reflex.Time
1415import System.Exit
15−import System.IO.Temp
16−import System.Linux.Namespaces
1716import System.Mem
18−import System.Posix
1917import System.Process
2018
19+import Test.Util.ChromeFlags
20+import Test.Util.UnshareNetwork
21+
2122#if MIN_VERSION_base(4,11,0)
2223import GHC.Stats (getRTSStatsEnabled, getRTSStats, RTSStats(..), gcdetails_live_bytes, gc)
2324currentBytesUsed :: RTSStats -> Int64
@@ -46,14 +47,14 @@ failureLimit = 0
4647
4748main :: IO ()
4849main = do
49− uid <- getEffectiveUserID
50− handle (\(_ :: IOError) -> return ()) $ do -- If we run into an exception with sandboxing, just don't bother
51− unshare [User, Network]
52− writeUserMappings Nothing [UserMapping 0 uid 1]
53− callCommand "ip link set lo up ; ip addr"
54− mainThread <- myThreadId
55− withSystemTempDirectory "reflex-dom-core_test_gc" $ \tmp -> do
56− browserProcess <- spawnCommand $ "echo 'Starting Chromium' ; chromium --headless --disable-gpu --no-sandbox --remote-debugging-port=9222 --user-data-dir=" ++ tmp ++ " http://localhost:3911 ; echo 'Chromium exited'"
50+ handle (\(_ :: IOError) -> return ()) $ unshareNetork -- If we run into an exception with sandboxing, just don't bother
51+ withSandboxedChromeFlags True $ \chromeFlags -> do
52+ mainThread <- myThreadId
53+ browserProcess <- spawnCommand $ mconcat
54+ [ "echo 'Starting Chromium' ; chromium "
55+ , unpack $ T.unwords chromeFlags
56+ , " http://localhost:3911 ; echo 'Chromium exited'"
57+ ]
5758 let finishTest result = do
5859 interruptProcessGroupOf browserProcess
5960 throwTo mainThread result
reflex-dom-core/test/hydration.hsnew file+1428
@@ -0,0 +1,1428 @@
1+{-# OPTIONS_GHC -fno-warn-orphans #-}
2+{-# LANGUAGE BangPatterns #-}
3+{-# LANGUAGE StandaloneDeriving #-}
4+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
5+{-# LANGUAGE UndecidableInstances #-}
6+{-# LANGUAGE ConstraintKinds #-}
7+{-# LANGUAGE CPP #-}
8+{-# LANGUAGE FlexibleContexts #-}
9+{-# LANGUAGE GADTs #-}
10+{-# LANGUAGE LambdaCase #-}
11+{-# LANGUAGE MultiParamTypeClasses #-}
12+{-# LANGUAGE OverloadedStrings #-}
13+{-# LANGUAGE PolyKinds #-}
14+{-# LANGUAGE RankNTypes #-}
15+{-# LANGUAGE RecursiveDo #-}
16+{-# LANGUAGE ScopedTypeVariables #-}
17+{-# LANGUAGE TemplateHaskell #-}
18+{-# LANGUAGE TupleSections #-}
19+{-# LANGUAGE TypeApplications #-}
20+{-# LANGUAGE TypeFamilies #-}
21+
22+import Control.Concurrent
23+import Control.Monad
24+import Control.Monad.Catch
25+import Control.Monad.Fix
26+import Control.Monad.IO.Class
27+import Control.Monad.Ref
28+import Data.Constraint.Extras
29+import Data.Constraint.Extras.TH
30+import Data.Dependent.Map (DMap)
31+import Data.Dependent.Sum (DSum(..), (==>), EqTag(..), ShowTag(..))
32+import Data.Functor.Identity
33+import Data.Functor.Misc
34+import Data.GADT.Compare.TH
35+import Data.GADT.Show.TH
36+import Data.IORef (IORef)
37+import Data.Maybe
38+import Data.Monoid
39+import Data.Proxy
40+import Data.Text (Text)
41+import Language.Javascript.JSaddle (syncPoint, liftJSM)
42+import Language.Javascript.JSaddle.Warp
43+import Network.HTTP.Types (status200)
44+import Network.Socket
45+import Network.Wai
46+import Network.WebSockets
47+import Reflex.Dom.Core
48+import Reflex.Patch.DMapWithMove
49+import System.Directory
50+import System.Environment
51+import System.IO (stderr)
52+import System.IO.Silently
53+import System.IO.Temp
54+import System.Process
55+import qualified Test.HUnit as HUnit (assertEqual, assertFailure)
56+import qualified Test.Hspec as H
57+import Test.Hspec (xit)
58+import Test.Hspec.WebDriver hiding (runWD, click, uploadFile, WD)
59+import qualified Test.Hspec.WebDriver as WD
60+import Test.WebDriver (WD(..))
61+
62+import qualified Data.ByteString.Lazy as LBS
63+import qualified Data.Dependent.Map as DMap
64+import qualified Data.IntMap as IntMap
65+import qualified Data.Text as T
66+import qualified Data.Text.Encoding as TE
67+import qualified GHCJS.DOM.File as File
68+import qualified Network.Wai.Handler.Warp as Warp
69+import qualified System.FilePath as FilePath
70+import qualified Test.WebDriver as WD
71+import qualified Test.WebDriver.Capabilities as WD
72+
73+import Test.Util.ChromeFlags
74+import Test.Util.UnshareNetwork
75+
76+seleniumPort, jsaddlePort :: PortNumber
77+seleniumPort = 8000
78+jsaddlePort = 8001
79+
80+-- TODO Remove orphan
81+instance MonadRef WD where
82+ type Ref WD = Ref IO
83+ newRef = WD . newRef
84+ readRef = WD . readRef
85+ writeRef r = WD . writeRef r
86+
87+assertEqual :: (MonadIO m, Eq a, Show a) => String -> a -> a -> m ()
88+assertEqual a b = liftIO . HUnit.assertEqual a b
89+
90+assertFailure :: MonadIO m => String -> m ()
91+assertFailure = liftIO . HUnit.assertFailure
92+
93+chromeConfig :: Text -> [Text] -> WD.WDConfig
94+chromeConfig fp flags = WD.useBrowser (WD.chrome { WD.chromeBinary = Just $ T.unpack fp, WD.chromeOptions = T.unpack <$> flags }) WD.defaultConfig
95+
96+keyMap :: DMap DKey Identity
97+keyMap = DMap.fromList
98+ [ Key_Int ==> 0
99+ , Key_Char ==> 'A'
100+ ]
101+
102+data DKey a where
103+ Key_Int :: DKey Int
104+ Key_Char :: DKey Char
105+ Key_Bool :: DKey Bool
106+
107+
108+textKey :: DKey a -> Text
109+textKey = \case
110+ Key_Int -> "Key_Int"
111+ Key_Char -> "Key_Char"
112+ Key_Bool -> "Key_Bool"
113+
114+deriveArgDict ''DKey
115+deriveGEq ''DKey
116+deriveGCompare ''DKey
117+deriveGShow ''DKey
118+
119+instance ShowTag DKey Identity where
120+ showTaggedPrec = \case
121+ Key_Int -> showsPrec
122+ Key_Char -> showsPrec
123+ Key_Bool -> showsPrec
124+
125+instance EqTag DKey Identity where
126+ eqTagged Key_Int Key_Int = (==)
127+ eqTagged Key_Char Key_Char = (==)
128+ eqTagged Key_Bool Key_Bool = (==)
129+
130+main :: IO ()
131+main = do
132+ unshareNetork
133+ isHeadless <- (== Nothing) <$> lookupEnv "NO_HEADLESS"
134+ withSandboxedChromeFlags isHeadless $ \chromeFlags -> do
135+ withSeleniumServer $ \selenium -> do
136+ browserPath <- liftIO $ T.strip . T.pack <$> readProcess "which" [ "chromium" ] ""
137+ when (T.null browserPath) $ fail "No browser found"
138+ withDebugging <- isJust <$> lookupEnv "DEBUG"
139+ let wdConfig = WD.defaultConfig { WD.wdPort = fromIntegral $ _selenium_portNumber selenium }
140+ chromeCaps' = WD.getCaps $ chromeConfig browserPath chromeFlags
141+ hspec (tests withDebugging wdConfig [chromeCaps'] selenium) `finally` _selenium_stopServer selenium
142+
143+tests :: Bool -> WD.WDConfig -> [Capabilities] -> Selenium -> Spec
144+tests withDebugging wdConfig caps _selenium = do
145+ let putStrLnDebug :: MonadIO m => Text -> m ()
146+ putStrLnDebug m = when withDebugging $ liftIO $ putStrLn $ T.unpack m
147+ session' = sessionWith wdConfig "" . using (map (,"") caps)
148+ runWD m = runWDOptions (WdOptions False) $ do
149+ putStrLnDebug "before"
150+ r <- m
151+ putStrLnDebug "after"
152+ return r
153+ testWidgetStatic :: WD b -> (forall m js. TestWidget js (SpiderTimeline Global) m => m ()) -> WD b
154+ testWidgetStatic = testWidgetStaticDebug withDebugging
155+ testWidget :: WD () -> WD b -> (forall m js. TestWidget js (SpiderTimeline Global) m => m ()) -> WD b
156+ testWidget = testWidgetDebug withDebugging
157+ testWidget' :: WD a -> (a -> WD b) -> (forall m js. TestWidget js (SpiderTimeline Global) m => m ()) -> WD b
158+ testWidget' = testWidgetDebug' withDebugging
159+ describe "text" $ session' $ do
160+ it "works" $ runWD $ do
161+ testWidgetStatic (checkBodyText "hello world") $ do
162+ text "hello world"
163+ it "works with postBuild" $ runWD $ do
164+ testWidgetStatic (checkBodyText "pb") $ do
165+ pb <- getPostBuild
166+ void $ textNode $ TextNodeConfig "" $ Just $ "pb" <$ pb
167+ it "works for adjacent text nodes" $ runWD $ do
168+ testWidgetStatic (checkBodyText "hello world") $ do
169+ text "hello "
170+ text "world"
171+ it "works for empty adjacent text nodes" $ runWD $ do
172+ testWidgetStatic (checkBodyText "hello world") $ do
173+ pb <- getPostBuild
174+ text ""
175+ text ""
176+ _ <- textNode $ TextNodeConfig "" $ Just $ "hello " <$ pb
177+ _ <- textNode $ TextNodeConfig "abc" $ Just $ "" <$ pb
178+ _ <- textNode $ TextNodeConfig "" $ Just $ "world" <$ pb
179+ text ""
180+ it "works when empty text nodes are the only children of an element" $ runWD $ do
181+ testWidgetStatic (checkBodyText "hello world") $ do
182+ el "div" $ do
183+ text ""
184+ text ""
185+ text "hello world"
186+ it "works when an empty text node is the first child before text" $ runWD $ do
187+ testWidgetStatic (checkTextInTag "div" "hello world") $ do
188+ el "div" $ do
189+ text ""
190+ text "hello world"
191+ it "works when an empty text node is the first child before element" $ runWD $ do
192+ testWidgetStatic (checkTextInTag "div" "hello world") $ do
193+ el "div" $ do
194+ text ""
195+ el "span" $ text "hello world"
196+ it "works when an empty text node is the last child" $ runWD $ do
197+ testWidgetStatic (checkTextInTag "div" "hello world") $ do
198+ el "div" $ do
199+ el "span" $ text "hello world"
200+ text ""
201+-- TODO I think these two tests are broken
202+-- it "updates after postBuild" $ runWD $ do
203+-- testWidget (checkBodyText "initial") (checkBodyText "after") $ do
204+-- after <- delay 0 =<< getPostBuild
205+-- void $ textNode $ TextNodeConfig "initial" $ Just $ "after" <$ after
206+-- it "updates immediately after postBuild" $ runWD $ do
207+-- testWidget (checkBodyText "pb") (checkBodyText "after") $ do
208+-- pb <- getPostBuild
209+-- after <- delay 0 pb
210+-- void $ textNode $ TextNodeConfig "initial" $ Just $ leftmost ["pb" <$ pb, "after" <$ after]
211+ it "updates in immediate mode" $ runWD $ do
212+ let checkUpdated = do
213+ checkBodyText "initial"
214+ WD.click =<< findElemWithRetry (WD.ByTag "button")
215+ checkBodyText "after"
216+ testWidget (pure ()) checkUpdated $ prerender_ (pure ()) $ do
217+ click <- button ""
218+ void $ textNode $ TextNodeConfig "initial" $ Just $ "after" <$ click
219+
220+ describe "element" $ session' $ do
221+ it "works with domEvent Click" $ runWD $ do
222+ clickedRef <- liftIO $ newRef False
223+ testWidget' (findElemWithRetry $ WD.ByTag "div") WD.click $ do
224+ (e, _) <- el' "div" $ text "hello world"
225+ performEvent_ $ liftIO (writeRef clickedRef True) <$ domEvent Click e
226+ readRef clickedRef `shouldBeWithRetryM` True
227+ it "works with eventFlags stopPropagation" $ runWD $ do
228+ firstClickedRef <- newRef False
229+ secondClickedRef <- newRef False
230+ let clickBoth = do
231+ findElemWithRetry (WD.ById "first") >>= WD.click
232+ findElemWithRetry (WD.ById "second") >>= WD.click
233+ testWidget (pure ()) clickBoth $ do
234+ (firstDivEl, _) <- el' "div" $ prerender_ (pure ()) $ do
235+ void $ elAttr "span" ("id" =: "first") $ text "hello world"
236+ performEvent_ $ liftIO (writeRef firstClickedRef True) <$ domEvent Click firstDivEl
237+ (secondDivEl, _) <- el' "div" $ prerender_ (pure ()) $ do
238+ let conf :: ElementConfig EventResult (SpiderTimeline Global) GhcjsDomSpace
239+ conf = (def :: ElementConfig EventResult (SpiderTimeline Global) GhcjsDomSpace)
240+ & initialAttributes .~ "id" =: "second"
241+ & elementConfig_eventSpec .~ addEventSpecFlags (Proxy :: Proxy GhcjsDomSpace) Click (\_ -> stopPropagation) def
242+ void $ element "span" conf $ text "hello world"
243+ performEvent_ $ liftIO (writeRef secondClickedRef True) <$ domEvent Click secondDivEl
244+ firstClicked <- readRef firstClickedRef
245+ secondClicked <- readRef secondClickedRef
246+ assertEqual "Click propagated when it should have stopped" (True, False) (firstClicked, secondClicked)
247+ it "works with eventFlags preventDefault" $ runWD $ do
248+ let click = do
249+ e <- findElemWithRetry $ WD.ByTag "input"
250+ s0 <- WD.isSelected e
251+ WD.click e
252+ s1 <- WD.isSelected e
253+ pure (s0, s1)
254+ clicked <- testWidget (pure ()) click $ prerender_ (pure ()) $ do
255+ let conf :: ElementConfig EventResult (SpiderTimeline Global) GhcjsDomSpace
256+ conf = (def :: ElementConfig EventResult (SpiderTimeline Global) GhcjsDomSpace)
257+ & elementConfig_eventSpec .~ addEventSpecFlags (Proxy :: Proxy GhcjsDomSpace) Click (\_ -> preventDefault) def
258+ & initialAttributes .~ "type" =: "checkbox"
259+ void $ element "input" conf $ text "hello world"
260+ assertEqual "Click not prevented" (False, False) clicked
261+ it "can add/update/remove attributes" $ runWD $ do
262+ let checkInitialAttrs = do
263+ e <- findElemWithRetry $ WD.ByTag "div"
264+ assertAttr e "const" (Just "const")
265+ assertAttr e "delete" (Just "delete")
266+ assertAttr e "init" (Just "init")
267+ assertAttr e "click" Nothing
268+ pure e
269+ checkModifyAttrs e = do
270+ WD.click e
271+ withRetry $ do
272+ assertAttr e "const" (Just "const")
273+ assertAttr e "delete" Nothing
274+ assertAttr e "init" (Just "click")
275+ assertAttr e "click" (Just "click")
276+ testWidget' checkInitialAttrs checkModifyAttrs $ mdo
277+ let conf = def
278+ & initialAttributes .~ "const" =: "const" <> "delete" =: "delete" <> "init" =: "init"
279+ & modifyAttributes .~ (("delete" =: Nothing <> "init" =: Just "click" <> "click" =: Just "click") <$ click)
280+ (e, ()) <- element "div" conf $ text "hello world"
281+ let click = domEvent Click e
282+ return ()
283+ -- TODO check this is the correct solution
284+ it "has ssr attribute, removes ssr attribute" $ runWD $ do
285+ let checkSSRAttr = do
286+ e <- findElemWithRetry $ WD.ByTag "div"
287+ assertAttr e "data-ssr" (Just "")
288+ pure e
289+ testWidget' checkSSRAttr (\e -> assertAttr e "data-ssr" Nothing) $ el "div" $ text "hello world"
290+
291+ describe "inputElement" $ do
292+ describe "hydration" $ session' $ do
293+ it "doesn't wipe user input when switching over" $ runWD $ do
294+ inputRef <- newRef ("" :: Text)
295+ testWidget'
296+ (do
297+ e <- findElemWithRetry $ WD.ByTag "input"
298+ WD.sendKeys "hello world" e
299+ pure e)
300+ (\e -> do
301+ WD.attr e "value" `shouldBeWithRetryM` Just "hello world"
302+ WD.click =<< findElemWithRetry (WD.ByTag "button")
303+ readRef inputRef `shouldBeWithRetryM` "hello world"
304+ ) $ do
305+ e <- inputElement def
306+ click <- button "save"
307+ performEvent_ $ liftIO . writeRef inputRef <$> tag (current (value e)) click
308+ it "captures user input after switchover" $ runWD $ do
309+ inputRef <- newRef ("" :: Text)
310+ let checkValue = do
311+ WD.sendKeys "hello world" =<< findElemWithRetry (WD.ByTag "input")
312+ WD.click =<< findElemWithRetry (WD.ByTag "button")
313+ readRef inputRef `shouldBeWithRetryM` "hello world"
314+ testWidget (pure ()) checkValue $ do
315+ e <- inputElement def
316+ click <- button "save"
317+ performEvent_ $ liftIO . writeRef inputRef <$> tag (current (value e)) click
318+ it "sets focus appropriately" $ runWD $ do
319+ focusRef <- newRef False
320+ let checkValue = do
321+ readRef focusRef `shouldBeWithRetryM` False
322+ e <- findElemWithRetry $ WD.ByTag "input"
323+ WD.click e
324+ readRef focusRef `shouldBeWithRetryM` True
325+ testWidget (pure ()) checkValue $ do
326+ e <- inputElement def
327+ performEvent_ $ liftIO . writeRef focusRef <$> updated (_inputElement_hasFocus e)
328+ it "sets focus when focus occurs before hydration" $ runWD $ do
329+ focusRef <- newRef False
330+ let setup = do
331+ e <- findElemWithRetry $ WD.ByTag "input"
332+ WD.click e
333+ ((== e) <$> WD.activeElem) `shouldBeWithRetryM` True
334+ readRef focusRef `shouldBeWithRetryM` False
335+ check = readRef focusRef `shouldBeWithRetryM` True
336+ testWidget setup check $ do
337+ e <- inputElement def
338+ performEvent_ $ liftIO . writeRef focusRef <$> updated (_inputElement_hasFocus e)
339+ it "sets value appropriately" $ runWD $ do
340+ valueByUIRef <- newRef ("" :: Text)
341+ valueRef <- newRef ("" :: Text)
342+ setValueChan :: Chan Text <- liftIO newChan
343+ let checkValue = do
344+ readRef valueByUIRef `shouldBeWithRetryM` ""
345+ readRef valueRef `shouldBeWithRetryM` ""
346+ e <- findElemWithRetry $ WD.ByTag "input"
347+ WD.sendKeys "hello" e
348+ readRef valueByUIRef `shouldBeWithRetryM` "hello"
349+ readRef valueRef `shouldBeWithRetryM` "hello"
350+ liftIO $ writeChan setValueChan "world"
351+ readRef valueByUIRef `shouldBeWithRetryM` "hello"
352+ readRef valueRef `shouldBeWithRetryM` "world"
353+ testWidget (pure ()) checkValue $ do
354+ update <- triggerEventWithChan setValueChan
355+ e <- inputElement $ def & inputElementConfig_setValue .~ update
356+ performEvent_ $ liftIO . writeRef valueByUIRef <$> _inputElement_input e
357+ performEvent_ $ liftIO . writeRef valueRef <$> updated (value e)
358+ it "sets checked appropriately" $ runWD $ do
359+ checkedByUIRef <- newRef False
360+ checkedRef <- newRef False
361+ setCheckedChan <- liftIO newChan
362+ let checkValue = do
363+ readRef checkedByUIRef `shouldBeWithRetryM` False
364+ readRef checkedRef `shouldBeWithRetryM` False
365+ e <- findElemWithRetry $ WD.ByTag "input"
366+ WD.moveToCenter e
367+ WD.click e
368+ readRef checkedByUIRef `shouldBeWithRetryM` True
369+ readRef checkedRef `shouldBeWithRetryM` True
370+ liftIO $ writeChan setCheckedChan False
371+ readRef checkedByUIRef `shouldBeWithRetryM` True
372+ readRef checkedRef `shouldBeWithRetryM` False
373+ testWidget (pure ()) checkValue $ do
374+ setChecked <- triggerEventWithChan setCheckedChan
375+ e <- inputElement $ def
376+ & initialAttributes .~ "type" =: "checkbox"
377+ & inputElementConfig_setChecked .~ setChecked
378+ performEvent_ $ liftIO . writeRef checkedByUIRef <$> _inputElement_checkedChange e
379+ performEvent_ $ liftIO . writeRef checkedRef <$> updated (_inputElement_checked e)
380+ it "captures file uploads" $ runWD $ do
381+ filesRef :: IORef [Text] <- newRef []
382+ let uploadFile = do
383+ e <- findElemWithRetry $ WD.ByTag "input"
384+ path <- liftIO $ writeSystemTempFile "testFile" "file contents"
385+ WD.sendKeys (T.pack path) e
386+ WD.click =<< findElemWithRetry (WD.ByTag "button")
387+ liftIO $ removeFile path
388+ readRef filesRef `shouldBeWithRetryM` [T.pack $ FilePath.takeFileName path]
389+ testWidget (pure ()) uploadFile $ do
390+ e <- inputElement $ def & initialAttributes .~ "type" =: "file"
391+ click <- button "save"
392+ prerender_ (pure ()) $ performEvent_ $ ffor (tag (current (_inputElement_files e)) click) $ \fs -> do
393+ names <- liftJSM $ traverse File.getName fs
394+ liftIO $ writeRef filesRef names
395+
396+ describe "hydration/immediate" $ session' $ do
397+ it "captures user input after switchover" $ runWD $ do
398+ inputRef :: IORef Text <- newRef ""
399+ let checkValue = do
400+ WD.sendKeys "hello world" =<< findElemWithRetry (WD.ByTag "input")
401+ WD.click =<< findElemWithRetry (WD.ByTag "button")
402+ readRef inputRef `shouldBeWithRetryM` "hello world"
403+ testWidget (pure ()) checkValue $ prerender_ (pure ()) $ do
404+ e <- inputElement def
405+ click <- button "save"
406+ performEvent_ $ liftIO . writeRef inputRef <$> tag (current (value e)) click
407+ it "sets focus appropriately" $ runWD $ do
408+ focusRef <- newRef False
409+ let checkValue = do
410+ readRef focusRef `shouldBeWithRetryM` False
411+ e <- findElemWithRetry $ WD.ByTag "input"
412+ WD.click e
413+ readRef focusRef `shouldBeWithRetryM` True
414+ testWidget (pure ()) checkValue $ prerender_ (pure ()) $ do
415+ e <- inputElement def
416+ performEvent_ $ liftIO . writeRef focusRef <$> updated (_inputElement_hasFocus e)
417+ it "sets value appropriately" $ runWD $ do
418+ valueByUIRef :: IORef Text <- newRef ""
419+ valueRef :: IORef Text <- newRef ""
420+ setValueChan :: Chan Text <- liftIO newChan
421+ let checkValue = do
422+ readRef valueByUIRef `shouldBeWithRetryM` ""
423+ readRef valueRef `shouldBeWithRetryM` ""
424+ e <- findElemWithRetry $ WD.ByTag "input"
425+ WD.sendKeys "hello" e
426+ readRef valueByUIRef `shouldBeWithRetryM` "hello"
427+ readRef valueRef `shouldBeWithRetryM` "hello"
428+ liftIO $ writeChan setValueChan "world"
429+ readRef valueByUIRef `shouldBeWithRetryM` "hello"
430+ readRef valueRef `shouldBeWithRetryM` "world"
431+ testWidget (pure ()) checkValue $ do
432+ update <- triggerEventWithChan setValueChan
433+ prerender_ (pure ()) $ do
434+ e <- inputElement $ def & inputElementConfig_setValue .~ update
435+ performEvent_ $ liftIO . writeRef valueByUIRef <$> _inputElement_input e
436+ performEvent_ $ liftIO . writeRef valueRef <$> updated (value e)
437+ it "sets checked appropriately" $ runWD $ do
438+ checkedByUIRef <- newRef False
439+ checkedRef <- newRef False
440+ setCheckedChan <- liftIO newChan
441+ let checkValue = do
442+ readRef checkedByUIRef `shouldBeWithRetryM` False
443+ readRef checkedRef `shouldBeWithRetryM` False
444+ e <- findElemWithRetry $ WD.ByTag "input"
445+ WD.moveToCenter e
446+ WD.click e
447+ readRef checkedByUIRef `shouldBeWithRetryM` True
448+ readRef checkedRef `shouldBeWithRetryM` True
449+ liftIO $ writeChan setCheckedChan False
450+ readRef checkedByUIRef `shouldBeWithRetryM` True
451+ readRef checkedRef `shouldBeWithRetryM` False
452+ testWidget (pure ()) checkValue $ do
453+ setChecked <- triggerEventWithChan setCheckedChan
454+ prerender_ (pure ()) $ do
455+ e <- inputElement $ def
456+ & initialAttributes .~ "type" =: "checkbox"
457+ & inputElementConfig_setChecked .~ setChecked
458+ performEvent_ $ liftIO . writeRef checkedByUIRef <$> _inputElement_checkedChange e
459+ performEvent_ $ liftIO . writeRef checkedRef <$> updated (_inputElement_checked e)
460+ it "captures file uploads" $ runWD $ do
461+ filesRef :: IORef [Text] <- newRef []
462+ let uploadFile = do
463+ e <- findElemWithRetry $ WD.ByTag "input"
464+ path <- liftIO $ writeSystemTempFile "testFile" "file contents"
465+ WD.sendKeys (T.pack path) e
466+ WD.click =<< findElemWithRetry (WD.ByTag "button")
467+ liftIO $ removeFile path
468+ readRef filesRef `shouldBeWithRetryM` [T.pack $ FilePath.takeFileName path]
469+ testWidget (pure ()) uploadFile $ prerender_ (pure ()) $ do
470+ e <- inputElement $ def & initialAttributes .~ "type" =: "file"
471+ click <- button "save"
472+ performEvent_ $ ffor (tag (current (_inputElement_files e)) click) $ \fs -> do
473+ names <- liftJSM $ traverse File.getName fs
474+ liftIO $ writeRef filesRef names
475+
476+ describe "textAreaElement" $ do
477+ describe "hydration" $ session' $ do
478+ it "doesn't wipe user input when switching over" $ runWD $ do
479+ inputRef <- newRef ("" :: Text)
480+ testWidget'
481+ (do
482+ e <- findElemWithRetry $ WD.ByTag "textarea"
483+ WD.sendKeys "hello world" e
484+ pure e)
485+ (\e -> do
486+ WD.attr e "value" `shouldBeWithRetryM` Just "hello world"
487+ WD.click <=< findElemWithRetry $ WD.ByTag "button"
488+ readRef inputRef `shouldBeWithRetryM` "hello world"
489+ ) $ do
490+ e <- textAreaElement def
491+ click <- button "save"
492+ performEvent_ $ liftIO . writeRef inputRef <$> tag (current (value e)) click
493+ it "captures user input after switchover" $ runWD $ do
494+ inputRef <- newRef ("" :: Text)
495+ let checkValue = do
496+ WD.sendKeys "hello world" =<< findElemWithRetry (WD.ByTag "textarea")
497+ WD.click =<< findElemWithRetry (WD.ByTag "button")
498+ readRef inputRef `shouldBeWithRetryM` "hello world"
499+ testWidget (pure ()) checkValue $ do
500+ e <- textAreaElement def
501+ click <- button "save"
502+ performEvent_ $ liftIO . writeRef inputRef <$> tag (current (value e)) click
503+ it "sets focus appropriately" $ runWD $ do
504+ focusRef <- newRef False
505+ let checkValue = do
506+ readRef focusRef `shouldBeWithRetryM` False
507+ e <- findElemWithRetry $ WD.ByTag "textarea"
508+ WD.click e
509+ readRef focusRef `shouldBeWithRetryM` True
510+ testWidget (pure ()) checkValue $ do
511+ e <- textAreaElement def
512+ performEvent_ $ liftIO . writeRef focusRef <$> updated (_textAreaElement_hasFocus e)
513+ it "sets focus when focus occurs before hydration" $ runWD $ do
514+ focusRef <- newRef False
515+ let setup = do
516+ e <- findElemWithRetry $ WD.ByTag "textarea"
517+ WD.click e
518+ ((== e) <$> WD.activeElem) `shouldBeWithRetryM` True
519+ readRef focusRef `shouldBeWithRetryM` False
520+ check = readRef focusRef `shouldBeWithRetryM` True
521+ testWidget setup check $ do
522+ e <- textAreaElement def
523+ performEvent_ $ liftIO . writeRef focusRef <$> updated (_textAreaElement_hasFocus e)
524+ it "sets value appropriately" $ runWD $ do
525+ valueByUIRef <- newRef ("" :: Text)
526+ valueRef <- newRef ("" :: Text)
527+ setValueChan :: Chan Text <- liftIO newChan
528+ let checkValue = do
529+ readRef valueByUIRef `shouldBeWithRetryM` ""
530+ readRef valueRef `shouldBeWithRetryM` ""
531+ e <- findElemWithRetry $ WD.ByTag "textarea"
532+ WD.sendKeys "hello" e
533+ readRef valueByUIRef `shouldBeWithRetryM` "hello"
534+ readRef valueRef `shouldBeWithRetryM` "hello"
535+ liftIO $ writeChan setValueChan "world"
536+ readRef valueByUIRef `shouldBeWithRetryM` "hello"
537+ readRef valueRef `shouldBeWithRetryM` "world"
538+ testWidget (pure ()) checkValue $ do
539+ setValue' <- triggerEventWithChan setValueChan
540+ e <- textAreaElement $ def { _textAreaElementConfig_setValue = Just setValue' }
541+ performEvent_ $ liftIO . writeRef valueByUIRef <$> _textAreaElement_input e
542+ performEvent_ $ liftIO . writeRef valueRef <$> updated (value e)
543+
544+ describe "hydration/immediate" $ session' $ do
545+ it "captures user input after switchover" $ runWD $ do
546+ inputRef :: IORef Text <- newRef ""
547+ let checkValue = do
548+ WD.sendKeys "hello world" <=< findElemWithRetry $ WD.ByTag "textarea"
549+ WD.click <=< findElemWithRetry $ WD.ByTag "button"
550+ readRef inputRef `shouldBeWithRetryM` "hello world"
551+ testWidget (pure ()) checkValue $ prerender_ (pure ()) $ do
552+ e <- textAreaElement def
553+ click <- button "save"
554+ performEvent_ $ liftIO . writeRef inputRef <$> tag (current (value e)) click
555+ it "sets focus appropriately" $ runWD $ do
556+ focusRef <- newRef False
557+ let checkValue = do
558+ readRef focusRef `shouldBeWithRetryM` False
559+ e <- findElemWithRetry $ WD.ByTag "textarea"
560+ WD.click e
561+ readRef focusRef `shouldBeWithRetryM` True
562+ testWidget (pure ()) checkValue $ prerender_ (pure ()) $ do
563+ e <- textAreaElement def
564+ performEvent_ $ liftIO . writeRef focusRef <$> updated (_textAreaElement_hasFocus e)
565+ it "sets value appropriately" $ runWD $ do
566+ valueByUIRef :: IORef Text <- newRef ""
567+ valueRef :: IORef Text <- newRef ""
568+ setValueChan :: Chan Text <- liftIO newChan
569+ let checkValue = do
570+ readRef valueByUIRef `shouldBeWithRetryM` ""
571+ readRef valueRef `shouldBeWithRetryM` ""
572+ e <- findElemWithRetry $ WD.ByTag "textarea"
573+ WD.sendKeys "hello" e
574+ readRef valueByUIRef `shouldBeWithRetryM` "hello"
575+ readRef valueRef `shouldBeWithRetryM` "hello"
576+ liftIO $ writeChan setValueChan "world"
577+ readRef valueByUIRef `shouldBeWithRetryM` "hello"
578+ readRef valueRef `shouldBeWithRetryM` "world"
579+ testWidget (pure ()) checkValue $ do
580+ setValue' <- triggerEventWithChan setValueChan
581+ prerender_ (pure ()) $ do
582+ e <- textAreaElement $ def { _textAreaElementConfig_setValue = Just setValue' }
583+ performEvent_ $ liftIO . writeRef valueByUIRef <$> _textAreaElement_input e
584+ performEvent_ $ liftIO . writeRef valueRef <$> updated (value e)
585+
586+ describe "selectElement" $ do
587+ let options :: DomBuilder t m => m ()
588+ options = do
589+ elAttr "option" ("value" =: "one" <> "id" =: "one") $ text "one"
590+ elAttr "option" ("value" =: "two" <> "id" =: "two") $ text "two"
591+ elAttr "option" ("value" =: "three" <> "id" =: "three") $ text "three"
592+ describe "hydration" $ session' $ do
593+ it "sets initial value correctly" $ runWD $ do
594+ inputRef <- newRef ("" :: Text)
595+ let setup = do
596+ e <- findElemWithRetry $ WD.ByTag "select"
597+ assertAttr e "value" (Just "three")
598+ WD.click =<< findElemWithRetry (WD.ById "two")
599+ pure e
600+ check e = do
601+ assertAttr e "value" (Just "two")
602+ readRef inputRef `shouldBeWithRetryM` "three"
603+ WD.click =<< findElemWithRetry (WD.ByTag "button")
604+ assertAttr e "value" (Just "two")
605+ readRef inputRef `shouldBeWithRetryM` "two"
606+ testWidget' setup check $ do
607+ (e, ()) <- selectElement (def { _selectElementConfig_initialValue = "three" }) options
608+ click <- button "save"
609+ liftIO . writeRef inputRef <=< sample $ current $ _selectElement_value e
610+ performEvent_ $ liftIO . writeRef inputRef <$> tag (current (_selectElement_value e)) click
611+ it "captures user input after switchover" $ runWD $ do
612+ inputRef <- newRef ("" :: Text)
613+ let checkValue = do
614+ e <- findElemWithRetry $ WD.ByTag "select"
615+ assertAttr e "value" (Just "one")
616+ WD.click =<< findElemWithRetry (WD.ById "two")
617+ assertAttr e "value" (Just "two")
618+ WD.click =<< findElemWithRetry (WD.ByTag "button")
619+ readRef inputRef `shouldBeWithRetryM` "two"
620+ testWidget (pure ()) checkValue $ do
621+ (e, ()) <- selectElement def options
622+ click <- button "save"
623+ performEvent_ $ liftIO . writeRef inputRef <$> tag (current (_selectElement_value e)) click
624+ it "sets focus appropriately" $ runWD $ do
625+ focusRef <- newRef False
626+ let checkValue = do
627+ readRef focusRef `shouldBeWithRetryM` False
628+ e <- findElemWithRetry $ WD.ByTag "select"
629+ WD.click e
630+ readRef focusRef `shouldBeWithRetryM` True
631+ testWidget (pure ()) checkValue $ do
632+ (e, ()) <- selectElement def options
633+ performEvent_ $ liftIO . writeRef focusRef <$> updated (_selectElement_hasFocus e)
634+ it "sets focus when focus occurs before hydration" $ runWD $ do
635+ focusRef <- newRef False
636+ let setup = do
637+ e <- findElemWithRetry $ WD.ByTag "select"
638+ WD.click e
639+ ((== e) <$> WD.activeElem) `shouldBeWithRetryM` True
640+ readRef focusRef `shouldBeWithRetryM` False
641+ check = readRef focusRef `shouldBeWithRetryM` True
642+ testWidget setup check $ do
643+ (e, ()) <- selectElement def options
644+ performEvent_ $ liftIO . writeRef focusRef <$> updated (_selectElement_hasFocus e)
645+ it "sets value appropriately" $ runWD $ do
646+ valueByUIRef <- newRef ("" :: Text)
647+ valueRef <- newRef ("" :: Text)
648+ setValueChan :: Chan Text <- liftIO newChan
649+ let checkValue = do
650+ e <- findElemWithRetry $ WD.ByTag "select"
651+ assertAttr e "value" (Just "one")
652+ readRef valueByUIRef `shouldBeWithRetryM` "one"
653+ readRef valueRef `shouldBeWithRetryM` "one"
654+ WD.click <=< findElemWithRetry $ WD.ById "two"
655+ readRef valueByUIRef `shouldBeWithRetryM` "two"
656+ readRef valueRef `shouldBeWithRetryM` "two"
657+ liftIO $ writeChan setValueChan "three"
658+ readRef valueByUIRef `shouldBeWithRetryM` "two"
659+ readRef valueRef `shouldBeWithRetryM` "three"
660+ testWidget (pure ()) checkValue $ do
661+ setValue' <- triggerEventWithChan setValueChan
662+ (e, ()) <- selectElement def { _selectElementConfig_setValue = Just setValue' } options
663+ performEvent_ $ liftIO . writeRef valueByUIRef <$> _selectElement_change e
664+ performEvent_ $ liftIO . writeRef valueRef <$> updated (_selectElement_value e)
665+
666+ describe "hydration/immediate" $ session' $ do
667+ it "captures user input after switchover" $ runWD $ do
668+ inputRef :: IORef Text <- newRef ""
669+ let checkValue = do
670+ WD.click <=< findElemWithRetry $ WD.ById "two"
671+ WD.click <=< findElemWithRetry $ WD.ByTag "button"
672+ readRef inputRef `shouldBeWithRetryM` "two"
673+ testWidget (pure ()) checkValue $ prerender_ (pure ()) $ do
674+ (e, ()) <- selectElement def options
675+ click <- button "save"
676+ performEvent_ $ liftIO . writeRef inputRef <$> tag (current (_selectElement_value e)) click
677+ it "sets focus appropriately" $ runWD $ do
678+ focusRef <- newRef False
679+ let checkValue = do
680+ readRef focusRef `shouldBeWithRetryM` False
681+ e <- findElemWithRetry $ WD.ByTag "select"
682+ WD.click e
683+ readRef focusRef `shouldBeWithRetryM` True
684+ testWidget (pure ()) checkValue $ prerender_ (pure ()) $ do
685+ (e, ()) <- selectElement def options
686+ performEvent_ $ liftIO . writeRef focusRef <$> updated (_selectElement_hasFocus e)
687+ it "has correct initial value" $ runWD $ do
688+ valueRef :: IORef Text <- newRef ""
689+ let checkValue = readRef valueRef `shouldBeWithRetryM` "one"
690+ testWidget (pure ()) checkValue $ do
691+ prerender_ (pure ()) $ do
692+ (e, ()) <- selectElement def { _selectElementConfig_initialValue = "one" } options
693+ liftIO . writeRef valueRef <=< sample $ current $ _selectElement_value e
694+ it "sets value appropriately" $ runWD $ do
695+ valueByUIRef :: IORef Text <- newRef ""
696+ valueRef :: IORef Text <- newRef ""
697+ setValueChan :: Chan Text <- liftIO newChan
698+ let checkValue = do
699+ WD.click =<< findElemWithRetry (WD.ById "two")
700+ readRef valueByUIRef `shouldBeWithRetryM` "two"
701+ readRef valueRef `shouldBeWithRetryM` "two"
702+ liftIO $ writeChan setValueChan "three"
703+ readRef valueByUIRef `shouldBeWithRetryM` "two"
704+ readRef valueRef `shouldBeWithRetryM` "three"
705+ testWidget (pure ()) checkValue $ do
706+ setValue' <- triggerEventWithChan setValueChan
707+ prerender_ (pure ()) $ do
708+ (e, ()) <- selectElement def { _selectElementConfig_setValue = Just setValue' } options
709+ performEvent_ $ liftIO . writeRef valueByUIRef <$> _selectElement_change e
710+ performEvent_ $ liftIO . writeRef valueRef <$> updated (_selectElement_value e)
711+
712+ describe "prerender" $ session' $ do
713+ it "works in simple case" $ runWD $ do
714+ testWidget (checkBodyText "One") (checkBodyText "Two") $ do
715+ prerender_ (text "One") (text "Two")
716+ it "removes element correctly" $ runWD $ do
717+ testWidget' (findElemWithRetry $ WD.ByTag "span") elementShouldBeRemoved $ do
718+ prerender_ (el "span" $ text "One") (text "Two")
719+ it "can be nested in server widget" $ runWD $ do
720+ testWidget (checkBodyText "One") (checkBodyText "Three") $ do
721+ prerender_ (prerender_ (text "One") (text "Two")) (text "Three")
722+ it "can be nested in client widget" $ runWD $ do
723+ testWidget (checkBodyText "One") (checkBodyText "Three") $ do
724+ prerender_ (text "One") (prerender_ (text "Two") (text "Three"))
725+ it "works with prerender_ siblings" $ runWD $ do
726+ testWidget
727+ (checkTextInId "a1" "One" >> checkTextInId "b1" "Three" >> checkTextInId "c1" "Five")
728+ (checkTextInId "a2" "Two" >> checkTextInId "b2" "Four" >> checkTextInId "c2" "Six") $ do
729+ prerender_ (divId "a1" $ text "One") (divId "a2" $ text "Two")
730+ prerender_ (divId "b1" $ text "Three") (divId "b2" $ text "Four")
731+ prerender_ (divId "c1" $ text "Five") (divId "c2" $ text "Six")
732+ it "works inside other element" $ runWD $ do
733+ testWidget (checkTextInTag "div" "One") (checkTextInTag "div" "Two") $ do
734+ el "div" $ prerender_ (text "One") (text "Two")
735+-- TODO re-enable this
736+-- it "places fences and removes them" $ runWD $ do
737+-- testWidget'
738+-- (do
739+-- scripts <- WD.findElems $ WD.ByTag "script"
740+-- filterM (\s -> maybe False (\t -> "prerender" `T.isPrefixOf` t) <$> WD.attr s "type") scripts
741+-- )
742+-- (traverse_ elementShouldBeRemoved)
743+-- (el "span" $ prerender_ (text "One") (text "Two"))
744+ it "postBuild works on server side" $ runWD $ do
745+ lock :: MVar () <- liftIO newEmptyMVar
746+ testWidget (liftIO $ takeMVar lock) (pure ()) $ do
747+ prerender_ (do
748+ pb <- getPostBuild
749+ performEvent_ $ liftIO (putMVar lock ()) <$ pb) blank
750+ it "postBuild works on client side" $ runWD $ do
751+ lock :: MVar () <- liftIO newEmptyMVar
752+ testWidget (pure ()) (liftIO $ takeMVar lock) $ do
753+ prerender_ blank $ do
754+ pb <- getPostBuild
755+ performEvent_ $ liftIO (putMVar lock ()) <$ pb
756+ it "result Dynamic is updated *after* switchover" $ runWD $ do
757+ let static = checkBodyText "PostBuild"
758+ check = checkBodyText "Client"
759+ testWidget static check $ void $ do
760+ d <- prerender (pure "Initial") (pure "Client")
761+ pb <- getPostBuild
762+ initial <- sample $ current d
763+ textNode $ TextNodeConfig initial $ Just $ leftmost [updated d, "PostBuild" <$ pb]
764+ -- This essentially checks that the client IO runs *after* switchover/postBuild,
765+ -- thus can't create conflicting DOM
766+ it "can't exploit IO to break hydration" $ runWD $ do
767+ let static = checkBodyText "Initial"
768+ testWidgetStatic static $ void $ do
769+ ref <- liftIO $ newRef "Initial"
770+ prerender_ (pure ()) (liftIO $ writeRef ref "Client")
771+ text <=< liftIO $ readRef ref
772+ -- As above, so below
773+ it "can't exploit triggerEvent to break hydration" $ runWD $ do
774+ let static = checkBodyText "Initial"
775+ check = checkBodyText "Client"
776+ testWidget static check $ void $ do
777+ (e, trigger) <- newTriggerEvent
778+ prerender_ (pure ()) (liftIO $ trigger "Client")
779+ textNode $ TextNodeConfig "Initial" $ Just e
780+
781+ describe "runWithReplace" $ session' $ do
782+ it "works" $ runWD $ do
783+ replaceChan :: Chan Text <- liftIO newChan
784+ let setup = findElemWithRetry $ WD.ByTag "div"
785+ check ssr = do
786+ -- Check that the original element still exists and has the correct text
787+ shouldContainText "two" ssr
788+ liftIO $ writeChan replaceChan "three"
789+ elementShouldBeRemoved ssr
790+ shouldContainText "three" =<< findElemWithRetry (WD.ByTag "span")
791+ testWidget' setup check $ do
792+ replace <- triggerEventWithChan replaceChan
793+ pb <- getPostBuild
794+ void $ runWithReplace (el "div" $ text "one") $ leftmost
795+ [ el "div" (text "two") <$ pb
796+ , el "span" . text <$> replace
797+ ]
798+ it "can be nested in initial widget" $ runWD $ do
799+ replaceChan :: Chan Text <- liftIO newChan
800+ let setup = findElemWithRetry $ WD.ByTag "div"
801+ check ssr = do
802+ -- Check that the original element still exists and has the correct text
803+ shouldContainText "two" ssr
804+ liftIO $ writeChan replaceChan "three"
805+ elementShouldBeRemoved ssr
806+ shouldContainText "three" =<< findElemWithRetry (WD.ByTag "span")
807+ testWidget' setup check $ void $ flip runWithReplace never $ do
808+ replace <- triggerEventWithChan replaceChan
809+ pb <- getPostBuild
810+ void $ runWithReplace (el "div" $ text "one") $ leftmost
811+ [ el "div" (text "two") <$ pb
812+ , el "span" . text <$> replace
813+ ]
814+ it "can be nested in postBuild widget" $ runWD $ do
815+ replaceChan <- liftIO newChan
816+ let setup = findElemWithRetry $ WD.ByTag "div"
817+ check ssr = do
818+ -- Check that the original element still exists and has the correct text
819+ shouldContainText "two" ssr
820+ liftIO $ writeChan replaceChan "three"
821+ elementShouldBeRemoved ssr
822+ shouldContainText "three" =<< findElemWithRetry (WD.ByTag "span")
823+ testWidget' setup check $ void $ do
824+ pb <- getPostBuild
825+ runWithReplace (pure ()) $ ffor pb $ \() -> do
826+ replace <- triggerEventWithChan replaceChan
827+ pb' <- getPostBuild
828+ void $ runWithReplace (el "div" $ text "one") $ leftmost
829+ [ el "div" (text "two") <$ pb'
830+ , el "span" . text <$> replace
831+ ]
832+ it "postBuild works in replaced widget" $ runWD $ do
833+ replaceChan <- liftIO newChan
834+ pbLock <- liftIO newEmptyMVar
835+ let check = liftIO $ do
836+ writeChan replaceChan ()
837+ takeMVar pbLock
838+ testWidget (pure ()) check $ void $ do
839+ replace <- triggerEventWithChan replaceChan
840+ runWithReplace (pure ()) $ ffor replace $ \() -> do
841+ pb <- getPostBuild
842+ performEvent_ $ liftIO (putMVar pbLock ()) <$ pb
843+ it "can be nested in event widget" $ runWD $ do
844+ replaceChan1 :: Chan Text <- liftIO newChan
845+ replaceChan2 :: Chan Text <- liftIO newChan
846+ lock :: MVar () <- liftIO newEmptyMVar
847+ let check = do
848+ shouldContainText "" =<< findElemWithRetry (WD.ByTag "body")
849+ liftIO $ do
850+ writeChan replaceChan1 "one"
851+ takeMVar lock
852+ one <- findElemWithRetry $ WD.ByTag "div"
853+ shouldContainText "pb" one
854+ liftIO $ writeChan replaceChan2 "two"
855+ elementShouldBeRemoved one
856+ shouldContainText "two" =<< findElemWithRetry (WD.ByTag "span")
857+ testWidget (pure ()) check $ void $ do
858+ replace1 <- triggerEventWithChan replaceChan1
859+ runWithReplace (pure ()) $ ffor replace1 $ \r1 -> do
860+ replace2 <- triggerEventWithChan replaceChan2
861+ pb <- getPostBuild
862+ performEvent_ $ liftIO (putMVar lock ()) <$ pb
863+ void $ runWithReplace (el "div" $ text r1) $ leftmost
864+ [ el "span" . text <$> replace2
865+ , el "div" (text "pb") <$ pb
866+ ]
867+ it "prerender works in replaced widget" $ runWD $ do
868+ replaceChan <- liftIO newChan
869+ lock :: MVar () <- liftIO newEmptyMVar
870+ pbLock :: MVar () <- liftIO newEmptyMVar
871+ let check = do
872+ liftIO $ do
873+ writeChan replaceChan ()
874+ takeMVar lock
875+ takeMVar pbLock
876+ testWidget (pure ()) check $ void $ do
877+ replace <- triggerEventWithChan replaceChan
878+ runWithReplace (pure ()) $ ffor replace $ \() -> do
879+ prerender_ (pure ()) $ do
880+ liftIO $ putMVar lock ()
881+ pb <- getPostBuild
882+ performEvent_ $ liftIO (putMVar pbLock ()) <$ pb
883+ it "prerender returns in immediate mode" $ runWD $ do
884+ replaceChan <- liftIO newChan
885+ pbLock <- liftIO newEmptyMVar
886+ let check = liftIO $ do
887+ writeChan replaceChan ()
888+ takeMVar pbLock
889+ testWidget (pure ()) check $ void $ do
890+ replace <- triggerEventWithChan replaceChan
891+ runWithReplace (pure ()) $ ffor replace $ \() -> do
892+ prerender_ (pure ()) (pure ())
893+ liftIO $ putMVar pbLock ()
894+ it "works in immediate mode (RHS of prerender)" $ runWD $ do
895+ replaceChan :: Chan Text <- liftIO newChan
896+ let check = do
897+ checkBodyText "one"
898+ one <- findElemWithRetry $ WD.ByTag "div"
899+ shouldContainText "one" one
900+ liftIO $ writeChan replaceChan "pb"
901+ elementShouldBeRemoved one
902+ shouldContainText "pb" =<< findElemWithRetry (WD.ByTag "span")
903+ testWidget (pure ()) check $ void $ do
904+ replace <- triggerEventWithChan replaceChan
905+ prerender_ blank $ do
906+ void $ runWithReplace (el "div" $ text "one") $ el "span" . text <$> replace
907+ it "works with postBuild in immediate mode (RHS of prerender)" $ runWD $ do
908+ replaceChan :: Chan Text <- liftIO newChan
909+ let check = do
910+ checkBodyText "two"
911+ two <- findElemWithRetry $ WD.ByTag "div"
912+ shouldContainText "two" two
913+ liftIO $ writeChan replaceChan "three"
914+ elementShouldBeRemoved two
915+ shouldContainText "three" =<< findElemWithRetry (WD.ByTag "span")
916+ testWidget (pure ()) check $ void $ do
917+ replace <- triggerEventWithChan replaceChan
918+ prerender_ blank $ do
919+ pb <- getPostBuild
920+ void $ runWithReplace (el "div" $ text "one") $ leftmost
921+ [ el "div" (text "two") <$ pb
922+ , el "span" . text <$> replace
923+ ]
924+
925+ describe "traverseDMapWithKeyWithAdjust" $ session' $ do
926+ let widget :: DomBuilder t m => DKey a -> Identity a -> m (Identity a)
927+ widget k (Identity v) = elAttr "li" ("id" =: textKey k) $ do
928+ elClass "span" "key" $ text $ textKey k
929+ elClass "span" "value" $ text $ T.pack $ has @Show k $ show v
930+ pure (Identity v)
931+ checkItem :: WD.Element -> Text -> Text -> WD ()
932+ checkItem li k v = do
933+ putStrLnDebug "checkItem"
934+ shouldContainTextNoRetry k =<< WD.findElemFrom li (WD.ByClass "key")
935+ shouldContainTextNoRetry v =<< WD.findElemFrom li (WD.ByClass "value")
936+ checkInitialItems dm xs = do
937+ putStrLnDebug "checkInitialItems"
938+ liftIO $ assertEqual "Wrong amount of items in DOM" (DMap.size dm) (length xs)
939+ forM_ (zip xs (DMap.toList dm)) $ \(e, k :=> Identity v) -> checkItem e (textKey k) (T.pack $ has @Show k $ show v)
940+ getAndCheckInitialItems dm = withRetry $ do
941+ putStrLnDebug "getAndCheckInitialItems"
942+ xs <- WD.findElems (WD.ByTag "li")
943+ checkInitialItems dm xs
944+ pure xs
945+ checkRemoval chan k = do
946+ putStrLnDebug "checkRemoval"
947+ e <- findElemWithRetry (WD.ById $ textKey k)
948+ liftIO $ writeChan chan $ PatchDMap $ DMap.singleton k (ComposeMaybe Nothing)
949+ elementShouldBeRemoved e
950+ checkReplace chan k v = do
951+ putStrLnDebug "checkReplace"
952+ e <- findElemWithRetry (WD.ById $ textKey k)
953+ liftIO $ writeChan chan $ PatchDMap $ DMap.singleton k (ComposeMaybe $ Just $ Identity v)
954+ elementShouldBeRemoved e
955+ e' <- findElemWithRetry $ WD.ById $ textKey k
956+ checkItem e' (textKey k) (T.pack $ show v)
957+ checkInsert chan k v = do
958+ putStrLnDebug "checkInsert"
959+ liftIO $ writeChan chan $ PatchDMap $ DMap.singleton k (ComposeMaybe $ Just $ Identity v)
960+ e <- findElemWithRetry (WD.ById $ textKey k)
961+ checkItem e (textKey k) (T.pack $ show v)
962+ postBuildPatch = PatchDMap $ DMap.fromList [Key_Char :=> ComposeMaybe Nothing, Key_Bool :=> ComposeMaybe (Just $ Identity True)]
963+ it "doesn't replace elements at switchover, can delete/update/insert" $ runWD $ do
964+ chan <- liftIO newChan
965+ let static :: WD [WD.Element]
966+ static = getAndCheckInitialItems keyMap
967+ check :: [WD.Element] -> WD ()
968+ check xs = do
969+ checkInitialItems keyMap xs
970+ checkRemoval chan Key_Int
971+ checkReplace chan Key_Char 'B'
972+ checkInsert chan Key_Bool True
973+ testWidget' static check $ do
974+ (dmap, _evt) <- traverseDMapWithKeyWithAdjust widget keyMap =<< triggerEventWithChan chan
975+ liftIO $ dmap `H.shouldBe` keyMap
976+ it "handles postBuild correctly" $ runWD $ do
977+ chan <- liftIO newChan
978+ let static = getAndCheckInitialItems $ applyAlways postBuildPatch keyMap
979+ check xs = do
980+ withRetry $ checkInitialItems (applyAlways postBuildPatch keyMap) xs
981+ checkRemoval chan Key_Int
982+ checkInsert chan Key_Char 'B'
983+ checkReplace chan Key_Bool True
984+ testWidget' static check $ void $ do
985+ pb <- getPostBuild
986+ replace <- triggerEventWithChan chan
987+ (dmap, _evt) <- traverseDMapWithKeyWithAdjust widget keyMap $ leftmost [postBuildPatch <$ pb, replace]
988+ liftIO $ dmap `H.shouldBe` keyMap
989+ it "can delete/update/insert when built in prerender" $ runWD $ do
990+ chan <- liftIO newChan
991+ let check = do
992+ _ <- getAndCheckInitialItems keyMap
993+ checkRemoval chan Key_Int
994+ checkReplace chan Key_Char 'B'
995+ checkInsert chan Key_Bool True
996+ testWidget (pure ()) check $ do
997+ replace <- triggerEventWithChan chan
998+ prerender_ (pure ()) $ do
999+ (dmap, _evt) <- traverseDMapWithKeyWithAdjust widget keyMap replace
1000+ liftIO $ dmap `H.shouldBe` keyMap
1001+ it "can delete/update/insert when built in immediate mode" $ runWD $ do
1002+ chan <- liftIO newChan
1003+ let check = do
1004+ _ <- getAndCheckInitialItems keyMap
1005+ checkRemoval chan Key_Int
1006+ checkReplace chan Key_Char 'B'
1007+ checkInsert chan Key_Bool True
1008+ testWidget (pure ()) check $ void $ do
1009+ pb <- getPostBuild
1010+ runWithReplace (pure ()) $ ffor pb $ \() -> void $ do
1011+ (dmap, _evt) <- traverseDMapWithKeyWithAdjust widget keyMap =<< triggerEventWithChan chan
1012+ liftIO $ dmap `H.shouldBe` keyMap
1013+ -- Should be fixed by prerender changes!
1014+ it "handles postBuild correctly in prerender" $ runWD $ do
1015+ chan <- liftIO newChan
1016+ let check = do
1017+ _ <- getAndCheckInitialItems $ applyAlways postBuildPatch keyMap
1018+ checkRemoval chan Key_Int
1019+ checkInsert chan Key_Char 'B'
1020+ checkReplace chan Key_Bool True
1021+ testWidget (pure ()) check $ do
1022+ replace <- triggerEventWithChan chan
1023+ prerender_ (pure ()) $ void $ do
1024+ pb <- getPostBuild
1025+ (dmap, _evt) <- traverseDMapWithKeyWithAdjust widget keyMap $ leftmost [postBuildPatch <$ pb, replace]
1026+ liftIO $ dmap `H.shouldBe` keyMap
1027+
1028+ describe "traverseIntMapWithKeyWithAdjust" $ session' $ do
1029+ let textKeyInt k = "key" <> T.pack (show k)
1030+ intMap = IntMap.fromList
1031+ [ (1, "one")
1032+ , (2, "two")
1033+ , (3, "three")
1034+ ]
1035+ let widget :: DomBuilder t m => IntMap.Key -> Text -> m Text
1036+ widget k v = elAttr "li" ("id" =: textKeyInt k) $ do
1037+ elClass "span" "key" $ text $ textKeyInt k
1038+ elClass "span" "value" $ text v
1039+ pure v
1040+ checkItem :: WD.Element -> Text -> Text -> WD ()
1041+ checkItem li k v = do
1042+ putStrLnDebug "checkItem"
1043+ shouldContainTextNoRetry k =<< WD.findElemFrom li (WD.ByClass "key")
1044+ shouldContainTextNoRetry v =<< WD.findElemFrom li (WD.ByClass "value")
1045+ checkInitialItems dm xs = do
1046+ putStrLnDebug "checkInitialItems"
1047+ liftIO $ assertEqual "Wrong amount of items in DOM" (IntMap.size dm) (length xs)
1048+ forM_ (zip xs (IntMap.toList dm)) $ \(e, (k, v)) -> checkItem e (textKeyInt k) v
1049+ getAndCheckInitialItems dm = withRetry $ do
1050+ putStrLnDebug "getAndCheckInitialItems"
1051+ xs <- WD.findElems (WD.ByTag "li")
1052+ checkInitialItems dm xs
1053+ pure xs
1054+ checkRemoval chan k = do
1055+ putStrLnDebug "checkRemoval"
1056+ e <- findElemWithRetry (WD.ById $ textKeyInt k)
1057+ liftIO $ writeChan chan $ PatchIntMap $ IntMap.singleton k Nothing
1058+ elementShouldBeRemoved e
1059+ checkReplace chan k v = do
1060+ putStrLnDebug "checkReplace"
1061+ e <- findElemWithRetry (WD.ById $ textKeyInt k)
1062+ liftIO $ writeChan chan $ PatchIntMap $ IntMap.singleton k $ Just v
1063+ elementShouldBeRemoved e
1064+ e' <- findElemWithRetry (WD.ById $ textKeyInt k)
1065+ withRetry $ checkItem e' (textKeyInt k) v
1066+ checkInsert chan k v = do
1067+ putStrLnDebug "checkInsert"
1068+ liftIO $ writeChan chan $ PatchIntMap $ IntMap.singleton k $ Just v
1069+ e <- findElemWithRetry (WD.ById $ textKeyInt k)
1070+ checkItem e (textKeyInt k) v
1071+ postBuildPatch = PatchIntMap $ IntMap.fromList [(2, Nothing), (3, Just "trois"), (4, Just "four")]
1072+ xit "doesn't replace elements at switchover, can delete/update/insert" $ runWD $ do
1073+ chan <- liftIO newChan
1074+ let static = getAndCheckInitialItems intMap
1075+ check xs = do
1076+ checkInitialItems intMap xs
1077+ checkRemoval chan 1
1078+ checkReplace chan 2 "deux"
1079+ checkInsert chan 4 "four"
1080+ testWidget' static check $ void $ do
1081+ (im, _evt) <- traverseIntMapWithKeyWithAdjust widget intMap =<< triggerEventWithChan chan
1082+ liftIO $ im `H.shouldBe` intMap
1083+ xit "handles postBuild correctly" $ runWD $ do
1084+ chan <- liftIO newChan
1085+ let static = getAndCheckInitialItems $ applyAlways postBuildPatch intMap
1086+ check xs = do
1087+ withRetry $ checkInitialItems (applyAlways postBuildPatch intMap) xs
1088+ checkRemoval chan 1
1089+ checkInsert chan 2 "deux"
1090+ checkReplace chan 3 "trois"
1091+ testWidget' static check $ void $ do
1092+ pb <- getPostBuild
1093+ replace <- triggerEventWithChan chan
1094+ (dmap, _evt) <- traverseIntMapWithKeyWithAdjust widget intMap $ leftmost [postBuildPatch <$ pb, replace]
1095+ liftIO $ dmap `H.shouldBe` intMap
1096+ xit "can delete/update/insert when built in prerender" $ runWD $ do
1097+ chan <- liftIO newChan
1098+ let check = do
1099+ _ <- getAndCheckInitialItems intMap
1100+ checkRemoval chan 1
1101+ checkReplace chan 2 "deux"
1102+ checkInsert chan 4 "quatre"
1103+ testWidget (pure ()) check $ do
1104+ replace <- triggerEventWithChan chan
1105+ prerender_ (pure ()) $ do
1106+ (dmap, _evt) <- traverseIntMapWithKeyWithAdjust widget intMap replace
1107+ liftIO $ dmap `H.shouldBe` intMap
1108+ xit "can delete/update/insert when built in immediate mode" $ runWD $ do
1109+ chan <- liftIO newChan
1110+ let check = do
1111+ _ <- getAndCheckInitialItems intMap
1112+ checkRemoval chan 1
1113+ checkReplace chan 2 "deux"
1114+ checkInsert chan 4 "quatre"
1115+ testWidget (pure ()) check $ void $ do
1116+ pb <- getPostBuild
1117+ runWithReplace (pure ()) $ ffor pb $ \() -> void $ do
1118+ (dmap, _evt) <- traverseIntMapWithKeyWithAdjust widget intMap =<< triggerEventWithChan chan
1119+ liftIO $ dmap `H.shouldBe` intMap
1120+ -- Should be fixed by prerender changes!
1121+ xit "handles postBuild correctly in prerender" $ runWD $ do
1122+ chan <- liftIO newChan
1123+ let check = do
1124+ _ <- getAndCheckInitialItems $ applyAlways postBuildPatch intMap
1125+ checkRemoval chan 1
1126+ checkInsert chan 2 "deux"
1127+ checkReplace chan 3 "trois"
1128+ testWidget (pure ()) check $ do
1129+ replace <- triggerEventWithChan chan
1130+ prerender_ (pure ()) $ void $ do
1131+ pb <- getPostBuild
1132+ (dmap, _evt) <- traverseIntMapWithKeyWithAdjust widget intMap $ leftmost [postBuildPatch <$ pb, replace]
1133+ liftIO $ dmap `H.shouldBe` intMap
1134+
1135+ describe "traverseDMapWithKeyWithAdjustWithMove" $ session' $ do
1136+ let widget :: DomBuilder t m => Key2 a -> Identity a -> m (Identity a)
1137+ widget k (Identity v) = elAttr "li" ("id" =: textKey2 k) $ do
1138+ elClass "span" "key" $ text $ textKey2 k
1139+ elClass "span" "value" $ text $ T.pack $ has @Show k $ show v
1140+ pure (Identity v)
1141+ textKey2 :: Key2 a -> Text
1142+ textKey2 = \case
1143+ Key2_Int i -> "i" <> T.pack (show i)
1144+ Key2_Char c -> "c" <> T.pack [c]
1145+ checkItem :: WD.Element -> Text -> Text -> WD ()
1146+ checkItem li k v = do
1147+ shouldContainTextNoRetry k =<< WD.findElemFrom li (WD.ByClass "key")
1148+ shouldContainTextNoRetry v =<< WD.findElemFrom li (WD.ByClass "value")
1149+ checkInitialItems dm xs = do
1150+ liftIO $ assertEqual "Wrong amount of items in DOM" (DMap.size dm) (length xs)
1151+ forM_ (zip xs (DMap.toList dm)) $ \(e, k :=> Identity v) -> checkItem e (textKey2 k) (T.pack $ has @Show k $ show v)
1152+ getAndCheckInitialItems dm = withRetry $ do
1153+ xs <- WD.findElems (WD.ByTag "li")
1154+ checkInitialItems dm xs
1155+ pure xs
1156+ moveSpec testMove = do
1157+ it "can insert an item" $ runWD $ testMove (DMap.fromList [Key2_Int 1 ==> 1, Key2_Int 3 ==> 3]) $ \body chan -> do
1158+ shouldContainText (T.strip $ T.unlines ["i11","i33"]) body
1159+ liftIO $ writeChan chan (insertDMapKey (Key2_Int 2) 2)
1160+ shouldContainText (T.strip $ T.unlines ["i11","i22","i33"]) body
1161+ it "can delete an item" $ runWD $ testMove (DMap.fromList [Key2_Int 1 ==> 1, Key2_Int 2 ==> 2, Key2_Int 3 ==> 3]) $ \body chan -> do
1162+ shouldContainText (T.strip $ T.unlines ["i11","i22","i33"]) body
1163+ liftIO $ writeChan chan (deleteDMapKey (Key2_Int 2))
1164+ shouldContainText (T.strip $ T.unlines ["i11","i33"]) body
1165+ it "can swap items" $ runWD $ testMove (DMap.fromList [Key2_Int 1 ==> 1, Key2_Int 2 ==> 2, Key2_Int 3 ==> 3, Key2_Int 4 ==> 4]) $ \body chan -> do
1166+ shouldContainText (T.strip $ T.unlines ["i11","i22","i33","i44"]) body
1167+ liftIO $ writeChan chan (swapDMapKey (Key2_Int 2) (Key2_Int 3))
1168+ shouldContainText (T.strip $ T.unlines ["i11","i33","i22","i44"]) body
1169+-- TODO re-enable this
1170+-- it "can move items" $ runWD $ testMove (DMap.fromList [Key2_Int 1 ==> 1, Key2_Int 2 ==> 2, Key2_Int 3 ==> 3, Key2_Int 4 ==> 4]) $ \body chan -> do
1171+-- shouldContainText (T.strip $ T.unlines ["i11","i22","i33", "i44"]) body
1172+-- liftIO $ writeChan chan (moveDMapKey (Key2_Int 2) (Key2_Int 3))
1173+-- shouldContainText (T.strip $ T.unlines ["i11","i22","i44"]) body
1174+-- liftIO $ writeChan chan (moveDMapKey (Key2_Int 2) (Key2_Int 3)) -- attempt a move to nonexistent key should delete
1175+-- shouldContainText (T.strip $ T.unlines ["i11","i44"]) body
1176+-- liftIO $ writeChan chan (moveDMapKey (Key2_Int 2) (Key2_Int 3)) -- this causes a JSException in immediate builder too
1177+-- shouldContainText (T.strip $ T.unlines ["i11","i44"]) body
1178+-- liftIO $ writeChan chan (insertDMapKey (Key2_Int 2) 2) -- this step will fail if the above caused an exception, thus works as a proxy for detecting the exception given we can't catch it
1179+-- shouldContainText (T.strip $ T.unlines ["i11","i22","i44"]) body
1180+
1181+ describe "hydration" $ moveSpec $ \initMap test -> do
1182+ chan <- liftIO newChan
1183+ let static = getAndCheckInitialItems initMap
1184+ check xs = do
1185+ checkInitialItems initMap xs
1186+ body <- getBody
1187+ test body chan
1188+ testWidget' static check $ void $ do
1189+ (dmap, _evt) <- traverseDMapWithKeyWithAdjustWithMove widget initMap =<< triggerEventWithChan chan
1190+ liftIO $ assertEqual "DMap" initMap dmap
1191+
1192+ describe "hydration/immediate" $ moveSpec $ \initMap test -> do
1193+ chan <- liftIO newChan
1194+ replace <- liftIO newChan
1195+ lock <- liftIO newEmptyMVar
1196+ let check = do
1197+ liftIO $ do
1198+ writeChan replace ()
1199+ takeMVar lock
1200+ xs <- getAndCheckInitialItems initMap
1201+ checkInitialItems initMap xs
1202+ body <- getBody
1203+ test body chan
1204+ testWidget (pure ()) check $ void $ do
1205+ e <- triggerEventWithChan replace
1206+ runWithReplace (pure ()) $ ffor e $ \() -> void $ do
1207+ pb <- getPostBuild
1208+ performEvent_ $ liftIO (putMVar lock ()) <$ pb
1209+ (dmap, _evt) <- traverseDMapWithKeyWithAdjustWithMove widget initMap =<< triggerEventWithChan chan
1210+ liftIO $ assertEqual "DMap" initMap dmap
1211+
1212+ describe "hydrating invalid HTML" $ session' $ do
1213+ it "can hydrate list in paragraph" $ runWD $ do
1214+ let static = do
1215+ checkBodyText "before\ninner\nafter"
1216+ -- Two <p> tags should be present
1217+ [p1, p2] <- WD.findElems (WD.ByTag "p")
1218+ ol <- findElemWithRetry (WD.ByTag "ol")
1219+ shouldContainText "before" p1
1220+ shouldContainText "inner" ol
1221+ shouldContainText "" p2
1222+ pure (p1, ol, p2)
1223+ check (p1, ol, p2) = do
1224+ checkBodyText "before\ninner\nafter"
1225+ shouldContainText "before\ninner\nafter" p1
1226+ elementShouldBeRemoved ol
1227+ elementShouldBeRemoved p2
1228+ testWidget' static check $ do
1229+ -- This is deliberately invalid HTML, the browser will interpret it as
1230+ -- <p>before</p><ol>inner</ol>after<p></p>
1231+ el "p" $ do
1232+ text "before"
1233+ el "ol" $ text "inner"
1234+ text "after"
1235+
1236+data Selenium = Selenium
1237+ { _selenium_portNumber :: PortNumber
1238+ , _selenium_stopServer :: IO ()
1239+ }
1240+
1241+startSeleniumServer :: PortNumber -> IO (IO ())
1242+startSeleniumServer port = do
1243+ (_,_,_,ph) <- createProcess $ (proc "selenium-server" ["-port", show port])
1244+ { std_in = NoStream
1245+ , std_out = NoStream
1246+ , std_err = NoStream
1247+ }
1248+ return $ terminateProcess ph
1249+
1250+withSeleniumServer :: (Selenium -> IO ()) -> IO ()
1251+withSeleniumServer f = do
1252+ stopServer <- startSeleniumServer seleniumPort
1253+ threadDelay $ 1000 * 1000 * 2 -- TODO poll or wait on a a signal to block on
1254+ f $ Selenium
1255+ { _selenium_portNumber = seleniumPort
1256+ , _selenium_stopServer = stopServer
1257+ }
1258+
1259+triggerEventWithChan :: (Reflex t, TriggerEvent t m, Prerender js t m) => Chan a -> m (Event t a)
1260+triggerEventWithChan chan = do
1261+ (e, trigger) <- newTriggerEvent
1262+ -- In prerender because we only want to do this on the client
1263+ prerender_ (pure ()) $ void $ liftIO $ forkIO $ forever $ trigger =<< readChan chan
1264+ pure e
1265+
1266+shouldBeWithRetryM :: (Eq a, Show a) => WD a -> a -> WD ()
1267+shouldBeWithRetryM m expected = withRetry $ do
1268+ got <- m
1269+ got `shouldBe` expected
1270+
1271+assertAttr :: WD.Element -> Text -> Maybe Text -> WD ()
1272+assertAttr e k v = liftIO . assertEqual "Incorrect attribute value" v =<< WD.attr e k
1273+
1274+elementShouldBeRemoved :: WD.Element -> WD ()
1275+elementShouldBeRemoved e = withRetry $ do
1276+ try (WD.getText e) >>= \case
1277+ Left (WD.FailedCommand WD.StaleElementReference _) -> return ()
1278+ Left err -> throwM err
1279+ Right !_ -> liftIO $ assertFailure "Expected element to be removed, but it still exists"
1280+
1281+shouldContainText :: Text -> WD.Element -> WD ()
1282+shouldContainText t = withRetry . shouldContainTextNoRetry t
1283+
1284+shouldContainTextNoRetry :: Text -> WD.Element -> WD ()
1285+shouldContainTextNoRetry t = flip shouldBe t <=< WD.getText
1286+
1287+checkBodyText :: Text -> WD ()
1288+checkBodyText = checkTextInTag "body"
1289+
1290+checkTextInTag :: Text -> Text -> WD ()
1291+checkTextInTag t expected = do
1292+ e <- findElemWithRetry (WD.ByTag t)
1293+ shouldContainText expected e
1294+
1295+checkTextInId :: Text -> Text -> WD ()
1296+checkTextInId i expected = do
1297+ e <- findElemWithRetry (WD.ById i)
1298+ shouldContainText expected e
1299+
1300+findElemWithRetry :: Selector -> WD WD.Element
1301+findElemWithRetry = withRetry . WD.findElem
1302+
1303+getBody :: WD WD.Element
1304+getBody = WD.findElem $ WD.ByTag "body"
1305+
1306+withRetry :: forall a. WD a -> WD a
1307+withRetry a = wait 300
1308+ where wait :: Int -> WD a
1309+ wait 0 = a
1310+ wait n = try a >>= \case
1311+ Left (_ :: SomeException) -> do
1312+ liftIO $ threadDelay 100000
1313+ wait $ n - 1
1314+ Right v -> return v
1315+
1316+divId :: DomBuilder t m => Text -> m a -> m a
1317+divId i = elAttr "div" ("id" =: i)
1318+
1319+type TestWidget js t m = (DomBuilder t m, MonadHold t m, PostBuild t m, Prerender js t m, PerformEvent t m, TriggerEvent t m, MonadFix m, MonadIO (Performable m), MonadIO m)
1320+
1321+testWidgetStaticDebug
1322+ :: Bool
1323+ -> WD b
1324+ -- ^ Webdriver commands to run before JS runs and after hydration switchover
1325+ -> (forall m js. TestWidget js (SpiderTimeline Global) m => m ())
1326+ -- ^ Widget we are testing
1327+ -> WD b
1328+testWidgetStaticDebug withDebugging w = testWidgetDebug withDebugging (void w) w
1329+
1330+-- | TODO: do something about JSExceptions not causing tests to fail
1331+testWidgetDebug
1332+ :: Bool
1333+ -> WD ()
1334+ -- ^ Webdriver commands to run before the JS runs (i.e. on the statically rendered page)
1335+ -> WD b
1336+ -- ^ Webdriver commands to run after hydration switchover
1337+ -> (forall m js. TestWidget js (SpiderTimeline Global) m => m ())
1338+ -- ^ Widget we are testing
1339+ -> WD b
1340+testWidgetDebug withDebugging beforeJS afterSwitchover =
1341+ testWidgetDebug' withDebugging beforeJS (const afterSwitchover)
1342+
1343+-- | TODO: do something about JSExceptions not causing tests to fail
1344+testWidgetDebug'
1345+ :: Bool
1346+ -> WD a
1347+ -- ^ Webdriver commands to run before the JS runs (i.e. on the statically rendered page)
1348+ -> (a -> WD b)
1349+ -- ^ Webdriver commands to run after hydration switchover
1350+ -> (forall m js. TestWidget js (SpiderTimeline Global) m => m ())
1351+ -- ^ Widget we are testing (contents of body)
1352+ -> WD b
1353+testWidgetDebug' withDebugging beforeJS afterSwitchover bodyWidget = do
1354+ let putStrLnDebug :: MonadIO m => Text -> m ()
1355+ putStrLnDebug m = when withDebugging $ liftIO $ putStrLn $ T.unpack m
1356+ staticApp = do
1357+ el "head" $ pure ()
1358+ el "body" $ do
1359+ bodyWidget
1360+ el "script" $ text $ TE.decodeUtf8 $ LBS.toStrict $ jsaddleJs False
1361+ putStrLnDebug "rendering static"
1362+ ((), html) <- liftIO $ renderStatic staticApp
1363+ putStrLnDebug "rendered static"
1364+ waitBeforeJS <- liftIO newEmptyMVar -- Empty until JS should be run
1365+ waitUntilSwitchover <- liftIO newEmptyMVar -- Empty until switchover
1366+ let entryPoint = do
1367+ putStrLnDebug "taking waitBeforeJS"
1368+ liftIO $ takeMVar waitBeforeJS
1369+ let switchOverAction = do
1370+ putStrLnDebug "switchover syncPoint"
1371+ syncPoint
1372+ putStrLnDebug "putting waitUntilSwitchover"
1373+ liftIO $ putMVar waitUntilSwitchover ()
1374+ putStrLnDebug "put waitUntilSwitchover"
1375+ putStrLnDebug "running mainHydrationWidgetWithSwitchoverAction"
1376+ mainHydrationWidgetWithSwitchoverAction switchOverAction blank bodyWidget
1377+ putStrLnDebug "syncPoint after mainHydrationWidgetWithSwitchoverAction"
1378+ syncPoint
1379+ application <- liftIO $ jsaddleOr defaultConnectionOptions entryPoint $ \_ sendResponse -> do
1380+ putStrLnDebug "sending response"
1381+ r <- sendResponse $ responseLBS status200 [] $ "<!doctype html>\n" <> LBS.fromStrict html
1382+ putStrLnDebug "sent response"
1383+ return r
1384+ waitJSaddle <- liftIO newEmptyMVar
1385+ let settings = foldr ($) Warp.defaultSettings
1386+ [ Warp.setPort $ fromIntegral $ toInteger jsaddlePort
1387+ , Warp.setBeforeMainLoop $ do
1388+ putStrLnDebug "putting waitJSaddle"
1389+ putMVar waitJSaddle ()
1390+ putStrLnDebug "put waitJSaddle"
1391+ ]
1392+ -- hSilence to get rid of ConnectionClosed logs
1393+ silenceIfDebug = if withDebugging then id else hSilence [stderr]
1394+ jsaddleWarp = forkIO $ silenceIfDebug $ Warp.runSettings settings application
1395+ jsaddleTid <- liftIO jsaddleWarp
1396+ putStrLnDebug "taking waitJSaddle"
1397+ liftIO $ takeMVar waitJSaddle
1398+ putStrLnDebug "opening page"
1399+ WD.openPage $ "http://localhost:" <> show jsaddlePort
1400+ putStrLnDebug "running beforeJS"
1401+ a <- beforeJS
1402+ putStrLnDebug "putting waitBeforeJS"
1403+ liftIO $ putMVar waitBeforeJS ()
1404+ putStrLnDebug "taking waitUntilSwitchover"
1405+ liftIO $ takeMVar waitUntilSwitchover
1406+ putStrLnDebug "running afterSwitchover"
1407+ b <- afterSwitchover a
1408+ putStrLnDebug "killing jsaddle thread"
1409+ liftIO $ killThread jsaddleTid
1410+ return b
1411+
1412+data Key2 a where
1413+ Key2_Int :: Int -> Key2 Int
1414+ Key2_Char :: Char -> Key2 Char
1415+
1416+deriveGEq ''Key2
1417+deriveGCompare ''Key2
1418+deriveGShow ''Key2
1419+deriveArgDict ''Key2
1420+
1421+instance ShowTag Key2 Identity where
1422+ showTaggedPrec = \case
1423+ Key2_Int _ -> showsPrec
1424+ Key2_Char _ -> showsPrec
1425+
1426+instance EqTag Key2 Identity where
1427+ eqTagged (Key2_Int _) (Key2_Int _) = (==)
1428+ eqTagged (Key2_Char _) (Key2_Char _) = (==)
reflex-dom/ChangeLog.mdnew file+6
@@ -0,0 +1,6 @@
1+# Revision history for reflex-dom
2+
3+## 0.5
4+
5+* Re-export new hydration widget "mainWidget" functions.
6+* The use-warp flag now properly takes precedence on macOS.
reflex-dom/benchmarks/krausest.hs+7−7
@@ -28,15 +28,15 @@ main = do
2828 seed <- randomIO
2929 mainWidgetWithHead' (\_ -> headW, \_ -> bodyW seed)
3030
31−titleW :: MonadWidget t m => m ()
31+titleW :: DomBuilder t m => m ()
3232titleW = text "Reflex-dom keyed"
3333
34−headW :: MonadWidget t m => m ()
34+headW :: DomBuilder t m => m ()
3535headW = do
3636 el "title" titleW
3737 elAttr "link" ("href" =: "/css/currentStyle.css" <> "rel" =: "stylesheet") blank
3838
39−bodyW :: forall t m. MonadWidget t m => Entropy -> m ()
39+bodyW :: forall t m. (MonadFix m, MonadHold t m, DomBuilder t m) => Entropy -> m ()
4040bodyW seed = divClass "main" $ divClass "container" $ mdo
4141 buttonEvents <- divClass "jumbotron" $ divClass "row" $ do
4242 divClass "col-md-6" $ el "h1" titleW
@@ -75,14 +75,14 @@ step (m, t) f = ((m', t'), dt)
7575 (m', dt) = f (m, t)
7676 t' = applyAlways dt t
7777
78−buttonW :: MonadWidget t m => Text -> Text -> a -> m (Event t a)
78+buttonW :: DomBuilder t m => Text -> Text -> a -> m (Event t a)
7979buttonW id txt val = divClass "col-sm-6 smallpad" $ buttonW' ("class" =: "btn btn-primary btn-block" <> "id" =: id)
8080 where
8181 buttonW' attrs = do
8282 (b, _) <- elAttr' "button" attrs $ text txt
8383 pure $ val <$ domEvent Click b
8484
85−tableW :: MonadWidget t m => Event t TableDiff -> m (Event t (IntMap Step))
85+tableW :: (MonadHold t m, DomBuilder t m) => Event t TableDiff -> m (Event t (IntMap Step))
8686tableW diff = do
8787 elClass "table" "table table-hover table-striped test-data" $ do
8888 el "tbody" $ do
@@ -90,7 +90,7 @@ tableW diff = do
9090 rowEvents <- holdIncremental v0 v'
9191 return $ mergeIntIncremental rowEvents
9292
93−rowW :: MonadWidget t m => Row -> m (Event t Step)
93+rowW :: DomBuilder t m => Row -> m (Event t Step)
9494rowW row = elClass "tr" (if selected row then "danger" else "") $
9595 do
9696 elClass "td" "col-md-1" $ do
@@ -102,7 +102,7 @@ rowW row = elClass "tr" (if selected row then "danger" else "") $
102102 where
103103 tagClick f el = f row <$ (domEvent Click el)
104104
105−deleteW :: MonadWidget t m => m ()
105+deleteW :: DomBuilder t m => m ()
106106deleteW = el "a" $ elAttr "span" ("aria-hidden" =: "true" <> "class" =: "glyphicon glyphicon-remove") blank
107107
108108
reflex-dom/reflex-dom.cabal+11−9
@@ -1,5 +1,5 @@
11Name: reflex-dom
2−Version: 0.4
2+Version: 0.5
33Synopsis: Functional Reactive Web Apps with Reflex
44Description:
55 Reflex-DOM is a Functional Reactive web framework based on the Reflex FRP engine.
@@ -13,7 +13,9 @@ Stability: Experimental
1313Category: FRP, Web, GUI, HTML, Javascript, Reactive, Reactivity, User Interfaces, User-interface
1414Build-type: Simple
1515cabal-version: >=1.24
16−extra-source-files: java/org/reflexfrp/reflexdom/MainWidget.java
16+extra-source-files:
17+ java/org/reflexfrp/reflexdom/MainWidget.java
18+ ChangeLog.md
1719
1820flag use-warp
1921 description: Use jsaddle-warp server
@@ -62,20 +64,20 @@ library
6264 build-depends:
6365 base >= 4.7 && < 4.12,
6466 bytestring == 0.10.*,
65− reflex == 0.5.*,
66− reflex-dom-core >=0.4 && <0.5,
67+ reflex == 0.6.*,
68+ reflex-dom-core >=0.5 && <0.6,
6769 text == 1.2.*
6870 if !impl(ghcjs)
69− if os(osx) || os(ios)
71+ if flag(use-warp) && (os(linux) || os(osx))
7072 build-depends:
71− data-default == 0.7.*,
7273 jsaddle,
73− jsaddle-wkwebview
74+ jsaddle-warp
7475 else
75− if flag(use-warp)
76+ if os(osx) || os(ios)
7677 build-depends:
78+ data-default == 0.7.*,
7779 jsaddle,
78− jsaddle-warp
80+ jsaddle-wkwebview
7981 else
8082 if flag(webkit2gtk)
8183 build-depends:
reflex-dom/src/Reflex/Dom.hs+2−1
@@ -6,5 +6,6 @@ module Reflex.Dom (module X) where
66
77import Foreign.JavaScript.Orphans ()
88import Reflex.Dom.Core as X hiding (mainWidget, mainWidgetInElementById, mainWidgetWithCss,
9− mainWidgetWithHead, mainWidgetWithHead', runApp')
9+ mainWidgetWithHead, mainWidgetWithHead', runApp',
10+ mainHydrationWidgetWithHead, mainHydrationWidgetWithHead')
1011import Reflex.Dom.Internal as X
reflex-dom/src/Reflex/Dom/Internal.hs+18−4
@@ -4,18 +4,24 @@
44{-# LANGUAGE RankNTypes #-}
55{-# LANGUAGE TypeFamilies #-}
66module Reflex.Dom.Internal
7− (module Main, run, mainWidget, mainWidgetWithHead, mainWidgetWithCss,
8− mainWidgetWithHead', mainWidgetInElementById, runApp') where
7+ ( module Main
8+ , run
9+ , mainWidget
10+ , mainWidgetWithHead, mainWidgetWithCss, mainWidgetWithHead', mainWidgetInElementById, runApp'
11+ , mainHydrationWidgetWithHead, mainHydrationWidgetWithHead'
12+ ) where
913
1014import Data.ByteString (ByteString)
1115import Data.Text (Text)
1216import Reflex.Dom.Core (Widget)
1317import Reflex.Dom.Main as Main hiding
1418 (mainWidget, mainWidgetWithHead, mainWidgetWithCss,
15− mainWidgetWithHead', mainWidgetInElementById, runApp')
19+ mainWidgetWithHead', mainWidgetInElementById, runApp',
20+ mainHydrationWidgetWithHead, mainHydrationWidgetWithHead')
1621import qualified Reflex.Dom.Main as Main
1722 (mainWidget, mainWidgetWithHead, mainWidgetWithCss,
18− mainWidgetWithHead', mainWidgetInElementById, runApp')
23+ mainWidgetWithHead', mainWidgetInElementById, runApp',
24+ mainHydrationWidgetWithHead, mainHydrationWidgetWithHead')
1925
2026#if defined(ghcjs_HOST_OS)
2127run :: a -> a
@@ -101,3 +107,11 @@ mainWidgetInElementById eid w = run $ Main.mainWidgetInElementById eid w
101107runApp' :: (forall x. AppInput DomTimeline -> Widget x (AppOutput DomTimeline)) -> IO ()
102108runApp' app = run $ Main.runApp' app
103109{-# INLINE runApp' #-}
110+
111+mainHydrationWidgetWithHead :: (forall x. HydrationWidget x ()) -> (forall x. HydrationWidget x ()) -> IO ()
112+mainHydrationWidgetWithHead h b = run $ Main.mainHydrationWidgetWithHead h b
113+{-# INLINE mainHydrationWidgetWithHead #-}
114+
115+mainHydrationWidgetWithHead' :: HydrationWidget () () -> HydrationWidget () () -> IO ()
116+mainHydrationWidgetWithHead' h b = run $ Main.mainHydrationWidgetWithHead' h b
117+{-# INLINE mainHydrationWidgetWithHead' #-}