Purescript Halogen manually trigger input validation outside of a form - purescript

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

Related

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 Halogen Component, Input vs State

I want to make a Halogen Component where the component's input differs from its state. According to the guide for Halogen (https://github.com/slamdata/purescript-halogen/blob/master/docs/5%20-%20Parent%20and%20child%20components.md#input-values) this should be possible. I changed the example from the guide as follows
import Prelude
import Data.Int (decimal, toStringAs)
import Halogen as H
import Halogen.HTML as HH
import Halogen.HTML.Events as HHE
type Input = Int
type State = String
data Query a = HandleInput Input a
component :: forall m. H.Component HH.HTML Query Input Void m
component =
H.component
{ initialState: id
, render
, eval
, receiver: HHE.input HandleInput
}
where
render :: State -> H.ComponentHTML Query
render state =
HH.div_
[ HH.text "My input value is:"
, HH.strong_ [ HH.text (show state) ]
]
eval :: Query ~> H.ComponentDSL State Query Void m
eval = case _ of
HandleInput n next -> do
oldN <- H.get
when (oldN /= (toStringAs decimal n)) $ H.put $ toStringAs decimal n
pure next
But then I get a compile error at the line with , receiver: HHE.input HandleInput
Could not match type
String
with type
Int
What am I doing wrong here?
The initialState is computed using the input value, and in the code you pasted it's implemented as id, so it's trying to force the input and state types to match.
Changed line { initialState: id in { initialState: const initialState and added after where the following lines
initialState :: State
initialState = ""

How to handle effects with Pux?

I'm just a beginner to the whole world of Purescript and Pux, so I'm a little confused as to where we handle effects.
Currently I'm modelling the effects in my type:
type State = { countries ∷ Maybe (Eff (random :: RANDOM) Countries) }
And then using that in my foldp function:
foldp (Request) state = { state, effects: [countries] }
Where countries is defined as:
countries = do
response <- attempt $ get "/countries.json"
let countries = either (Left <<< show) decode response
pure $ Just $ Receive $ case shuffle <$> countries of
Left _ → Nothing
Right xs → Just xs
However at some point I need to unwrap the RANDOM effect from the type to be able to return it from my view: State → HTML Event.
You simply don't do any side effects in the view. Lift the random effect in your foldp:
data Countries
data Event = Request | Received (Maybe Countries) | FetchError String
type State = {countries :: Maybe Countries, error :: String}
type AppFx fx = (random :: RANDOM | fx)
foldp :: ∀ fx. FoldP State Event (AppFx fx)
foldp (FetchError msg) state = noEffects state{error = msg}
foldp (Received countries) state = noEffects state{countries = countries}
foldp Request state = {state, effects: pure countriesRequest}
where
countriesRequest = Just <$> do
response <- attempt $ getCountries
case response of
Left errorMsg -> pure $ FetchError $ show errorMsg
Right countries -> case shuffle countries of
Left _ -> pure $ Received Nothing
Right shuffleEff -> do
shuffledCountries <- liftEff shuffleEff
pure $ Received $ Just shuffledCountries
shuffle :: ∀ fx. Countries -> Either String (Eff (random :: RANDOM | fx) Countries)
shuffle = unsafeCrashWith "TODO"
getCountries :: ∀ fx. Aff fx Countries
getCountries = unsafeCrashWith "TODO"

Purescript Reuse Argonaut JSON Decoding for Affjax Respondeable

I'm trying to fetch some JSON data from a Haskell server, but I'm having trouble with the Respondeable instance, as well as just Affjax in general. I've defined EncodeJson + DecodeJson with Data.Argonaut.Generic.Aeson (GA), but I can't figure out how to fit that in with the Respondeable instance and it's fromResponse function.
It gives me the error "Could not match type Foreign with type Json" but is it possible to reuse my decodeJson instance without having to create anything else by hand? Maybe by creating an IsForeign instance, but using GA.decodeJson in that? I'm just not sure how to go about doing it. I've seen how it's done in https://github.com/purescript/purescript-foreign/blob/master/examples/Complex.purs by hand, but I have complex types that need to match up with my Haskell JSON output, and it's going to be a huge pain to do it manually.
I'm using purescript 10.7, Affjax 3.02, and argonaut 2.0.0, and argonaut-generic-codecs 5.1.0. Thanks!
testAffjax :: forall eff. Aff (ajax :: AJAX | eff) (Answer)
testAffjax = launchAff do
res <- affjax $ defaultRequest { url = "/", method = Left GET }
pure res.response
data Answer = Answer {
_answer :: String
, _isCorrect :: Boolean
, _hint :: String
}
{- PROBLEM -}
instance respondableAnswer :: Respondable Answer where
responseType = Tuple Nothing JSONResponse
fromResponse = GA.decodeJson {- Error here -}
derive instance genericAnswer :: Generic Answer
instance showAnswer :: Show Answer where
show = gShow
instance encodeAnswer :: EncodeJson Answer where
encodeJson = GA.encodeJson
instance decodeAnswer :: DecodeJson Answer where
decodeJson = GA.decodeJson
What you're looking for is a function that adapts a JSON decoder:
decodeJson :: forall a. Json -> Either String a
To return using F rather than Either. F is a synonym defined in Data.Foreign for Except MultipleErrors a. To do that we need to:
Translate our String error into a MultipleErrors
Convert from Either to Except
MultipleErrors is another synonym defined in Data.Foreign, this time for NonEmptyList ForeignError. Looking at ForeignError there's a constructor also called ForeignError that lets us provide some string message. That leaves us with the need to create a NonEmptyList, which is pretty easy:
remapError = pure <<< ForeignError
NonEmptyList is Applicative, so we can create a one-element list with pure.
To go from Either to Except is also straightforward. Again looking at the definitions in Pursuit we can see:
newtype ExceptT m e a = ExceptT (m (Either e a))
type Except = ExceptT Identity
So ExceptT is just a fancy Either already, giving us:
eitherToExcept = ExceptT <<< pure
The pure here is to lift Either e a into m (Either e a), which for Except m ~ Identity.
So now we can take this stuff, and make a general "decode JSON for Affjax responses" function:
decodeJsonResponse :: forall a. DecodeJson a => Json -> F a
decodeJsonResponse =
ExceptT <<< pure <<< lmap (pure <<< ForeignError) <<< decodeJson
The only other thing that happened in here is we used lmap to map over the left part of the Either, to do the error-message-type-conversion bit.
We can now use Kleisli composition ((<=<)) to chain this decodeJsonResponse together with the original fromResponse that will do the initial ResponseContent -> F Json:
instance respondableAnswer :: Respondable Answer where
responseType = Tuple (Just applicationJSON) JSONResponse
fromResponse = decodeJsonResponse <=< fromResponse
Here's the full example using your Answer type:
module Main where
import Prelude
import Control.Monad.Aff (Aff)
import Control.Monad.Except (ExceptT(..))
import Data.Argonaut (class DecodeJson, class EncodeJson, Json, decodeJson)
import Data.Argonaut.Generic.Argonaut as GA
import Data.Bifunctor (lmap)
import Data.Foreign (F, ForeignError(..))
import Data.Generic (class Generic, gShow)
import Data.Maybe (Maybe(..))
import Data.MediaType.Common as MediaType
import Data.Tuple (Tuple(..))
import Network.HTTP.Affjax as AX
import Network.HTTP.Affjax.Response as AXR
testAffjax :: forall eff. Aff (ajax :: AX.AJAX | eff) Answer
testAffjax = _.response <$> AX.get "/"
newtype Answer = Answer
{ _answer :: String
, _isCorrect :: Boolean
, _hint :: String
}
derive instance genericAnswer :: Generic Answer
instance showAnswer :: Show Answer where
show = gShow
instance encodeAnswer :: EncodeJson Answer where
encodeJson = GA.encodeJson
instance decodeAnswer :: DecodeJson Answer where
decodeJson = GA.decodeJson
instance respondableAnswer :: AXR.Respondable Answer where
responseType = Tuple (Just MediaType.applicationJSON) AXR.JSONResponse
fromResponse = decodeJsonResponse <=< AXR.fromResponse
decodeJsonResponse :: forall a. DecodeJson a => Json -> F a
decodeJsonResponse =
ExceptT <<< pure <<< lmap (pure <<< ForeignError) <<< decodeJson

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! (: