Deku logo

Advanced usage

Custom elements

How to bend Deku to do your bidding.

Sometimes, Deku's representation of DOM elements doesn't fit with how you need them to be used. In some cases, the DOM's components are not enough. When you need a bit more out of your DOM, Deku provides a backdoor to define whatever component you want while adding a modicum of type safety to the affair.


Defining custom elements

In this section, we’ll see how to take an element already in the DOM and give it a new alias through Deku.

The elementify function

In order to create a custom element, use the elementify function. It's actually pretty safe all things considered, as the worst it can do is crash your app. Unless your app is controlling traffic lights or something, in which case it is definitely not safe.

As Deku has the whole DOM covered, there's not a single tag we can think of that isn't Deku-ified. So this method is only useful to create custom aliases for tags that have different polls or to add new tags if Deku is out of line with the DOM spec.

View on GithubVITE_START=UnsafeCustomElement pnpm example
module Examples.UnsafeCustomElement where

import Prelude
import Deku.Toplevel (runInBody)

import Data.Maybe (Maybe(..))
import Deku.Control (elementify, text_)
import Deku.Core (Nut)
import Deku.DOM (Attribute, HTMLAnchorElement)
import Deku.Toplevel (runInBody)
import Effect (Effect)
import ExampleAssitant (ExampleSignature)
import FRP.Poll (Poll)
import Type.Proxy (Proxy)
import Deku.Toplevel (runInBody)

type HTMLMyNiftyAnchor (r :: Row Type) =
  ( __tag :: Proxy "HTMLMyNiftyAnchor"
  | HTMLAnchorElement r
  )

myNiftyAnchor
  :: Array (Poll (Attribute (HTMLMyNiftyAnchor ()))) -> Array Nut -> Nut
myNiftyAnchor = elementify Nothing "a"

main :: Effect Unit
main = void $ runInBody do
  myNiftyAnchor [] [ text_ "hi" ]
hi

Defining attributes

Things get more interesting when custom attributes are involved. By defining what attributes are settable for your custom element and how they should be marshaled into strings or listeners, you can create domain-specific logic for your DOM.

View on GithubVITE_START=AddingCustomElements pnpm example
module Examples.AddingCustomElements where

import Prelude
import Deku.Toplevel (runInBody)

import Data.Maybe (Maybe(..))
import Deku.Attribute (Attribute)
import Deku.Control (elementify, text_)
import Deku.Core (Nut, attributeAtYourOwnRisk)
import Deku.DOM (HTMLAnchorElement)
import Deku.Toplevel (runInBody)
import Effect (Effect)
import ExampleAssitant (ExampleSignature)
import FRP.Poll (Poll)
import Type.Proxy (Proxy)
import Deku.Toplevel (runInBody)

type HTMLMyNiftyAnchor (r :: Row Type) =
  ( __tag :: Proxy "HTMLMyNiftyAnchor"
  , href :: MyPages
  , target :: MyTarget
  | HTMLAnchorElement r
  )

myNiftyAnchor
  :: Array (Poll (Attribute (HTMLMyNiftyAnchor ()))) -> Array Nut -> Nut
myNiftyAnchor = elementify Nothing "a"

data MyPages = PureScript | MikeSolomonOrg
data MyTarget = Blank

myNiftyHref
  :: forall r
   . Poll MyPages
  -> Poll (Attribute (href :: MyPages | r))
myNiftyHref = map
  ( attributeAtYourOwnRisk "class" <<< case _ of
      PureScript -> "https://purescript.org"
      MikeSolomonOrg -> "https://mikesolomon.org"
  )

myNiftyTarget
  :: forall r
   . Poll MyTarget
  -> Poll (Attribute (target :: MyTarget | r))
myNiftyTarget = map
  ( attributeAtYourOwnRisk "target" <<< const "_blank"
  )

main :: Effect Unit
main = void $ runInBody do
  myNiftyAnchor
    [ myNiftyHref $ pure PureScript
    , myNiftyTarget $ pure Blank
    ]
    [ text_ "hi" ]
hi

An example using fake Discord messages

You can also use this method to work with components created by libraries like Stencil.js. In this section, we’ll bring stencil components from wc-discord-message into Deku.

Importing our custom elements

Before using custom elements, make sure to import them using whatever instructions are given in the README.md of the library you’re using. In the case of wc-discord-message, that'd be:

$ pnpm i wc-discord-message

And then:

import { applyPolyfills, defineCustomElements } from 'wc-discord-message/loader'

applyPolyfills().then(() => defineCustomElements(window))

You can also import the package via unpkg, which is what this documentation does. It's important to note that Deku can't help you with this. To initialize your components, you’ll still need to write some JavaScript or hire a consultant to.

Using our custom elements

We can define our Discord elements the same way we defined our custom anchor element above.

View on GithubVITE_START=MockDiscord pnpm example
module Examples.MockDiscord where

import Prelude
import Deku.Toplevel (runInBody)

import Assets (beluMomURL, belugaURL)
import Data.Maybe (Maybe(..))
import Deku.Attribute (Attribute)
import Deku.Control (elementify, text_)
import Deku.Core (Nut, attributeAtYourOwnRisk)
import Deku.DOM (HTMLElement)
import Deku.Toplevel (runInBody)
import Effect (Effect)
import ExampleAssitant (ExampleSignature)
import FRP.Poll (Poll)
import Type.Proxy (Proxy)
import Deku.Toplevel (runInBody)

type HTMLDiscordMessage (r :: Row Type) =
  ( __tag :: Proxy "HTMLDiscordMessage"
  , author :: String
  , avatar :: String
  | HTMLElement r
  )

discordMessage
  :: Array (Poll (Attribute (HTMLDiscordMessage ()))) -> Array Nut -> Nut
discordMessage = elementify Nothing "discord-message"

author
  :: forall r
   . Poll String
  -> Poll (Attribute (author :: String | r))
author = map (attributeAtYourOwnRisk "author")

avatar
  :: forall r
   . Poll String
  -> Poll (Attribute (author :: String | r))
avatar = map (attributeAtYourOwnRisk "avatar")

type HTMLDiscordMessages (r :: Row Type) =
  ( __tag :: Proxy "HTMLDiscordMessages"
  | HTMLElement r
  )

discordMessages
  :: Array (Poll (Attribute (HTMLDiscordMessages ()))) -> Array Nut -> Nut
discordMessages = elementify Nothing "discord-messages"

main :: Effect Unit
main = void $ runInBody do
  discordMessages []
    [ discordMessage
        [ author $ pure "beluga"
        , avatar $ pure belugaURL
        ]
        [ text_ "mom" ]
    , discordMessage
        [ author $ pure "belu-mom🌸"
        , avatar $ pure beluMomURL
        ]
        [ text_ "yes beluga" ]
    , discordMessage
        [ author $ pure "beluga"
        , avatar $ pure belugaURL
        ]
        [ text_ "whos my dad?" ]
    , discordMessage
        [ author $ pure "belu-mom🌸"
        , avatar $ pure beluMomURL
        ]
        [ text_ "it's complicated..." ]
    ]
momyes belugawhos my dad?it's complicated...