ngModel - How to deal with its different behavior in different browsers? - javascript

I'm trying to deal with different behavior of ngModel in different browsers.
My directive wraps jqueryUI autocomplete and on its select event it calls ngModel.$setViewValue(selectedItem.id). Autocomplete allows user to select item by mouse click or by pressing enter on the keyboard.
If suggested item is:
{
"name": "Apple",
"id": "1000"
}
I expect after selecting it, the ngModel value will be selected item's id - 1000.
In Chrome it works OK - it sets $viewValue and $modelValue correctly ($modelValue=1000).
In Firefox it sets model as in Chrome ($modelValue=1000), but when I click somewhere else - make blur (then browser probably fires change event), model changes and it becomes same as visible input value ($modelValue='Apple').
In IE 11 it sets model correct only when I select item with mouse click. If I select it by pressing enter, the model becomes visible input value ($modelValue='Apple')
Here is plunkr: http://plnkr.co/edit/o2Jkgprf8EakGqnpu22Y?p=preview
I'd like to reach the same behavior in every browser. How to deal with that problems?

This seems to be related to http://bugs.jqueryui.com/ticket/8878
As pointed out in the above link, the change event is triggered only in Firefox and not in Chrome. So in your case, $setViewValue is again triggered when clicked outside and the model value is set to "Apple".
There is change callback for autocomplete jquery ui widget. To handle both the case/browsers may be you would have to explicitly set view value again on this call back (and it works).
http://plnkr.co/edit/GFxhzwieBJTSL8zjSPSZ?p=preview
link: function(scope, elem, attrs, ngModel) {
elem.on('change', function(){
// This will not be printed in Chrome and only on firefox
console.log('change');
});
select: function(event, ui) {
ngModel.$setViewValue(ui.item.data.id);
scope.$apply();
},
// To handle firefox browser were change event is triggered
// when clicked outside/blur
change: function(event, ui) {
ngModel.$setViewValue(ui.item.data.id);
scope.$apply();
}

Ok, I think I've made it. The solution is based on Yoshi's comment and it uses local model to keep selected data.
When user selects something, local model is set to selected Object and $viewValue is set to the text value of selected item. Then parser sets id property of local model as $modelValue.
select: function(event, ui) {
if(ui.item && ui.item.data){
model = ui.item.data
ngModel.$setViewValue(model.name);
scope.$apply();
}
}
ngModel.$parsers.push(function(value) {
if(_.isObject(model) && value!==model.name){
model = value;
return model;
}
return model.id;
});
Parser function do also one important thing. Because it's run when user type something or on the change event (that was the problem in firefox!), it checks if the value is same as current local model's text value, and if not it changes local model into this value. It means that if parser function is run by change event value would be the same as text value, so $modelValue is not changed, but if user type something model is updated to the typed value (it becomes String).
Validator function checks if local model is an Object. If not it means that field is invalid so by default its $modelValue disappears.
Here is the plunkr:
http://plnkr.co/edit/2ZkXFvgLIwDljfJoyeJ1?p=preview
(In formatter function I return that what comes, so $viewValue is temporarily an Object but then in $render method I call $setViewValue to set $viewValue and $modelValue correctly, so it becomes String. I heard $setViewValue should not be run in $render method, but I don't see other way to set correct $modelValue when something comes from outside).

I had similiar fights with the ngModelController and $setViewValue.
Eventually I looked for alternative solutions.
One approach that I found which worked pretty well was to create a new element as component directive which includes the input tag as a transcluded element.
app.directive('fruitAutocomplete', function($http) {
return {
restrict: 'E',
require: 'ngModel',
transclude: true,
template: '<ng-transclude></ng-transclude>',
link: function(scope, elem, attrs, ngModelController) {
var $input = elem.find('input');
$input.autocomplete({
...
});
}
}
})
In the HTML:
<fruit-autocomplete name="fruit" ng-model="model.fruit">
<input ng-disabled="inputDisabled" placeholder="input fruit"/>
</fruit-autocomplete>
Here is a working Plunker
With this proposed solution you can isolate the ngModelController and the jQueryUI modal interplay to its own custom element and it does not interfere with the "normal" <input> tag and you are not concerned by the jQueryUI bug.
By using the <input> tag as transcluded element you can still benefit from most the Angular input goodies like ng-disabled, placeholder, etc...

Related

Input with Datalist - ng-change is not fired in IE for AngularJS

I have an input tag with datalist for which the ng-change is not getting fired on selection in Internet Explorer 11. It only gets fired on blur of the input. It is working in Chrome.
Codepen Below:
https://codepen.io/vijayvmenon/pen/gzLYgp
<input list="testList" name="origin node" ng-model="SelectedDoctor"
ng-change="LoadSessionData(SelectedDoctor)"
autocomplete="off" required />
<datalist id="testList" >
<option value={{value.id}} ng-repeat="value in data">
</datalist>
<p>{{selectedVal}}</p>
If you check the code, you can see that in chrome the data list value is shown below on selection. In IE , the value is shown only on tab key press or when we click outside the tag.
Please let me know how I can get this working in IE, so that the ng-change can be fired on selection of datalist value.
Note: If you change AngularJS version to 1.2.x, it is working fine. Anything above, its not working. This is a simplified version for a bigger application and I am triggering a backend service on selection from the datalist.
To achieve expected result, use below option of oninput event for input field
<input list="testList" name="origin node" ng-model="SelectedDoctor" oninput = "angular.element(document.getElementById('check')).scope().LoadSessionData(this)" autocompletestListte="off" required />
<datalist id="testList" >
ng-change is not fired because of the datalist on which ng-click or ng-change doesn't work
After assigning value to scope variable - selectedVal , run $scope.$apply() to see the selected option on the UI
code sample - https://codepen.io/nagasai/pen/jxVOrp
AngularJS ignores the input event in Internet Explorer 11, because Microsoft's support for input is very buggy. However, you could implement your own directive and assign it to the element, in order to emit a change event yourself, whenever an input event occurs.
.directive('input', function() {
return {
link: function(scope, element, attrs) {
element.on('input', function() { element.triggerHandler('change'); });
}
};
});
I do not recommend using the oninput attribute, because you should always stick with the ng-* attributes and not mix up AngularJS callbacks and native JavaScript event handlers. Using oninput, you might end up with other problems concerning the AngularJS data model.

How to check if Kendo UI ComboBox is Dirty (i.e. value has been changed from default value)?

I have a simple Kendo ComboBox:
HTML:
<div>
<h5>Brand</h5>
<div id="combo1"></div>
</div>
JavaScript:
j$("#combo1").kendoComboBox({
dataTextField: "name",
dataValueField: "id",
value: "Original_Brand_Value",
dataSource: {
transport: {
read: {
url: "..."
}
}
}
});
Note that the ComboBox has a default initial value of "Original_Brand_Value". How do I check if the Kendo ComboBox is "dirty" i.e. currently has a value other than "Original_Brand_Value"? Seems like I should be able to do something like:
j$("#combo1").data("kendoComboBox").isChanged()
**OR**
j$("#combo1").data("kendoComboBox").dataSource.isChanged()
But I have searched far and wide and there seems to be no such method. There has to be some way to do this, this must be a common use case.
As you have concluded, the kendoComboBox widget itself does not track whether it has been changed state. The datasource does have a hasChanges() method but making a selection does not change the datasource, it just gets a different value from it. However the kendoComboBox does raise events that you can use, for example 'select' that is fired when the user makes a selection: http://docs.telerik.com/kendo-ui/api/javascript/ui/combobox#events-select
For example you could add this to your comboBox configuration:
select: function(e) {
var item = e.item; //jQuery object representing the selection
isDirty = true; //Set a flag or call a function as required. Perhaps check the item as well to make sure it isn't the default value.
}
Alternatively there is also a method called 'select' that can be used to get or set the selected item. You can use this to get the selected index (if it is not zero, then the comboBox has a non-default selection):
var selectedIndex = j$("#combo1").data("kendoComboBox").select();
This isn't the only way. If you were to use MVVM declarative syntax with the comboBox bound to a property on a kendo.Data.Model (which is observable), any changes will automatically set the dirty flag on that model: http://docs.telerik.com/kendo-ui/api/javascript/data/model#fields-dirty
MVVM is a very powerful design pattern to use with kendo but I think going into more detail is outside the scope of what you're asking.
You can use the value() method to check the current widget value and compare it to the initial value, but probably this approach is not appropriate, as it is too obvious.
http://docs.telerik.com/kendo-ui/api/javascript/ui/combobox#methods-value
Another possible option is to subscribe to the change event of the widget and raise a custom dirty flag in a JavaScript variable. You could even set an expando on the ComboBox widget object.
http://docs.telerik.com/kendo-ui/api/javascript/ui/combobox#events-change
On a side note, the ComboBox is an input widget that should hold and submit a form value. That's why it should be created from an input or select element, not from a div.

angularjs validators not working when used with another directive

I have a "uniqueCheck" directive which checks if the value is already present in a list or not and accordingly validates the ngModel. This directive when used on say an input tag works as expected but when used on a directive which renders an input tag the result is not as expected.
The validator function inside the directive is getting called but it doesn't validate or invalidate the ngModel of the input.
You can view the complete code of the directives on the plnkr link provided
Plnkr Link : plnkr
html is as follows :
<--! when used with a directive -->
<my-wrapper ng-model="values.abc" unique-check="" list="list" prop="name"> </my-wrapper>
<--! when used on an input tag-->
<div ng-form="myform">
<input type="text" unique-check
list="list" prop="name"
name="myfield"
ng-model="values.pqr"/>
<span>isDuplicate:{{myform.myfield.$error.isDuplicate}}</span>
</div>
You're creating 2 separate ngModel instances, that are both updated when the input's changed.
The first is created by the <input> itself, which is the one assigned to 'myform'. This is the one that the <span> error message within my-wrapper is bound too.
The second one is the one created by the my-wrapper directive - which is the one that has the validator attached to it.
If you check the console (for the plnkr below) and inspect the values being output by the validator when the input is changed, you can see that the ngModel associated with the validator, is not the same ngModel that's associated with the form. But that both are actually being updated when the input's changed.
Clear the console once the page has loaded and then check the output when you change the first input.
http://plnkr.co/edit/nz6ODOVpn6lJlb055Svs?p=preview
Why is this happening?
Because both ng-model directives get passed the same string ('values.abc'), which are then evaluated against scope to determine which object property they should watch and update - i.e two way binding.
So when you change the input you're changing the value of scope.values.abc through the inputs ngModel instance. This change is picked up by the my-wrapper ngModelinstance - as it's watching the same object property - that then validates itself.
You can't solve the problem in this way, as the ngModel directive expects a string, not another ngModelinstance.
Solution
You could transfer the attributes from my-wrapper to the input at compile:
app.directive("myWrapper", function(){
var templateFn = function(element, attrs){
return '<div ng-form="myform">'+
'<input type="text" name="myfield"/>'+
'<span>(inside directive) : isDuplicate:{{myform.myfield.$error.isDuplicate}}</span>'
'</div>';
}
return {
restrict :'E',
template : templateFn,
require: 'ngModel',
scope: true,
compile: function(element, attrs) {
var attr;
angular.forEach(element.find('input'), function(elem) {
elem = angular.element(elem)
for(attr in attrs.$attr) {
elem.attr(attrs.$attr[attr], attrs[attr]);
}
});
for(attr in attrs.$attr) {
element.removeAttr(attrs.$attr[attr]);
}
}
}
});
http://plnkr.co/edit/m2TV4BZKuyHz3JuLjHrY?p=preview
Dont use scope in your myWrapper directive, it creates a separate scope of variables. Also, you need to use element.ngModel, not just a string 'ngModel' as the ng-model.
Change your myWrapper directive like this to work:
app.directive("myWrapper", function(){
var templateFn = function(scope, element, attrs){
return '<div ng-form="myform">'+
'<input type="text" name="myfield" ng-model="'+element.ngModel+'"/>'+
'<span>isDuplicate:{{myform.myfield.$error.isDuplicate}}</span>'
'</div>';
}
return {
restrict :'E',
template : templateFn,
//require: 'ngModel',
//scope: {'ngModel' : '='}
}
});

Trigger a model update from view in Angularjs

If the view (e.g the value in a text input) is changed by an external javascript, it will not be reflected in the model.
How can I trigger a model update (to read back all binding values from the view).
P.S: for some reasons, manually setting the model value is not an option for me, I just need to call the same function that is called when user is typing in the text box.
If you are changing the value of an element, for example input, that uses ng-model it should suffice to trigger the element's 'change' event.
jQuery:
$('#input').val('new value').trigger('change');
No jQuery:
var input = document.querySelector('#input');
input.value = 'new value';
var event = document.createEvent('HTMLEvents');
event.initEvent('change', true, false);
input.dispatchEvent(event);
Demo: http://plnkr.co/edit/IFb8OmegaGAAniy9ttn5?p=preview

Knockout JS: Working with old validation code that changes the DOM

See edit at the bottom.
My company has a huge code base and we want to start using knockout more effectively. However, we have validation code in place already that takes care of all aspects of client-side validation. It uses jQuery to show validation error messages and to sanitize user input.
For example, if I add the class "validate-range" to an input, it will use jQuery change/focusout events to track changes and then if a value is out of the range, it will replace it with the min/max value using $(input).val(). Since this validation code makes changes this way programmatically, my knockout view model won't be updated when these kind of changes are made.
This validation code is used everywhere in the system, and can't be replaced at the moment, so in order to use knockout, I have to make it work along side this code. What i've tried so far is creating a custom value binding that adds an additional change event handler which is used to update the view model whenever the validation code changes an input's value.
This works surprisingly well in all cases except inside a foreach binding (which is the same as using the template/with binding I would imagine). My change event handler isn't being fired on any inputs inside the foreach that use the custom value binding, even though the custom binding is being reapplied to all inputs inside the foreach every time the observable array changes.
I was hoping someone has dealt with this problem before, having to make knockout work with existing javascript code that changes DOM values, and thus doesn't update the view model. Any help is greatly appreciated.
Javascript code for custom binding, creating view model, and old validation code:
// custom value binding for amounts
ko.bindingHandlers.amountValue = {
init: function (element, valueAccessor) {
var underlyingObservable = valueAccessor(),
interceptor = ko.computed({
read: function () {
var value = underlyingObservable();
return formatAmount(value);
},
write: function (newValue) {
var current = underlyingObservable(),
valueToWrite = parseAmount(newValue);
if (valueToWrite !== current)
underlyingObservable(valueToWrite);
else if (newValue !== current.toString())
underlyingObservable.valueHasMutated();
}
});
// i apply a change event handler when applying the bindings which calls the write function of the interceptor.
// the intention is to have the change handler be called anytime the old validation code changes an input box's value via
// $(input).val("new value"); In the case of the foreach binding, whenever the observable array changes, and the table rows
// are re-rendered, this code does get ran when re-applying the bindings, however the change handler doesn't get called when values are changed.
ko.applyBindingsToNode(element, { value: interceptor, event: { change: function () { interceptor($(element).val()); } } });
}
};
// view model creation
// auto create ko view model from json sent from server
$(function () {
viewModel = ko.mapping.fromJS(jsonModel);
ko.applyBindings(viewModel);
});
// old validation code
$(document).on("focusout", ".validate-range", function () {
var $element = $(this),
val = $element.val(),
min = $element.attr("data-val-range-min"),
max = $element.attr("data-val-range-max");
if (val < min)
// my change handler from custom binding doesn't fire after this to update view model
$element.val(min);
if (val > max)
// my change handler from custom binding doesn't fire after this to update view model
$element.val(max);
// more code to show error message
});
HTML code that uses the custom binding inside of a foreach binding:
<table>
<thead>
<tr>
<td>Payment Amount</td>
</tr>
</thead>
<tbody data-bind="foreach: Payments">
<tr>
<td><input type="text" class="validate-range" data-val-range-min="0" data-val-range-max="9999999" data-bind="amountValue: Amount" /></td>
</tr>
</tbody>
</table>
So in the above example, if I enter "-155" in an amount text box, my custom binding runs and sets the view model Amount to -155. Then the old validation runs and re-sets the value of the textbox to "0" with $(input).val(0). My view model doesn't get updated at this point, and still reflects the -155 value. My change event handler from the custom binding is supposed to be ran to update the view model to 0, but it doesn't.
Edit:
As pointed out in the answer, .val() does not trigger any change events. The change event handler I added didn't do anything. The reason the view model was being updated when the validation code changed a value outside of the foreach binding was because we had logic somewhere else in our javascript code that was manually triggering the change event using the blur event, which in turn triggered my custom binding to run and update the view model. This blur event handler was directly bound to the text boxes, instead of being delegated, so it worked for text boxes that were there when the page is first rendered, but not for the ones dynamically inserted by the foreach binding.
For now, I just changed this logic to delegate the events within the document, so it would include dynamically inserted text boxes, and it seems to be working fine. I'm hoping to come up with a better solution in the future.
Calling $(element).val("some value"); does not trigger the change event.
You would need to do: $(element).val("some value").change();

Categories