(PureScript) How can I run a DOM event listener callback within a monadic context other than Eff? - dom

I'm making a canvas game using PureScript and I'm wondering what the best way to handle event listeners is, particularly running the callbacks within a custom monad stack. This is my game stack...
type BaseEffect e = Eff (canvas :: CANVAS, dom :: DOM, console :: CONSOLE | e)
type GameState = { canvas :: CanvasElement, context :: Context2D, angle :: Number }
type GameEffect e a = StateT GameState (BaseEffect e) a
What I'd like to do is change the "angle" property in the GameState when any key is pressed (just for development purposes so I can tweak the graphics). This is my callback function...
changeState :: forall e. Event -> GameEffect e Unit
changeState a = do
modify \s -> s { angle = s.angle + 1.0 }
liftEff $ log "keypress"
pure unit
However
addEventListener
and eventListener
look like they're meant to be used only with Eff, so the following won't type check...
addEventListener
(EventType "keypress")
(eventListener changeState)
false
((elementToEventTarget <<< htmlElementToElement) bodyHtmlElement)
I thought I could define addEventListener and eventListener myself and
import them using the foreign function interfaces (changing Eff to GameEffect). That typed checked, but caused a console error when I tried running in the browser.
foreign import addEventListener :: forall e. EventType -> EventListener e -> Boolean -> EventTarget -> GameEffect e Unit
foreign import eventListener :: forall e. (Event -> GameEffect e Unit) -> EventListener e
What's the best way to handle running callbacks within a monad stack?

I would use purescript-aff-coroutines for this. It means changing BaseEffect to Aff, but anything Eff can do Aff can do too:
import Prelude
import Control.Coroutine as CR
import Control.Coroutine.Aff as CRA
import Control.Monad.Aff (Aff)
import Control.Monad.Aff.AVar (AVAR)
import Control.Monad.Eff.Class (liftEff)
import Control.Monad.Eff.Console (CONSOLE, log)
import Control.Monad.Rec.Class (forever)
import Control.Monad.State (StateT, lift, modify)
import Data.Either (Either(..))
import DOM (DOM)
import DOM.Event.EventTarget (addEventListener, eventListener)
import DOM.Event.Types (Event, EventTarget, EventType(..))
import DOM.HTML.Types (HTMLElement, htmlElementToElement)
import DOM.Node.Types (elementToEventTarget)
import Graphics.Canvas (CANVAS, CanvasElement, Context2D)
type BaseEffect e = Aff (canvas :: CANVAS, dom :: DOM, console :: CONSOLE, avar :: AVAR | e)
type GameState = { canvas :: CanvasElement, context :: Context2D, angle :: Number }
type GameEffect e = StateT GameState (BaseEffect e)
changeState :: forall e. Event -> GameEffect e Unit
changeState a = do
modify \s -> s { angle = s.angle + 1.0 }
liftEff $ log "keypress"
pure unit
eventProducer :: forall e. EventType -> EventTarget -> CR.Producer Event (GameEffect e) Unit
eventProducer eventType target =
CRA.produce' \emit ->
addEventListener eventType (eventListener (emit <<< Left)) false target
setupListener :: forall e. HTMLElement -> GameEffect e Unit
setupListener bodyHtmlElement = CR.runProcess $ consumer `CR.pullFrom` producer
where
producer :: CR.Producer Event (GameEffect e) Unit
producer =
eventProducer
(EventType "keypress")
((elementToEventTarget <<< htmlElementToElement) bodyHtmlElement)
consumer :: CR.Consumer Event (GameEffect e) Unit
consumer = forever $ lift <<< changeState =<< CR.await
So in here the eventProducer function creates a coroutine producer for an event listener, and then setupListener does the the equivalent of the theoretical addEventListener usage you had above.
This works by creating a producer for the listener and then connecting it to a consumer that calls changeState when it receives an Event. Coroutine processes run with a monadic context, here being your GameEffect monad, which is why everything works out.

Related

How Can i Get to transform a Maybe to a Value?

