Skip to content

Commit 4d3fcc4

Browse files
committed
Give field views access to raw multi-input values
A field view receives the submitted input as Either Text a, which on failure only carries the first raw value for the field's name. A field that renders several inputs under one name -- a composite amount-plus-currency field, for example -- therefore cannot restore everything the user typed when the form fails to parse or validate, whether re-rendered directly or replayed by runFormPRG. mhelper now records the raw parameter values each field was run against in a per-request cache, and the new lookupRawFieldInput function exposes them to views by field name. The store is populated for direct submissions and Post/Redirect/Get replays alike, and is empty for generateFormPost runs and identifyForm-wrapped forms that were not submitted, so views fall back to their defaults exactly as before. The addition is backwards compatible: no existing signature changes, and the cache write is observable only through the new function.
1 parent 85e9b92 commit 4d3fcc4

4 files changed

Lines changed: 138 additions & 1 deletion

File tree

yesod-form/ChangeLog.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# ChangeLog for yesod-form
22

3+
## 1.7.11
4+
5+
* Add `lookupRawFieldInput`, giving field views access to all raw
6+
parameter values the named field was run against — whether from a
7+
direct submission or a `runFormPRG` replay. The `Either Text a`
8+
argument a view receives only carries the first submitted value, so
9+
fields rendering several inputs under one name (e.g. a composite
10+
amount-plus-currency field) previously could not restore the user's
11+
input after a failed submission.
12+
313
## 1.7.10.1
414

515
* Move the `runFormPRG` integration tests to yesod-test's test suite,

yesod-form/Yesod/Form/Functions.hs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ module Yesod.Form.Functions
6060
, convertField
6161
, addClass
6262
, removeClass
63+
, lookupRawFieldInput
6364
) where
6465

6566
import Yesod.Form.Types
@@ -70,6 +71,7 @@ import Control.Monad.Trans.Class
7071
import Control.Monad.Trans.RWS (ask, get, put, runRWST, tell, evalRWST, local, mapRWST)
7172
import Control.Monad.Trans.Writer (runWriterT, writer)
7273
import Control.Monad (liftM, join, when)
74+
import Data.IORef (IORef, newIORef, modifyIORef', readIORef)
7375
import qualified Data.Aeson as A
7476
import Data.Byteable (constEqBytes)
7577
import qualified Data.ByteString.Lazy as BL
@@ -227,6 +229,34 @@ mopt :: (site ~ HandlerSite m, MonadHandler m)
227229
-> MForm m (FormResult (Maybe a), FieldView site)
228230
mopt field fs mdef = mhelper field fs (join mdef) (const $ const $ FormSuccess Nothing) (FormSuccess . Just) False
229231

232+
-- | Per-request store of the raw parameter values each field was run
233+
-- against, keyed by field name; see 'lookupRawFieldInput'.
234+
newtype RawFieldInput = RawFieldInput (IORef (Map.Map Text [Text]))
235+
236+
rawFieldInputRef :: MonadHandler m => m (IORef (Map.Map Text [Text]))
237+
rawFieldInputRef = do
238+
RawFieldInput ref <- cachedBy "yesod-form-raw-field-input" $
239+
fmap RawFieldInput $ liftIO $ newIORef Map.empty
240+
return ref
241+
242+
-- | The raw parameter values the named field was run against in this
243+
-- request, whether from a direct submission or a Post\/Redirect\/Get
244+
-- replay ('runFormPRG'). Empty if no form containing the field has run
245+
-- with a submission in this request (e.g. 'generateFormPost', or an
246+
-- 'identifyForm'-wrapped form that was not the one submitted).
247+
--
248+
-- This lets the view of a field rendering several inputs under one name
249+
-- restore all of the user's raw input after a failed submission: the
250+
-- @Either Text a@ argument a view receives can only carry the first
251+
-- submitted value. Only fields run through 'mreq'\/'mopt' (and the
252+
-- applicative\/widget helpers built on them) record their values here.
253+
--
254+
-- @since 1.7.11
255+
lookupRawFieldInput :: MonadHandler m => Text -> m [Text]
256+
lookupRawFieldInput name = do
257+
ref <- rawFieldInputRef
258+
Map.findWithDefault [] name `liftM` liftIO (readIORef ref)
259+
230260
mhelper :: (site ~ HandlerSite m, MonadHandler m)
231261
=> Field m a
232262
-> FieldSettings site
@@ -250,6 +280,9 @@ mhelper Field {..} FieldSettings {..} mdef onMissing onFound isReq = do
250280
mfs <- askFiles
251281
let mvals = fromMaybe [] $ Map.lookup name p
252282
files = fromMaybe [] $ mfs >>= Map.lookup name
283+
lift $ do
284+
ref <- rawFieldInputRef
285+
liftIO $ modifyIORef' ref $ Map.insert name mvals
253286
emx <- lift $ fieldParse mvals files
254287
return $ case emx of
255288
Left (SomeMessage e) -> (FormFailure [renderMessage site langs e], maybe (Left "") Left (listToMaybe mvals))

yesod-form/yesod-form.cabal

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
cabal-version: >= 1.10
22
name: yesod-form
3-
version: 1.7.10.1
3+
version: 1.7.11
44
license: MIT
55
license-file: LICENSE
66
author: Michael Snoyman <michael@snoyman.com>

yesod-test/test/PRGSpec.hs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ mkYesod "App" [parseRoutes|
3232
/two TwoR GET POST
3333
/corrupt CorruptR GET
3434
/stale StaleR GET
35+
/pair PairR GET POST
36+
/pair-rerender PairRerenderR GET POST
3537
|]
3638

3739
instance Yesod App where
@@ -78,6 +80,73 @@ numForm name csrf = do
7880
|]
7981
return (res, widget)
8082

