angular js: updating $watcher from directive not working - angularjs

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 */
});

Related

Update a scope variable from a directive (angularJS)

I have a directive I would like to apply to multiple input elements to change their value. I've been successful in applying it to the input elements value, but for some reason that is not being reflected in the scope. I'm kinda new to Angular and apologize if I'm missing some kind of obvious answer.
http://jsfiddle.net/hmko75td/ JS Fiddle
<div ng-app="myApp">
<div ng-controller="MyCtrl">
<select ng-model='Factor'>
<option value=1>1</option>
<option value=2>2</option>
<option value=5>5</option>
</select>
<br />
<input convert-input ng-model="myNumber">
{{myNumber}}
<br />
<input convert-input ng-model="myNumber2">
{{myNumber2}}
<br />
<input convert-input ng-model="myNumber3">
{{myNumber3}}
<br />
</div>
</div>
var app = angular.module('myApp',[]);
app.controller('MyCtrl', function($scope) {
$scope.myNumber = 1;
$scope.myNumber2 = 2;
$scope.myNumber3 = 3;
$scope.Factor = 1;
});
app.directive("convertInput", function () {
return {
require: 'ngModel',
restrict: "A",
link: function (scope, element, attrs) {
scope.$watch('Factor', function () {
if(scope.Factor){
element[0].value = scope.Factor * element[0].value;
}
});
}
};
});
This simplified example shows the crux of my problem. When changing the value of the droplist it correctly updates the element's value on the page, but that does not get translated correctly back into the scope variable.
Any ideas how to either 1) tell the directive which scope variable needs to be updated or 2) force the model to update based on the inputs value?
Thanks!
One constraint I see with your snippet is that you need all uses of your convert-input directive to share Factor, which exists in an enclosing parent scope.
In such a case, one viable approach would be to use the attrs passed into the directive to extract the name of the ng-model binding, and then to update the corresponding binding via scope.
scope[attrs.ngModel] *= scope.Factor;
Here's a fork of your JSFiddle demonstrating this:
http://jsfiddle.net/m4hvre2y/
Another approach to having directives update an ng-model in a parent scope would be to declare two-way binding (e.g. scope: { ngModel: '='), but it isn't applicable in your case due to the constraint I mentioned above. If you did this, the isolated scope means you lose access to Factor unless it's specifically passed into the directive.

Directive with ng-model Attribute Not Resolving Using $http

Trying to make a rating directive but I'm stuck at getting rating2 to work. The first rating worked because the rating1 is hardcoded within the controller. But normally I have to get the saved rating from the db, which I'm trying to do with rating2, as u can see the value is fetched but the directive is not appearing.
https://codepen.io/eldyvoon/pen/MbBNLP
<div star-rating ng-model="rating.rating1" max="10" on-rating-select="rating.rateFunction(rating)"></div>
<br>but rating2 is actually there:
{{rating.rating2}}
<star-rating ng-model="rating.rating2" readonly="rating.isReadonly"></star-rating>
Need expert of directive to help.
Initiate rating2 :
function RatingController($http) {
this.rating1 = 5;
this.rating2 = 0; //ADD THIS LINE
var self = this;
it works for me
check here
First of all, I'm not a directive expert but i'm trying to help. I think that when html is first load, the values from db not finish execute and bind into html. The best way is not using directive instead using controller to fetch data from db.
You pass a model without rating2 into your directive and the changes from the parent controller won't affect it, because variable is created afterwards. Adding a watcher in your linker on parent scope will solve the problem;
scope.$parent.$watch('', function(rating){
updateStars();
});
Other solution would be to define a starting value in your controller.
this.rating2 = 1;
Notice that it is bad design to have a scope variable for each rating. It is cleaner to have an array of ratings and you actually do not need the watcher by doing so.
https://codepen.io/hoschnok/pen/LbJPqL
angular controller
function RatingController($http) {
this.ratings = [4];
var self = this;
$http.get('https://api.myjson.com/bins/o0r69').then(function(res){
self.ratings.push(res.data.rating2);
});
}
HTML
<div ng-app="app" ng-controller="RatingController as rating" class="container">
<div ng-repeat="r in rating.ratings">
<div star-rating ng-model="r" max="10" on-rating-select="rating.rateFunction(rating)"></div>
</div>
</div>
The watcher change handler function has parameters reversed:
//INCORRECT parameters
//scope.$watch('ratingValue', function(oldValue, newValue) {
//CORRECT parameters
scope.$watch('ratingValue', function(newValue, oldValue) {
if (newValue) {
updateStars();
}
});
The first argument of the listening function should be newValue.
The DEMO on CodePen
ALSO
The ng- prefix is reserved for core directives. See AngularJS Wiki -- Best Practices
JS
scope: {
//Avoid using ng- prefix
//ratingValue: '=ngModel',
ratingValue: '=myModel',
max: '=?', // optional (default is 5)
onRatingSelect: '&?',
readonly: '=?'
},
HTML
<!-- AVOID using the ng- prefix
<star-rating ng-if='rating' ng-model="rating.rating2"
max="10" on-rating-select="rating.rateFunction(rating)">
</star-rating>
-->
<!-- INSTEAD -->
<star-rating ng-if='rating' my-model="rating.rating2"
max="10" on-rating-select="rating.rateFunction(rating)">
</star-rating>
When a custom directve uses the name ng-model for an attribute, the AngularJS framework instantiates an ngModelController. If the directive doesn't use the services of that controller, it is best not to instantiate it.

How to invoke a JQuery method after Angular select directive applies a model change?

I am trying to change the selection of David Stutz's bootstrap-multiselect via Angular ng-model:
<select ng-model="selection" multiple="multiple" id="my-example">
<option value="cheese">Cheese</option>
<option value="tomatoes">Tomatoes</option>
<option value="mozarella">Mozzarella</option>
<option value="mushrooms">Mushrooms</option>
<option value="pepperoni">Pepperoni</option>
<option value="onions">Onions</option>
</select>
The changes to the model only get applied to the underlying select element, but the bootstrap-multiselect doesn't get updated automatically. Looking at its documentation, this is expected: you are required to call multiselect('refresh') afterwards to propagate the changes:
$('#my-example').multiselect('refresh');
My question is:
How to invoke this method when the model changes after Angular is done updating the select element?
Since I need to access the element, I assume directives are the way to go. I was looking at decorators, which in theory I could use to modify the behavior of the built-in select directive, but I don't know how to get my code invoked at the right moment.
I've prepared a plunk to demo the issue:
I have two multiselects bound to the same model: a bootstrap-multiselect and a plain one
I initialize both with a default selection
The first button changes the selection. The plain multiselect is updated immediatelly, but the bootstrap-multiselect appears unchanged.
The second button shows the current model value in an alert.
The third button calls refresh on bootstrap-multiselect, which causes it to update. This is what I would like to get called automatically by Angular.
In the end I managed to solve my problem with a decorator. I based it on the Directive Decorator Example in AngularJS documentation.
Unlike ngHrefDirective from the example, selectDirective defines both preLink and postLink, therefore the compile override must also return both. I only needed to change postLink though, where $render is defined. In my version of the method I simply invoked the original method, which updates the select element, and called multiselect('refresh') afterwards, which was my original requirement:
app.config(['$provide', function($provide) {
$provide.decorator('selectDirective', ['$delegate', function($delegate) {
var directive = $delegate[0];
directive.compile = function() {
function post(scope, element, attrs, ctrls) {
directive.link.post.apply(this, arguments);
var ngModelController = ctrls[1];
if (ngModelController) {
originalRender = ngModelController.$render;
ngModelController.$render = function() {
originalRender();
element.multiselect('refresh');
};
}
}
return {
pre: directive.link.pre,
post: post
};
};
return $delegate;
}]);
}]);
I dont think you should mix that (why refresh data with jquery while the data is in angular's watch cycle?). You can do something like this with your options:
<option ng-value="Cheese.val">{{Cheese.text}}</option>
<option ng-value="Tomatoes.val">{{Tomatoes.text}}</option>
And handle the rest with Angular (maybe google for angular + multiselect)

angular directive (2-way-data-binding) - parent is not updated via ng-click

I have a nested directive with an isolated scope. An Array of objects is bound to it via 2 way data binding.
.directive('mapMarkerInput',['mapmarkerService', '$filter', '$timeout',function(mapMarkerService, $filter, $timeout) {
return {
restrict: 'EA',
templateUrl:'templates/mapmarkerInputView.html',
replace: true,
scope: {
mapmarkers: '='
},
link: function($scope, element, attrs) {
//some other code
$scope.addMapmarker = function($event) {
var mapmarker = {};
var offsetLeft = $($event.currentTarget).offset().left,
offsetTop = $($event.currentTarget).offset().top;
mapmarker.y_coord = $event.pageY - offsetTop;
mapmarker.x_coord = $event.pageX - offsetLeft;
mapmarker.map = $scope.currentMap;
$scope.mapmarkers = $scope.mapmarkers.concat(mapmarker);
};
$scope.deleteMapmarker = function(mapmarker) {
var index = $scope.mapmarkers.indexOf(mapmarker);
if(index !== -1) {
$scope.mapmarkers.splice(index,1);
}
};
//some other code
)
}]);
These 2 functions are triggered via ng-click:
<img ng-if="currentMap" ng-click="addMapmarker($event)" ng-src="/xenobladex/attachment/{{currentMap.attachment.id}}" />
<div class="mapmarker-wrapper" ng-repeat="mapmarker in shownMapmarkers" ng-click="setZIndex($event)" style="position: absolute; top: {{mapmarker.y_coord}}px; left: {{mapmarker.x_coord}}px;">
<!-- some other code -->
<div class="form-group">
<label>Name:</label>
<input ng-model="mapmarker.name" value="mapmarker.name" class="form-control" type="text">
</div>
<div class="form-group">
<label>Description:</label>
<input ng-model="mapmarker.description" value="mapmarker.description" class="form-control" type="text">
</div>
<button class="btn btn-danger" ng-click="deleteMapmarker(mapmarker)">Delete</button>
</div>
As you can see I am binding the name and description directly via ng-model and that works just fine. The properties are also available in the parent scope, but neither the delete nor the add works (its changed within the directives scope, but not the parent scope).
As far as I understand these changes should be applied, because I'm calling these functions via ng-click and I have other examples where this works. The only difference is, that I am binding to an array of objects and not a single object / property.
I tried using $timer and updateParent() ($scope.$apply() does not work -> throws an exception that the function is already within the digest cycle) but with no success, so it looks like these changes are not watched at all.
The directive code looks like this:
<map-marker-input ng-if="$parent.formFieldBind" mapmarkers="$parent.formFieldBind"></map-marker-input>
It is nested within a custom form field directive which gets the correct form field template dynamically and has therefore template: '<div ng-include="getTemplate()"></div>' as template, which creates a new child scope - that's why the $parent is needed here.
The binding definitely works in one way, the expected data is available within the directive and if I'm logging the data after changing it via delete or add, it's also correct, but only from the inside of the directive.
Because ng-model works I guess there might be a simple solution to the problem.
UPDATE
I created a plunkr with a simplified version:
http://plnkr.co/85oNM3ECFgCzyrSPahIr
Just click anywhere inside the blue area and new points are added from within the mapmarker directive. Right now I dont really prevent adding points if you delete or edit these - so you'll end up with a lot of points fast ;-)
There is a button to show the data from the parent scope and from the child scope.
If you edit the name or description of the one existing point that will also be changed in the parent scope (bound via ng-model). But all new points or deletions are ignored (bound within the functions called via ng-click).
If you want to update the parent scope, you need to access it via $parent once more,
i change
mapmarkers="$parent.formFieldBind"
to :
mapmarkers="$parent.$parent.formFieldBind"
ng-include create one more scope, so you need to access the parent once more.
http://plnkr.co/edit/27qF6ABUxIum8A3Hrvmt?p=preview

How to create this custom control with AngularJS directive?

I'm a bit new to AngularJS and am trying to write a custom select control based on Zurb Foundation's custom select(see here: http://foundation.zurb.com/docs/components/custom-forms.html)
I know I need to use a directive for this but am not sure how to accomplish this.
It's going to have to be reusable and allow for the iterating of whatever array is passed in to it. A callback when the user selects the item from the dropdown list is probably needed.
Here is the markup for the custom Foundation dropdown list:
<select name="selectedUIC" style="display:none;"></select>
<div class="custom dropdown medium" style="background-color:red;">
Please select item
<ul ng-repeat="uic in uics">
<li class="custom-select" ng-click="selectUIC(uic.Name)">{{uic.Name}}</li>
</ul>
</div>
This works for now. I am able to populate the control from this page's Ctrl. However, as you can see, I'd have to do this every time I wanted to use a custom dropdown control.
Any ideas as to how I can turn this baby into a reusable directive?
Thanks for any help!
Chris
If you want to make your directives reusable not just on the same page, but across multiple AngularJS apps, then it's pretty handy to set them up in their own module and import that module as a dependency in your app.
I took Cuong Vo's plnkr above (so initial credit goes to him) and separated it out with this approach. Now this means that if you want to create a new directive, simply add it to reusableDirectives.js and all apps that already have ['reusableDirectives'] as a dependency, will be able to use that new directive without needing to add any extra js to that particular app.
I also moved the markup for the directive into it's own html template, as it's much easy to read, edit and maintain than having it directly inside the directive as a string.
Plnkr Demo
html
<zurb-select data-label="{{'Select an option'}}" data-options="names"
data-change-callback="callback(value)"></zurb-select>
app.js
// Add reusableDirectives as a dependency in your app
angular.module('angularjs-starter', ['reusableDirectives'])
.controller('MainCtrl', ['$scope', function($scope) {
$scope.names = [{name: 'Gavin'}, {name: 'Joseph'}, {name: 'Ken'}];
$scope.callback = function(name) {
alert(name);
};
}]);
reusableDirectives.js
angular.module('reusableDirectives', [])
.directive('zurbSelect', [function(){
return {
scope: {
label: '#', // optional
changeCallback: '&',
options: '='
},
restrict: 'E',
replace: true, // optional
templateUrl: 'zurb-select.html',
link: function(scope, element, attr) { }
};
}]);
zurb-select.html
<div class="row">
<div class="large-12 columns">
<label>{{label || 'Please select'}}</label>
<select data-ng-model="zurbOptions.name" data-ng-change="changeCallback({value: zurbOptions.name})"
data-ng-options="o.name as o.name for o in options">
</select>
</div>
</div>
Is something like this what you're looking for?
http://plnkr.co/edit/wUHmLP
In the above example you can pass in two attribute parameters to your custom zurbSelect directive. Options is a list of select option objects with a name attribute and clickCallback is the function available on the controller's scope that you want the directive to invoke when a user clicks on a section.
Notice there's no code in the link function (this is where the logic for your directive would generally go). All we're doing is wrapping a template so that it's reusable and accepts some parameters.
We created an isolated scope so the directive doesn't need to depend on parent scopes. We binded the isolated scope to the attribute parameters passed in. The '&' means bind to the expression on the parent scope calling this (in our case the callback function available in our controller) and the '=' means create a two way binding between the options attribute so when it changes in the outter scope, the change is reflected here and vice versa.
We're also restricting the usage of this directive to only elements (). You can set this to class, attributes, etc..
For more details the AngularJs directives guide is really good:
http://docs.angularjs.org/guide/directive
Hope this helps.

Resources