Open bitemyapp opened 8 years ago
There's a documentation task implicit in this, if this is not fixed. The docs currently say:
http://hackage.haskell.org/package/yesod-form-1.4.7.1/docs/Yesod-Form-Types.html#t:FieldViewFunc
Either (invalid text) or (legitimate result)
What it doesn't say is that "invalid text" doesn't distinguish from a form that has attempted validation against an actual POST vs. one that is merely being rendered fresh, making it useless to the author of the Field
definition save as a means of injecting it into the value=""
component of the input tag.
Either
is probably inappropriate for this. The real state of each input is something like:
data FieldValue a =
Fresh
| Invalid Text
| Valid a
There's also the matter of errors being all string based, but I'm leaving that dragon be for now.
I was able to workaround this by hoisting the CSS rule to a span conditionally injected by the form renderer, rather than attempting to condition error-styling on the presence of an error in the field input itself.
renderAbide :: Monad m => FormRender m a
renderAbide aform fragment = do
(res, views') <- aFormToForm aform
let views = views' []
hasError view = isJust (fvErrors view)
let widget = [whamlet|
$newline never
\#{fragment}
$forall view <- views
<div.row>
$if (hasError view)
<span.input-error>
^{fvInput view}
$else
^{fvInput view}
$maybe err <- fvErrors view
<small.form-error.is-visible>#{err}
|]
return (res, widget)
This still leaves a lot of fairly ordinary things I'd like to do out of reach and feels silly as Hell, but I needed for this "just work" so I could move on to other things.
Sorry, I'm not following the report. I know that we have the ability to distinguish between missing and invalid in general, since most fields work that way. Can you explain what's different in this case from, say, textField?
I think I've run into a similar issue. Bootstrap v4 requires you to add the .is-invalid
class directly on the <input>
element (docs).
My first attempt was to create my own textField
function and use that instead of the one from yesod-form
:
textField ::
(Monad m, RenderMessage (HandlerSite m) FormMessage) =>
Field m Text
textField = Field
{ fieldParse = parseHelper $ Right,
fieldView = \theId name attrs val isReq ->
[whamlet|
$newline never
<input id="#{theId}" name="#{name}" *{addInputClasses attrs val} type="text" :isReq:required value="#{either id id val}">
|],
fieldEnctype = UrlEncoded
}
addInputClasses :: [(Text, Text)] -> Either Text a -> [(Text, Text)]
addInputClasses attrs val = addClass "form-control" $
case val of
Left _ -> addClass "is-invalid" attrs
Right _ -> attrs
(Notice I'm matching on the Either Text a
to add the .is-invalid
class when we have a Left Text
).
Although that gives us the intended behavior when a form is submitted (here the "Edit project" page):
The problem is on a "fresh form" with no values filled out (here the "Create project" page), the input also shows up as invalid (although the form hasn't been submitted yet):
My workaround for now is the same as described earlier in this thread, I added some CSS rules to be able to apply the .is-invalid
class on the parent element:
/* With `yesod-form` can't add a class directly to the `<input>` element
so need to add it to the parent `.form-group`, copy the Bootstrap
styles and change a bit the way the CSS selectors work. */
.is-invalid .form-control {
border-color: #dc3545;
padding-right: calc(1.5em + 0.75rem);
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right calc(0.375em + 0.1875rem) center;
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
}
.is-invalid .invalid-feedback {
display: block;
}
renderDefaultForm :: Monad m => FormRender m a
renderDefaultForm aform fragment = do
(res, views') <- aFormToForm aform
let views = views' []
hasErrors v = isJust $ fvErrors v
widget =
[whamlet|
\#{fragment}
$forall view <- views
<div class="form-group" :hasErrors view:.is-invalid>
<label for="#{fvId view}">
^{fvLabel view}
^{fvInput view}
$maybe err <- fvErrors view
<p class="invalid-feedback">#{err}
|]
return (res, widget)
Even for optional inputs.
This means I cannot use
val
from the arguments of thefieldView
function (Either Text a
) to vary input rendering based on error status.If I add a janky
Left "" -> Nothing
case for my error guard, it doesn't mark the input invalid when the error is that the value was required.Example widget reproducing the above:
As it stands, if I don't do the
Left "" -> Nothing
hack, a fresh form renders with.input-error
for all inputs, even ones marked optional viaaopt
.Here's what it looks like without the
Left "" -> Nothing
hack, on a fresh GET of my form:As it stands, only the form renderer knows if there are actual errors, AFAICT, but the class was meant to go to the input. Now I'll need to figure out some kind of passthrough div to hoist the CSS target.