Object properties being wiped by fast changes - javascript

I have a form that uses server-side validation and coercion.
In Vue, the state of the form fields is held in an object called instance, on the data object. Each field's value is represented by a property of instance.
onChange of any field, instance is posted to an API method that returns validation results and a coerced dataset (coercion does things like adding spaces to phone numbers, capitalising postcodes etc.).
Vue takes the response and iterates through the coerced data, replacing the properties of instance. If a field has not yet been reached by the user it is skipped (There is a reached object that keeps track of which fields the user has made it to).
The issue that I'm having is that occasionally (when entering data extremely quickly from one field to the next) the input of the current field gets cleared when the coerced data is returned from the previous one.
Initially I thought that there must be some issue with the reached logic, and that the null data returned for the field that the user is working on is overwriting the current input. But this is not the case; I can see in my logs that fields are being skipped yet the input is still clearing.
I'm starting to think that this might be a bug with Vue. Or at least, something specific to how Vue handles the data/dom elements that I need to account for. Is there a way that setting instance.foo could cause instance.bar to be reset?
//this is called onChange for any field.
change: function(e) {
this.$set(this.instance, e.name, e.value);
this.setReached(e.name);
this.validate(true);
},
validate: function(reachedOnly) {
axios.post(this.validateUrl, this.getFormData(false)).then(response => {
this.allErrors = response.data.errors;
this.setFormData(response.data.values, reachedOnly);
this.fieldNumberValidated = this.fieldNumberReached;
});
},
setFormData: function(data, reachedOnly) {
for (var fieldName in this.fieldNames) {
var value = data[fieldName];
if(reachedOnly && !this.reached[fieldName]){
console.log('skipping - '+fieldName);
continue;
}
if (value && value.date) {
value = value.date.replace(/\.\d+$/,'');
}
this.$set(this.instance,fieldName,value);
}
},
* UPDATE: *
I think I know what's happening now.
Field A triggers change()
Data gets sent for validation
User starts inputting into field B
Validated data gets returned. And set on this.instance.
Vue skips field B because it isnt in this.reached
BUT this.instance is being updated and redrawn.
Field B may have text entered in its input but it hasn't been added to this.instance because it hasn't triggered change() yet. So this.instance is redrawn based on field B having no value, which in turn updates the input and wipes whatever may have been in there.

This isn't a full answer but just some thoughts.
I'm not certain about why a field is being cleared, however I would like to point out a concurrency issue you may have. If you're calling the API for each keypress, you're not guaranteed that they will respond in the correct order, and it could be that you are setting the form data to an old validation response which would cause you to lose any text entered into the textbox since the request was fired. Also it's generally a good idea not to spam the server with too many requests.
At a minimum you should probably debounce the API calls, or use blur instead of change event, or you could implement some logic that cancels any pending validation request before firing another one.
Is there any particular reason why you are using this.$set? It should only be used if you're adding a property to an object.
Initially I thought that there must be some issue with the reached logic, and that the null data returned for the field that the user is working on is overwriting the current input. But this is not the case; I can see in my logs that fields are being skipped yet the input is still clearing.
It might be better to log when you set the data, instead of when you skip. The issue is some fields are being cleared, so log every time they are set so you can identify times when the field is being set when it shouldn't be.
Is there a way that setting instance.foo could cause instance.bar to be reset?
Not that I'm aware of. It would help if you can provide a MCVE.

I eventually solved this by having 2 different events on my input fields - one for input that updates instance and another on blur that sends the validation request.
change: function(e) {
this.validate(true);
},
input: function(e) {
this.$set(this.instance, e.name, e.value);
},
This ensures that the properties of instance are always in line with their related input fields, and so nothing gets erased when instance is redrawn.

Related

Using AngularJS, how can I prevent $watch from firing on load?

