Set a $watch on a <SELECT> element in an Angular directive? - angularjs

How do I set a $watch on the selectedvalue property of a SELECT element in an angular directive? What is the syntax? I have a partial directive below. It doesn't blow up, but the watch never fires.
link: function (scope, elem, attr, ngModel) {
scope.$watch(elem.context.selectedOptions, function (selectedType) {
console.log("nodup choice");
})
}

I don't see a need here to put a watcher over an select dropdown to fire something on its value change. I'd suggest you to use ng-change event over that select box & have function over it. So that will get fire whenever input value gets changed.
<select ng-model="selectedValue" ng-change="changeFn()">
<option value="1"></option>
<option value="2"></option>
<option value="3"></option>
</select>

Related

Angular ng-model in `<select>` change function find dynamically

Using Angular 1.x I have two groups of select boxes, both call a function on change:
ng-click="checkOneOptionSelected()"
In my controller I would like to know which group of select boxes called the function:
$scope.checkOneOptionSelected = function(value){
.. do something...
}
Is there a way to get the value of ng-model dynamically without passing a string as a parameter, perhaps using 'this' or similar. HArdcoding the value as a param will work but feels hacky?
When using the ng-model directive, the code should use the ng-change directive instead of ng-click.
<select ng-model="$ctrl.sel1" ng-change="$ctrl.updateSel('sel1',$ctrl.sel1)"
ng-options="...">
<option value="">Select sel1</option>
</select>
<select ng-model="$ctrl.sel2" ng-change="$ctrl.updateSel('sel2',$ctrl.sel2)"
ng-options="...">
<option value="">Select sel2</option>
</select>
To share the same update function, simply add arguments to indicate the source of the change.
this.updateSel = function (id, val) {
console.log(id,val);
//...
};
The ng-click directive fights with the ngModelController. The ng-change directive uses the $viewChangeListeners property of the ngModelController. The ngChange expression is only evaluated when a change in the input value causes a new value to be committed to the model.
For more information, see
AngularJS ng-change Directive API Reference.
You may pass builtin $event object (mentioned in ng-click reference in arguments), which is iQuery event object.
ng-click="checkOneOptionSelected($event)"
Then use its target field to reason about what was clicked:
$scope.checkOneOptionSelected = function(event){
.. do something with event.target, e.g...
if(event.target.id == 'Option123') {
...
}
}

Directive isn't loading scope variable

So I have a very simple directive that's supposed to execute a jquery plugin function:
angular.module('myproject.directives').directive('starRating', function () {
return {
restrict: 'A',
scope: {
rating: '='
},
link: function (scope, elem, attr) {
console.log('rating', scope.rating);
elem.barrating({
theme: 'css-stars',
readonly: true
});
elem.barrating('set', scope.rating);
}
};
});
Here is the HTML:
<select class="service-rating"
ng-show="!!currentJob.Review.ServiceRating"
star-rating rating="currentJob.Review.ServiceRating">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
</select>
The 'currentJob' variable is only set after an $http call, however the div is only set to show once this is populated. The log is returning 'null' for scope.rating, however if I log 'scope' on it's own, it clearly shows a 'rating' property that's populated as expected.
Also if I just enter a hard-coded number for the 'rating' attribute the directive works as expected.
I'm not really sure where I'm going wrong here? Any ideas?
Essentially the problem is, as you're using ng-show, your directive gets loaded into DOM tree before your star value retrieved from the Ajax, it processed and barrating gets attached to DOM with star value 0. Basically what ng-show does is, it just hide or show DOM on html, just by toggling display css property on DOM.
So You can have two options to make your star component working.
Use ng-if instead of ng-show
Use $watch inside a component to update star rating(this will be good solution to go).
link: function (scope, elem, attr) {
console.log('rating', scope.rating);
elem.barrating({
theme: 'css-stars',
readonly: true
});
scope.$watch('star', function(newValue){
elem.barrating('set', newValue);
});
}
You could have combination of both. Show ratings only when you have rating using ng-if & then any change in rating will be taken care by $watch to update on barrating element.
Change the directive to react to changes in the rating:
app.directive('starRating', function () {
return {
restrict: 'A',
scope: false,
link: function (scope, elem, attr) {
elem.barrating({
theme: 'css-stars',
readonly: true
});
scope.$watch(attr.rating, function (newValue) {
elem.barrating('set', newValue);
console.log('rating', newValue);
});
}
};
});
Coded this way, on every digest cycle the watcher checks for changes to the Angular Expression defined by the rating attribute, and updates the barrating plugin appropriately.
Update
I did think of using a watch but I was curious as to why my original setup didn't work.
The original setup didn't work because it sets the star rating, only once, when the directive initializes. Since the data arrives from the server after the directive initializes, the new data is not seen by the plugin. By using a watch, the setting updates every time the controller changes the variable including the time when the value arrives from the server.
Also notice that the other answer hardwires the watch to a specific scope variable (not wise). It is wiser to use an attribute to declare the specific scope variable. It makes for a more versatile directive.
When a directive lacks a template that uses AngularJS bindings, it is wiser to avoid isolate scope. Put watches directly on attributes as shown in this example.

