I have a multistep form on vue js. I need to dynamically during input send requests, validate on the server and receive a response to display validation errors.
My data:
data() {
return {
form: {...this.candidate}, // we get an object with fields already filled *
errors: new Errors(), // object with validation errors
}
},
Now for each input I have computed property:
characteristicFuturePlans: {
get() {
return this.form.characteristic_future_plans;
},
set(value) {
this.saveField('characteristic_future_plans', value);
}
},
saveField method sends data:
saveField(field, value) {
this.form[field] = value; // keep the data in the object relevant
axios.put(`/api/candidate/${this.candidate.token}`, {[field]: value})
.then(() => { this.errors.clear(field) })
.catch((error) => {
this.errors.record(field, error.response.data.errors[field]);
});
},
Now, with each change of input, a request will be sent. But with this approach, when we quickly type text in a field, sometimes the penultimate request sent comes after the last. It turns out that if you quickly write "Johnny", sometimes a query with the text "Johnn" will come after a query with the text "Johnny" and the wrong value will be saved in the database.
Then I made sure that the data was sent 1 second after the termination of text input. Added timerId: {} to data(){} and then:
saveField(field, value) {
if(this.timerId[field]) {
clearTimeout(this.timerId[field]);
}
this.timerId[field] = setTimeout(this.send, 1000, field, value);
},
send(field, value) {
this.form[field] = value;
axios.put(`/api/candidate/${this.candidate.token}`, {[field]: value})
.then(() => { this.errors.clear(field) })
.catch((error) => {
this.errors.record(field, error.response.data.errors[field]);
});
},
But now, if after filling in the input in less than a second, press the button to go to the next step of the form, the page will simply refresh. (the button to go to the next step will send a request to the server to check if the required fields are filled)
How correctly save data to the database during text input? Can I do this without setTimeout()? If so, how can I make sure that the data of the last request, and not the penultimate, is stored in the database?
I will be glad to any tip.
Updated. Attach some template code.
Part of Step[i].vue component:
<div class="row">
<div class="form-group col-md-6">
<element-form title="Title text" :error="errors.get('characteristic_future_plans')" required isBold>
<input-input v-model="characteristicFuturePlans" :is-error="errors.has('characteristic_future_plans')"
:placeholder="'placeholder text'"/>
</element-form>
</div>
</div>
template of input-input component:
<input :value="value" :type="type" class="form-control" :class="isErrorClass"
:placeholder="placeholder" #input="$emit('input',$event.target.value)">
Components of form steps are called from the Page component. Nearby is the component of the button for moving to the next step.
<component :is="currentStep"
:candidate="candidate"
// many props
:global-errors="globalErrors"/>
<next-step :current-step="step" :token="candidate.token"
#switch-step-event="switchStep" #throw-errors="passErrors"></next-step>
NextStep component sends a request to the server, it is checking whether the required fields are filled in the database. If not, throw out a validation error. If so, go to the next form step.
you could try with watching the input values, and then use _.debounce() from underscore.js (src: https://underscorejs.org/#debounce) to delay the method call that makes a server request:
watch: {
fieldName: _.debounce(function(value) {
if(value === ''){
return;
}
this.saveField(this.fieldName, value);
},
...
Related
I am having issues sending data to a server using remix run - I'm not sure I fully understand how useAction data works. I understand how the useLoaderData functions work but when you are trying to send data to a server I get errors.
What I want to do is send a post request to my server when I click a button - if I try and call create cart in the handleCLick event it says that createCart is not a function when it is
const submit = useSubmit()
function action({ request }) {
is this where i do my POST api call?
}
async function handleClick(event) {
await createCart(id, amount)
}
Cant seem to find any documentation which tells you how to do this?
EDIT: Hit send too early
With Remix, actions always run on the server. It is the method that Remix will call when you POST to a route.
// route.tsx
import { json, type ActionArgs, type LoaderArgs } from '#remix-run/node'
import { Form, useActionData, useLoaderData, useSubmit } from '#remix-run/react'
import { createCart } from '~/models/cart.server' // your app code
import { getUserId } from '~/models/user.server'
// loader is called on GET
export const loader = async ({request}: LoaderArgs) => {
// get current user id
const id = await getUserId(request)
// return
return json({ id })
}
// action is called on POST
export const action = async ({request}: ActionArgs) => {
// get the form data from the POST
const formData = await request.formData()
// get the values from form data converting types
const id = Number(formData.get('id'))
const amount = Number(formData.get('amount'))
// call function on back end to create cart
const cart = await createCart(id, amount)
// return the cart to the client
return json({ cart })
}
// this is your UI component
export default function Cart() {
// useLoaderData is simply returning the data from loader, it has already
// been fetched before component is rendered. It does NOT do the actual
// fetch, Remix fetches for you
const { id } = useLoaderData<typeof loader>()
// useActionData returns result from action (it's undefined until
// action has been called so guard against that for destructuring
const { cart } = useActionData<typeof action>() ?? {}
// Remix handles Form submit automatically so you don't really
// need the useSubmit hook
const submit = useSubmit()
const handleSubmit = (e) => {
submit(e.target.form)
}
return (
<Form method="post">
{/* hidden form field to pass back user id *}
<input type="hidden" name="id"/>
<input type="number" name="amount"/>
{/* Remix will automatically call submit when you click button *}
<button>Create Cart</button>
{/* show returned cart data from action */}
<pre>{JSON.stringify(cart, null, 2)}</pre>
</Form>
)
}
I have a simple form as below:
some.component.html
<form class="example-form" novalidate (ngSubmit)="onSubmit()" autocomplete="off" [formGroup]="testform">
<input type="text" formControlName="name" class="form-control" placeholder="Enter name" required/>
<app-show-errors [control]="claimform.controls.name"></app-show-errors>
<button type="submit" (click)="onSubmit()">Next</button>
</form>
some.component.ts
ngOnInit() {
this.testform= new FormGroup({
name: new FormControl('', { validators: Validators.required})
}, {updateOn: 'submit'});
}
onSubmit() {
if (this.testform.valid) {
alert('saving data');
} else {
this._validationService.validateAllFormFields(this.testform);
}
}
validationService.ts
validateAllFormFields(formGroup: FormGroup) {
Object.keys(formGroup.controls).forEach(field => {
const control = formGroup.get(field);
if (control instanceof FormControl) {
control.markAsTouched({ onlySelf: true });
} else if (control instanceof FormGroup) {
this.validateAllFormFields(control);
}
});
}
Reference
Problem
The form will validate on submit if left blank, but even after filling the value when I check this.testform.valid it returns false. But if I remove updateOn:'submit' on form then it validates on blur of input control and when value is entered it validates form return true. Not sure if updateOn is working fine or not or whether I've implemented this in a proper way. Could someone point me in the right direction.
in your HTML you have two calls to onSubmit() function, from submit button:
<button type="submit" (click)="onSubmit()">Next</button>
and from the form:
<form class="example-form"
ovalidate
(ngSubmit)="onSubmit()"
autocomplete="off"
[formGroup]="testform">
The first call to be triggered is the button's trigger, which actually does nothing in terms of updating your reactive form, since you set FormGroup's option to {updateOn: 'submit'}. The second call to be triggered is the form's trigger, which does actual form update.
Here is FormGroup directive config:
#Directive({
selector: '[formGroup]',
providers: [formDirectiveProvider],
host: {'(submit)': 'onSubmit($event)', '(reset)': 'onReset()'},
exportAs: 'ngForm'
})
as we can see in host property DOM form's submit (triggered by hitting ENTER while focused within form or clicking form's submit button) will call onSubmit() function:
onSubmit($event: Event): boolean {
(this as{submitted: boolean}).submitted = true;
syncPendingControls(this.form, this.directives);
this.ngSubmit.emit($event);
return false;
}
which then will call syncPendingControls() function:
export function syncPendingControls(form: FormGroup, directives: NgControl[]): void {
form._syncPendingControls();
directives.forEach(dir => {
const control = dir.control as FormControl;
if (control.updateOn === 'submit' && control._pendingChange) {
dir.viewToModelUpdate(control._pendingValue);
control._pendingChange = false;
}
});
}
which updates a model at last.
So, in your case, just remove (click)="onSubmit()" from the submit button:
<button type="submit">Next</button>
also you do not need required DOM element property on your input, since you set it using Reactive Forms API validators: Validators.required and since you set your form to novalidate which cancels HTML5 form validation.
I have a form in which I need to call two action methods, one after the other. This is how the flow goes.
First I check if the prerequisite data is entered by the user. If not then I show a message that user needs to enter the data first.
If all the prerequisite data is entered, I call an action method which return data. If there is no data returned then I show a message "No data found" on the same page.
If data is returned then I call another action method present in a different controller, which returns a view with all the data, in a new tab.
The View:
#using (Ajax.BeginForm("Index", "OrderListItems", null, new AjaxOptions { OnBegin = "verifyRequiredData"}, new { #id = "formCreateOrderListReport", #target = "_blank" }))
{
//Contains controls and a button
}
The Script in this View:
function verifyRequiredData() {
if ($("#dtScheduledDate").val() == "") {
$('#dvValidationSummary').html("");
var errorMessage = "";
errorMessage = "<span>Please correct the following errors:</span><ul>";
errorMessage += "<li>Please enter Scheduled date</li>";
$('#dvValidationSummary').append(errorMessage);
$('#dvValidationSummary').removeClass('validation-summary-valid').addClass('validation-summary-errors');
return false;
}
else {
$('#dvValidationSummary').addClass('validation-summary-valid').removeClass('validation-summary-errors');
$('#dvValidationSummary').html("");
$.ajax({
type: "GET",
url: '#Url.Action("GetOrderListReport", "OrderList")',
data: {
ScheduledDate: $("#dtScheduledDate").val(),
Crews: $('#selAddCrewMembers').val(),
Priorities: $('#selPriority').val(),
ServiceTypes: $('#selServiceTypes').val(),
IsMeterInfoRequired: $('#chkPrintMeterInfo').val()
},
cache: false,
success: function (data) {
debugger;
if (data !== "No data found") {
//var newUrl = '#Url.Action("Index", "OrderListItems")';
//window.open(newUrl, '_blank');
return true;
} else {
//Show message "No data found"
return false;
}
}
});
return false;
}
}
The "GetOrderListReport" Action method in "OrderList" Controller:
public ActionResult GetOrderListReport(OrderListModel model)
{
var contract = new OrderReportDrilldownParamDataContract
{
ScheduledDate = model.ScheduledDate
//Setting other properties as well
};
var result = OrderDataModel.GetOrderList(contract);
if (string.IsNullOrWhiteSpace(result) || string.IsNullOrEmpty(result))
{
return Json("No data found", JsonRequestBehavior.AllowGet);
}
var deserializedData = SO.Core.ExtensionMethods.DeserializeObjectFromJson<OrderReportDrilldownDataContract>(result);
// send it to index method for list
TempData["DataContract"] = deserializedData;
return Json(deserializedData, JsonRequestBehavior.AllowGet);
}
The last action method present in OrderListItems Controller, the result of which needs to be shown in a new tab:
public ActionResult Index()
{
var deserializedData = TempData["DataContract"] as OrderReportDrilldownDataContract;
var model = new OrderListItemViewModel(deserializedData);
return View(model);
}
The problem is that I am not seeing this data in a new tab, although I have used #target = "_blank" in the Ajax.BeginForm. I have also tried to use window.open(newUrl, '_blank') as can be seen above. But still the result is not shown in a new tab.
Please assist as to where I am going wrong?
If you are using the Ajax.BeginForm you shouldn't also be doing an ajax post, as the unobtrusive ajax library will automatically perform an ajax post when submitting the form.
Also, if you use a view model with data annotation validations and client unobtrusive validations, then there would be no need for you to manually validate the data in the begin ajax callback as the form won't be submitted if any validation errors are found.
The only javascript code you need to add in this scenario is a piece of code for the ajax success callback. That will look as the one you currently have, but you need to take into account that opening in new tabs depends on the browser and user settings. It may even be considered as a pop-up by the browser and blocked, requiring the user intervention to allow them as in IE8. You can give it a try on this fiddle.
So this would be your model:
public class OrderListModel
{
[Required]
public DateTime ScheduledDate { get; set; }
//the other properties of the OrderListModel
}
The form will be posted using unobtrusive Ajax to the GetOrderListReport of the OrderList controller. On the sucess callback you will check for the response and when it is different from "No data found", you will then manually open the OrderListItems page on a new tab.
This would be your view:
#model someNamespace.OrderListModel
<script type="text/javascript">
function ViewOrderListItems(data){
debugger;
if (data !== "No data found") {
var newUrl = '#Url.Action("Index", "OrderListItems")';
//this will work or not depending on browser and user settings.
//passing _newtab may work in Firefox too.
window.open(newUrl, '_blank');
} else {
//Show message "No data found" somewhere in the current page
}
}
</script>
#using (Ajax.BeginForm("GetOrderListReport", "OrderList", null,
new AjaxOptions { OnSucces= "ViewOrderListItems"},
new { #id = "formCreateOrderListReport" }))
{
#Html.ValidationSummary(false)
//input and submit buttons
//for inputs, make sure to use the helpers like #Html.TextBoxFor(), #Html.CheckBoxFor(), etc
//so the unobtrusive validation attributes are added to your input elements.
//You may consider using #Html.ValidationMessageFor() so error messages are displayed next to the inputs instead in the validation summary
//Example:
<div>
#Html.LabelFor(m => m.ScheduledDate)
</div>
<div>
#Html.TextBoxFor(m => m.ScheduledDate, new {id = "dtScheduledDate"})
#Html.ValidationMessageFor(m => m.ScheduledDate)
</div>
<input type="submit" value="Get Report" />
}
With this in place, you should be able to post the data in the initial page using ajax. Then based on the response received you will open another window\tab (as mentioned, depending on browser and user settings this may be opened in a new window or even be blocked) with the second page content (OrderListItems).
Here's a skeleton of what I think you are trying to do. Note that window.open is a popup though and most user will have popups blocked.
<form id="formCreateOrderListReport">
<input type="text" vaule="testing" name="id" id="id"/>
<input type="submit" value="submit" />
</form>
<script type="text/javascript">
$('#formCreateOrderListReport').on('submit', function (event) {
$.ajax({
type: "POST",
url: '/home/test',
data: { id: $('#id').val()},
cache: false
}).done(function () {
debugger;
alert("success");
var newUrl = '/home/contact';
window.open(newUrl, '_blank');
}).fail(function () {
debugger;
alert("error");
});
return false;
});
</script>
Scale down the app to get the UI flow that you want then work with data.
So I followed the example in the Dojo - Using the Dojo JavaScript Library to Build Ajax Applications to add server-side validation to the username validationtextbox field on my form. Basically I added a usernameOnChange function that submitted an xhrGet request, the xhrGet returns JSON and is handled by the usernameValidationHandler.
It works great, but the usernameValidationHandler only sets the tooltip display message to an error. It doesn't set the field to be invalid and thus the user can still submit the form. How do I set the field to be invalid so the form won't submit?
<input type="text" id="userName" name="userName" size="20"
dojoType="dijit.form.ValidationTextBox"
trim="true"
required="true"
onChange="userNameOnChange"
regExp="\w+"
invalidMessage="User name is required"
/>
function userNameOnChange() {
var userName = dijit.byId("userName").getValue();
if (userName == "") {
return;
}
dojo.xhrGet( {
url: "validateUserName.jsp?userName=" + userName,
handleAs: "json",
handle: userNameValidationHandler
});
}
function userNameValidationHandler(response) {
dijit.byId("userName").displayMessage();
if (!response.valid) {
var errorMessage = "User name already taken";
// Display error message as tooltip next to field
dijit.byId("userName").displayMessage(errorMessage);
// HOW DO I SET THE FIELD TO BE INVALID NOW???
}
}
It seems I was having the same issues when I used the validation method (validator) for the control. I think the issue is with the nature of the xhrGet method as it is asychronous, so the method for determining if value is valid returns before the query is complete. Anyways, this is what I did to get it working. If there is another way, hopefully someone can post.
dojo.require("dijit.form.ValidationTextBox");
function validateUsername(value, constraint) {
// I believe the whole reason you have to hack at control to get it to
// display an error is due to the nature of the xhrGet call. Since the
// call is being made asychronously, the method would have already
// returned a result to the html control before query has finished.
// Therefore you have to do the extra method calls below. Also note
// that when the form goes for submission, it calls each controls validator
// method again! Meaning it will query the webpage again.
var loginID = dijit.byId("user_login");
var bNoNameFound = ("Error" == loginID.get("state")) ? false : true;
if ("" == loginID.value) {
// for some required=true is not kicking in, so we are forcing it.
bNoNameFound = false;
} else {
if (false == loginID._focused) {
console.log("Checking username...");
dojo.xhrGet({
url: "functions/does_user_exist.php?user_login=" + value,
handleAs: 'text',
content: {
l: value
},
timeout: 10000,
load: function(data) {
if (1 == data) {
// setting the state to error since the username is already taken
bNoNameFound = false;
loginID.set("state", "Error");
loginID.displayMessage("The username is already taken...");
// used to change the style of the control to represent a error
loginID._setStateClass();
console.log("Invalid username");
} else {
bNoNameFound = true;
loginID.set("state", "");
loginID.displayMessage("");
}
}
});
}
}
return bNoNameFound;
}
<input dojoType="dijit.form.ValidationTextBox" type="text" name="user_login" id="user_login" value="" minSize="1" maxSize="25" tabindex="10" required="true" trim="true" regExp="\w+" validator=validateUsername />
Note: I also tried using "sync" in the xhrGet parameters as well. It also had issues (aside from the obvious of locking the control until query is done)...
You could subclass the widget to override the validator method. Perhaps chain up, if you want the base functionality, then, if the result is true, run your own test with the getXhr and return the result.
An example at dojocampus just clobbers the validator method. That might also work, depending on what you want to do.
I am using the ajaxform() plugin, which so far is working well. However, my input fields have default values, and if the user just submits the untouched form, I need to blank them out before the form is submitted using the beforeSubmit: callback.
In nutshell, I don't know the syntax to check the forms input fields and stop the submit if necessary. I have an idea its using the each() method and this.defaultValue, and maybe a return false? but I'm not sure of the details.
Could anyone perhaps give me an idea? Thanks. Heres my code so far, its the checkValues() function that I'm stuck with.
$(document).ready(function(){
//========= Functions =========
function styleForm() {
$('.quickcontact label').hide();
$('input[type="text"],textarea').addClass("idleField");
$('input[type="text"],textarea').focus(function() {
$(this).removeClass("idleField").addClass("focusField");
if (this.value == this.defaultValue){
this.value = '';
}
if(this.value != this.defaultValue){
this.select();
}
});
$('input[type="text"],textarea').blur(function() {
$(this).removeClass("focusField").addClass("idleField");
if ($.trim(this.value) == ''){
this.value = (this.defaultValue ? this.defaultValue : '');
}
});
}
//options for ajaxform() function
var options = {
target: '.quickcontactDisplay', // target element(s) to be updated with server response
beforeSubmit: checkValues, // pre-submit callback
success: reBind // post-submit callback
// other available options:
//url: url // override for form's 'action' attribute
//type: type // 'get' or 'post', override for form's 'method' attribute
//dataType: null // 'xml', 'script', or 'json' (expected server response type)
//clearForm: true // clear all form fields after successful submit
//resetForm: true // reset the form after successful submit
// $.ajax options can be used here too, for example:
//timeout: 3000
};
//rebinds the ajax functionality to updated form html
function reBind() {
// re-do the form, as it has just been replaced
$('form.quickcontact').ajaxForm(options);
styleForm();
}
//checks for default values of form on submit to prevent them being submitted
function checkValues(){
}
// ==== logic =====
$('form.quickcontact').ajaxForm(options);
styleForm();
});
And my form html:
<form action="/enquiries/add" method="post" id="EnquiryAddForm" class="quickcontact">
<input type="hidden" value="POST" name="_method"/>
<input type="hidden" id="EnquiryVisitorId" value="276" name="data[Enquiry][visitor_id]"/>
<input type="text" id="EnquiryName" maxlength="200" value="Your name" name="data[Enquiry][name]"/>
<input type="text" id="EnquiryEmailAddress" maxlength="200" value="Your Email" name="data[Enquiry][emailAddress]"/>
<textarea id="EnquiryEnquiry" rows="6" cols="30" name="data[Enquiry][enquiry]">Your Email Address</textarea>
<input type="submit" value="Ok, I'm done"/>
</form>
You are abusing the default value as a label. This is causing you problems. Rather then trying to work around those problems, I suggest fixing the cause instead.
When setting default values — set default values. Don't use the default value as a pseudo-label. Use a <label> element instead.
Haven't you looked at the documentation?
beforeSubmit:
Callback function to be invoked before the form is submitted. The
'beforeSubmit' callback can be
provided as a hook for running
pre-submit logic or for validating the
form data. If the 'beforeSubmit'
callback returns false then the form
will not be submitted. The
'beforeSubmit' callback is invoked
with three arguments: the form data in
array format, the jQuery object for
the form, and the Options Object
passed into ajaxForm/ajaxSubmit. The
array of form data takes the following
form:
[ { name: 'username', value: 'jresig' }, { name: 'password', value: 'secret' } ]
Default value: null
Here the idea, didn't check it yet.
function checkValues(formData, jqForm, options)
{
for( var i in formData)
if ( formData[i].value == "")
return false;
return true;
}
sounds as if you need to:
run through all the inputs / textarea at the start and grab the default values, then stick it into an associative array with the element id as key
within checkValues, iterate through inputs once again and compare the pre-submit value against your array - when finding a match, you can set the value to "".