I have a form with a scope, in this scope there are key value pairs for the element ids and respective values.
Keys and values are auto-generated. There can also be user-defined default values, which are attached to the window and dynamically added to the scope.
What I need to do is, on first load, add the default values to the form. If the user then fills in values, the scope, with the key value pairs is saved to localStorage - this works well.
When the page reloads, I want to take these values from localStorage and fill out the form. This works for basic string / integer / boolean input values, but fails for more complex objects e.g. option boxes where values are stored in this format:
{
"example": {
"a": {"x": true, "y": false},
"b": {"x": true, "y": true},
"c": {"x": false, "y": true}
}
}
I've tried using $watchCollection, - this works for most controls, however, it doesn't work for controls that have more than one level in the hierarchy (see example).
Because $watchCollection doesn't work with more than one level in the hierarchy, I've tried using $watch, as well.
This results in everything saving to localStorage perfectly - including the above example, however, when the page reloads - the $watch is attaching and firing straight away.
This results in localStorage being overwritten before filling in the form values on load!
frmCtrl.ts
this.$scope.$watch('lpForm', () => this.formAutoSaveService.saveData(this.pageId, this.$scope.lpForm), true);
frmAutoSaveService.ts
saveData(pageId: string, storedData: any) {
localStorage.setItem(pageId, JSON.stringify(storedData));
}
What I need is:
to get the localStorageand fill in the form with the values set without whilst maintaining any default values unless the user has changed the value of the field.
for $watch to start 'watching', but not to fire the method to save to localStorage until the user has changed something on the form.
for localStorage not to be overridden with the form $scope when the form is loaded!
Can anyone help please?
From the Docs:
After a watcher is registered with the scope, the listener fn is called asynchronously (via $evalAsync to initialize the watcher. In rare cases, this is undesirable because the listener is called when the result of watchExpression didn't change. To detect this scenario within the listener fn, you can compare the newVal and oldVal. If these two values are identical (===) then the listener was called due to initialization.
Instead of preventing the watch from firing, detect the initialization and ignore it.
this.$scope.$watch('lpForm', (newVal,oldVal) => {
if (newVal == oldVal) return;
//ELSE
this.formAutoSaveService.saveData(this.pageId, this.$scope.myForm)
}, true);
For more information, see
AngularJS scope/rootScope Type API Reference - $watch

Why does min attribute cause ngChange to be called?

I have the following input field:
<input type="number"
class="menu-control validate"
style="width: 50px;"
ng-disabled="!ctrl.editable()"
min="1"
ng-change="ctrl.updateBookingPriceRequest()"
ng-model-options="{ updateOn: 'default blur', debounce: { 'default': 2000, 'blur': 0 }}"
ng-model="extra.quantity" />
My problem is the min directive. While it's there, angular starts repeatedly calling ng-change without the input having changed at all (not good since I'm performing an ajax call on change). If I remove min it works just fine, and I don't have the same problem with max.
It doesn't seem to matter if the model is above or below min initially.
Can anyone see something that I can't?
Edit:
I tried making my change function do nothing, and it stopped the problem, so it must be due to my code. But what I don't understand is why it works fine without min!
this.updateBookingPriceRequest = function () {
_this.prices.getBookingPrice(_this.bookingPrice).then(function (response) {
if (response.successful) {
_this.bookingPrice = response.data;
_this.bookingPrice.mooringExtras.bookingExtras.forEach(function (extra) {
var feature = _this.features.filter(function (f) { return f.featureId === extra.mooringFeatureId; })[0];
extra.unitOfMeasureId = feature.unitOfMeasureId;
extra.pricedQty = feature.pricedQuantity;
extra.pricingType = feature.pricingType;
});
if (_this.bookingPrice.mooringDiscounts) {
_this.bookingPrice.mooringDiscounts.forEach(function (discount) {
discount.discountName = _this.harborDiscounts.filter(function (x) { return x.id === discount.discountModelId; })[0].name;
});
}
}
else
_this.Error.showErrorMessage('Error getting booking price: ' + response.message);
});
};
The "extra" object on which the model is a property is changed in the function, however the "quantity" property remains the same. Could this cause ng-change to be triggered?
Edit to show how objects are defined (see comment by valepu):
The extra object(s) are in an array (my input field is inside a repeater, but in my current test there is only one object in the array), which is defined on a property called mooringExtras, which in turn is a property of a bookingPrice object, which is updated via an http request when ng-change gets called (see code). I know it gets complicated, my apologies for not knowing how to simplify it better.
The extra object contains a number of properties, with "quantity", a number, being the model for the input.
Here is an JSON of the extra object:
{"id":401,"bookableFeatureId":13,"mooringFeatureId":4,"featureName":"Wi-fi","bookingId":1104,"booked":true,"price":100.00,"totalAmount":300.00,"days":8,"quantity":3,"currencyUnit":"SEK","created":1460542055177}
Every time ng-change is called the bookingPrice object is changed, however, the value of extra.quantity remains the same.
I have just realized that in your onChange function you do this:
_this.bookingPrice = response.data;
Which, according to what you wrote in your question, is the object containing the array you iterate on to create your inputs.
When you completely replace the object, ng-repeat will create the inputs from scratch. When you have min set in your input this will trigger ng-change on input creation if the starting input is not valid (angular will set the ng-model to undefined in this case), which will change the whole array, which will trigger ng-repeat again, recreating inputs with a min attribute, which will trigger ng-change again and so on...
Normally ng-repeat generates an hash of the object to track changes on the data it's iterating on, if you completely replace it then it will think you deleted the old object and put in a new one (even though they have the same data), by using track by extra.id will tell ng-repeat that even though you replaced the object, they actually didn't change (they still have the same .id) and won't recreate the objects from scratch but, this is a fix but it's probably a good practice to just replace the values of the current array.
I have managed to recreate your issue in this plunkr: http://plnkr.co/edit/XyEyGTvuYKyz1GGmWjuP?p=preview
if you remove the line:
ctrl.objects = [{quantity: 0, price: 0, booked: true}, {quantity: 0, price: 0, booked: true}];
it will work again
I'm still not quite sure why the problem only occurred with the min attribute on the field, but by adding "track by extra.id" to the ng-repeat that wrapped the input field, I solved the problem. I guess when the "extra" object, on which the model was a property, changed, angular regenerated the input field, triggering ng-change. By tracking by an unchanging id, angular doesn't need to regenerate the input field since the id remains the same, thus not triggering ng-change.
I'll accept this as my answer, but if anyone can explain why it worked without min, I will happily accept their answer instead.

