Skip to content

Latest commit

 

History

History
155 lines (114 loc) · 5.04 KB

custom-events.md

File metadata and controls

155 lines (114 loc) · 5.04 KB

In Dominator/Chili we have two types for each event. One is an algebraic data type which represents the event type (aka, event name). The other type is a wrapper around the JSVal which actually contains the event.

So, for mouse events we have:

data MouseEvent
  = Click
  | ContextMenu
  | DblClick
  | MouseDown
  | MouseEnter
  | MouseLeave
  | MouseMove
  | MouseOver
  | MouseOut
  | MouseUp
    deriving (Eq, Ord, Show, Read, Enum, Bounded)
newtype MouseEventObject = MouseEventObject { unMouseEventObject :: JSVal }

We, of course, want to link these to things together. If we are listening for a Click event then our handler is going to receive a MouseEventObject.

There is a type family which associates events with their event objects.

type family EventObjectOf event :: *
type instance EventObjectOf MouseEvent     = MouseEventObject

When we call addEventListener we see that the type ensures the handler matches the event,

addEventListener :: (MonadIO m, IsEventTarget self, IsEvent event, FromJSVal (EventObjectOf event)) =>
                  self
               -> event -- ^ event to listen for
               -> (EventObjectOf event -> IO ()) -- ^ event handler
               -> Bool
               -> m ()
addEventListener self event callback useCapture = ...

The DOM provides a CustomEvent object which allows use to create our own events. Unlike MouseEvent, KeyboardEvent, etc, we do not know what event types the user might want to use. So we need to get a bit more abstract.

Our CustomEvent type now looks like this,

data CustomEvent (e :: k) = CustomEvent

to create user supplied types we have two mechanisms. We can refer adhoc CustomEvents using Symbols.

CustomEvent ("myevent" :: Symbol)

Alteratively, if we use DataKinds we can use promoted types:

data MyEvents = MyEvent1 | MyEvent2

myEvent1 :: CustomEvent 'MyEvent1
myEvent1 = CustomEvent

The CustomEventObject looks like:

newtype CustomEventObject (e :: k) a = CustomEventObject { unCustomEventObject :: JSVal }

The type variable a is the type of the value that may be stored in the detail of a CustomEvent.

to tie the CustomEvent and CustomEventObject together we create a type family instances,

type instance EventObjectOf (CustomEvent 'MyEvent1)   = CustomEventObject 'MyEvent1 Text
type instance EventObjectOf (CustomEvent 'MyEvent2)   = CustomEventObject 'MyEvent2 Int

This tells the compiler that for 'MyEvent1 it is going to need a handler for the type CustomEventObject 'MyEvent1 Text. The Text paramater specifies that the detail will be type Text.

With the above instances, you could not have a single handler that works for any MyEvent because each constructor maps to a unique CustomEventObject type. It would be possible to have them all resolve to the same type:

type instance EventObjectOf (CustomEvent 'MyEvent1)   = CustomEventObject 'MyEvent Text
type instance EventObjectOf (CustomEvent 'MyEvent2)   = CustomEventObject 'MyEvent Text

To add an event listener we just define a handler function and use addEventListener

myEvent1Handler :: CustomEventObject 'EditorAppendEvent Text -> IO ()
myEvent1Handler ev = ...

main :: IO ()
main =
 do  ...
     addEventListener rootNode (CustomEvent :: CustomEvent 'MyEvent1) myEvent1Handler False

To create a new CustomEvent we use the newCustomEvent function

newCustomEvent :: (IsEvent (CustomEvent ev), FromJSVal a, ToJSVal a) => CustomEvent ev -> Maybe a -> Bool -> Bool -> IO (CustomEventObject ev a)
newCustomEvent ev detail bubbles cancelable = ...

Once we have our CustomEvent we can use the standard dispatchEvent function:

     myEvent1 <- newCustomEvent (CustomEvent :: CustomEvent 'MyEvent1) (Just ("my event 1 detail" :: Text)) True True
     dispatchEvent eventTarget myEvent1

That is the same dispatchEvent function which is used for built-in events like MouseEvent, KeyboardEvent, etc. The only difference is that we use newCustomEvent instead of newEvent to create the object.

If we do not want to create a custom data type we can just use Symbols,

foobarHandler :: CustomEventObject "foobar" Text -> IO ()
foobarHandler e =
  do do md <- detail e
        putStrLn $ "foobarHandler -> " ++ show md

type instance EventObjectOf (CustomEvent "foobar")   = CustomEventObject "foobar" Text

main :: IO ()
main =
  do ...
     addEventListener rootNode (CustomEvent :: CustomEvent "foobar") foobarHandler False

     foobarEvent <- newCustomEvent (CustomEvent :: CustomEvent "foobar") (Just ("symbol event detail" :: Text)) True True
     dispatchEvent rootNode foobarEvent

Note that we still use EventObjectOf to ensure that type for detail is consistent between newCustomEvent, addEventListener, and foobarHandler for the custom "foobar" event.

To get the detail, we just use the detail function and pass it the CustomEventObject:

detail :: (FromJSVal a) => CustomEventObject e a -> IO (Maybe a)
detail e =

The detail can be any value with 'ToJSVal/FromJSVal` instances.