AngularJS Dropdown Directive Set Selected Item Through Two Way Binding

I have an angularjs dropdown directive. I want to be able to pass the id of the item I want to be selected as an attribute of the directive. Something like this:
<dropdown selected-item-id="ctrl.selectedItemId"></dropdown>
I implemented this and it's not working. If I display the value of itemId on the directive code, I can see the right value, but the dropdown selection does not update. Here's the relevant code:
(function () {
'use strict';
var dropdown = function ($state, service) {
return {
restrict: 'E',
replace: true,
templateUrl: '/dropdown.html',
scope: {
selectedItemId:"="
},
link: function (scope, elem, attr) {
service
.getItems()
.then(function (items) {
scope.items = items;
});
}
};
};
dropdown.$inject = ['$state', 'service'];
angular.module('app').directive('dropdown', dropdown);
})();
<select class="form-control"
ng-model="selectedItemId"
ng-options="item.id as item.name for item in items">
<option value="" selected disabled>Select an item</option>
</select>
Like I said, if I display the selectedItemId on the directive template (e.g. as one of the options) I see the right id value, however the dropdown selection doesn't change.
Any thoughts?
EDIT:
I had a typo (happened when typing the code, the actual code on my editor is correct) on the dropdown's property, item-id to selected-item-id
You are not binding selected value to item-id as you think according to your html code. You are binding selected value to selected-item-id.
Try changing your html to this:
<dropdown selected-item-id="ctrl.selectedItemId"></dropdown>
Looks like you might be having a race condition on the $digest cycle. If you call $apply() from your service callback on the first version of your code it should work. However, you will have the side effect that from time to time angular will complain about an $apply being already in progress so the second version of your code should do the trick.
I'm not sure why it wasn't working like I had it, but I made some changes an now it works ok. Here's what I did (the explanation is included as comments on the source code):
//Here I use the selectedItem bidirectional binding on the .then of my service call
//to get the id of the items that's supposed to be selected.
//Then, I filter the array of items using that id, so I get the actual object that matches the id.
//Finally, I set the scope object "selectedItem" to that object I just got.
//The "selectedItem" object is what's bound to the select via ng-model so that does the selection.
(function () {
'use strict';
var dropdown = function ($state, service) {
return {
restrict: 'E',
replace: true,
templateUrl: '/dropdown.html',
scope: {
selectedItemId:"="
},
link: function (scope, elem, attr) {
service
.getItems()
.then(function (items) {
scope.items = items;
var selectedItem = $filter('filter')(items, { id: scope.selectedItemId })[0];
scope.selectedItem = selectedItem;
});
}
};
};
dropdown.$inject = ['$state', 'service'];
angular.module('app').directive('dropdown', dropdown);
})();
<!--Here I replaced the ng-model to use an object rather than an id and did the same on the ng-options -->
<select class="form-control"
ng-model="selectedItem"
ng-options="item as item.name for item in items">
<option value="" selected disabled>Select an item</option>
</select>