$watchGroup unexpected behaviour

I've been using $watchGroup to watch a range of fields and trigger a range of functions depending if a particular field has been changed.
I've set up the following plnkr to demonstrate the unexpected behaviour I've came across.
$scope.$watchGroup(['first', 'second', 'third'], function(newValues, oldValues)
{
var message =
{
first: newValues[0],
second: newValues[1],
third: newValues[2],
firstOld: oldValues[0],
secondOld: oldValues[1],
thirdOld: oldValues[2]
};
if(newValues[0] !== oldValues[0]){
console.log('First changed')
}
if(newValues[1] !== oldValues[1]){
console.log('Second changed')
}
if(newValues[2] !== oldValues[2]){
console.log('Third changed')
}
$scope.messages.push(message);
});
The scenario involves three watched fields and I'd like to trigger a function depending on which field has changed. I've been using the 'newValues' and 'oldValues' to monitor which field has changed.
The problem I've came across is that if I've changed the "Second" field then go and change the "First" or "Third" field, the "Second" function is triggered as its storing the previous 'newValues' and 'oldValues' which makes it look like the "Second" field has changed as demonstrated in this image.
I've highlighted the anomaly in the picture. I'd expect once I started changing the "Third" field, the 'newValues' and 'oldValues' for "Second" to be the same as it isn't the field changing.
I'm aware that I could persist two levels of old values and compare them to get around this however I'd expect it to work as I've described. Any clarification if this is a bug or intended functionality would be appreciated.
The angular documentation for $watchGroup states that watchExpressions is an "Array of expressions that will be individually watched using $watch()". Which makes me think that this isn't intended functionality.
Going by the Angular docs for $watch group and that it internally uses $watch for each individual expression I think what you are seeing is the expected behavior
From the docs for $watchGroup,
* The `newValues` array contains the current values of the `watchExpressions`, with the indexes matching
* those of `watchExpression`
* and the `oldValues` array contains the previous values of the `watchExpressions`, with the indexes matching
* those of `watchExpression`
So the new value always has only the latest value and old values contains the previous value.
Secondly, the $watchGroup internally calls the $watch [And what you see is the same behavior for watch]. $watch updates the last value and current value and then calls the listener function only if the current value is different from last value. So in this case, say when you update 'first' expression after 'second' expression, the listener function is not invoked for the 'second' expression and old value is still 'second value'.
If your listener function is really dependent on the which expression has changed, then you are better off using $watch instead of $watchGroup [IMHO, i don't see a performance difference as the $watch is going to be triggered for all expressions]. But if you want call a common handler and pass all new values irrespective of which expression has changed then you could go for $watchGroup.
All said, it would be still be good if you could post this in angular group and get it confirmed from "horse's mouth" :)

How to properly clean form with invalid input from AngularJS controller?

