Using jquery-select2 (not ui-select) and angular, I'm trying to set the value to the ng-model.
I've tried using $watch and ng-change, but none seem to fire after selecting an item with select2.
Unfortunately, I am using a purchased template and cannot use angular-ui.
HTML:
<input type="hidden" class="form-control select2remote input-medium"
ng-model="contact.person.id"
value="{{ contact.person.id }}"
data-display-value="{{ contact.person.name }}"
data-remote-search-url="api_post_person_search"
data-remote-load-url="api_get_person"
ng-change="updatePerson(contact, contact.person)">
ClientController:
$scope.updatePerson = function (contact, person) {
console.log('ng change');
console.log(contact);
console.log(person);
} // not firing
$scope.$watch("client", function () {
console.log($scope.client);
}, true); // not firing either
JQuery integration:
var handleSelect2RemoteSelection = function () {
if ($().select2) {
var $elements = $('input[type=hidden].select2remote');
$elements.each(function(){
var $this = $(this);
if ($this.data('remote-search-url') && $this.data('remote-load-url')) {
$this.select2({
placeholder: "Select",
allowClear: true,
minimumInputLength: 1,
ajax: { // instead of writing the function to execute the request we use Select2's convenient helper
url: Routing.generate($this.data('remote-search-url'), {'_format': 'json'}),
type: 'post',
dataType: 'json',
delay: 250,
data: function (term, page) {
return {
query: term, // search term
};
},
results: function (data, page) { // parse the results into the format expected by Select2.
return {
results: $.map(data, function (datum) {
var result = {
'id': datum.id,
'text': datum.name
};
for (var prop in datum) {
if (datum.hasOwnProperty(prop)) {
result['data-' + prop] = datum[prop];
}
}
return result;
})
}
}
},
initSelection: function (element, callback) {
// the input tag has a value attribute preloaded that points to a preselected movie's id
// this function resolves that id attribute to an object that select2 can render
// using its formatResult renderer - that way the movie name is shown preselected
var id = $(element).val(),
displayValue = $(element).data('display-value');
if (id && id !== "") {
if (displayValue && displayValue !== "") {
callback({'id': $(element).val(), 'text': $(element).data('display-value')});
} else {
$.ajax(Routing.generate($this.data('remote-load-url'), {'id': id, '_format': 'json'}), {
dataType: "json"
}).done(function (data) {
callback({'id': data.id, 'text': data.name});
});
}
}
},
});
}
});
}
};
Any advice would be greatly appreciated! :)
UPDATE
I've managed to put together a plunk which seems to similarly reproduce the problem - it now appears as if the ng-watch and the $watch events are fired only when first changing the value.
Nevertheless, in my code (and when adding further complexity like dynamically adding and removing from the collection), it doesn't even seem to fire once.
Again, pointers in the right direction (or in any direction really) would be greatly appreciated!
There are a number of issues with your example. I'm not sure I am going to be able to provide an "answer", but hopefully the following suggestions and explanations will help you out.
First, you are "mixing" jQuery and Angular. In general, this really doesn't work. For example:
In script.js, you run
$(document).ready(function() {
var $elements = $('input[type=hidden].select2remote');
$elements.each(function() {
//...
});
});
This code is going to run once, when the DOM is initially ready. It will select hidden input elements with the select2remote class that are currently in the DOM and initialized the select2 plugin on them.
The problem is that any new input[type=hidden].select2remote elements added after this function is run will not be initialized at all. This would happen if you are loading data asynchronously and populating an ng-repeat, for example.
The fix is to move the select2 initialization code to a directive, and place this directive on each input element. Abridged, this directive might look like:
.directive('select2', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attr, ngModel) {
//$this becomes element
element.select2({
//options removed for clarity
});
element.on('change', function() {
console.log('on change event');
var val = $(this).value;
scope.$apply(function(){
//will cause the ng-model to be updated.
ngModel.setViewValue(val);
});
});
ngModel.$render = function() {
//if this is called, the model was changed outside of select, and we need to set the value
//not sure what the select2 api is, but something like:
element.value = ngModel.$viewValue;
}
}
}
});
I apologize that I'm not familiar enough with select2 to know the API for getting and setting the current value of the control. If you provide that to me in a comment I can modify the example.
Your markup would change to:
<input select2 type="hidden" class="form-control select2remote input-medium"
ng-model="contact.person.id"
value="{{ contact.person.id }}"
data-display-value="{{ contact.person.name }}"
data-remote-search-url="api_post_person_search"
data-remote-load-url="api_get_person"
ng-change="updatePerson(contact, contact.person)">
After implementing this directive, you could remove the entirety of script.js.
In your controller you have the following:
$('.select2remote').on('change', function () {
console.log('change');
var value = $(this).value;
$scope.$apply(function () {
$scope.contact.person.id = value;
});
});
There are two problems here:
First, you are using jQuery in a controller, which you really shouldn't do.
Second, this line of code is going to fire a change event on every element with the select2remote class in the entire application that was in the DOM when the controller was instatiated.
It is likely that elements added by Angular (i.e through ng-repeat) will not have the change listener registered on them because they will be added to the DOM after the controller is instantiated (at the next digest cycle).
Also, elements outside the scope of the controller that have change events will modify the state of the controller's $scope. The solution to this, again, is to move this functionality into the directive and rely on ng-model functionality.
Remember that anytime you leave Angular's context (i.e if you are using jQuery's $.ajax functionality), you have to use scope.$apply() to reenter Angular's execution context.
I hope these suggestions help you out.
Related
I have a $watch in my component which is keeping an eye on a factory for changes.
This part works and I can see the $watch noticing and responding.
My problem is that I need to cause the template to re-render with the new
label. before I moved to a $watch I was able to use $apply to nudge the page
into reloading but that doesn't seem to work now.
I can also plug in an alert and see that the updated information is definitely hitting the component.
`
mapApp.component("vesselInfo", {
template : '<div class="vessel-info" >
<div ng-bind="viCtrl.title"></div>
</div>',
bindings: {
},
controller: VesselInfoController,
controllerAs: 'viCtrl'
});
function VesselInfoController($scope, $element, $attrs, $http, SelectedVessel) {
this.title = 'No Vessel Selected';
// bind the controller property to the service collection
this.selectedVessel = SelectedVessel.getSelectedVessel();
// watch the collection for changes
$scope.$watch(watchSource, function(current, previous){
this.selectedVessel = current;
if(current) {
this.title = 'Name : ' + current.name;
}
else {
this.title = 'No Vessel Selected';
}
});
function watchSource(){
return SelectedVessel.getSelectedVessel();
}
}
`
Try it with the following code
$scope.$watch(watchSource, function(current, previous) {
// Your watch code here
}, true);
At the end of your $watch invocation, you can pass a third argument which handles objectEquality (see docs here)
If you pass true here it will use angular.equals method which should handle checking your collection for differences more thoroughly.
I can't figure out what's happening in the following example. I just trying to create my own required validation in my own directive where I have an array and I want to make it required (it's a simplification of what I want to do but enough to show the point)
Fiddler: http://jsfiddle.net/gsubiran/p3zxkqwe/3/
angular.module('myApp', [])
.directive('myDirective', function($timeout) {
return {
restrict: 'EA',
require: 'ngModel',
controller: 'myDirectiveController',
controllerAs: 'D_MD',
link: function(scope, element, attrs, ngModel) {
ngModel.$validators.required = function(modelValue) {
var result = false;
if (modelValue && modelValue.length > 0)
result = true;
return result;
};
},
bindToController: {
ngModel: '='
},
template: '(<span>ArrayLength:{{D_MD.ngModel.length}}</span>)<br /><input type=button value="add (inside directive)" ng-click=D_MD.AddElem() /><br /><input value="clear (inside directive)" type=button ng-click=D_MD.Clear() />'
}; }) .controller('myDirectiveController', [function() {
var CTX = this;
//debugger;
//CTX.ngModel = "pipo";
CTX.clearModel = function() {
CTX.ngModel = [];
};
CTX.AddElem = function() {
CTX.ngModel.push({
Name: 'obj100',
Value: 100
});
};
CTX.Clear = function() {
CTX.ngModel = [];
}; }]) .controller('MainCtrl', function($scope) {
var CTX = this;
CTX.patito = 'donde esta el patito';
CTX.arrayElements = [];
CTX.setElements = function() {
CTX.arrayElements = [{
Name: 'obj0',
Value: 0
}, {
Name: 'obj1',
Value: 1
}, {
Name: 'obj2',
Value: 2
}];
};
CTX.clearElements = function() {
CTX.arrayElements = [];
}; })
When I hit the add (outside directive) button, the required works fine,
but when I hit the add (inside directive) button I still getting the required error in the form (the form is defined outside directive).
But the more confusing thing for me is the following:
When I hit the clear (inside directive) button after hitting add (outside directive) button to make the required error go out, in this case the form is updating and the validation error is showing.
Why $validations.required is not firing inside the directive when I add new element to array but yes when I clear it?
Any ideas?
******* UPDATE *******
It seems to be related with array.push if I change array.push with the assignation of new array with wanted elements inside it works ok.
Still the question why it is happening.
As workaround I changed in the directive the AddElem function in this way:
CTX.AddElem = function() {
CTX.ngModel = CTX.ngModel.concat({
Name: 'obj100',
Value: 100
});
};
The ngModel you use here is a JS object. Angular has a reference to that object in its $modelValue and $viewValue (because angular basically does $viewValue = $modelValue). The $modelValue is the actual value of the ngModel, which, if you change it, will change the $viewValue after having run $validators.
To know if your ngModel has Changed, angular compares the ngModel.$viewValue with the ngModel.$modelValue. Here, you are doing a push() to the $viewValue which is updating the $modelValue at the same time because they are just references of each other. Therefore, when comparing them, they have the same value ! Which is why angular does not run your $validator.
The docs explain it :
Since ng-model does not do a deep watch, $render() is only invoked if the values of $modelValue and $viewValue are actually different from their previous values. If $modelValue or $viewValue are objects (rather than a string or number) then $render() will not be invoked if you only change a property on the objects.
If I over-simplify angular code, this snippet explains it:
var myArray = [];
var ngModel = {
$viewValue: myArray,
$modelValue: myArray,
$validate: function () { console.log('validators updated'); }, // log when validators are updated
}
function $apply() { // the function that is called on the scope
if (ngModel.$viewValue !== ngModel.$modelValue) {
ngModel.$viewValue = ngModel.$modelValue;
ngModel.$validate(); // this will trigger your validator
} else {
console.log('value not changed'); // the new value is no different than before, do not call $validate
}
}
// your push is like doing :
ngModel.$viewValue.push(12);
$apply(); // will output 'value not changed', because we changed the view value as well as the model value
// whereas your should do:
var newArray = [];
// create a copy of the array (you can use angular.copy)
for (var i = 0; i < myArray.length; i++) {
newArray.push(myArray[i]);
}
ngModel.$viewValue.push(12);
ngModel.$viewValue = newArray; // here we clearly update the $viewValue without changing the model value
$apply(); // will output 'validators updated'
Of course you are not forced to do an array copy. Instead, you can force the update of your ngModel. This is done by calling ngModel.$validate();
One way of doing it would be to add a forceUpdate() function in your scope, and call it from the controller after you do a push();
Example: http://jsfiddle.net/L7Lxkq1f/
Is there anyway to trigger opening the match results on a typeahead input text box from the controller?
use case:
user goes to https://example.com/search/searchText
controller of page sets the input text to "searchText" (ng-model) on initialization
trigger showing the typeahead results from the controller
I can only seem to get the typeahead results, obviously, while typing in the input text box.
I got it to work in a couple ways, but both require changes to ui-bootstrap. I suppose I could create a pull request but not sure if my particular use case is a common one.
1) Custom directive and calling UibTypeaheadController.scheduleSearchWithTimeout method on focus of input element.
Directive:
.directive("showSearchResultsOnFocus", function($stateParams) {
return {
require: ['uibTypeahead', 'ngModel'],
link: function (scope, element, attr, ctrls) {
var typeaheadCtrl = ctrls[0];
var modelCtrl = ctrls[1];
element.bind('focus', function () {
if (!$stateParams.search || !modelCtrl.$viewValue) return;
typeaheadCtrl.exportScheduleSearchWithTimeout(modelCtrl.$viewValue);
});
}
}
Update to ui-bootstrap:
this.exportScheduleSearchWithTimeout = function(inputValue) {
return scheduleSearchWithTimeout(inputValue);
};
Bad: Requires making the method public on controller. Only method available is the init method and scope is isolated. Not meant to call from outside controller.
2) Add new typeahead attribute to allow setting default value and show results on focus:
Update to ui-bootstrap:
var isAllowedDefaultOnFocus = originalScope.$eval(attrs.typeaheadAllowDefaultOnFocus) !== false;
originalScope.$watch(attrs.typeaheadAllowedDefaultOnFocus, function (newVal) {
isAllowedDefaultOnFocus = newVal !== false;
});
element.bind('focus', function (evt) {
hasFocus = true;
// this was line before: if (minLength === 0 && !modelCtrl.$viewValue) {
if ((minLength === 0 && !modelCtrl.$viewValue) || isAllowedDefaultOnFocus) {
$timeout(function() {
getMatchesAsync(modelCtrl.$viewValue, evt);
}, 0);
}
});
Bad: Pull Request to ui-bootstrap but change perhaps not a common use feature. Submitted a PR here: https://github.com/angular-ui/bootstrap/pull/6353 Not sure if will be merged or not but using fork until then.
Any other suggestions?
Versions
Angular: 1.5.8, UIBS: 2.2.0, Bootstrap: 3.3.7
I love Bootstrap-Select and I am currently using it through the help of a directive made by another user joaoneto/angular-bootstrap-select and it works as intended except when I try to fill my <select> element with an $http or in my case a dataService wrapper. I seem to get some timing issue, the data comes after the selectpicker got displayed/refreshed and then I end up having an empty Bootstrap-Select list.. though with Firebug, I do see the list of values in the now hidden <select>. If I then go in console and manually execute a $('.selectpicker').selectpicker('refresh') it then works. I got it temporarily working by doing a patch and adding a .selectpicker('refresh') inside a $timeout but as you know it's not ideal since we're using jQuery directly in an ngController...ouch!So I believe the directive is possibly missing a watcher or at least something to trigger that the ngModel got changed or updated. Html sample code:
<div class="col-sm-5">
<select name="language" class="form-control show-tick"
ng-model="vm.profile.language"
selectpicker data-live-search="true"
ng-options="language.value as language.name for language in vm.languages">
</select>
<!-- also tried with an ng-repeat, which has the same effect -->
</div>
then inside my Angular Controller:
// get list of languages from DB
dataService
.getLanguages()
.then(function(data) {
vm.languages = data;
// need a timeout patch to properly refresh the Bootstrap-Select selectpicker
// not so good to use this inside an ngController but it's the only working way I have found
$timeout(function() {
$('.selectpicker, select[selectpicker]').selectpicker('refresh');
}, 1);
});
and here is the directive made by (joaoneto) on GitHub for Angular-Bootstrap-Select
function selectpickerDirective($parse, $timeout) {
return {
restrict: 'A',
priority: 1000,
link: function (scope, element, attrs) {
function refresh(newVal) {
scope.$applyAsync(function () {
if (attrs.ngOptions && /track by/.test(attrs.ngOptions)) element.val(newVal);
element.selectpicker('refresh');
});
}
attrs.$observe('spTheme', function (val) {
$timeout(function () {
element.data('selectpicker').$button.removeClass(function (i, c) {
return (c.match(/(^|\s)?btn-\S+/g) || []).join(' ');
});
element.selectpicker('setStyle', val);
});
});
$timeout(function () {
element.selectpicker($parse(attrs.selectpicker)());
element.selectpicker('refresh');
});
if (attrs.ngModel) {
scope.$watch(attrs.ngModel, refresh, true);
}
if (attrs.ngDisabled) {
scope.$watch(attrs.ngDisabled, refresh, true);
}
scope.$on('$destroy', function () {
$timeout(function () {
element.selectpicker('destroy');
});
});
}
};
}
One problem with the angular-bootstrap-select directive, is that it only watches ngModel, and not the object that's actually populating the options in the select. For example, if vm.profile.language is set to '' by default, and vm.languages has a '' option, the select won't update with the new options, because ngModel stays the same. I added a selectModel attribute to the select, and modified the angular-bootstrap-select code slightly.
<div class="col-sm-5">
<select name="language" class="form-control show-tick"
ng-model="vm.profile.language"
select-model="vm.languages"
selectpicker data-live-search="true"
ng-options="language.value as language.name for language in vm.languages">
</select>
</div>
Then, in the angular-bootstrap-select code, I added
if (attrs.selectModel) {
scope.$watch(attrs.selectModel, refresh, true);
}
Now, when vm.languages is updated, the select will be updated too. A better method would probably be to simply detect which object should be watched by using ngOptions, but using this method allows for use of ngRepeat within a select as well.
Edit:
An alternative to using selectModel is automatically detecting the object to watch from ngOptions.
if (attrs.ngOptions && / in /.test(attrs.ngOptions)) {
scope.$watch(attrs.ngOptions.split(' in ')[1], refresh, true);
}
Edit 2:
Rather than using the refresh function, you'd probably be better off just calling element.selectpicker('refresh'); again, as you only want to actually update the value of the select when ngModel changes. I ran into a scenario where the list of options were being updated, and the value of the select changed, but the model didn't change, and as a result it didn't match the selectpicker. This resolved it for me:
if (attrs.ngOptions && / in /.test(attrs.ngOptions)) {
scope.$watch(attrs.ngOptions.split(' in ')[1], function() {
scope.$applyAsync(function () {
element.selectpicker('refresh');
});
}, true);
}
Well, this is an old one... But I had to use it. This is what I added in the link(..) function of the directive:
scope.$watch(
_ => element[0].innerHTML,
(newVal, oldVal) => {
if (newVal !== oldVal)
{
element.selectpicker('refresh');
}
}
)
select2 provides some custom events and i want to be able to listen to them, particularly the 'select2-removed' or better yet its custom 'change' event, and I’ve been searching the internet for some example, but with no luck.
here's what I’ve done so far:
HTML:
<input type="hidden" class="form-control" id="tags" ui-select2="modal.tags" data-placeholder="Available Tags" ng-model="form.tags">
JavaScript (Angular)
$scope.form = {tags: []};
postalTags = [
{
id: 1,
text: 'Permanent Address'
},
{
id: 2,
text: 'Present Address'
}
];
$scope.modal {
tags: {
'data': postalTags,
'multiple': true
}
};
// I doubt this is going to work, since i think this is only going to
// listen on events emitted by $emit and $broadcast.
$scope.$on('select2-removed', function(event) {
console.log(event);
});
// I can do this, but then i will not know which was removed and added
$scope.$watch('form.tags', function(data) {
console.log(data);
});
What the user actually is doing here is edit the tags tagged to his/her address, and by edit I mean the user can tag new tags to his/her address or remove previously tagged tags. That's why I need to track which tags where added and which are removed.
UPDATE
I saw a plausible solution on this discussion, but i cant make the provided code to work with mine, so i did some workaround, and this is what i did.
so instead of adding the,
scope.$emit('select2:change', a);
somewhere around here,
elm.select2(opts);
// Set initial value - I'm not sure about this but it seems to need to be there
elm.val(controller.$viewValue)
I put it here,
if (!isSelect) {
// Set the view and model value and update the angular template manually for the ajax/multiple select2.
elm.bind("change", function (e) {
// custom added.
scope.$emit('select2:change', e);
e.stopImmediatePropagation();
then i did the usual on my controller,
$scope.$on('select2:change', function(event, element) {
if(element.added) console.log(element.added);
if(element.removed) console.log(element.removed);
}
and works just fine.
But i doubt this is very good idea, and i'm still hoping for a better solution.
I used a directive to encapsulate the select and then in the link function I just retrieve the select element and at the event handler.
Markup:
<select name="id" data-ng-model="tagsSelection" ui-select2="select2Options">
<option value="{{user.id}}" data-ng-repeat="user in users">{{user.name}}</option>
</select>
Javascript:
link: function (scope, element, attributes) {
var select = element.find('select')[0];
$(select).on("change", function(evt) {
if (evt.added) {
// Do something
} else if (evt.removed) {
// Do something.
}
});
$scope.select2Options = {
multiple: true
}
}