I having a problem with my code I'm trying to compile it but it throws me Could Not Match Maybe Element with Element
Why? how does Maybe Work?, how do I convert it to a Value? why is so hard to understand monads and functors,
and why isn't an easy library to get some newbie getting started wit purescript?
:P
(for those who didn't understand I was just asking the first 3)
module Main where
import Prelude --(Unit, bind, pure, ($), (<$>))
import Effect (Effect)
import Data.Maybe --(Just,Maybe,fromJust, fromMaybe)
import Data.Foldable (traverse_)
import Effect.Console (log)
import Web.HTML (window)
import Web.HTML.Window (document)
import Web.HTML.HTMLDocument (toNonElementParentNode)
import Web.Event.Event (Event, target)
import Web.HTML.HTMLInputElement (value, fromEventTarget)
import Web.DOM.NonElementParentNode (NonElementParentNode,getElementById)
import Web.DOM.Node (setTextContent)
import Web.DOM.Element (Element,toNode)
import Web.Event.EventTarget (addEventListener)
pname = "#inputName" :: String
bname = "#badgeName" :: String
main :: Effect Unit
main = do
nod1 <- returnNonElementParentNode
--querySelector :: QuerySelector -> ParentNode -> Effect (Maybe Element)
elementTarget <- getElementById pname nod1
--addEventListener :: EventType -> EventListener -> Boolean -> EventTarget
addEventListener "input" updateBadge false elementTarget
updateBadge :: Event -> Unit
updateBadge event = do
nod2 <- returnNonElementParentNode
elementTarget <- getElementById bname nod2
tget <- target event
inml <- fromEventTarget tget
input <- value inml
badge <- toNode (pure elementTarget)
if not(badge == Nothing)
then setTextContent input (toNode badge)
else Nothing
returnNonElementParentNode :: Effect NonElementParentNode
returnNonElementParentNode = do
win <- window
doc <- document win
--let nd = toNonElementParentNode doc
let
nod = toNonElementParentNode doc
pure nod
I just want to understand how to get the input value from an htmlInputElement and pass it to the textContent of Another HTMLElement
pretty much how to convert a Maybe to a Value to pass to toNode function
pattern matching ... https://github.com/purescript/documentation/blob/master/language/Pattern-Matching.md
Data.Maybe#v:maybe ... https://pursuit.purescript.org/packages/purescript-maybe/4.0.1/docs/Data.Maybe#v:maybe
Data.Maybe#v:fromMaybe ... https://pursuit.purescript.org/packages/purescript-maybe/4.0.1/docs/Data.Maybe#v:fromMaybe
Partial.Unsafe#v:unsafePartial + Data.Maybe#v:fromJust https://pursuit.purescript.org/packages/purescript-partial/2.0.1/docs/Partial.Unsafe#v:unsafePartial https://pursuit.purescript.org/packages/purescript-maybe/4.0.1/docs/Data.Maybe#v:fromJust

Purescript Halogen manually trigger input validation outside of a form

I have input fields which I have marked with a required attribute, but can't figure out a way to trigger a validation check (I am not working inside of a form, so using a default submit button action won't work for me).
A quick pursuit search shows many validity functions for core html element types, but I'm not sure how to apply these to Halogen.
Is there some way to trigger a DOM effect to check all required inputs on the page and get a result back?
Here is an example component showing what I'm trying to achieve
import Prelude
import Data.Maybe (Maybe(..))
import Halogen as H
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.HTML.Properties as HP
data Message = Void
type State =
{ textValue :: String
, verified :: Boolean
}
data Query a = ContinueClicked a | InputEntered String a
inputHtml :: State -> H.ComponentHTML Query
inputHtml state =
HH.div [ HP.class_ $ H.ClassName "input-div" ]
[ HH.label_ [ HH.text "This is a required field" ]
, HH.input [ HP.type_ HP.InputText
, HE.onValueInput $ HE.input InputEntered
, HP.value state.textValue
, HP.required true
]
, HH.button [ HE.onClick $ HE.input_ ContinueClicked ]
[ HH.text "Continue"]
]
verifiedHtml :: H.ComponentHTML Query
verifiedHtml =
HH.div_ [ HH.h3_ [ HH.text "Verified!" ] ]
render :: State -> H.ComponentHTML Query
render state = if state.verified then verifiedHtml else inputHtml state
eval :: forall m. Query ~> H.ComponentDSL State Query Message m
eval = case _ of
InputEntered v next -> do
H.modify $ (_ { textValue = v })
pure next
ContinueClicked next -> do
let inputValid = false -- somehow use the required prop to determine if valid
when inputValid $ H.modify $ (_ { verified = true })
pure next
initialState :: State
initialState =
{ textValue : ""
, verified : false
}
component :: forall m. H.Component HH.HTML Query Unit Message m
component =
H.component
{ initialState: const initialState
, render
, eval
, receiver: const Nothing
}
I don't think relying on HTML form validation is the most effective way of checking inputs within a Halogen application. But I'll assume you have your reasons and present an answer anyway.
First things first, if we want to deal with DOM elements we need a way to retrieve them. Here's a purescript version of document.getElementById
getElementById
:: forall a eff
. (Foreign -> F a)
-> String
-> Eff (dom :: DOM | eff) (Maybe a)
getElementById reader elementId =
DOM.window
>>= DOM.document
<#> DOM.htmlDocumentToNonElementParentNode
>>= DOM.getElementById (wrap elementId)
<#> (_ >>= runReader reader)
runReader :: forall a b. (Foreign -> F b) -> a -> Maybe b
runReader r =
hush <<< runExcept <<< r <<< toForeign
(Don't worry about the new imports for now, there's a complete module at the end)
This getElementById function takes a read* function (probably from DOM.HTML.Types) to determine the type of element you get back, and an element id as a string.
In order to use this, we need to add an extra property to your HH.input:
HH.input [ HP.type_ HP.InputText
, HE.onValueInput $ HE.input InputEntered
, HP.value state.textValue
, HP.required true
, HP.id_ "myInput" <-- edit
]
Aside: a sum type with a Show instance would be safer than hard-coding stringy ids everywhere. I'll leave that one to you.
Cool. Now we need to call this from the ContinueClicked branch of your eval function:
ContinueClicked next ->
do maybeInput <- H.liftEff $
getElementById DOM.readHTMLInputElement "myInput"
...
This gives us a Maybe HTMLInputElement to play with. And that HTMLInputElement should have a validity property of type ValidityState, which has the information we're after.
DOM.HTML.HTMLInputElement has a validity function that will give us access to that property. Then we'll need to do some foreign value manipulation to try and get the data out that we want. For simplicity, let's just try and pull out the valid field:
isValid :: DOM.ValidityState -> Maybe Boolean
isValid =
runReader (readProp "valid" >=> readBoolean)
And with that little helper, we can finish the ContinueClicked branch:
ContinueClicked next ->
do maybeInput <- H.liftEff $
getElementById DOM.readHTMLInputElement "myInput"
pure next <*
case maybeInput of
Just input ->
do validityState <- H.liftEff $ DOM.validity input
when (fromMaybe false $ isValid validityState) $
H.modify (_ { verified = true })
Nothing ->
H.liftEff $ log "myInput not found"
And then putting it all together we have...
module Main where
import Prelude
import Control.Monad.Aff (Aff)
import Control.Monad.Eff (Eff)
import Control.Monad.Eff.Console (CONSOLE, log)
import Control.Monad.Except (runExcept)
import Data.Either (hush)
import Data.Foreign (Foreign, F, toForeign, readBoolean)
import Data.Foreign.Index (readProp)
import Data.Maybe (Maybe(..), fromMaybe)
import Data.Newtype (wrap)
import DOM (DOM)
import DOM.HTML (window) as DOM
import DOM.HTML.HTMLInputElement (validity) as DOM
import DOM.HTML.Types
(ValidityState, htmlDocumentToNonElementParentNode, readHTMLInputElement) as DOM
import DOM.HTML.Window (document) as DOM
import DOM.Node.NonElementParentNode (getElementById) as DOM
import Halogen as H
import Halogen.Aff as HA
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.HTML.Properties as HP
import Halogen.VDom.Driver (runUI)
main :: Eff (HA.HalogenEffects (console :: CONSOLE)) Unit
main = HA.runHalogenAff do
body <- HA.awaitBody
runUI component unit body
type Message
= Void
type Input
= Unit
type State
= { textValue :: String
, verified :: Boolean
}
data Query a
= ContinueClicked a
| InputEntered String a
component
:: forall eff
. H.Component HH.HTML Query Unit Message (Aff (console :: CONSOLE, dom :: DOM | eff))
component =
H.component
{ initialState: const initialState
, render
, eval
, receiver: const Nothing
}
initialState :: State
initialState =
{ textValue : ""
, verified : false
}
render :: State -> H.ComponentHTML Query
render state =
if state.verified then verifiedHtml else inputHtml
where
verifiedHtml =
HH.div_ [ HH.h3_ [ HH.text "Verified!" ] ]
inputHtml =
HH.div
[ HP.class_ $ H.ClassName "input-div" ]
[ HH.label_ [ HH.text "This is a required field" ]
, HH.input
[ HP.type_ HP.InputText
, HE.onValueInput $ HE.input InputEntered
, HP.value state.textValue
, HP.id_ "myInput"
, HP.required true
]
, HH.button
[ HE.onClick $ HE.input_ ContinueClicked ]
[ HH.text "Continue" ]
]
eval
:: forall eff
. Query
~> H.ComponentDSL State Query Message (Aff (console :: CONSOLE, dom :: DOM | eff))
eval = case _ of
InputEntered v next ->
do H.modify (_{ textValue = v })
pure next
ContinueClicked next ->
do maybeInput <- H.liftEff $
getElementById DOM.readHTMLInputElement "myInput"
pure next <*
case maybeInput of
Just input ->
do validityState <- H.liftEff $ DOM.validity input
when (fromMaybe false $ isValid validityState) $
H.modify (_ { verified = true })
Nothing ->
H.liftEff $ log "myInput not found"
getElementById
:: forall a eff
. (Foreign -> F a)
-> String
-> Eff (dom :: DOM | eff) (Maybe a)
getElementById reader elementId =
DOM.window
>>= DOM.document
<#> DOM.htmlDocumentToNonElementParentNode
>>= DOM.getElementById (wrap elementId)
<#> (_ >>= runReader reader)
isValid :: DOM.ValidityState -> Maybe Boolean
isValid =
runReader (readProp "valid" >=> readBoolean)
runReader :: forall a b. (Foreign -> F b) -> a -> Maybe b
runReader r =
hush <<< runExcept <<< r <<< toForeign

