Deku logo

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.

View on GithubVITE_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) ] ]
    )
I
have
this
many
kids:

Who would've thunk?

Because it is not a listener, the 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.

View on GithubVITE_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.

View on GithubVITE_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