Deku logo

Core concepts

Collections

Working with lists and dynamic elements.

So far, all of the examples we've seen with the exception of <#~> work with the DOM as a static entity. The <#~> operator is sufficient for many use cases where you want to refresh content, however, it can be heavy for long lists. For example, if a single avatar in a single profile in a collection of 100 profiles updates, or if you need to insert a new profile card in a large collection, you don't want to re-render the whole DOM. This page will present efficient methods for managing these scenarios.


Arrays of components

Sometimes, you have a fixed array of components that you'd like to insert into a parent element. For these cases, there's the fixed operator.

Grouping components together

To group components together, you can pass an array of components to the fixed function. The components will render into the correct position in the parent component.

As an example, consider the following version of Old MacDonald. We use fixed for the lyric e i e i o so as to avoid needless code duplication.

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

import Deku.Toplevel (runInBody)
import Effect (Effect)
import Prelude
import ExampleAssitant (ExampleSignature)
import Deku.Toplevel (runInBody)

import Deku.DOM.Attributes as DA
import Deku.Control (text_)
import Deku.Core (fixed)
import Deku.DOM as D
import Deku.Toplevel (runInBody)

main :: Effect Unit
main = void $ runInBody do
  let
    eieio = fixed
      [ D.span [ DA.klass_ "text-blue-400" ] [ text_ "e " ]
      , D.span [ DA.klass_ "text-red-400" ] [ text_ "i " ]
      , D.span [ DA.klass_ "text-green-400" ] [ text_ "e " ]
      , D.span [ DA.klass_ "text-teal-400" ] [ text_ "i " ]
      , D.span [ DA.klass_ "text-orange-400" ] [ text_ "o" ]
      ]

  D.div_
    [ text_ "Old MacDonald had a farm, "
    , eieio
    , text_ ". And on that farm he had a dog, "
    , eieio
    , text_
        ". With a woof-woof here and a woof-woof there. "
    , text_ "Here a woof, there a woof, everywhere a woof-woof. "
    , text_ "Old MacDonald had a farm, "
    , eieio
    , text_ "."
    ]
Old MacDonald had a farm, e i e i o. And on that farm he had a dog, e i e i o. With a woof-woof here and a woof-woof there. Here a woof, there a woof, everywhere a woof-woof. Old MacDonald had a farm, e i e i o.

Animal sounds in different cultures

Dogs say woof in English, but every culture has its own animal sounds. Before using Deku, make sure to familiarize yourself with how dogs bark in multiple languages.

With fixed, you don't need to do any extra accounting when subbing arrays in and out of the DOM. Deku automatically manages node insertion and deletion so that fixed elements always show up in the right place.

Guarding

In an array of components, sometimes you'd like one to disappear for a bit. Enter guard which gates the presence or absence of an argument on a Boolean condition derived from the state.

Here's an example of nested guards going into the same container element.

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

import Prelude
import Deku.Toplevel (runInBody)

import Assets (alexanderBackgroundURL, alexanderURL)
import Data.String (Pattern(..), Replacement(..), replaceAll)
import Data.Tuple.Nested ((/\))
import Deku.Control (text_)
import Deku.Core (fixed)
import Deku.DOM as D
import Deku.DOM.Attributes as DA
import Deku.DOM.Listeners as DL
import Deku.Do as Deku
import Deku.Hooks (guard, useState)
import Deku.Toplevel (runInBody)
import Effect (Effect)
import ExampleAssitant (ExampleSignature)
import Deku.Toplevel (runInBody)

buttonClass :: String -> String
buttonClass color =
  replaceAll (Pattern "COLOR") (Replacement color)
    """mb-3 inline-flex items-center rounded-md
border border-transparent bg-COLOR-600 px-3 py-2
text-sm font-medium leading-4 text-white shadow-sm
hover:bg-COLOR-700 focus:outline-none focus:ring-2
focus:ring-COLOR-500 focus:ring-offset-2"""

data AlexandersDay = Bad | Worse | Worst

