Why does min attribute cause ngChange to be called? - javascript

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.

Related

Object properties being wiped by fast changes

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.

Angularjs Select with group and initial value

I have a form that includes a SELECT element. I load the possible values in my controller from a function that returns the data from a database. I want to group the options by a group name.
I have the list of options loading properly showing the grouping. My problem, is the initial value is not displaying - this is based on a data model. It shows as blank. If I choose any option in the list, it does properly display.
I followed the example from this solution Populate a Dropdown list by grouping using AngularJs
From the various other examples that I have seen, this should work...I'm guessing it is some little thing I accidentally overlooked.
This loads the possible values for the drop down:
$http.get("api/getIndustry").success(function(data){
$rootScope.industryData = [];
$.each(data, function (i, data) {
$rootScope.industryData.push({
group: data.group,
id: data.id,
text: data.text
});
});
});
For now, I am trying to initially set a selected value (eventually it will be set from reading a record):
$scope.example3model = {group: 'Energy and Natural Resources', id: '25', text: 'Utilities'};
And this is a portion of my view.
<td colspan="4" ng-hide="editableForm.$visible">
<select ng-model="example3model" class="form-control input-md" ng-options="industry.text group by industry.group for industry in industryData" >
</select></br>
{{example3model}} <- did this to see what was chosen
</td>
I'm not sure what else to try to get this to work...the only problem I see right now is that the list is not showing the 'default' value of what is initially in example3model (so the list shows as blank). If I choose a value in the list it is displayed correctly.
Any suggestions would be greatly appreciated.
The problem is that you're trying to set the initial value to an object literal, and even though it may look the same as one inside the select options it is not.
This happens because of how Javascript and AngularJS both work to set that initial object-value (note that this wouldn't happen if options was an array of primitives such as numbers and strings): {} and {} look the same from a human perspective, but they're clearly not the same in JS, try doing this in the browser console:
{} == {}
// this will be false
{ a: 1 } == { a: 1 }
// this will be false as well
Now, what Angular does behind the scenes is checking if the ngModel matches any reference inside ngOptions, that's why we need to set the initial value specifically referenced from the options array.
The initialization, in your example, must be something like this in the specific case you provided (note that I'll be hard-coding the id to match the needs of your post, but you could change it to match whatever you need)
const defaultId = 25;
$scope.example3model = $rootScope.industryData.find(data => +data.id === defaultId)
Now the ngModel value is pointing to the referenced array object that we want.
* Take a look at the official documentation about complex models for ngOptions
[note that this will not work if none of the objects in the ngOptions array has that defaulted id as it will not match any of them]

KnockoutJS - UI not updating with built-in observableArray methods except push and pop

When I do a push or pop operation on my observable array, it is reflected in the ui. However other operations on the array won't change anything in the UI. Here's an example of my case:
<ul data-bind="foreach: addresses">
<!-- ko template: {name: 'AddressItemTemplate', data: {address: $data, page: 'update-page'} }-->
<!-- /ko -->
</ul>
I use my template in two different pages and thats the reason I am using the template data like that.
<script type="text/html" id="AddressItemTemplate">
<p data-bind="text: (page == 'update-page') ? 'updating' : 'declined'"</p>
<p data-bind="text: address.title"></p>
</script>
Now on js side, ofc I declared the addresses as an observable array
this.addresses = ko.observableArray([addresObject1, addressObject2, ...])
Somewhere on the page, I edit the address values. To have UI reflecting the changes, I do the following:
//suppose we know that the first address is being edited
var tmp_addresses = addresses();
tmp_addresses[0].title = 'blabla';
addresses(tmp_addresses);
And there it is, in the viewModel, I can see that the content of the addresses has been updated, but not in the UI??
addresses.push(someAddressObject);
or
addresses.pop();
works (updates the UI with the new/removed element). But addresses.splice(0, 1, newAddressObject) does not do anything in the UI again.
What am I missing here? How can push pop work and not the others??
Am I experiencing a bug in knockout framework?
UPDATE
I found out a way to do it, but there's something wrong. I'll come to that but first:
I am well aware that if I use observable objects in the observable array, the changes would be reflected in UI. However that is exactly the thing I want to avoid. It is an overkill.
Observable properties should be required in cases where properties are really exposed to user interaction. For example, if you have a UI for setting each of the fields of an object, then yes, observable property would be the right call.
However in my case, I dont even have a UI for updating the address field. Moreover, I dont need tinkering and constantly watching all the properties of all the addresses. In my case, every now and then an update occurs from the server and that changes only a single field in a single address field.
On another perspective the way I suggest should work. I simply update the whole array at once, not every element individually. It's the exactly the same logic with:
someObservableObject({newObject: withNewFields, ...});
Thats why I dont need my objects as observables. I simply want to re-declare the array and be done with the change. For example, it is advised that if you are going to make lots of pushes into the observable array, dont use array.push(...) multiple times, instead re-declare the larger array on to the observable array variable in a similar way I do it in my question. Otherwise, I am telling knockout to track every single object and every single field in them, which is hardly what I want.
Now, I finally got it working but the way I do suggests that there is a cleaner way to do it.
I found out that, the items in the observable array are somehow tracked and not updated when you re-declare the array with them. For example the code I gave in the question would not work. However the code below works:
var tmp_addresses = addresses();
var tmp_addr = tmp_addresses[0];
var new_addr = {};
Object.keys(tmp_addr).forEach(function(key){
new_addr[key] = tmp_addr[key];
});
new_addr.title = 'Hey this is something new!'
addresses.splice(0, 1, new_addr);
Not satisfied? The code below is going to work as well, because we are re-defining the array:
var newAddressObject1 = {...}, newAddressObject2 = {...};
addresses([newAddressObject1, newAddressObject2]);
But the following would not work!
var tmp_addresses = addresses();
var tmp_addr = tmp_addresses[0];
tmp_addr.title = 'Hey this address wont update';
addresses.splice(0, 1, tmp_addr);
How come? I think knockout adds an internal property to his items in observableArrays and when I try to reinsert one, it will not update.
My problem has now morphed into creating a new object with the same properties of the desired item in the observable array. The way I coded above is simply very dirty-looking. There's gotta be a better way to do that
You are wrongly assigning value to observable title that is the reason why UI not reflecting its changes (2 way binding broken).
Thumb rule is always use () notation while assigning a value to observable (keeps two way binding intact)
viewModel:
var ViewModel = function () {
var self = this;
self.addresses = ko.observableArray([{
'title': ko.observable('one')
}, {
'title': ko.observable('two')
}])
setTimeout(function () {
var tmp_addresses = self.addresses();
tmp_addresses[0].title('blabla'); //assigning data to observable
self.addresses(tmp_addresses);
}, 2000)
};
ko.applyBindings(new ViewModel());
working sample here
PS: Don't get deceived by seeing the value change in viewModel the moment you done assigning using = two binding is broken UI wont reflect VM'S changes .
when you splice up your observableArray UI takes it changes check here
The problem was exactly as #jason9187 pointed out in the comments: The references of the objects in the observable array does not change when I edit a field of them. Therefore, KO would not interpret my array as changed. If the observableArray had contained simple data types, then the way I suggested could work without a problem. However, I have an Object in the array, therefore although I edit the Object, it's reference (pointer) remains the same, and KO thinks that all Objects are the same as before.
In order to achieve what I wanted, we have to solve the deep cloning problem in javascript like in this post.
Now there's a trade-off there, deep cloning is very simple in vanilla if you don't have a circular architecture or functions in your objects. In my case, there's nothing like that. The data comes from a restful API. If anybody in the future gets hold of this problem, they need to deep-clone their 'hard-to-clone' objects.
Here's my solution:
var tmp_addresses = JSON.parse(JSON.stringify(addresses())); //Creates a new array with new references and data
tmp_addresses[0].title = 'my new title';
addresses(tmp_addresses);
Or, if you can create address objects, following will work as well:
var tmp_addresses = addresses();
tmp_addresses[0] = new randomAddressObject();
addresses(tmp_addresses);
Here is a fiddle that I demonstrate both of the methods in a single example

$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" :)

Modifying a watched value within a $watch

I have an <input ng-model='list' ng-list>, and I want to make sure that no duplicates appear in this text field—I want to automatically remove them if the list contains duplicates.
I put a $scope.$watch('list', function(listValues) { in the controller, and try to remove any duplicates from listValues, but have problems. From within the watch function, if I set listValues = _.unique(listValues), $scope.list's value never changes. If I try $scope.list = _.unique(listValues), I get an error about the digest cycle already running.
How can I watch for a scope variable to change, and when it does, perform an operation to change that new value?
Here's an example of it not working: http://plnkr.co/edit/b0bAuP1aXPg3HryxCD9k?p=preview
I thought this would be simple. Is there some other approach that I should be using?
ng-change is probably a better approach in this case. In particular, this attribute of ng-change:
if the model is changed programmatically and not by a change to the
input value
If you place your de-dupe in a function and then use ng-change to call it, I think you will get the results you are after.

Categories

Resources