I have an AngularJS form that contains - among other fields - one of type url. The latter is important as this forces the corresponding input to be a valid URL.
Under certain conditions (for instance, a modal dialog with such a form is to be closed), I want to clear that form programmatically. For that purpose, I implemented method reset that basically clears the corresponding form model by setting $scope.formData = {}. Thus, it sets the form model to a new, blank object.
While that assignment clears all valid fields in the rendered HTML form, it does not clear invalid fields, like an invalid URL. For instance, if the user would provide invalid input ht://t/p as URL, that input would not be removed from the rendered form.
I think this is due to the fact that any invalid URL is not reflected by the model - such an invalid URL just wouldn't "make" it to the model because it does not pass validation in the NgModelController#$parsers array. Thus, in the model - there is no URL at all. Consequently, resetting the form model to {} cannot actually change the model's URL as it has not been set yet.
However, if method reset explicitly sets field $scope.formData.url = "", the invalid URL will be cleared properly (at least, the rendered form won't show it anymore). This is caused by the explicit change of the URL in the model. However, now, model variable formData.url contains the empty string (well, not surprisingly), while by using = {}, all fields would be undefined instead.
While assigning individual fields to "" works as workaround for simple forms, it quickly becomes cumbersome for more complex forms with many fields.
Thus, how could I programmatically reset the form efficiently and effectively - including all invalid input fields as well?
I created a Plunker at http://plnkr.co/c2Yhzs where you can examine and run a complete example showing the above effect.
Specify the type of your button as reset. That will not only call the ngClick function, it will also clear the content of the HTML form.
<button type="reset" ng-click="resetFormData()">Reset</button>
I think this solution is moderately elegant: your plnkr reviewed
The big difference is the initialization of your model object.
I think things gets messed up when a variable becomes undefined, it doesn't get updated anymore.. it should be connected (veeeery) deeply with how validation works (docs link)
Returning undefined in that case makes the model not get updated, i think this is exactly what happens behind the curtain
PS: you can recycle resetImplicitly for all your forms in the webapp :)
After trying several answers without success in similar questions, this worked for me.
In my controller:
$scope.cleanForm = function() {
$scope.myFormName.$rollbackViewValue();
};
Just call with some ng-click or any way you want.
Cheers
The Thing is tag is of type "url" which means
if user will enter specifically a valid url then only it will set values of model
If user will expicitly reset it which means setting model values to "" will again make textbox empty .
It is looking like it is setting the values but actually not ,so when you set its value to "" .Angular will set modal value to ""
Lets take another example : put replace "text" with "email"
<input type="email" ng-model="formData.name" />
<br />URL:
<input type="url" ng-model="formData.url" />
<br />
In above code If you will enter invalid email it will not set the values of email's model.
You probably need to make a copy of the model in its pristine state and set the model to pristine when you reset.
There's a good example here:
http://www.angularjshub.com/examples/forms/formreset/
The url form fields are passed into the model only if they are valid. Thus in case of an invlaid-url entry in the form, the scope variable is not assigned with the model and clearing the forms entry by assigning an empty object to the model will still persist the value at the UI front.
The best alternative to this is to assign the model associated with the form data with a null. A similar answer appears here:
https://stackoverflow.com/a/18874550/5065857
ng-click="formData={};"
just give like this ,
<button ng-click="formData={}">(1) Reset Full Data: formData = {}</button>
Reset your form data directly in ng-click itself.

Force re-validation in jsViews if data-linked value doesn't change

I ran into an issue while using jsViews with validation (using code from jsviews.com/#samples) and jQuery UI Autocomplete:
I have a converter (convertBack) on the field that transforms the entered text back into a GUID based on a dictionary. It returns null if the field is empty or invalid.
The issue is that jsViews doesn't notice the update from one null value to the other (i.e. blank to invalid, and vice versa). I tried to fix this by adding a call to refreshValidates() on the validation tag to the DOM onChange manually, but any invalid value entered gets deleted.
Question: Is there a way to achieve re-validation in jsViews natively?
I changed the jsViews validation code to allow checking displayed value:
[End of onAfterLink]: It passes the current (displayed) value, not only the converted (which is null in both cases):
(...)
tag.validate(tagCtx.args[0], tag.linkedElem.val()); // Validate initial data
tag.linkedElem is the HTML element on which you are doing 2-way data-binding, (and validation).
So if you just want to get the current value of the input element, yes, tag.linkedElem.val() is good.
Additional response added following your comment below:
Looking at your jsfiddle: http://jsfiddle.net/w43hD/1/
You are using
data-link="{validate DictionaryValue inDictionary='dictionary' convert='fromGuid' convertBack='toGuid'}"
which will trigger validation when the DictionaryValue it is binding to changes. But you are mapping both invalid user entry strings and the empty string to the same DictionaryValue of null. So of course switching from invalid to empty string does not trigger validation.
You can change your getKey converter to map the empty string to something other than null, e.g. "" - and then it works. See updated version: http://jsfiddle.net/w43hD/2/

Categories

Resources