Select2 AngularJS selected item is lost when change state

I have a problem with my AngularJS component Select2, I create a directive:
app.directive('autocomplete', function() {
return {
restrict : 'A',
link : function( scope, element, attrs, ngModelCtrl) {
$(function() {
element.select2({
formatNoMatches : function() {
return 'No results';
}
});
});
}
}
});
Implementation:
<select id="animals" name="animals" class="form-control" ng-model="item.animals" autocomplete >
<option ng-repeat="animal in animals" value="{{animal.code}}" >{{animal.value}}</option>
</select>
The component at this point works well, but, I have a simple $state.go that load other view and later return to this main view, when back to the view the selected animal doesn't load but the value of item.animals contain the previous correct value selected.
¿How I can selected again the item? I really don't understand why the selection is lost when change the state.
¡Thanks for advance!
That is why the ng-options exists. You could use the ng-options and the previous selected value will be loaded correctly.
<select id="animals" name="animals" class="form-control"
ng-model="item.animals" autocomplete=""
ng-options="animal.code as animal.value for animal in animals">
</select>
Example Plunker: http://plnkr.co/edit/5XLjJ2gm9ksjQfmTAPPp?p=preview

angular js: updating $watcher from directive not working

I have an application where I have a 2 drop down boxes, one for state and another for city, and a directive that has a mock up of values not tied to anything.
I need to establish the connection between the directive and these two drop down boxes.
(Before I begin, I'd like to give credit where credit is due, Jonathan Wright: Angular JS - Mapquest)
<select ui-select2="select2Options" ng-model="LocationModel.State">
<option value=""></option>
<option ng-repeat="state in states" value="{{state.id}}">{{state.name}}</option>
</select>
<select ui-select2="select2Options" ng-model="LocationModel.City">
<option value=""></option>
<option ng-repeat="city in cities" value="{{city.id}}">{{city.name}}</option>
</select>
Here is my html directive template:
<map class="mapper" height="400" width="700"></map>
and here's the angular directive (this doesn't work)
mapapp.app.directive('map', function (logger) {
var directiveDefinitionObject = {
restrict: 'E',
template: '<div id="map"></div>',
link: function link(scope, element, attrs)
{
var map_height = attrs['height'] || 400;
var map_width = attrs['width'] || 400;
$('#map').css('width', map_width).css('height', map_height);
//somehow get the scope values to show up here every time
//the dropdown gets selected
var city = scope.LocationModel.City;
var state = scope.LocationModel.State;
/* do mapping logic here */
}
};
});
As you can see the gist of what I'm trying to do, I'm trying to make my directive recognize the dropdowns.
I'm thinking that my directive should have it's own ng-model, and that the value of the ng-model should reflect on the model's two drop downs, but I'm not exactly sure how to do that. I've looked around and wasn't able to find anything that'd help me out.
[Edit - 1/28/2014 - 7:13pm eastern time]
After following Dalorzo's advice, I created the following fiddlers:
Here is a jsfiddle of a $watch working in the controller:
http://jsfiddle.net/W4ZSQ/
However, when removing this watch and trying to use the $watch located in the directive, it doesn't work.
http://jsfiddle.net/W4ZSQ/1/
[Edit - 1/28/2014 - 10:52pm eastern time]
Figured out it out. Since I was calling LocationCtrl twice, I thought that the scope model will be shared between both html elements. Apparently this is not the case; what happens is that I create another instance of the scope model, where the scope will be updated for the drop down, but not the directive. By sharing them under one scope, the sees that the value "LocationModel.State" has been changed.
http://jsfiddle.net/W4ZSQ/2/
I found a resourceful link on how to have one controller communicate with another:
http://onehungrymind.com/angularjs-communicating-between-controllers/
This is what you need todo in your directive is use a new attribute that will be added to your html, something like:
data-bound-field="LocationModel.State"
For example:
<map class="mapper" height="400" width="700" data-bound-field="LocationModel.State"></map>
Then in your directive code:
scope.$watch(attrs.boundField,function(newValue,oldValue, scope){
/* do mapping logic here */
});

Resources