Application Development with Reflex-DOM¶
Debugging¶
Functionality¶
In addition to the normal Debug.Trace
APIs, the following can be used for debugging.
The output of these APIs will be in the browser console when compiled with ghcjs
.
For jsaddle-warp
and webkit
based apps the output will be on the terminal.:
traceEvent :: (Reflex t, Show a) => String -> Event t a -> Event t a
traceEventWith :: Reflex t => (a -> String) -> Event t a -> Event t a
Moreover the reflex-dom-contrib package contains a bunch of utility functions. One can just copy-paste these functions, ie use them without dependency on the package.:
-- Reflex.Dom.Contrib.Utils
-- pops up a javascript alert dialog box
alertEvent :: (_) => (a -> String) -> Event t a -> m ()
-- pops up a javascript confirmation dialog box
confirmEvent :: (_) => (a -> String) -> Event t a -> m (Event t a)
-- | Prints a string when an event fires. This differs slightly from
-- traceEvent because it will print even if the event is otherwise unused.
putDebugLnE :: MonadWidget t m => Event t a -> (a -> String) -> m ()
Hang / Stack Overflow¶
In general its possible to create a loop by mistake with this kind of code in a “pure” haskell.:
let
f v = ... (f v)
But thanks to MonadFix
(RecursiveDo
) this is a very common problem, even in a “monadic” code.
Basically for doing anything useful one has to introduce a feedback in the event propagation graph. And often this can lead to either a loop or a deadlock.
To fix this
Breaking down a big
rec
block into nestedrec
blocks or a series ofrec
blocks. Moving the code in a separate functions can also help simplify therec
block.Also see: using newTriggerEvent to break down a big
rec
block.Avoid using
switchPromptlyDyn
/tagPromptlyDyn
, instead useswitch . current
/tag . current
Many times what one really need is the previous value of a
Dynamic
to create a cyclic event propagation.Use
widgetHold
againstdyn
Separating an initial value from an update event means that the function using them doesn’t have to call
sample
on aDynamic
, which can be unsafe when you don’t know whether the MonadFix knot has been tied.Using
widgetHold
ensures that the user doesn’t accidentally give an untied Dynamic.
For more details checkout the articles on MonadFix / RecursiveDo
Compilation Errors¶
These are a few common compile time errors which can occur while using the widgets
If you define a widget but don’t use it any where
-- 't' is not used anywhere let t = textInput $ def Compile error • Couldn't match type ‘DomBuilderSpace m0’ with ‘GhcjsDomSpace’ arising from a use of ‘textInput’ The type variable ‘m0’ is ambiguous • In the expression: textInput $ def In an equation for ‘t’: t = textInput $ def
Solution: Simply comment this code or use it.
In a
rec
block if use a “pure” API in a “monadic” context, then you can get weird type errors:-- This will lead to type-checker assume the monad to be Dynamic ev <- switchPromptlyDyn dynEv
The biggest problem with such errors is that the line numbers are not correct, so it can take a while to figure out the source of error
One possible solution is to explicitly specify the type of functions and expression in the
let
anddo
block inside ofrec
:-- This is required to specify the types -- {-# LANGUAGE ScopedTypeVariables #-} -- This can be useful to specify types partially, just to help figure out source of error -- {-# LANGUAGE PartialTypeSignatures #-} -- Specify an explicit forall myWidget :: forall t m k . (MonadWidget t m, Ord k) => Map k Text -> m () myWidget mapInput = do .. rec let eTabClicks :: Event t k = leftmost tabClicksList d :: Dynamic t k <- do someCodeThatIsSupposedToReturnDynamicK
Web APIs and FFI¶
For working with DOM and using Web APIs the
ghcjs-dom
package should suffice.It provides APIs like
getElementById
,getBoundingRect
to work with DOM, and many other Web APIs related to geolocation, media management, web audio, etc.To use the DOM related APIs for
reflex-dom
created elements, extract the raw element from the reflex elementimport qualified GHCJS.DOM.Types as DOM import qualified GHCJS.DOM.DOMRectReadOnly as DOM import qualified GHCJS.DOM.Element as DOM (e,_) <- el' "div" $ text "Hello" let getCoords e = DOM.liftJSM $ do rect <- DOM.getBoundingClientRect (_element_raw e) y <- DOM.getY rect h <- DOM.getHeight rect return (y,h) performEvent (getCoords e <$ ev)
But when using external .js files, one has to do arbitrary JS code execution.
For doing this
jsaddle
package is preferred as it provides a type-safe way to execute the JS code.See documentation of
Language.Javascript.JSaddle.Object
for examplesSee DOM-UI Libraries for example usage.
It is also possible to do arbitrary JS code block execution using
eval
API fromLanguage.Javascript.JSaddle.Evaluate
.eval :: (ToJSString script) => script -> JSM JSVal liftJSM $ eval "console.log('Hello World')"
JSFFI functions
This will only work with
ghcjs
:import GHCJS.Types (JSVal) foreign import javascript unsafe "try { $r = $1 / $2; } catch (e) { $r = "error"; }" divide :: Double -> Double -> JSVal
See https://github.com/ghcjs/ghcjs/blob/master/doc/foreign-function-interface.md
Capturing DOM events with FFI¶
Many of the Web APIs work on a callback mechanism, where a user supplied function will be called. Many of these APIs in JS code start with on prefix.
Example JS code for creating an AudioNode to handle audio data, Source
// Give the node a function to process audio events
scriptNode.onaudioprocess = function(audioProcessingEvent) {
// The input buffer is the song we loaded earlier
var inputBuffer = audioProcessingEvent.inputBuffer;
..
}
Similar callback can be created by using the on
API from GHCJS.DOM.EventM
-- here audioProcess is the equivalent "tag" for JS onaudioprocess
myNode :: ScriptProcessorNode
liftJSM $ on myNode audioProcess myAudioProcessHandler
myAudioProcessHandler :: EventM ScriptProcessorNode AudioProcessingEvent ()
myAudioProcessHandler = do
-- aEv :: AudioProcessingEvent
aEv <- ask
buf <- getInputBuffer aEv
..
Exception Handling¶
Integrating CSS and embed in HTML¶
reflex-dom
has the following entry points for embedding CSS and a head widget:
mainWidget :: (forall x. Widget x ()) -> IO ()
mainWidgetWithHead :: (forall x. Widget x ()) -> (forall x. Widget x ()) -> IO ()
-- Share data between head and body widgets
mainWidgetWithHead' :: (a -> Widget () b, b -> Widget () a) -> IO ()
-- import Data.FileEmbed -- from file-embed package
-- This requires TemplateHaskell
-- customCss :: ByteString
-- customCss = $(embedFile "src/custom.css")
mainWidgetWithCss :: ByteString -> (forall x. Widget x ()) -> IO ()
mainWidgetInElementById :: Text -> (forall x. Widget x ()) -> IO ()
reflex-dom-core
provides equivalent functions in Reflex.Dom.Main
for use with jsaddle-warp
Deploying¶
Nix based server¶
If your server has nix
installed then the steps to deploy are quite simple.
If you are using reflex-project-skeleton or following project-development.md
follow the instructions and create the nix-build
outputs of your backend and frontend projects.
Frontend
For
ghcjs
based projects thefrontend-result
will contain the *.js files which you can simply copy to the desired location on server.For information on the use of closure compiler to reduce the size of
all.js
see https://github.com/ghcjs/ghcjs/wiki/DeploymentBackend
For
backend-result
once you have the build products ready, copy them to server using:# or nix copy, if using nix 2.0 $ nix-copy-closure --to someuser@server.org backend-result
You will have to configure the server’s nix configuration and add someuser to trusted users:
For NixOS add this to
/etc/nixos/configuration.nix
:nix.trustedUsers = [ "someuser" ];
For non NixOS, add this to
/etc/nix/nix.conf
:trusted-users = someuser
On the server then use the same nix-path
Miscellaneous¶
Rendering image from ByteString
¶
If you have the encoded image data as ByteString
then you can render the image in browser using the img tag in combination with createObjectURL.
This API will create a URL which can be specified in the img
tag’s src
attribute:
foreign import javascript unsafe "window['URL']['createObjectURL']($1)" createObjectURL_ :: Blob.Blob -> IO JS.JSVal
createObjectURL :: ByteString -> IO Text
createObjectURL bs = do
let opt :: Maybe JS.BlobPropertyBag
opt = Nothing
-- bsToArrayBuffer :: MonadJSM m => ByteString -> m ArrayBuffer
ba <- bsToArrayBuffer bs
b <- Blob.newBlob [ba] opt
url <- createObjectURL_ b
return $ T.pack $ JS.fromJSString $ JS.pFromJSVal url
Android / iOS Apps¶
On a mobile device the speed of a ghcjs
based browser app can be extremely bad. But the good news is that with little effort the reflex-dom
apps can be compiled to run as a native mobile app. The performance of these apps can be considerably faster (of the order of 10x) as the haskell runtime runs on the actual processor.
See the README of reflex-project-skeleton or project-development.md for instructions of creating an android or iOS app from your frontend project.
Also see: https://github.com/gonimo/gonimo
Note
Cross-compiling currently doesn’t support Template Haskell, so replace all the makeLenses
, etc code with generated splices
Todo
Expand this section