on develop help sign in
Merge remote-tracking branch 'crappygraphix/develop' into ts-deprecate-input
commit5916a107273c857dc2cc9318330221f6d8cbd3fe authorJohn Ericson <John.Ericson@Obsidian.Systems> dateTue 10 Dec 2019, 14:04 (7 years ago) parentdd202d3c818dc7
45 files changed+4381−918
CONTRIBUTING.md+65 README.md+4−3 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+52−5 dep/reflex-platform/default.nix+7 dep/reflex-platform/github.json+7 reflex-dom-core/ChangeLog.md+34 reflex-dom-core/default.nix−47 reflex-dom-core/reflex-dom-core.cabal+83−20 reflex-dom-core/src/Foreign/JavaScript/Orphans.hs+2 reflex-dom-core/src/Foreign/JavaScript/TH.hs+2−4 reflex-dom-core/src/Reflex/Dom/Builder/Class.hs+54−5 reflex-dom-core/src/Reflex/Dom/Builder/Class/Events.hs−1 reflex-dom-core/src/Reflex/Dom/Builder/Hydratable.hs+116 reflex-dom-core/src/Reflex/Dom/Builder/Immediate.hs+1651−563 reflex-dom-core/src/Reflex/Dom/Builder/InputDisabled.hs+6 reflex-dom-core/src/Reflex/Dom/Builder/Static.hs+71−3 reflex-dom-core/src/Reflex/Dom/Core.hs+2 reflex-dom-core/src/Reflex/Dom/Location.hs+39 reflex-dom-core/src/Reflex/Dom/Main.hs+167−36 reflex-dom-core/src/Reflex/Dom/Old.hs−1 reflex-dom-core/src/Reflex/Dom/Prerender.hs+287−52 reflex-dom-core/src/Reflex/Dom/Specializations.hs−62 reflex-dom-core/src/Reflex/Dom/WebSocket.hs+12−4 reflex-dom-core/src/Reflex/Dom/WebSocket/Foreign.hs+37−23 reflex-dom-core/src/Reflex/Dom/Widget/Basic.hs+29−7 reflex-dom-core/src/Reflex/Dom/Widget/Input.hs+1−1 reflex-dom-core/src/Reflex/Dom/Widget/Lazy.hs+1−1 reflex-dom-core/src/Reflex/Dom/Widget/Resize.hs+4 reflex-dom-core/src/Reflex/Dom/Xhr/Foreign.hs−1 reflex-dom-core/src/Reflex/Dom/Xhr/FormData.hs+47 reflex-dom-core/test/gc.hs+12−11 reflex-dom-core/test/hlint.hs+2 reflex-dom-core/test/hydration.hs+1416 reflex-dom/ChangeLog.md+12 reflex-dom/benchmarks/krausest.hs+7−7 reflex-dom/default.nix−37 reflex-dom/java/org/reflexfrp/reflexdom/MainWidget.java+8−1 reflex-dom/reflex-dom.cabal+18−18 reflex-dom/src/Reflex/Dom.hs+2−1 reflex-dom/src/Reflex/Dom/Internal.hs+18−4 release.nix+35
CONTRIBUTING.mdnew file+65
@@ -0,0 +1,65 @@
1+# Contribution Guide
2+
3+Contributions and issue reports are encouraged and appreciated!
4+
5+- [Opening Issues](#opening-issues)
6+- [Submitting Changes](#submitting-changes)
7+ - [Guidelines for Commit Messages](#guidelines-for-commit-messages)
8+ - [Guidelines for Pull Requests](#guidelines-for-pull-requests)
9+ - [Code Quality](#code-quality)
10+ - [Documentation](#documentation)
11+
12+## Opening Issues
13+
14+Before opening an issue, please check whether your issue has already been reported. Assuming it has not:
15+
16+* Describe the issue you're encountering or the suggestion you're making
17+* Include any relevant steps to reproduce or code samples you can. It's always easier for us to debug if we have something that demonstrates the error.
18+* Let us know what version of this project you were using. If you're using a github checkout, provide the git hash.
19+* Describe how you're building this project (i.e., via reflex-platform, cabal install, stack, obelisk, etc.). If you're using reflex-platform or obelisk, provide the git hash of your checkout.
20+
21+## Submitting Changes
22+
23+### Guidelines for Commit Messages
24+
25+#### Summary Line
26+The summary line of your commit message should summarize the changes being made. Commit messages should be written in the imperative mood and should describe what happens when the commit is applied.
27+
28+One way to think about it is that your commit message should be able to complete the sentence:
29+"When applied, this commit will..."
30+
31+#### Body
32+For breaking changes, new features, refactors, or other major changes, the body of the commit message should describe the motivation behind the change in greater detail and may include references to the issue tracker. The body shouldn't repeat code/comments from the diff.
33+
34+### Guidelines for Pull Requests
35+
36+Wherever possible, pull requests should add a single feature or fix a single bug. Pull requests should not bundle several unrelated changes.
37+
38+### Code Quality
39+
40+#### Warnings
41+
42+Your pull request should add no new warnings to the project. It should also generally not disable any warnings.
43+
44+#### Build and Test
45+
46+Make sure the project builds and that the tests pass! This will generally also be checked by CI before merge, but trying it yourself first means you'll catch problems earlier and your contribution can be merged that much sooner!
47+
48+### Documentation
49+
50+#### In the code
51+We're always striving to improve documentation. Please include [haddock](https://haskell-haddock.readthedocs.io/en/latest/index.html) documentation for any added code, and update the documentation for any code you modify.
52+
53+#### In the Changelog
54+Add an entry to the changelog when your PR:
55+* Adds a feature
56+* Deprecates something
57+* Includes a breaking change
58+* Makes any other change that will impact users
59+
60+You may need to add changelog entries to one or both of the changelogs in this repository:
61+* [reflex-dom-core's changelog](reflex-dom-core/ChangeLog.md)
62+* [reflex-dom's changelog](reflex-dom/ChangeLog.md)
63+
64+#### In the [Readme](README.md)
65+The readme is the first place a lot of people look for information about the repository. Update any parts of the readme that are affected by your PR.
README.md+4−3
@@ -1,4 +1,7 @@
11## [Reflex-DOM](https://reflex-frp.org)
2+
3+[![Hackage](https://img.shields.io/hackage/v/reflex-dom.svg)](http://hackage.haskell.org/package/reflex-dom)
4+
25### A Framework for Functional Reactive Web Apps in Haskell
36
47Reflex-DOM is a framework for building web applications in Haskell, based on the [Functional Reactive Programming](https://wiki.haskell.org/Functional_Reactive_Programming) library [Reflex](https://github.com/reflex-frp/reflex).
@@ -12,11 +15,9 @@ A summary of Reflex-DOM functions is available in the [quick reference](Quickref
1215
1316[/r/reflexfrp](https://www.reddit.com/r/reflexfrp)
1417
15−[hackage](https://hackage.haskell.org/package/reflex)
16−
1718[irc.freenode.net #reflex-frp](http://webchat.freenode.net?channels=%23reflex-frp&uio=d4)
1819
1920### Hacking
2021
21−Use the `./scripts/hack-on dep/reflex-dom` script in [Reflex Platform](https://github.com/reflex-frp/reflex-platform) to checkout the source code of `reflex-dom` locally in `./dep/reflex-dom` directory.
22+Use the `./scripts/hack-on haskell-overlays/reflex-packages/dep/reflex-dom` script in [Reflex Platform](https://github.com/reflex-frp/reflex-platform) to checkout the source code of `reflex-dom` locally in `./dep/reflex-dom` directory.
2223Then do modifications to the source in place, and use the `./try-reflex` or `./scripts/work-on` scripts to create the shell to test your changes.
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+52−5
@@ -1,6 +1,53 @@
1−haskellPackages: nixpkgs: {
2− reflex-dom = haskellPackages.callPackage ./reflex-dom {};
3− reflex-dom-core = haskellPackages.callPackage ./reflex-dom-core {
4− inherit (nixpkgs) iproute chromium;
5− };
1+haskellPackages: let
2+ pkgs = haskellPackages.callPackage ({ pkgs }: pkgs) {};
3+ inherit (pkgs) stdenv;
4+ haskellLib = pkgs.haskell.lib;
5+in {
6+ reflex-dom-core = let
7+ inherit (haskellPackages) ghc;
8+ noGcTest = stdenv.hostPlatform.system != "x86_64-linux"
9+ || stdenv.hostPlatform != stdenv.buildPlatform
10+ || (ghc.isGhcjs or false);
11+ in haskellLib.overrideCabal
12+ (haskellPackages.callCabal2nix "reflex-dom-core" ./reflex-dom-core { })
13+ (drv: {
14+ #TODO: Get hlint working for cross-compilation
15+ doCheck = stdenv.hostPlatform == stdenv.buildPlatform && !(ghc.isGhcjs or false);
16+
17+ # The headless browser run as part of the tests will exit without this
18+ preBuild = ''
19+ export HOME="$PWD"
20+ '';
21+
22+ # Show some output while running tests, so we might notice what's wrong
23+ testTarget = "--show-details=streaming";
24+
25+ testHaskellDepends = with haskellPackages; (drv.testHaskellDepends or []) ++ stdenv.lib.optionals (!noGcTest) [
26+ temporary
27+ jsaddle-warp
28+ process
29+ chrome-test-utils
30+ ];
31+
32+ testSystemDepends = with pkgs; (drv.testSystemDepends or []) ++ [
33+ selenium-server-standalone which
34+ ] ++ stdenv.lib.optionals (!noGcTest) [
35+ chromium
36+ pkgs.iproute
37+ ];
38+ } // stdenv.lib.optionalAttrs (!noGcTest) {
39+ # The headless browser run as part of gc tests would hang/crash without this
40+ preCheck = ''
41+ export FONTCONFIG_PATH=${pkgs.fontconfig.out}/etc/fonts
42+ '';
43+ });
44+ reflex-dom = haskellLib.overrideCabal
45+ (haskellPackages.callCabal2nix "reflex-dom" ./reflex-dom { })
46+ (drv: {
47+ # Hack until https://github.com/NixOS/cabal2nix/pull/432 lands
48+ libraryHaskellDepends = (drv.libraryHaskellDepends or []) ++ stdenv.lib.optionals (with stdenv.hostPlatform; isAndroid && is32bit) [
49+ haskellPackages.android-activity
50+ ];
51+ });
52+ chrome-test-utils = haskellPackages.callCabal2nix "chrome-test-utils" ./chrome-test-utils {};
653}
dep/reflex-platform/default.nixnew file+7
@@ -0,0 +1,7 @@
1+# DO NOT HAND-EDIT THIS FILE
2+import ((import <nixpkgs> {}).fetchFromGitHub (
3+ let json = builtins.fromJSON (builtins.readFile ./github.json);
4+ in { inherit (json) owner repo rev sha256;
5+ private = json.private or false;
6+ }
7+))
dep/reflex-platform/github.jsonnew file+7
@@ -0,0 +1,7 @@
1+{
2+ "owner": "reflex-frp",
3+ "repo": "reflex-platform",
4+ "branch": "develop",
5+ "rev": "338fc907e37d70e6469c529456f33221fb05ca1f",
6+ "sha256": "08gfhpp0pkvvi1wnlpcn6wmpzdjvy5z7dy1h66gp2w8b5cx8hx5h"
7+}
reflex-dom-core/ChangeLog.mdnew file+34
@@ -0,0 +1,34 @@
1+# Revision history for reflex-dom-core
2+
3+## Unreleased
4+
5+* Deprecate a number of old inflexible widget helpers in `Reflex.Dom.Widget.Basic`:
6+ * `Link`
7+ * `button`
8+ * `dtdd`
9+ * `linkClass`
10+ * `link`
11+ * `tabDisplay`
12+ * `tableDynAttr`
13+
14+## 0.5.2
15+
16+* Update to use new dependent-sum/map packages and drop dependency on `*Tag` classes (e.g., `ShowTag`).
17+* Update version bounds of base, containers, and stm
18+* Update to use the newly split `these`/`semialign` packages. To use the pre-split `these` package, set the `split-these` flag to false.
19+* Reintroduce "data-ssr": elements without this attribute are skipped during
20+ hydration.
21+* Fix an issue in the hydration tests that prevented the test from finding the chromium executable
22+* Relax constraints on `dyn` and `widgetHold` to match the ones in `networkView` and `networkHold` respectively
23+* Fix prerender for RequesterT so that it doesn't accidentally discard a request that is made at the same moment as getPostBuild's Event fires
24+
25+## 0.5.1
26+
27+* Added support for marking elements with a "data-skip-hydration" attribute, which will cause hydration to ignore and skip over them.
28+* Removed "data-ssr" attributes from statically rendered output.
29+
30+## 0.5
31+
32+* Add HydrationDomBuilderT to support hydration of statically rendered DOM nodes. See the note at the top of Reflex.Dom.Builder.Immediate.
33+* 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.
34+* Add the Reflex.Dom.Xhr.FormData module to make posting formdata over xhr more convenient.
reflex-dom-core/default.nixdeleted−47
@@ -1,47 +0,0 @@
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
9−}:
10−let addGcTestDepends = drv: if (stdenv.system != "x86_64-linux" || stdenv.hostPlatform != stdenv.buildPlatform || (ghc.isGhcjs or false)) then drv else drv // {
11− # The headless browser run as part of gc tests would hang/crash without this
12− preCheck = ''
13− export FONTCONFIG_PATH=${fontconfig.out}/etc/fonts
14− '';
15− testHaskellDepends = (drv.testHaskellDepends or []) ++ [ temporary jsaddle-warp process linux-namespaces ];
16− testSystemDepends = (drv.testSystemDepends or []) ++ [ chromium iproute ];
17− };
18−in mkDerivation (addGcTestDepends {
19− pname = "reflex-dom-core";
20− version = "0.4";
21− src = ./.;
22− libraryHaskellDepends = [
23− aeson base bifunctors bimap blaze-builder bytestring constraints
24− containers contravariant data-default dependent-map dependent-sum
25− 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
29− ] ++ (if ghc.isGhcjs or false then [
30− hashable
31− ] else []);
32−
33− #TODO: Get hlint working for cross-compilation
34− doCheck = stdenv.hostPlatform == stdenv.buildPlatform && !(ghc.isGhcjs or false);
35−
36− # The headless browser run as part of the tests will exit without this
37− preBuild = ''
38− export HOME="$PWD"
39− '';
40−
41− # Show some output while running tests, so we might notice what's wrong
42− testTarget = "--show-details=streaming";
43−
44− testHaskellDepends = [ base hlint ];
45− description = "Functional Reactive Web Apps with Reflex";
46− license = stdenv.lib.licenses.bsd3;
47−})
reflex-dom-core/reflex-dom-core.cabal+83−20
@@ -1,5 +1,5 @@
11Name: reflex-dom-core
2−Version: 0.4
2+Version: 0.5.2.0
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
@@ -38,22 +39,27 @@ flag profile-reflex
3839 default: False
3940 manual: True
4041
42+flag split-these
43+ description: Use split these/semialign packages
44+ manual: False
45+ default: True
46+
4147library
4248 hs-source-dirs: src
4349 build-depends:
44− aeson >= 0.8 && < 1.4,
45− base >= 4.7 && < 4.12,
50+ aeson >= 0.8 && < 1.5,
51+ base >= 4.7 && < 4.13,
4652 bifunctors >= 4.2 && < 6,
4753 bimap >= 0.3 && < 0.4,
4854 blaze-builder,
4955 bytestring == 0.10.*,
50− containers == 0.5.*,
51− constraints >= 0.9 && < 0.11,
52− contravariant >= 1.4 && < 1.5,
56+ containers >= 0.6 && < 0.7,
57+ constraints >= 0.9 && < 0.12,
58+ contravariant >= 1.4 && < 1.6,
5359 data-default >= 0.5 && < 0.8,
54− dependent-map == 0.2.*,
55− dependent-sum >= 0.3 && < 0.5,
56− dependent-sum-template >= 0.0.0.4 && < 0.1,
60+ dependent-map >= 0.3 && < 0.4,
61+ dependent-sum >= 0.6 && < 0.7,
62+ dependent-sum-template >= 0.1 && < 0.2,
5763 directory >= 1.2 && < 1.4,
5864 exception-transformers == 0.4.*,
5965 ghcjs-dom >= 0.9.1.0 && < 0.10,
@@ -64,12 +70,12 @@ library
6470 monad-control >= 1.0.1 && < 1.1,
6571 mtl >= 2.1 && < 2.3,
6672 primitive >= 0.5 && < 0.7,
73+ random,
6774 ref-tf == 0.4.*,
68− reflex == 0.5.*,
75+ reflex >= 0.6.2,
6976 semigroups >= 0.16 && < 0.19,
70− stm == 2.4.*,
77+ stm >= 2.4 && < 2.6,
7178 text == 1.2.*,
72− these >= 0.4 && < 0.8,
7379 transformers >= 0.3 && < 0.6,
7480 network-uri >= 2.6.1 && < 2.7,
7581 zenc == 0.1.*
@@ -84,6 +90,14 @@ library
8490 if !os(windows)
8591 build-depends: unix == 2.7.*
8692
93+ if flag(split-these)
94+ build-depends:
95+ semialign >= 1 && < 1.1,
96+ these >= 1 && < 1.1
97+ else
98+ build-depends:
99+ these >= 0.4 && < 0.9
100+
87101 exposed-modules:
88102 Foreign.JavaScript.TH
89103 Foreign.JavaScript.Orphans
@@ -92,6 +106,7 @@ library
92106 Reflex.Dom.Builder.Class.Events
93107 Reflex.Dom.Builder.Immediate
94108 Reflex.Dom.Builder.InputDisabled
109+ Reflex.Dom.Builder.Hydratable
95110 Reflex.Dom.Builder.Static
96111 Reflex.Dom.Class
97112 Reflex.Dom.Core
@@ -100,7 +115,6 @@ library
100115 Reflex.Dom.Modals.Class
101116 Reflex.Dom.Old
102117 Reflex.Dom.Prerender
103− Reflex.Dom.Specializations
104118 Reflex.Dom.Time
105119 Reflex.Dom.WebSocket
106120 Reflex.Dom.WebSocket.Query
@@ -110,6 +124,7 @@ library
110124 Reflex.Dom.Widget.Lazy
111125 Reflex.Dom.Widget.Resize
112126 Reflex.Dom.Xhr
127+ Reflex.Dom.Xhr.FormData
113128 other-modules:
114129 Foreign.JavaScript.Internal.Utils
115130 Reflex.Dom.WebSocket.Foreign
@@ -131,8 +146,8 @@ library
131146
132147 if flag(use-template-haskell)
133148 build-depends:
134− dependent-sum >= 0.3 && < 0.5,
135− dependent-sum-template >= 0.0.0.5 && < 0.1,
149+ dependent-sum >= 0.6,
150+ dependent-sum-template >= 0.1 && < 0.2,
136151 template-haskell
137152 other-extensions: TemplateHaskell
138153 cpp-options: -DUSE_TEMPLATE_HASKELL
@@ -140,7 +155,7 @@ library
140155 Reflex.Dom.Builder.Class.TH
141156 else
142157 build-depends:
143− dependent-sum == 0.4.*
158+ dependent-sum == 0.6.*
144159
145160test-suite hlint
146161 build-depends: base, hlint
@@ -148,16 +163,64 @@ test-suite hlint
148163 main-is: hlint.hs
149164 type: exitcode-stdio-1.0
150165
151−test-suite gc
166+test-suite hydration
152167 build-depends: base
168+ , aeson
169+ , bytestring
170+ , chrome-test-utils
171+ , constraints
172+ , constraints-extras
173+ , containers
174+ , dependent-map
175+ , dependent-sum
176+ , dependent-sum-template
177+ , directory
178+ , exceptions
179+ , filepath
180+ , ghcjs-dom
181+ , hspec
182+ , hspec-core
183+ , hspec-webdriver
184+ , http-types
185+ , HUnit
186+ , jsaddle
187+ , jsaddle-warp
188+ , lens
189+ , lifted-base
190+ , network
191+ , random
192+ , ref-tf
153193 , reflex
154194 , reflex-dom-core
195+ , process
196+ , silently
197+ , temporary
198+ , text
199+ , wai
200+ , wai-websockets
201+ , warp
202+ , webdriver
203+ , websockets
204+ , which
205+ hs-source-dirs: test
206+ ghc-options: -rtsopts -with-rtsopts=-T -Wall -fwarn-tabs -funbox-strict-fields -O2 -ferror-spans
207+ ghc-prof-options: -fprof-auto -optP-DPROFILING
208+ main-is: hydration.hs
209+ type: exitcode-stdio-1.0
210+ if !os(linux) || !arch(x86_64) || flag(profile-reflex)
211+ buildable: False
212+
213+-- broken test on base 4.11 & ghc 8.4.3
214+-- needs to be updated for changes in GHC.Stats
215+test-suite gc
216+ build-depends: base
217+ , chrome-test-utils
155218 , jsaddle
156219 , jsaddle-warp
157− , linux-namespaces
158220 , process
159− , temporary
160− , unix
221+ , reflex
222+ , reflex-dom-core
223+ , text
161224 hs-source-dirs: test
162225 ghc-options: -rtsopts -with-rtsopts=-T -Wall -fwarn-tabs -funbox-strict-fields -O2 -ferror-spans
163226 ghc-prof-options: -fprof-auto -optP-DPROFILING
reflex-dom-core/src/Foreign/JavaScript/Orphans.hs+2
@@ -1,6 +1,8 @@
11{-# OPTIONS_HADDOCK hide #-}
22{-# LANGUAGE CPP #-}
33{-# LANGUAGE FlexibleContexts #-}
4+{-# LANGUAGE UndecidableInstances #-}
5+
46{-# OPTIONS_GHC -fno-warn-orphans #-}
57module Foreign.JavaScript.Orphans where
68
reflex-dom-core/src/Foreign/JavaScript/TH.hs+2−4
@@ -6,11 +6,9 @@
66{-# LANGUAGE FlexibleInstances #-}
77{-# LANGUAGE FunctionalDependencies #-}
88{-# LANGUAGE GeneralizedNewtypeDeriving #-}
9−{-# LANGUAGE MultiParamTypeClasses #-}
109{-# LANGUAGE RankNTypes #-}
1110{-# LANGUAGE StandaloneDeriving #-}
1211{-# LANGUAGE TypeFamilies #-}
13−{-# LANGUAGE TypeSynonymInstances #-}
1412{-# LANGUAGE UndecidableInstances #-}
1513#ifdef USE_TEMPLATE_HASKELL
1614{-# LANGUAGE TemplateHaskell #-}
@@ -294,7 +292,7 @@ class Monad m => MonadJS x m | m -> x where
294292#ifdef ghcjs_HOST_OS
295293
296294data JSCtx_IO
297−type HasJS' = HasJS JSCtx_IO
295+type JS' = JSCtx_IO
298296
299297instance MonadIO m => HasJS JSCtx_IO (WithJSContextSingleton x m) where
300298 type JSX (WithJSContextSingleton x m) = IO
@@ -345,7 +343,7 @@ foreign import javascript unsafe "function(){ return $1(arguments); }" funWithAr
345343#else
346344
347345data JSCtx_JavaScriptCore x
348−type HasJS' = HasJS (JSCtx_JavaScriptCore ())
346+type JS' = JSCtx_JavaScriptCore ()
349347
350348instance IsJSContext (JSCtx_JavaScriptCore x) where
351349 newtype JSRef (JSCtx_JavaScriptCore x) = JSRef_JavaScriptCore { unJSRef_JavaScriptCore :: JSVal }
reflex-dom-core/src/Reflex/Dom/Builder/Class.hs+54−5
@@ -11,10 +11,9 @@
1111{-# LANGUAGE ImpredicativeTypes #-}
1212#endif
1313{-# LANGUAGE LambdaCase #-}
14−{-# LANGUAGE MultiParamTypeClasses #-}
1514{-# LANGUAGE OverloadedStrings #-}
1615{-# LANGUAGE PolyKinds #-}
17−{-# LANGUAGE RankNTypes #-}
16+{-# LANGUAGE Rank2Types #-}
1817{-# LANGUAGE RecursiveDo #-}
1918{-# LANGUAGE ScopedTypeVariables #-}
2019{-# LANGUAGE StandaloneDeriving #-}
@@ -35,6 +34,7 @@ import Reflex.Dom.Builder.Class.Events
3534#ifdef USE_TEMPLATE_HASKELL
3635import Reflex.Dom.Builder.Class.TH
3736#endif
37+import Reflex.BehaviorWriter.Base
3838import Reflex.DynamicWriter.Base
3939import Reflex.EventWriter.Base
4040import Reflex.NotReady.Class
@@ -61,13 +61,14 @@ import Data.String
6161import Data.Text (Text)
6262import Data.Type.Coercion
6363import GHCJS.DOM.Types (JSM)
64+import qualified GHCJS.DOM.Types as DOM
6465
6566class Default (EventSpec d EventResult) => DomSpace d where
6667 type EventSpec d :: (EventTag -> *) -> *
6768 type RawDocument d :: *
6869 type RawTextNode d :: *
70+ type RawCommentNode d :: *
6971 type RawElement d :: *
70− type RawFile d :: *
7172 type RawInputElement d :: *
7273 type RawTextAreaElement d :: *
7374 type RawSelectElement d :: *
@@ -86,6 +87,15 @@ class (Monad m, Reflex t, DomSpace (DomBuilderSpace m), NotReady t m, Adjustable
8687 => TextNodeConfig t -> m (TextNode (DomBuilderSpace m) t)
8788 textNode = lift . textNode
8889 {-# INLINABLE textNode #-}
90+ commentNode :: CommentNodeConfig t -> m (CommentNode (DomBuilderSpace m) t)
91+ default commentNode :: ( MonadTrans f
92+ , m ~ f m'
93+ , DomBuilderSpace m' ~ DomBuilderSpace m
94+ , DomBuilder t m'
95+ )
96+ => CommentNodeConfig t -> m (CommentNode (DomBuilderSpace m) t)
97+ commentNode = lift . commentNode
98+ {-# INLINABLE commentNode #-}
8999 element :: Text -> ElementConfig er t (DomBuilderSpace m) -> m a -> m (Element er (DomBuilderSpace m) t, a)
90100 default element :: ( MonadTransControl f
91101 , StT f a ~ a
@@ -175,6 +185,28 @@ newtype TextNode d t = TextNode
175185 { _textNode_raw :: RawTextNode d
176186 }
177187
188+data CommentNodeConfig t
189+ = CommentNodeConfig { _commentNodeConfig_initialContents :: {-# UNPACK #-} !Text
190+ , _commentNodeConfig_setContents :: !(Maybe (Event t Text))
191+ }
192+
193+#ifndef USE_TEMPLATE_HASKELL
194+commentNodeConfig_initialContents :: Lens' (CommentNodeConfig t) Text
195+commentNodeConfig_initialContents f (CommentNodeConfig a b) = (\a' -> CommentNodeConfig a' b) <$> f a
196+{-# INLINE commentNodeConfig_initialContents #-}
197+#endif
198+
199+instance (Reflex t) => Default (CommentNodeConfig t) where
200+ {-# INLINABLE def #-}
201+ def = CommentNodeConfig
202+ { _commentNodeConfig_initialContents = mempty
203+ , _commentNodeConfig_setContents = Nothing
204+ }
205+
206+newtype CommentNode d t = CommentNode
207+ { _commentNode_raw :: RawCommentNode d
208+ }
209+
178210data AttributeName = AttributeName !(Maybe Namespace) !Text deriving (Show, Read, Eq, Ord)
179211
180212mapKeysToAttributeName :: Map Text v -> Map AttributeName v
@@ -291,7 +323,7 @@ data InputElement er d t
291323 , _inputElement_hasFocus :: Dynamic t Bool
292324 , _inputElement_element :: Element er d t
293325 , _inputElement_raw :: RawInputElement d
294− , _inputElement_files :: Dynamic t [RawFile d]
326+ , _inputElement_files :: Dynamic t [DOM.File]
295327 }
296328
297329data TextAreaElementConfig er t m
@@ -393,6 +425,7 @@ data SelectElement er d t = SelectElement
393425#ifdef USE_TEMPLATE_HASKELL
394426concat <$> mapM (uncurry makeLensesWithoutField)
395427 [ (["_textNodeConfig_setContents"], ''TextNodeConfig)
428+ , (["_commentNodeConfig_setContents"], ''CommentNodeConfig)
396429 , ([ "_inputElementConfig_setValue"
397430 , "_inputElementConfig_setChecked" ], ''InputElementConfig)
398431 , (["_rawElementConfig_modifyAttributes"], ''RawElementConfig)
@@ -409,6 +442,13 @@ textNodeConfig_setContents =
409442 setter t e = t { _textNodeConfig_setContents = Just e }
410443 in lens getter setter
411444
445+-- | 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'.
446+commentNodeConfig_setContents :: Reflex t => Lens (CommentNodeConfig t) (CommentNodeConfig t) (Event t Text) (Event t Text)
447+commentNodeConfig_setContents =
448+ let getter = fromMaybe never . _commentNodeConfig_setContents
449+ setter t e = t { _commentNodeConfig_setContents = Just e }
450+ in lens getter setter
451+
412452-- | 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'.
413453inputElementConfig_setValue :: Reflex t => Lens' (InputElementConfig er t m) (Event t Text)
414454inputElementConfig_setValue =
@@ -521,6 +561,7 @@ instance (MountableDomBuilder t m, PerformEvent t m, MonadFix m, MonadHold t m)
521561instance (DomBuilder t m, Monoid w, MonadHold t m, MonadFix m) => DomBuilder t (DynamicWriterT t w m) where
522562 type DomBuilderSpace (DynamicWriterT t w m) = DomBuilderSpace m
523563 textNode = liftTextNode
564+ commentNode = liftCommentNode
524565 element elementTag cfg (DynamicWriterT child) = DynamicWriterT $ do
525566 s <- get
526567 (el, (a, newS)) <- lift $ element elementTag cfg $ runStateT child s
@@ -539,6 +580,7 @@ instance (DomBuilder t m, Monoid w, MonadHold t m, MonadFix m) => DomBuilder t (
539580instance (DomBuilder t m, MonadHold t m, MonadFix m) => DomBuilder t (RequesterT t request response m) where
540581 type DomBuilderSpace (RequesterT t request response m) = DomBuilderSpace m
541582 textNode = liftTextNode
583+ commentNode = liftCommentNode
542584 element elementTag cfg (RequesterT child) = RequesterT $ do
543585 r <- ask
544586 old <- get
@@ -559,6 +601,7 @@ instance (DomBuilder t m, MonadHold t m, MonadFix m) => DomBuilder t (RequesterT
559601instance (DomBuilder t m, MonadHold t m, MonadFix m, Semigroup w) => DomBuilder t (EventWriterT t w m) where
560602 type DomBuilderSpace (EventWriterT t w m) = DomBuilderSpace m
561603 textNode = liftTextNode
604+ commentNode = liftCommentNode
562605 element elementTag cfg (EventWriterT child) = EventWriterT $ do
563606 old <- get
564607 (el, (a, new)) <- lift $ element elementTag cfg $ runStateT child old
@@ -574,9 +617,10 @@ instance (DomBuilder t m, MonadHold t m, MonadFix m, Semigroup w) => DomBuilder
574617 placeRawElement = lift . placeRawElement
575618 wrapRawElement e = lift . wrapRawElement e
576619
577−instance (DomBuilder t m, MonadFix m, MonadHold t m, Group q, Query q, Additive q) => DomBuilder t (QueryT t q m) where
620+instance (DomBuilder t m, MonadFix m, MonadHold t m, Group q, Query q, Additive q, Eq q) => DomBuilder t (QueryT t q m) where
578621 type DomBuilderSpace (QueryT t q m) = DomBuilderSpace m
579622 textNode = liftTextNode
623+ commentNode = liftCommentNode
580624 element elementTag cfg (QueryT child) = QueryT $ do
581625 s <- get
582626 (e, (a, newS)) <- lift $ element elementTag cfg $ runStateT child s
@@ -645,6 +689,9 @@ liftWithStateless a = liftWith $ \run -> a $ fmap (fromStT (Proxy :: Proxy t)) .
645689liftTextNode :: (MonadTrans f, DomBuilder t m) => TextNodeConfig t -> f m (TextNode (DomBuilderSpace m) t)
646690liftTextNode = lift . textNode
647691
692+liftCommentNode :: (MonadTrans f, DomBuilder t m) => CommentNodeConfig t -> f m (CommentNode (DomBuilderSpace m) t)
693+liftCommentNode = lift . commentNode
694+
648695liftElement :: LiftDomBuilder t f m => Text -> ElementConfig er t (DomBuilderSpace m) -> f m a -> f m (Element er (DomBuilderSpace m) t, a)
649696liftElement elementTag cfg child = liftWithStateless $ \run -> element elementTag cfg $ run child
650697
@@ -668,7 +715,9 @@ instance DomRenderHook t m => DomRenderHook t (Lazy.StateT e m) where
668715 requestDomAction = lift . requestDomAction
669716 requestDomAction_ = lift . requestDomAction_
670717
718+deriving instance DomRenderHook t m => DomRenderHook t (BehaviorWriterT t w m)
671719deriving instance DomRenderHook t m => DomRenderHook t (EventWriterT t w m)
720+deriving instance DomRenderHook t m => DomRenderHook t (DynamicWriterT t w m)
672721deriving instance DomRenderHook t m => DomRenderHook t (RequesterT t req rsp m)
673722deriving instance DomRenderHook t m => DomRenderHook t (PostBuildT t m)
674723deriving instance DomRenderHook t m => DomRenderHook t (QueryT t q m)
reflex-dom-core/src/Reflex/Dom/Builder/Class/Events.hs−1
@@ -1,7 +1,6 @@
11{-# LANGUAGE CPP #-}
22{-# LANGUAGE DataKinds #-}
33{-# LANGUAGE GADTs #-}
4−{-# LANGUAGE KindSignatures #-}
54{-# LANGUAGE PolyKinds #-}
65#ifdef USE_TEMPLATE_HASKELL
76{-# LANGUAGE TemplateHaskell #-}
reflex-dom-core/src/Reflex/Dom/Builder/Hydratable.hsnew file+116
@@ -0,0 +1,116 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE FlexibleInstances #-}
3+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
4+{-# LANGUAGE MultiParamTypeClasses #-}
5+{-# LANGUAGE OverloadedStrings #-}
6+{-# LANGUAGE StandaloneDeriving #-}
7+{-# LANGUAGE TypeFamilies #-}
8+{-# LANGUAGE UndecidableInstances #-}
9+module Reflex.Dom.Builder.Hydratable where
10+
11+import Control.Monad.Fix
12+import Control.Monad.Primitive
13+import Control.Monad.Ref
14+import Control.Monad.Trans
15+import Control.Monad.Trans.Control
16+import Data.Coerce
17+import qualified Data.Map as Map
18+import Foreign.JavaScript.TH
19+#ifndef ghcjs_HOST_OS
20+import GHCJS.DOM.Types (MonadJSM (..))
21+#endif
22+import Reflex
23+import Reflex.Dom.Builder.Class
24+import Reflex.Dom.Builder.Immediate (HasDocument (..))
25+import Reflex.Host.Class
26+
27+-- | A DomBuilder transformer that adds "data-ssr" to all elements such that the
28+-- hydration builder knows which bits of DOM were added by us, and which were
29+-- added by external scripts.
30+newtype HydratableT m a = HydratableT { runHydratableT :: m a } deriving (Functor, Applicative, Monad, MonadAtomicRef, MonadFix, MonadIO)
31+
32+#ifndef ghcjs_HOST_OS
33+instance MonadJSM m => MonadJSM (HydratableT m) where
34+ liftJSM' = HydratableT . liftJSM'
35+#endif
36+
37+deriving instance MonadSample t m => MonadSample t (HydratableT m)
38+deriving instance MonadHold t m => MonadHold t (HydratableT m)
39+
40+instance MonadTrans HydratableT where
41+ lift = HydratableT
42+
43+instance MonadTransControl HydratableT where
44+ type StT HydratableT a = a
45+ liftWith f = HydratableT $ f runHydratableT
46+ restoreT = HydratableT
47+
48+instance MonadRef m => MonadRef (HydratableT m) where
49+ type Ref (HydratableT m) = Ref m
50+ newRef = lift . newRef
51+ readRef = lift . readRef
52+ writeRef ref = lift . writeRef ref
53+
54+instance PerformEvent t m => PerformEvent t (HydratableT m) where
55+ type Performable (HydratableT m) = Performable m
56+ performEvent_ = lift . performEvent_
57+ performEvent = lift . performEvent
58+
59+instance PrimMonad m => PrimMonad (HydratableT m) where
60+ type PrimState (HydratableT m) = PrimState m
61+ primitive = lift . primitive
62+
63+makeHydratable :: Reflex t => ElementConfig er t m -> ElementConfig er t m
64+makeHydratable cfg = cfg
65+ { _elementConfig_initialAttributes = Map.insert "data-ssr" "" $ _elementConfig_initialAttributes cfg
66+ , _elementConfig_modifyAttributes = fmap (Map.delete "data-ssr") <$> _elementConfig_modifyAttributes cfg
67+ }
68+
69+instance PostBuild t m => PostBuild t (HydratableT m) where
70+ getPostBuild = lift getPostBuild
71+
72+deriving instance TriggerEvent t m => TriggerEvent t (HydratableT m)
73+
74+instance MonadReflexCreateTrigger t m => MonadReflexCreateTrigger t (HydratableT m) where
75+ newEventWithTrigger = lift . newEventWithTrigger
76+ newFanEventWithTrigger f = lift $ newFanEventWithTrigger f
77+
78+instance Adjustable t m => Adjustable t (HydratableT m) where
79+ runWithReplace a0 a' = HydratableT $ runWithReplace (coerce a0) (coerceEvent a')
80+ traverseDMapWithKeyWithAdjust f dm0 dm' = HydratableT $ traverseDMapWithKeyWithAdjust (\k v -> runHydratableT $ f k v) (coerce dm0) (coerceEvent dm')
81+ traverseDMapWithKeyWithAdjustWithMove f dm0 dm' = HydratableT $ traverseDMapWithKeyWithAdjustWithMove (\k v -> runHydratableT $ f k v) (coerce dm0) (coerceEvent dm')
82+ traverseIntMapWithKeyWithAdjust f m0 m' = HydratableT $ traverseIntMapWithKeyWithAdjust (\k v -> runHydratableT $ f k v) (coerce m0) (coerceEvent m')
83+
84+instance NotReady t m => NotReady t (HydratableT m) where
85+ notReadyUntil = lift . notReadyUntil
86+ notReady = lift notReady
87+
88+instance DomBuilder t m => DomBuilder t (HydratableT m) where
89+ type DomBuilderSpace (HydratableT m) = DomBuilderSpace m
90+ element t cfg = lift . element t (makeHydratable cfg) . runHydratableT
91+ inputElement cfg = lift $ inputElement $ cfg
92+ { _inputElementConfig_elementConfig = makeHydratable $ _inputElementConfig_elementConfig cfg
93+ }
94+ textAreaElement cfg = lift $ textAreaElement $ cfg
95+ { _textAreaElementConfig_elementConfig = makeHydratable $ _textAreaElementConfig_elementConfig cfg
96+ }
97+ selectElement cfg child = do
98+ let cfg' = cfg
99+ { _selectElementConfig_elementConfig = makeHydratable $ _selectElementConfig_elementConfig cfg
100+ }
101+ lift $ selectElement cfg' $ runHydratableT child
102+
103+instance HasDocument m => HasDocument (HydratableT m)
104+
105+instance HasJSContext m => HasJSContext (HydratableT m) where
106+ type JSContextPhantom (HydratableT m) = JSContextPhantom m
107+ askJSContext = lift askJSContext
108+
109+instance HasJS js m => HasJS js (HydratableT m) where
110+ type JSX (HydratableT m) = JSX m
111+ liftJS = lift . liftJS
112+
113+instance DomRenderHook t m => DomRenderHook t (HydratableT m) where
114+ withRenderHook f = HydratableT . withRenderHook f . runHydratableT
115+ requestDomAction = HydratableT . requestDomAction
116+ requestDomAction_ = HydratableT . requestDomAction_
reflex-dom-core/src/Reflex/Dom/Builder/Immediate.hs+1651−563
@@ -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_
260+
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 (fmap Identity) 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
195360
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
201− return (ffor req $ \rm -> hook $ traverseRequesterData (\r -> Identity <$> r) rm, x)
202− requestDomAction = ImmediateDomBuilderT . lift . requestingIdentity
203− requestDomAction_ = ImmediateDomBuilderT . lift . requesting_
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
365+ return (ffor req $ \rm -> hook $ traverseRequesterData (fmap Identity) rm, x)
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,753 @@ 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 skipAttr = "data-hydration-skip" :: DOM.JSString
768+ ssrAttr = "data-ssr" :: DOM.JSString
769+ shouldSkip :: DOM.Element -> HydrationRunnerT t m Bool
770+ shouldSkip e = case cfg ^. namespace of
771+ Nothing -> do
772+ skip <- hasAttribute e skipAttr
773+ ssr <- hasAttribute e ssrAttr
774+ pure $ skip || not ssr
775+ Just ns -> do
776+ skip <- hasAttributeNS e (Just ns) skipAttr
777+ ssr <- hasAttributeNS e (Just ns) ssrAttr
778+ pure $ skip || not ssr
779+ childDom <- liftIO $ readIORef childDelayedRef
780+ let rawCfg = extractRawElementConfig cfg
781+ doc <- askDocument
782+ addHydrationStep $ do
783+ parent <- askParent
784+ lastHydrationNode <- getPreviousNode
785+ let go mLastNode = maybe (Node.getFirstChild parent) Node.getNextSibling mLastNode >>= \case
786+ Nothing -> do -- ran out of nodes, create the element
787+ HydrationRunnerT $ modify' $ \s -> s { _hydrationState_failed = True }
788+ e <- makeElement doc elementTag cfg
789+ insertAfterPreviousNode e
790+ pure e
791+ Just node -> DOM.castTo DOM.Element node >>= \case
792+ Nothing -> go (Just node) -- this node is not an element, skip
793+ Just e -> shouldSkip e >>= \case
794+ True -> go (Just node) -- this element is explicitly marked for being skipped by hydration
795+ False -> do
796+ t <- Element.getTagName e
797+ -- TODO: check attributes?
798+ if T.toCaseFold elementTag == T.toCaseFold t
799+ then pure e
800+ -- we came to some other statically rendered element, so something has gone wrong
801+ else do
802+ HydrationRunnerT $ modify' $ \s -> s { _hydrationState_failed = True }
803+ n <- makeElement doc elementTag cfg
804+ insertAfterPreviousNode n
805+ pure n
806+ e <- go lastHydrationNode
807+ setPreviousNode $ Just $ toNode e
808+ -- Update the parent node used by the children
809+ liftIO $ writeIORef parentRef $ toNode e
810+ liftIO $ writeIORef e' e
811+ -- Setup events, store the result so we can wait on it later
812+ refs <- wrap events e rawCfg
813+ liftIO $ putMVar wrapResult (e, refs)
814+ localRunner childDom Nothing $ toNode e
815+ -- We need the EventSelector to switch to the real event handler after activation
816+ es <- newFanEventWithTrigger $ \(WrapArg en) t -> do
817+ cleanup <- newEmptyMVar
818+ threadId <- forkIO $ do
819+ -- Wait on the data we need from the delayed action
820+ (e, eventTriggerRefs) <- readMVar wrapResult
821+ bracketOnError
822+ -- Run the setup, acquiring the cleanup action
823+ (triggerBody ctx rawCfg events eventTriggerRefs e (WrapArg en) t)
824+ -- Run the cleanup, if we have it - but only when an exception is
825+ -- raised (we might get killed between acquiring the cleanup action
826+ -- from 'triggerBody' and putting it in the MVar)
827+ id
828+ -- Try to put this action into the cleanup MVar
829+ (putMVar cleanup)
830+ pure $ do
831+ tryReadMVar cleanup >>= \case
832+ Nothing -> killThread threadId
833+ Just c -> c
834+ return ((Element es (), result), e')
835+
836+{-# SPECIALIZE hydrateElement
837+ :: Text
838+ -> ElementConfig er DomTimeline HydrationDomSpace
839+ -> HydrationDomBuilderT HydrationDomSpace DomTimeline HydrationM a
840+ -> HydrationDomBuilderT HydrationDomSpace DomTimeline HydrationM ((Element er HydrationDomSpace DomTimeline, a), IORef DOM.Element)
841+ #-}
842+
843+{-# INLINE inputElementImmediate #-}
844+inputElementImmediate
845+ :: ( RawDocument (DomBuilderSpace (HydrationDomBuilderT s t m)) ~ Document, EventSpec s ~ GhcjsEventSpec
846+ , MonadJSM m, Reflex t, MonadReflexCreateTrigger t m, MonadFix m, MonadHold t m
847+ , MonadRef m, Ref m ~ IORef )
848+ => InputElementConfig er t s -> HydrationDomBuilderT s t m (InputElement er GhcjsDomSpace t)
849+inputElementImmediate cfg = do
850+ (e@(Element eventSelector domElement), ()) <- elementImmediate "input" (_inputElementConfig_elementConfig cfg) $ return ()
851+ let domInputElement = uncheckedCastTo DOM.HTMLInputElement domElement
852+ Input.setValue domInputElement $ cfg ^. inputElementConfig_initialValue
853+ v0 <- Input.getValue domInputElement
854+ let getMyValue = Input.getValue domInputElement
855+ valueChangedByUI <- requestDomAction $ liftJSM getMyValue <$ Reflex.select eventSelector (WrapArg Input)
856+ valueChangedBySetValue <- case _inputElementConfig_setValue cfg of
857+ Nothing -> return never
858+ Just eSetValue -> requestDomAction $ ffor eSetValue $ \v' -> do
859+ Input.setValue domInputElement v'
860+ getMyValue -- We get the value after setting it in case the browser has mucked with it somehow
861+ v <- holdDyn v0 $ leftmost
862+ [ valueChangedBySetValue
863+ , valueChangedByUI
864+ ]
865+ Input.setChecked domInputElement $ _inputElementConfig_initialChecked cfg
866+ checkedChangedByUI <- wrapDomEvent domInputElement (`on` Events.click) $ do
867+ Input.getChecked domInputElement
868+ checkedChangedBySetChecked <- case _inputElementConfig_setChecked cfg of
869+ Nothing -> return never
870+ Just eNewchecked -> requestDomAction $ ffor eNewchecked $ \newChecked -> do
871+ oldChecked <- Input.getChecked domInputElement
872+ Input.setChecked domInputElement newChecked
873+ return $ if newChecked /= oldChecked
874+ then Just newChecked
875+ else Nothing
876+ c <- holdDyn (_inputElementConfig_initialChecked cfg) $ leftmost
877+ [ fmapMaybe id checkedChangedBySetChecked
878+ , checkedChangedByUI
879+ ]
880+ hasFocus <- mkHasFocus e
881+ files <- holdDyn mempty <=< wrapDomEvent domInputElement (`on` Events.change) $ do
882+ mfiles <- Input.getFiles domInputElement
883+ let getMyFiles xs = fmap catMaybes . mapM (FileList.item xs) . flip take [0..] . fromIntegral =<< FileList.getLength xs
884+ maybe (return []) getMyFiles mfiles
885+ checked <- holdUniqDyn c
886+ return $ InputElement
887+ { _inputElement_value = v
888+ , _inputElement_checked = checked
889+ , _inputElement_checkedChange = checkedChangedByUI
890+ , _inputElement_input = valueChangedByUI
891+ , _inputElement_hasFocus = hasFocus
892+ , _inputElement_element = e
893+ , _inputElement_raw = domInputElement
894+ , _inputElement_files = files
895+ }
896+
897+{-# INLINE inputElementInternal #-}
898+inputElementInternal
899+ :: ( MonadJSM m, Reflex t, MonadReflexCreateTrigger t m, MonadFix m, MonadHold t m
900+ , MonadRef m, Ref m ~ IORef )
901+ => InputElementConfig er t HydrationDomSpace -> HydrationDomBuilderT HydrationDomSpace t m (InputElement er HydrationDomSpace t)
902+inputElementInternal cfg = getHydrationMode >>= \case
903+ HydrationMode_Immediate -> ffor (inputElementImmediate cfg) $ \result -> result
904+ { _inputElement_element = Element (_element_events $ _inputElement_element result) ()
905+ , _inputElement_raw = ()
906+ }
907+ HydrationMode_Hydrating -> do
908+ ((e, _), domElementRef) <- hydrateElement "input" (cfg ^. inputElementConfig_elementConfig) $ return ()
909+ (valueChangedByUI, triggerChangeByUI) <- newTriggerEvent
910+ (valueChangedBySetValue, triggerChangeBySetValue) <- newTriggerEvent
911+ (focusChange, triggerFocusChange) <- newTriggerEvent
912+ (checkedChangedByUI, triggerCheckedChangedByUI) <- newTriggerEvent
913+ (checkedChangedBySetChecked, triggerCheckedChangedBySetChecked) <- newTriggerEvent
914+ (fileChange, triggerFileChange) <- newTriggerEvent
915+ doc <- askDocument
916+ -- Expected initial value from config
917+ let v0 = _inputElementConfig_initialValue cfg
918+ addHydrationStep $ do
919+ domElement <- liftIO $ readIORef domElementRef
920+ let domInputElement = uncheckedCastTo DOM.HTMLInputElement domElement
921+ getValue = Input.getValue domInputElement
922+ -- The browser might have messed with the value, or the user could have
923+ -- altered it before activation, so we set it if it isn't what we expect
924+ liftJSM getValue >>= \v0' -> do
925+ when (v0' /= v0) $ liftIO $ triggerChangeByUI v0'
926+ -- Watch for user interaction and trigger event accordingly
927+ requestDomAction_ $ (liftJSM getValue >>= liftIO . triggerChangeByUI) <$ Reflex.select (_element_events e) (WrapArg Input)
928+ for_ (_inputElementConfig_setValue cfg) $ \eSetValue ->
929+ requestDomAction_ $ ffor eSetValue $ \v' -> do
930+ Input.setValue domInputElement v'
931+ v <- getValue -- We get the value after setting it in case the browser has mucked with it somehow
932+ liftIO $ triggerChangeBySetValue v
933+ let focusChange' = leftmost
934+ [ False <$ Reflex.select (_element_events e) (WrapArg Blur)
935+ , True <$ Reflex.select (_element_events e) (WrapArg Focus)
936+ ]
937+ liftIO . triggerFocusChange =<< Node.isSameNode (toNode domElement) . fmap toNode =<< Document.getActiveElement doc
938+ requestDomAction_ $ liftIO . triggerFocusChange <$> focusChange'
939+ Input.setChecked domInputElement $ _inputElementConfig_initialChecked cfg
940+ _ <- liftJSM $ domInputElement `on` Events.click $ do
941+ liftIO . triggerCheckedChangedByUI =<< Input.getChecked domInputElement
942+ for_ (_inputElementConfig_setChecked cfg) $ \eNewchecked ->
943+ requestDomAction $ ffor eNewchecked $ \newChecked -> do
944+ oldChecked <- Input.getChecked domInputElement
945+ Input.setChecked domInputElement newChecked
946+ when (newChecked /= oldChecked) $ liftIO $ triggerCheckedChangedBySetChecked newChecked
947+ _ <- liftJSM $ domInputElement `on` Events.change $ do
948+ mfiles <- Input.getFiles domInputElement
949+ let getMyFiles xs = fmap catMaybes . mapM (FileList.item xs) . flip take [0..] . fromIntegral =<< FileList.getLength xs
950+ liftIO . triggerFileChange =<< maybe (return []) getMyFiles mfiles
951+ return ()
952+ checked' <- holdDyn (_inputElementConfig_initialChecked cfg) $ leftmost
953+ [ checkedChangedBySetChecked
954+ , checkedChangedByUI
955+ ]
956+ checked <- holdUniqDyn checked'
957+ let initialFocus = False -- Assume it isn't focused, but we update the actual focus state at switchover
958+ hasFocus <- holdUniqDyn =<< holdDyn initialFocus focusChange
959+ v <- holdDyn v0 $ leftmost
960+ [ valueChangedBySetValue
961+ , valueChangedByUI
962+ ]
963+ files <- holdDyn mempty fileChange
964+ return $ InputElement
965+ { _inputElement_value = v
966+ , _inputElement_checked = checked
967+ , _inputElement_checkedChange = checkedChangedByUI
968+ , _inputElement_input = valueChangedByUI
969+ , _inputElement_hasFocus = hasFocus
970+ , _inputElement_element = e
971+ , _inputElement_raw = ()
972+ , _inputElement_files = files
973+ }
974+
975+{-# INLINE textAreaElementImmediate #-}
976+textAreaElementImmediate
977+ :: ( RawDocument (DomBuilderSpace (HydrationDomBuilderT s t m)) ~ Document, EventSpec s ~ GhcjsEventSpec
978+ , MonadJSM m, Reflex t, MonadReflexCreateTrigger t m, MonadFix m, MonadHold t m
979+ , MonadRef m, Ref m ~ IORef )
980+ => TextAreaElementConfig er t s -> HydrationDomBuilderT s t m (TextAreaElement er GhcjsDomSpace t)
981+textAreaElementImmediate cfg = do
982+ (e@(Element eventSelector domElement), _) <- elementImmediate "textarea" (cfg ^. textAreaElementConfig_elementConfig) $ return ()
983+ let domTextAreaElement = uncheckedCastTo DOM.HTMLTextAreaElement domElement
984+ TextArea.setValue domTextAreaElement $ cfg ^. textAreaElementConfig_initialValue
985+ v0 <- TextArea.getValue domTextAreaElement
986+ let getMyValue = TextArea.getValue domTextAreaElement
987+ valueChangedByUI <- requestDomAction $ liftJSM getMyValue <$ Reflex.select eventSelector (WrapArg Input)
988+ valueChangedBySetValue <- case _textAreaElementConfig_setValue cfg of
989+ Nothing -> return never
990+ Just eSetValue -> requestDomAction $ ffor eSetValue $ \v' -> do
991+ TextArea.setValue domTextAreaElement v'
992+ getMyValue -- We get the value after setting it in case the browser has mucked with it somehow
993+ v <- holdDyn v0 $ leftmost
994+ [ valueChangedBySetValue
995+ , valueChangedByUI
996+ ]
997+ hasFocus <- mkHasFocus e
998+ return $ TextAreaElement
999+ { _textAreaElement_value = v
1000+ , _textAreaElement_input = valueChangedByUI
1001+ , _textAreaElement_hasFocus = hasFocus
1002+ , _textAreaElement_element = e
1003+ , _textAreaElement_raw = domTextAreaElement
1004+ }
1005+
1006+{-# INLINE textAreaElementInternal #-}
1007+textAreaElementInternal
1008+ :: ( MonadJSM m, Reflex t, MonadReflexCreateTrigger t m, MonadFix m, MonadHold t m
1009+ , MonadRef m, Ref m ~ IORef )
1010+ => TextAreaElementConfig er t HydrationDomSpace -> HydrationDomBuilderT HydrationDomSpace t m (TextAreaElement er HydrationDomSpace t)
1011+textAreaElementInternal cfg = getHydrationMode >>= \case
1012+ HydrationMode_Immediate -> ffor (textAreaElementImmediate cfg) $ \result -> result
1013+ { _textAreaElement_element = Element (_element_events $ _textAreaElement_element result) ()
1014+ , _textAreaElement_raw = ()
1015+ }
1016+ HydrationMode_Hydrating -> do
1017+ ((e, _), domElementRef) <- hydrateElement "textarea" (cfg ^. textAreaElementConfig_elementConfig) $ return ()
1018+ (valueChangedByUI, triggerChangeByUI) <- newTriggerEvent
1019+ (valueChangedBySetValue, triggerChangeBySetValue) <- newTriggerEvent
1020+ (focusChange, triggerFocusChange) <- newTriggerEvent
1021+ doc <- askDocument
1022+ -- Expected initial value from config
1023+ let v0 = _textAreaElementConfig_initialValue cfg
1024+ addHydrationStep $ do
1025+ domElement <- liftIO $ readIORef domElementRef
1026+ let domTextAreaElement = uncheckedCastTo DOM.HTMLTextAreaElement domElement
1027+ getValue = TextArea.getValue domTextAreaElement
1028+ -- The browser might have messed with the value, or the user could have
1029+ -- altered it before activation, so we set it if it isn't what we expect
1030+ liftJSM getValue >>= \v0' -> do
1031+ when (v0' /= v0) $ liftIO $ triggerChangeByUI v0'
1032+ -- Watch for user interaction and trigger event accordingly
1033+ requestDomAction_ $ (liftJSM getValue >>= liftIO . triggerChangeByUI) <$ Reflex.select (_element_events e) (WrapArg Input)
1034+ for_ (_textAreaElementConfig_setValue cfg) $ \eSetValue ->
1035+ requestDomAction_ $ ffor eSetValue $ \v' -> do
1036+ TextArea.setValue domTextAreaElement v'
1037+ v <- getValue -- We get the value after setting it in case the browser has mucked with it somehow
1038+ liftIO $ triggerChangeBySetValue v
1039+ let focusChange' = leftmost
1040+ [ False <$ Reflex.select (_element_events e) (WrapArg Blur)
1041+ , True <$ Reflex.select (_element_events e) (WrapArg Focus)
1042+ ]
1043+ liftIO . triggerFocusChange =<< Node.isSameNode (toNode domElement) . fmap toNode =<< Document.getActiveElement doc
1044+ requestDomAction_ $ liftIO . triggerFocusChange <$> focusChange'
1045+ let initialFocus = False -- Assume it isn't focused, but we update the actual focus state at switchover
1046+ hasFocus <- holdUniqDyn =<< holdDyn initialFocus focusChange
1047+ v <- holdDyn v0 $ leftmost
1048+ [ valueChangedBySetValue
1049+ , valueChangedByUI
1050+ ]
1051+ return $ TextAreaElement
1052+ { _textAreaElement_value = v
1053+ , _textAreaElement_input = valueChangedByUI
1054+ , _textAreaElement_hasFocus = hasFocus
1055+ , _textAreaElement_element = e
1056+ , _textAreaElement_raw = ()
1057+ }
1058+
1059+{-# INLINE selectElementImmediate #-}
1060+selectElementImmediate
1061+ :: ( EventSpec s ~ GhcjsEventSpec, RawDocument (DomBuilderSpace (HydrationDomBuilderT s t m)) ~ Document
1062+ , MonadJSM m, Reflex t, MonadReflexCreateTrigger t m, MonadFix m, MonadHold t m )
1063+ => SelectElementConfig er t s
1064+ -> HydrationDomBuilderT s t m a
1065+ -> HydrationDomBuilderT s t m (SelectElement er GhcjsDomSpace t, a)
1066+selectElementImmediate cfg child = do
1067+ (e@(Element eventSelector domElement), result) <- elementImmediate "select" (cfg ^. selectElementConfig_elementConfig) child
1068+ let domSelectElement = uncheckedCastTo DOM.HTMLSelectElement domElement
1069+ Select.setValue domSelectElement $ cfg ^. selectElementConfig_initialValue
1070+ v0 <- Select.getValue domSelectElement
1071+ let getMyValue = Select.getValue domSelectElement
1072+ valueChangedByUI <- requestDomAction $ liftJSM getMyValue <$ Reflex.select eventSelector (WrapArg Change)
1073+ valueChangedBySetValue <- case _selectElementConfig_setValue cfg of
1074+ Nothing -> return never
1075+ Just eSetValue -> requestDomAction $ ffor eSetValue $ \v' -> do
1076+ Select.setValue domSelectElement v'
1077+ getMyValue -- We get the value after setting it in case the browser has mucked with it somehow
1078+ v <- holdDyn v0 $ leftmost
1079+ [ valueChangedBySetValue
1080+ , valueChangedByUI
1081+ ]
1082+ hasFocus <- mkHasFocus e
1083+ let wrapped = SelectElement
1084+ { _selectElement_value = v
1085+ , _selectElement_change = valueChangedByUI
1086+ , _selectElement_hasFocus = hasFocus
1087+ , _selectElement_element = e
1088+ , _selectElement_raw = domSelectElement
1089+ }
1090+ return (wrapped, result)
1091+
1092+{-# INLINE selectElementInternal #-}
1093+selectElementInternal
1094+ :: ( MonadJSM m, Reflex t, MonadReflexCreateTrigger t m, MonadFix m, MonadHold t m
1095+ , MonadRef m, Ref m ~ IORef )
1096+ => SelectElementConfig er t HydrationDomSpace
1097+ -> HydrationDomBuilderT HydrationDomSpace t m a
1098+ -> HydrationDomBuilderT HydrationDomSpace t m (SelectElement er HydrationDomSpace t, a)
1099+selectElementInternal cfg child = getHydrationMode >>= \case
1100+ HydrationMode_Immediate -> ffor (selectElementImmediate cfg child) $ \(e, result) -> (e
1101+ { _selectElement_element = Element (_element_events $ _selectElement_element e) ()
1102+ , _selectElement_raw = ()
1103+ }, result)
1104+ HydrationMode_Hydrating -> do
1105+ ((e, result), domElementRef) <- hydrateElement "select" (cfg ^. selectElementConfig_elementConfig) child
1106+ (valueChangedByUI, triggerChangeByUI) <- newTriggerEvent
1107+ (valueChangedBySetValue, triggerChangeBySetValue) <- newTriggerEvent
1108+ (focusChange, triggerFocusChange) <- newTriggerEvent
1109+ doc <- askDocument
1110+ -- Expected initial value from config
1111+ let v0 = _selectElementConfig_initialValue cfg
1112+ addHydrationStep $ do
1113+ domElement <- liftIO $ readIORef domElementRef
1114+ let domSelectElement = uncheckedCastTo DOM.HTMLSelectElement domElement
1115+ getValue = Select.getValue domSelectElement
1116+ -- The browser might have messed with the value, or the user could have
1117+ -- altered it before activation, so we set it if it isn't what we expect
1118+ liftJSM getValue >>= \v0' -> do
1119+ when (v0' /= v0) $ liftIO $ triggerChangeByUI v0'
1120+ -- Watch for user interaction and trigger event accordingly
1121+ requestDomAction_ $ (liftJSM getValue >>= liftIO . triggerChangeByUI) <$ Reflex.select (_element_events e) (WrapArg Change)
1122+ for_ (_selectElementConfig_setValue cfg) $ \eSetValue ->
1123+ requestDomAction_ $ ffor eSetValue $ \v' -> do
1124+ Select.setValue domSelectElement v'
1125+ v <- getValue -- We get the value after setting it in case the browser has mucked with it somehow
1126+ liftIO $ triggerChangeBySetValue v
1127+ let focusChange' = leftmost
1128+ [ False <$ Reflex.select (_element_events e) (WrapArg Blur)
1129+ , True <$ Reflex.select (_element_events e) (WrapArg Focus)
1130+ ]
1131+ liftIO . triggerFocusChange =<< Node.isSameNode (toNode domElement) . fmap toNode =<< Document.getActiveElement doc
1132+ requestDomAction_ $ liftIO . triggerFocusChange <$> focusChange'
1133+ let initialFocus = False -- Assume it isn't focused, but we update the actual focus state at switchover
1134+ hasFocus <- holdUniqDyn =<< holdDyn initialFocus focusChange
1135+ v <- holdDyn v0 $ leftmost
1136+ [ valueChangedBySetValue
1137+ , valueChangedByUI
1138+ ]
1139+ return $ (,result) $ SelectElement
1140+ { _selectElement_value = v
1141+ , _selectElement_change = valueChangedByUI
1142+ , _selectElement_hasFocus = hasFocus
1143+ , _selectElement_element = e
1144+ , _selectElement_raw = ()
1145+ }
1146+
1147+{-# INLINE textNodeImmediate #-}
1148+textNodeImmediate
1149+ :: (RawDocument (DomBuilderSpace (HydrationDomBuilderT s t m)) ~ Document, MonadJSM m, Reflex t, MonadFix m)
1150+ => TextNodeConfig t -> HydrationDomBuilderT s t m DOM.Text
1151+textNodeImmediate (TextNodeConfig !t mSetContents) = do
1152+ p <- getParent
1153+ doc <- askDocument
1154+ n <- createTextNode doc t
1155+ appendChild_ p n
1156+ mapM_ (requestDomAction_ . fmap (setNodeValue n . Just)) mSetContents
1157+ pure n
1158+
1159+{-# SPECIALIZE textNodeImmediate
1160+ :: TextNodeConfig DomTimeline
1161+ -> HydrationDomBuilderT HydrationDomSpace DomTimeline HydrationM DOM.Text
1162+ #-}
1163+
1164+{-# SPECIALIZE textNodeImmediate
1165+ :: TextNodeConfig DomTimeline
1166+ -> HydrationDomBuilderT GhcjsDomSpace DomTimeline HydrationM DOM.Text
1167+ #-}
1168+
1169+{-# INLINE textNodeInternal #-}
1170+textNodeInternal
1171+ :: (Adjustable t m, MonadHold t m, MonadJSM m, MonadFix m, Reflex t)
1172+ => TextNodeConfig t -> HydrationDomBuilderT HydrationDomSpace t m (TextNode HydrationDomSpace t)
1173+textNodeInternal tc@(TextNodeConfig !t mSetContents) = do
1174+ doc <- askDocument
1175+ getHydrationMode >>= \case
1176+ HydrationMode_Immediate -> void $ textNodeImmediate tc
1177+ HydrationMode_Hydrating -> addHydrationStepWithSetup (maybe (pure $ pure t) (hold t) mSetContents) $ \currentText -> do
1178+ n <- hydrateTextNode doc =<< sample currentText
1179+ mapM_ (requestDomAction_ . fmap (setNodeValue n . Just)) mSetContents
1180+ pure $ TextNode ()
1181+
1182+{-# SPECIALIZE textNodeInternal
1183+ :: TextNodeConfig DomTimeline
1184+ -> HydrationDomBuilderT HydrationDomSpace DomTimeline HydrationM (TextNode HydrationDomSpace DomTimeline)
1185+ #-}
1186+
1187+-- | The static builder mashes adjacent text nodes into one node: we check the
1188+-- text content of each node we come to, comparing it to the content we
1189+-- expect. We also have a special case for empty text nodes - we always create
1190+-- the and add them after the previous node reference.
1191+{-# INLINE hydrateTextNode #-}
1192+hydrateTextNode :: MonadJSM m => Document -> Text -> HydrationRunnerT t m DOM.Text
1193+hydrateTextNode doc t@"" = do
1194+ tn <- createTextNode doc t
1195+ insertAfterPreviousNode tn
1196+ pure tn
1197+hydrateTextNode doc t = do
1198+ n <- join $ go <$> askParent <*> getPreviousNode
1199+ setPreviousNode $ Just $ toNode n
1200+ return n
1201+ where
1202+ go parent mLastNode = maybe (Node.getFirstChild parent) Node.getNextSibling mLastNode >>= \case
1203+ Nothing -> do
1204+ HydrationRunnerT $ modify' $ \s -> s { _hydrationState_failed = True }
1205+ n <- createTextNode doc t
1206+ insertAfterPreviousNode n
1207+ pure n
1208+ Just node -> DOM.castTo DOM.Text node >>= \case
1209+ Nothing -> go parent $ Just node
1210+ Just originalNode -> do
1211+ originalText <- Node.getTextContentUnchecked originalNode
1212+ case T.stripPrefix t originalText of
1213+ Just "" -> return originalNode
1214+ Just _ -> do
1215+ -- If we have the right prefix, we split the text node into a node containing the
1216+ -- required text and a subsequent sibling node containing the rest of the text.
1217+ DOM.splitText_ originalNode $ fromIntegral $ T.length t
1218+ return originalNode
1219+ Nothing -> do
1220+ HydrationRunnerT $ modify' $ \s -> s { _hydrationState_failed = True }
1221+ n <- createTextNode doc t
1222+ insertAfterPreviousNode n
1223+ pure n
1224+
1225+{-# INLINE commentNodeImmediate #-}
1226+commentNodeImmediate
1227+ :: (RawDocument (DomBuilderSpace (HydrationDomBuilderT s t m)) ~ Document, MonadJSM m, Reflex t, MonadFix m)
1228+ => CommentNodeConfig t -> HydrationDomBuilderT s t m DOM.Comment
1229+commentNodeImmediate (CommentNodeConfig !t mSetContents) = do
1230+ p <- getParent
1231+ doc <- askDocument
1232+ n <- createComment doc t
1233+ appendChild_ p n
1234+ mapM_ (requestDomAction_ . fmap (setNodeValue n . Just)) mSetContents
1235+ pure n
1236+
1237+{-# INLINE commentNodeInternal #-}
1238+commentNodeInternal
1239+ :: (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)
1240+ => CommentNodeConfig t -> HydrationDomBuilderT HydrationDomSpace t m (CommentNode HydrationDomSpace t)
1241+commentNodeInternal tc@(CommentNodeConfig t0 mSetContents) = do
1242+ doc <- askDocument
1243+ getHydrationMode >>= \case
1244+ HydrationMode_Immediate -> void $ commentNodeInternal tc
1245+ HydrationMode_Hydrating -> addHydrationStepWithSetup (maybe (pure $ pure t0) (hold t0) mSetContents) $ \bt -> do
1246+ t <- sample bt
1247+ void $ hydrateComment doc t mSetContents
1248+ pure $ CommentNode ()
1249+
1250+{-# INLINE hydrateComment #-}
1251+hydrateComment :: (MonadJSM m, Reflex t, MonadFix m) => Document -> Text -> Maybe (Event t Text) -> HydrationRunnerT t m DOM.Comment
1252+hydrateComment doc t mSetContents = do
1253+ parent <- askParent
1254+ let go mLastNode = maybe (Node.getFirstChild parent) Node.getNextSibling mLastNode >>= \case
1255+ Nothing -> do
1256+ c <- createComment doc t
1257+ insertAfterPreviousNode c
1258+ pure c
1259+ Just node -> DOM.castTo DOM.Comment node >>= \case
1260+ Nothing -> go (Just node)
1261+ Just c -> do
1262+ t' <- Node.getTextContentUnchecked c
1263+ if t == t'
1264+ then pure c
1265+ else do
1266+ c' <- createComment doc t
1267+ insertAfterPreviousNode c'
1268+ pure c'
1269+ n <- go =<< getPreviousNode
1270+ setPreviousNode $ Just $ toNode n
1271+ mapM_ (requestDomAction_ . fmap (setNodeValue n . Just)) mSetContents
1272+ pure n
1273+
1274+-- | We leave markers in the static builder as comments, and rip these comments
1275+-- out at hydration time, replacing them with empty text nodes.
1276+{-# INLINABLE skipToAndReplaceComment #-}
1277+skipToAndReplaceComment
1278+ :: (MonadJSM m, Reflex t, MonadFix m, Adjustable t m, MonadHold t m, RawDocument (DomBuilderSpace (HydrationDomBuilderT s t m)) ~ Document)
1279+ => Text
1280+ -> IORef Text
1281+ -> HydrationDomBuilderT s t m (HydrationRunnerT t m (), IORef DOM.Text, IORef Text)
1282+skipToAndReplaceComment prefix key0Ref = getHydrationMode >>= \case
1283+ HydrationMode_Immediate -> do
1284+ -- If we're in immediate mode, we don't try to replace an existing comment,
1285+ -- and just return a dummy key
1286+ t <- textNodeImmediate $ TextNodeConfig ("" :: Text) Nothing
1287+ append $ toNode t
1288+ textNodeRef <- liftIO $ newIORef t
1289+ keyRef <- liftIO $ newIORef ""
1290+ pure (pure (), textNodeRef, keyRef)
1291+ HydrationMode_Hydrating -> do
1292+ doc <- askDocument
1293+ textNodeRef <- liftIO $ newIORef $ error "textNodeRef not yet initialized"
1294+ keyRef <- liftIO $ newIORef $ error "keyRef not yet initialized"
1295+ let go key0 mLastNode = do
1296+ parent <- askParent
1297+ node <- maybe (Node.getFirstChildUnchecked parent) Node.getNextSiblingUnchecked mLastNode
1298+ DOM.castTo DOM.Comment node >>= \case
1299+ Just comment -> do
1300+ commentText <- Node.getTextContentUnchecked comment
1301+ case T.stripPrefix (prefix <> key0) commentText of
1302+ Just key -> do
1303+ -- Replace the comment with an (invisible) text node
1304+ tn <- createTextNode doc ("" :: Text)
1305+ Node.replaceChild_ parent tn comment
1306+ pure (tn, key)
1307+ Nothing -> do
1308+ go key0 (Just node)
1309+ Nothing -> do
1310+ go key0 (Just node)
1311+ switchComment = do
1312+ key0 <- liftIO $ readIORef key0Ref
1313+ (tn, key) <- go key0 =<< getPreviousNode
1314+ setPreviousNode $ Just $ toNode tn
1315+ liftIO $ do
1316+ writeIORef textNodeRef tn
1317+ writeIORef keyRef key
1318+ pure (switchComment, textNodeRef, keyRef)
1319+
1320+{-# INLINABLE skipToReplaceStart #-}
1321+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)
1322+skipToReplaceStart = skipToAndReplaceComment "replace-start" =<< liftIO (newIORef "")
1323+
1324+{-# INLINABLE skipToReplaceEnd #-}
1325+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)
1326+skipToReplaceEnd key = fmap (\(m,e,_) -> (m,e)) $ skipToAndReplaceComment "replace-end" key
1327+
1328+instance SupportsHydrationDomBuilder t m => NotReady t (HydrationDomBuilderT s t m) where
4421329 notReadyUntil e = do
4431330 eOnce <- headE e
444− env <- ImmediateDomBuilderT ask
445− let unreadyChildren = _immediateDomBuilderEnv_unreadyChildren env
1331+ unreadyChildren <- askUnreadyChildren
1332+ commitAction <- askCommitAction
4461333 liftIO $ modifyIORef' unreadyChildren succ
4471334 let ready = do
4481335 old <- liftIO $ readIORef unreadyChildren
4491336 let new = pred old
4501337 liftIO $ writeIORef unreadyChildren $! new
451− when (new == 0) $ _immediateDomBuilderEnv_commitAction env
1338+ when (new == 0) commitAction
4521339 requestDomAction_ $ ready <$ eOnce
4531340 notReady = do
454− env <- ImmediateDomBuilderT ask
455− let unreadyChildren = _immediateDomBuilderEnv_unreadyChildren env
1341+ unreadyChildren <- askUnreadyChildren
4561342 liftIO $ modifyIORef' unreadyChildren succ
4571343
458−instance SupportsImmediateDomBuilder t m => DomBuilder t (ImmediateDomBuilderT t m) where
459− type DomBuilderSpace (ImmediateDomBuilderT t m) = GhcjsDomSpace
1344+data HydrationDomSpace
1345+
1346+instance DomSpace HydrationDomSpace where
1347+ type EventSpec HydrationDomSpace = GhcjsEventSpec
1348+ type RawDocument HydrationDomSpace = DOM.Document
1349+ type RawTextNode HydrationDomSpace = ()
1350+ type RawCommentNode HydrationDomSpace = ()
1351+ type RawElement HydrationDomSpace = ()
1352+ type RawInputElement HydrationDomSpace = ()
1353+ type RawTextAreaElement HydrationDomSpace = ()
1354+ type RawSelectElement HydrationDomSpace = ()
1355+ addEventSpecFlags _ en f es = es
1356+ { _ghcjsEventSpec_filters =
1357+ let f' = Just . GhcjsEventFilter . \case
1358+ Nothing -> \evt -> do
1359+ mEventResult <- unGhcjsEventHandler (_ghcjsEventSpec_handler es) (en, evt)
1360+ return (f mEventResult, return mEventResult)
1361+ Just (GhcjsEventFilter oldFilter) -> \evt -> do
1362+ (oldFlags, oldContinuation) <- oldFilter evt
1363+ mEventResult <- oldContinuation
1364+ let newFlags = oldFlags <> f mEventResult
1365+ return (newFlags, return mEventResult)
1366+ in DMap.alter f' en $ _ghcjsEventSpec_filters es
1367+ }
1368+
1369+instance SupportsHydrationDomBuilder t m => DomBuilder t (HydrationDomBuilderT HydrationDomSpace t m) where
1370+ type DomBuilderSpace (HydrationDomBuilderT HydrationDomSpace t m) = HydrationDomSpace
1371+ {-# INLINABLE element #-}
1372+ element = elementInternal
4601373 {-# INLINABLE textNode #-}
461− textNode (TextNodeConfig initialContents mSetContents) = do
462− n <- textNodeInternal initialContents
463− mapM_ (requestDomAction_ . fmap (setNodeValue n . Just)) mSetContents
464− return $ TextNode n
1374+ textNode = textNodeInternal
1375+ {-# INLINABLE commentNode #-}
1376+ commentNode = commentNodeInternal
1377+ {-# INLINABLE inputElement #-}
1378+ inputElement = inputElementInternal
1379+ {-# INLINABLE textAreaElement #-}
1380+ textAreaElement = textAreaElementInternal
1381+ {-# INLINABLE selectElement #-}
1382+ selectElement = selectElementInternal
1383+ placeRawElement () = pure ()
1384+ wrapRawElement () _cfg = pure $ Element (EventSelector $ const never) ()
1385+
1386+instance SupportsHydrationDomBuilder t m => DomBuilder t (HydrationDomBuilderT GhcjsDomSpace t m) where
1387+ type DomBuilderSpace (HydrationDomBuilderT GhcjsDomSpace t m) = GhcjsDomSpace
4651388 {-# INLINABLE element #-}
466− element elementTag cfg child = fst <$> makeElement elementTag cfg child
1389+ element = elementImmediate
1390+ {-# INLINABLE textNode #-}
1391+ textNode = fmap TextNode . textNodeImmediate
1392+ {-# INLINABLE commentNode #-}
1393+ commentNode = fmap CommentNode . commentNodeImmediate
4671394 {-# 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− }
1395+ inputElement = inputElementImmediate
5191396 {-# 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− }
1397+ textAreaElement = textAreaElementImmediate
5441398 {-# 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)
1399+ selectElement = selectElementImmediate
5701400 placeRawElement = append . toNode
571− wrapRawElement = wrap
1401+ wrapRawElement e rawCfg = do
1402+ events <- askEvents
1403+ ctx <- askJSM
1404+ eventTriggerRefs <- wrap events e rawCfg
1405+ es <- newFanEventWithTrigger $ triggerBody ctx rawCfg events eventTriggerRefs e
1406+ pure $ Element es e
5721407
5731408data FragmentState
5741409 = FragmentState_Unmounted
@@ -588,21 +1423,21 @@ extractFragment fragment = do
5881423 extractBetweenExclusive (_immediateDomFragment_document fragment) before after
5891424 liftIO $ writeIORef (_immediateDomFragment_state fragment) FragmentState_Unmounted
5901425
591−instance SupportsImmediateDomBuilder t m => MountableDomBuilder t (ImmediateDomBuilderT t m) where
592− type DomFragment (ImmediateDomBuilderT t m) = ImmediateDomFragment
1426+instance SupportsHydrationDomBuilder t m => MountableDomBuilder t (HydrationDomBuilderT GhcjsDomSpace t m) where
1427+ type DomFragment (HydrationDomBuilderT GhcjsDomSpace t m) = ImmediateDomFragment
5931428 buildDomFragment w = do
5941429 df <- createDocumentFragment =<< askDocument
5951430 result <- flip localEnv w $ \env -> env
596− { _immediateDomBuilderEnv_parent = toNode df
1431+ { _hydrationDomBuilderEnv_parent = Left $ toNode df
5971432 }
5981433 state <- liftIO $ newIORef FragmentState_Unmounted
5991434 return (ImmediateDomFragment df state, result)
6001435 mountDomFragment fragment setFragment = do
601− parent <- askParent
1436+ parent <- getParent
6021437 extractFragment fragment
603− before <- textNodeInternal ("" :: Text)
1438+ before <- textNodeImmediate $ TextNodeConfig ("" :: Text) Nothing
6041439 appendChild_ parent $ _immediateDomFragment_document fragment
605− after <- textNodeInternal ("" :: Text)
1440+ after <- textNodeImmediate $ TextNodeConfig ("" :: Text) Nothing
6061441 xs <- foldDyn (\new (previous, _) -> (new, Just previous)) (fragment, Nothing) setFragment
6071442 requestDomAction_ $ ffor (updated xs) $ \(childFragment, Just previousFragment) -> do
6081443 extractFragment previousFragment
@@ -611,12 +1446,19 @@ instance SupportsImmediateDomBuilder t m => MountableDomBuilder t (ImmediateDomB
6111446 liftIO $ writeIORef (_immediateDomFragment_state childFragment) $ FragmentState_Mounted (before, after)
6121447 liftIO $ writeIORef (_immediateDomFragment_state fragment) $ FragmentState_Mounted (before, after)
6131448
614−instance (Reflex t, Adjustable t m, MonadJSM m, MonadHold t m, MonadFix m, PrimMonad m) => Adjustable t (ImmediateDomBuilderT t m) where
1449+instance (Reflex t, Monad m, Adjustable t m, MonadHold t m, MonadFix m) => Adjustable t (DomRenderHookT t m) where
1450+ runWithReplace a0 a' = DomRenderHookT $ runWithReplace (unDomRenderHookT a0) (fmapCheap unDomRenderHookT a')
1451+ traverseIntMapWithKeyWithAdjust f m = DomRenderHookT . traverseIntMapWithKeyWithAdjust (\k -> unDomRenderHookT . f k) m
1452+ traverseDMapWithKeyWithAdjust f m = DomRenderHookT . traverseDMapWithKeyWithAdjust (\k -> unDomRenderHookT . f k) m
1453+ traverseDMapWithKeyWithAdjustWithMove f m = DomRenderHookT . traverseDMapWithKeyWithAdjustWithMove (\k -> unDomRenderHookT . f k) m
1454+
1455+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
6151456 {-# INLINABLE runWithReplace #-}
6161457 runWithReplace a0 a' = do
617− initialEnv <- ImmediateDomBuilderT ask
618− before <- textNodeInternal ("" :: Text)
619− let parentUnreadyChildren = _immediateDomBuilderEnv_unreadyChildren initialEnv
1458+ initialEnv <- HydrationDomBuilderT ask
1459+ let hydrating = _hydrationDomBuilderEnv_hydrationMode initialEnv
1460+ (hydrateStart, before, beforeKey) <- skipToReplaceStart
1461+ let parentUnreadyChildren = _hydrationDomBuilderEnv_unreadyChildren initialEnv
6201462 haveEverBeenReady <- liftIO $ newIORef False
6211463 currentCohort <- liftIO $ newIORef (-1 :: Int) -- Equal to the cohort currently in the DOM
6221464 let myCommitAction = do
@@ -627,79 +1469,117 @@ instance (Reflex t, Adjustable t m, MonadJSM m, MonadHold t m, MonadFix m, PrimM
6271469 old <- liftIO $ readIORef parentUnreadyChildren
6281470 let new = pred old
6291471 liftIO $ writeIORef parentUnreadyChildren $! new
630− when (new == 0) $ _immediateDomBuilderEnv_commitAction initialEnv
1472+ when (new == 0) $ _hydrationDomBuilderEnv_commitAction initialEnv
6311473 -- We draw 'after' in this roundabout way to avoid using MonadFix
6321474 doc <- askDocument
633− after <- createTextNode doc ("" :: Text)
1475+ parent <- getParent
1476+ (hydrateEnd, after) <- skipToReplaceEnd beforeKey
6341477 let drawInitialChild = do
1478+ h <- liftIO $ readIORef hydrating
1479+ p' <- case h of
1480+ HydrationMode_Hydrating -> pure parent
1481+ HydrationMode_Immediate -> toNode <$> createDocumentFragment doc
6351482 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
1483+ let a0' = case h of
1484+ HydrationMode_Hydrating -> a0
1485+ HydrationMode_Immediate -> do
1486+ a <- a0
1487+ insertBefore p' =<< liftIO (readIORef after)
1488+ pure a
1489+ delayed <- case h of
1490+ HydrationMode_Hydrating -> liftIO $ newIORef $ pure ()
1491+ HydrationMode_Immediate -> pure $ _hydrationDomBuilderEnv_delayed initialEnv
1492+ result <- runReaderT (unHydrationDomBuilderT a0') initialEnv
1493+ { _hydrationDomBuilderEnv_unreadyChildren = unreadyChildren
1494+ , _hydrationDomBuilderEnv_commitAction = myCommitAction
1495+ , _hydrationDomBuilderEnv_parent = Left p'
1496+ , _hydrationDomBuilderEnv_delayed = delayed
6431497 }
1498+ dom <- case h of
1499+ HydrationMode_Hydrating -> liftIO $ readIORef delayed
1500+ HydrationMode_Immediate -> pure $ pure ()
6441501 liftIO $ readIORef unreadyChildren >>= \case
6451502 0 -> writeIORef haveEverBeenReady True
6461503 _ -> modifyIORef' parentUnreadyChildren succ
647− return result
1504+ return (dom, result)
6481505 a'' <- numberOccurrences a'
649− (result0, child') <- ImmediateDomBuilderT $ lift $ runWithReplace drawInitialChild $ ffor a'' $ \(cohortId, child) -> do
650− df <- createDocumentFragment doc
1506+ ((hydrate0, result0), child') <- HydrationDomBuilderT $ lift $ runWithReplace drawInitialChild $ ffor a'' $ \(cohortId, child) -> do
1507+ h <- liftIO $ readIORef hydrating
1508+ p' <- case h of
1509+ HydrationMode_Hydrating -> pure parent
1510+ HydrationMode_Immediate -> toNode <$> createDocumentFragment doc
6511511 unreadyChildren <- liftIO $ newIORef 0
6521512 let commitAction = do
6531513 c <- liftIO $ readIORef currentCohort
6541514 when (c <= cohortId) $ do -- If a newer cohort has already been committed, just ignore this
655− deleteBetweenExclusive before after
656− insertBefore df after
1515+ !before' <- liftIO $ readIORef before
1516+ !after' <- liftIO $ readIORef after
1517+ deleteBetweenExclusive before' after'
1518+ insertBefore p' after'
6571519 liftIO $ writeIORef currentCohort cohortId
6581520 myCommitAction
659− result <- runReaderT (unImmediateDomBuilderT child) $ initialEnv
660− { _immediateDomBuilderEnv_parent = toNode df
661− , _immediateDomBuilderEnv_unreadyChildren = unreadyChildren
662− , _immediateDomBuilderEnv_commitAction = commitAction
663− }
1521+ delayed <- case h of
1522+ HydrationMode_Hydrating -> liftIO $ newIORef $ pure ()
1523+ HydrationMode_Immediate -> pure $ _hydrationDomBuilderEnv_delayed initialEnv
1524+ result <- runReaderT (unHydrationDomBuilderT child) $ initialEnv
1525+ { _hydrationDomBuilderEnv_unreadyChildren = unreadyChildren
1526+ , _hydrationDomBuilderEnv_commitAction = case h of
1527+ HydrationMode_Hydrating -> myCommitAction
1528+ HydrationMode_Immediate -> commitAction
1529+ , _hydrationDomBuilderEnv_parent = Left p'
1530+ , _hydrationDomBuilderEnv_delayed = delayed
1531+ }
1532+ dom <- case h of
1533+ HydrationMode_Hydrating -> liftIO $ readIORef delayed
1534+ HydrationMode_Immediate -> pure $ pure ()
6641535 uc <- liftIO $ readIORef unreadyChildren
6651536 let commitActionToRunNow = if uc == 0
666− then Just commitAction
1537+ then Just $ commitAction
6671538 else Nothing -- A child will run it when unreadyChildren is decremented to 0
668− return (commitActionToRunNow, result)
669− requestDomAction_ $ fmapMaybe fst child'
1539+ actions = case h of
1540+ HydrationMode_Hydrating -> Left dom
1541+ HydrationMode_Immediate -> Right commitActionToRunNow
1542+ return (actions, result)
1543+ let (hydrate', commitAction) = fanEither $ fmap fst child'
1544+ addHydrationStepWithSetup (hold hydrate0 hydrate') $ \contents -> do
1545+ hydrateStart
1546+ join $ sample contents
1547+ hydrateEnd
1548+ requestDomAction_ $ fmapMaybe id commitAction
6701549 return (result0, snd <$> child')
1550+
6711551 {-# INLINABLE traverseIntMapWithKeyWithAdjust #-}
6721552 traverseIntMapWithKeyWithAdjust = traverseIntMapWithKeyWithAdjust'
6731553 {-# INLINABLE traverseDMapWithKeyWithAdjust #-}
6741554 traverseDMapWithKeyWithAdjust = traverseDMapWithKeyWithAdjust'
6751555 {-# INLINABLE traverseDMapWithKeyWithAdjustWithMove #-}
6761556 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)
1557+ let updateChildUnreadiness (p :: PatchDMapWithMove k (Compose (TraverseChild t m (Some k)) v')) old = do
1558+ 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)
6791559 new k = PatchDMapWithMove.nodeInfoMapFromM $ \case
680− PatchDMapWithMove.From_Insert (Compose (_, _, sRef, _)) -> do
681− readIORef sRef >>= \case
1560+ PatchDMapWithMove.From_Insert (Compose (TraverseChild (Left _hydration) _)) -> return PatchDMapWithMove.From_Delete
1561+ PatchDMapWithMove.From_Insert (Compose (TraverseChild (Right immediate) _)) -> do
1562+ readIORef (_traverseChildImmediate_childReadyState immediate) >>= \case
6821563 ChildReadyState_Ready -> return PatchDMapWithMove.From_Delete
6831564 ChildReadyState_Unready _ -> do
684− writeIORef sRef $ ChildReadyState_Unready $ Just $ Some.This k
685− return $ PatchDMapWithMove.From_Insert $ Constant sRef
1565+ writeIORef (_traverseChildImmediate_childReadyState immediate) $ ChildReadyState_Unready $ Just $ Some k
1566+ return $ PatchDMapWithMove.From_Insert $ Constant (_traverseChildImmediate_childReadyState immediate)
6861567 PatchDMapWithMove.From_Delete -> return PatchDMapWithMove.From_Delete
6871568 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)
1569+ deleteOrMove :: forall a. k a -> Product (Constant (IORef (ChildReadyState (Some k)))) (ComposeMaybe k) a -> IO (Constant () a)
6891570 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
1571+ writeIORef sRef $ ChildReadyState_Unready $ Some <$> mToKey -- This will be Nothing if deleting, and Just if moving, so it works out in both cases
6911572 return $ Constant ()
6921573 p' <- fmap unsafePatchDMapWithMove $ DMap.traverseWithKey new $ unPatchDMapWithMove p
6931574 _ <- DMap.traverseWithKey deleteOrMove $ PatchDMapWithMove.getDeletionsAndMoves p old
6941575 return $ applyAlways p' old
695− hoistTraverseWithKeyWithAdjust traverseDMapWithKeyWithAdjustWithMove mapPatchDMapWithMove updateChildUnreadiness $ \placeholders lastPlaceholderRef (p_ :: PatchDMapWithMove k (Compose ((,,,) DOM.DocumentFragment DOM.Text (IORef (ChildReadyState k))) v')) -> do
1576+ hoistTraverseWithKeyWithAdjust traverseDMapWithKeyWithAdjustWithMove mapPatchDMapWithMove updateChildUnreadiness $ \placeholders lastPlaceholder (p_ :: PatchDMapWithMove k (Compose (TraverseChild t m (Some k)) v')) -> do
6961577 let p = unPatchDMapWithMove p_
6971578 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)
1579+ let collectIfMoved :: forall a. k a -> PatchDMapWithMove.NodeInfo k (Compose (TraverseChild t m (Some k)) v') a -> JSM (Constant (Maybe DOM.DocumentFragment) a)
7001580 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
1581+ let mThisPlaceholder = Map.lookup (Some k) phsBefore -- Will be Nothing if this element wasn't present before
1582+ nextPlaceholder = maybe lastPlaceholder snd $ Map.lookupGT (Some k) phsBefore
7031583 case isJust $ getComposeMaybe $ PatchDMapWithMove._nodeInfo_to e of
7041584 False -> do
7051585 mapM_ (`deleteUpTo` nextPlaceholder) mThisPlaceholder
@@ -707,13 +1587,22 @@ instance (Reflex t, Adjustable t m, MonadJSM m, MonadHold t m, MonadFix m, PrimM
7071587 True -> do
7081588 Constant <$> mapM (`collectUpTo` nextPlaceholder) mThisPlaceholder
7091589 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)
1590+ let !phsAfter = fromMaybe phsBefore $ apply filtered phsBefore
1591+ weakened :: PatchMapWithMove (Some k) (Either (TraverseChildHydration t m) (TraverseChildImmediate (Some k)))
1592+ weakened = weakenPatchDMapWithMoveWith (_traverseChild_mode . getCompose) p_
1593+ filtered :: PatchMapWithMove (Some k) DOM.Text
1594+ filtered = PatchMapWithMove $ flip Map.mapMaybe (unPatchMapWithMove weakened) $ \(PatchMapWithMove.NodeInfo from to) -> flip PatchMapWithMove.NodeInfo to <$> case from of
1595+ PatchMapWithMove.From_Insert (Left _hydration) -> Nothing
1596+ PatchMapWithMove.From_Insert (Right immediate) -> Just $ PatchMapWithMove.From_Insert $ _traverseChildImmediate_placeholder immediate
1597+ PatchMapWithMove.From_Delete -> Just $ PatchMapWithMove.From_Delete
1598+ PatchMapWithMove.From_Move k -> Just $ PatchMapWithMove.From_Move k
1599+ let placeFragment :: forall a. k a -> PatchDMapWithMove.NodeInfo k (Compose (TraverseChild t m (Some k)) v') a -> JSM (Constant () a)
7121600 placeFragment k e = do
713− let nextPlaceholder = maybe lastPlaceholder snd $ Map.lookupGT (Some.This k) phsAfter
1601+ let nextPlaceholder = maybe lastPlaceholder snd $ Map.lookupGT (Some k) phsAfter
7141602 case PatchDMapWithMove._nodeInfo_from e of
715− PatchDMapWithMove.From_Insert (Compose (df, _, _, _)) -> do
716− df `insertBefore` nextPlaceholder
1603+ PatchDMapWithMove.From_Insert (Compose (TraverseChild x _)) -> case x of
1604+ Left _ -> pure ()
1605+ Right immediate -> _traverseChildImmediate_fragment immediate `insertBefore` nextPlaceholder
7171606 PatchDMapWithMove.From_Delete -> do
7181607 return ()
7191608 PatchDMapWithMove.From_Move fromKey -> do
@@ -724,105 +1613,166 @@ instance (Reflex t, Adjustable t m, MonadJSM m, MonadHold t m, MonadFix m, PrimM
7241613 liftIO $ writeIORef placeholders $! phsAfter
7251614
7261615{-# 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'))
1616+traverseDMapWithKeyWithAdjust'
1617+ :: 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)
1618+ => (forall a. k a -> v a -> HydrationDomBuilderT s t m (v' a))
1619+ -> DMap k v
1620+ -> Event t (PatchDMap k v)
1621+ -> HydrationDomBuilderT s t m (DMap k v', Event t (PatchDMap k v'))
7281622traverseDMapWithKeyWithAdjust' = 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)
1623+ let updateChildUnreadiness (p :: PatchDMap k (Compose (TraverseChild t m (Some k)) v')) old = do
1624+ let new :: forall a. k a -> ComposeMaybe (Compose (TraverseChild t m (Some k)) v') a -> IO (ComposeMaybe (Constant (IORef (ChildReadyState (Some k)))) a)
7311625 new k (ComposeMaybe m) = ComposeMaybe <$> case m of
7321626 Nothing -> return Nothing
733− Just (Compose (_, _, sRef, _)) -> do
734− readIORef sRef >>= \case
1627+ Just (Compose (TraverseChild (Left _hydration) _)) -> pure Nothing
1628+ Just (Compose (TraverseChild (Right immediate) _)) -> do
1629+ readIORef (_traverseChildImmediate_childReadyState immediate) >>= \case
7351630 ChildReadyState_Ready -> return Nothing -- Delete this child, since it's ready
7361631 ChildReadyState_Unready _ -> do
737− writeIORef sRef $ ChildReadyState_Unready $ Just $ Some.This k
738− return $ Just $ Constant sRef
1632+ writeIORef (_traverseChildImmediate_childReadyState immediate) $ ChildReadyState_Unready $ Just $ Some k
1633+ return $ Just $ Constant (_traverseChildImmediate_childReadyState immediate)
7391634 delete _ (Constant sRef) = do
7401635 writeIORef sRef $ ChildReadyState_Unready Nothing
7411636 return $ Constant ()
7421637 p' <- fmap PatchDMap $ DMap.traverseWithKey new $ unPatchDMap p
7431638 _ <- DMap.traverseWithKey delete $ PatchDMap.getDeletions p old
7441639 return $ applyAlways p' old
745− hoistTraverseWithKeyWithAdjust traverseDMapWithKeyWithAdjust mapPatchDMap updateChildUnreadiness $ \placeholders lastPlaceholderRef (PatchDMap p) -> do
1640+ hoistTraverseWithKeyWithAdjust traverseDMapWithKeyWithAdjust mapPatchDMap updateChildUnreadiness $ \placeholders lastPlaceholder (PatchDMap patch) -> do
7461641 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
1642+ forM_ (DMap.toList patch) $ \(k :=> ComposeMaybe mv) -> do
1643+ let nextPlaceholder = maybe lastPlaceholder snd $ Map.lookupGT (Some k) phs
1644+ -- Delete old node
1645+ forM_ (Map.lookup (Some k) phs) $ \thisPlaceholder -> do
1646+ thisPlaceholder `deleteUpTo` nextPlaceholder
1647+ -- Insert new node
1648+ forM_ mv $ \(Compose (TraverseChild e _)) -> case e of
1649+ Left _hydration -> pure ()
1650+ Right immediate -> do
1651+ _traverseChildImmediate_fragment immediate `insertBefore` nextPlaceholder
1652+ let weakened :: PatchMap (Some k) (Either (TraverseChildHydration t m) (TraverseChildImmediate (Some k)))
1653+ weakened = weakenPatchDMapWith (_traverseChild_mode . getCompose) $ PatchDMap patch
1654+ filtered :: PatchMap (Some k) DOM.Text
1655+ filtered = PatchMap $ flip Map.mapMaybe (unPatchMap weakened) $ \case
1656+ Nothing -> Just Nothing -- deletion
1657+ Just (Left _) -> Nothing
1658+ Just (Right immediate) -> Just $ Just $ _traverseChildImmediate_placeholder immediate
1659+ liftIO $ writeIORef placeholders $! fromMaybe phs $ apply filtered phs
7531660
7541661{-# 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'))
1662+traverseIntMapWithKeyWithAdjust'
1663+ :: 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)
1664+ => (IntMap.Key -> v -> HydrationDomBuilderT s t m v')
1665+ -> IntMap v
1666+ -> Event t (PatchIntMap v)
1667+ -> HydrationDomBuilderT s t m (IntMap v', Event t (PatchIntMap v'))
7561668traverseIntMapWithKeyWithAdjust' = 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))
1669+ let updateChildUnreadiness (p@(PatchIntMap pInner) :: PatchIntMap (TraverseChild t m Int v')) old = do
1670+ let new :: IntMap.Key -> Maybe (TraverseChild t m Int v') -> IO (Maybe (IORef (ChildReadyState Int)))
7591671 new k m = case m of
7601672 Nothing -> return Nothing
761− Just (_, _, sRef, _) -> do
1673+ Just (TraverseChild (Left _hydration) _) -> pure Nothing
1674+ Just (TraverseChild (Right immediate) _) -> do
1675+ let sRef = _traverseChildImmediate_childReadyState immediate
7621676 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
1677+ ChildReadyState_Ready -> return Nothing -- Delete this child, since it's ready
1678+ ChildReadyState_Unready _ -> do
1679+ writeIORef sRef $ ChildReadyState_Unready $ Just k
7661680 return $ Just sRef
7671681 delete _ sRef = do
768− writeIORef sRef $ ChildReadyStateInt_Unready Nothing
1682+ writeIORef sRef $ ChildReadyState_Unready Nothing
7691683 return ()
7701684 p' <- PatchIntMap <$> IntMap.traverseWithKey new pInner
7711685 _ <- IntMap.traverseWithKey delete $ FastMutableIntMap.getDeletions p old
7721686 return $ applyAlways p' old
773− hoistTraverseIntMapWithKeyWithAdjust traverseIntMapWithKeyWithAdjust updateChildUnreadiness $ \placeholders lastPlaceholderRef (PatchIntMap p) -> do
1687+ hoistTraverseIntMapWithKeyWithAdjust traverseIntMapWithKeyWithAdjust updateChildUnreadiness $ \placeholders lastPlaceholder (PatchIntMap p) -> do
7741688 phs <- liftIO $ readIORef placeholders
7751689 forM_ (IntMap.toList p) $ \(k, mv) -> do
776− lastPlaceholder <- liftIO $ readIORef lastPlaceholderRef
7771690 let nextPlaceholder = maybe lastPlaceholder snd $ IntMap.lookupGT k phs
1691+ -- Delete old node
7781692 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
1693+ -- Insert new node
1694+ forM_ mv $ \(TraverseChild e _) -> case e of
1695+ Left _hydration -> pure ()
1696+ Right immediate -> do
1697+ _traverseChildImmediate_fragment immediate `insertBefore` nextPlaceholder
1698+ let filtered :: PatchIntMap DOM.Text
1699+ filtered = PatchIntMap $ flip IntMap.mapMaybe p $ \case
1700+ Nothing -> Just Nothing -- deletion
1701+ Just tc
1702+ | Right immediate <- _traverseChild_mode tc -> Just $ Just $ _traverseChildImmediate_placeholder immediate
1703+ | otherwise -> Nothing
1704+ liftIO $ writeIORef placeholders $! fromMaybe phs $ apply filtered phs
1705+
1706+{-# SPECIALIZE traverseIntMapWithKeyWithAdjust'
1707+ :: (IntMap.Key -> v -> HydrationDomBuilderT GhcjsDomSpace DomTimeline HydrationM v')
1708+ -> IntMap v
1709+ -> Event DomTimeline (PatchIntMap v)
1710+ -> HydrationDomBuilderT GhcjsDomSpace DomTimeline HydrationM (IntMap v', Event DomTimeline (PatchIntMap v'))
1711+ #-}
7811712
782−#if MIN_VERSION_base(4,9,0)
783−data ChildReadyState k
784−#else
785−data ChildReadyState (k :: * -> *)
786−#endif
1713+{-# SPECIALIZE traverseIntMapWithKeyWithAdjust'
1714+ :: (IntMap.Key -> v -> HydrationDomBuilderT HydrationDomSpace DomTimeline HydrationM v')
1715+ -> IntMap v
1716+ -> Event DomTimeline (PatchIntMap v)
1717+ -> HydrationDomBuilderT HydrationDomSpace DomTimeline HydrationM (IntMap v', Event DomTimeline (PatchIntMap v'))
1718+ #-}
1719+
1720+data ChildReadyState a
7871721 = ChildReadyState_Ready
788− | ChildReadyState_Unready !(Maybe (Some k))
1722+ | ChildReadyState_Unready !(Maybe a)
7891723 deriving (Show, Read, Eq, Ord)
7901724
791−data ChildReadyStateInt
792− = ChildReadyStateInt_Ready
793− | ChildReadyStateInt_Unready !(Maybe Int)
794− deriving (Show, Read, Eq, Ord)
1725+insertAfterPreviousNode :: (Monad m, MonadJSM m) => DOM.IsNode node => node -> HydrationRunnerT t m ()
1726+insertAfterPreviousNode node = do
1727+ parent <- askParent
1728+ nextNode <- maybe (Node.getFirstChild parent) Node.getNextSibling =<< getPreviousNode
1729+ Node.insertBefore_ parent node nextNode
1730+ setPreviousNode $ Just $ toNode node
7951731
796−{-# INLINE hoistTraverseIntMapWithKeyWithAdjust #-}
797−hoistTraverseIntMapWithKeyWithAdjust :: forall v v' t m p.
1732+{-# INLINABLE hoistTraverseWithKeyWithAdjust #-}
1733+hoistTraverseWithKeyWithAdjust
1734+ ::
7981735 ( Adjustable t m
1736+ , MonadHold t m
1737+ , DMap.GCompare k
7991738 , MonadIO m
8001739 , MonadJSM m
801− , MonadFix m
8021740 , PrimMonad m
803− , Monoid (p (DOM.DocumentFragment, DOM.Text, IORef ChildReadyStateInt, v'))
804− , Functor p
1741+ , MonadFix m
1742+ , Patch (p k v)
1743+ , Patch (p k (Constant Int))
1744+ , PatchTarget (p k (Constant Int)) ~ DMap k (Constant Int)
1745+ , Patch (p k (Compose (TraverseChild t m (Some k)) v'))
1746+ , PatchTarget (p k (Compose (TraverseChild t m (Some k)) v')) ~ DMap k (Compose (TraverseChild t m (Some k)) v')
1747+ , Monoid (p k (Compose (TraverseChild t m (Some k)) v'))
1748+ , RawDocument (DomBuilderSpace (HydrationDomBuilderT s t m)) ~ Document
8051749 )
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
1750+ => (forall vv vv'.
1751+ (forall a. k a -> vv a -> DomRenderHookT t m (vv' a))
1752+ -> DMap k vv
1753+ -> Event t (p k vv)
1754+ -> DomRenderHookT t m (DMap k vv', Event t (p k vv')))
1755+ -- ^ The base monad's traversal
1756+ -> (forall vv vv'. (forall a. vv a -> vv' a) -> p k vv -> p k vv')
1757+ -- ^ A way of mapping over the patch type
1758+ -> (p k (Compose (TraverseChild t m (Some k)) v') -> DMap k (Constant (IORef (ChildReadyState (Some k)))) -> IO (DMap k (Constant (IORef (ChildReadyState (Some k))))))
1759+ -- ^ Given a patch for the children DOM elements, produce a patch for the childrens' unreadiness state
1760+ -> (IORef (Map.Map (Some k) DOM.Text) -> DOM.Text -> p k (Compose (TraverseChild t m (Some k)) v') -> JSM ())
1761+ -- ^ Apply a patch to the DOM
1762+ -> (forall a. k a -> v a -> HydrationDomBuilderT s t m (v' a))
1763+ -> DMap k v
1764+ -> Event t (p k v)
1765+ -> HydrationDomBuilderT s t m (DMap k v', Event t (p k v'))
1766+hoistTraverseWithKeyWithAdjust base mapPatch updateChildUnreadiness applyDomUpdate_ f dm0 dm' = do
1767+ doc <- askDocument
1768+ initialEnv <- HydrationDomBuilderT ask
1769+ let parentUnreadyChildren = _hydrationDomBuilderEnv_unreadyChildren initialEnv
1770+ pendingChange :: IORef (DMap k (Constant (IORef (ChildReadyState (Some k)))), p k (Compose (TraverseChild t m (Some k)) v')) <- liftIO $ newIORef mempty
8211771 haveEverBeenReady <- liftIO $ newIORef False
822− placeholders <- liftIO $ newIORef $ error "placeholders not yet initialized"
823− lastPlaceholderRef <- liftIO $ newIORef $ error "lastPlaceholderRef not yet initialized"
1772+ placeholders <- liftIO $ newIORef Map.empty
1773+ lastPlaceholder <- createTextNode doc ("" :: Text)
8241774 let applyDomUpdate p = do
825− applyDomUpdate_ placeholders lastPlaceholderRef p
1775+ applyDomUpdate_ placeholders lastPlaceholder p
8261776 markSelfReady
8271777 liftIO $ writeIORef pendingChange $! mempty
8281778 markSelfReady = do
@@ -833,86 +1783,105 @@ hoistTraverseIntMapWithKeyWithAdjust base updateChildUnreadiness applyDomUpdate_
8331783 old <- liftIO $ readIORef parentUnreadyChildren
8341784 let new = pred old
8351785 liftIO $ writeIORef parentUnreadyChildren $! new
836− when (new == 0) $ _immediateDomBuilderEnv_commitAction initialEnv
837− markChildReady :: IORef ChildReadyStateInt -> JSM ()
1786+ when (new == 0) $ _hydrationDomBuilderEnv_commitAction initialEnv
1787+ markChildReady :: IORef (ChildReadyState (Some k)) -> JSM ()
8381788 markChildReady childReadyState = do
8391789 liftIO (readIORef childReadyState) >>= \case
840− ChildReadyStateInt_Ready -> return ()
841− ChildReadyStateInt_Unready countedAt -> do
842− liftIO $ writeIORef childReadyState ChildReadyStateInt_Ready
1790+ ChildReadyState_Ready -> return ()
1791+ ChildReadyState_Unready countedAt -> do
1792+ liftIO $ writeIORef childReadyState ChildReadyState_Ready
8431793 case countedAt of
8441794 Nothing -> return ()
845− Just k -> do -- This child has been counted as unready, so we need to remove it from the unready set
1795+ Just (Some k) -> do -- This child has been counted as unready, so we need to remove it from the unready set
8461796 (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
1797+ when (not $ DMap.null oldUnready) $ do -- This shouldn't actually ever be null
1798+ let newUnready = DMap.delete k oldUnready
8491799 liftIO $ writeIORef pendingChange (newUnready, p)
850− when (IntMap.null newUnready) $ do
1800+ when (DMap.null newUnready) $ do
8511801 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
1802+ (children0 :: DMap k (Compose (TraverseChild t m (Some k)) v'), children' :: Event t (p k (Compose (TraverseChild t m (Some k)) v')))
1803+ <- HydrationDomBuilderT $ lift $ base (\k v -> drawChildUpdate initialEnv markChildReady $ f k v) dm0 dm'
1804+ let processChild k (Compose (TraverseChild e _)) = case e of
1805+ Left _ -> pure $ ComposeMaybe Nothing
1806+ Right immediate -> ComposeMaybe <$> do
1807+ readIORef (_traverseChildImmediate_childReadyState immediate) >>= \case
1808+ ChildReadyState_Ready -> return Nothing
1809+ ChildReadyState_Unready _ -> do
1810+ writeIORef (_traverseChildImmediate_childReadyState immediate) $ ChildReadyState_Unready $ Just $ Some k
1811+ return $ Just $ Constant (_traverseChildImmediate_childReadyState immediate)
1812+ initialUnready <- liftIO $ DMap.mapMaybeWithKey (\_ -> getComposeMaybe) <$> DMap.traverseWithKey processChild children0
1813+ liftIO $ if DMap.null initialUnready
8611814 then writeIORef haveEverBeenReady True
8621815 else do
8631816 modifyIORef' parentUnreadyChildren succ
8641817 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)
1818+ getHydrationMode >>= \case
1819+ HydrationMode_Hydrating -> addHydrationStepWithSetup (holdIncremental children0 children') $ \children -> do
1820+ dm :: DMap k (Compose (TraverseChild t m (Some k)) v') <- sample $ currentIncremental children
1821+ phs <- traverse id $ weakenDMapWith (either _traverseChildHydration_delayed (pure . _traverseChildImmediate_placeholder) . _traverseChild_mode . getCompose) dm
1822+ liftIO $ writeIORef placeholders $! phs
1823+ insertAfterPreviousNode lastPlaceholder
1824+ HydrationMode_Immediate -> do
1825+ let activate i = do
1826+ append $ toNode $ _traverseChildImmediate_fragment i
1827+ pure $ _traverseChildImmediate_placeholder i
1828+ phs <- traverse id $ weakenDMapWith (either (error "impossible") activate . _traverseChild_mode . getCompose) children0
1829+ liftIO $ writeIORef placeholders $! phs
1830+ append $ toNode lastPlaceholder
8711831 requestDomAction_ $ ffor children' $ \p -> do
8721832 (oldUnready, oldP) <- liftIO $ readIORef pendingChange
8731833 newUnready <- liftIO $ updateChildUnreadiness p oldUnready
8741834 let !newP = p <> oldP
8751835 liftIO $ writeIORef pendingChange (newUnready, newP)
876− when (IntMap.null newUnready) $ do
1836+ when (DMap.null newUnready) $ do
8771837 applyDomUpdate newP
1838+ let result0 = DMap.map (_traverseChild_result . getCompose) children0
1839+ result' = ffor children' $ mapPatch $ _traverseChild_result . getCompose
8781840 return (result0, result')
8791841
880−{-# INLINABLE hoistTraverseWithKeyWithAdjust #-}
881−hoistTraverseWithKeyWithAdjust :: forall (k :: * -> *) v v' t m p.
1842+{-# INLINE hoistTraverseIntMapWithKeyWithAdjust #-}
1843+hoistTraverseIntMapWithKeyWithAdjust ::
8821844 ( Adjustable t m
8831845 , MonadHold t m
884− , DMap.GCompare k
885− , MonadIO m
8861846 , MonadJSM m
887− , PrimMonad m
8881847 , 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))
1848+ , PrimMonad m
1849+ , Monoid (p (TraverseChild t m Int v'))
1850+ , Functor p
1851+ , PatchTarget (p (HydrationRunnerT t m ())) ~ IntMap (HydrationRunnerT t m ())
1852+ , PatchTarget (p (TraverseChild t m Int v')) ~ IntMap (TraverseChild t m Int v')
1853+ , Patch (p (HydrationRunnerT t m ()))
1854+ , Patch (p (TraverseChild t m Int v'))
1855+ , RawDocument (DomBuilderSpace (HydrationDomBuilderT s t m)) ~ Document
8931856 )
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
1857+ => ((IntMap.Key -> v -> DomRenderHookT t m (TraverseChild t m Int v'))
1858+ -> IntMap v
1859+ -> Event t (p v)
1860+ -> DomRenderHookT t m (IntMap (TraverseChild t m Int v'), Event t (p (TraverseChild t m Int v'))))
1861+ -- ^ The base monad's traversal
1862+ -> (p (TraverseChild t m Int v')
1863+ -> IntMap (IORef (ChildReadyState Int))
1864+ -> IO (IntMap (IORef (ChildReadyState Int))))
1865+ -- ^ Given a patch for the children DOM elements, produce a patch for the childrens' unreadiness state
1866+ -> (IORef (IntMap DOM.Text)
1867+ -> DOM.Text
1868+ -> p (TraverseChild t m Int v')
1869+ -> JSM ())
1870+ -- ^ Apply a patch to the DOM
1871+ -> (IntMap.Key -> v -> HydrationDomBuilderT s t m v')
1872+ -> IntMap v
1873+ -> Event t (p v)
1874+ -> HydrationDomBuilderT s t m (IntMap v', Event t (p v'))
1875+hoistTraverseIntMapWithKeyWithAdjust base updateChildUnreadiness applyDomUpdate_ f dm0 dm' = do
1876+ doc <- askDocument
1877+ initialEnv <- HydrationDomBuilderT ask
1878+ let parentUnreadyChildren = _hydrationDomBuilderEnv_unreadyChildren initialEnv
1879+ pendingChange :: IORef (IntMap (IORef (ChildReadyState Int)), p (TraverseChild t m Int v')) <- liftIO $ newIORef mempty
9111880 haveEverBeenReady <- liftIO $ newIORef False
912− placeholders <- liftIO $ newIORef $ error "placeholders not yet initialized"
913− lastPlaceholderRef <- liftIO $ newIORef $ error "lastPlaceholderRef not yet initialized"
1881+ placeholders <- liftIO $ newIORef IntMap.empty
1882+ lastPlaceholder <- createTextNode doc ("" :: Text)
9141883 let applyDomUpdate p = do
915− applyDomUpdate_ placeholders lastPlaceholderRef p
1884+ applyDomUpdate_ placeholders lastPlaceholder p
9161885 markSelfReady
9171886 liftIO $ writeIORef pendingChange $! mempty
9181887 markSelfReady = do
@@ -923,8 +1892,8 @@ hoistTraverseWithKeyWithAdjust base mapPatch updateChildUnreadiness applyDomUpda
9231892 old <- liftIO $ readIORef parentUnreadyChildren
9241893 let new = pred old
9251894 liftIO $ writeIORef parentUnreadyChildren $! new
926− when (new == 0) $ _immediateDomBuilderEnv_commitAction initialEnv
927− markChildReady :: IORef (ChildReadyState k) -> JSM ()
1895+ when (new == 0) $ _hydrationDomBuilderEnv_commitAction initialEnv
1896+ markChildReady :: IORef (ChildReadyState Int) -> JSM ()
9281897 markChildReady childReadyState = do
9291898 liftIO (readIORef childReadyState) >>= \case
9301899 ChildReadyState_Ready -> return ()
@@ -932,82 +1901,191 @@ hoistTraverseWithKeyWithAdjust base mapPatch updateChildUnreadiness applyDomUpda
9321901 liftIO $ writeIORef childReadyState ChildReadyState_Ready
9331902 case countedAt of
9341903 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
1904+ Just k -> do -- This child has been counted as unready, so we need to remove it from the unready set
9361905 (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
1906+ when (not $ IntMap.null oldUnready) $ do -- This shouldn't actually ever be null
1907+ let newUnready = IntMap.delete k oldUnready
9391908 liftIO $ writeIORef pendingChange (newUnready, p)
940− when (DMap.null newUnready) $ do
1909+ when (IntMap.null newUnready) $ do
9411910 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
1911+ (children0 :: IntMap (TraverseChild t m Int v'), children' :: Event t (p (TraverseChild t m Int v')))
1912+ <- HydrationDomBuilderT $ lift $ base (\k v -> drawChildUpdateInt initialEnv markChildReady $ f k v) dm0 dm'
1913+ let processChild k (TraverseChild e _) = case e of
1914+ Left _ -> pure Nothing
1915+ Right immediate -> do
1916+ readIORef (_traverseChildImmediate_childReadyState immediate) >>= \case
1917+ ChildReadyState_Ready -> return Nothing
1918+ ChildReadyState_Unready _ -> do
1919+ writeIORef (_traverseChildImmediate_childReadyState immediate) $ ChildReadyState_Unready $ Just k
1920+ return $ Just (_traverseChildImmediate_childReadyState immediate)
1921+ initialUnready <- liftIO $ IntMap.mapMaybe id <$> IntMap.traverseWithKey processChild children0
1922+ liftIO $ if IntMap.null initialUnready
9511923 then writeIORef haveEverBeenReady True
9521924 else do
9531925 modifyIORef' parentUnreadyChildren succ
9541926 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)
1927+ getHydrationMode >>= \case
1928+ HydrationMode_Hydrating -> addHydrationStepWithSetup (holdIncremental children0 children') $ \children -> do
1929+ dm :: IntMap (TraverseChild t m Int v') <- sample $ currentIncremental children
1930+ phs <- traverse (either _traverseChildHydration_delayed (pure . _traverseChildImmediate_placeholder) . _traverseChild_mode) dm
1931+ liftIO $ writeIORef placeholders $! phs
1932+ insertAfterPreviousNode lastPlaceholder
1933+ HydrationMode_Immediate -> do
1934+ let activate i = do
1935+ append $ toNode $ _traverseChildImmediate_fragment i
1936+ pure $ _traverseChildImmediate_placeholder i
1937+ phs <- traverse (either (error "impossible") activate . _traverseChild_mode) children0
1938+ liftIO $ writeIORef placeholders $! phs
1939+ append $ toNode lastPlaceholder
9611940 requestDomAction_ $ ffor children' $ \p -> do
9621941 (oldUnready, oldP) <- liftIO $ readIORef pendingChange
9631942 newUnready <- liftIO $ updateChildUnreadiness p oldUnready
9641943 let !newP = p <> oldP
9651944 liftIO $ writeIORef pendingChange (newUnready, newP)
966− when (DMap.null newUnready) $ do
1945+ when (IntMap.null newUnready) $ do
9671946 applyDomUpdate newP
1947+ let result0 = IntMap.map _traverseChild_result children0
1948+ result' = ffor children' $ fmap $ _traverseChild_result
9681949 return (result0, result')
9691950
1951+{-# SPECIALIZE hoistTraverseIntMapWithKeyWithAdjust
1952+ :: ((IntMap.Key -> v -> DomRenderHookT DomTimeline HydrationM (TraverseChild DomTimeline HydrationM Int v'))
1953+ -> IntMap v
1954+ -> Event DomTimeline (PatchIntMap v)
1955+ -> DomRenderHookT DomTimeline HydrationM (IntMap (TraverseChild DomTimeline HydrationM Int v'), Event DomTimeline (PatchIntMap (TraverseChild DomTimeline HydrationM Int v'))))
1956+ -> (PatchIntMap (TraverseChild DomTimeline HydrationM Int v')
1957+ -> IntMap (IORef (ChildReadyState Int))
1958+ -> IO (IntMap (IORef (ChildReadyState Int))))
1959+ -> (IORef (IntMap DOM.Text)
1960+ -> DOM.Text
1961+ -> PatchIntMap (TraverseChild DomTimeline HydrationM Int v')
1962+ -> JSM ())
1963+ -> (IntMap.Key -> v -> HydrationDomBuilderT HydrationDomSpace DomTimeline HydrationM v')
1964+ -> IntMap v
1965+ -> Event DomTimeline (PatchIntMap v)
1966+ -> HydrationDomBuilderT HydrationDomSpace DomTimeline HydrationM (IntMap v', Event DomTimeline (PatchIntMap v'))
1967+ #-}
1968+
1969+{-# SPECIALIZE hoistTraverseIntMapWithKeyWithAdjust
1970+ :: ((IntMap.Key -> v -> DomRenderHookT DomTimeline HydrationM (TraverseChild DomTimeline HydrationM Int v'))
1971+ -> IntMap v
1972+ -> Event DomTimeline (PatchIntMap v)
1973+ -> DomRenderHookT DomTimeline HydrationM (IntMap (TraverseChild DomTimeline HydrationM Int v'), Event DomTimeline (PatchIntMap (TraverseChild DomTimeline HydrationM Int v'))))
1974+ -> (PatchIntMap (TraverseChild DomTimeline HydrationM Int v')
1975+ -> IntMap (IORef (ChildReadyState Int))
1976+ -> IO (IntMap (IORef (ChildReadyState Int))))
1977+ -> (IORef (IntMap DOM.Text)
1978+ -> DOM.Text
1979+ -> PatchIntMap (TraverseChild DomTimeline HydrationM Int v')
1980+ -> JSM ())
1981+ -> (IntMap.Key -> v -> HydrationDomBuilderT GhcjsDomSpace DomTimeline HydrationM v')
1982+ -> IntMap v
1983+ -> Event DomTimeline (PatchIntMap v)
1984+ -> HydrationDomBuilderT GhcjsDomSpace DomTimeline HydrationM (IntMap v', Event DomTimeline (PatchIntMap v'))
1985+ #-}
1986+
1987+data TraverseChildImmediate k = TraverseChildImmediate
1988+ { _traverseChildImmediate_fragment :: {-# UNPACK #-} !DOM.DocumentFragment
1989+ -- ^ Child is appended to this fragment
1990+ , _traverseChildImmediate_placeholder :: {-# UNPACK #-} !DOM.Text
1991+ -- ^ Placeholder reference
1992+ , _traverseChildImmediate_childReadyState :: {-# UNPACK #-} !(IORef (ChildReadyState k))
1993+ }
1994+
1995+newtype TraverseChildHydration t m = TraverseChildHydration
1996+ { _traverseChildHydration_delayed :: HydrationRunnerT t m DOM.Text
1997+ -- ^ Action to run at switchover, returns the placeholder
1998+ }
1999+
2000+data TraverseChild t m k a = TraverseChild
2001+ { _traverseChild_mode :: !(Either (TraverseChildHydration t m) (TraverseChildImmediate k))
2002+ , _traverseChild_result :: !a
2003+ } deriving Functor
2004+
9702005{-# INLINABLE drawChildUpdate #-}
971−drawChildUpdate :: (MonadIO m, MonadJSM m)
972− => ImmediateDomBuilderEnv t
2006+drawChildUpdate :: (MonadJSM m, Reflex t)
2007+ => HydrationDomBuilderEnv t m
9732008 -> (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)
2009+ -> HydrationDomBuilderT s t m (f a)
2010+ -> DomRenderHookT t m (Compose (TraverseChild t m k) f a)
9762011drawChildUpdate initialEnv markReady child = do
977− childReadyState <- liftIO $ newIORef $ ChildReadyState_Unready Nothing
2012+ let doc = _hydrationDomBuilderEnv_document initialEnv
9782013 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)
2014+ liftIO (readIORef $ _hydrationDomBuilderEnv_hydrationMode initialEnv) >>= \case
2015+ HydrationMode_Hydrating -> do
2016+ childDelayedRef <- liftIO $ newIORef $ pure ()
2017+ result <- runReaderT (unHydrationDomBuilderT child) initialEnv
2018+ { _hydrationDomBuilderEnv_unreadyChildren = unreadyChildren
2019+ , _hydrationDomBuilderEnv_delayed = childDelayedRef
2020+ }
2021+ childDelayed <- liftIO $ readIORef childDelayedRef
2022+ return $ Compose $ TraverseChild
2023+ { _traverseChild_result = result
2024+ , _traverseChild_mode = Left TraverseChildHydration
2025+ { _traverseChildHydration_delayed = do
2026+ placeholder <- createTextNode doc ("" :: Text)
2027+ insertAfterPreviousNode placeholder
2028+ childDelayed
2029+ pure placeholder
2030+ }
2031+ }
2032+ HydrationMode_Immediate -> do
2033+ childReadyState <- liftIO $ newIORef $ ChildReadyState_Unready Nothing
2034+ df <- createDocumentFragment doc
2035+ placeholder <- createTextNode doc ("" :: Text)
2036+ Node.appendChild_ df placeholder
2037+ result <- runReaderT (unHydrationDomBuilderT child) initialEnv
2038+ { _hydrationDomBuilderEnv_parent = Left $ toNode df
2039+ , _hydrationDomBuilderEnv_unreadyChildren = unreadyChildren
2040+ , _hydrationDomBuilderEnv_commitAction = markReady childReadyState
2041+ }
2042+ u <- liftIO $ readIORef unreadyChildren
2043+ when (u == 0) $ liftIO $ writeIORef childReadyState ChildReadyState_Ready
2044+ return $ Compose $ TraverseChild
2045+ { _traverseChild_result = result
2046+ , _traverseChild_mode = Right TraverseChildImmediate
2047+ { _traverseChildImmediate_fragment = df
2048+ , _traverseChildImmediate_placeholder = placeholder
2049+ , _traverseChildImmediate_childReadyState = childReadyState
2050+ }
2051+ }
9882052
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)
2053+{-# SPECIALIZE drawChildUpdate
2054+ :: HydrationDomBuilderEnv DomTimeline HydrationM
2055+ -> (IORef (ChildReadyState Int) -> JSM ())
2056+ -> HydrationDomBuilderT s DomTimeline HydrationM (Identity a)
2057+ -> DomRenderHookT DomTimeline HydrationM (Compose (TraverseChild DomTimeline HydrationM Int) Identity a)
2058+ #-}
2059+
2060+{-# SPECIALIZE drawChildUpdate
2061+ :: HydrationDomBuilderEnv DomTimeline HydrationM
2062+ -> (IORef (ChildReadyState (Some k)) -> JSM ())
2063+ -> HydrationDomBuilderT s DomTimeline HydrationM (f a)
2064+ -> DomRenderHookT DomTimeline HydrationM (Compose (TraverseChild DomTimeline HydrationM (Some k)) f a)
2065+ #-}
10072066
1008−mkHasFocus :: (MonadHold t m, Reflex t) => Element er d t -> m (Dynamic t Bool)
2067+{-# INLINABLE drawChildUpdateInt #-}
2068+drawChildUpdateInt :: (MonadIO m, MonadJSM m, Reflex t)
2069+ => HydrationDomBuilderEnv t m
2070+ -> (IORef (ChildReadyState k) -> JSM ())
2071+ -> HydrationDomBuilderT s t m v
2072+ -> DomRenderHookT t m (TraverseChild t m k v)
2073+drawChildUpdateInt env mark m = fmap runIdentity . getCompose <$> drawChildUpdate env mark (Identity <$> m)
2074+
2075+{-# SPECIALIZE drawChildUpdateInt
2076+ :: HydrationDomBuilderEnv DomTimeline HydrationM
2077+ -> (IORef (ChildReadyState k) -> JSM ())
2078+ -> HydrationDomBuilderT s DomTimeline HydrationM v
2079+ -> DomRenderHookT DomTimeline HydrationM (TraverseChild DomTimeline HydrationM k v)
2080+ #-}
2081+
2082+{-# INLINE mkHasFocus #-}
2083+mkHasFocus
2084+ :: (HasDocument m, MonadJSM m, IsNode (RawElement d), MonadHold t m, Reflex t, DOM.IsDocumentOrShadowRoot (RawDocument (DomBuilderSpace m)))
2085+ => Element er d t -> m (Dynamic t Bool)
10092086mkHasFocus e = do
1010− let initialFocus = False --TODO: Actually get the initial focus of the element
2087+ doc <- askDocument
2088+ initialFocus <- Node.isSameNode (toNode $ _element_raw e) . fmap toNode =<< Document.getActiveElement doc
10112089 holdDyn initialFocus $ leftmost
10122090 [ False <$ Reflex.select (_element_events e) (WrapArg Blur)
10132091 , True <$ Reflex.select (_element_events e) (WrapArg Focus)
@@ -1016,39 +2094,49 @@ mkHasFocus e = do
10162094insertBefore :: (MonadJSM m, IsNode new, IsNode existing) => new -> existing -> m ()
10172095insertBefore new existing = do
10182096 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
2097+ 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
10202098
1021−instance PerformEvent t m => PerformEvent t (ImmediateDomBuilderT t m) where
1022− type Performable (ImmediateDomBuilderT t m) = Performable m
2099+type ImmediateDomBuilderT = HydrationDomBuilderT GhcjsDomSpace
2100+
2101+instance PerformEvent t m => PerformEvent t (HydrationDomBuilderT s t m) where
2102+ type Performable (HydrationDomBuilderT s t m) = Performable m
10232103 {-# INLINABLE performEvent_ #-}
10242104 performEvent_ e = lift $ performEvent_ e
10252105 {-# INLINABLE performEvent #-}
10262106 performEvent e = lift $ performEvent e
10272107
1028−instance PostBuild t m => PostBuild t (ImmediateDomBuilderT t m) where
2108+instance PostBuild t m => PostBuild t (HydrationDomBuilderT s t m) where
10292109 {-# INLINABLE getPostBuild #-}
10302110 getPostBuild = lift getPostBuild
10312111
1032−instance MonadReflexCreateTrigger t m => MonadReflexCreateTrigger t (ImmediateDomBuilderT t m) where
2112+instance MonadReflexCreateTrigger t m => MonadReflexCreateTrigger t (HydrationDomBuilderT s t m) where
10332113 {-# INLINABLE newEventWithTrigger #-}
10342114 newEventWithTrigger = lift . newEventWithTrigger
10352115 {-# INLINABLE newFanEventWithTrigger #-}
10362116 newFanEventWithTrigger f = lift $ newFanEventWithTrigger f
10372117
1038−instance (Monad m, MonadRef m, Ref m ~ Ref IO, MonadReflexCreateTrigger t m) => TriggerEvent t (ImmediateDomBuilderT t m) where
2118+instance (Monad m, MonadRef m, Ref m ~ Ref IO, MonadReflexCreateTrigger t m) => TriggerEvent t (HydrationDomBuilderT s t m) where
2119+ {-# INLINABLE newTriggerEvent #-}
2120+ newTriggerEvent = HydrationDomBuilderT . lift $ newTriggerEvent
2121+ {-# INLINABLE newTriggerEventWithOnComplete #-}
2122+ newTriggerEventWithOnComplete = HydrationDomBuilderT . lift $ newTriggerEventWithOnComplete
2123+ {-# INLINABLE newEventWithLazyTriggerWithOnComplete #-}
2124+ newEventWithLazyTriggerWithOnComplete f = HydrationDomBuilderT . lift $ newEventWithLazyTriggerWithOnComplete f
2125+
2126+instance (Monad m, MonadRef m, Ref m ~ Ref IO, MonadReflexCreateTrigger t m) => TriggerEvent t (DomRenderHookT t m) where
10392127 {-# INLINABLE newTriggerEvent #-}
1040− newTriggerEvent = ImmediateDomBuilderT . lift . lift $ newTriggerEvent
2128+ newTriggerEvent = DomRenderHookT . lift $ newTriggerEvent
10412129 {-# INLINABLE newTriggerEventWithOnComplete #-}
1042− newTriggerEventWithOnComplete = ImmediateDomBuilderT . lift . lift $ newTriggerEventWithOnComplete
2130+ newTriggerEventWithOnComplete = DomRenderHookT . lift $ newTriggerEventWithOnComplete
10432131 {-# INLINABLE newEventWithLazyTriggerWithOnComplete #-}
1044− newEventWithLazyTriggerWithOnComplete f = ImmediateDomBuilderT . lift . lift $ newEventWithLazyTriggerWithOnComplete f
2132+ newEventWithLazyTriggerWithOnComplete f = DomRenderHookT . lift $ newEventWithLazyTriggerWithOnComplete f
10452133
1046−instance HasJSContext m => HasJSContext (ImmediateDomBuilderT t m) where
1047− type JSContextPhantom (ImmediateDomBuilderT t m) = JSContextPhantom m
2134+instance HasJSContext m => HasJSContext (HydrationDomBuilderT s t m) where
2135+ type JSContextPhantom (HydrationDomBuilderT s t m) = JSContextPhantom m
10482136 askJSContext = lift askJSContext
10492137
1050−instance MonadRef m => MonadRef (ImmediateDomBuilderT t m) where
1051− type Ref (ImmediateDomBuilderT t m) = Ref m
2138+instance MonadRef m => MonadRef (HydrationDomBuilderT s t m) where
2139+ type Ref (HydrationDomBuilderT s t m) = Ref m
10522140 {-# INLINABLE newRef #-}
10532141 newRef = lift . newRef
10542142 {-# INLINABLE readRef #-}
@@ -1056,12 +2144,12 @@ instance MonadRef m => MonadRef (ImmediateDomBuilderT t m) where
10562144 {-# INLINABLE writeRef #-}
10572145 writeRef r = lift . writeRef r
10582146
1059−instance MonadAtomicRef m => MonadAtomicRef (ImmediateDomBuilderT t m) where
2147+instance MonadAtomicRef m => MonadAtomicRef (HydrationDomBuilderT s t m) where
10602148 {-# INLINABLE atomicModifyRef #-}
10612149 atomicModifyRef r = lift . atomicModifyRef r
10622150
1063−instance (HasJS x m, ReflexHost t) => HasJS x (ImmediateDomBuilderT t m) where
1064− type JSX (ImmediateDomBuilderT t m) = JSX m
2151+instance (HasJS x m, ReflexHost t) => HasJS x (HydrationDomBuilderT s t m) where
2152+ type JSX (HydrationDomBuilderT s t m) = JSX m
10652153 liftJS = lift . liftJS
10662154
10672155type family EventType en where
@@ -1519,11 +2607,11 @@ getTouchEvent = do
15192607 , _touchEventResult_touches = touches
15202608 }
15212609
1522−instance MonadSample t m => MonadSample t (ImmediateDomBuilderT t m) where
2610+instance MonadSample t m => MonadSample t (HydrationDomBuilderT s t m) where
15232611 {-# INLINABLE sample #-}
15242612 sample = lift . sample
15252613
1526−instance MonadHold t m => MonadHold t (ImmediateDomBuilderT t m) where
2614+instance MonadHold t m => MonadHold t (HydrationDomBuilderT s t m) where
15272615 {-# INLINABLE hold #-}
15282616 hold v0 v' = lift $ hold v0 v'
15292617 {-# INLINABLE holdDyn #-}
@@ -1545,7 +2633,7 @@ data Window t = Window
15452633 , _window_raw :: DOM.Window
15462634 }
15472635
1548−wrapWindow :: (MonadJSM m, MonadReflexCreateTrigger t m) => DOM.Window -> WindowConfig t -> ImmediateDomBuilderT t m (Window t)
2636+wrapWindow :: (MonadJSM m, MonadReflexCreateTrigger t m) => DOM.Window -> WindowConfig t -> HydrationDomBuilderT GhcjsDomSpace t m (Window t)
15492637wrapWindow wv _ = do
15502638 events <- wrapDomEventsMaybe wv (defaultDomWindowEventHandler wv) windowOnEventName
15512639 return $ Window
reflex-dom-core/src/Reflex/Dom/Builder/InputDisabled.hs+6
@@ -80,6 +80,7 @@ instance Adjustable t m => Adjustable t (InputDisabledT m) where
8080 runWithReplace a0 a' = InputDisabledT $ runWithReplace (coerce a0) (coerceEvent a')
8181 traverseDMapWithKeyWithAdjust f dm0 dm' = InputDisabledT $ traverseDMapWithKeyWithAdjust (\k v -> runInputDisabledT $ f k v) (coerce dm0) (coerceEvent dm')
8282 traverseDMapWithKeyWithAdjustWithMove f dm0 dm' = InputDisabledT $ traverseDMapWithKeyWithAdjustWithMove (\k v -> runInputDisabledT $ f k v) (coerce dm0) (coerceEvent dm')
83+ traverseIntMapWithKeyWithAdjust f m0 m' = InputDisabledT $ traverseIntMapWithKeyWithAdjust (\k v -> runInputDisabledT $ f k v) (coerce m0) (coerceEvent m')
8384
8485instance NotReady t m => NotReady t (InputDisabledT m) where
8586 notReadyUntil = lift . notReadyUntil
@@ -108,3 +109,8 @@ instance HasJSContext m => HasJSContext (InputDisabledT m) where
108109instance HasJS js m => HasJS js (InputDisabledT m) where
109110 type JSX (InputDisabledT m) = JSX m
110111 liftJS = lift . liftJS
112+
113+instance DomRenderHook t m => DomRenderHook t (InputDisabledT m) where
114+ withRenderHook f = InputDisabledT . withRenderHook f . runInputDisabledT
115+ requestDomAction = InputDisabledT . requestDomAction
116+ requestDomAction_ = InputDisabledT . requestDomAction_
reflex-dom-core/src/Reflex/Dom/Builder/Static.hs+71−3
@@ -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
reflex-dom-core/src/Reflex/Dom/Core.hs+2
@@ -6,6 +6,7 @@ module Reflex.Dom.Core (module X) where
66
77import Reflex as X hiding (askEvents)
88import Reflex.Dom.Builder.Class as X
9+import Reflex.Dom.Builder.Hydratable as X
910import Reflex.Dom.Builder.Immediate as X
1011import Reflex.Dom.Builder.InputDisabled as X
1112import Reflex.Dom.Builder.Static as X
@@ -18,3 +19,4 @@ import Reflex.Dom.Prerender as X
1819import Reflex.Dom.WebSocket as X
1920import Reflex.Dom.Widget as X
2021import Reflex.Dom.Xhr as X
22+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+167−36
@@ -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" --TODO: test invoking this more than once
125+ -> (forall c. HydrationWidget () c -> FloatingWidget () c) -- "Append to body" --TODO: test invoking this more than once
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,40 +206,54 @@ 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
110213attachWidget rootElement wv w = fst <$> attachWidget' rootElement wv w
111214
112−-- | Warning: `mainWidgetWithHead'` is provided only as performance tweak. It is expected to disappear in future releases.
113−mainWidgetWithHead' :: (a -> Widget () b, b -> Widget () a) -> JSM ()
114−mainWidgetWithHead' widgets = withJSContextSingletonMono $ \jsSing -> do
115− doc <- currentDocumentUnchecked
116− headElement <- getHeadUnchecked doc
117− headFragment <- createDocumentFragment doc
118− bodyElement <- getBodyUnchecked doc
119− bodyFragment <- createDocumentFragment doc
120− (events, fc) <- liftIO . attachWidget'' $ \events -> do
121− let (headWidget, bodyWidget) = widgets
215+{-# INLINABLE runImmediateWidgetWithHeadAndBody #-}
216+runImmediateWidgetWithHeadAndBody
217+ :: ( (forall c. Widget () c -> FloatingWidget () c) -- "Append to head"
218+ -> (forall c. Widget () c -> FloatingWidget () c) -- "Append to body"
219+ -> FloatingWidget () ()
220+ )
221+ -> JSM ()
222+runImmediateWidgetWithHeadAndBody app = withJSContextSingletonMono $ \jsSing -> do
223+ globalDoc <- currentDocumentUnchecked
224+ headElement <- getHeadUnchecked globalDoc
225+ bodyElement <- getBodyUnchecked globalDoc
226+ headFragment <- createDocumentFragment globalDoc
227+ bodyFragment <- createDocumentFragment globalDoc
228+ (events, fc) <- liftIO . attachImmediateWidget $ \hydrationMode events -> do
122229 (postBuild, postBuildTriggerRef) <- newEventWithTriggerRef
123− let go :: forall c. Widget () c -> DOM.DocumentFragment -> PerformEventT DomTimeline DomHost c
124− go w df = do
230+ let go :: forall c. DOM.DocumentFragment -> Widget () c -> FloatingWidget () c
231+ go df w = do
125232 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
233+ delayed <- liftIO $ newIORef $ pure ()
234+ let builderEnv = HydrationDomBuilderEnv
235+ { _hydrationDomBuilderEnv_document = globalDoc
236+ , _hydrationDomBuilderEnv_parent = Left $ toNode df
237+ , _hydrationDomBuilderEnv_unreadyChildren = unreadyChildren
238+ , _hydrationDomBuilderEnv_commitAction = pure () --TODO: possibly `replaceElementContents n f`
239+ , _hydrationDomBuilderEnv_hydrationMode = hydrationMode
240+ , _hydrationDomBuilderEnv_switchover = never
241+ , _hydrationDomBuilderEnv_delayed = delayed
131242 }
132− runWithJSContextSingleton (runImmediateDomBuilderT (runPostBuildT w postBuild) builderEnv events) jsSing
133− rec b <- go (headWidget a) headFragment
134− a <- go (bodyWidget b) bodyFragment
243+ lift $ runHydrationDomBuilderT w builderEnv events
244+ runWithJSContextSingleton (runPostBuildT (runTriggerEventT (app (go headFragment) (go bodyFragment)) events) postBuild) jsSing
135245 return (events, postBuildTriggerRef)
136246 replaceElementContents headElement headFragment
137247 replaceElementContents bodyElement bodyFragment
138248 liftIO $ processAsyncEvents events fc
139249
250+-- | Warning: `mainWidgetWithHead'` is provided only as performance tweak. It is expected to disappear in future releases.
251+mainWidgetWithHead' :: (a -> Widget () b, b -> Widget () a) -> JSM ()
252+mainWidgetWithHead' (h, b) = runImmediateWidgetWithHeadAndBody $ \appendHead appendBody -> do
253+ rec hOut <- appendHead $ h bOut
254+ bOut <- appendBody $ b hOut
255+ pure ()
256+
140257replaceElementContents :: DOM.IsElement e => e -> DOM.DocumentFragment -> JSM ()
141258replaceElementContents e df = do
142259 setInnerHTML e ("" :: String)
@@ -148,16 +265,20 @@ attachWidget' :: DOM.IsElement e => e -> JSContextSingleton x -> Widget x a -> J
148265attachWidget' rootElement jsSing w = do
149266 doc <- getOwnerDocumentUnchecked rootElement
150267 df <- createDocumentFragment doc
151− ((a, events), fc) <- liftIO . attachWidget'' $ \events -> do
268+ ((a, events), fc) <- liftIO . attachImmediateWidget $ \hydrationMode events -> do
152269 (postBuild, postBuildTriggerRef) <- newEventWithTriggerRef
153270 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
271+ delayed <- liftIO $ newIORef $ pure ()
272+ let builderEnv = HydrationDomBuilderEnv
273+ { _hydrationDomBuilderEnv_document = toDocument doc
274+ , _hydrationDomBuilderEnv_parent = Left $ toNode df
275+ , _hydrationDomBuilderEnv_unreadyChildren = unreadyChildren
276+ , _hydrationDomBuilderEnv_commitAction = return () --TODO
277+ , _hydrationDomBuilderEnv_switchover = never
278+ , _hydrationDomBuilderEnv_delayed = delayed
279+ , _hydrationDomBuilderEnv_hydrationMode = hydrationMode
159280 }
160− a <- runWithJSContextSingleton (runImmediateDomBuilderT (runPostBuildT w postBuild) builderEnv events) jsSing
281+ a <- runWithJSContextSingleton (runPostBuildT (runHydrationDomBuilderT w builderEnv events) postBuild) jsSing
161282 return ((a, events), postBuildTriggerRef)
162283 replaceElementContents rootElement df
163284 liftIO $ processAsyncEvents events fc
@@ -165,12 +286,18 @@ attachWidget' rootElement jsSing w = do
165286
166287type EventChannel = Chan [DSum (EventTriggerRef DomTimeline) TriggerInvocation]
167288
168−{-# INLINABLE attachWidget'' #-}
169−attachWidget'' :: (EventChannel -> PerformEventT DomTimeline DomHost (a, IORef (Maybe (EventTrigger DomTimeline ())))) -> IO (a, FireCommand DomTimeline DomHost)
170−attachWidget'' w = do
289+{-# INLINABLE attachImmediateWidget #-}
290+attachImmediateWidget
291+ :: ( IORef HydrationMode
292+ -> EventChannel
293+ -> PerformEventT DomTimeline DomHost (a, IORef (Maybe (EventTrigger DomTimeline ())))
294+ )
295+ -> IO (a, FireCommand DomTimeline DomHost)
296+attachImmediateWidget w = do
297+ hydrationMode <- liftIO $ newIORef HydrationMode_Immediate
171298 events <- newChan
172299 runDomHost $ do
173− ((result, postBuildTriggerRef), fc@(FireCommand fire)) <- hostPerformEventT $ w events
300+ ((result, postBuildTriggerRef), fc@(FireCommand fire)) <- hostPerformEventT $ w hydrationMode events
174301 mPostBuildTrigger <- readRef postBuildTriggerRef
175302 forM_ mPostBuildTrigger $ \postBuildTrigger -> fire [postBuildTrigger :=> Identity ()] $ return ()
176303 return (result, fc)
@@ -186,7 +313,6 @@ processAsyncEvents events (FireCommand fire) = void $ forkIO $ forever $ do
186313 liftIO $ forM_ ers $ \(_ :=> TriggerInvocation _ cb) -> cb
187314 return ()
188315
189−
190316-- | Run a reflex-dom application inside of an existing DOM element with the given ID
191317mainWidgetInElementById :: Text -> (forall x. Widget x ()) -> JSM ()
192318mainWidgetInElementById eid w = withJSContextSingleton $ \jsSing -> do
@@ -208,8 +334,13 @@ runApp' app = withJSContextSingleton $ \jsSing -> do
208334 body <- getBodyUnchecked doc
209335 win <- getDefaultViewUnchecked doc
210336 rec o <- attachWidget body jsSing $ do
211− w <- lift $ wrapWindow win $ _appOutput_windowConfig o
337+ w <- wrapWindow win $ _appOutput_windowConfig o
212338 app $ AppInput
213339 { _appInput_window = w
214340 }
215341 return ()
342+
343+{-# DEPRECATED attachWidget'' "Use 'attachImmediateWidget . const' instead" #-}
344+{-# INLINABLE attachWidget'' #-}
345+attachWidget'' :: (EventChannel -> PerformEventT DomTimeline DomHost (a, IORef (Maybe (EventTrigger DomTimeline ())))) -> IO (a, FireCommand DomTimeline DomHost)
346+attachWidget'' = attachImmediateWidget . const
reflex-dom-core/src/Reflex/Dom/Old.hs−1
@@ -1,6 +1,5 @@
11{-# LANGUAGE CPP #-}
22{-# LANGUAGE ConstraintKinds #-}
3−{-# LANGUAGE ExplicitForAll #-}
43{-# LANGUAGE FlexibleContexts #-}
54{-# LANGUAGE FlexibleInstances #-}
65{-# LANGUAGE MultiParamTypeClasses #-}
reflex-dom-core/src/Reflex/Dom/Prerender.hs+287−52
@@ -1,88 +1,323 @@
11{-# LANGUAGE ConstraintKinds #-}
2−{-# LANGUAGE EmptyDataDecls #-}
2+{-# LANGUAGE CPP #-}
3+{-# LANGUAGE EmptyCase #-}
34{-# LANGUAGE FlexibleContexts #-}
45{-# LANGUAGE FlexibleInstances #-}
56{-# LANGUAGE FunctionalDependencies #-}
6−{-# LANGUAGE MultiParamTypeClasses #-}
7+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
8+{-# LANGUAGE LambdaCase #-}
9+{-# LANGUAGE OverloadedStrings #-}
710{-# LANGUAGE Rank2Types #-}
11+{-# LANGUAGE RecursiveDo #-}
812{-# LANGUAGE ScopedTypeVariables #-}
913{-# LANGUAGE TypeFamilies #-}
10−{-# LANGUAGE TypeSynonymInstances #-}
1114{-# LANGUAGE UndecidableInstances #-}
15+{-# LANGUAGE UndecidableSuperClasses #-}
16+
17+-- | Render the first widget on the server, and the second on the client.
1218module Reflex.Dom.Prerender
1319 ( Prerender (..)
14− , prerender
20+ , prerender_
1521 , PrerenderClientConstraint
22+ , PrerenderBaseConstraints
1623 ) where
1724
25+import Control.Monad.Primitive (PrimMonad(..))
1826import Control.Monad.Reader
19−import Data.Constraint
27+import Control.Monad.Ref (MonadRef(..))
28+import Data.IORef (IORef, newIORef)
29+import Data.Semigroup (Semigroup)
30+import Data.Text (Text)
31+import Data.Void
2032import Foreign.JavaScript.TH
2133import GHCJS.DOM.Types (MonadJSM)
22−import Reflex
34+import Reflex hiding (askEvents)
2335import Reflex.Dom.Builder.Class
24−import Reflex.Dom.Builder.InputDisabled
36+import Reflex.Dom.Builder.Hydratable
2537import Reflex.Dom.Builder.Immediate
38+import Reflex.Dom.Builder.InputDisabled
2639import Reflex.Dom.Builder.Static
2740import Reflex.Host.Class
41+import Data.IntMap.Strict (IntMap)
42+import qualified Data.IntMap.Strict as IntMap
2843
29−type PrerenderClientConstraint js m =
30− ( HasJS js m
31− , HasJS js (Performable m)
32− , MonadJSM m
33− , MonadJSM (Performable m)
44+import qualified GHCJS.DOM.Document as Document
45+import qualified GHCJS.DOM.Node as Node
46+import qualified GHCJS.DOM.Types as DOM
47+
48+type PrerenderClientConstraint js t m =
49+ ( DomBuilder t m
50+ , DomBuilderSpace m ~ GhcjsDomSpace
51+ , DomRenderHook t m
52+ , HasDocument m
53+ , TriggerEvent t m
54+ , PrerenderBaseConstraints js t m
55+ )
56+
57+type PrerenderBaseConstraints js t m =
58+ ( HasJSContext (Performable m)
3459 , HasJSContext m
35− , HasJSContext (Performable m)
3660 , MonadFix m
37− , MonadFix (Performable m)
38− , DomBuilderSpace m ~ GhcjsDomSpace
61+ , MonadHold t m
62+ , MonadJSM (Performable m)
63+ , MonadJSM m
64+ , MonadRef (Performable m)
65+ , MonadRef m
66+ , MonadReflexCreateTrigger t m
67+ , MonadSample t (Performable m)
68+ , PerformEvent t m
69+ , PostBuild t m
70+ , PrimMonad m
71+ , Ref (Performable m) ~ IORef
72+ , Ref m ~ IORef
73+ , HasJS js m
74+ , HasJS js (Performable m)
3975 )
4076
41−class Prerender js m | m -> js where
42− prerenderClientDict :: Maybe (Dict (PrerenderClientConstraint js m))
77+-- | Render the first widget on the server, and the second on the client. The
78+-- hydration builder will run *both* widgets.
79+prerender_
80+ :: (Functor m, Reflex t, Prerender js t m)
81+ => m () -> Client m () -> m ()
82+prerender_ server client = void $ prerender server client
83+
84+class (PrerenderClientConstraint js t (Client m), Client (Client m) ~ Client m, Prerender js t (Client m)) => Prerender js t m | m -> t js where
85+ -- | Monad in which the client widget is built
86+ type Client m :: * -> *
87+ -- | Render the first widget on the server, and the second on the client. The
88+ -- hydration builder will run *both* widgets, updating the result dynamic at
89+ -- switchover time.
90+ prerender :: m a -> Client m a -> m (Dynamic t a)
91+
92+instance (ReflexHost t, Adjustable t m, PrerenderBaseConstraints js t m) => Prerender js t (HydrationDomBuilderT GhcjsDomSpace t m) where
93+ type Client (HydrationDomBuilderT GhcjsDomSpace t m) = HydrationDomBuilderT GhcjsDomSpace t m
94+ prerender _ client = pure <$> client
95+
96+instance (Adjustable t m, PrerenderBaseConstraints js t m, ReflexHost t) => Prerender js t (HydrationDomBuilderT HydrationDomSpace t m) where
97+ -- | PostBuildT is needed here because we delay running the client builder
98+ -- until after switchover, at which point the postBuild of @m@ has already fired
99+ type Client (HydrationDomBuilderT HydrationDomSpace t m) = PostBuildT t (HydrationDomBuilderT GhcjsDomSpace t m)
100+ -- | Runs the server widget up until switchover, then replaces it with the
101+ -- client widget.
102+ prerender server client = do
103+ env <- HydrationDomBuilderT ask
104+ events <- askEvents
105+ doc <- askDocument
106+ serverDf <- Document.createDocumentFragment doc -- server dom should not be mounted in the window's doc in hydration
107+ df <- Document.createDocumentFragment doc
108+ unreadyChildren <- HydrationDomBuilderT $ asks _hydrationDomBuilderEnv_unreadyChildren
109+ immediateMode <- liftIO $ newIORef HydrationMode_Immediate
110+ delayed <- liftIO $ newIORef $ pure ()
111+ let clientEnv = env
112+ { _hydrationDomBuilderEnv_parent = Left $ DOM.toNode df
113+ , _hydrationDomBuilderEnv_hydrationMode = immediateMode
114+ }
115+ serverEnv = HydrationDomBuilderEnv
116+ { _hydrationDomBuilderEnv_document = doc
117+ , _hydrationDomBuilderEnv_parent = Left $ DOM.toNode serverDf
118+ , _hydrationDomBuilderEnv_unreadyChildren = unreadyChildren
119+ , _hydrationDomBuilderEnv_commitAction = pure ()
120+ , _hydrationDomBuilderEnv_delayed = delayed
121+ , _hydrationDomBuilderEnv_hydrationMode = immediateMode
122+ , _hydrationDomBuilderEnv_switchover = never
123+ }
124+ a0 <- lift $ runHydrationDomBuilderT server serverEnv events
125+ (a', trigger) <- newTriggerEvent
126+ getHydrationMode >>= \case
127+ HydrationMode_Immediate -> do
128+ liftIO . trigger <=< lift $ runHydrationDomBuilderT (runPostBuildT client $ void a') clientEnv events
129+ append $ DOM.toNode df
130+ HydrationMode_Hydrating -> addHydrationStep $ do
131+ liftIO . trigger <=< lift $ runHydrationDomBuilderT (runPostBuildT client $ void a') clientEnv events
132+ insertBefore df =<< deleteToPrerenderEnd doc
133+ holdDyn a0 a'
134+
135+newtype UnrunnableT js t m a = UnrunnableT (ReaderT Void m a)
136+ deriving (Functor, Applicative, Monad, MonadTrans)
137+
138+unrunnable :: UnrunnableT js t m a
139+unrunnable = UnrunnableT $ ReaderT $ \case {}
140+
141+instance (Reflex t, Monad m) => DomBuilder t (UnrunnableT js t m) where
142+ type DomBuilderSpace (UnrunnableT js t m) = GhcjsDomSpace
143+ textNode _ = unrunnable
144+ commentNode _ = unrunnable
145+ element _ _ _ = unrunnable
146+ inputElement _ = unrunnable
147+ textAreaElement _ = unrunnable
148+ selectElement _ _ = unrunnable
149+ placeRawElement _ = unrunnable
150+ wrapRawElement _ _ = unrunnable
151+instance (Reflex t, Monad m) => NotReady t (UnrunnableT js t m) where
152+ notReadyUntil _ = unrunnable
153+ notReady = unrunnable
154+instance (Reflex t, Monad m) => Adjustable t (UnrunnableT js t m) where
155+ runWithReplace _ _ = unrunnable
156+ traverseIntMapWithKeyWithAdjust _ _ _ = unrunnable
157+ traverseDMapWithKeyWithAdjust _ _ _ = unrunnable
158+ traverseDMapWithKeyWithAdjustWithMove _ _ _ = unrunnable
159+instance (Reflex t, Monad m) => PerformEvent t (UnrunnableT js t m) where
160+ type Performable (UnrunnableT js t m) = UnrunnableT js t m
161+ performEvent _ = unrunnable
162+ performEvent_ _ = unrunnable
163+instance Monad m => MonadRef (UnrunnableT js t m) where
164+ type Ref (UnrunnableT js t m) = Ref IO
165+ newRef _ = unrunnable
166+ readRef _ = unrunnable
167+ writeRef _ _ = unrunnable
168+instance Monad m => HasDocument (UnrunnableT js t m) where
169+ askDocument = unrunnable
170+instance Monad m => HasJSContext (UnrunnableT js t m) where
171+ type JSContextPhantom (UnrunnableT js t m) = ()
172+ askJSContext = unrunnable
173+instance Monad m => HasJS JS' (UnrunnableT js t m) where
174+ type JSX (UnrunnableT js t m) = UnrunnableT js t m
175+ liftJS _ = unrunnable
176+instance Monad m => MonadJS JS' (UnrunnableT js t m) where
177+ runJS _ _ = unrunnable
178+ forkJS _ = unrunnable
179+ mkJSUndefined = unrunnable
180+ isJSNull _ = unrunnable
181+ isJSUndefined _ = unrunnable
182+ fromJSBool _ = unrunnable
183+ fromJSString _ = unrunnable
184+ fromJSArray _ = unrunnable
185+ fromJSUint8Array _ = unrunnable
186+ fromJSNumber _ = unrunnable
187+ withJSBool _ _ = unrunnable
188+ withJSString _ _ = unrunnable
189+ withJSNumber _ _ = unrunnable
190+ withJSArray _ _ = unrunnable
191+ withJSUint8Array _ _ = unrunnable
192+ mkJSFun _ = unrunnable
193+ freeJSFun _ = unrunnable
194+ setJSProp _ _ _ = unrunnable
195+ getJSProp _ _ = unrunnable
196+ withJSNode _ _ = unrunnable
197+instance Monad m => TriggerEvent t (UnrunnableT js t m) where
198+ newTriggerEvent = unrunnable
199+ newTriggerEventWithOnComplete = unrunnable
200+ newEventWithLazyTriggerWithOnComplete _ = unrunnable
201+instance Monad m => MonadReflexCreateTrigger t (UnrunnableT js t m) where
202+ newEventWithTrigger _ = unrunnable
203+ newFanEventWithTrigger _ = unrunnable
204+instance Monad m => MonadFix (UnrunnableT js t m) where
205+ mfix _ = unrunnable
206+instance Monad m => MonadHold t (UnrunnableT js t m) where
207+ hold _ _ = unrunnable
208+ holdDyn _ _ = unrunnable
209+ holdIncremental _ _ = unrunnable
210+ buildDynamic _ _ = unrunnable
211+ headE _ = unrunnable
212+instance Monad m => MonadSample t (UnrunnableT js t m) where
213+ sample _ = unrunnable
214+instance Monad m => MonadIO (UnrunnableT js t m) where
215+ liftIO _ = unrunnable
216+#ifndef ghcjs_HOST_OS
217+instance Monad m => MonadJSM (UnrunnableT js t m) where
218+ liftJSM' _ = unrunnable
219+#endif
220+instance (Reflex t, Monad m) => PostBuild t (UnrunnableT js t m) where
221+ getPostBuild = unrunnable
222+instance Monad m => PrimMonad (UnrunnableT js t m) where
223+ type PrimState (UnrunnableT js t m) = PrimState IO
224+ primitive _ = unrunnable
225+instance (Reflex t, Monad m) => DomRenderHook t (UnrunnableT js t m) where
226+ withRenderHook _ _ = unrunnable
227+ requestDomAction _ = unrunnable
228+ requestDomAction_ _ = unrunnable
229+instance (Reflex t, Monad m) => Prerender JS' t (UnrunnableT js t m) where
230+ type Client (UnrunnableT js t m) = UnrunnableT js t m
231+ prerender _ _ = unrunnable
43232
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
233+instance (SupportsStaticDomBuilder t m) => Prerender JS' t (StaticDomBuilderT t m) where
234+ type Client (StaticDomBuilderT t m) = UnrunnableT JS' t m
235+ prerender server _ = do
236+ _ <- commentNode $ CommentNodeConfig startMarker Nothing
237+ a <- server
238+ _ <- commentNode $ CommentNodeConfig endMarker Nothing
239+ pure $ pure a
51240
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
241+instance (Prerender js t m, Monad m) => Prerender js t (ReaderT r m) where
242+ type Client (ReaderT r m) = ReaderT r (Client m)
243+ prerender server client = do
244+ r <- ask
245+ lift $ prerender (runReaderT server r) (runReaderT client r)
63246
64−data NoJavaScript -- This type should never have a HasJS instance
247+instance (Prerender js t m, Monad m, Reflex t, MonadFix m, Monoid w) => Prerender js t (DynamicWriterT t w m) where
248+ type Client (DynamicWriterT t w m) = DynamicWriterT t w (Client m)
249+ prerender server client = do
250+ x <- lift $ prerender (runDynamicWriterT server) (runDynamicWriterT client)
251+ let (a, w') = splitDynPure x
252+ w = join w'
253+ tellDyn w
254+ pure a
65255
66−instance js ~ NoJavaScript => Prerender js (StaticDomBuilderT t m) where
67− prerenderClientDict = Nothing
256+instance (Prerender js t m, Monad m, Reflex t, Semigroup w) => Prerender js t (EventWriterT t w m) where
257+ type Client (EventWriterT t w m) = EventWriterT t w (Client m)
258+ prerender server client = do
259+ x <- lift $ prerender (runEventWriterT server) (runEventWriterT client)
260+ let (a, w') = splitDynPure x
261+ w = switch $ current w'
262+ tellEvent w
263+ pure a
68264
69−instance (Prerender js m, ReflexHost t) => Prerender js (PostBuildT t m) where
70− prerenderClientDict = fmap (\Dict -> Dict) (prerenderClientDict :: Maybe (Dict (PrerenderClientConstraint js m)))
265+instance (Prerender js t m, MonadFix m, Reflex t) => Prerender js t (RequesterT t request response m) where
266+ type Client (RequesterT t request response m) = RequesterT t request response (Client m)
267+ prerender server client = mdo
268+ let fannedResponses = fanInt responses
269+ withFannedResponses :: forall m' a. Monad m' => RequesterT t request response m' a -> Int -> m' (a, Event t (IntMap (RequesterData request)))
270+ withFannedResponses w selector = do
271+ (x, e) <- runRequesterT w (selectInt fannedResponses selector)
272+ pure (x, fmapCheap (IntMap.singleton selector) e)
273+ (result, requestsDyn) <- fmap splitDynPure $ lift $ prerender (withFannedResponses server 0) (withFannedResponses client 1)
274+ responses <- fmap (fmapCheap unMultiEntry) $ requesting' $ fmapCheap multiEntry $ switchPromptlyDyn requestsDyn
275+ return result
71276
72−instance Prerender js m => Prerender js (DynamicWriterT t w m) where
73− prerenderClientDict = fmap (\Dict -> Dict) (prerenderClientDict :: Maybe (Dict (PrerenderClientConstraint js m)))
277+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
278+ type Client (QueryT t q m) = QueryT t q (Client m)
279+ prerender server client = mdo
280+ result <- queryDyn query
281+ x <- lift $ prerender (runQueryT server result) (runQueryT client result)
282+ let (a, inc) = splitDynPure x
283+ query = incrementalToDynamic =<< inc -- Can we avoid the incrementalToDynamic?
284+ pure a
74285
75−instance Prerender js m => Prerender js (EventWriterT t w m) where
76− prerenderClientDict = fmap (\Dict -> Dict) (prerenderClientDict :: Maybe (Dict (PrerenderClientConstraint js m)))
286+instance (Prerender js t m, Monad m) => Prerender js t (InputDisabledT m) where
287+ type Client (InputDisabledT m) = InputDisabledT (Client m)
288+ prerender (InputDisabledT server) (InputDisabledT client) = InputDisabledT $ prerender server client
77289
78−instance Prerender js m => Prerender js (ReaderT w m) where
79− prerenderClientDict = fmap (\Dict -> Dict) (prerenderClientDict :: Maybe (Dict (PrerenderClientConstraint js m)))
290+instance (Prerender js t m, Monad m) => Prerender js t (HydratableT m) where
291+ type Client (HydratableT m) = HydratableT (Client m)
292+ prerender (HydratableT server) (HydratableT client) = HydratableT $ prerender server client
80293
81−instance Prerender js m => Prerender js (RequesterT t request response m) where
82− prerenderClientDict = fmap (\Dict -> Dict) (prerenderClientDict :: Maybe (Dict (PrerenderClientConstraint js m)))
294+instance (Prerender js t m, Monad m, ReflexHost t) => Prerender js t (PostBuildT t m) where
295+ type Client (PostBuildT t m) = PostBuildT t (Client m)
296+ prerender server client = PostBuildT $ do
297+ pb <- ask
298+ lift $ prerender (runPostBuildT server pb) (runPostBuildT client pb)
83299
84−instance Prerender js m => Prerender js (QueryT t q m) where
85− prerenderClientDict = fmap (\Dict -> Dict) (prerenderClientDict :: Maybe (Dict (PrerenderClientConstraint js m)))
300+startMarker, endMarker :: Text
301+startMarker = "prerender/start"
302+endMarker = "prerender/end"
86303
87−instance Prerender js m => Prerender js (InputDisabledT m) where
88− prerenderClientDict = fmap (\Dict -> Dict) (prerenderClientDict :: Maybe (Dict (PrerenderClientConstraint js m)))
304+deleteToPrerenderEnd :: (MonadIO m, MonadJSM m, Reflex t, MonadFix m) => DOM.Document -> HydrationRunnerT t m DOM.Comment
305+deleteToPrerenderEnd doc = do
306+ startNode <- hydrateComment doc startMarker Nothing
307+ let go (n :: Int) lastNode = Node.getNextSibling lastNode >>= \case
308+ Nothing -> do
309+ c <- Document.createComment doc endMarker
310+ insertAfterPreviousNode c
311+ pure c
312+ Just node -> DOM.castTo DOM.Comment node >>= \case
313+ Nothing -> go n node
314+ Just c -> Node.getTextContentUnchecked c >>= \case
315+ t | t == startMarker -> go (succ n) node
316+ | t == endMarker -> case n of
317+ 0 -> pure c
318+ _ -> go (pred n) node
319+ | otherwise -> go n node
320+ endNode <- go 0 $ DOM.toNode startNode
321+ deleteBetweenExclusive startNode endNode
322+ setPreviousNode $ Just $ DOM.toNode endNode
323+ 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/WebSocket.hs+12−4
@@ -55,6 +55,7 @@ import Data.Text
5555import Data.Text.Encoding
5656import Foreign.JavaScript.Utils (jsonDecode)
5757import GHCJS.DOM.Types (runJSM, askJSM, MonadJSM, liftJSM, JSM)
58+import GHCJS.DOM.WebSocket (getReadyState)
5859import GHCJS.Marshal
5960import qualified Language.Javascript.JSaddle.Monad as JS (catch)
6061
@@ -101,7 +102,7 @@ webSocket' url config onRawMessage = do
101102 liftIO $ triggerEClose args
102103 _ <- liftIO $ atomically $ tryTakeTMVar isOpen
103104 liftIO $ writeIORef currentSocketRef Nothing
104− when (_webSocketConfig_reconnect config) $ do
105+ when (_webSocketConfig_reconnect config) $ forkJSM $ do
105106 liftIO $ threadDelay 1000000
106107 start
107108 start = do
@@ -127,9 +128,11 @@ webSocket' url config onRawMessage = do
127128 mws <- liftIO $ readIORef currentSocketRef
128129 success <- case mws of
129130 Nothing -> return False
130− Just ws -> runJSM ((webSocketSend ws payload >> return True)
131− `JS.catch`
132− (\(_ :: SomeException) -> return False)) ctx
131+ Just ws -> flip runJSM ctx $ do
132+ rs <- getReadyState $ unWebSocket ws
133+ if rs == 1
134+ then (webSocketSend ws payload >> return True) `JS.catch` (\(_ :: SomeException) -> return False)
135+ else return False
133136 unless success $ atomically $ unGetTQueue payloadQueue payload
134137 return $ RawWebSocket eRecv eOpen eError eClose
135138
@@ -141,6 +144,11 @@ jsonWebSocket url cfg = do
141144 ws <- textWebSocket url $ cfg { _webSocketConfig_send = fmap (decodeUtf8 . toStrict . encode) <$> _webSocketConfig_send cfg }
142145 return ws { _webSocket_recv = jsonDecode . textToJSString <$> _webSocket_recv ws }
143146
147+forkJSM :: JSM () -> JSM ()
148+forkJSM a = do
149+ jsm <- askJSM
150+ void $ liftIO $ forkIO $ runJSM a jsm
151+
144152#ifdef USE_TEMPLATE_HASKELL
145153makeLensesWith (lensRules & simpleLenses .~ True) ''WebSocketConfig
146154makeLensesWith (lensRules & simpleLenses .~ True) ''RawWebSocket
reflex-dom-core/src/Reflex/Dom/WebSocket/Foreign.hs+37−23
@@ -12,7 +12,6 @@ module Reflex.Dom.WebSocket.Foreign
1212
1313import Prelude hiding (all, concat, concatMap, div, mapM, mapM_, sequence, span)
1414
15−import Control.Monad.Reader
1615import Data.Bifoldable
1716import Data.ByteString (ByteString)
1817import qualified Data.ByteString.Lazy as LBS
@@ -20,12 +19,11 @@ import Data.Text (Text)
2019import Data.Text.Encoding
2120import Foreign.JavaScript.Utils (bsFromMutableArrayBuffer, bsToArrayBuffer)
2221import GHCJS.DOM.CloseEvent
23−import GHCJS.DOM.EventM (on)
2422import GHCJS.DOM.MessageEvent
25−import GHCJS.DOM.Types (JSM, JSVal, liftJSM, fromJSValUnchecked)
26−import GHCJS.DOM.WebSocket (WebSocket)
23+import GHCJS.DOM.Types (JSM, JSVal, liftJSM, fromJSValUnchecked, WebSocket(..))
2724import qualified GHCJS.DOM.WebSocket as DOM
2825import GHCJS.Foreign (JSType(..), jsTypeOf)
26+import Language.Javascript.JSaddle (fun, eval, toJSVal, call)
2927import Language.Javascript.JSaddle.Helper (mutableArrayBufferFromJSVal)
3028import Language.Javascript.JSaddle.Types (ghcjsPure)
3129
@@ -64,25 +62,41 @@ newWebSocket
6462 -> ((Bool, Word, Text) -> JSM ()) -- onclose
6563 -> JSM JSWebSocket
6664newWebSocket _ url protocols onMessage onOpen onError onClose = do
67− ws <- DOM.newWebSocket url protocols
68− DOM.setBinaryType ws "arraybuffer"
69− _ <- on ws DOM.open $ liftJSM onOpen
70− _ <- on ws DOM.error $ liftJSM onError
71− _ <- on ws DOM.closeEvent $ do
72− e <- ask
73− wasClean <- getWasClean e
74− code <- getCode e
75− reason <- getReason e
76− liftJSM $ onClose (wasClean, code, reason)
77− _ <- on ws DOM.message $ do
78− e <- ask
79− d <- getData e
80− liftJSM $ ghcjsPure (jsTypeOf d) >>= \case
81− String -> onMessage $ Right d
82− _ -> do
83− ab <- mutableArrayBufferFromJSVal d
84− bsFromMutableArrayBuffer ab >>= onMessage . Left
85− return $ JSWebSocket ws
65+ let onOpenWrapped = fun $ \_ _ _ -> onOpen
66+ onErrorWrapped = fun $ \_ _ _ -> onError
67+ onCloseWrapped = fun $ \_ _ (e:_) -> do
68+ let e' = CloseEvent e
69+ wasClean <- getWasClean e'
70+ code <- getCode e'
71+ reason <- getReason e'
72+ liftJSM $ onClose (wasClean, code, reason)
73+ onMessageWrapped = fun $ \_ _ (e:_) -> do
74+ let e' = MessageEvent e
75+ d <- getData e'
76+ liftJSM $ ghcjsPure (jsTypeOf d) >>= \case
77+ String -> onMessage $ Right d
78+ _ -> do
79+ ab <- mutableArrayBufferFromJSVal d
80+ bsFromMutableArrayBuffer ab >>= onMessage . Left
81+ newWS <- eval $ unlines
82+ [ "(function(url, protos, open, error, close, message) {"
83+ , " var ws = new window['WebSocket'](url, protos);"
84+ , " ws['binaryType'] = 'arraybuffer';"
85+ , " ws['addEventListener']('open', open);"
86+ , " ws['addEventListener']('error', error);"
87+ , " ws['addEventListener']('close', close);"
88+ , " ws['addEventListener']('message', message);"
89+ , " return ws;"
90+ , "})"
91+ ]
92+ url' <- toJSVal url
93+ protocols' <- toJSVal protocols
94+ onOpen' <- toJSVal onOpenWrapped
95+ onError' <- toJSVal onErrorWrapped
96+ onClose' <- toJSVal onCloseWrapped
97+ onMessage' <- toJSVal onMessageWrapped
98+ ws <- call newWS newWS [url', protocols', onOpen', onError', onClose', onMessage']
99+ return $ JSWebSocket $ WebSocket ws
86100
87101onBSMessage :: Either ByteString JSVal -> JSM ByteString
88102onBSMessage = either return $ fmap encodeUtf8 . fromJSValUnchecked
reflex-dom-core/src/Reflex/Dom/Widget/Basic.hs+29−7
@@ -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
@@ -58,6 +60,7 @@ module Reflex.Dom.Widget.Basic
5860 , partitionMapBySetLT
5961 ) where
6062
63+import Reflex.Adjustable.Class
6164import Reflex.Class
6265import Reflex.Collection
6366import Reflex.Dom.Builder.Class
@@ -119,9 +122,23 @@ dynText t = do
119122 ]
120123 notReadyUntil postBuild
121124
125+comment :: DomBuilder t m => Text -> m ()
126+comment t = void $ commentNode $ def & commentNodeConfig_initialContents .~ t
127+
128+{-# INLINABLE dynComment #-}
129+dynComment :: forall t m. (PostBuild t m, DomBuilder t m) => Dynamic t Text -> m ()
130+dynComment t = do
131+ postBuild <- getPostBuild
132+ void $ commentNode $ (def :: CommentNodeConfig t) & commentNodeConfig_setContents .~ leftmost
133+ [ updated t
134+ , tag (current t) postBuild
135+ ]
136+ notReadyUntil postBuild
137+
122138display :: (PostBuild t m, DomBuilder t m, Show a) => Dynamic t a -> m ()
123139display = dynText . fmap (T.pack . show)
124140
141+{-# DEPRECATED button "Use 'elAttr'' in combination with 'domEvent' instead" #-}
125142button :: DomBuilder t m => Text -> m (Event t ())
126143button t = do
127144 (e, _) <- element "button" def $ text t
@@ -131,21 +148,21 @@ button t = do
131148-- | Given a Dynamic of widget-creating actions, create a widget that is recreated whenever the Dynamic updates.
132149-- The returned Event of widget results occurs when the Dynamic does.
133150-- Note: Often, the type @a@ is an 'Event', in which case the return value is an Event-of-Events that would typically be flattened (via 'switchHold').
134−dyn :: (DomBuilder t m, PostBuild t m) => Dynamic t (m a) -> m (Event t a)
151+dyn :: (Adjustable t m, NotReady t m, PostBuild t m) => Dynamic t (m a) -> m (Event t a)
135152dyn = networkView
136153
137154-- | Like 'dyn' but discards result.
138−dyn_ :: (DomBuilder t m, PostBuild t m) => Dynamic t (m a) -> m ()
155+dyn_ :: (Adjustable t m, NotReady t m, PostBuild t m) => Dynamic t (m a) -> m ()
139156dyn_ = void . dyn
140157
141158-- | Given an initial widget and an Event of widget-creating actions, create a widget that is recreated whenever the Event fires.
142159-- The returned Dynamic of widget results occurs when the Event does.
143160-- Note: Often, the type 'a' is an Event, in which case the return value is a Dynamic-of-Events that would typically be flattened (via 'switchDyn').
144−widgetHold :: (DomBuilder t m, MonadHold t m) => m a -> Event t (m a) -> m (Dynamic t a)
161+widgetHold :: (Adjustable t m, MonadHold t m) => m a -> Event t (m a) -> m (Dynamic t a)
145162widgetHold = networkHold
146163
147164-- | Like 'widgetHold' but discards result.
148−widgetHold_ :: (DomBuilder t m, MonadHold t m) => m a -> Event t (m a) -> m ()
165+widgetHold_ :: (Adjustable t m, MonadHold t m) => m a -> Event t (m a) -> m ()
149166widgetHold_ z = void . widgetHold z
150167
151168-- | Create a DOM element
@@ -253,24 +270,25 @@ dynamicAttributesToModifyAttributesWithInitial attrs0 d = do
253270 return $ if Map.null p then Nothing else Just p
254271 return modificationsNeeded
255272
256−{-# DEPRECATED Link, linkClass, link "Use domEvent directly" #-}
273+{-# DEPRECATED Link "Will be removed when 'linkClass' and 'link' are removed. Follow those functions' deprecation instructions." #-}
257274newtype Link t
258275 = Link { _link_clicked :: Event t ()
259276 }
260277
261−-- | >>> linkClass "Click here" "link-class"
262−-- > <a class="link-class">Click here</a>
278+{-# DEPRECATED linkClass "Use 'elAttr'' in combination with 'domEvent' for just clicks. Use 'routeLink' for Obelisk navigation" #-}
263279linkClass :: DomBuilder t m => Text -> Text -> m (Link t)
264280linkClass s c = do
265281 (l,_) <- elAttr' "a" ("class" =: c) $ text s
266282 return $ Link $ domEvent Click l
267283
284+{-# DEPRECATED link "Use 'elAttr'' in combination with 'domEvent' for just clicks. Use 'routeLink' for Obelisk navigation" #-}
268285link :: DomBuilder t m => Text -> m (Link t)
269286link s = linkClass s ""
270287
271288divClass :: forall t m a. DomBuilder t m => Text -> m a -> m a
272289divClass = elClass "div"
273290
291+{-# DEPRECATED dtdd "Use an application specific widget generating function" #-}
274292dtdd :: forall t m a. DomBuilder t m => Text -> m a -> m a
275293dtdd h w = do
276294 el "dt" $ text h
@@ -279,7 +297,9 @@ dtdd h w = do
279297blank :: forall m. Monad m => m ()
280298blank = return ()
281299
300+-- TODO: Move to an example project.
282301-- | A widget to display a table with static columns and dynamic rows.
302+{-# DEPRECATED tableDynAttr "Use an application specific widget generating function" #-}
283303tableDynAttr :: forall t m r k v. (Ord k, DomBuilder t m, MonadHold t m, PostBuild t m, MonadFix m)
284304 => Text -- ^ Class applied to <table> element
285305 -> [(Text, k -> Dynamic t r -> m v)] -- ^ Columns of (header, row key -> row value -> child widget)
@@ -295,9 +315,11 @@ tableDynAttr klass cols dRows rowAttrs = elAttr "div" (Map.singleton "style" "zo
295315 dAttrs <- rowAttrs k
296316 elDynAttr' "tr" dAttrs $ mapM (\x -> el "td" $ snd x k r) cols)
297317
318+-- TODO: Move to an example project.
298319-- | A widget to construct a tabbed view that shows only one of its child widgets at a time.
299320-- Creates a header bar containing a <ul> with one <li> per child; clicking a <li> displays
300321-- the corresponding child and hides all others.
322+{-# DEPRECATED tabDisplay "Use an application specific widget generating function" #-}
301323tabDisplay :: forall t m k. (MonadFix m, DomBuilder t m, MonadHold t m, PostBuild t m, Ord k)
302324 => Text -- ^ Class applied to <ul> element
303325 -> Text -- ^ Class applied to currently active <li> element
reflex-dom-core/src/Reflex/Dom/Widget/Input.hs+1−1
@@ -289,7 +289,7 @@ regularToCheckboxViewEventType en r = case en of
289289
290290newtype CheckboxViewEventResult en = CheckboxViewEventResult { unCheckboxViewEventResult :: CheckboxViewEventResultType en }
291291
292−--TODO
292+-- | Create a view only checkbox
293293{-# INLINABLE checkboxView #-}
294294checkboxView :: forall t m. (DomBuilder t m, DomBuilderSpace m ~ GhcjsDomSpace, PostBuild t m, MonadHold t m) => Dynamic t (Map Text Text) -> Dynamic t Bool -> m (Event t Bool)
295295checkboxView dAttrs dValue = do
reflex-dom-core/src/Reflex/Dom/Widget/Lazy.hs+1−1
@@ -108,7 +108,7 @@ virtualList heightPx rowPx maxIndex i0 setI keyToIndex items0 itemsUpdate itemBu
108108 uniqWindow <- holdUniqDyn window
109109 return (uniqWindow, result)
110110 where
111− toStyleAttr m = "style" =: Map.foldWithKey (\k v s -> k <> ":" <> v <> ";" <> s) "" m
111+ toStyleAttr m = "style" =: Map.foldrWithKey (\k v s -> k <> ":" <> v <> ";" <> s) "" m
112112 mkViewport h = toStyleAttr $ "overflow" =: "auto" <> "position" =: "absolute" <>
113113 "left" =: "0" <> "right" =: "0" <> "height" =: (T.pack (show h) <> "px")
114114 mkContainer h = toStyleAttr $ "position" =: "relative" <> "height" =: (T.pack (show h) <> "px")
reflex-dom-core/src/Reflex/Dom/Widget/Resize.hs+4
@@ -33,6 +33,10 @@ import qualified GHCJS.DOM.Types as DOM
3333
3434-- | A widget that wraps the given widget in a div and fires an event when resized.
3535-- Adapted from @github.com\/marcj\/css-element-queries@
36+--
37+-- This function can cause strange scrollbars to appear in some circumstances.
38+-- These can be hidden with pseudo selectors, for example, in webkit browsers:
39+-- .wrapper *::-webkit-scrollbar { width: 0px; background: transparent; }
3640resizeDetector :: (MonadJSM m, DomBuilder t m, PostBuild t m, TriggerEvent t m, PerformEvent t m, MonadHold t m, DomBuilderSpace m ~ GhcjsDomSpace, MonadJSM (Performable m), MonadFix m) => m a -> m (Event t (), a)
3741resizeDetector = resizeDetectorWithStyle ""
3842
reflex-dom-core/src/Reflex/Dom/Xhr/Foreign.hs−1
@@ -1,7 +1,6 @@
11{-# LANGUAGE FlexibleInstances #-}
22{-# LANGUAGE JavaScriptFFI #-}
33{-# LANGUAGE OverloadedStrings #-}
4−{-# LANGUAGE TypeSynonymInstances #-}
54
65module Reflex.Dom.Xhr.Foreign (
76 XMLHttpRequest
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/hlint.hs+2
@@ -16,9 +16,11 @@ main = do
1616 , "--ignore=Use unless"
1717 , "--ignore=Use if"
1818 , "--ignore=Use fewer imports"
19+ , "--ignore=Use list comprehension"
1920 , "--ignore=Unnecessary hiding" -- Interferes with cross-version compatibility
2021 , "--ignore=Use <$>"
2122 , "--ignore=Reduce duplication" --TODO: Re-enable this test
23+ , "--ignore=Use list comprehension"
2224 , "--cpp-define=USE_TEMPLATE_HASKELL"
2325 ]
2426 if null ideas then exitSuccess else exitFailure
reflex-dom-core/test/hydration.hsnew file+1416
@@ -0,0 +1,1416 @@
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 FlexibleInstances #-}
10+{-# LANGUAGE GADTs #-}
11+{-# LANGUAGE LambdaCase #-}
12+{-# LANGUAGE MultiParamTypeClasses #-}
13+{-# LANGUAGE OverloadedStrings #-}
14+{-# LANGUAGE PolyKinds #-}
15+{-# LANGUAGE RankNTypes #-}
16+{-# LANGUAGE RecursiveDo #-}
17+{-# LANGUAGE ScopedTypeVariables #-}
18+{-# LANGUAGE TemplateHaskell #-}
19+{-# LANGUAGE TupleSections #-}
20+{-# LANGUAGE TypeApplications #-}
21+{-# LANGUAGE TypeFamilies #-}
22+
23+import Prelude hiding (fail)
24+import Control.Concurrent
25+import Control.Monad hiding (fail)
26+import Control.Monad.Catch
27+import Control.Monad.Fail
28+import Control.Monad.Fix
29+import Control.Monad.IO.Class
30+import Control.Monad.Ref
31+import Data.Constraint.Extras
32+import Data.Constraint.Extras.TH
33+import Data.Dependent.Map (DMap)
34+import Data.Dependent.Sum (DSum(..), (==>), EqTag(..), ShowTag(..))
35+import Data.Functor.Identity
36+import Data.Functor.Misc
37+import Data.GADT.Compare.TH
38+import Data.GADT.Show.TH
39+import Data.IORef (IORef)
40+import Data.Maybe
41+import Data.Proxy
42+import Data.Text (Text)
43+import Language.Javascript.JSaddle (syncPoint, liftJSM)
44+import Language.Javascript.JSaddle.Warp
45+import Network.HTTP.Types (status200)
46+import Network.Socket
47+import Network.Wai
48+import Network.WebSockets
49+import Reflex.Dom.Core
50+import Reflex.Patch.DMapWithMove
51+import System.Directory
52+import System.Environment
53+import System.IO (stderr)
54+import System.IO.Silently
55+import System.IO.Temp
56+import System.Process
57+import System.Which (staticWhich)
58+import qualified Test.HUnit as HUnit (assertEqual, assertFailure)
59+import qualified Test.Hspec as H
60+import qualified Test.Hspec.Core.Spec as H
61+import Test.Hspec (xit)
62+import Test.Hspec.WebDriver hiding (runWD, click, uploadFile, WD)
63+import qualified Test.Hspec.WebDriver as WD
64+import Test.WebDriver (WD(..))
65+import Test.WebDriver.Exceptions (ServerError(..))
66+
67+import qualified Data.ByteString.Lazy as LBS
68+import qualified Data.Dependent.Map as DMap
69+import qualified Data.IntMap as IntMap
70+import qualified Data.Text as T
71+import qualified Data.Text.Encoding as TE
72+import qualified GHCJS.DOM.File as File
73+import qualified Network.Wai.Handler.Warp as Warp
74+import qualified System.FilePath as FilePath
75+import qualified Test.WebDriver as WD
76+import qualified Test.WebDriver.Capabilities as WD
77+
78+import Test.Util.ChromeFlags
79+import Test.Util.UnshareNetwork
80+
81+chromium :: FilePath
82+chromium = $(staticWhich "chromium")
83+
84+seleniumPort, jsaddlePort :: PortNumber
85+seleniumPort = 8000
86+jsaddlePort = 8001
87+
88+-- TODO Remove orphan
89+instance MonadRef WD where
90+ type Ref WD = Ref IO
91+ newRef = WD . newRef
92+ readRef = WD . readRef
93+ writeRef r = WD . writeRef r
94+
95+assertEqual :: (MonadIO m, Eq a, Show a) => String -> a -> a -> m ()
96+assertEqual a b = liftIO . HUnit.assertEqual a b
97+
98+assertFailure :: MonadIO m => String -> m ()
99+assertFailure = liftIO . HUnit.assertFailure
100+
101+chromeConfig :: Text -> [Text] -> WD.WDConfig
102+chromeConfig fp flags = WD.useBrowser (WD.chrome { WD.chromeBinary = Just $ T.unpack fp, WD.chromeOptions = T.unpack <$> flags }) WD.defaultConfig
103+
104+keyMap :: DMap DKey Identity
105+keyMap = DMap.fromList
106+ [ Key_Int ==> 0
107+ , Key_Char ==> 'A'
108+ ]
109+
110+data DKey a where
111+ Key_Int :: DKey Int
112+ Key_Char :: DKey Char
113+ Key_Bool :: DKey Bool
114+
115+
116+textKey :: DKey a -> Text
117+textKey = \case
118+ Key_Int -> "Key_Int"
119+ Key_Char -> "Key_Char"
120+ Key_Bool -> "Key_Bool"
121+
122+deriveArgDict ''DKey
123+deriveGEq ''DKey
124+deriveGCompare ''DKey
125+deriveGShow ''DKey
126+
127+deriving instance MonadFail WD
128+
129+main :: IO ()
130+main = do
131+ unshareNetork
132+ isHeadless <- (== Nothing) <$> lookupEnv "NO_HEADLESS"
133+ withSandboxedChromeFlags isHeadless $ \chromeFlags -> do
134+ withSeleniumServer $ \selenium -> do
135+ let browserPath = T.strip $ T.pack chromium
136+ when (T.null browserPath) $ fail "No browser found"
137+ withDebugging <- isNothing <$> lookupEnv "NO_DEBUG"
138+ let wdConfig = WD.defaultConfig { WD.wdPort = fromIntegral $ _selenium_portNumber selenium }
139+ chromeCaps' = WD.getCaps $ chromeConfig browserPath chromeFlags
140+ hspec (tests withDebugging wdConfig [chromeCaps'] selenium) `finally` _selenium_stopServer selenium
141+
142+tests :: Bool -> WD.WDConfig -> [Capabilities] -> Selenium -> Spec
143+tests withDebugging wdConfig caps _selenium = do
144+ let putStrLnDebug :: MonadIO m => Text -> m ()
145+ putStrLnDebug m = when withDebugging $ liftIO $ putStrLn $ T.unpack m
146+ session' = sessionWith wdConfig "" . using (map (,"") caps)
147+ runWD m = runWDOptions (WdOptions False) $ do
148+ putStrLnDebug "before"
149+ r <- m
150+ putStrLnDebug "after"
151+ return r
152+ testWidgetStatic :: WD b -> (forall m js. TestWidget js (SpiderTimeline Global) m => m ()) -> WD b
153+ testWidgetStatic = testWidgetStaticDebug withDebugging
154+ testWidget :: WD () -> WD b -> (forall m js. TestWidget js (SpiderTimeline Global) m => m ()) -> WD b
155+ testWidget = testWidgetDebug withDebugging
156+ testWidget' :: WD a -> (a -> WD b) -> (forall m js. TestWidget js (SpiderTimeline Global) m => m ()) -> WD b
157+ testWidget' = testWidgetDebug' withDebugging
158+ describe "text" $ session' $ do
159+ it "works" $ runWD $ do
160+ testWidgetStatic (checkBodyText "hello world") $ do
161+ text "hello world"
162+ it "works with postBuild" $ runWD $ do
163+ testWidgetStatic (checkBodyText "pb") $ do
164+ pb <- getPostBuild
165+ void $ textNode $ TextNodeConfig "" $ Just $ "pb" <$ pb
166+ it "works for adjacent text nodes" $ runWD $ do
167+ testWidgetStatic (checkBodyText "hello world") $ do
168+ text "hello "
169+ text "world"
170+ it "works for empty adjacent text nodes" $ runWD $ do
171+ testWidgetStatic (checkBodyText "hello world") $ do
172+ pb <- getPostBuild
173+ text ""
174+ text ""
175+ _ <- textNode $ TextNodeConfig "" $ Just $ "hello " <$ pb
176+ _ <- textNode $ TextNodeConfig "abc" $ Just $ "" <$ pb
177+ _ <- textNode $ TextNodeConfig "" $ Just $ "world" <$ pb
178+ text ""
179+ it "works when empty text nodes are the only children of an element" $ runWD $ do
180+ testWidgetStatic (checkBodyText "hello world") $ do
181+ el "div" $ do
182+ text ""
183+ text ""
184+ text "hello world"
185+ it "works when an empty text node is the first child before text" $ runWD $ do
186+ testWidgetStatic (checkTextInTag "div" "hello world") $ do
187+ el "div" $ do
188+ text ""
189+ text "hello world"
190+ it "works when an empty text node is the first child before element" $ runWD $ do
191+ testWidgetStatic (checkTextInTag "div" "hello world") $ do
192+ el "div" $ do
193+ text ""
194+ el "span" $ text "hello world"
195+ it "works when an empty text node is the last child" $ runWD $ do
196+ testWidgetStatic (checkTextInTag "div" "hello world") $ do
197+ el "div" $ do
198+ el "span" $ text "hello world"
199+ text ""
200+-- TODO I think these two tests are broken
201+-- it "updates after postBuild" $ runWD $ do
202+-- testWidget (checkBodyText "initial") (checkBodyText "after") $ do
203+-- after <- delay 0 =<< getPostBuild
204+-- void $ textNode $ TextNodeConfig "initial" $ Just $ "after" <$ after
205+-- it "updates immediately after postBuild" $ runWD $ do
206+-- testWidget (checkBodyText "pb") (checkBodyText "after") $ do
207+-- pb <- getPostBuild
208+-- after <- delay 0 pb
209+-- void $ textNode $ TextNodeConfig "initial" $ Just $ leftmost ["pb" <$ pb, "after" <$ after]
210+ it "updates in immediate mode" $ runWD $ do
211+ let checkUpdated = do
212+ checkBodyText "initial"
213+ WD.click =<< findElemWithRetry (WD.ByTag "button")
214+ checkBodyText "after"
215+ testWidget (pure ()) checkUpdated $ prerender_ (pure ()) $ do
216+ click <- button ""
217+ void $ textNode $ TextNodeConfig "initial" $ Just $ "after" <$ click
218+
219+ describe "element" $ session' $ do
220+ it "works with domEvent Click" $ runWD $ do
221+ clickedRef <- liftIO $ newRef False
222+ testWidget' (findElemWithRetry $ WD.ByTag "div") WD.click $ do
223+ (e, _) <- el' "div" $ text "hello world"
224+ performEvent_ $ liftIO (writeRef clickedRef True) <$ domEvent Click e
225+ readRef clickedRef `shouldBeWithRetryM` True
226+ it "works with eventFlags stopPropagation" $ runWD $ do
227+ firstClickedRef <- newRef False
228+ secondClickedRef <- newRef False
229+ let clickBoth = do
230+ findElemWithRetry (WD.ById "first") >>= WD.click
231+ findElemWithRetry (WD.ById "second") >>= WD.click
232+ testWidget (pure ()) clickBoth $ do
233+ (firstDivEl, _) <- el' "div" $ prerender_ (pure ()) $ do
234+ void $ elAttr "span" ("id" =: "first") $ text "hello world"
235+ performEvent_ $ liftIO (writeRef firstClickedRef True) <$ domEvent Click firstDivEl
236+ (secondDivEl, _) <- el' "div" $ prerender_ (pure ()) $ do
237+ let conf :: ElementConfig EventResult (SpiderTimeline Global) GhcjsDomSpace
238+ conf = (def :: ElementConfig EventResult (SpiderTimeline Global) GhcjsDomSpace)
239+ & initialAttributes .~ "id" =: "second"
240+ & elementConfig_eventSpec .~ addEventSpecFlags (Proxy :: Proxy GhcjsDomSpace) Click (\_ -> stopPropagation) def
241+ void $ element "span" conf $ text "hello world"
242+ performEvent_ $ liftIO (writeRef secondClickedRef True) <$ domEvent Click secondDivEl
243+ firstClicked <- readRef firstClickedRef
244+ secondClicked <- readRef secondClickedRef
245+ assertEqual "Click propagated when it should have stopped" (True, False) (firstClicked, secondClicked)
246+ it "works with eventFlags preventDefault" $ runWD $ do
247+ let click = do
248+ e <- findElemWithRetry $ WD.ByTag "input"
249+ s0 <- WD.isSelected e
250+ WD.click e
251+ s1 <- WD.isSelected e
252+ pure (s0, s1)
253+ clicked <- testWidget (pure ()) click $ prerender_ (pure ()) $ do
254+ let conf :: ElementConfig EventResult (SpiderTimeline Global) GhcjsDomSpace
255+ conf = (def :: ElementConfig EventResult (SpiderTimeline Global) GhcjsDomSpace)
256+ & elementConfig_eventSpec .~ addEventSpecFlags (Proxy :: Proxy GhcjsDomSpace) Click (\_ -> preventDefault) def
257+ & initialAttributes .~ "type" =: "checkbox"
258+ void $ element "input" conf $ text "hello world"
259+ assertEqual "Click not prevented" (False, False) clicked
260+ it "can add/update/remove attributes" $ runWD $ do
261+ let checkInitialAttrs = do
262+ e <- findElemWithRetry $ WD.ByTag "div"
263+ assertAttr e "const" (Just "const")
264+ assertAttr e "delete" (Just "delete")
265+ assertAttr e "init" (Just "init")
266+ assertAttr e "click" Nothing
267+ pure e
268+ checkModifyAttrs e = do
269+ WD.click e
270+ withRetry $ do
271+ assertAttr e "const" (Just "const")
272+ assertAttr e "delete" Nothing
273+ assertAttr e "init" (Just "click")
274+ assertAttr e "click" (Just "click")
275+ testWidget' checkInitialAttrs checkModifyAttrs $ mdo
276+ let conf = def
277+ & initialAttributes .~ "const" =: "const" <> "delete" =: "delete" <> "init" =: "init"
278+ & modifyAttributes .~ (("delete" =: Nothing <> "init" =: Just "click" <> "click" =: Just "click") <$ click)
279+ (e, ()) <- element "div" conf $ text "hello world"
280+ let click = domEvent Click e
281+ return ()
282+
283+ describe "inputElement" $ do
284+ describe "hydration" $ session' $ do
285+ it "doesn't wipe user input when switching over" $ runWD $ do
286+ inputRef <- newRef ("" :: Text)
287+ testWidget'
288+ (do
289+ e <- findElemWithRetry $ WD.ByTag "input"
290+ WD.sendKeys "hello world" e
291+ pure e)
292+ (\e -> do
293+ WD.attr e "value" `shouldBeWithRetryM` Just "hello world"
294+ WD.click =<< findElemWithRetry (WD.ByTag "button")
295+ readRef inputRef `shouldBeWithRetryM` "hello world"
296+ ) $ do
297+ e <- inputElement def
298+ click <- button "save"
299+ performEvent_ $ liftIO . writeRef inputRef <$> tag (current (value e)) click
300+ it "captures user input after switchover" $ runWD $ do
301+ inputRef <- newRef ("" :: Text)
302+ let checkValue = do
303+ WD.sendKeys "hello world" =<< findElemWithRetry (WD.ByTag "input")
304+ WD.click =<< findElemWithRetry (WD.ByTag "button")
305+ readRef inputRef `shouldBeWithRetryM` "hello world"
306+ testWidget (pure ()) checkValue $ do
307+ e <- inputElement def
308+ click <- button "save"
309+ performEvent_ $ liftIO . writeRef inputRef <$> tag (current (value e)) click
310+ it "sets focus appropriately" $ runWD $ do
311+ focusRef <- newRef False
312+ let checkValue = do
313+ readRef focusRef `shouldBeWithRetryM` False
314+ e <- findElemWithRetry $ WD.ByTag "input"
315+ WD.click e
316+ readRef focusRef `shouldBeWithRetryM` True
317+ testWidget (pure ()) checkValue $ do
318+ e <- inputElement def
319+ performEvent_ $ liftIO . writeRef focusRef <$> updated (_inputElement_hasFocus e)
320+ it "sets focus when focus occurs before hydration" $ runWD $ do
321+ focusRef <- newRef False
322+ let setup = do
323+ e <- findElemWithRetry $ WD.ByTag "input"
324+ WD.click e
325+ ((== e) <$> WD.activeElem) `shouldBeWithRetryM` True
326+ readRef focusRef `shouldBeWithRetryM` False
327+ check = readRef focusRef `shouldBeWithRetryM` True
328+ testWidget setup check $ do
329+ e <- inputElement def
330+ performEvent_ $ liftIO . writeRef focusRef <$> updated (_inputElement_hasFocus e)
331+ it "sets value appropriately" $ runWD $ do
332+ valueByUIRef <- newRef ("" :: Text)
333+ valueRef <- newRef ("" :: Text)
334+ setValueChan :: Chan Text <- liftIO newChan
335+ let checkValue = do
336+ readRef valueByUIRef `shouldBeWithRetryM` ""
337+ readRef valueRef `shouldBeWithRetryM` ""
338+ e <- findElemWithRetry $ WD.ByTag "input"
339+ WD.sendKeys "hello" e
340+ readRef valueByUIRef `shouldBeWithRetryM` "hello"
341+ readRef valueRef `shouldBeWithRetryM` "hello"
342+ liftIO $ writeChan setValueChan "world"
343+ readRef valueByUIRef `shouldBeWithRetryM` "hello"
344+ readRef valueRef `shouldBeWithRetryM` "world"
345+ testWidget (pure ()) checkValue $ do
346+ update <- triggerEventWithChan setValueChan
347+ e <- inputElement $ def & inputElementConfig_setValue .~ update
348+ performEvent_ $ liftIO . writeRef valueByUIRef <$> _inputElement_input e
349+ performEvent_ $ liftIO . writeRef valueRef <$> updated (value e)
350+ it "sets checked appropriately" $ runWD $ do
351+ checkedByUIRef <- newRef False
352+ checkedRef <- newRef False
353+ setCheckedChan <- liftIO newChan
354+ let checkValue = do
355+ readRef checkedByUIRef `shouldBeWithRetryM` False
356+ readRef checkedRef `shouldBeWithRetryM` False
357+ e <- findElemWithRetry $ WD.ByTag "input"
358+ WD.moveToCenter e
359+ WD.click e
360+ readRef checkedByUIRef `shouldBeWithRetryM` True
361+ readRef checkedRef `shouldBeWithRetryM` True
362+ liftIO $ writeChan setCheckedChan False
363+ readRef checkedByUIRef `shouldBeWithRetryM` True
364+ readRef checkedRef `shouldBeWithRetryM` False
365+ testWidget (pure ()) checkValue $ do
366+ setChecked <- triggerEventWithChan setCheckedChan
367+ e <- inputElement $ def
368+ & initialAttributes .~ "type" =: "checkbox"
369+ & inputElementConfig_setChecked .~ setChecked
370+ performEvent_ $ liftIO . writeRef checkedByUIRef <$> _inputElement_checkedChange e
371+ performEvent_ $ liftIO . writeRef checkedRef <$> updated (_inputElement_checked e)
372+ it "captures file uploads" $ runWD $ do
373+ filesRef :: IORef [Text] <- newRef []
374+ let uploadFile = do
375+ e <- findElemWithRetry $ WD.ByTag "input"
376+ path <- liftIO $ writeSystemTempFile "testFile" "file contents"
377+ WD.sendKeys (T.pack path) e
378+ WD.click =<< findElemWithRetry (WD.ByTag "button")
379+ liftIO $ removeFile path
380+ readRef filesRef `shouldBeWithRetryM` [T.pack $ FilePath.takeFileName path]
381+ testWidget (pure ()) uploadFile $ do
382+ e <- inputElement $ def & initialAttributes .~ "type" =: "file"
383+ click <- button "save"
384+ prerender_ (pure ()) $ performEvent_ $ ffor (tag (current (_inputElement_files e)) click) $ \fs -> do
385+ names <- liftJSM $ traverse File.getName fs
386+ liftIO $ writeRef filesRef names
387+
388+ describe "hydration/immediate" $ session' $ do
389+ it "captures user input after switchover" $ runWD $ do
390+ inputRef :: IORef Text <- newRef ""
391+ let checkValue = do
392+ WD.sendKeys "hello world" =<< findElemWithRetry (WD.ByTag "input")
393+ WD.click =<< findElemWithRetry (WD.ByTag "button")
394+ readRef inputRef `shouldBeWithRetryM` "hello world"
395+ testWidget (pure ()) checkValue $ prerender_ (pure ()) $ do
396+ e <- inputElement def
397+ click <- button "save"
398+ performEvent_ $ liftIO . writeRef inputRef <$> tag (current (value e)) click
399+ it "sets focus appropriately" $ runWD $ do
400+ focusRef <- newRef False
401+ let checkValue = do
402+ readRef focusRef `shouldBeWithRetryM` False
403+ e <- findElemWithRetry $ WD.ByTag "input"
404+ WD.click e
405+ readRef focusRef `shouldBeWithRetryM` True
406+ testWidget (pure ()) checkValue $ prerender_ (pure ()) $ do
407+ e <- inputElement def
408+ performEvent_ $ liftIO . writeRef focusRef <$> updated (_inputElement_hasFocus e)
409+ it "sets value appropriately" $ runWD $ do
410+ valueByUIRef :: IORef Text <- newRef ""
411+ valueRef :: IORef Text <- newRef ""
412+ setValueChan :: Chan Text <- liftIO newChan
413+ let checkValue = do
414+ readRef valueByUIRef `shouldBeWithRetryM` ""
415+ readRef valueRef `shouldBeWithRetryM` ""
416+ e <- findElemWithRetry $ WD.ByTag "input"
417+ WD.sendKeys "hello" e
418+ readRef valueByUIRef `shouldBeWithRetryM` "hello"
419+ readRef valueRef `shouldBeWithRetryM` "hello"
420+ liftIO $ writeChan setValueChan "world"
421+ readRef valueByUIRef `shouldBeWithRetryM` "hello"
422+ readRef valueRef `shouldBeWithRetryM` "world"
423+ testWidget (pure ()) checkValue $ do
424+ update <- triggerEventWithChan setValueChan
425+ prerender_ (pure ()) $ do
426+ e <- inputElement $ def & inputElementConfig_setValue .~ update
427+ performEvent_ $ liftIO . writeRef valueByUIRef <$> _inputElement_input e
428+ performEvent_ $ liftIO . writeRef valueRef <$> updated (value e)
429+ it "sets checked appropriately" $ runWD $ do
430+ checkedByUIRef <- newRef False
431+ checkedRef <- newRef False
432+ setCheckedChan <- liftIO newChan
433+ let checkValue = do
434+ readRef checkedByUIRef `shouldBeWithRetryM` False
435+ readRef checkedRef `shouldBeWithRetryM` False
436+ e <- findElemWithRetry $ WD.ByTag "input"
437+ WD.moveToCenter e
438+ WD.click e
439+ readRef checkedByUIRef `shouldBeWithRetryM` True
440+ readRef checkedRef `shouldBeWithRetryM` True
441+ liftIO $ writeChan setCheckedChan False
442+ readRef checkedByUIRef `shouldBeWithRetryM` True
443+ readRef checkedRef `shouldBeWithRetryM` False
444+ testWidget (pure ()) checkValue $ do
445+ setChecked <- triggerEventWithChan setCheckedChan
446+ prerender_ (pure ()) $ do
447+ e <- inputElement $ def
448+ & initialAttributes .~ "type" =: "checkbox"
449+ & inputElementConfig_setChecked .~ setChecked
450+ performEvent_ $ liftIO . writeRef checkedByUIRef <$> _inputElement_checkedChange e
451+ performEvent_ $ liftIO . writeRef checkedRef <$> updated (_inputElement_checked e)
452+ it "captures file uploads" $ runWD $ do
453+ filesRef :: IORef [Text] <- newRef []
454+ let uploadFile = do
455+ e <- findElemWithRetry $ WD.ByTag "input"
456+ path <- liftIO $ writeSystemTempFile "testFile" "file contents"
457+ WD.sendKeys (T.pack path) e
458+ WD.click =<< findElemWithRetry (WD.ByTag "button")
459+ liftIO $ removeFile path
460+ readRef filesRef `shouldBeWithRetryM` [T.pack $ FilePath.takeFileName path]
461+ testWidget (pure ()) uploadFile $ prerender_ (pure ()) $ do
462+ e <- inputElement $ def & initialAttributes .~ "type" =: "file"
463+ click <- button "save"
464+ performEvent_ $ ffor (tag (current (_inputElement_files e)) click) $ \fs -> do
465+ names <- liftJSM $ traverse File.getName fs
466+ liftIO $ writeRef filesRef names
467+
468+ describe "textAreaElement" $ do
469+ describe "hydration" $ session' $ do
470+ it "doesn't wipe user input when switching over" $ runWD $ do
471+ inputRef <- newRef ("" :: Text)
472+ testWidget'
473+ (do
474+ e <- findElemWithRetry $ WD.ByTag "textarea"
475+ WD.sendKeys "hello world" e
476+ pure e)
477+ (\e -> do
478+ WD.attr e "value" `shouldBeWithRetryM` Just "hello world"
479+ WD.click <=< findElemWithRetry $ WD.ByTag "button"
480+ readRef inputRef `shouldBeWithRetryM` "hello world"
481+ ) $ do
482+ e <- textAreaElement def
483+ click <- button "save"
484+ performEvent_ $ liftIO . writeRef inputRef <$> tag (current (value e)) click
485+ it "captures user input after switchover" $ runWD $ do
486+ inputRef <- newRef ("" :: Text)
487+ let checkValue = do
488+ WD.sendKeys "hello world" =<< findElemWithRetry (WD.ByTag "textarea")
489+ WD.click =<< findElemWithRetry (WD.ByTag "button")
490+ readRef inputRef `shouldBeWithRetryM` "hello world"
491+ testWidget (pure ()) checkValue $ do
492+ e <- textAreaElement def
493+ click <- button "save"
494+ performEvent_ $ liftIO . writeRef inputRef <$> tag (current (value e)) click
495+ it "sets focus appropriately" $ runWD $ do
496+ focusRef <- newRef False
497+ let checkValue = do
498+ readRef focusRef `shouldBeWithRetryM` False
499+ e <- findElemWithRetry $ WD.ByTag "textarea"
500+ WD.click e
501+ readRef focusRef `shouldBeWithRetryM` True
502+ testWidget (pure ()) checkValue $ do
503+ e <- textAreaElement def
504+ performEvent_ $ liftIO . writeRef focusRef <$> updated (_textAreaElement_hasFocus e)
505+ it "sets focus when focus occurs before hydration" $ runWD $ do
506+ focusRef <- newRef False
507+ let setup = do
508+ e <- findElemWithRetry $ WD.ByTag "textarea"
509+ WD.click e
510+ ((== e) <$> WD.activeElem) `shouldBeWithRetryM` True
511+ readRef focusRef `shouldBeWithRetryM` False
512+ check = readRef focusRef `shouldBeWithRetryM` True
513+ testWidget setup check $ do
514+ e <- textAreaElement def
515+ performEvent_ $ liftIO . writeRef focusRef <$> updated (_textAreaElement_hasFocus e)
516+ it "sets value appropriately" $ runWD $ do
517+ valueByUIRef <- newRef ("" :: Text)
518+ valueRef <- newRef ("" :: Text)
519+ setValueChan :: Chan Text <- liftIO newChan
520+ let checkValue = do
521+ readRef valueByUIRef `shouldBeWithRetryM` ""
522+ readRef valueRef `shouldBeWithRetryM` ""
523+ e <- findElemWithRetry $ WD.ByTag "textarea"
524+ WD.sendKeys "hello" e
525+ readRef valueByUIRef `shouldBeWithRetryM` "hello"
526+ readRef valueRef `shouldBeWithRetryM` "hello"
527+ liftIO $ writeChan setValueChan "world"
528+ readRef valueByUIRef `shouldBeWithRetryM` "hello"
529+ readRef valueRef `shouldBeWithRetryM` "world"
530+ testWidget (pure ()) checkValue $ do
531+ setValue' <- triggerEventWithChan setValueChan
532+ e <- textAreaElement $ def { _textAreaElementConfig_setValue = Just setValue' }
533+ performEvent_ $ liftIO . writeRef valueByUIRef <$> _textAreaElement_input e
534+ performEvent_ $ liftIO . writeRef valueRef <$> updated (value e)
535+
536+ describe "hydration/immediate" $ session' $ do
537+ it "captures user input after switchover" $ runWD $ do
538+ inputRef :: IORef Text <- newRef ""
539+ let checkValue = do
540+ WD.sendKeys "hello world" <=< findElemWithRetry $ WD.ByTag "textarea"
541+ WD.click <=< findElemWithRetry $ WD.ByTag "button"
542+ readRef inputRef `shouldBeWithRetryM` "hello world"
543+ testWidget (pure ()) checkValue $ prerender_ (pure ()) $ do
544+ e <- textAreaElement def
545+ click <- button "save"
546+ performEvent_ $ liftIO . writeRef inputRef <$> tag (current (value e)) click
547+ it "sets focus appropriately" $ runWD $ do
548+ focusRef <- newRef False
549+ let checkValue = do
550+ readRef focusRef `shouldBeWithRetryM` False
551+ e <- findElemWithRetry $ WD.ByTag "textarea"
552+ WD.click e
553+ readRef focusRef `shouldBeWithRetryM` True
554+ testWidget (pure ()) checkValue $ prerender_ (pure ()) $ do
555+ e <- textAreaElement def
556+ performEvent_ $ liftIO . writeRef focusRef <$> updated (_textAreaElement_hasFocus e)
557+ it "sets value appropriately" $ runWD $ do
558+ valueByUIRef :: IORef Text <- newRef ""
559+ valueRef :: IORef Text <- newRef ""
560+ setValueChan :: Chan Text <- liftIO newChan
561+ let checkValue = do
562+ readRef valueByUIRef `shouldBeWithRetryM` ""
563+ readRef valueRef `shouldBeWithRetryM` ""
564+ e <- findElemWithRetry $ WD.ByTag "textarea"
565+ WD.sendKeys "hello" e
566+ readRef valueByUIRef `shouldBeWithRetryM` "hello"
567+ readRef valueRef `shouldBeWithRetryM` "hello"
568+ liftIO $ writeChan setValueChan "world"
569+ readRef valueByUIRef `shouldBeWithRetryM` "hello"
570+ readRef valueRef `shouldBeWithRetryM` "world"
571+ testWidget (pure ()) checkValue $ do
572+ setValue' <- triggerEventWithChan setValueChan
573+ prerender_ (pure ()) $ do
574+ e <- textAreaElement $ def { _textAreaElementConfig_setValue = Just setValue' }
575+ performEvent_ $ liftIO . writeRef valueByUIRef <$> _textAreaElement_input e
576+ performEvent_ $ liftIO . writeRef valueRef <$> updated (value e)
577+
578+ describe "selectElement" $ do
579+ let options :: DomBuilder t m => m ()
580+ options = do
581+ elAttr "option" ("value" =: "one" <> "id" =: "one") $ text "one"
582+ elAttr "option" ("value" =: "two" <> "id" =: "two") $ text "two"
583+ elAttr "option" ("value" =: "three" <> "id" =: "three") $ text "three"
584+ describe "hydration" $ session' $ do
585+ it "sets initial value correctly" $ runWD $ do
586+ inputRef <- newRef ("" :: Text)
587+ let setup = do
588+ e <- findElemWithRetry $ WD.ByTag "select"
589+ assertAttr e "value" (Just "three")
590+ WD.click =<< findElemWithRetry (WD.ById "two")
591+ pure e
592+ check e = do
593+ assertAttr e "value" (Just "two")
594+ readRef inputRef `shouldBeWithRetryM` "three"
595+ WD.click =<< findElemWithRetry (WD.ByTag "button")
596+ assertAttr e "value" (Just "two")
597+ readRef inputRef `shouldBeWithRetryM` "two"
598+ testWidget' setup check $ do
599+ (e, ()) <- selectElement (def { _selectElementConfig_initialValue = "three" }) options
600+ click <- button "save"
601+ liftIO . writeRef inputRef <=< sample $ current $ _selectElement_value e
602+ performEvent_ $ liftIO . writeRef inputRef <$> tag (current (_selectElement_value e)) click
603+ it "captures user input after switchover" $ runWD $ do
604+ inputRef <- newRef ("" :: Text)
605+ let checkValue = do
606+ e <- findElemWithRetry $ WD.ByTag "select"
607+ assertAttr e "value" (Just "one")
608+ WD.click =<< findElemWithRetry (WD.ById "two")
609+ assertAttr e "value" (Just "two")
610+ WD.click =<< findElemWithRetry (WD.ByTag "button")
611+ readRef inputRef `shouldBeWithRetryM` "two"
612+ testWidget (pure ()) checkValue $ do
613+ (e, ()) <- selectElement def options
614+ click <- button "save"
615+ performEvent_ $ liftIO . writeRef inputRef <$> tag (current (_selectElement_value e)) click
616+ it "sets focus appropriately" $ runWD $ do
617+ focusRef <- newRef False
618+ let checkValue = do
619+ readRef focusRef `shouldBeWithRetryM` False
620+ e <- findElemWithRetry $ WD.ByTag "select"
621+ WD.click e
622+ readRef focusRef `shouldBeWithRetryM` True
623+ testWidget (pure ()) checkValue $ do
624+ (e, ()) <- selectElement def options
625+ performEvent_ $ liftIO . writeRef focusRef <$> updated (_selectElement_hasFocus e)
626+ it "sets focus when focus occurs before hydration" $ runWD $ do
627+ focusRef <- newRef False
628+ let setup = do
629+ e <- findElemWithRetry $ WD.ByTag "select"
630+ WD.click e
631+ ((== e) <$> WD.activeElem) `shouldBeWithRetryM` True
632+ readRef focusRef `shouldBeWithRetryM` False
633+ check = readRef focusRef `shouldBeWithRetryM` True
634+ testWidget setup check $ do
635+ (e, ()) <- selectElement def options
636+ performEvent_ $ liftIO . writeRef focusRef <$> updated (_selectElement_hasFocus e)
637+ it "sets value appropriately" $ runWD $ do
638+ valueByUIRef <- newRef ("" :: Text)
639+ valueRef <- newRef ("" :: Text)
640+ setValueChan :: Chan Text <- liftIO newChan
641+ let checkValue = do
642+ e <- findElemWithRetry $ WD.ByTag "select"
643+ assertAttr e "value" (Just "one")
644+ readRef valueByUIRef `shouldBeWithRetryM` "one"
645+ readRef valueRef `shouldBeWithRetryM` "one"
646+ WD.click <=< findElemWithRetry $ WD.ById "two"
647+ readRef valueByUIRef `shouldBeWithRetryM` "two"
648+ readRef valueRef `shouldBeWithRetryM` "two"
649+ liftIO $ writeChan setValueChan "three"
650+ readRef valueByUIRef `shouldBeWithRetryM` "two"
651+ readRef valueRef `shouldBeWithRetryM` "three"
652+ testWidget (pure ()) checkValue $ do
653+ setValue' <- triggerEventWithChan setValueChan
654+ (e, ()) <- selectElement def { _selectElementConfig_setValue = Just setValue' } options
655+ performEvent_ $ liftIO . writeRef valueByUIRef <$> _selectElement_change e
656+ performEvent_ $ liftIO . writeRef valueRef <$> updated (_selectElement_value e)
657+
658+ describe "hydration/immediate" $ session' $ do
659+ it "captures user input after switchover" $ runWD $ do
660+ inputRef :: IORef Text <- newRef ""
661+ let checkValue = do
662+ WD.click <=< findElemWithRetry $ WD.ById "two"
663+ WD.click <=< findElemWithRetry $ WD.ByTag "button"
664+ readRef inputRef `shouldBeWithRetryM` "two"
665+ testWidget (pure ()) checkValue $ prerender_ (pure ()) $ do
666+ (e, ()) <- selectElement def options
667+ click <- button "save"
668+ performEvent_ $ liftIO . writeRef inputRef <$> tag (current (_selectElement_value e)) click
669+ it "sets focus appropriately" $ runWD $ do
670+ focusRef <- newRef False
671+ let checkValue = do
672+ readRef focusRef `shouldBeWithRetryM` False
673+ e <- findElemWithRetry $ WD.ByTag "select"
674+ WD.click e
675+ readRef focusRef `shouldBeWithRetryM` True
676+ testWidget (pure ()) checkValue $ prerender_ (pure ()) $ do
677+ (e, ()) <- selectElement def options
678+ performEvent_ $ liftIO . writeRef focusRef <$> updated (_selectElement_hasFocus e)
679+ it "has correct initial value" $ runWD $ do
680+ valueRef :: IORef Text <- newRef ""
681+ let checkValue = readRef valueRef `shouldBeWithRetryM` "one"
682+ testWidget (pure ()) checkValue $ do
683+ prerender_ (pure ()) $ do
684+ (e, ()) <- selectElement def { _selectElementConfig_initialValue = "one" } options
685+ liftIO . writeRef valueRef <=< sample $ current $ _selectElement_value e
686+ it "sets value appropriately" $ runWD $ do
687+ valueByUIRef :: IORef Text <- newRef ""
688+ valueRef :: IORef Text <- newRef ""
689+ setValueChan :: Chan Text <- liftIO newChan
690+ let checkValue = do
691+ WD.click =<< findElemWithRetry (WD.ById "two")
692+ readRef valueByUIRef `shouldBeWithRetryM` "two"
693+ readRef valueRef `shouldBeWithRetryM` "two"
694+ liftIO $ writeChan setValueChan "three"
695+ readRef valueByUIRef `shouldBeWithRetryM` "two"
696+ readRef valueRef `shouldBeWithRetryM` "three"
697+ testWidget (pure ()) checkValue $ do
698+ setValue' <- triggerEventWithChan setValueChan
699+ prerender_ (pure ()) $ do
700+ (e, ()) <- selectElement def { _selectElementConfig_setValue = Just setValue' } options
701+ performEvent_ $ liftIO . writeRef valueByUIRef <$> _selectElement_change e
702+ performEvent_ $ liftIO . writeRef valueRef <$> updated (_selectElement_value e)
703+
704+ describe "prerender" $ session' $ do
705+ it "works in simple case" $ runWD $ do
706+ testWidget (checkBodyText "One") (checkBodyText "Two") $ do
707+ prerender_ (text "One") (text "Two")
708+ it "removes element correctly" $ runWD $ do
709+ testWidget' (findElemWithRetry $ WD.ByTag "span") elementShouldBeRemoved $ do
710+ prerender_ (el "span" $ text "One") (text "Two")
711+ it "can be nested in server widget" $ runWD $ do
712+ testWidget (checkBodyText "One") (checkBodyText "Three") $ do
713+ prerender_ (prerender_ (text "One") (text "Two")) (text "Three")
714+ it "can be nested in client widget" $ runWD $ do
715+ testWidget (checkBodyText "One") (checkBodyText "Three") $ do
716+ prerender_ (text "One") (prerender_ (text "Two") (text "Three"))
717+ it "works with prerender_ siblings" $ runWD $ do
718+ testWidget
719+ (checkTextInId "a1" "One" >> checkTextInId "b1" "Three" >> checkTextInId "c1" "Five")
720+ (checkTextInId "a2" "Two" >> checkTextInId "b2" "Four" >> checkTextInId "c2" "Six") $ do
721+ prerender_ (divId "a1" $ text "One") (divId "a2" $ text "Two")
722+ prerender_ (divId "b1" $ text "Three") (divId "b2" $ text "Four")
723+ prerender_ (divId "c1" $ text "Five") (divId "c2" $ text "Six")
724+ it "works inside other element" $ runWD $ do
725+ testWidget (checkTextInTag "div" "One") (checkTextInTag "div" "Two") $ do
726+ el "div" $ prerender_ (text "One") (text "Two")
727+-- TODO re-enable this
728+-- it "places fences and removes them" $ runWD $ do
729+-- testWidget'
730+-- (do
731+-- scripts <- WD.findElems $ WD.ByTag "script"
732+-- filterM (\s -> maybe False (\t -> "prerender" `T.isPrefixOf` t) <$> WD.attr s "type") scripts
733+-- )
734+-- (traverse_ elementShouldBeRemoved)
735+-- (el "span" $ prerender_ (text "One") (text "Two"))
736+ it "postBuild works on server side" $ runWD $ do
737+ lock :: MVar () <- liftIO newEmptyMVar
738+ testWidget (liftIO $ takeMVar lock) (pure ()) $ do
739+ prerender_ (do
740+ pb <- getPostBuild
741+ performEvent_ $ liftIO (putMVar lock ()) <$ pb) blank
742+ it "postBuild works on client side" $ runWD $ do
743+ lock :: MVar () <- liftIO newEmptyMVar
744+ testWidget (pure ()) (liftIO $ takeMVar lock) $ do
745+ prerender_ blank $ do
746+ pb <- getPostBuild
747+ performEvent_ $ liftIO (putMVar lock ()) <$ pb
748+ it "result Dynamic is updated *after* switchover" $ runWD $ do
749+ let preSwitchover = checkBodyText "PostBuild"
750+ check = checkBodyText "Client"
751+ testWidget preSwitchover check $ void $ do
752+ d <- prerender (pure "Initial") (pure "Client")
753+ pb <- getPostBuild
754+ initial <- sample $ current d
755+ textNode $ TextNodeConfig initial $ Just $ leftmost [updated d, "PostBuild" <$ pb]
756+ -- This essentially checks that the client IO runs *after* switchover/postBuild,
757+ -- thus can't create conflicting DOM
758+ it "can't exploit IO to break hydration" $ runWD $ do
759+ let preSwitchover = checkBodyText "Initial"
760+ testWidgetStatic preSwitchover $ void $ do
761+ ref <- liftIO $ newRef "Initial"
762+ prerender_ (pure ()) (liftIO $ writeRef ref "Client")
763+ text <=< liftIO $ readRef ref
764+ -- As above, so below
765+ it "can't exploit triggerEvent to break hydration" $ runWD $ do
766+ let preSwitchover = checkBodyText "Initial"
767+ check = checkBodyText "Client"
768+ testWidget preSwitchover check $ void $ do
769+ (e, trigger) <- newTriggerEvent
770+ prerender_ (pure ()) (liftIO $ trigger "Client")
771+ textNode $ TextNodeConfig "Initial" $ Just e
772+
773+ describe "runWithReplace" $ session' $ do
774+ it "works" $ runWD $ do
775+ replaceChan :: Chan Text <- liftIO newChan
776+ let setup = findElemWithRetry $ WD.ByTag "div"
777+ check ssr = do
778+ -- Check that the original element still exists and has the correct text
779+ shouldContainText "two" ssr
780+ liftIO $ writeChan replaceChan "three"
781+ elementShouldBeRemoved ssr
782+ shouldContainText "three" =<< findElemWithRetry (WD.ByTag "span")
783+ testWidget' setup check $ do
784+ replace <- triggerEventWithChan replaceChan
785+ pb <- getPostBuild
786+ void $ runWithReplace (el "div" $ text "one") $ leftmost
787+ [ el "div" (text "two") <$ pb
788+ , el "span" . text <$> replace
789+ ]
790+ it "can be nested in initial widget" $ runWD $ do
791+ replaceChan :: Chan Text <- liftIO newChan
792+ let setup = findElemWithRetry $ WD.ByTag "div"
793+ check ssr = do
794+ -- Check that the original element still exists and has the correct text
795+ shouldContainText "two" ssr
796+ liftIO $ writeChan replaceChan "three"
797+ elementShouldBeRemoved ssr
798+ shouldContainText "three" =<< findElemWithRetry (WD.ByTag "span")
799+ testWidget' setup check $ void $ flip runWithReplace never $ do
800+ replace <- triggerEventWithChan replaceChan
801+ pb <- getPostBuild
802+ void $ runWithReplace (el "div" $ text "one") $ leftmost
803+ [ el "div" (text "two") <$ pb
804+ , el "span" . text <$> replace
805+ ]
806+ it "can be nested in postBuild widget" $ runWD $ do
807+ replaceChan :: Chan Text <- liftIO newChan
808+ let setup = findElemWithRetry $ WD.ByTag "div"
809+ check ssr = do
810+ -- Check that the original element still exists and has the correct text
811+ shouldContainText "two" ssr
812+ liftIO $ writeChan replaceChan "three"
813+ elementShouldBeRemoved ssr
814+ shouldContainText "three" =<< findElemWithRetry (WD.ByTag "span")
815+ testWidget' setup check $ void $ do
816+ pb <- getPostBuild
817+ runWithReplace (pure ()) $ ffor pb $ \() -> do
818+ replace <- triggerEventWithChan replaceChan
819+ pb' <- getPostBuild
820+ void $ runWithReplace (el "div" $ text "one") $ leftmost
821+ [ el "div" (text "two") <$ pb'
822+ , el "span" . text <$> replace
823+ ]
824+ it "postBuild works in replaced widget" $ runWD $ do
825+ replaceChan <- liftIO newChan
826+ pbLock <- liftIO newEmptyMVar
827+ let check = liftIO $ do
828+ writeChan replaceChan ()
829+ takeMVar pbLock
830+ testWidget (pure ()) check $ void $ do
831+ replace <- triggerEventWithChan replaceChan
832+ runWithReplace (pure ()) $ ffor replace $ \() -> do
833+ pb <- getPostBuild
834+ performEvent_ $ liftIO (putMVar pbLock ()) <$ pb
835+ it "can be nested in event widget" $ runWD $ do
836+ replaceChan1 :: Chan Text <- liftIO newChan
837+ replaceChan2 :: Chan Text <- liftIO newChan
838+ lock :: MVar () <- liftIO newEmptyMVar
839+ let check = do
840+ shouldContainText "" =<< findElemWithRetry (WD.ByTag "body")
841+ liftIO $ do
842+ writeChan replaceChan1 "one"
843+ takeMVar lock
844+ one <- findElemWithRetry $ WD.ByTag "div"
845+ shouldContainText "pb" one
846+ liftIO $ writeChan replaceChan2 "two"
847+ elementShouldBeRemoved one
848+ shouldContainText "two" =<< findElemWithRetry (WD.ByTag "span")
849+ testWidget (pure ()) check $ void $ do
850+ replace1 <- triggerEventWithChan replaceChan1
851+ runWithReplace (pure ()) $ ffor replace1 $ \r1 -> do
852+ replace2 <- triggerEventWithChan replaceChan2
853+ pb <- getPostBuild
854+ performEvent_ $ liftIO (putMVar lock ()) <$ pb
855+ void $ runWithReplace (el "div" $ text r1) $ leftmost
856+ [ el "span" . text <$> replace2
857+ , el "div" (text "pb") <$ pb
858+ ]
859+ it "prerender works in replaced widget" $ runWD $ do
860+ replaceChan <- liftIO newChan
861+ lock :: MVar () <- liftIO newEmptyMVar
862+ pbLock :: MVar () <- liftIO newEmptyMVar
863+ let check = do
864+ liftIO $ do
865+ writeChan replaceChan ()
866+ takeMVar lock
867+ takeMVar pbLock
868+ testWidget (pure ()) check $ void $ do
869+ replace <- triggerEventWithChan replaceChan
870+ runWithReplace (pure ()) $ ffor replace $ \() -> do
871+ prerender_ (pure ()) $ do
872+ liftIO $ putMVar lock ()
873+ pb <- getPostBuild
874+ performEvent_ $ liftIO (putMVar pbLock ()) <$ pb
875+ it "prerender returns in immediate mode" $ runWD $ do
876+ replaceChan <- liftIO newChan
877+ pbLock <- liftIO newEmptyMVar
878+ let check = liftIO $ do
879+ writeChan replaceChan ()
880+ takeMVar pbLock
881+ testWidget (pure ()) check $ void $ do
882+ replace <- triggerEventWithChan replaceChan
883+ runWithReplace (pure ()) $ ffor replace $ \() -> do
884+ prerender_ (pure ()) (pure ())
885+ liftIO $ putMVar pbLock ()
886+ it "works in immediate mode (RHS of prerender)" $ runWD $ do
887+ replaceChan :: Chan Text <- liftIO newChan
888+ let check = do
889+ checkBodyText "one"
890+ one <- findElemWithRetry $ WD.ByTag "div"
891+ shouldContainText "one" one
892+ liftIO $ writeChan replaceChan "pb"
893+ elementShouldBeRemoved one
894+ shouldContainText "pb" =<< findElemWithRetry (WD.ByTag "span")
895+ testWidget (pure ()) check $ void $ do
896+ replace <- triggerEventWithChan replaceChan
897+ prerender_ blank $ do
898+ void $ runWithReplace (el "div" $ text "one") $ el "span" . text <$> replace
899+ it "works with postBuild in immediate mode (RHS of prerender)" $ runWD $ do
900+ replaceChan :: Chan Text <- liftIO newChan
901+ let check = do
902+ checkBodyText "two"
903+ two <- findElemWithRetry $ WD.ByTag "div"
904+ shouldContainText "two" two
905+ liftIO $ writeChan replaceChan "three"
906+ elementShouldBeRemoved two
907+ shouldContainText "three" =<< findElemWithRetry (WD.ByTag "span")
908+ testWidget (pure ()) check $ void $ do
909+ replace <- triggerEventWithChan replaceChan
910+ prerender_ blank $ do
911+ pb <- getPostBuild
912+ void $ runWithReplace (el "div" $ text "one") $ leftmost
913+ [ el "div" (text "two") <$ pb
914+ , el "span" . text <$> replace
915+ ]
916+
917+ describe "traverseDMapWithKeyWithAdjust" $ session' $ do
918+ let widget :: DomBuilder t m => DKey a -> Identity a -> m (Identity a)
919+ widget k (Identity v) = elAttr "li" ("id" =: textKey k) $ do
920+ elClass "span" "key" $ text $ textKey k
921+ elClass "span" "value" $ text $ T.pack $ has @Show k $ show v
922+ pure (Identity v)
923+ checkItem :: WD.Element -> Text -> Text -> WD ()
924+ checkItem li k v = do
925+ putStrLnDebug "checkItem"
926+ shouldContainTextNoRetry k =<< WD.findElemFrom li (WD.ByClass "key")
927+ shouldContainTextNoRetry v =<< WD.findElemFrom li (WD.ByClass "value")
928+ checkInitialItems dm xs = do
929+ putStrLnDebug "checkInitialItems"
930+ liftIO $ assertEqual "Wrong amount of items in DOM" (DMap.size dm) (length xs)
931+ forM_ (zip xs (DMap.toList dm)) $ \(e, k :=> Identity v) -> checkItem e (textKey k) (T.pack $ has @Show k $ show v)
932+ getAndCheckInitialItems dm = withRetry $ do
933+ putStrLnDebug "getAndCheckInitialItems"
934+ xs <- WD.findElems (WD.ByTag "li")
935+ checkInitialItems dm xs
936+ pure xs
937+ checkRemoval chan k = do
938+ putStrLnDebug "checkRemoval"
939+ e <- findElemWithRetry (WD.ById $ textKey k)
940+ liftIO $ writeChan chan $ PatchDMap $ DMap.singleton k (ComposeMaybe Nothing)
941+ elementShouldBeRemoved e
942+ checkReplace chan k v = do
943+ putStrLnDebug "checkReplace"
944+ e <- findElemWithRetry (WD.ById $ textKey k)
945+ liftIO $ writeChan chan $ PatchDMap $ DMap.singleton k (ComposeMaybe $ Just $ Identity v)
946+ elementShouldBeRemoved e
947+ e' <- findElemWithRetry $ WD.ById $ textKey k
948+ checkItem e' (textKey k) (T.pack $ show v)
949+ checkInsert chan k v = do
950+ putStrLnDebug "checkInsert"
951+ liftIO $ writeChan chan $ PatchDMap $ DMap.singleton k (ComposeMaybe $ Just $ Identity v)
952+ e <- findElemWithRetry (WD.ById $ textKey k)
953+ checkItem e (textKey k) (T.pack $ show v)
954+ postBuildPatch = PatchDMap $ DMap.fromList [Key_Char :=> ComposeMaybe Nothing, Key_Bool :=> ComposeMaybe (Just $ Identity True)]
955+ it "doesn't replace elements at switchover, can delete/update/insert" $ runWD $ do
956+ chan <- liftIO newChan
957+ let preSwitchover :: WD [WD.Element]
958+ preSwitchover = getAndCheckInitialItems keyMap
959+ check :: [WD.Element] -> WD ()
960+ check xs = do
961+ checkInitialItems keyMap xs
962+ checkRemoval chan Key_Int
963+ checkReplace chan Key_Char 'B'
964+ checkInsert chan Key_Bool True
965+ testWidget' preSwitchover check $ do
966+ (dmap, _evt) <- traverseDMapWithKeyWithAdjust widget keyMap =<< triggerEventWithChan chan
967+ liftIO $ dmap `H.shouldBe` keyMap
968+ it "handles postBuild correctly" $ runWD $ do
969+ chan <- liftIO newChan
970+ let preSwitchover = getAndCheckInitialItems $ applyAlways postBuildPatch keyMap
971+ check xs = do
972+ withRetry $ checkInitialItems (applyAlways postBuildPatch keyMap) xs
973+ checkRemoval chan Key_Int
974+ checkInsert chan Key_Char 'B'
975+ checkReplace chan Key_Bool True
976+ testWidget' preSwitchover check $ void $ do
977+ pb <- getPostBuild
978+ replace <- triggerEventWithChan chan
979+ (dmap, _evt) <- traverseDMapWithKeyWithAdjust widget keyMap $ leftmost [postBuildPatch <$ pb, replace]
980+ liftIO $ dmap `H.shouldBe` keyMap
981+ it "can delete/update/insert when built in prerender" $ runWD $ do
982+ chan <- liftIO newChan
983+ let check = do
984+ _ <- getAndCheckInitialItems keyMap
985+ checkRemoval chan Key_Int
986+ checkReplace chan Key_Char 'B'
987+ checkInsert chan Key_Bool True
988+ testWidget (pure ()) check $ do
989+ replace <- triggerEventWithChan chan
990+ prerender_ (pure ()) $ do
991+ (dmap, _evt) <- traverseDMapWithKeyWithAdjust widget keyMap replace
992+ liftIO $ dmap `H.shouldBe` keyMap
993+ it "can delete/update/insert when built in immediate mode" $ runWD $ do
994+ chan <- liftIO newChan
995+ let check = do
996+ _ <- getAndCheckInitialItems keyMap
997+ checkRemoval chan Key_Int
998+ checkReplace chan Key_Char 'B'
999+ checkInsert chan Key_Bool True
1000+ testWidget (pure ()) check $ void $ do
1001+ pb <- getPostBuild
1002+ runWithReplace (pure ()) $ ffor pb $ \() -> void $ do
1003+ (dmap, _evt) <- traverseDMapWithKeyWithAdjust widget keyMap =<< triggerEventWithChan chan
1004+ liftIO $ dmap `H.shouldBe` keyMap
1005+ -- Should be fixed by prerender changes!
1006+ it "handles postBuild correctly in prerender" $ runWD $ do
1007+ chan <- liftIO newChan
1008+ let check = do
1009+ _ <- getAndCheckInitialItems $ applyAlways postBuildPatch keyMap
1010+ checkRemoval chan Key_Int
1011+ checkInsert chan Key_Char 'B'
1012+ checkReplace chan Key_Bool True
1013+ testWidget (pure ()) check $ do
1014+ replace <- triggerEventWithChan chan
1015+ prerender_ (pure ()) $ void $ do
1016+ pb <- getPostBuild
1017+ (dmap, _evt) <- traverseDMapWithKeyWithAdjust widget keyMap $ leftmost [postBuildPatch <$ pb, replace]
1018+ liftIO $ dmap `H.shouldBe` keyMap
1019+
1020+ describe "traverseIntMapWithKeyWithAdjust" $ session' $ do
1021+ let textKeyInt k = "key" <> T.pack (show k)
1022+ intMap = IntMap.fromList
1023+ [ (1, "one")
1024+ , (2, "two")
1025+ , (3, "three")
1026+ ]
1027+ let widget :: DomBuilder t m => IntMap.Key -> Text -> m Text
1028+ widget k v = elAttr "li" ("id" =: textKeyInt k) $ do
1029+ elClass "span" "key" $ text $ textKeyInt k
1030+ elClass "span" "value" $ text v
1031+ pure v
1032+ checkItem :: WD.Element -> Text -> Text -> WD ()
1033+ checkItem li k v = do
1034+ putStrLnDebug "checkItem"
1035+ shouldContainTextNoRetry k =<< WD.findElemFrom li (WD.ByClass "key")
1036+ shouldContainTextNoRetry v =<< WD.findElemFrom li (WD.ByClass "value")
1037+ checkInitialItems dm xs = do
1038+ putStrLnDebug "checkInitialItems"
1039+ liftIO $ assertEqual "Wrong amount of items in DOM" (IntMap.size dm) (length xs)
1040+ forM_ (zip xs (IntMap.toList dm)) $ \(e, (k, v)) -> checkItem e (textKeyInt k) v
1041+ getAndCheckInitialItems dm = withRetry $ do
1042+ putStrLnDebug "getAndCheckInitialItems"
1043+ xs <- WD.findElems (WD.ByTag "li")
1044+ checkInitialItems dm xs
1045+ pure xs
1046+ checkRemoval chan k = do
1047+ putStrLnDebug "checkRemoval"
1048+ e <- findElemWithRetry (WD.ById $ textKeyInt k)
1049+ liftIO $ writeChan chan $ PatchIntMap $ IntMap.singleton k Nothing
1050+ elementShouldBeRemoved e
1051+ checkReplace chan k v = do
1052+ putStrLnDebug "checkReplace"
1053+ e <- findElemWithRetry (WD.ById $ textKeyInt k)
1054+ liftIO $ writeChan chan $ PatchIntMap $ IntMap.singleton k $ Just v
1055+ elementShouldBeRemoved e
1056+ e' <- findElemWithRetry (WD.ById $ textKeyInt k)
1057+ withRetry $ checkItem e' (textKeyInt k) v
1058+ checkInsert chan k v = do
1059+ putStrLnDebug "checkInsert"
1060+ liftIO $ writeChan chan $ PatchIntMap $ IntMap.singleton k $ Just v
1061+ e <- findElemWithRetry (WD.ById $ textKeyInt k)
1062+ checkItem e (textKeyInt k) v
1063+ postBuildPatch = PatchIntMap $ IntMap.fromList [(2, Nothing), (3, Just "trois"), (4, Just "four")]
1064+ xit "doesn't replace elements at switchover, can delete/update/insert" $ runWD $ do
1065+ chan <- liftIO newChan
1066+ let preSwitchover = getAndCheckInitialItems intMap
1067+ check xs = do
1068+ checkInitialItems intMap xs
1069+ checkRemoval chan 1
1070+ checkReplace chan 2 "deux"
1071+ checkInsert chan 4 "four"
1072+ testWidget' preSwitchover check $ void $ do
1073+ (im, _evt) <- traverseIntMapWithKeyWithAdjust widget intMap =<< triggerEventWithChan chan
1074+ liftIO $ im `H.shouldBe` intMap
1075+ xit "handles postBuild correctly" $ runWD $ do
1076+ chan <- liftIO newChan
1077+ let preSwitchover = getAndCheckInitialItems $ applyAlways postBuildPatch intMap
1078+ check xs = do
1079+ withRetry $ checkInitialItems (applyAlways postBuildPatch intMap) xs
1080+ checkRemoval chan 1
1081+ checkInsert chan 2 "deux"
1082+ checkReplace chan 3 "trois"
1083+ testWidget' preSwitchover check $ void $ do
1084+ pb <- getPostBuild
1085+ replace <- triggerEventWithChan chan
1086+ (dmap, _evt) <- traverseIntMapWithKeyWithAdjust widget intMap $ leftmost [postBuildPatch <$ pb, replace]
1087+ liftIO $ dmap `H.shouldBe` intMap
1088+ xit "can delete/update/insert when built in prerender" $ runWD $ do
1089+ chan <- liftIO newChan
1090+ let check = do
1091+ _ <- getAndCheckInitialItems intMap
1092+ checkRemoval chan 1
1093+ checkReplace chan 2 "deux"
1094+ checkInsert chan 4 "quatre"
1095+ testWidget (pure ()) check $ do
1096+ replace <- triggerEventWithChan chan
1097+ prerender_ (pure ()) $ do
1098+ (dmap, _evt) <- traverseIntMapWithKeyWithAdjust widget intMap replace
1099+ liftIO $ dmap `H.shouldBe` intMap
1100+ xit "can delete/update/insert when built in immediate mode" $ runWD $ do
1101+ chan <- liftIO newChan
1102+ let check = do
1103+ _ <- getAndCheckInitialItems intMap
1104+ checkRemoval chan 1
1105+ checkReplace chan 2 "deux"
1106+ checkInsert chan 4 "quatre"
1107+ testWidget (pure ()) check $ void $ do
1108+ pb <- getPostBuild
1109+ runWithReplace (pure ()) $ ffor pb $ \() -> void $ do
1110+ (dmap, _evt) <- traverseIntMapWithKeyWithAdjust widget intMap =<< triggerEventWithChan chan
1111+ liftIO $ dmap `H.shouldBe` intMap
1112+ -- Should be fixed by prerender changes!
1113+ xit "handles postBuild correctly in prerender" $ runWD $ do
1114+ chan <- liftIO newChan
1115+ let check = do
1116+ _ <- getAndCheckInitialItems $ applyAlways postBuildPatch intMap
1117+ checkRemoval chan 1
1118+ checkInsert chan 2 "deux"
1119+ checkReplace chan 3 "trois"
1120+ testWidget (pure ()) check $ do
1121+ replace <- triggerEventWithChan chan
1122+ prerender_ (pure ()) $ void $ do
1123+ pb <- getPostBuild
1124+ (dmap, _evt) <- traverseIntMapWithKeyWithAdjust widget intMap $ leftmost [postBuildPatch <$ pb, replace]
1125+ liftIO $ dmap `H.shouldBe` intMap
1126+
1127+ describe "traverseDMapWithKeyWithAdjustWithMove" $ session' $ do
1128+ let widget :: DomBuilder t m => Key2 a -> Identity a -> m (Identity a)
1129+ widget k (Identity v) = elAttr "li" ("id" =: textKey2 k) $ do
1130+ elClass "span" "key" $ text $ textKey2 k
1131+ elClass "span" "value" $ text $ T.pack $ has @Show k $ show v
1132+ pure (Identity v)
1133+ textKey2 :: Key2 a -> Text
1134+ textKey2 = \case
1135+ Key2_Int i -> "i" <> T.pack (show i)
1136+ Key2_Char c -> "c" <> T.pack [c]
1137+ checkItem :: WD.Element -> Text -> Text -> WD ()
1138+ checkItem li k v = do
1139+ shouldContainTextNoRetry k =<< WD.findElemFrom li (WD.ByClass "key")
1140+ shouldContainTextNoRetry v =<< WD.findElemFrom li (WD.ByClass "value")
1141+ checkInitialItems dm xs = do
1142+ liftIO $ assertEqual "Wrong amount of items in DOM" (DMap.size dm) (length xs)
1143+ forM_ (zip xs (DMap.toList dm)) $ \(e, k :=> Identity v) -> checkItem e (textKey2 k) (T.pack $ has @Show k $ show v)
1144+ getAndCheckInitialItems dm = withRetry $ do
1145+ xs <- WD.findElems (WD.ByTag "li")
1146+ checkInitialItems dm xs
1147+ pure xs
1148+ moveSpec
1149+ :: (DMap Key2 Identity -> (WD.Element -> Chan (PatchDMapWithMove Key2 Identity) -> WD ()) -> WD ())
1150+ -> H.SpecM (WdTestSession ()) ()
1151+ moveSpec testMove = do
1152+ it "can insert an item" $ runWD $ testMove (DMap.fromList [Key2_Int 1 ==> 1, Key2_Int 3 ==> 3]) $ \body chan -> do
1153+ shouldContainText (T.strip $ T.unlines ["i11","i33"]) body
1154+ liftIO $ writeChan chan (insertDMapKey (Key2_Int 2) 2)
1155+ shouldContainText (T.strip $ T.unlines ["i11","i22","i33"]) body
1156+ it "can delete an item" $ runWD $ testMove (DMap.fromList [Key2_Int 1 ==> 1, Key2_Int 2 ==> 2, Key2_Int 3 ==> 3]) $ \body chan -> do
1157+ shouldContainText (T.strip $ T.unlines ["i11","i22","i33"]) body
1158+ liftIO $ writeChan chan (deleteDMapKey (Key2_Int 2))
1159+ shouldContainText (T.strip $ T.unlines ["i11","i33"]) body
1160+ 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
1161+ shouldContainText (T.strip $ T.unlines ["i11","i22","i33","i44"]) body
1162+ liftIO $ writeChan chan (swapDMapKey (Key2_Int 2) (Key2_Int 3))
1163+ shouldContainText (T.strip $ T.unlines ["i11","i33","i22","i44"]) body
1164+-- TODO re-enable this
1165+-- 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
1166+-- shouldContainText (T.strip $ T.unlines ["i11","i22","i33", "i44"]) body
1167+-- liftIO $ writeChan chan (moveDMapKey (Key2_Int 2) (Key2_Int 3))
1168+-- shouldContainText (T.strip $ T.unlines ["i11","i22","i44"]) body
1169+-- liftIO $ writeChan chan (moveDMapKey (Key2_Int 2) (Key2_Int 3)) -- attempt a move to nonexistent key should delete
1170+-- shouldContainText (T.strip $ T.unlines ["i11","i44"]) body
1171+-- liftIO $ writeChan chan (moveDMapKey (Key2_Int 2) (Key2_Int 3)) -- this causes a JSException in immediate builder too
1172+-- shouldContainText (T.strip $ T.unlines ["i11","i44"]) body
1173+-- 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
1174+-- shouldContainText (T.strip $ T.unlines ["i11","i22","i44"]) body
1175+
1176+ describe "hydration" $ moveSpec $ \initMap test -> do
1177+ chan <- liftIO newChan
1178+ let preSwitchover = getAndCheckInitialItems initMap
1179+ check xs = do
1180+ checkInitialItems initMap xs
1181+ body <- getBody
1182+ test body chan
1183+ testWidget' preSwitchover check $ void $ do
1184+ (dmap, _evt) <- traverseDMapWithKeyWithAdjustWithMove widget initMap =<< triggerEventWithChan chan
1185+ liftIO $ assertEqual "DMap" initMap dmap
1186+
1187+ describe "hydration/immediate" $ moveSpec $ \initMap test -> do
1188+ chan <- liftIO newChan
1189+ replace <- liftIO newChan
1190+ lock :: MVar () <- liftIO newEmptyMVar
1191+ let check = do
1192+ liftIO $ do
1193+ writeChan replace ()
1194+ takeMVar lock
1195+ xs <- getAndCheckInitialItems initMap
1196+ checkInitialItems initMap xs
1197+ body <- getBody
1198+ test body chan
1199+ testWidget (pure ()) check $ void $ do
1200+ e <- triggerEventWithChan replace
1201+ runWithReplace (pure ()) $ ffor e $ \() -> void $ do
1202+ pb <- getPostBuild
1203+ performEvent_ $ liftIO (putMVar lock ()) <$ pb
1204+ (dmap, _evt) <- traverseDMapWithKeyWithAdjustWithMove widget initMap =<< triggerEventWithChan chan
1205+ liftIO $ assertEqual "DMap" initMap dmap
1206+
1207+ describe "hydrating invalid HTML" $ session' $ do
1208+ it "can hydrate list in paragraph" $ runWD $ do
1209+ let preSwitchover = do
1210+ checkBodyText "before\ninner\nafter"
1211+ -- Two <p> tags should be present
1212+ (p1, p2) <- WD.findElems (WD.ByTag "p") >>= \case
1213+ [p1, p2] -> pure (p1, p2)
1214+ _ -> error "Unexpected number of `p` tags (expected 2)"
1215+ ol <- findElemWithRetry (WD.ByTag "ol")
1216+ shouldContainText "before" p1
1217+ shouldContainText "inner" ol
1218+ shouldContainText "" p2
1219+ pure (p1, ol, p2)
1220+ check (p1, ol, p2) = do
1221+ checkBodyText "before\ninner\nafter"
1222+ shouldContainText "before\ninner\nafter" p1
1223+ elementShouldBeRemoved ol
1224+ elementShouldBeRemoved p2
1225+ testWidget' preSwitchover check $ do
1226+ -- This is deliberately invalid HTML, the browser will interpret it as
1227+ -- <p>before</p><ol>inner</ol>after<p></p>
1228+ el "p" $ do
1229+ text "before"
1230+ el "ol" $ text "inner"
1231+ text "after"
1232+
1233+data Selenium = Selenium
1234+ { _selenium_portNumber :: PortNumber
1235+ , _selenium_stopServer :: IO ()
1236+ }
1237+
1238+startSeleniumServer :: PortNumber -> IO (IO ())
1239+startSeleniumServer port = do
1240+ (_,_,_,ph) <- createProcess $ (proc "selenium-server" ["-port", show port])
1241+ { std_in = NoStream
1242+ , std_out = NoStream
1243+ , std_err = NoStream
1244+ }
1245+ return $ terminateProcess ph
1246+
1247+withSeleniumServer :: (Selenium -> IO ()) -> IO ()
1248+withSeleniumServer f = do
1249+ stopServer <- startSeleniumServer seleniumPort
1250+ threadDelay $ 1000 * 1000 * 2 -- TODO poll or wait on a a signal to block on
1251+ f $ Selenium
1252+ { _selenium_portNumber = seleniumPort
1253+ , _selenium_stopServer = stopServer
1254+ }
1255+
1256+triggerEventWithChan :: (Reflex t, TriggerEvent t m, Prerender js t m) => Chan a -> m (Event t a)
1257+triggerEventWithChan chan = do
1258+ (e, trigger) <- newTriggerEvent
1259+ -- In prerender because we only want to do this on the client
1260+ prerender_ (pure ()) $ void $ liftIO $ forkIO $ forever $ trigger =<< readChan chan
1261+ pure e
1262+
1263+shouldBeWithRetryM :: (Eq a, Show a) => WD a -> a -> WD ()
1264+shouldBeWithRetryM m expected = withRetry $ do
1265+ got <- m
1266+ got `shouldBe` expected
1267+
1268+assertAttr :: WD.Element -> Text -> Maybe Text -> WD ()
1269+assertAttr e k v = liftIO . assertEqual "Incorrect attribute value" v =<< WD.attr e k
1270+
1271+elementShouldBeRemoved :: WD.Element -> WD ()
1272+elementShouldBeRemoved e = withRetry $ do
1273+ try (WD.getText e) >>= \case
1274+ Left (WD.FailedCommand WD.StaleElementReference _) -> return ()
1275+ Left err -> throwM err
1276+ Right !_ -> liftIO $ assertFailure "Expected element to be removed, but it still exists"
1277+
1278+shouldContainText :: Text -> WD.Element -> WD ()
1279+shouldContainText t = withRetry . shouldContainTextNoRetry t
1280+
1281+shouldContainTextNoRetry :: Text -> WD.Element -> WD ()
1282+shouldContainTextNoRetry t = flip shouldBe t <=< WD.getText
1283+
1284+checkBodyText :: Text -> WD ()
1285+checkBodyText = checkTextInTag "body"
1286+
1287+checkTextInTag :: Text -> Text -> WD ()
1288+checkTextInTag t expected = do
1289+ e <- findElemWithRetry (WD.ByTag t)
1290+ shouldContainText expected e
1291+
1292+checkTextInId :: Text -> Text -> WD ()
1293+checkTextInId i expected = do
1294+ e <- findElemWithRetry (WD.ById i)
1295+ shouldContainText expected e
1296+
1297+findElemWithRetry :: Selector -> WD WD.Element
1298+findElemWithRetry = withRetry . WD.findElem
1299+
1300+getBody :: WD WD.Element
1301+getBody = WD.findElem $ WD.ByTag "body"
1302+
1303+withRetry :: forall a. WD a -> WD a
1304+withRetry a = wait 300
1305+ where wait :: Int -> WD a
1306+ wait 0 = a
1307+ wait n = try a >>= \case
1308+ Left (_ :: SomeException) -> do
1309+ liftIO $ threadDelay 100000
1310+ wait $ n - 1
1311+ Right v -> return v
1312+
1313+divId :: DomBuilder t m => Text -> m a -> m a
1314+divId i = elAttr "div" ("id" =: i)
1315+
1316+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)
1317+
1318+testWidgetStaticDebug
1319+ :: Bool
1320+ -> WD b
1321+ -- ^ Webdriver commands to run before JS runs and after hydration switchover
1322+ -> (forall m js. TestWidget js (SpiderTimeline Global) m => m ())
1323+ -- ^ Widget we are testing
1324+ -> WD b
1325+testWidgetStaticDebug withDebugging w = testWidgetDebug withDebugging (void w) w
1326+
1327+-- | TODO: do something about JSExceptions not causing tests to fail
1328+testWidgetDebug
1329+ :: Bool
1330+ -> WD ()
1331+ -- ^ Webdriver commands to run before the JS runs (i.e. on the statically rendered page)
1332+ -> WD b
1333+ -- ^ Webdriver commands to run after hydration switchover
1334+ -> (forall m js. TestWidget js (SpiderTimeline Global) m => m ())
1335+ -- ^ Widget we are testing
1336+ -> WD b
1337+testWidgetDebug withDebugging beforeJS afterSwitchover =
1338+ testWidgetDebug' withDebugging beforeJS (const afterSwitchover)
1339+
1340+-- | TODO: do something about JSExceptions not causing tests to fail
1341+testWidgetDebug'
1342+ :: Bool
1343+ -> WD a
1344+ -- ^ Webdriver commands to run before the JS runs (i.e. on the statically rendered page)
1345+ -> (a -> WD b)
1346+ -- ^ Webdriver commands to run after hydration switchover
1347+ -> (forall m js. TestWidget js (SpiderTimeline Global) m => m ())
1348+ -- ^ Widget we are testing (contents of body)
1349+ -> WD b
1350+testWidgetDebug' withDebugging beforeJS afterSwitchover bodyWidget = do
1351+ let putStrLnDebug :: MonadIO m => Text -> m ()
1352+ putStrLnDebug m = when withDebugging $ liftIO $ putStrLn $ T.unpack m
1353+ staticApp = do
1354+ el "head" $ pure ()
1355+ el "body" $ do
1356+ bodyWidget
1357+ el "script" $ text $ TE.decodeUtf8 $ LBS.toStrict $ jsaddleJs False
1358+ putStrLnDebug "rendering static"
1359+ ((), html) <- liftIO $ renderStatic $ runHydratableT staticApp
1360+ putStrLnDebug "rendered static"
1361+ waitBeforeJS <- liftIO newEmptyMVar -- Empty until JS should be run
1362+ waitUntilSwitchover <- liftIO newEmptyMVar -- Empty until switchover
1363+ let entryPoint = do
1364+ putStrLnDebug "taking waitBeforeJS"
1365+ liftIO $ takeMVar waitBeforeJS
1366+ let switchOverAction = do
1367+ putStrLnDebug "switchover syncPoint"
1368+ syncPoint
1369+ putStrLnDebug "putting waitUntilSwitchover"
1370+ liftIO $ putMVar waitUntilSwitchover ()
1371+ putStrLnDebug "put waitUntilSwitchover"
1372+ putStrLnDebug "running mainHydrationWidgetWithSwitchoverAction"
1373+ mainHydrationWidgetWithSwitchoverAction switchOverAction blank bodyWidget
1374+ putStrLnDebug "syncPoint after mainHydrationWidgetWithSwitchoverAction"
1375+ syncPoint
1376+ application <- liftIO $ jsaddleOr defaultConnectionOptions entryPoint $ \_ sendResponse -> do
1377+ putStrLnDebug "sending response"
1378+ r <- sendResponse $ responseLBS status200 [] $ "<!doctype html>\n" <> LBS.fromStrict html
1379+ putStrLnDebug "sent response"
1380+ return r
1381+ waitJSaddle <- liftIO newEmptyMVar
1382+ let settings = foldr ($) Warp.defaultSettings
1383+ [ Warp.setPort $ fromIntegral $ toInteger jsaddlePort
1384+ , Warp.setBeforeMainLoop $ do
1385+ putStrLnDebug "putting waitJSaddle"
1386+ putMVar waitJSaddle ()
1387+ putStrLnDebug "put waitJSaddle"
1388+ ]
1389+ -- hSilence to get rid of ConnectionClosed logs
1390+ silenceIfDebug = if withDebugging then id else hSilence [stderr]
1391+ jsaddleWarp = forkIO $ silenceIfDebug $ Warp.runSettings settings application
1392+ jsaddleTid <- liftIO jsaddleWarp
1393+ putStrLnDebug "taking waitJSaddle"
1394+ liftIO $ takeMVar waitJSaddle
1395+ putStrLnDebug "opening page"
1396+ WD.openPage $ "http://localhost:" <> show jsaddlePort
1397+ putStrLnDebug "running beforeJS"
1398+ a <- beforeJS
1399+ putStrLnDebug "putting waitBeforeJS"
1400+ liftIO $ putMVar waitBeforeJS ()
1401+ putStrLnDebug "taking waitUntilSwitchover"
1402+ liftIO $ takeMVar waitUntilSwitchover
1403+ putStrLnDebug "running afterSwitchover"
1404+ b <- afterSwitchover a
1405+ putStrLnDebug "killing jsaddle thread"
1406+ liftIO $ killThread jsaddleTid
1407+ return b
1408+
1409+data Key2 a where
1410+ Key2_Int :: Int -> Key2 Int
1411+ Key2_Char :: Char -> Key2 Char
1412+
1413+deriveGEq ''Key2
1414+deriveGCompare ''Key2
1415+deriveGShow ''Key2
1416+deriveArgDict ''Key2
reflex-dom/ChangeLog.mdnew file+12
@@ -0,0 +1,12 @@
1+# Revision history for reflex-dom
2+
3+## 0.5.1
4+
5+* The default jsaddle backend on macOS when built from nix
6+ is now jsaddle-wkwebview, matching the behaviour of cabal
7+ builds.
8+
9+## 0.5
10+
11+* Re-export new hydration widget "mainWidget" functions.
12+* 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/default.nixdeleted−37
@@ -1,37 +0,0 @@
1−{ mkDerivation, base, bytestring, jsaddle-webkit2gtk, jsaddle-wkwebview, jsaddle-warp, reflex
2−, reflex-dom-core, stdenv, text, ghc, hostPlatform, jsaddle-clib, android-activity ? null
3−, ghcBackend ? if hostPlatform.isDarwin then "warp" else "webkit2gtk"
4−}:
5−assert (builtins.elem ghcBackend [ "warp" "webkit2gtk" ]);
6−let isAndroid = hostPlatform.libc == "bionic";
7− ghcBackendPackage = {
8− webkit2gtk = jsaddle-webkit2gtk;
9− warp = jsaddle-warp;
10− }.${ghcBackend};
11−in mkDerivation {
12− pname = "reflex-dom";
13− version = "0.4";
14− src = builtins.filterSource (path: type: !(builtins.elem (baseNameOf path) [ ".git" "dist" ])) ./.;
15− libraryHaskellDepends = [
16− base bytestring reflex reflex-dom-core text
17− ] ++ (if ghc.isGhcjs or false then [
18− ] else if hostPlatform.isiOS then [
19− jsaddle-wkwebview
20− ] else if isAndroid then [
21− jsaddle-clib
22− android-activity
23− ] else if hostPlatform.isMacOS then [
24− jsaddle-wkwebview
25− ghcBackendPackage
26− ] else [
27− ghcBackendPackage
28− ]);
29− configureFlags = if isAndroid then [
30− "-fandroid"
31− ] else if ghcBackend == "warp" then [
32− "-fuse-warp"
33− ] else [
34− ];
35− description = "Functional Reactive Web Apps with Reflex";
36− license = stdenv.lib.licenses.bsd3;
37−}
reflex-dom/java/org/reflexfrp/reflexdom/MainWidget.java+8−1
@@ -22,6 +22,7 @@ import java.io.IOException;
2222import java.io.InputStream;
2323import android.content.Intent;
2424import android.content.ActivityNotFoundException;
25+import java.util.concurrent.atomic.AtomicBoolean;
2526
2627import java.nio.charset.StandardCharsets;
2728
@@ -45,11 +46,17 @@ public class MainWidget {
4546 wv.setWebContentsDebuggingEnabled(true);
4647 // allow video to play without user interaction
4748 wv.getSettings().setMediaPlaybackRequiresUserGesture(false);
49+ final AtomicBoolean jsaddleLoaded = new AtomicBoolean(false);
4850
4951 wv.setWebViewClient(new WebViewClient() {
5052 @Override
5153 public void onPageFinished(WebView _view, String _url) {
52− wv.evaluateJavascript(initialJS, null);
54+ Log.i("reflex", "onPageFinished");
55+ boolean alreadyLoaded = jsaddleLoaded.getAndSet(true);
56+ if(!alreadyLoaded) {
57+ Log.i("reflex", "loading jsaddle");
58+ wv.evaluateJavascript(initialJS, null);
59+ }
5360 }
5461
5562 // Re-route / to /android_asset
reflex-dom/reflex-dom.cabal+18−18
@@ -1,7 +1,10 @@
11Name: reflex-dom
2−Version: 0.4
2+Version: 0.5.2.0
33Synopsis: Functional Reactive Web Apps with Reflex
4−Description: Reflex-DOM is a Functional Reactive web framework based on the Reflex FRP engine
4+Description:
5+ Reflex-DOM is a Functional Reactive web framework based on the Reflex FRP engine.
6+ .
7+ For hackage documentation, please see: <https://hackage.haskell.org/package/reflex-dom-core>.
58License: BSD3
69License-file: LICENSE
710Author: Ryan Trinkle
@@ -10,7 +13,9 @@ Stability: Experimental
1013Category: FRP, Web, GUI, HTML, Javascript, Reactive, Reactivity, User Interfaces, User-interface
1114Build-type: Simple
1215cabal-version: >=1.24
13−extra-source-files: java/org/reflexfrp/reflexdom/MainWidget.java
16+extra-source-files:
17+ java/org/reflexfrp/reflexdom/MainWidget.java
18+ ChangeLog.md
1419
1520flag use-warp
1621 description: Use jsaddle-warp server
@@ -36,14 +41,9 @@ flag build-examples
3641 default: False
3742 manual: True
3843
39−flag android
40− description: Build an Android app
41− default: False
42− manual: True
43−
4444library
4545 hs-source-dirs: src
46− if flag(android)
46+ if os(android)
4747 hs-source-dirs: src-android
4848 other-modules: Reflex.Dom.Android.MainWidget
4949 build-depends:
@@ -57,28 +57,28 @@ library
5757 cpp-options: -DANDROID
5858 build-tools: hsc2hs
5959 build-depends:
60− base >= 4.7 && < 4.12,
60+ base >= 4.7 && < 4.13,
6161 bytestring == 0.10.*,
62− reflex == 0.5.*,
63− reflex-dom-core >=0.4 && <0.5,
62+ reflex >= 0.6.2,
63+ reflex-dom-core >=0.5.2 && <0.6,
6464 text == 1.2.*
6565 if !impl(ghcjs)
66− if os(osx) || os(ios)
66+ if flag(use-warp) && (os(linux) || os(osx))
6767 build-depends:
68− data-default == 0.7.*,
6968 jsaddle,
70− jsaddle-wkwebview
69+ jsaddle-warp
7170 else
72− if flag(use-warp)
71+ if os(osx) || os(ios)
7372 build-depends:
73+ data-default == 0.7.*,
7474 jsaddle,
75− jsaddle-warp
75+ jsaddle-wkwebview
7676 else
7777 if flag(webkit2gtk)
7878 build-depends:
7979 jsaddle-webkit2gtk
8080 else
81− if !flag(android)
81+ if !os(android)
8282 build-depends:
8383 jsaddle-webkit2gtk
8484
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' #-}
release.nixnew file+35
@@ -0,0 +1,35 @@
1+{ reflex-platform-fun ? import ./dep/reflex-platform
2+}:
3+
4+let
5+ native-reflex-platform = reflex-platform-fun {};
6+ inherit (native-reflex-platform.nixpkgs) lib;
7+ systems = ["x86_64-linux" "x86_64-darwin"];
8+
9+ perPlatform = lib.genAttrs systems (system: let
10+ reflex-platform = reflex-platform-fun { inherit system; };
11+ compilers = [
12+ "ghc"
13+ "ghcjs"
14+ ] ++ lib.optionals (reflex-platform.androidSupport) [
15+ "ghcAndroidAarch64"
16+ "ghcAndroidAarch32"
17+ ] ++ lib.optionals (reflex-platform.iosSupport) [
18+ "ghcIosAarch64"
19+ ];
20+ hsPkgs = lib.genAttrs compilers (ghc: let
21+ ghc' = reflex-platform.${ghc}.override {
22+ overrides = self: super: import ./. self;
23+ };
24+ in {
25+ inherit (ghc') reflex-dom reflex-dom-core;
26+ });
27+ in hsPkgs // {
28+ cache = reflex-platform.pinBuildInputs "reflex-dom-${system}"
29+ (lib.concatLists (map builtins.attrValues (builtins.attrValues hsPkgs)));
30+ });
31+
32+ metaCache = native-reflex-platform.pinBuildInputs "reflex-dom-everywhere"
33+ (map (a: a.cache) (builtins.attrValues perPlatform));
34+
35+in perPlatform // { inherit metaCache; }