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.
VITE_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
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 guard
s going into the same container element.
VITE_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
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
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.
VITE_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'aibien compris l'autre jour que vous avieztoujours une envie folle de me fairedanser. Je garde le souvenir de votrebaiser et je voudrais bien que ce soitune preuve que je puisse être aiméepar vous. Je suis prête à montrer monaffection toute désintéressée et sans cal-cul, et si vous voulez me voir ainsivous dévoiler, sans artifice, mon âmetoute nue, daignez me faire visite,nous causerons et en amis franchementje vous prouverai que je suis la femmesincère, capable de vous offrir l'affectionla plus profonde, comme la plus étroiteamitié, en un mot : la meilleure épousedont vous puissiez rêver. Puisque votreâme est libre, pensez que l'abandon ou jevis est bien long, bien dur et souvent bieninsupportable. Mon chagrin est tropgros. Accourrez bien vite et venez me lefaire 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
.
VITE_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:
VITE_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 a
which is constructed from two values:
- An index at which to insert the element; and
- The element to insert.
VITE_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
<|*>
. 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.
VITE_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.
VITE_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 Effect
s and not Event
s, 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.
VITE_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.