Advanced usage
Accessing the DOM
Raw-as-raw-gets DOM components
Sometimes, we need direct access to a DOM element. For example, when dealing with forms, we need a way to get the contents of an input
element on submit. This section goes over how to access the DOM on a per-element basis and also how to work with global DOM APIs at the top-level of your application.
The Self attribute
All Deku elements can get a reference to themself via the Self
attribute. This section covers how to wield that power for good and not for evil.
Know thy Self
An event hooked up to the special Self
attribute will invoke an effectful function with the DOM element as its argument whenever the event fires. Note that the event fires before an element's attributes and children are added, so make sure to defer your computation until the next browser tick if you want these things to be present, like in the example below.
VITE_START=KnowThySelf pnpm example
module Examples.KnowThySelf where
import Prelude
import Deku.Toplevel (runInBody)
import Data.String.Utils (words)
import Data.Tuple.Nested ((/\))
import Deku.Control (text)
import Deku.DOM as D
import Deku.DOM.Self as Self
import Deku.Do as Deku
import Deku.Hooks (useState')
import Deku.Toplevel (runInBody)
import Effect (Effect)
import Effect.Aff (Milliseconds(..), delay, launchAff_)
import Effect.Class (liftEffect)
import ExampleAssitant (ExampleSignature)
import Web.DOM.Element (toParentNode)
import Web.DOM.HTMLCollection as HTMLCollection
import Web.DOM.ParentNode (children)
import Deku.Toplevel (runInBody)
main :: Effect Unit
main = void $ runInBody Deku.do
setLength /\ length <- useState'
D.div
[ Self.self_ \e -> launchAff_ do
delay (Milliseconds 0.0)
liftEffect do
kids <- children (toParentNode e)
HTMLCollection.length kids >>= setLength
]
( (words "I have this many kids:" <#> D.div__) <>
[ D.div_ [ text (show <$> length) ] ]
)
Ihavethismanykids:
Who would've thunk?
Self
attribute thunks its effect immediately when an event occurs. So make sure to manage your events carefully and/or to make sure your effectful shenanigans with yourSelf
are idempotent.Know thy SelfT
Certain elements, like input
and button
classes, have a strongly-typed variant of Self
called SelfT
that makes it a bit easier to work with the element using PureScript's DOM APIs.
VITE_START=KnowThySelfT pnpm example
module Examples.KnowThySelfT where
import Prelude
import Deku.Toplevel (runInBody)
import Data.Tuple.Nested ((/\))
import Deku.Control (text)
import Deku.DOM as D
import Deku.DOM.Attributes as DA
import Deku.DOM.Listeners as DL
import Deku.DOM.Self as Self
import Deku.Do as Deku
import Deku.Hooks (useState')
import Deku.Toplevel (runInBody)
import Effect (Effect)
import ExampleAssitant (ExampleSignature)
import Web.HTML.HTMLInputElement (value)
import Deku.Toplevel (runInBody)
inputKls :: String
inputKls =
"""rounded-md
border-gray-300 shadow-sm
border-2 mr-2
border-solid
focus:border-indigo-500 focus:ring-indigo-500
sm:text-sm """
main :: Effect Unit
main = void $ runInBody Deku.do
setTxt /\ txt <- useState'
setInput /\ input <- useState'
D.div_
[ D.input
[ DA.klass_ inputKls
, DL.input $ input <#> \i _ -> value i >>= setTxt
, Self.selfT_ setInput
]
[]
, D.div_ [ text txt ]
]
Safety with Self
Because you’re working with the raw DOM when you use Self
and SelfT
, it's possible to run into issues where you provoke a memory leak by holding onto a reference of an element too long. In general, try to use elements injected by Self
and SelfT
in the limited context where they make sense, or alternatively, only inject a raw DOM element into every nook and cranny of your app if it is some sort of singleton that should persist throughout the app's entire lifetime.
Top-level considerations
Every game or app in the wild uses some sort of global tear down and set up, often having to do with authenticating users, confirming hardware, adding third-party widgets, and setting up global app state like routing. In these cases, you’ll need to do a bunch of stuff before invoking runInBody
. There's no special trick to how to organize this code, but we’ll present a couple examples below just to give you a sense of how these things could be done.
Global handlers
One common scenario in a web app is to have a top-level auth handler. We've already seen an example of this on the Providers page, but a more realistic example would be to sync a third-party auth API to the event-based architecture and pass the event to Deku.
In the example below, we use an API sold to use by FlakyAuth to power our application's authentication. FlakyAuth provides a simple PureScript authentication API with the following single function:
doAuth :: (Boolean -> Effect Unit) -> Effect (Effect Unit)
The callback is invoked whenever auth state changes from true to false. The company has exceptionally given us permission to copy and paste their source code into the example below for instructional purposes.
VITE_START=GlobalHandlers pnpm example
module Examples.GlobalHandlers where
import Deku.Toplevel (runInBody)
import Prelude
import Deku.Toplevel (runInBody)
import Control.Monad.ST.Class (liftST)
import Data.Int (floor)
import Deku.Control (text)
import Effect (Effect)
import Effect.Random (random)
import Effect.Ref (new, read, write)
import Effect.Timer (setTimeout)
import ExampleAssitant (ExampleSignature)
import FRP.Poll (create)
import Deku.Toplevel (runInBody)
doAuth :: (Boolean -> Effect Unit) -> Effect (Effect Unit)
doAuth f = do
onOff <- new true
let
eff tf = do
oo <- read onOff
when oo do
f tf
t <- random
void $ setTimeout (floor $ t * 3000.0) (eff (not tf))
eff false
pure $ write false onOff
main :: Effect Unit
main = do
authEvent <- liftST create
u <- void $ runInBody
( text $ authEvent.poll <#>
if _ then "Welcome back!" else "Please log in."
)
_ <- doAuth authEvent.push
pure u
Note that, for the Deku DOM to catch the initial auth event, it must be created before the authentication handler is activated, otherwise it will miss the first event. An alternative to this is to create a burning
event, which memoizes its value for all future subscriptions.
main = do
authEvent <- create
myAuth <- burning false authEvent.event
_ <- doAuth authEvent.push
runInBody
( text $ myAuth.event <#>
if _ then "Welcome back!" else "Please log in."
)
pure unit