grails gsp with <input type="datetime-local" when posted to controller fails validation and wont save - grails

grails v3.3.9, and bootstrap.
I have been wrestling with getting grails, fields and bootstrap to work happily when processing Java LocalDateTime. Nearly there but not quite ...
I have created a /fields/localdateTime/_widget.gsp like this
<div class="input-group date col-sm-8" >
<input id="${property}-label" name="${property}" type='datetime-local' class="form-control" value="${value}" placeholder="<empty>"/>
<div class="input-group-append" >
<button class="btn btn-icon-fixed-width btn-outline-secondary btn-block" disabled aria-disabled="true" type="button" >
<i class="fas fa-calendar"></i>
</button>
</div>
</div>
this uses bootstrap
when the edit action is triggered on show view, it correctly renders this field in the edit form, and fields plugin, reads the _widget.gsp and renders the field correctly.
The control (in chrome) seems to work ok, and i can select a date, and set the time in the input field
however when i submit the update button, the data posted back to the update action does not pick up my changed date value, but rather sends the original value that was in the domain object before hand.
worst however is that the controller gets the object passed to it and in the debugger the domain object arrives with correct id and what looks like a valid LocalDateTime in the debugger (albeit the original and not the edited value )
def update(BootstrapTest bootstrapTest) {
if (bootstrapTest == null) {
notFound()
return
}
if (!bootstrapTest.validate()){
println "object delivered to update action from edit form doesnt validate "
respond bootstrapTest.errors, view:'edit'
return
}
however whilst it looks ok - when you try and validate it fails
and whats rendered on the browser is "Property ldtProp is type-mismatched". What is wrong with the LocalDateTime validation?
I can create grails bootstrap data and save to db so its not the domain class nor validation when saving that way - its just when the record is delivered to the controller that it breaks
I've tried an equivalent field with LocalDate - and that seems to work and validates correctly, and i can save the object posted to the controller update action.
This is just so frustrating can any one elucidate on what this fails and what to do to correct it ?
this last bit has me stumped.
in order to wrestle with what fields plugin is actually doing, i've got a private version here private plugin copy with adjusted content
i've run run this as private clone of fields plugin because i couldnt watch what was happening under the covers, so this has a number of libraries, style sheets imported back in so i can do in situ debug. various /fields/xxx/*gsp defined to render the bootstrap form /display bits.

Blimey, another nest of worms...
First, the <input type="date" or <input type="datetime-local" as interpreted by chrome renders the browser value as 'mm/dd/yyyy' for date and for datetime-local as mm/dd/yyyy HH:mm:(ss:SSS - greyed out )
When you provide a value="string" it ignores your string - so you have to pass the actual value into input as value="${value}". If you convert that into a formatted field before, then it gets ignored, and default template is shown.
Next problem - when you submit the form the value sent to your params block is actually a ISO formatted string - not a LocalDateTime etc!
So when Grails tries to load the record from the db for you and inject it into your controller, its already tried to update the record once.
The problem is that this has fired a validation which has already failed. So when you get the domain object in your 'Update (Domain xxx) {...}' method the object already has its errors set.
So I tried to convert the strings sent in params to LocalDateTime, LocalDate, and then update my domain object in the controller - but they failed when using validate() as the preexisting errors where already there.
So I had to clear the errors first, then convert the params strings to the right Java date types and then update the domain object and then validate. This then is happy.
So the controller action now has to look like this
def update(BootstrapTest bootstrapTest) {
if (bootstrapTest == null) {
notFound()
return
}
LocalDateTime ldtProp
LocalDate dtProp
if (bootstrapTest.hasErrors()) {
bootstrapTest.clearErrors()
try {
ldtProp = LocalDateTime.parse(params.ldtProp?.toString()) //ISO_LOCAL_DATE_TIME
dtProp = LocalDate.parse(params.dtProp?.toString(), DateTimeFormatter.ISO_LOCAL_DATE) //ISO_LOCAL_DATE
bootstrapTest.ldtProp = ldtProp
bootstrapTest.dtProp = dtProp
bootstrapTest.validate()
} catch (ex) {
println "exception $ex.message"
respond bootstrapTest.errors, view:'edit'
return
}
}
try {
bootstrapTestService.save(bootstrapTest)
} catch (ValidationException e) {
respond bootstrapTest.errors, view:'edit'
return
} ...
So clear the errors first - then correct/convert the params map format to domain object format thats ok for the domain class property (or define set method in the domain that accepts a String and does the conversion internally)
Once you do that the re validation succeeds this time and your controller/db updates start to work.
Without using custom date pickers etc ( and for scaffolding that's overkill ) you have to live with the present data handling for
I have not tried this on another browser but Chrome is my 'normal' dev browser and behaves as indicated above.
--PS - additional trace. In the background when the Domain object is located and loaded, grails seems to have a convertor from String to DateTime, because i built a setDtProp (dt) setter - and this is called with converted value before the object is delivered to the controller - which is why I saw the dtProp change originally described, but no change to the ldtProp (where I have added a setter also in the domain object).
So however Grails is doing the injection process it doesn't do it for LocalDateTime as string from the browser (but it does put string into params for you ), but it will call a setter for LocalDate. Either way when the domain object is loaded its errors() has been set - so you need to handle this from the controller as shown. Trying to fix with setters in Domain class doesn't work for LocalDateTime as your setter is never called!
This can only to be handled in the controller once the object is injected as shown above, as I'd started to do in the first place.

Related

Stimulus not geting values from html

I'm doing a basic controller and trying to get data from the HTML with the tag value.
The problem is that the data is always empty
<div data-controller="selectable">
<div class="flex" data-selectable-iconurl="test" data-selectable-iconurl-value="test">
something here
</div>
</div>
Notice I did multiple combinations of the value tag ( from different posts) in order to verify if one is working.
Now when I try to access the values in the controller is always empty
// selectable_controller.js
import {Controller} from "#hotwired/stimulus"
export default class extends Controller {
static values = {iconurl: String }
connect() {
console.log(this.iconurlValue)
}
}
I double-checked the documentation and could not find why the value is not passed to the controller and is always empty
When Stimulus controller is on the same element as the value, you should be able to retrieve the value:
<div data-controller="selectable" data-selectable-iconurl-value="test">
If your value needs to be set on a child element, then I suggest to just access it the old way :
console.log(this.element.querySelector("div").dataset.selectableIconurlValue)
Though your value may just be treated like a common JS dataset component. You lose every Turbo goodness such as a mutation observer to the value : https://dev.to/borama/stimulus-2-0-value-change-callbacks-what-are-they-good-for-4kho
Not sure why the required location of the value is not precised in the Stimulus Handbook. Maybe I am wrong too and there is a way to have it acknowledged by Stimulus...

.Net MVC Object Parameters

I'm creating a web page to manage my settings on a website. The settings are stored as a JSON file, and then parsed as runtime in to a 'CoreSettings' object.
My current code is as such:
public ActionResult Settings(WebDocInterop.Settings.CoreSettings id)
{
if(id != null)
{
// Set the settings here
}
}
I then have a form page which displays the fields within CoreSettings:
<form method="post">
<input name="SomeSettingName" id="SomeSettingName">
<button>Save</button>
</form>
When pressing the submit, my initial expectation was that the CoreSetting object would be populated by the settings that are named in the form, however, upon inspection it looks as though the object is being initialized, but the fields are always null.
Would anybody be able to confirm to me if this is the correct way to approach the situation, or if I'm going to have to use the 'FormCollection' parameter?
This has been resolved by using the writing a method to convert a FormCollection to required object.

Context parameter not working in Tapestry 5.3.2 for Submit

I have a loop displaying records, and I want to add submit buttons to all the rows. The reason they need to be submit is because there is a form at the bottom I would like to have persisted when the user selected one of the buttons.
I've seen the comments about using defer, etc, but nothing seems to work for me. The current submit code I'm trying is:
<input t:id="deleteItem" t:type="submit" t:context="item.number.content" value="deleteItem" class="deleteItem" />
To expand on the context:
The current context I have listed, is just a string within the number object within the item object. In fact, it's being displayed in the code above perfectly fine.
To test this differently, I've replace item.number.content with a getContext() method and had it return a hard-coded 1. I then debug this method and see it being called when the page is SUBMITTED, not when the page is rendered as I would have expected.
The context is never populated until after the button is pushed. Am I misunderstanding something??
Edit:
So my issue is with getting the context value. Take for instance my code:
<t:loop source="itemsList" value="item" formState="none">
<!-- Display info here -->
<input t:id="deleteItem" t:type="submit" t:context="myContext" value="deleteItem" class="deleteItem" />
</t:loop>
The definition for getMyContext is:
public String getMyContext() {
String context = item.getNumber().getContent();
return context;
}
The problem is, the method is not called until after the submit has been pressed at which time the variable "item" is null. I was expecting getMyContext to be called each time the submit button is rendered, and then when the submit is selected, the event would be triggered with the appropriate context. Does that make sense?
I finally figured out what my problem was:
formState="none"
I originally did this because it was having trouble translating between my Objects and a String. When this is selected, the submit buttons don't work properly. I ended up changing it to formState="iteration" and now it is working exactly as I expected it to (and I needed the defer as expected too).
Try adding the following to the page/component you add this t:submit to:
#OnEvent(component = "deleteItem")
private void handleSubmit(Integer contextValue) {
//Do whatever you need to do with the passed context value here.
//Most commonly you would store the context in a java page/component field to
//be used by the form eventhandler to do some sort of CRUD
}
The context value will not get written out while rendering the page, it will be passed as a parameter to your event handler while submitting the form. So what you are seeing is correct behavior.

MVC validator errors disappear by the time control is given to the controller

I have a simple model FilesModel for updating a string Description and the boolean value of a checkbox Archived for a few (already uploaded) files, and FilesModel has a validator FilesModelValidator that gets run when this data is posted. This validator does nothing more than check that each file has a description. I know that it runs and correctly returns an error for empty descriptions based on my debugging so far.
However, when control is given to the Action method in the Controller, ModelState is different from what I expect. There are no errors on the description fields, but there is one error for each checkbox that is checked: "The value 'on' is not valid for Archived."
Validation of this sort works just fine in other areas of the site, so I'm sure there's some minute thing I'm overlooking. Any suggestions as to why this may be happening and how to fix it?
Validator
public FilesModelValidator()
{
RuleFor(f => f.Files)
.Must(AllHaveADescription).WithMessage("Must have a description");
}
public static bool AllHaveADescription(Files files)
{
// This is run on postback, and returns false when any Description is empty
return files.All(f => f.Description != null && f.Description.Length > 0);
}
Controller
[HttpPost]
public virtual ActionResult Update(FilesModel model)
{
// At this point, ModelState contains an error for each checked checkbox
// and no errors for empty descriptions
if (ModelState.IsValid)
{
// Save
}
return View(model);
}
It turns out the checkbox thing was the entire problem. I found a solution to this problem elsewhere in our code, so I used it. It seems kind of hacky, but it works.
The idea is that you need to make sure that the checkbox's value is true and not "on". So do this:
<input type="checkbox" id="myCheckbox" value="true" />
Then add a hidden input with the same id with its value as false immediately after the checkbox:
<input type="hidden" id="myCheckbox" value="false" />
When a checkbox is not checked, the checkbox value does not get posted back to the server. So when the postback occurs, the server sees myCheckbox=false which is exactly what we would want in this case. When the checkbox is checked, both input values get posted to the server. But the server uses only the first value (which is the value of the checkbox itself, since we put it before the hidden field). So the server sees myCheckbox=true.
I used the following in the view rather than manually creating the checkbox:
Html.CheckBox("FieldName")
I had the same issue for items in a drop down that were linked to an Enum. I had stripped out the default value and so when someone didn't select something I got an error.
So, having said that, I think your problem might be kinda linked in that the binder is looking for a true/false when your model is expeting a yes/no or vice versa.
Two ways around this might be to change your view to be True/False or, and this is what I did, to write my own ModelBinder which does the conversion from Yes to True.
I hope this is of some help to you.

Form Submits same checkbox values on subsequent submit action in MVC

I have followed the suggestion in this question...
[How to handle checkboxes in ASP.NET MVC forms?
...to setup multiple checkboxes with the same name="..." attribute and the form behaves as expected the FIRST time its submitted. Subsequent submissions of the form use the original array of Guid values instead of properly sending the new array of checked item values.
Relevant code in the view...
<% foreach (ItemType itemType in ViewData.Model.ItemTypes) %>
<%{ %>
<li>
<input id="selectedItems" name="selectedItems" type="checkbox" value="<%= itemType.Id%>" />
<%= itemType.Description %></li>
<%} %>
This produces a series of checkboxes, one each for each item with the value="..." attribute set to the Id of the item.
Then in my controller action, the method signature is...
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult SelectItems(Guid[] selectedItems)
{...}
The first time thru the method, the selectedItems array properly holds the Guid of each item selected. But subsequent submits of the form will always still contain whatever was first selected in the initial submit action, no matter what changes you make to what it checked before you submit the form. This doesn't seem to have anything to do with my code, as inspecting the selectedItems array that the MVC framework passes to the method evidences that the framework seems to always be submitting the same value over and over again.
Close browser, start again, selecet different initial checkbox on submit and the process starts all over again (the initially-selected checkbox ids are always what's in the selectedItems argument).
Assume I must be thick and overlooking some kind of caching of form values by the framework, but I would swear this didn't behave this way in Preview 5.
Driving me nuts and probably simple issue; any ideas????
FWIW, here is what I do (not sure if it related):
// please MS, stop screwing around!!!!!!!!!!!!!!!
string r = Request.Form["r"];
Then proceed to extract the values manually from 'r'. I still use Preview 4, as they have really broken too many existing features, and not fixed reported bugs.
I'm not sure what is causing your issue, but I have a WAG...
Do you RedirectToAction in your controller's Post method?
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult SelectItems(Guid[] selectedItems)
{
/* lol snip */
return RedirectToAction("WhateverActionIsTheGetVersionOfThisPostAction");
}
It might serve to reset anything going on in the background... Again, wild-ass guess...

Resources