83+
-- | A composite field rendering two inputs under one name. Its view
84+
-- restores both raw values via 'lookupRawFieldInput'; the @Either@
85+
-- argument alone could only restore the first.
86+
pairField :: Field Handler (Text, Text)
87+
pairField = Field
88+
{ fieldParse = \vals _files -> return $ case vals of
89+
[a, b]
90+
| T.null a && T.null b -> Right Nothing
91+
| any ("!" `T.isPrefixOf`) [a, b] ->
92+
Left $ SomeMessage ("no exclamations" :: Text)
93+
| otherwise -> Right $ Just (a, b)
94+
_ -> Right Nothing
95+
, fieldView = \theId name _attrs eval isReq -> do
96+
raws <- lookupRawFieldInput name
97+
let (v1, v2) = case eval of
98+
Right (a, b) -> (a, b)
99+
Left _ -> case raws of
100+
[a, b] -> (a, b)
101+
_ -> ("", "")
102+
[whamlet|
103+
<input id=#{theId} name=#{name} value=#{v1} :isReq:required>
104+
<input name=#{name} value=#{v2} :isReq:required>
105+
|]
106+
, fieldEnctype = UrlEncoded
107+
}
108+
109+
pairForm :: Html -> MForm Handler (FormResult (Text, Text), Widget)
110+
pairForm csrf = do
111+
(res, view) <- mreq pairField (fs "pair") Nothing
112+
let widget = [whamlet|
113+
#{csrf}
114+
^{fvInput view}
115+
$maybe err <- fvErrors view
116+
<p .error>#{err}
117+
|]
118+
return (res, widget)
119+
120+
getPairR :: Handler Html
121+
getPairR = do
122+
((_res, widget), enctype) <- runFormPRG pairForm
123+
defaultLayout [whamlet|
124+
<form method=post action=@{PairR} enctype=#{enctype}>
125+
^{widget}
126+
<button>go
127+
|]
128+
129+
postPairR :: Handler Html
130+
postPairR = do
131+
((res, _widget), _enctype) <- runFormPRG pairForm
132+
case res of
133+
FormSuccess _ -> setMessage "paired" >> redirect PairR
134+
_ -> redirect PairR
135+
136+
-- | The non-redirecting counterpart: on failure the POST response
137+
-- itself re-renders the form, values restored from the live request.
138+
getPairRerenderR :: Handler Html
139+
getPairRerenderR = do
140+
((_res, widget), enctype) <- runFormPost pairForm
141+
defaultLayout [whamlet|
142+
<form method=post action=@{PairRerenderR} enctype=#{enctype}>
143+
^{widget}
144+
<button>go
145+
|]
146+
147+
postPairRerenderR :: Handler Html
148+
postPairRerenderR = getPairRerenderR
149+
81150
getFormR :: Handler Html
82151
getFormR = do
83152
mmsg <- getMessage
@@ -235,6 +304,31 @@ spec = yesodSpec App $
235304
statusIs 200
236305
bodyContains "name=\"name\""
237306

307+
yit "restores multi-input raw values after a replay (lookupRawFieldInput)" $ do
308+
get PairR
309+
token <- extractToken
310+
postParams PairR token [("pair", "hello"), ("pair", "!bad")]
311+
statusIs 303
312+
get PairR
313+
statusIs 200
314+
bodyContains "value=\"hello\""
315+
bodyContains "value=\"!bad\""
316+
bodyContains "no exclamations"
317+
318+
yit "restores multi-input raw values on a plain re-render (lookupRawFieldInput)" $ do
319+
get PairRerenderR
320+
token <- extractToken
321+
postParams PairRerenderR token [("pair", "hello"), ("pair", "!bad")]
322+
statusIs 200
323+
bodyContains "value=\"hello\""
324+
bodyContains "value=\"!bad\""
325+
326+
yit "records nothing for a fresh GET (lookupRawFieldInput)" $ do
327+
get PairR
328+
statusIs 200
329+
bodyContains "value=\"\""
330+
bodyNotContains "hello"
331+
238332
yit "keys the stash by path" $ do
239333
get FormR
240334
token <- extractToken

0 commit comments

Comments
 (0)