I have a very simple Halogen program: a single input, related to my State (a simple String).
An action is attached to this input: its content is rewritten to a constant value each time I type something in it.
The first time I type something in my input, the content actually is rewritten as expected. The second character I type doesn’t rerender the input. I keep what I typed in it.
Here is a minimal working example.
module Main where
import Prelude
import Effect (Effect)
import Halogen.Aff as HA
import Halogen.VDom.Driver (runUI)
import Effect.Class (class MonadEffect)
import Effect.Class.Console (log)
import Halogen as H
import Halogen.HTML as HH
import Halogen.HTML.Properties as HP
import Halogen.HTML.Events as HE
main :: Effect Unit
main = HA.runHalogenAff do
body <- HA.awaitBody
runUI component unit body
type State = String
-- let's say we want to work on both original value
-- and the user input
data Action = Update String String
component :: forall query input output m. MonadEffect m => H.Component query input output m
component =
H.mkComponent
{ initialState
, render
, eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
}
initialState :: forall input. input -> State
initialState _ = "0"
render :: forall m. State -> H.ComponentHTML Action () m
render state = HH.div_ [ HH.input [ HP.value state, HE.onValueInput (Update state) ] ]
handleAction :: forall output m. MonadEffect m => Action -> H.HalogenM State Action () output m Unit
handleAction = case _ of
Update original current -> do
log ("Update: from '" <> original <> "' to '" <> current <> "'?")
let new_value = extract_num current
log (" => REAL new value " <> new_value)
H.put $ new_value
-- TODO: currently only returning a constant value
extract_num :: forall a. a -> String
extract_num _ = "truc"
Question: how do I get my input to be rerendered each time I type something in it?
I guess that this cannot be done by default since we cannot track down which value in the State (that can be arbitrary complex) actually is rendered in my input. But it has to be a simple way to get the job done.
The issue is with extract_num. It currently always returns truc. So, on your first render, the initial state is “0”. Once you type something and trigger the onValueInput handler, that function will always return truc regardless of what you input.
Since you’re typing in numbers, you might want to implement that function using fromString to parse the String into an Int. If you get a Nothing, perhaps you should continue using the old state until a valid integer is provided.
Thanks for your response, but you completely missed my point!
I know that my function actually is just a placeholder returning a constant value. The problem is elsewhere: when I type something in my input, whatever I type should be erased since I rewrite my state to the constant value “truc”.
But what is actually happening? The first time I type something, this works, my input is rewritten to be “truc”. The second time I type a character in the input, the character stays in it while it should have been erased. I log the action, so I know that my action Update was triggered, but it didn’t imply to render my input.
I’m not an halogen expert. But if the rendering works anything like react, then it’s gonna make a shallow compare of the old state and new state. And if nothing has changed its not gonna rerender the component.
Since you always return “truc” as the new state it would only render the first time the user inputs text.
And you are correct. If I do change the content, it is rendered again.
The problem is that I don’t want my content to change: if someone puts a letter instead of a digit, I just want to remove it. But this means that my State isn’t changed, so it won’t be rendered, so my input will keep the user input.
PS: I guess that I could change a property of my input each time the user input is wrong instead of removing the wrong character. This would (probably) be preferable in practice (I’m interested in your opinions!). But this was a test to have a better understanding of Halogen.
To be honest, I’m not exactly sure what’s going on here!
There is a special case for setting values in the VDom patching though, as when you set value on an <input>, browsers reset the user’s text cursor to the end of the input, which is very annoying if you’re trying to edit a value in a field rather than just typing normally in it. The special case is supposed to check if the current value already matches the value in the VDom, and only change it when they differ. So it’ll definitely be something to do with this, but given the logic it kinda seems like it should still be overwriting the value.
Maybe it’s the order of events - the input’s value actually changes in response to typing after the event that causes re-rendering been raised, so if you keep typing it will only let you get 1 character out of sync with "truc", or something like that?
Partially because of weirdness like this, and also because it’s not great UX, I avoid hijacking user inputs and instead tend to validate separately from handling the input. This means invalid values can be captured in the state, and then reported as invalid, giving you the opportunity to explain to the user why their attempted input is wrong rather than just silently ignoring/rewriting what they’re doing. I came to this after encountering fields in the past that rewrite what you’re doing, sometimes making it very difficult to edit a value after it’s been entered, to the point where it was easier to write the value somewhere else and paste it in!
I haven’t dug enough to halogen to know how to do this there, but I’m pretty sure you can hook into a texfieldfield’s onKeyDown event and ignore inputs you don’t want. Then you don’t need to worry about state or rendering into the field.
There’s less risk of a browser having a funny reaction as javascript sets a new value in the field (because it doesn’t need to).
That being said, I agree with @garyb. I would avoid changing the user’s input or otherwise preventing them from typing something while they’re doing it. I think it’s likely to be confusing and lead to a bad user experience.
@garyb and I agree, what I try to do will lead to a bad UX. I just wanted to see how to re-render my state despite having no changes to it. Maybe that will never be useful and this is pointless, but I still don’t know how to do it.
Maybe I should forget this question because what I’m trying to do isn’t “right”, but if you know how to explicitly tell Halogen to render the state, I’m all ears.
And yes, I could have a useless value in my state that I can change each time, so the state is rendered again, that could work. Thanks!
I’ll say that my question has been answered. Thanks everyone.
Ah! Well, every time you modify or put the state it will re-render immediately… in all situations aside from this specific one of patching a value property.
It doesn’t batch or attempt to do anything clever, to ensure that things are predictable. If you do something on the line after a modify that depends on the DOM being updated, it will have been done (again, aside from this edge case ).
Actually, there’s a very minor caveat to this, and it might actually be relevant to the situation here, interacting with the value special case: if the value that is set with a put or modify is referentially equal to the prior value, then the render will also be skipped.
This is a black magic internal implementation detail because of the way MonadState is implemented - get, put, and modify are all implemented with the state function, so this unsafeRefEq check is a workaround to prevent renders happening every time get is used.
Since strings are referentially equivalent in JavaScript if they have the same value, it’s probably skipping a render here due to "truc" == "truc".
Regarding the issue of keeping selections around for inputs, I made a small library that’s supposed to help with this very issue: purescript-textcursor - Pursuit. It hasn’t been updated in a while, but I’m happy to do that if someone wants to use it! Basically it just makes the cursor state part of the data that Halogen can deal with, instead of keeping it hidden in DOM and only using flat strings to set/get the state. It would make the user experience smoother with these kinds of forced inputs, though I haven’t used it in a serious project yet.