Before I waste anyone’s time, I want to start with the fact that I am an absolute beginner in the world of Purescript, so go easy on me.
I am attempting to use Purescript for as much of my (DApp) project as possible and when Purescript isn’t the right tool, I’d hopefully be using Haskell and Cardano’s Haskell subset called Plutus for the minimal on-chain code that would be required.
I was hoping to avoid having to use the FFI at least for the functionality outlined in the example below: but perhaps the example is just too dynamic and impossible to do in a functional paradigm in this case. I am trying to keep it all fairly low in unnecessary dependencies as well.
Anyway, I have been desperately fighting to try and get this (what I though was a) relatively simple example ported from Javascript/Typescript into my new favorite language: Purescript. I was cruising along methodically until I recently ran into the something I had been trying to avoid: being forced to use Unsafe.Coerce (to poll the mouse position in this case). Obviously, this is an impure operation, so I have been steadily coming to grips with the reality (?) that I will need to try and accomplish this using the FFI or some spooky but necessary Unsafe.Coerce code that can feed the re-ordered data into Halogen?
But, I wonder if someone has some advice for me to get this short example FULLY working in the latest version of Purescript with the latest version of Halogen. Here’s what the simple example looks like in its original form as Javascript:
and here’s what all of the code looks like so far when I try and replicate this small example with my beginner-level code getting as far as I can without help:
import Prelude
import Web.DOM
import Data.Array (deleteAt, insertAt, mapWithIndex, (!!))
import Data.Maybe (Maybe(..), fromMaybe)
import Data.Tuple (Tuple(..))
import Effect (Effect)
import Effect.Aff.Class (class MonadAff)
import Halogen (Component, ComponentHTML, HalogenM, defaultEval, mkComponent, mkEval, modify_) as H
import Halogen.Aff as HA
import Halogen.HTML as HH
import Halogen.HTML.Events (onDragOver, onMouseDown, onMouseMove, onMouseUp)
import Halogen.HTML.Events as HE
import Halogen.HTML.Properties as HP
import Halogen.Store.Connect (Connected)
import Halogen.Store.Select as Store
import Halogen.VDom.Driver (runUI)
import Web.DOM.Element (Element)
import Web.DOM.Element (getBoundingClientRect, DOMRect)
import Web.DOM.NonElementParentNode (getElementById)
import Web.HTML.Common (ClassName(..))
import Web.HTML.Event.DragEvent.EventTypes (dragover)
import Web.HTML.Event.DragEvent.EventTypes as MDE
import Web.UIEvent.MouseEvent as MEV
import Web.UIEvent.MouseEvent.EventTypes (mousedown, mousemove, mouseup)
import Web.UIEvent.MouseEvent.EventTypes as MVT
type Input = Unit
deriveState :: Connected Store Input -> Store
deriveState { context } = context
selectStore :: Store.Selector Store Store
selectStore = Store.selectEq \store -> store
type DragState =
{ index :: Maybe Int
, originalX :: Int
, originalY :: Int
, currentX :: Int
, currentY :: Int
, isDragging :: Boolean
}
type Store =
{ rows :: Array (Tuple String String)
, dragState :: DragState
, dragOverIndex :: Maybe Int
}
data Action
= StartDrag Int Int Int Int
| MoveDrag Int Int
| DragOver Int Int
| EndDrag
| NoOp
initialStore :: Store
initialStore =
{ rows: [ Tuple "April Douglas" "Health Educator"
, Tuple "Salma Mcbride" "Mental Health Counselor"
, Tuple "Kassandra Donovan" "Makeup Artists"
, Tuple "Yosef Hartman" "Theatrical and Performance"
, Tuple "Ronald Mayo" "Plant Etiologist"
, Tuple "Trey Woolley" "Maxillofacial Surgeon"
]
, dragState: { index: Nothing, originalX: 0, originalY: 0, currentX: 0, currentY: 0, isDragging: false }
, dragOverIndex: Nothing
}
component :: forall q m. MonadAff m => H.Component q Unit Void m
component =
H.mkComponent
{ initialState
, render
, eval: H.mkEval H.defaultEval
{ handleAction = handleAction
, handleQuery = \_ -> pure Nothing
, receive = const Nothing
, initialize = Nothing
, finalize = Nothing
}
}
initialState :: Input -> Store
initialState _ = initialStore
rowHeight :: Int
rowHeight = 50
findDropIndex :: Int -> Maybe Int
findDropIndex currentY =
let
rowIndex = currentY `div` rowHeight
in
Just rowIndex
render :: forall m. MonadAff m => Store -> H.ComponentHTML Action () m
render state =
HH.div []
[ HH.table
[ HP.class_ (HH.ClassName "draggable-table") ]
[ HH.thead_ [ HH.tr_ [ HH.th_ [ HH.text "Name" ], HH.th_ [ HH.text "Occupation" ]]]
, HH.tbody_ $ mapWithIndex (\index (Tuple name occupation) -> renderRow index name occupation state.dragState.index) state.rows
]
]
renderRow :: forall m. Int -> String -> String -> Maybe Int -> H.ComponentHTML Action () m
renderRow index name occupation dragging =
HH.tr
[ HP.classes $ ClassName <$> ["draggable-table__row"] <>
if Just index == dragging then ["is-dragging"] else []
, onMouseDown $ HE.handler MVT.mousedown (\event -> StartDrag index (MEV.clientX event) (MEV.clientY event))
, onMouseMove $ HE.handler MVT.mousemove (\event -> MoveDrag (MEV.clientX event) (MEV.clientY event))
, onMouseUp $ HE.handler MVT.mouseup (\_ -> EndDrag)
, onDragOver $ HE.handler MDE.dragover (\event -> DragOver index (MEV.clientY event))
, HP.draggable true
]
[ renderCell name, renderCell occupation ]
renderCell :: forall m. String -> H.ComponentHTML Action () m
renderCell content =
HH.td
[ HP.class_ (HH.ClassName "table-cell") ]
[ HH.text content ]
handleAction :: forall m. MonadAff m => Action -> H.HalogenM Store Action () Void m Unit
handleAction action =
case action of
StartDrag index x y ->
H.modify_ \s -> s { dragState = { index: Just index, originalX: x, originalY: y, currentX: x, currentY: y, isDragging: true }}
MoveDrag x y ->
H.modify_ \s -> s { dragState = s.dragState { currentX = x, currentY = y }}
DragOver mouseY ->
H.modify_ \s -> s { dragOverIndex = findDropIndex mouseY }
EndDrag ->
H.modify_ \s -> case s.dragState.index of
Just fromIndex -> case s.dragOverIndex of
Just toIndex -> s { rows = moveRow fromIndex toIndex s.rows
, dragState = resetDragState
, dragOverIndex = Nothing }
Nothing -> s { dragState = resetDragState, dragOverIndex = Nothing }
Nothing -> s
NoOp -> pure unit
main :: Effect Unit
main = HA.runHalogenAff do
body <- HA.awaitBody
runUI component unit body
moveRow :: Int -> Int -> Array (Tuple String String) -> Array (Tuple String String)
moveRow from to rows =
let
draggedRow = fromMaybe (Tuple "" "") $ rows !! from
rowsWithoutDragged = fromMaybe rows $ deleteAt from rows
in
fromMaybe rowsWithoutDragged $ insertAt to draggedRow rowsWithoutDragged
resetDragState :: DragState
resetDragState =
{ index: Nothing, originalX: 0, originalY: 0, currentX: 0, currentY: 0, isDragging: false }
-- conceptual to get my idea across: will not directly compile without proper imports and adjustments
getElementSizeAndPosition :: String -> Effect (Maybe DOMRect)
getElementSizeAndPosition elementId = do
document <- HA.selectElement
maybeElement <- getElementById elementId document
case maybeElement of
Just element -> do
rect <- getBoundingClientRect element
pure $ Just rect
Nothing -> pure Nothing
I get the following (totally expected) error:
[1 of 1] Compiling Main
Error found:
in module Main
at src/Main.purs:118:86 - 118:91 (line 118, column 86 - line 118, column 91)
Could not match type
Event
with type
MouseEvent
while checking that type Event
is at least as general as type MouseEvent
while checking that expression event
has type MouseEvent
in value declaration renderRow
Let me know if you have any advice or want to help me get this working. I simply wanted a nice, intuitive, idiomatic Purescript-Halogen way to allow a user to reorder a ranked list by dragging it around then submitting the new reordered list once I get this part working but this is breaking my morale and I am starting to get discouraged and burnt out on something so trivial in the ultimate functionality of my DApp. I figure this can also help our ecosystem if we were to accomplish something as dynamic as is shown in the example while still being totally Purescript.
ps. I also wonder how I can get the size of the table without setting it directly in my code? That seems like another case where I’d need to use some unsafe code as well.
Thanks in advance for any advice anyone wants to offer. Purescript is super hard to work in for a beginner like me, but I am dedicated to learning it and Haskell to create my DApp as elegantly as possible.