PureScript Halogen coordinates of the element on the page

How do I get the coordinates of the element itself on the page that triggered this event when I hover over it?
In purescript there is an opportunity to take page, screen and client coordinates.
Is it possible to know the coordinates of the element itself under the mouse?
I found a way to do this for example like this.
import DOM.Event.Event as DEE
import DOM.Event.MouseEvent as ME
import DOM.HTML.HTMLElement
import Halogen.HTML.Events as HE
import Halogen as H
import Halogen.HTML as HH
...
data Query a = Toggle HTMLElement a | ...
render :: State -> H.ComponentHTML Query
render state =
HH.html
[
HE.onMouseEnter (\e -> HE.input_ (Toggle $ U.unsafeCoerce $ DEE.target $ ME.mouseEventToEvent e) e)
]
[HH.text state]
update :: forall eff. Query ~> H.ComponentDSL State Query Unit (Aff (dom :: DOM | eff))
update query = case query of
RememberElementPos element next -> do
posisition <- H.liftEff (getBoundingClientRect element) // this is the position of the element itself
H.modify (\state -> updateState state position)
pure next
updateState :: State -> DOMRect -> State
...

purescript type signature fails to compile, code works fine without; suggested signature not working

The following code produces this error:
module Broken1 where
import Control.Monad.Aff.Class (MonadAff)
import Control.Monad.Aff (Aff())
import DOM.HTML.Types
import Halogen
import DOM
import Control.Monad.Eff.Exception
import Control.Monad.Aff.AVar
import Prelude
import qualified Halogen.HTML.Indexed as H
data State = State
data Query a = Query a
ui :: forall eff g. (MonadAff (HalogenEffects eff) g) => Component State Query g
ui = component (\_ -> H.div_ []) (\(Query next) -> pure next)
main' :: forall eff a. (HTMLElement -> Aff (HalogenEffects eff) a)
-> Aff (HalogenEffects eff) Unit
main' addToDOM = do
{ node: node, driver: driver } <- runUI ui State
let driver' :: Natural Query (Aff (HalogenEffects eff))
driver' = driver
return unit
the error:
at Broken1.purs line 34, column 19 - line 36, column 5
Could not match type
a2
with type
a1
while trying to match type a2
with type a1
while checking that expression driver
has type Query a1 -> Aff ( avar :: AVAR
, err :: EXCEPTION
, dom :: DOM
| eff1
)
a1
in value declaration main'
If I omit the type signature for driver', there is no compiler error, as I was hoping. If I ask psc for a signature (by replacing the type with _), this is the suggestion:
Wildcard type definition has the inferred type
forall a. Query a -> Aff ( dom :: DOM
, err :: EXCEPTION
, avar :: AVAR
| eff4
)
a
When I cut&paste this into the code instead of my original type, the error is the same as above.
In the second case this actually makes sense, since the quantifier opens a new scope for a which should be captured in the signature of main'. But even if I remove the forall a, the type error sticks around.
garyb earlier today on #purescript said it may be a bug in the type checker. I am posting this here anyway until that is established fact.
thanks! (:

Polymorphic instruction in Free Monad in Purescript

I'm trying to get this small piece of code to compile.
module Sodium where
import Prelude
import Control.Monad.Free
import Data.Coyoneda
import Data.Tuple
data ReactiveF more
= RFNewEvent (forall a. (Tuple (Event a) (a -> Reactive Unit) -> more))
type Reactive a = FreeC ReactiveF a
data Event a = Event a
newEvent :: forall a. Reactive (Tuple (Event a) (a -> Reactive Unit))
newEvent = liftFC $ RFNewEvent id
If I instead use "Number" instead of "a" in RFNewEvent, then everything compiles fine. But the moment I go "forall a." and replace "Number" with "a" it no longer compiles.
I get the following error message
Cannot unify type
a1
with type
a0
Does anyone know how to make this work?
I'm using version 0.5.0 of purescript-free.
Edit
If I use the following
data NewEventData = NewEventData forall a. Tuple (Event a) (a -> Reactive Unit)
and substitute it into RFNewEvent, then it will compile. But I end up with an undesired type signature for newEvent.
newEvent :: Reactive NewEventData
newEvent = liftFC $ RFNewEvent id
Which lets me create an event, but lets me shoot different event values to the event stream instead of the same type of value. (missing forall a. now on newEvent)
I might of made a mistake.
The overall goal is to simulate SodiumFRP's interface using a Free Monad. Then plug in an existing JavaScript FRP library that works similar to Sodium via FFI when interpreting the Free Monad.
Is this possible?
The following code now compiles and has the desired type signature for "newEvent"
module FRP.Sodium where
import Prelude
import Control.Monad.Free
import Data.Coyoneda
import Data.Tuple
data ReactiveF more
= RFNewEvent (NewEventData -> more)
type Reactive a = FreeC ReactiveF a
data NewEventData = NewEventData forall a. Tuple (Event a) (a -> Reactive Unit)
data Event a
= ENever
| EMerge (Event a) (Event a)
| EFilterJust (Event (Maybe a))
| ECoalesce (a -> a -> a) (Event a)
| EOnce (Event a)
| ESplit (Event (Array a))
| EVar Int
data Behaviour a = BVar Int
extractNewEventData :: forall a. NewEventData -> (Tuple (Event a) (a -> Reactive Unit))
extractNewEventData (NewEventData x) = x
newEvent :: forall a. Reactive (Tuple (Event a) (a -> Reactive Unit))
newEvent = map extractNewEventData $ liftFC $ RFNewEvent id
Edit
Also trying out purescript-exists. Makes it possible to define "sample"
RFSample gets added to ReactiveF ...
.
.
.
data ReactiveF more
= RFNewEvent (NewEventData -> more)
| RFSample (SampleData more)
.
.
.
data SampleDataF more a = SampleDataF (Behaviour a) (a -> more)
type SampleData more = Exists (SampleDataF more)
sample :: forall a. Behaviour a -> Reactive a
sample beh = liftFC $ RFSample $ mkExists $ SampleDataF beh id
Thank you Phil Freeman for your comment.