derive instance Eq AlexandersDay
derive instance Ord AlexandersDay

main :: Effect Unit
main = void $ runInBody Deku.do
  setBadness /\ badness <- useState Bad
  D.div_
    [ D.div [ DA.klass_ "flex justify-between" ]
        [ D.button
            [ DA.klass_ $ buttonClass "indigo"
            , DL.click_ \_ -> setBadness Bad
            ]
            [ text_ "Bad" ]
        , D.button
            [ DA.klass_ $ buttonClass "pink"
            , DL.click_ \_ -> setBadness Worse
            ]
            [ text_ "Worse" ]
        , D.button
            [ DA.klass_ $ buttonClass "green"
            , DL.click_ \_ -> setBadness Worst
            ]
            [ text_ "Worst" ]
        ]
    , D.div
        [ DA.style_ $ "background-image: url('" <> alexanderBackgroundURL <>
            "');"
        ]
        [ D.div [ DA.klass_ "p-3" ]
            [ D.span
                [ DA.klass_ "font-aldine text-4xl text-alexander" ]
                [ text_
                    "Alexander and the Terrible, Horrible,"
                , guard (badness <#> (_ > Bad)) $ fixed
                    [ text_ " Dreadful,"
                    , guard (badness <#> (_ > Worse)) $ fixed
                        [ text_ " Hideous,"
                        , text_ " Soul-crushing,"
                        ]
                    , text_ " Ruinous,"
                    ]
                , text_ " No Good,"
                , text_ " Very Bad Day"
                ]
            ]
        , D.div_
            [ D.img
                [ DA.alt_
                    "Cover of Alexander and the Terrible, Horrible, No Good, Very Bad Day"
                , DA.src_ alexanderURL
                ]
                []
            ]
        ]
    ]
Alexander and the Terrible, Horrible, No Good, Very Bad Day
Cover of Alexander and the Terrible, Horrible, No Good, Very Bad Day

Monoids

Deku components are also Monoids, which means they can be appended together. Furthermore, there is the empty component blank that, when appended to any component, yields the component back.

Terminology brush up

When we talk about Semigroup and Monoid instances for Deku components, we’re referring to components as defined in the Components section. That is, they are PureScript terms with type Nut. As Nut is a newtype, we can implement typeclass instances for it.

Components as semigroups

Components can be appended together, and the result will be a component. This is the same as enclosing all of the components in a fixed block. Let's see this in action with George Sand's famous letter to Alfred de Musset.

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

import Deku.Toplevel (runInBody)
import Effect (Effect)
import Prelude
import Deku.Toplevel (runInBody)

import Data.String (Pattern(..), Replacement(..), replaceAll)
import Data.Tuple.Nested ((/\))
import Deku.DOM.Attributes as DA
import Deku.Control (text_)
import Deku.DOM as D
import Deku.Do as Deku
import Deku.Hooks (guard, useState)
import Deku.DOM.Listeners as DL
import ExampleAssitant (ExampleSignature)
import Deku.Toplevel (runInBody)

buttonClass :: String -> String
buttonClass color =
  replaceAll (Pattern "COLOR") (Replacement color)
    """mb-3 inline-flex items-center rounded-md
border border-transparent bg-COLOR-600 px-3 py-2
text-sm font-medium leading-4 text-white shadow-sm
hover:bg-COLOR-700 focus:outline-none focus:ring-2
focus:ring-COLOR-500 focus:ring-offset-2"""

main :: Effect Unit
main = void $ runInBody Deku.do
  setAstuce /\ astuce <- useState true
  D.div_
    [ D.div [ DA.klass_ "space-x-2" ]
        [ D.button
            [ DA.klass_ $ buttonClass "indigo"
            , DL.click_ \_ -> setAstuce true
            ]
            [ text_ "Sage" ]
        , D.button
            [ DA.klass_ $ buttonClass "green"
            , DL.click_ \_ -> setAstuce false
            ]
            [ text_ "Pas sage" ]
        ]
    , D.div_
        [ D.div__ "Cher ami,"
            <> D.div__ "Je suis toute émue de vous dire que j'ai"
            <> guard astuce
              (D.div__ "bien compris l'autre jour que vous aviez")
            <> D.div__ "toujours une envie folle de me faire"
            <> guard astuce
              (D.div__ "danser. Je garde le souvenir de votre")
            <> D.div__ "baiser et je voudrais bien que ce soit"
            <> guard astuce
              (D.div__ "une preuve que je puisse être aimée")
            <> D.div__ "par vous. Je suis prête à montrer mon"
            <> guard astuce
              (D.div__ "affection toute désintéressée et sans cal-")
            <> D.div__ "cul, et si vous voulez me voir ainsi"
            <> guard astuce
              (D.div__ "vous dévoiler, sans artifice, mon âme")
            <> D.div__ "toute nue, daignez me faire visite,"
            <> guard astuce
              (D.div__ "nous causerons et en amis franchement")
            <> D.div__ "je vous prouverai que je suis la femme"
            <> guard astuce
              (D.div__ "sincère, capable de vous offrir l'affection")
            <> D.div__ "la plus profonde, comme la plus étroite"
            <> guard astuce
              (D.div__ "amitié, en un mot : la meilleure épouse")
            <> D.div__ "dont vous puissiez rêver. Puisque votre"
            <> guard astuce
              (D.div__ "âme est libre, pensez que l'abandon ou je")
            <> D.div__ "vis est bien long, bien dur et souvent bien"
            <> guard astuce
              (D.div__ "insupportable. Mon chagrin est trop")
            <> D.div__ "gros. Accourrez bien vite et venez me le"
            <> guard astuce
              (D.div__ "faire oublier. À vous je veux me sou-")
            <> D.div__ "mettre entièrement."
            <> D.div__ "Votre poupée"
        ]
    ]
Cher ami,
Je suis toute émue de vous dire que j'ai
bien compris l'autre jour que vous aviez
toujours une envie folle de me faire
danser. Je garde le souvenir de votre
baiser et je voudrais bien que ce soit
une preuve que je puisse être aimée
par vous. Je suis prête à montrer mon
affection toute désintéressée et sans cal-
cul, et si vous voulez me voir ainsi
vous dévoiler, sans artifice, mon âme
toute nue, daignez me faire visite,
nous causerons et en amis franchement
je vous prouverai que je suis la femme
sincère, capable de vous offrir l'affection
la plus profonde, comme la plus étroite
amitié, en un mot : la meilleure épouse
dont vous puissiez rêver. Puisque votre
âme est libre, pensez que l'abandon ou je
vis est bien long, bien dur et souvent bien
insupportable. Mon chagrin est trop
gros. Accourrez bien vite et venez me le
faire oublier. À vous je veux me sou-
mettre entièrement.
Votre poupée

Components as monoids

In addition to being a semigroup, components are a monoid, with the empty element being blank. Because it's a monoid, we can use all sorts of cool functions on Deku components, like intercalate.

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

import Deku.Toplevel (runInBody)
import Effect (Effect)
import Prelude
import ExampleAssitant (ExampleSignature)
import Deku.Toplevel (runInBody)

import Data.Array (drop, intercalate, zipWith)
import Data.FunctorWithIndex (mapWithIndex)
import Data.Lens (over)
import Data.Lens.Index (ix)
import Data.String (Pattern(..), split)
import Deku.DOM.Attributes as DA
import Deku.Control (text_)
import Deku.DOM as D
import Deku.Toplevel (runInBody)

lyrics :: Array String
lyrics = split (Pattern "\n")
  """Twelve drummers drumming
Eleven pipers piping
Ten lords a-leaping
Nine ladies dancing
Eight maids a-milking
Seven swans a-swimming
Six geese a-laying
Five golden rings
Four calling birds
Three french hens
Two turtle doves
A partridge in a pear tree"""

textColors :: Array String
textColors = split (Pattern "\n")
  """text-lime-400
text-red-400
text-sky-400
text-purple-400
text-emerald-400
text-stone-400
text-pink-400
text-yellow-400
text-indigo-400
text-zinc-400
text-amber-400
text-neutral-400"""

toWord :: Int -> String
toWord 1 = "first"
toWord 2 = "second"
toWord 3 = "third"
toWord 4 = "fourth"
toWord 5 = "fifth"
toWord 6 = "sixth"
toWord 7 = "seventh"
toWord 8 = "eighth"
toWord 9 = "ninth"
toWord 10 = "tenth"
toWord 11 = "eleventh"
toWord 12 = "twelfth"
toWord _ = "nth"

main :: Effect Unit
main = void $ runInBody do
  let
    styleF s t = D.span [ DA.klass_ s ] [ text_ t ]
    zipStyles = zipWith styleF textColors
    lyrics0 = zipStyles lyrics
    lyrics1 = zipStyles (over (ix 11) ("and " <> _) lyrics)
  D.div_
    [ D.ol_
        ( lyrics # mapWithIndex \i _ ->
            D.p_
              [ text_ "On the "
                  <> text_ (toWord (i + 1))
                  <> text_
                    " day of Christmas my true love gave to me: "
                  <> intercalate (text_ ", ")
                    ( drop (11 - i) $
                        if i == 0 then lyrics0 else lyrics1
                    )
                  <> text_ "."
              ]
        )
    ]

    On the first day of Christmas my true love gave to me: A partridge in a pear tree.

    On the second day of Christmas my true love gave to me: Two turtle doves, and A partridge in a pear tree.

    On the third day of Christmas my true love gave to me: Three french hens, Two turtle doves, and A partridge in a pear tree.

    On the fourth day of Christmas my true love gave to me: Four calling birds, Three french hens, Two turtle doves, and A partridge in a pear tree.

    On the fifth day of Christmas my true love gave to me: Five golden rings, Four calling birds, Three french hens, Two turtle doves, and A partridge in a pear tree.

    On the sixth day of Christmas my true love gave to me: Six geese a-laying, Five golden rings, Four calling birds, Three french hens, Two turtle doves, and A partridge in a pear tree.

    On the seventh day of Christmas my true love gave to me: Seven swans a-swimming, Six geese a-laying, Five golden rings, Four calling birds, Three french hens, Two turtle doves, and A partridge in a pear tree.

    On the eighth day of Christmas my true love gave to me: Eight maids a-milking, Seven swans a-swimming, Six geese a-laying, Five golden rings, Four calling birds, Three french hens, Two turtle doves, and A partridge in a pear tree.

    On the ninth day of Christmas my true love gave to me: Nine ladies dancing, Eight maids a-milking, Seven swans a-swimming, Six geese a-laying, Five golden rings, Four calling birds, Three french hens, Two turtle doves, and A partridge in a pear tree.

    On the tenth day of Christmas my true love gave to me: Ten lords a-leaping, Nine ladies dancing, Eight maids a-milking, Seven swans a-swimming, Six geese a-laying, Five golden rings, Four calling birds, Three french hens, Two turtle doves, and A partridge in a pear tree.

    On the eleventh day of Christmas my true love gave to me: Eleven pipers piping, Ten lords a-leaping, Nine ladies dancing, Eight maids a-milking, Seven swans a-swimming, Six geese a-laying, Five golden rings, Four calling birds, Three french hens, Two turtle doves, and A partridge in a pear tree.

    On the twelfth day of Christmas my true love gave to me: Twelve drummers drumming, Eleven pipers piping, Ten lords a-leaping, Nine ladies dancing, Eight maids a-milking, Seven swans a-swimming, Six geese a-laying, Five golden rings, Four calling birds, Three french hens, Two turtle doves, and A partridge in a pear tree.


Dynamic components

Sometimes, instead of dealing with a fixed collection of components, you have a dynamic collection. Classic examples of this include todo lists and contact lists in a chat app. Deku provides the useDyn hook to facilitate the rendering of dynamic collections. In the following section, we’ll build intuition for useDyn through a small Todo app.

The useDyn hook

To render dynamic components with the most recent component appearing at the top of a collection, look no further than the useDynAtBeginning hook. The hook takes an event and outputs a value called value that can be used to render the most recent component. Here's an example:

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

import Prelude
import Deku.Toplevel (runInBody)

import Data.Foldable (for_, traverse_)
import Data.Maybe (Maybe(..))
import Data.String (Pattern(..), Replacement(..), replaceAll)
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 (useDynAtBeginning, useRef, useState')
import Deku.Toplevel (runInBody)
import Effect (Effect)
import ExampleAssitant (ExampleSignature)
import Web.Event.Event (target)
import Web.HTML (window)
import Web.HTML.HTMLInputElement (fromEventTarget, value)
import Web.HTML.Window (alert)
import Web.UIEvent.KeyboardEvent (code, toEvent)
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"""

buttonClass :: String -> String
buttonClass color =
  replaceAll (Pattern "COLOR") (Replacement color)
    """mb-3 inline-flex items-center rounded-md
border border-transparent bg-COLOR-600 px-3 py-2
text-sm font-medium leading-4 text-white shadow-sm
hover:bg-COLOR-700 focus:outline-none focus:ring-2
focus:ring-COLOR-500 focus:ring-offset-2"""

main :: Effect Unit
main = void $ runInBody Deku.do
  setItem /\ item <- useState'
  setInput /\ input <- useState'
  iref <- useRef Nothing (Just <$> input)
  let
    guardAgainstEmpty e = do
      v <- value e
      if v == "" then
        window >>= alert "Item cannot be empty"
      else setItem v
    top =
      D.div_
        [ D.input
            [ DA.value_ "Tasko primo"
            , DL.keyup_ \evt -> do
                when (code evt == "Enter") $
                  for_
                    ( (target >=> fromEventTarget)
                        (toEvent evt)
                    )
                    guardAgainstEmpty
            , Self.selfT_ setInput
            , DA.klass_ inputKls
            ]
            []
        , D.button
            [ DL.click_ \_ -> do
                iref >>= traverse_ guardAgainstEmpty
            , DA.klass_ $ buttonClass "green"
            ]
            [ text_ "Add" ]
        ]
  D.div_
    [ top
    , Deku.do
        { value: t } <- useDynAtBeginning item
        D.div_ [ text_ t ]
    ]

As we learned in State, the right side of a state hook is of type Poll a, where a is whatever's being pushed to the pusher. Because Poll is a functor, we can map over it. So far, we've been doing simple transformations like mapping over Poll Int to turn it into Poll String. Here, we’re mapping over an Poll to transform it into a Deku component. So instead of streaming text to a text node, we’re streaming components to the DOM, but it's the same pattern!

Inserting in a different order

Sometimes, you want to insert elements in a particular order instead of the first element being inserted at the top of a list. There's a hook for that! Instead of useDynAtBeginning or its homologue useDynAtEnd, we’ll use plain old useDyn. This hook expects an event of type Tuple Int awhich is constructed from two values:

  1. An index at which to insert the element; and
  2. The element to insert.

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

import Prelude
import Deku.Toplevel (runInBody)

import Data.Foldable (for_, traverse_)
import Data.Int (floor)
import Data.Maybe (Maybe(..))
import Data.String (Pattern(..), Replacement(..), replaceAll)
import Data.Tuple (Tuple(..))
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 (useDyn, useRef, useState, useState')
import Deku.Toplevel (runInBody)
import Effect (Effect)
import ExampleAssitant (ExampleSignature)
import FRP.Event.Class ((<|*>))
import Web.Event.Event (target)
import Web.HTML (window)
import Web.HTML.HTMLInputElement (fromEventTarget, value)
import Web.HTML.Window (alert)
import Web.UIEvent.KeyboardEvent (code, toEvent)
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"""

buttonClass :: String -> String
buttonClass color =
  replaceAll (Pattern "COLOR") (Replacement color)
    """mb-3 inline-flex items-center rounded-md
border border-transparent bg-COLOR-600 px-3 py-2
text-sm font-medium leading-4 text-white shadow-sm
hover:bg-COLOR-700 focus:outline-none focus:ring-2
focus:ring-COLOR-500 focus:ring-offset-2"""

main :: Effect Unit
main = void $ runInBody Deku.do
  setPos /\ pos <- useState 0
  setItem /\ item <- useState'
  setInput /\ input <- useState'
  iref <- useRef Nothing (Just <$> input)
  let
    guardAgainstEmpty e = do
      v <- value e
      if v == "" then
        window >>= alert "Item cannot be empty"
      else setItem v
    top =
      D.div_
        [ D.input
            [ DA.value_ "Tasko primo"
            , DL.keyup_ $ \evt -> do
                when (code evt == "Enter") $
                  for_
                    ((target >=> fromEventTarget) (toEvent evt))
                    guardAgainstEmpty
            , Self.selfT_ setInput
            , DA.klass_ inputKls
            ]
            []
        , D.input
            [ DA.klass_ inputKls
            , DA.xtypeNumber
            , DA.min_ "0"
            , DA.value_ "0"
            , DL.numberOn_ DL.change (floor >>> setPos)
            ]
            []
        , D.button
            [ DL.click_ \_ -> do
                iref >>= traverse_ guardAgainstEmpty
            , DA.klass_ $ buttonClass "green"
            ]
            [ text_ "Add" ]
        ]
  D.div_
    [ top
    , Deku.do
        { value: t } <- useDyn
          (Tuple <$> (pure <$> pos) <|*> item)
        D.div_ [ text_ t ]
    ]

The <|*> operator

In the example above, we see a new operator <|*>. We need to use it here because otherwise we'd add a todo item whenever we change the number in the input. This operator is part of a larger collection of operators used for Effect systems, which we’ll go over later.

Note that, if the index overshoots or undershoots the collection's bounds, the element will go to the end or beginning of the collection respectively.

Moving elements

The useDyn hook can be destructured to get some useful methods. In this section, we’ll see how the sendTo function moves an element of a dynamic list to a different position.

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

import Prelude
import Deku.Toplevel (runInBody)

import Data.Foldable (for_, traverse_)
import Data.Int (floor)
import Data.Maybe (Maybe(..))
import Data.String (Pattern(..), Replacement(..), replaceAll)
import Data.Tuple (Tuple(..))
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 (useDyn, useRef, useState, useState')
import Deku.Toplevel (runInBody)
import Effect (Effect)
import ExampleAssitant (ExampleSignature)
import FRP.Event.Class ((<|*>))
import Web.Event.Event (target)
import Web.HTML (window)
import Web.HTML.HTMLInputElement (fromEventTarget, value)
import Web.HTML.Window (alert)
import Web.UIEvent.KeyboardEvent (code, toEvent)
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"""

buttonClass :: String -> String
buttonClass color =
  replaceAll (Pattern "COLOR") (Replacement color)
    """mb-3 inline-flex items-center rounded-md
border border-transparent bg-COLOR-600 px-3 py-2
text-sm font-medium leading-4 text-white shadow-sm
hover:bg-COLOR-700 focus:outline-none focus:ring-2
focus:ring-COLOR-500 focus:ring-offset-2"""

main :: Effect Unit
main = void $ runInBody Deku.do
  setPos /\ pos <- useState 0
  setItem /\ item <- useState'
  setInput /\ input <- useState'
  iref <- useRef Nothing (Just <$> input)
  let
    guardAgainstEmpty e = do
      v <- value e
      if v == "" then
        window >>= alert "Item cannot be empty"
      else setItem v
    top =
      D.div_
        [ D.input
            [ DA.value_ "Tasko primo"
            , DL.keyup_ $ \evt -> do
                when (code evt == "Enter") $
                  for_
                    ((target >=> fromEventTarget) (toEvent evt))
                    guardAgainstEmpty
            , Self.selfT_ setInput
            , DA.klass_ inputKls
            ]
            []
        , D.input
            [ DA.klass_ inputKls
            , DA.xtypeNumber
            , DA.min_ "0"
            , DA.value_ "0"
            , DL.numberOn_ DL.change (floor >>> setPos)
            ]
            []
        , D.button
            [ DL.click_ \_ -> do
                iref >>= traverse_ guardAgainstEmpty
            , DA.klass_ $ buttonClass "green"
            ]
            [ text_ "Add" ]
        ]
  D.div_
    [ top
    , Deku.do
        { value: t, sendTo } <- useDyn
          (Tuple <$> (pure <$> pos) <|*> item)
        D.div_
          [ text_ t
          , D.button
              [ DA.klass_ $ "ml-2 " <> buttonClass "indigo"
              , DL.click_ \_ -> (sendTo 0)
              ]
              [ text_ "Prioritize" ]
          ]
    ]

If the chosen position is larger than the length of the list, the element will be sent to the end.

Removing elements

The useDyn hook can be destructured to get a remove effect that removes the component from the collection.

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

import Prelude
import Deku.Toplevel (runInBody)

import Data.Foldable (for_, traverse_)
import Data.Int (floor)
import Data.Maybe (Maybe(..))
import Data.String (Pattern(..), Replacement(..), replaceAll)
import Data.Tuple (Tuple(..))
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 (useDyn, useRef, useState, useState')
import Deku.Toplevel (runInBody)
import Effect (Effect)
import ExampleAssitant (ExampleSignature)
import FRP.Event.Class ((<|*>))
import Web.Event.Event (target)
import Web.HTML (window)
import Web.HTML.HTMLInputElement (fromEventTarget, value)
import Web.HTML.Window (alert)
import Web.UIEvent.KeyboardEvent (code, toEvent)
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"""

buttonClass :: String -> String
buttonClass color =
  replaceAll (Pattern "COLOR") (Replacement color)
    """mb-3 inline-flex items-center rounded-md
border border-transparent bg-COLOR-600 px-3 py-2
text-sm font-medium leading-4 text-white shadow-sm
hover:bg-COLOR-700 focus:outline-none focus:ring-2
focus:ring-COLOR-500 focus:ring-offset-2"""

main :: Effect Unit
main = void $ runInBody Deku.do
  setPos /\ pos <- useState 0
  setItem /\ item <- useState'
  setInput /\ input <- useState'
  iref <- useRef Nothing (Just <$> input)
  let
    guardAgainstEmpty e = do
      v <- value e
      if v == "" then
        window >>= alert "Item cannot be empty"
      else setItem v
    top =
      D.div_
        [ D.input
            [ DA.value_ "Tasko primo"
            , DL.keyup_ $ \evt -> do
                when (code evt == "Enter") $
                  for_
                    ((target >=> fromEventTarget) (toEvent evt))
                    guardAgainstEmpty
            , Self.selfT_ setInput
            , DA.klass_ inputKls
            ]
            []
        , D.input
            [ DA.klass_ inputKls
            , DA.xtypeNumber
            , DA.min_ "0"
            , DA.value_ "0"
            , DL.numberOn_ DL.change (floor >>> setPos)
            ]
            []
        , D.button
            [ DL.click_ \_ -> do
                iref >>= traverse_ guardAgainstEmpty
            , DA.klass_ $ buttonClass "green"
            ]
            [ text_ "Add" ]
        ]
  D.div_
    [ top
    , Deku.do
        { value: t, sendTo, remove } <- useDyn
          (Tuple <$> (pure <$> pos) <|*> item)
        D.div_
          [ text_ t
          , D.button
              [ DA.klass_ $ "ml-2 " <> buttonClass "indigo"
              , DL.click_ \_ -> (sendTo 0)
              ]
              [ text_ "Prioritize" ]
          , D.button
              [ DA.klass_ $ "ml-2 " <> buttonClass "pink"
              , DL.click_ \_ -> remove
              ]
              [ text_ "Delete" ]
          ]
    ]

Inter-component communication

Sometimes, you need to communicate between components in a dynamic structure. As we've seen in other examples, this is possible by pushing to a hook at a higher level and subscribing to that hook at a lower level. However, because sendTo and remove are Effects and not Events, we can only use them in a listener. If we want to use them in conjunction with a hook, like for example using a hook to delete all items, we need to use the useEffect hook that you’ll learn about a bit later.

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

import Prelude
import Deku.Toplevel (runInBody)

import Data.Foldable (for_, traverse_)
import Data.Int (floor)
import Data.Maybe (Maybe(..))
import Data.String (Pattern(..), Replacement(..), replaceAll)
import Data.Tuple (Tuple(..))
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 (dynOptions, useDynWith, useRef, useState, useState')
import Deku.Toplevel (runInBody)
import Effect (Effect)
import ExampleAssitant (ExampleSignature)
import FRP.Event.Class ((<|*>))
import Web.Event.Event (target)
import Web.HTML (window)
import Web.HTML.HTMLInputElement (fromEventTarget, value)
import Web.HTML.Window (alert)
import Web.UIEvent.KeyboardEvent (code, toEvent)
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"""

buttonClass :: String -> String
buttonClass color =
  replaceAll (Pattern "COLOR") (Replacement color)
    """mb-3 inline-flex items-center rounded-md
border border-transparent bg-COLOR-600 px-3 py-2
text-sm font-medium leading-4 text-white shadow-sm
hover:bg-COLOR-700 focus:outline-none focus:ring-2
focus:ring-COLOR-500 focus:ring-offset-2"""

main :: Effect Unit
main = void $ runInBody Deku.do
  setPos /\ pos <- useState 0
  setItem /\ item <- useState'
  setRemoveAll /\ removeAll <- useState'
  setInput /\ input <- useState'
  iref <- useRef Nothing (Just <$> input)
  let
    guardAgainstEmpty e = do
      v <- value e
      if v == "" then
        window >>= alert "Item cannot be empty"
      else setItem v
    top =
      D.div_
        [ D.input
            [ DA.value_ "Tasko primo"
            , DL.keyup_ $ \evt -> do
                when (code evt == "Enter") $
                  for_
                    ((target >=> fromEventTarget) (toEvent evt))
                    guardAgainstEmpty
            , Self.selfT_ setInput
            , DA.klass_ inputKls
            ]
            []
        , D.input
            [ DA.klass_ inputKls
            , DA.xtypeNumber
            , DA.min_ "0"
            , DA.value_ "0"
            , DL.numberOn_ DL.change (floor >>> setPos)
            ]
            []
        , D.button
            [ DL.click_ \_ -> do
                iref >>= traverse_ guardAgainstEmpty
            , DA.klass_ $ buttonClass "green"
            ]
            [ text_ "Add" ]
        ]
  D.div_
    [ top
    , Deku.do
        { value: t, sendTo, remove } <-
          useDynWith
            (Tuple <$> (pure <$> pos) <|*> item) $ dynOptions
            { remove = const removeAll }
        D.div_
          [ text_ t
          , D.button
              [ DA.klass_ $ "ml-2 " <> buttonClass "indigo"
              , DL.click_ \_ -> (sendTo 0)
              ]
              [ text_ "Prioritize" ]
          , D.button
              [ DA.klass_ $ "ml-2 " <> buttonClass "pink"
              , DL.click_ \_ -> remove
              ]
              [ text_ "Delete" ]
          , D.button
              [ DA.klass_ $ "ml-2 " <> buttonClass "fuchsia"
              , DL.click_ \_ -> (setRemoveAll unit)
              ]
              [ text_ "Remove all" ]
          ]
    ]

Using these patterns, you can implement 99.87% of DOM business logic. We'll get to the remaining bits in the Portals and Custom elements sections.

Previous
State