on master help sign in
New Hydration transformer

Note: Currently doesn't run correctly if the head has any content

commit742e3bc8eb3ece92f37185212b3a6f0aaab490f9 authorTom Smalley <tfsmalley@gmail.com> dateThu 15 Nov 2018, 18:31 (8 years ago) parent64336bc
3 files changed+241−29
lib/frontend/src/Obelisk/Frontend.hs+62−24 skeleton/frontend/frontend.cabal+1 skeleton/frontend/src/Frontend.hs+178−5
lib/frontend/src/Obelisk/Frontend.hs+62−24
@@ -5,6 +5,7 @@
55{-# LANGUAGE MultiParamTypeClasses #-}
66{-# LANGUAGE OverloadedStrings #-}
77{-# LANGUAGE RankNTypes #-}
8+{-# LANGUAGE RecursiveDo #-}
89{-# LANGUAGE ScopedTypeVariables #-}
910{-# LANGUAGE TemplateHaskell #-}
1011{-# LANGUAGE TypeFamilies #-}
@@ -21,7 +22,7 @@ import Prelude hiding ((.))
2122import Control.Category
2223import Control.Concurrent
2324import Control.Lens
24−import Control.Monad
25+import Control.Monad hiding (sequence, sequence_)
2526import Control.Monad.Fix
2627import Control.Monad.IO.Class
2728import Control.Monad.Primitive
@@ -33,6 +34,8 @@ import Data.Functor.Sum
3334import Data.IORef
3435import Data.Monoid ((<>))
3536import Data.Text (Text)
37+import Data.Foldable (sequence_)
38+import Data.Traversable (sequence)
3639import GHCJS.DOM hiding (bracket, catch)
3740import GHCJS.DOM.Document
3841import GHCJS.DOM.Node
@@ -40,6 +43,7 @@ import qualified GHCJS.DOM.Types as DOM
4043import Language.Javascript.JSaddle (JSM)
4144import Obelisk.Route.Frontend
4245import Reflex.Dom.Core
46+import Reflex.Dom.Builder.Hydration
4347import Reflex.Host.Class
4448import qualified Reflex.TriggerEvent.Base as TriggerEvent
4549
@@ -73,7 +77,7 @@ data Frontend route = Frontend
7377 , _frontend_body :: !(forall t m x. ObeliskWidget t x route m => RoutedT t route m ())
7478 }
7579
76−type Widget' x = ImmediateDomBuilderT DomTimeline (DomCoreWidget x)
80+type Widget' x a = HydrationDomBuilderT DomTimeline (DomCoreWidget x) a
7781
7882-- | A widget that isn't attached to any particular part of the DOM hierarchy
7983type FloatingWidget x = TriggerEventT DomTimeline (DomCoreWidget x)
@@ -83,16 +87,43 @@ type DomCoreWidget x = PostBuildT DomTimeline (WithJSContextSingleton x (Perform
8387--TODO: Rename
8488{-# INLINABLE attachWidget''' #-}
8589attachWidget'''
86− :: IORef Bool
90+ :: Document
91+ -> Node
92+ -> IORef HydrationMode
93+ -> IORef [DOM DomTimeline (PostBuildT DomTimeline (WithJSContextSingleton () (PerformEventT DomTimeline DomHost)))]
94+ -> JSContextSingleton ()
8795 -> (EventChannel -> PerformEventT DomTimeline DomHost (IORef (Maybe (EventTrigger DomTimeline ()))))
8896 -> IO ()
89−attachWidget''' hydration w = runDomHost $ do
90− events <- liftIO newChan
91− (postBuildTriggerRef, fc@(FireCommand fire)) <- hostPerformEventT $ w events
92− mPostBuildTrigger <- readRef postBuildTriggerRef
93− forM_ mPostBuildTrigger $ \postBuildTrigger -> fire [postBuildTrigger :=> Identity ()] $ return ()
94− liftIO $ writeIORef hydration False
95− liftIO $ processAsyncEvents events fc
97+attachWidget''' doc bodyNode hydrationMode hres jsSing w = do
98+ events <- newChan
99+ runDomHost $ flip runTriggerEventT events $ mdo
100+ (syncEvent, fireSync) <- newTriggerEvent
101+ liftIO $ putStrLn "-------------------------------------- Running host"
102+ (postBuildTriggerRef, fc@(FireCommand fire)) <- lift $ hostPerformEventT $ do
103+ a <- w events
104+ runWithReplace (return ()) $ delayedAction <$ syncEvent
105+ pure a
106+ mPostBuildTrigger <- readRef postBuildTriggerRef
107+ lift $ forM_ mPostBuildTrigger $ \postBuildTrigger -> fire [postBuildTrigger :=> Identity ()] $ return ()
108+ liftIO $ putStrLn "-------------------------------------- Finished postBuild"
109+ liftIO $ fireSync ()
110+ unreadyChildren <- liftIO $ newIORef 0
111+ forest <- liftIO $ readIORef hres
112+ -- TODO: DomHost is SpiderHost Global only if we're not in profiling mode
113+ let env = HydrationRunnerEnv
114+ { _hydrationRunnerEnv_document = doc
115+ , _hydrationRunnerEnv_parent = bodyNode
116+ , _hydrationRunnerEnv_unreadyChildren = unreadyChildren
117+ , _hydrationRunnerEnv_commitAction = pure ()
118+ }
119+ state = HydrationRunnerState
120+ { _hydrationRunnerState_previousNode = Nothing
121+ }
122+ delayedAction = do
123+ void $ runWithJSContextSingleton (runPostBuildT (runHydrationDomBuilderT (runDOMForest forest) env state events) never) jsSing
124+ liftIO $ writeIORef hydrationMode HydrationMode_Immediate
125+ liftIO $ putStrLn "-------------------------------------- Performing delayed actions"
126+ liftIO $ processAsyncEvents events fc
96127
97128--TODO: This is a collection of random stuff; we should make it make some more sense and then upstream to reflex-dom-core
98129runWithHeadAndBody
@@ -106,24 +137,31 @@ runWithHeadAndBody app = withJSContextSingletonMono $ \jsSing -> do
106137 headElement <- getHeadUnchecked globalDoc
107138 bodyElement <- getBodyUnchecked globalDoc
108139 unreadyChildren <- liftIO $ newIORef 0
109− hydration <- liftIO $ newIORef True
110− liftIO $ attachWidget''' hydration $ \events -> flip runWithJSContextSingleton jsSing $ do
140+ hydrationMode <- liftIO $ newIORef HydrationMode_Hydrating
141+ hydrationResult <- liftIO $ newIORef []
142+ liftIO $ attachWidget''' globalDoc (toNode bodyElement) hydrationMode hydrationResult jsSing $ \events -> flip runWithJSContextSingleton jsSing $ do
111143 (postBuild, postBuildTriggerRef) <- newEventWithTriggerRef
112− let appendImmediateDom :: DOM.Node -> Widget' () c -> FloatingWidget () c
113− appendImmediateDom n w = do
114− hydrationNode <- liftIO $ newIORef Nothing
144+ let hydrateDom :: DOM.Node -> Widget' () c -> FloatingWidget () c
145+ hydrateDom n w = do
115146 events' <- TriggerEvent.askEvents
147+ parent <- liftIO $ newIORef $ toNode n
148+ prerenderDepth <- liftIO $ newIORef 0
116149 lift $ do
117− let builderEnv = ImmediateDomBuilderEnv
118− { _immediateDomBuilderEnv_document = globalDoc
119− , _immediateDomBuilderEnv_parent = toNode n
120− , _immediateDomBuilderEnv_unreadyChildren = unreadyChildren
121− , _immediateDomBuilderEnv_commitAction = pure ()
122− , _immediateDomBuilderEnv_hydrating = hydration
123− , _immediateDomBuilderEnv_hydrationNode = hydrationNode
150+ let builderEnv = HydrationDomBuilderEnv
151+ { _hydrationDomBuilderEnv_document = globalDoc
152+ , _hydrationDomBuilderEnv_parent = parent
153+ , _hydrationDomBuilderEnv_unreadyChildren = unreadyChildren
154+ , _hydrationDomBuilderEnv_commitAction = pure ()
155+ , _hydrationDomBuilderEnv_hydrationMode = hydrationMode
156+ , _hydrationDomBuilderEnv_prerenderDepth = prerenderDepth
124157 }
125− runImmediateDomBuilderT w builderEnv events'
126− flip runPostBuildT postBuild $ flip runTriggerEventT events $ app (appendImmediateDom $ toNode headElement) (appendImmediateDom $ toNode bodyElement)
158+ state = HydrationDomBuilderState
159+ { _hydrationDomBuilderState_delayed = []
160+ }
161+ (a, res) <- runHydrationDomBuilderT w builderEnv state events'
162+ liftIO $ modifyIORef' hydrationResult $ \xs -> xs ++ _hydrationDomBuilderState_delayed res
163+ pure a
164+ flip runPostBuildT postBuild $ flip runTriggerEventT events $ app (hydrateDom $ toNode headElement) (hydrateDom $ toNode bodyElement)
127165-- liftIO (readIORef unreadyChildren) >>= \case
128166-- 0 -> DOM.liftJSM commit
129167-- _ -> return ()
skeleton/frontend/frontend.cabal+1
@@ -7,6 +7,7 @@ library
77 hs-source-dirs: src
88 build-depends: base
99 , common
10+ , constraints-extras
1011 , obelisk-frontend
1112 , obelisk-route
1213 , reflex-dom
skeleton/frontend/src/Frontend.hs+178−5
@@ -1,27 +1,200 @@
11{-# LANGUAGE DataKinds #-}
2+{-# LANGUAGE FlexibleContexts #-}
3+{-# LANGUAGE FlexibleInstances #-}
4+{-# LANGUAGE GADTs #-}
5+{-# LANGUAGE LambdaCase #-}
26{-# LANGUAGE OverloadedStrings #-}
7+{-# LANGUAGE TemplateHaskell #-}
38{-# LANGUAGE TypeApplications #-}
9+{-# LANGUAGE TypeFamilies #-}
410module Frontend where
511
12+import Control.Monad.Fix
13+import Control.Monad.IO.Class
14+import Data.Constraint (Dict(..))
15+import Data.Constraint.Extras
16+import Data.Constraint.Extras.TH
17+import Data.Dependent.Sum (DSum(..), (==>))
18+import Data.Dependent.Map (DMap)
19+import Data.IntMap (IntMap)
20+import qualified Data.IntMap as IntMap
21+import Data.Functor.Misc
22+import Data.GADT.Compare.TH
23+import qualified Data.Dependent.Map as DMap
24+import Data.Functor.Identity
25+import Data.Time (getCurrentTime)
26+import Data.Text (Text)
627import qualified Data.Text as T
728import Obelisk.Frontend
829import Obelisk.Route
930import Reflex.Dom.Core
31+import Reflex.Patch.DMapWithMove
1032
1133import Common.Api
1234import Common.Route
1335import Obelisk.Generated.Static
1436
37+tshow :: Show a => a -> Text
38+tshow = T.pack . show
1539
1640frontend :: Frontend (R FrontendRoute)
1741frontend = Frontend
18− { _frontend_head = el "title" $ text "Obelisk Minimal Example"
42+ { _frontend_head = pure () -- el "title" $ text "Obelisk Minimal Example"
1943 , _frontend_body = do
2044 text "Welcome to Obelisk!"
2145 el "p" $ text $ T.pack commonStuff
22− elAttr "img" ("src" =: static @"obelisk.jpg") blank
23− e <- button "click"
24− el "p" $ display =<< count e
46+ (e, ()) <- elAttr' "img" ("src" =: static @"obelisk.jpg") blank
47+ clicks <- count $ domEvent Click e
48+ performEvent $ liftIO . print <$> updated clicks
49+ el "p" $ text "Clicks: " >> el "span" (display clicks) -- TODO multiple text bug
50+
51+ timer <- tickLossy 1 =<< liftIO getCurrentTime
52+ seconds <- count timer
53+ el "p" $ display seconds
54+ text "-----------------------------------------------"
55+
56+-- elAttr "article" ("style" =: "background: lightcoral") $ do
57+-- prerender
58+-- (prerender
59+-- (el "span" $ text "Server span")
60+-- (el "a" $ text "JS link"))
61+-- (el "a" $ text "JS link")
62+
2563 pb <- getPostBuild
26− widgetHold_ (el "div" $ text "first") $ ffor pb $ \() -> el "div" $ text "second"
64+ pure ()
65+
66+ elAttr "article" ("style" =: "background: moccasin") $ do
67+ widgetHold_ (el "div" $ text "first") $ ffor pb $ \() -> display seconds
68+
69+-- elAttr "article" ("style" =: "background: lightgreen") $ do
70+-- widgetHold_ (do prerender (liftIO $ putStrLn "-------- server missiles") (liftIO $ putStrLn "-------- client missiles"); el "div" $ text "first") never
71+
72+ elAttr "article" ("style" =: "background: paleturquoise") $ do
73+ dyn_ $ ffor clicks $ \x -> case x `mod` 2 of
74+ 0 -> el "span" $ text "Even"
75+ 1 -> el "span" $ text "Odd"
76+
77+ elAttr "article" ("style" =: "background: cornflowerblue") $ do
78+ widgetHold (text "first") $ ffor (updated clicks) $ \x -> case x `mod` 2 of
79+ 0 -> do
80+ dyn_ $ ffor seconds $ \x -> case x `mod` 2 of
81+ 0 -> el "span" $ text "Even"
82+ 1 -> text "Odd"
83+ 1 -> el "span" $ text "Odd inner"
84+
85+ pure ()
86+
87+ -- These will be squashed into a single text node by the static renderer
88+-- text "Test"
89+-- text "Test"
90+-- text "Test"
91+-- text "Test"
92+-- text "Test"
93+
94+ {-
95+ --traverseDMapWithKeyWithAdjust' :: (forall a. k a -> v a -> m (v' a)) -> DMap k v -> Event t (PatchDMap k v) -> m (DMap k v', Event t (PatchDMap k v'))
96+ elAttr "article" ("style" =: "background: thistle") $ do
97+ (m', e') <- traverseDMapWithKeyWithAdjust (\k v -> has @Show k $ dkeyWidget k v) keyMap $ ffor (updated clicks) $ \x -> case x `mod` 2 of
98+ 0 -> PatchDMap $ DMap.singleton Key_Int (ComposeMaybe $ Just $ Identity x)
99+ 1 -> PatchDMap $ DMap.singleton Key_Bool (ComposeMaybe $ case x `mod` 3 of 0 -> Nothing; _ -> Just $ Identity True) <> DMap.singleton Key_Char (ComposeMaybe $ Just $ Identity (toEnum $ fromEnum 'A' + x))
100+ pure ()
101+-}
102+
103+ {-
104+ elAttr "article" ("style" =: "background: maroon") $ do
105+ widgetHold (text "first") $ ffor pb $ \_ -> do
106+ (m', e') <- traverseDMapWithKeyWithAdjust (\k v -> has @Show k $ dkeyWidget k v) keyMap $ ffor (updated clicks) $ \x -> case x `mod` 2 of
107+ 0 -> PatchDMap $ DMap.singleton Key_Int (ComposeMaybe $ Just $ Identity x)
108+ 1 -> PatchDMap $ DMap.singleton Key_Bool (ComposeMaybe $ case x `mod` 3 of 0 -> Nothing; _ -> Just $ Identity True) <> DMap.singleton Key_Char (ComposeMaybe $ Just $ Identity (toEnum $ fromEnum 'A' + x))
109+ pure ()
110+
111+ move <- button "Move"
112+ ins <- button "Insert"
113+ del <- button "Delete"
114+
115+ --traverseDMapWithKeyWithAdjust' :: (forall a. k a -> v a -> m (v' a)) -> DMap k v -> Event t (PatchDMap k v) -> m (DMap k v', Event t (PatchDMap k v'))
116+ elAttr "article" ("style" =: "background: yellow") $ do
117+ (m', e') <- traverseDMapWithKeyWithAdjustWithMove (\k v -> has @Show k $ dkeyWidget k v) keyMap $ leftmost
118+ -- TODO: move causes JSException here and in Immediate builder
119+ [ ffor move $ \() -> moveDMapKey Key_Int2 Key_Int
120+ , ffor ins $ \() -> insertDMapKey Key_Int (Identity 4)
121+ , ffor del $ \() -> deleteDMapKey Key_Int
122+ ]
123+ pure ()
124+
125+ elAttr "article" ("style" =: "background: cyan") $ do
126+ (m', e') <- traverseIntMapWithKeyWithAdjust (\k v -> intkeyWidget k v) intMap $ leftmost
127+ -- TODO: move causes JSException here and in Immediate builder
128+ [ ffor move $ \() -> PatchIntMap $ IntMap.singleton 1 (Just 'Z')
129+ , ffor ins $ \() -> PatchIntMap $ IntMap.singleton 5 (Just 'Z')
130+ , ffor del $ \() -> PatchIntMap $ IntMap.singleton 3 Nothing
131+ ]
132+ pure ()
133+
134+-}
135+
136+-- el "footer" $ text "Footer"
27137 }
138+
139+intkeyWidget :: (MonadHold t m, PostBuild t m, PerformEvent t m, TriggerEvent t m, DomBuilder t m, MonadFix m, MonadIO m, MonadIO (Performable m)) => Int -> Char -> m ()
140+intkeyWidget k v = el "div" $ do
141+ el "span" (text $ T.pack $ show k)
142+ case k of
143+ 5 -> do
144+ seconds <- count =<< tickLossy 1 =<< liftIO getCurrentTime
145+ text " -> "
146+ dyn_ $ ffor seconds $ el "span" . text . tshow
147+ _ -> text $ " -> " <> tshow v
148+ pure ()
149+
150+intMap :: IntMap Char
151+intMap = IntMap.fromList
152+ [ (1, 'A')
153+ , (2, 'B')
154+ , (3, 'C')
155+ , (4, 'D')
156+ ]
157+
158+dkeyWidget :: (MonadHold t m, PostBuild t m, PerformEvent t m, TriggerEvent t m, DomBuilder t m, Show a, MonadFix m, MonadIO m, MonadIO (Performable m)) => DKey a -> Identity a -> m (Identity a)
159+dkeyWidget k (Identity v) = el "div" $ do
160+ el "span" (text $ textKey k)
161+ case k of
162+ Key_Bool | v -> do
163+ seconds <- count =<< tickLossy 1 =<< liftIO getCurrentTime
164+ text " -> "
165+ dyn_ $ ffor seconds $ el "span" . text . tshow
166+ _ -> text $ " -> " <> tshow v
167+ pure $ Identity v
168+
169+keyMap :: DMap DKey Identity
170+keyMap = DMap.fromList
171+ [ Key_Int ==> -1
172+ , Key_Int2 ==> -100
173+ , Key_Bool ==> False
174+ , Key_Char ==> 'A'
175+ ]
176+
177+data DKey a where
178+ Key_Int :: DKey Int
179+ Key_Char :: DKey Char
180+ Key_Bool :: DKey Bool
181+ Key_Int2 :: DKey Int
182+
183+instance ArgDict DKey where
184+ type ConstraintsFor DKey c = (c Int, c Char, c Bool)
185+ argDict = \case
186+ Key_Int -> Dict
187+ Key_Int2 -> Dict
188+ Key_Char -> Dict
189+ Key_Bool -> Dict
190+
191+textKey :: DKey a -> Text
192+textKey = \case
193+ Key_Int -> "Key_Int"
194+ Key_Int2 -> "Key_Int2"
195+ Key_Char -> "Key_Char"
196+ Key_Bool -> "Key_Bool"
197+
198+deriveGEq ''DKey
199+deriveGCompare ''DKey
200+--deriveArgDict ''DKey