From 7a1f7f06f2296618991fe84923eab0466e97c32f Mon Sep 17 00:00:00 2001 From: lazyLambda Date: Mon, 17 Mar 2025 01:13:53 -0400 Subject: [PATCH 1/5] Allow cross-origin cookies in Android WebView --- .../java/org/reflexfrp/reflexdom/MainWidget.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/reflex-dom/java/org/reflexfrp/reflexdom/MainWidget.java b/reflex-dom/java/org/reflexfrp/reflexdom/MainWidget.java index 96c824f0..3c26a9cc 100644 --- a/reflex-dom/java/org/reflexfrp/reflexdom/MainWidget.java +++ b/reflex-dom/java/org/reflexfrp/reflexdom/MainWidget.java @@ -31,8 +31,6 @@ public class MainWidget { private static Object startMainWidget(final HaskellActivity a, String url, long jsaddleCallbacks, final String initialJS) { - CookieManager.setAcceptFileSchemeCookies(true); //TODO: Can we do this just for our own WebView? - // Remove title and notification bars a.requestWindowFeature(Window.FEATURE_NO_TITLE); @@ -43,6 +41,11 @@ private static Object startMainWidget(final HaskellActivity a, String url, long ws.setJavaScriptEnabled(true); ws.setDomStorageEnabled(true); wv.setWebContentsDebuggingEnabled(true); + CookieManager cookieManager = CookieManager.getInstance(); + cookieManager.setAcceptCookie(true); + cookieManager.setAcceptThirdPartyCookies(wv, true); + cookieManager.setAcceptFileSchemeCookies(true); //TODO: Can we do this just for our own WebView? + // allow video to play without user interaction wv.getSettings().setMediaPlaybackRequiresUserGesture(false); final AtomicBoolean jsaddleLoaded = new AtomicBoolean(false); @@ -79,8 +82,7 @@ public WebResourceResponse shouldInterceptRequest (WebView view, WebResourceRequ catch (IOException e) { Log.i("reflex", "Opening resource failed, Webview will handle the request .."); e.printStackTrace(); - } - + } return null; } From 7f1ebd3d50733e37ad61fd9a1335a506a13a1af6 Mon Sep 17 00:00:00 2001 From: lazyLambda Date: Fri, 26 Sep 2025 20:21:41 -0400 Subject: [PATCH 2/5] Expose Android.MainWidget on android --- reflex-dom/reflex-dom.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reflex-dom/reflex-dom.cabal b/reflex-dom/reflex-dom.cabal index 55caaca3..c02b232a 100644 --- a/reflex-dom/reflex-dom.cabal +++ b/reflex-dom/reflex-dom.cabal @@ -59,7 +59,7 @@ library hs-source-dirs: src if os(android) hs-source-dirs: src-android - other-modules: Reflex.Dom.Android.MainWidget + exposed-modules: Reflex.Dom.Android.MainWidget build-depends: aeson >= 1.4 && < 2.2, android-activity == 0.2.*, From 91a4f4d0990841f0afd248427a6bc13238e3c343 Mon Sep 17 00:00:00 2001 From: lazyLambda Date: Fri, 20 Mar 2026 18:20:26 -0400 Subject: [PATCH 3/5] Add comprehensive Haddock documentation to reflex-dom-core Document all major types, classes, and functions across 13 source files with Haddock comments, code examples, cross-references, and @since annotations. Add new Tutorial.hs documentation-only module with 11 sections covering DomBuilder, DOM spaces, elements, events, inputs, prerender, and common patterns. Register Tutorial module in cabal file. Documented modules: - Builder/Static.hs: StaticDomBuilderT, renderStatic, Adjustable instance - Builder/Immediate.hs: HydrationDomBuilderT, GhcjsDomSpace, internal helpers - Builder/Class.hs: DomBuilder class, element, inputElement, domEvent - Builder/Class/Events.hs: EventResult/EventResultType type family tables - Widget/Basic.hs: el, text, dyn, widgetHold, display, button examples - Widget/Input.hs: deprecation guide - Old.hs: MonadWidget migration guide - Prerender.hs: server/client split patterns - WebSocket.hs: webSocket usage examples - Location.hs: browserHistoryWith, manageHistory - Main.hs: mainWidget, Widget type, entry point examples - Class.hs: =: operator, holdOnStartup --- reflex-dom-core/reflex-dom-core.cabal | 1 + .../src/Reflex/Dom/Builder/Class.hs | 243 ++++- .../src/Reflex/Dom/Builder/Class/Events.hs | 73 ++ .../src/Reflex/Dom/Builder/Immediate.hs | 261 +++++- .../src/Reflex/Dom/Builder/Static.hs | 135 +++ reflex-dom-core/src/Reflex/Dom/Class.hs | 28 +- reflex-dom-core/src/Reflex/Dom/Location.hs | 29 + reflex-dom-core/src/Reflex/Dom/Main.hs | 75 +- reflex-dom-core/src/Reflex/Dom/Old.hs | 48 + reflex-dom-core/src/Reflex/Dom/Prerender.hs | 94 +- reflex-dom-core/src/Reflex/Dom/Tutorial.hs | 868 ++++++++++++++++++ reflex-dom-core/src/Reflex/Dom/WebSocket.hs | 63 +- .../src/Reflex/Dom/Widget/Basic.hs | 181 +++- .../src/Reflex/Dom/Widget/Input.hs | 24 + 14 files changed, 2075 insertions(+), 48 deletions(-) create mode 100644 reflex-dom-core/src/Reflex/Dom/Tutorial.hs diff --git a/reflex-dom-core/reflex-dom-core.cabal b/reflex-dom-core/reflex-dom-core.cabal index 84b0aecc..6c91ed83 100644 --- a/reflex-dom-core/reflex-dom-core.cabal +++ b/reflex-dom-core/reflex-dom-core.cabal @@ -138,6 +138,7 @@ library Reflex.Dom.Old Reflex.Dom.Prerender Reflex.Dom.Time + Reflex.Dom.Tutorial Reflex.Dom.WebSocket Reflex.Dom.WebSocket.Query Reflex.Dom.Widget diff --git a/reflex-dom-core/src/Reflex/Dom/Builder/Class.hs b/reflex-dom-core/src/Reflex/Dom/Builder/Class.hs index c75eb501..e77b8057 100644 --- a/reflex-dom-core/src/Reflex/Dom/Builder/Class.hs +++ b/reflex-dom-core/src/Reflex/Dom/Builder/Class.hs @@ -23,6 +23,64 @@ #endif {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-} +-- | +-- Module: Reflex.Dom.Builder.Class +-- +-- The core abstraction for building DOM in reflex-dom. This module defines +-- 'DomBuilder', the typeclass that all DOM-constructing code is written against, +-- along with the configuration and result types for elements, inputs, and events. +-- +-- == DOM Spaces +-- +-- reflex-dom supports three rendering backends, selected by the phantom type +-- in 'DomBuilderSpace': +-- +-- * 'StaticDomSpace' — server-side rendering to 'ByteString'. All events are +-- 'never', all raw elements are @()@, no JavaScript context available. +-- Used by Lamarckian static page generation and 'renderStatic'. +-- +-- * 'GhcjsDomSpace' — live GHCJS DOM. Raw elements are real @DOM.Element@ +-- values, events fire from @addEventListener@ callbacks, full 'MonadJSM' +-- access. Used by the Obelisk frontend and any GHCJS-compiled app. +-- +-- * 'HydrationDomSpace' — hybrid SSR reattach. Reuses server-rendered DOM +-- nodes, then switches to live mode after a switchover event. +-- +-- == Constraint Design +-- +-- 'DomBuilder' intentionally has a minimal superclass set: +-- +-- @ +-- (Monad m, Reflex t, DomSpace (DomBuilderSpace m), NotReady t m, Adjustable t m) +-- @ +-- +-- It does /not/ imply 'MonadHold', 'PostBuild', 'MonadJSM', 'PerformEvent', +-- or 'TriggerEvent'. Add those constraints explicitly when needed: +-- +-- @ +-- \-\- Just build DOM (static-compatible): +-- myWidget :: DomBuilder t m => m () +-- +-- \-\- Dynamic attributes (still static-compatible, attrs just won\'t change): +-- myWidget :: (DomBuilder t m, PostBuild t m) => m () +-- +-- \-\- State management (still static-compatible, state is constant in static): +-- myWidget :: (DomBuilder t m, PostBuild t m, MonadHold t m) => m () +-- +-- \-\- Full interactive widget (static-compatible): +-- myWidget :: (DomBuilder t m, PostBuild t m, MonadHold t m, MonadFix m) => m () +-- @ +-- +-- == Extracting Events +-- +-- Use the primed variant of element builders (e.g. 'Reflex.Dom.Widget.Basic.elAttr\'') +-- to get an 'Element' handle, then 'domEvent' to extract specific events: +-- +-- @ +-- (btnEl, _) <- elAttr\' \"button\" (\"class\" =: \"btn\") $ text \"Click\" +-- let clickEvt = domEvent Click btnEl -- Event t () +-- let keyEvt = domEvent Keydown btnEl -- Event t Word +-- @ module Reflex.Dom.Builder.Class ( module Reflex.Dom.Builder.Class , module Reflex.Dom.Builder.Class.Events @@ -65,21 +123,82 @@ import Data.Type.Coercion import GHCJS.DOM.Types (JSM) import qualified GHCJS.DOM.Types as DOM +-- | Abstraction over the rendering target. Each DOM space defines what +-- raw node types look like and how events are represented. +-- +-- Three spaces exist: +-- +-- [@'StaticDomSpace'@] All @Raw*@ types are @()@. 'EventSpec' is trivial (no events fire). +-- Defined in "Reflex.Dom.Builder.Static". +-- [@'GhcjsDomSpace'@] @Raw*@ types are real jsaddle-dom types (e.g. @DOM.Element@). +-- Events use 'GhcjsEventSpec' backed by @addEventListener@. +-- Defined in "Reflex.Dom.Builder.Immediate". +-- [@'HydrationDomSpace'@] @RawDocument@ is @DOM.Document@ but element types are @()@ +-- until hydration switchover. Defined in "Reflex.Dom.Builder.Immediate". +-- +-- @since 0.8.0.0 class Default (EventSpec d EventResult) => DomSpace d where + -- | How events are specified for this space. 'StaticDomSpace' uses a no-op + -- spec; 'GhcjsDomSpace' uses 'GhcjsEventSpec' with JS callbacks. type EventSpec d :: (EventTag -> *) -> * + -- | The document handle type. @()@ in static, @DOM.Document@ in GHCJS. type RawDocument d :: * + -- | Raw text node. @()@ in static, @DOM.Text@ in GHCJS. type RawTextNode d :: * + -- | Raw comment node. @()@ in static, @DOM.Comment@ in GHCJS. type RawCommentNode d :: * + -- | Raw element node. @()@ in static, @DOM.Element@ in GHCJS. + -- Available via '_element_raw' on the 'Element' returned by primed builders. type RawElement d :: * + -- | Raw input element. @()@ in static, @DOM.HTMLInputElement@ in GHCJS. type RawInputElement d :: * + -- | Raw textarea element. @()@ in static, @DOM.HTMLTextAreaElement@ in GHCJS. type RawTextAreaElement d :: * + -- | Raw select element. @()@ in static, @DOM.HTMLSelectElement@ in GHCJS. type RawSelectElement d :: * + -- | Add event flags (stop propagation, prevent default) to an event spec. addEventSpecFlags :: proxy d -> EventName en -> (Maybe (er en) -> EventFlags) -> EventSpec d er -> EventSpec d er -- | @'DomBuilder' t m@ indicates that @m@ is a 'Monad' capable of building --- dynamic DOM in the 'Reflex' timeline @t@ +-- dynamic DOM in the 'Reflex' timeline @t@. +-- +-- This is the central typeclass of reflex-dom. All element-building functions +-- ('Reflex.Dom.Widget.Basic.el', 'Reflex.Dom.Widget.Basic.elAttr', etc.) are +-- implemented in terms of 'element'. +-- +-- == Superclasses +-- +-- * 'Monad' @m@ +-- * 'Reflex' @t@ — the FRP timeline +-- * 'DomSpace' @(DomBuilderSpace m)@ — which rendering backend +-- * 'NotReady' @t m@ — widget readiness tracking +-- * 'Adjustable' @t m@ — dynamic widget replacement ('runWithReplace') +-- +-- Notably absent: 'MonadHold', 'PostBuild', 'MonadJSM', 'PerformEvent'. +-- These are added as separate constraints where needed, keeping the base +-- interface compatible with all three DOM spaces. +-- +-- == Methods +-- +-- Most user code should use the convenience functions in +-- "Reflex.Dom.Widget.Basic" rather than calling these methods directly. +-- The methods here are the low-level primitives that those functions wrap. +-- +-- == Instances +-- +-- Instances exist for 'StaticDomBuilderT' (from "Reflex.Dom.Builder.Static"), +-- 'ImmediateDomBuilderT' and 'HydrationDomBuilderT' (from +-- "Reflex.Dom.Builder.Immediate"), and all standard monad transformers +-- ('ReaderT', 'PostBuildT', 'RequesterT', 'EventWriterT', etc.). +-- +-- @since 0.8.0.0 class (Monad m, Reflex t, DomSpace (DomBuilderSpace m), NotReady t m, Adjustable t m) => DomBuilder t m | m -> t where + -- | Which 'DomSpace' this builder targets. Determines the concrete types + -- of raw elements, events, and document handles. type DomBuilderSpace m :: * + -- | Create a text node. In static context, serializes the text content. + -- In GHCJS, calls @document.createTextNode@. The optional 'Event' in the + -- config allows updating the text content over time (only fires in GHCJS). textNode :: TextNodeConfig t -> m (TextNode (DomBuilderSpace m) t) default textNode :: ( MonadTrans f , m ~ f m' @@ -89,6 +208,8 @@ class (Monad m, Reflex t, DomSpace (DomBuilderSpace m), NotReady t m, Adjustable => TextNodeConfig t -> m (TextNode (DomBuilderSpace m) t) textNode = lift . textNode {-# INLINABLE textNode #-} + -- | Create an HTML comment node. Used internally for marking dynamic + -- replacement boundaries (e.g. 'runWithReplace' insertion points). commentNode :: CommentNodeConfig t -> m (CommentNode (DomBuilderSpace m) t) default commentNode :: ( MonadTrans f , m ~ f m' @@ -98,6 +219,24 @@ class (Monad m, Reflex t, DomSpace (DomBuilderSpace m), NotReady t m, Adjustable => CommentNodeConfig t -> m (CommentNode (DomBuilderSpace m) t) commentNode = lift . commentNode {-# INLINABLE commentNode #-} + -- | Create a DOM element with the given tag name, configuration, and children. + -- Returns the 'Element' handle (for event queries via 'domEvent') and the + -- child result. This is the primitive that 'Reflex.Dom.Widget.Basic.el', + -- 'Reflex.Dom.Widget.Basic.elAttr', etc. are built on. + -- + -- In 'StaticDomSpace', serializes @\children\<\/tag\>@ as bytes. + -- In 'GhcjsDomSpace', calls @document.createElement@, sets attributes, and + -- appends to the parent node. + -- + -- Most user code should prefer the convenience wrappers: + -- + -- @ + -- \-\- Instead of this: + -- (el_, result) <- element \"div\" (def & initialAttributes .~ (\"class\" =: \"box\")) $ text \"Hi\" + -- + -- \-\- Write this: + -- (el_, result) <- elAttr\' \"div\" (\"class\" =: \"box\") $ text \"Hi\" + -- @ element :: Text -> ElementConfig er t (DomBuilderSpace m) -> m a -> m (Element er (DomBuilderSpace m) t, a) default element :: ( MonadTransControl f , StT f a ~ a @@ -108,6 +247,26 @@ class (Monad m, Reflex t, DomSpace (DomBuilderSpace m), NotReady t m, Adjustable => Text -> ElementConfig er t (DomBuilderSpace m) -> m a -> m (Element er (DomBuilderSpace m) t, a) element t cfg child = liftWith $ \run -> element t cfg $ run child {-# INLINABLE element #-} + -- | Create an @\@ element. Returns an 'InputElement' with: + -- + -- * '_inputElement_value' — @Dynamic t Text@ of the current value + -- * '_inputElement_checked' — @Dynamic t Bool@ of the checked state + -- * '_inputElement_input' — @Event t Text@ firing on user input + -- * '_inputElement_checkedChange' — @Event t Bool@ firing on check change + -- * '_inputElement_hasFocus' — @Dynamic t Bool@ of focus state + -- + -- In 'StaticDomSpace', the value is @constDyn initialValue@ and all + -- events are 'never'. In 'GhcjsDomSpace', values are two-way bound + -- to the live DOM element. + -- + -- @ + -- inp <- inputElement $ def + -- & inputElementConfig_initialValue .~ \"\" + -- & inputElementConfig_elementConfig . elementConfig_initialAttributes .~ + -- (\"placeholder\" =: \"Enter name\" \<\> \"type\" =: \"text\") + -- let valueDyn = _inputElement_value inp -- Dynamic t Text + -- let inputEvt = _inputElement_input inp -- Event t Text + -- @ inputElement :: InputElementConfig er t (DomBuilderSpace m) -> m (InputElement er (DomBuilderSpace m) t) default inputElement :: ( MonadTransControl f , m ~ f m' @@ -117,6 +276,7 @@ class (Monad m, Reflex t, DomSpace (DomBuilderSpace m), NotReady t m, Adjustable => InputElementConfig er t (DomBuilderSpace m) -> m (InputElement er (DomBuilderSpace m) t) inputElement = lift . inputElement {-# INLINABLE inputElement #-} + -- | Create a @\@ element. Like 'inputElement' but for multi-line text. textAreaElement :: TextAreaElementConfig er t (DomBuilderSpace m) -> m (TextAreaElement er (DomBuilderSpace m) t) default textAreaElement :: ( MonadTransControl f , m ~ f m' @@ -126,6 +286,8 @@ class (Monad m, Reflex t, DomSpace (DomBuilderSpace m), NotReady t m, Adjustable => TextAreaElementConfig er t (DomBuilderSpace m) -> m (TextAreaElement er (DomBuilderSpace m) t) textAreaElement = lift . textAreaElement {-# INLINABLE textAreaElement #-} + -- | Create a @\@ element. The child @m a@ should contain @\@ + -- elements. Returns a 'SelectElement' with the current selection value. selectElement :: SelectElementConfig er t (DomBuilderSpace m) -> m a -> m (SelectElement er (DomBuilderSpace m) t, a) default selectElement :: ( MonadTransControl f , StT f a ~ a @@ -137,6 +299,9 @@ class (Monad m, Reflex t, DomSpace (DomBuilderSpace m), NotReady t m, Adjustable selectElement cfg child = do liftWith $ \run -> selectElement cfg $ run child {-# INLINABLE selectElement #-} + -- | Insert a pre-existing raw DOM element into the builder's current + -- position. Only meaningful in 'GhcjsDomSpace' where raw elements are + -- real @DOM.Element@ values. placeRawElement :: RawElement (DomBuilderSpace m) -> m () default placeRawElement :: ( MonadTrans f , m ~ f m' @@ -146,6 +311,9 @@ class (Monad m, Reflex t, DomSpace (DomBuilderSpace m), NotReady t m, Adjustable => RawElement (DomBuilderSpace m) -> m () placeRawElement = lift . placeRawElement {-# INLINABLE placeRawElement #-} + -- | Wrap a pre-existing raw DOM element, attaching event handlers and + -- producing an 'Element' handle. Useful for integrating with elements + -- created outside of reflex-dom (e.g. from a JS library). wrapRawElement :: RawElement (DomBuilderSpace m) -> RawElementConfig er t (DomBuilderSpace m) -> m (Element er (DomBuilderSpace m) t) default wrapRawElement :: ( MonadTrans f , m ~ f m' @@ -218,10 +386,11 @@ mapKeysToAttributeName = Map.mapKeysMonotonic (AttributeName Nothing) instance IsString AttributeName where fromString = AttributeName Nothing . fromString +-- | Controls whether a DOM event continues propagating up the tree. data Propagation - = Propagation_Continue - | Propagation_Stop - | Propagation_StopImmediate + = Propagation_Continue -- ^ Allow normal event propagation. + | Propagation_Stop -- ^ Call @event.stopPropagation()@. + | Propagation_StopImmediate -- ^ Call @event.stopImmediatePropagation()@. deriving (Show, Read, Eq, Ord) instance Semigroup Propagation where @@ -234,9 +403,11 @@ instance Monoid Propagation where {-# INLINABLE mappend #-} mappend = (<>) -data EventFlags = EventFlags --TODO: Monoid; ways of building each flag - { _eventFlags_propagation :: Propagation - , _eventFlags_preventDefault :: Bool +-- | Flags that modify DOM event behavior. Use 'preventDefault' and +-- 'stopPropagation' as convenient constructors. Combine with @('<>')@. +data EventFlags = EventFlags + { _eventFlags_propagation :: Propagation -- ^ How propagation is handled. + , _eventFlags_preventDefault :: Bool -- ^ Whether to call @event.preventDefault()@. } instance Semigroup EventFlags where @@ -255,11 +426,20 @@ preventDefault = mempty { _eventFlags_preventDefault = True } stopPropagation :: EventFlags stopPropagation = mempty { _eventFlags_propagation = Propagation_Stop } +-- | Configuration for creating a DOM element via 'element'. +-- +-- Use 'def' for defaults (no namespace, no attributes, no modifications, +-- default event spec). Lenses are provided for all fields. data ElementConfig er t s = ElementConfig { _elementConfig_namespace :: Maybe Namespace + -- ^ Optional XML namespace (e.g. @Just \"http:\/\/www.w3.org\/2000\/svg\"@ for SVG). , _elementConfig_initialAttributes :: Map AttributeName Text + -- ^ Attributes set at creation time. , _elementConfig_modifyAttributes :: Maybe (Event t (Map AttributeName (Maybe Text))) + -- ^ Dynamic attribute patches. @Just v@ sets\/updates; @Nothing@ removes. + -- Only fires in GHCJS; in static rendering the initial attributes are all that matter. , _elementConfig_eventSpec :: EventSpec s er + -- ^ Event specification for this element. } #ifndef USE_TEMPLATE_HASKELL @@ -278,9 +458,25 @@ elementConfig_eventSpec f (ElementConfig a b c d) = (\d' -> ElementConfig a b c {-# INLINE elementConfig_eventSpec #-} #endif +-- | A handle to a created DOM element. Returned by the primed variants of +-- element builders (e.g. 'Reflex.Dom.Widget.Basic.el\'', 'Reflex.Dom.Widget.Basic.elAttr\''). +-- +-- Use 'domEvent' to extract specific events: +-- +-- @ +-- (e, _) <- el\' \"button\" $ text \"Click\" +-- let click = domEvent Click e -- Event t () +-- let keys = domEvent Keydown e -- Event t Word +-- let mouse = domEvent Mousemove e -- Event t (Int, Int) +-- @ +-- +-- Access the raw DOM element (only useful in 'GhcjsDomSpace') via '_element_raw'. data Element er d t - = Element { _element_events :: EventSelector t (WrapArg er EventName) --TODO: EventSelector should have two arguments + = Element { _element_events :: EventSelector t (WrapArg er EventName) + -- ^ Fan of all possible events on this element. Use 'domEvent' to select one. , _element_raw :: RawElement d + -- ^ The underlying raw DOM element. @()@ in 'StaticDomSpace', + -- @DOM.Element@ in 'GhcjsDomSpace'. } data InputElementConfig er t s @@ -640,6 +836,24 @@ instance (DomBuilder t m, MonadFix m, MonadHold t m, Group q, Query q, Commutati -- * Convenience functions +-- | Extract a specific event from a DOM element, input, or text area. +-- +-- The result type depends on the 'EventName' — see 'EventResultType' for the +-- complete mapping. Common examples: +-- +-- @ +-- (e, _) <- el\' \"div\" $ text \"Hello\" +-- let click = domEvent Click e -- Event t () +-- let keydown = domEvent Keydown e -- Event t Word +-- let mouse = domEvent Mousemove e -- Event t (Int, Int) +-- @ +-- +-- Works on 'Element', 'InputElement', and 'TextAreaElement': +-- +-- @ +-- inp <- inputElement def +-- let inputKeypress = domEvent Keypress inp -- Event t Word +-- @ class HasDomEvent t target eventName | target -> t where type DomEventType target eventName :: * domEvent :: EventName eventName -> target -> Event t (DomEventType target eventName) @@ -728,6 +942,19 @@ deriving instance DomRenderHook t m => DomRenderHook t (QueryT t q m) liftElementConfig :: ElementConfig er t s -> ElementConfig er t s liftElementConfig = id +-- | Provides access to the raw document object for the current 'DomSpace'. +-- +-- In 'GhcjsDomSpace' (from "Reflex.Dom.Builder.Immediate"), @askDocument@ +-- returns a real @DOM.Document@ that you can use for direct DOM operations +-- (e.g. @createElement@, @querySelector@). +-- +-- In 'StaticDomSpace' (from "Reflex.Dom.Builder.Static"), @askDocument@ +-- returns @()@ — there is no document. +-- +-- This class is primarily used internally by the builder implementations. +-- Application code typically uses 'DomBuilder' methods instead. +-- +-- @since 0.8.0.0 class Monad m => HasDocument m where askDocument :: m (RawDocument (DomBuilderSpace m)) default askDocument diff --git a/reflex-dom-core/src/Reflex/Dom/Builder/Class/Events.hs b/reflex-dom-core/src/Reflex/Dom/Builder/Class/Events.hs index 84085b3a..4624d912 100644 --- a/reflex-dom-core/src/Reflex/Dom/Builder/Class/Events.hs +++ b/reflex-dom-core/src/Reflex/Dom/Builder/Class/Events.hs @@ -6,6 +6,24 @@ {-# LANGUAGE TemplateHaskell #-} #endif {-# LANGUAGE TypeFamilies #-} +-- | +-- Module: Reflex.Dom.Builder.Class.Events +-- +-- Type-level enumeration of DOM events. Every standard DOM event has a +-- corresponding 'EventTag' promoted constructor and a matching 'EventName' +-- GADT value. +-- +-- These are used throughout reflex-dom to provide type-safe event selection: +-- +-- @ +-- domEvent Click myElement :: Event t () +-- domEvent Keypress myElement :: Event t Word +-- domEvent Input myElement :: Event t Text +-- @ +-- +-- The result type for each event is determined by the 'EventResult' type +-- family in "Reflex.Dom.Builder.Class", which maps @ClickTag@ to @()@, +-- @KeypressTag@ to @Word@, @InputTag@ to @Text@, etc. module Reflex.Dom.Builder.Class.Events where #ifdef USE_TEMPLATE_HASKELL @@ -17,6 +35,8 @@ import Data.GADT.Compare #endif import Data.Text (Text) +-- | Enumeration of all supported DOM event types. Promoted to the kind level +-- via DataKinds and used as the index for 'EventName' and 'EventResult'. data EventTag = AbortTag | BlurTag @@ -65,6 +85,16 @@ data EventTag | TouchendTag | TouchcancelTag +-- | Singleton GADT for event names. Each constructor corresponds to an +-- 'EventTag' and carries the tag at the type level. +-- +-- Use these constructors with 'domEvent' to select specific event streams: +-- +-- @ +-- clicks = domEvent Click el -- Event t () +-- keypresses = domEvent Keypress el -- Event t Word +-- scrolls = domEvent Scroll el -- Event t Int +-- @ data EventName :: EventTag -> * where Abort :: EventName 'AbortTag Blur :: EventName 'BlurTag @@ -113,8 +143,45 @@ data EventName :: EventTag -> * where Touchend :: EventName 'TouchendTag Touchcancel :: EventName 'TouchcancelTag +-- | Wrapper newtype that pairs an 'EventTag' with its result value. +-- Used as the @er@ parameter throughout the 'DomBuilder' API. +-- +-- When you write @domEvent Click el@, reflex-dom internally selects from +-- an @EventSelector (WrapArg EventResult)@ and unwraps the 'EventResult' +-- to give you the raw 'EventResultType'. +-- +-- @since 0.8.0.0 newtype EventResult en = EventResult { unEventResult :: EventResultType en } +-- | Maps each 'EventTag' to the Haskell type of data extracted from that +-- DOM event. This is what determines the type of @domEvent SomeEvent el@. +-- +-- == Quick reference +-- +-- @ +-- domEvent Click el :: Event t () -- ClickTag → () +-- domEvent Dblclick el :: Event t (Int, Int) -- mouse coordinates +-- domEvent Keypress el :: Event t Word -- key code +-- domEvent Keydown el :: Event t Word -- key code +-- domEvent Keyup el :: Event t Word -- key code +-- domEvent Scroll el :: Event t Double -- scroll position +-- domEvent Mousemove el :: Event t (Int, Int) -- mouse coordinates +-- domEvent Mousedown el :: Event t (Int, Int) -- mouse coordinates +-- domEvent Mouseup el :: Event t (Int, Int) -- mouse coordinates +-- domEvent Input el :: Event t () -- (use _inputElement_value for text) +-- domEvent Change el :: Event t () +-- domEvent Focus el :: Event t () +-- domEvent Blur el :: Event t () +-- domEvent Paste el :: Event t (Maybe Text) -- clipboard text +-- domEvent Touchstart el :: Event t TouchEventResult +-- domEvent Wheel el :: Event t WheelEventResult +-- @ +-- +-- Most events return @()@ — the event firing IS the information. For events +-- with payload (keyboard, mouse, touch, wheel, paste), the result carries +-- the extracted data. +-- +-- @since 0.8.0.0 type family EventResultType (en :: EventTag) :: * where EventResultType 'ClickTag = () EventResultType 'DblclickTag = (Int, Int) @@ -163,9 +230,12 @@ type family EventResultType (en :: EventTag) :: * where EventResultType 'TouchcancelTag = TouchEventResult EventResultType 'WheelTag = WheelEventResult +-- | How to interpret the delta values in a 'WheelEventResult'. data DeltaMode = DeltaPixel | DeltaLine | DeltaPage deriving (Show, Read, Eq, Ord, Bounded, Enum) +-- | Data extracted from a @wheel@ DOM event. Contains scroll delta values +-- along all three axes and the unit of measurement. data WheelEventResult = WheelEventResult { _wheelEventResult_deltaX :: Double , _wheelEventResult_deltaY :: Double @@ -173,6 +243,9 @@ data WheelEventResult = WheelEventResult , _wheelEventResult_deltaMode :: DeltaMode } deriving (Show, Read, Eq, Ord) +-- | Data extracted from a touch DOM event. Contains modifier key states and +-- three lists of 'TouchResult': changed touches, target touches, and all +-- active touches. data TouchEventResult = TouchEventResult { _touchEventResult_altKey :: Bool , _touchEventResult_changedTouches :: [TouchResult] diff --git a/reflex-dom-core/src/Reflex/Dom/Builder/Immediate.hs b/reflex-dom-core/src/Reflex/Dom/Builder/Immediate.hs index fce08d03..dad93ab4 100644 --- a/reflex-dom-core/src/Reflex/Dom/Builder/Immediate.hs +++ b/reflex-dom-core/src/Reflex/Dom/Builder/Immediate.hs @@ -228,6 +228,14 @@ instance MonadJSM m => MonadJSM (DomRenderHookT t m) where liftJSM' = lift . liftJSM' #endif +-- | Environment for 'HydrationDomBuilderT'. Carries references to the DOM +-- document, parent node, hydration state, and synchronization machinery. +-- +-- During hydration, the builder walks the pre-existing DOM tree (produced by +-- 'Reflex.Dom.Builder.Static.renderStatic') and attaches event handlers to +-- existing nodes rather than creating new ones. After the switchover event +-- fires, it transitions to immediate mode where new DOM nodes are created +-- and appended directly. data HydrationDomBuilderEnv t m = HydrationDomBuilderEnv { _hydrationDomBuilderEnv_document :: {-# UNPACK #-} !Document -- ^ Reference to the document @@ -244,11 +252,41 @@ data HydrationDomBuilderEnv t m = HydrationDomBuilderEnv , _hydrationDomBuilderEnv_delayed :: {-# UNPACK #-} !(IORef (HydrationRunnerT t m ())) } --- | A monad for DomBuilder which just gets the results of children and pushes --- work into an action that is delayed until after postBuild (to match the --- static builder). The action runs in 'HydrationRunnerT', which performs the --- DOM takeover and sets up the events, after which point this monad will --- continue in the vein of 'ImmediateDomBuilderT'. +-- | The main client-side 'DomBuilder' monad transformer, parameterized by a +-- 'DomSpace' @s@ which determines whether it operates in hydration mode +-- or immediate mode. +-- +-- == Two modes of operation +-- +-- * @HydrationDomBuilderT 'HydrationDomSpace' t m@ — __hydration mode__: +-- walks pre-existing DOM nodes (from server-side rendering) and attaches +-- event handlers without creating new elements. After the switchover event, +-- transitions to creating nodes normally. +-- +-- * @HydrationDomBuilderT 'GhcjsDomSpace' t m@ — __immediate mode__ +-- (aliased as 'ImmediateDomBuilderT'): creates and appends real DOM nodes +-- on every call to 'element', 'textNode', etc. +-- +-- == Monad stack +-- +-- Internally, this is a @ReaderT ('HydrationDomBuilderEnv' t m) ('DomRenderHookT' t m)@. +-- The 'DomRenderHookT' layer manages deferred DOM actions (for safe batching +-- of DOM mutations) and event trigger channels. +-- +-- == Instances provided +-- +-- 'DomBuilder', 'PostBuild', 'TriggerEvent', 'PerformEvent', 'MonadHold', +-- 'MonadSample', 'MonadJSM', 'Adjustable', 'NotReady', 'HasDocument', +-- 'Requester'. +-- +-- When @s ~ GhcjsDomSpace@, 'Element' values contain real DOM references +-- and 'domEvent' returns live events wired to actual browser event listeners. +-- +-- The server-side counterpart is 'StaticDomBuilderT' (from +-- "Reflex.Dom.Builder.Static"). For prerender-based splitting between +-- server and client code, see "Reflex.Dom.Prerender". +-- +-- @since 0.8.0.0 newtype HydrationDomBuilderT s t m a = HydrationDomBuilderT { unHydrationDomBuilderT :: ReaderT (HydrationDomBuilderEnv t m) (DomRenderHookT t m) a } deriving (Functor, Applicative, Monad, MonadFix, MonadIO, MonadException #if MIN_VERSION_base(4,9,1) @@ -268,8 +306,22 @@ instance (Reflex t, MonadFix m) => DomRenderHook t (HydrationDomBuilderT s t m) requestDomAction = HydrationDomBuilderT . lift . requestDomAction requestDomAction_ = HydrationDomBuilderT . lift . requestDomAction_ --- | The monad which performs the delayed actions to reuse prerendered nodes and set up events. --- State contains reference to the previous node sibling, if any, and the reader contains reference to the parent node. +-- | The monad that performs the actual DOM hydration walk at switchover time. +-- +-- When the page loads with server-rendered HTML, 'HydrationDomBuilderT' +-- accumulates deferred actions. When the switchover event fires, those actions +-- run inside 'HydrationRunnerT', which walks the existing DOM tree: +-- +-- * The 'ReaderT Node' carries the current parent node +-- * The 'StateT HydrationState' tracks the most recently visited sibling +-- (so the runner can advance through child nodes sequentially) +-- * If the DOM doesn't match expectations (e.g. a text node where an element +-- was expected), the runner sets @_hydrationState_failed@ and prints a +-- warning +-- +-- After the hydration walk completes, any remaining DOM nodes after the last +-- visited sibling are removed (via 'removeSubsequentNodes'). This cleans up +-- server-rendered content that the client doesn't expect. newtype HydrationRunnerT t m a = HydrationRunnerT { unHydrationRunnerT :: StateT HydrationState (ReaderT Node (DomRenderHookT t m)) a } deriving (Functor, Applicative, Monad, MonadFix, MonadIO, MonadException #if MIN_VERSION_base(4,9,1) @@ -432,11 +484,22 @@ append n = do -> HydrationDomBuilderT s Spider HydrationM () #-} +-- | Tracks whether the builder is still walking pre-rendered DOM nodes +-- or has switched to creating them from scratch. +-- +-- During 'HydrationMode_Hydrating', 'HydrationDomBuilderT' expects to find +-- matching DOM nodes already present (produced by the static renderer). +-- If the actual DOM doesn't match what the builder expects, hydration will +-- fail with a warning. +-- +-- After the switchover event fires, the mode changes to +-- 'HydrationMode_Immediate' and all subsequent widget builds create new DOM +-- nodes normally. data HydrationMode = HydrationMode_Hydrating - -- ^ The time from initial load to parity with static builder + -- ^ Walking pre-existing server-rendered DOM nodes, attaching event handlers | HydrationMode_Immediate - -- ^ After hydration + -- ^ Creating and appending new DOM nodes (normal operation) deriving (Eq, Ord, Show) {-# INLINABLE getPreviousNode #-} @@ -517,14 +580,29 @@ extractUpTo df s e = liftJSM $ do void $ call f f (df, s, e) #endif +-- | Constraint bundle for monads that can run 'HydrationDomBuilderT'. +-- +-- Compared to 'SupportsStaticDomBuilder', this additionally requires +-- 'MonadJSM' (and @MonadJSM (Performable m)@) because the hydration and +-- immediate DOM builders need access to the JavaScript context for DOM +-- manipulation and event wiring. +-- +-- This is satisfied by the concrete monad stack used by @mainWidget@ and +-- friends: 'PerformEventT' over 'DomHost' with a JSM context. 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)) +-- | Collect all DOM nodes between @start@ (inclusive) and @end@ (exclusive) +-- into a 'DocumentFragment', removing them from the live DOM. Used internally +-- by the 'Adjustable' instance to extract widget content regions for +-- replacement or reordering. {-# INLINABLE collectUpTo #-} collectUpTo :: (MonadJSM m, IsNode start, IsNode end) => start -> end -> m DOM.DocumentFragment collectUpTo s e = do currentParent <- getParentNodeUnchecked e -- May be different than it was at initial construction, e.g., because the parent may have dumped us in from a DocumentFragment collectUpToGivenParent currentParent s e +-- | Like 'collectUpTo' but takes an explicit parent node, avoiding a +-- 'getParentNodeUnchecked' call. Useful when the parent is already known. {-# INLINABLE collectUpToGivenParent #-} collectUpToGivenParent :: (MonadJSM m, IsNode parent, IsNode start, IsNode end) => parent -> start -> end -> m DOM.DocumentFragment collectUpToGivenParent currentParent s e = do @@ -635,6 +713,26 @@ newtype GhcjsDomHandler1 a b = GhcjsDomHandler1 { unGhcjsDomHandler1 :: forall ( newtype GhcjsDomEvent en = GhcjsDomEvent { unGhcjsDomEvent :: EventType en } +-- | The 'DomSpace' for live client-side rendering via GHCJS (or JSaddle). +-- +-- All associated types resolve to real DOM objects: +-- +-- * @RawDocument GhcjsDomSpace = Document@ +-- * @RawElement GhcjsDomSpace = Element@ +-- * @RawTextNode GhcjsDomSpace = Text@ +-- * @RawInputElement GhcjsDomSpace = HTMLInputElement@ +-- * @RawTextAreaElement GhcjsDomSpace = HTMLTextAreaElement@ +-- * @RawSelectElement GhcjsDomSpace = HTMLSelectElement@ +-- +-- This is what 'ImmediateDomBuilderT' uses. 'Element' values from this space +-- carry real DOM node references, so you can pass @_element_raw@ to JavaScript +-- FFI, read element dimensions, attach custom event listeners, etc. +-- +-- Compare with 'StaticDomSpace' (all @()@, from "Reflex.Dom.Builder.Static") +-- and 'HydrationDomSpace' (also @()@ for raw nodes, but uses real event +-- processing). +-- +-- @since 0.8.0.0 data GhcjsDomSpace instance DomSpace GhcjsDomSpace where @@ -666,6 +764,22 @@ data Pair1 (f :: k -> *) (g :: k -> *) (a :: k) = Pair1 (f a) (g a) data Maybe1 f a = Nothing1 | Just1 (f a) +-- | Specification for how DOM events are processed in 'GhcjsDomSpace' and +-- 'HydrationDomSpace'. +-- +-- The @er@ parameter is typically 'EventResult', which maps each +-- 'EventTag' to its result type (e.g. @ClickTag@ → @()@, +-- @InputTag@ → @Text@, @KeypressTag@ → @Word@). +-- +-- * @_ghcjsEventSpec_filters@ — per-event-name filters that can inspect the +-- raw DOM event and return 'EventFlags' (preventDefault, stopPropagation) +-- plus an optional result. These are installed by 'addEventSpecFlags' in +-- the 'DomSpace' instance. +-- +-- * @_ghcjsEventSpec_handler@ — the default handler used for events without +-- a specific filter. Delegates to 'defaultDomEventHandler' which extracts +-- the appropriate value from the DOM event (e.g. mouse coordinates for +-- click, key code for keypress). data GhcjsEventSpec er = GhcjsEventSpec { _ghcjsEventSpec_filters :: DMap EventName (GhcjsEventFilter er) , _ghcjsEventSpec_handler :: GhcjsEventHandler er @@ -1416,6 +1530,30 @@ instance SupportsHydrationDomBuilder t m => NotReady t (HydrationDomBuilderT s t unreadyChildren <- askUnreadyChildren liftIO $ modifyIORef' unreadyChildren succ +-- | The 'DomSpace' used during hydration — the process of taking over +-- server-rendered HTML and attaching event handlers to the existing DOM. +-- +-- Raw element types are @()@ (just like 'StaticDomSpace') because during +-- hydration we don't hold references to individual elements in the monad's +-- return values. However, the event processing infrastructure is real +-- ('GhcjsEventSpec'), so event handlers are correctly wired up during the +-- hydration walk. +-- +-- After hydration completes, 'HydrationDomBuilderT' switches to +-- 'HydrationMode_Immediate' internally, but the @s@ type parameter remains +-- 'HydrationDomSpace' — only the runtime behavior changes. +-- +-- Compare with 'StaticDomSpace' (from "Reflex.Dom.Builder.Static", pure +-- server-side rendering with no event processing) and 'GhcjsDomSpace' +-- (full live DOM with real element references). +-- +-- @ +-- RawDocument HydrationDomSpace = Document -- real document reference +-- RawElement HydrationDomSpace = () -- no element references +-- RawTextNode HydrationDomSpace = () -- no text node references +-- @ +-- +-- @since 0.8.0.0 data HydrationDomSpace instance DomSpace HydrationDomSpace where @@ -1527,6 +1665,29 @@ instance (Reflex t, Monad m, Adjustable t m, MonadHold t m, MonadFix m) => Adjus traverseDMapWithKeyWithAdjust f m = DomRenderHookT . traverseDMapWithKeyWithAdjust (\k -> unDomRenderHookT . f k) m traverseDMapWithKeyWithAdjustWithMove f m = DomRenderHookT . traverseDMapWithKeyWithAdjustWithMove (\k -> unDomRenderHookT . f k) m +-- | 'Adjustable' instance for client-side DOM building. Powers 'dyn', 'dyn_', +-- 'widgetHold', 'listWithKey', and all dynamic widget swapping. +-- +-- == How it works in immediate\/hydration mode +-- +-- 'runWithReplace' creates a DOM region bounded by two comment sentinel nodes. +-- The initial widget @a0@ is rendered between them. When the replacement event +-- @a'@ fires, the region between the sentinels is cleared (all child nodes +-- removed) and the new widget is rendered in its place. +-- +-- This is fundamentally different from the static 'Adjustable' instance: +-- here, DOM nodes are actually created and destroyed. The implementation +-- tracks \"cohorts\" (generations of content) to handle rapid replacements +-- and ensure the DOM stays consistent. +-- +-- During hydration, 'runWithReplace' must match the sentinel comments that +-- the static renderer emitted. After switchover, it operates like the +-- immediate builder. +-- +-- 'traverseDMapWithKeyWithAdjust' (used by @listWithKey@, @simpleList@, etc.) +-- maintains a live collection of child widgets, each bounded by their own +-- sentinel nodes. Patches (insertions, deletions, moves) are applied to the +-- DOM incrementally — only changed children are touched. 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 {-# INLINABLE runWithReplace #-} runWithReplace a0 a' = do @@ -1792,6 +1953,13 @@ traverseIntMapWithKeyWithAdjust' = do -> HydrationDomBuilderT HydrationDomSpace DomTimeline HydrationM (IntMap v', Event DomTimeline (PatchIntMap v')) #-} +-- | Tracks whether a child widget has finished its initial build. Used by +-- 'drawChildUpdate' and the 'Adjustable' instance to coordinate when all +-- children are ready (so the parent can fire its commit action). +-- +-- * 'ChildReadyState_Ready' — the child is fully rendered +-- * @ChildReadyState_Unready (Just key)@ — still rendering, identified by @key@ +-- * @ChildReadyState_Unready Nothing@ — still rendering, no key data ChildReadyState a = ChildReadyState_Ready | ChildReadyState_Unready !(Maybe a) @@ -2077,6 +2245,15 @@ data TraverseChild t m k a = TraverseChild , _traverseChild_result :: !a } deriving Functor +-- | Render a single child widget within the context of +-- 'traverseDMapWithKeyWithAdjust' (i.e. list rendering). Creates the child's +-- DOM region with sentinel nodes, tracks its 'ChildReadyState', and returns +-- the result wrapped in @TraverseChild@ for incremental patch application. +-- +-- The @markReady@ callback is invoked when the child finishes rendering, but +-- only if the child was NOT immediately ready. If the child is ready at +-- initialization time, the returned 'ChildReadyState' will be +-- 'ChildReadyState_Ready' and the callback is never called. {-# INLINABLE drawChildUpdate #-} drawChildUpdate :: (MonadJSM m, Reflex t) => HydrationDomBuilderEnv t m @@ -2166,11 +2343,30 @@ mkHasFocus e = do , True <$ Reflex.select (_element_events e) (WrapArg Focus) ] +-- | Insert a node immediately before an existing sibling node in the DOM. +-- Used internally for inserting content at specific positions during +-- 'Adjustable' patch application. insertBefore :: (MonadJSM m, IsNode new, IsNode existing) => new -> existing -> m () insertBefore new existing = do p <- getParentNodeUnchecked existing 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 +-- | Convenience alias for direct (non-hydrating) client-side DOM building. +-- +-- @ImmediateDomBuilderT t m ≡ HydrationDomBuilderT GhcjsDomSpace t m@ +-- +-- When you see @Widget x@ from "Reflex.Dom.Main", the outermost transformer +-- is 'ImmediateDomBuilderT'. This is the concrete monad that actually creates +-- and appends DOM elements in the browser. +-- +-- In this mode, 'Element' values carry real @DOM.Element@ references, and +-- 'domEvent' wires up actual browser event listeners via the 'GhcjsEventSpec' +-- infrastructure. +-- +-- The server-side counterpart is 'StaticDomBuilderT' (from +-- "Reflex.Dom.Builder.Static"). +-- +-- @since 0.8.0.0 type ImmediateDomBuilderT = HydrationDomBuilderT GhcjsDomSpace instance PerformEvent t m => PerformEvent t (HydrationDomBuilderT s t m) where @@ -2219,6 +2415,17 @@ instance MonadAtomicRef m => MonadAtomicRef (HydrationDomBuilderT s t m) where {-# INLINABLE atomicModifyRef #-} atomicModifyRef r = lift . atomicModifyRef r +-- | Maps each 'EventTag' to its corresponding GHCJS DOM event type. +-- +-- This type family is used by 'GhcjsDomEvent' and the event handling +-- infrastructure to determine the correct DOM event type for each event name. +-- For example, @EventType 'ClickTag = MouseEvent@ ensures that click event +-- handlers receive a 'MouseEvent', while @EventType 'KeypressTag = KeyboardEvent@ +-- provides keyboard-specific information. +-- +-- The mapping covers all standard DOM events: mouse events, keyboard events, +-- focus events, touch events, wheel events, clipboard events, drag events, +-- and generic UI events. type family EventType en where EventType 'AbortTag = UIEvent EventType 'BlurTag = FocusEvent @@ -2267,6 +2474,19 @@ type family EventType en where EventType 'TouchendTag = TouchEvent EventType 'TouchcancelTag = TouchEvent +-- | Default handler for each DOM event type, extracting the appropriate value. +-- +-- This is what runs when no custom 'GhcjsEventFilter' is installed for an +-- event. The return value depends on the event tag: +-- +-- * @Click@, @Dblclick@ → @()@ +-- * @Keypress@, @Keydown@, @Keyup@ → key code ('Word') +-- * @Scroll@ → scroll position +-- * @Mousemove@, @Mousedown@, etc. → @(Int, Int)@ coordinates +-- * @Input@, @Change@ → current element value ('Text') +-- * @Touchstart@, @Touchmove@, etc. → list of touch points +-- * @Wheel@ → delta values +-- * @Paste@ → clipboard 'Text' {-# INLINABLE defaultDomEventHandler #-} defaultDomEventHandler :: IsElement e => e -> EventName en -> EventM e (EventType en) (Maybe (EventResult en)) defaultDomEventHandler e evt = fmap (Just . EventResult) $ case evt of @@ -2575,6 +2795,15 @@ windowOnEventName en e = case en of Touchend -> on e Events.touchEnd Touchcancel -> on e Events.touchCancel +-- | Subscribe to a DOM event on an element and produce a Reflex 'Event'. +-- +-- This is the bridge between the DOM event system and Reflex FRP. It registers +-- a DOM event listener (via the provided subscription function) and routes +-- the results into a Reflex 'Event' via 'TriggerEvent'. +-- +-- Typically you don't call this directly — 'domEvent' on 'Element' values +-- handles it for you. Use this when you need to subscribe to events on +-- raw DOM nodes obtained via @_element_raw@ or JavaScript FFI. {-# INLINABLE wrapDomEvent #-} wrapDomEvent :: (TriggerEvent t m, MonadJSM m) => e -> (e -> EventM e event () -> JSM (JSM ())) -> EventM e event a -> m (Event t a) wrapDomEvent el elementOnevent getValue = wrapDomEventMaybe el elementOnevent $ fmap Just getValue @@ -2719,16 +2948,30 @@ instance MonadHold t m => MonadHold t (HydrationDomBuilderT s t m) where {-# INLINABLE headE #-} headE = lift . headE +-- | Configuration for subscribing to window-level DOM events. +-- Currently has no configuration options (placeholder for future extension). data WindowConfig t = WindowConfig -- No config options yet instance Default (WindowConfig t) where def = WindowConfig +-- | A wrapped browser window with Reflex event subscriptions. +-- Use @_window_events@ with 'select' to subscribe to window-level events +-- (e.g. resize, scroll, focus, blur): +-- +-- @ +-- win <- wrapWindow jsWindow def +-- let resizeEvt = select (_window_events win) (WrapArg Resize) +-- @ data Window t = Window { _window_events :: EventSelector t (WrapArg EventResult EventName) + -- ^ Event selector for all window-level DOM events , _window_raw :: DOM.Window + -- ^ The underlying GHCJS Window object } +-- | Wrap a raw GHCJS 'DOM.Window' into a Reflex 'Window' with event +-- subscriptions. Requires 'GhcjsDomSpace' — not available in static rendering. wrapWindow :: (MonadJSM m, MonadReflexCreateTrigger t m) => DOM.Window -> WindowConfig t -> HydrationDomBuilderT GhcjsDomSpace t m (Window t) wrapWindow wv _ = do events <- wrapDomEventsMaybe wv (defaultDomWindowEventHandler wv) windowOnEventName diff --git a/reflex-dom-core/src/Reflex/Dom/Builder/Static.hs b/reflex-dom-core/src/Reflex/Dom/Builder/Static.hs index 140c25ae..8cdcf3dd 100644 --- a/reflex-dom-core/src/Reflex/Dom/Builder/Static.hs +++ b/reflex-dom-core/src/Reflex/Dom/Builder/Static.hs @@ -13,6 +13,37 @@ {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE UndecidableInstances #-} +-- | +-- Module: Reflex.Dom.Builder.Static +-- +-- Server-side rendering of reflex-dom widgets to HTML 'ByteString'. This module +-- provides 'StaticDomBuilderT', the 'DomBuilder' implementation that serializes +-- elements as HTML bytes rather than creating live DOM nodes. +-- +-- == Key Characteristics +-- +-- * All 'RawElement', 'RawTextNode', etc. types are @()@ — there is no DOM. +-- * All events are 'never' — no interactivity in static rendering. +-- * @domEvent Click el@ returns 'never'. @toggle False clickEvt@ returns @constDyn False@. +-- * 'TriggerEvent' instance returns @(never, \\_ -> pure ())@ — no external events. +-- * 'MonadJSM' is NOT available — no JavaScript context. +-- * @\@ tags are serialized as HTML; the browser will execute them when +-- it parses the static HTML output. +-- +-- == Usage +-- +-- @ +-- (result, htmlBytes) <- renderStatic $ do +-- el \"div\" $ text \"Hello, world!\" +-- @ +-- +-- == FRP Code in Static Context +-- +-- FRP code (Dynamic, Event, etc.) compiles and runs in static context, but is +-- inert: all Dynamics hold their initial value forever, all Events are 'never', +-- and 'MonadHold' operations produce constant values. This is by design — +-- the same polymorphic widget code can run in both static and GHCJS contexts +-- without branching. module Reflex.Dom.Builder.Static where import Data.IORef (IORef) @@ -67,6 +98,31 @@ data StaticDomBuilderEnv t = StaticDomBuilderEnv , _staticDomBuilderEnv_nextRunWithReplaceKey :: IORef Int } +-- | The static rendering monad transformer. Builds up HTML as a 'ByteString' +-- builder rather than creating live DOM nodes. +-- +-- Internally, this is a @ReaderT env (StateT [Behavior t Builder] m)@. The +-- state accumulates HTML fragments in reverse order; 'runStaticDomBuilderT' +-- reverses and concatenates them into the final output. +-- +-- This is the 'DomBuilder' instance used by 'renderStatic' and by Obelisk's +-- server-side rendering. All 'RawElement', 'RawTextNode', etc. types are @()@, +-- meaning you cannot interact with the DOM at all — but the same polymorphic +-- widget code that works in GHCJS will compile and run here, producing HTML +-- output. +-- +-- The client-side counterparts are 'ImmediateDomBuilderT' (direct DOM +-- construction) and 'HydrationDomBuilderT' (SSR hydration reattach). +-- +-- Notable behaviors: +-- +-- * 'TriggerEvent' returns @(never, \\_ -> pure ())@ — no external event sources +-- * 'MonadHold' works (values are held) but 'Event's never fire, so held values +-- never change +-- * @\@ tags are serialized into the output; the browser will execute +-- them when it parses the HTML +-- +-- @since 0.8.0.0 newtype StaticDomBuilderT t m a = StaticDomBuilderT { unStaticDomBuilderT :: ReaderT (StaticDomBuilderEnv t) (StateT [Behavior t Builder] m) a -- Accumulated Html will be in reversed order } @@ -134,8 +190,30 @@ instance MonadRef m => MonadRef (StaticDomBuilderT t m) where instance MonadAtomicRef m => MonadAtomicRef (StaticDomBuilderT t m) where atomicModifyRef r = lift . atomicModifyRef r +-- | Constraint bundle for monads that can run 'StaticDomBuilderT'. +-- +-- This is satisfied by the 'DomHost' monad (via 'runDomHost' / 'renderStatic'), +-- which provides the Spider timeline, IO, and event infrastructure needed for +-- sampling 'Behavior's during rendering. type SupportsStaticDomBuilder t m = (Reflex t, MonadIO m, MonadHold t m, MonadFix m, PerformEvent t m, MonadReflexCreateTrigger t m, MonadRef m, Ref m ~ Ref IO, Adjustable t m) +-- | The 'DomSpace' for static (server-side) rendering. +-- +-- All associated types are @()@: +-- +-- * @RawDocument StaticDomSpace = ()@ +-- * @RawElement StaticDomSpace = ()@ +-- * @RawTextNode StaticDomSpace = ()@ +-- * @RawInputElement StaticDomSpace = ()@ +-- * etc. +-- +-- This means 'Element' values from static rendering carry no DOM references. +-- 'domEvent' on a static element always returns 'never'. +-- +-- Compare with 'GhcjsDomSpace' (real GHCJS DOM objects) and +-- 'HydrationDomSpace' (hybrid SSR reattach). +-- +-- @since 0.8.0.0 data StaticDomSpace -- | Static documents never produce any events, so this type has no inhabitants @@ -162,6 +240,27 @@ instance DomSpace StaticDomSpace where instance (SupportsStaticDomBuilder t m, Monad m) => HasDocument (StaticDomBuilderT t m) where askDocument = pure () +-- | 'Adjustable' instance for static rendering. This is what powers 'dyn', +-- 'dyn_', and 'widgetHold' in static context. +-- +-- == How it works in static rendering +-- +-- 'runWithReplace' renders the initial widget @a0@ to HTML immediately. When +-- the replacement event @a'@ fires, the new widget is also rendered — but since +-- this is static rendering, the \"replacement\" is implemented by holding the +-- latest output 'Behavior' and sampling it at render time. +-- +-- In practice, only the initial widget's HTML appears in the output of +-- 'renderStatic', because no events ever fire during static rendering. +-- The replacement event @a'@ is effectively dead code in this context. +-- +-- HTML comment markers (@\