Why is my $watch not getting called? - angularjs

I just don't understand why my $watch is NOT called a second time with the updated value of "entity".
.directive('myParent', function() {
return {
controller: function($scope, $log) {
$scope.$on('thingChanged', function(e, newVal) {
$log.log('thingChanged event hit...');
$scope.$apply(function() {
$scope.thing = newVal;
});
});
},
template: '<div><div my-child entity="thing" v="{{thing.awesome}}"></div><div my-child2></div></div>'
};
})
.directive('myChild', function($parse, $log) {
return {
controller: function($scope) {
},
scope: {
entity: '=',
v: '#'
},
link: function(scope, element, attrs) {
$log.log('v in link:');
$log.log(scope.v);
attrs.$observe('v', function(v) {
$log.log('v in observe in link:');
$log.log(v);
})
$log.log('entity in link:');
$log.log(scope.entity);
scope.$watch(scope.entity, function(nv, ov) {
$log.log('$watch detected change: ');
$log.log(scope.entity);
});
},
template: '<span>{{v}}</span>-<span>{{entity}}</span>'
};
})
.directive('myChild2', function($parse, $log, $timeout) {
return {
controller: function($scope) {
var count = 0;
$timeout(function() {
$scope.$emit('thingChanged', { awesome: 'cool'+count++ });
}, 2000);
},
scope: true,
link: function(scope, element, attrs) {
},
template: '<span><button ng-click="change()"</span>'
};
})
Plunker: http://plnkr.co/edit/2jfa1dcwguO400zRjJxU?p=preview

Watch should be the name of the property to be watched, try scope.$watch("entity", ..., if you want to deep watch (that is object properties) call it with true as the last parameter.
scope.$watch("entity", function() { ... }, true);

Related

angularjs nested cascading directive for dropdown

I want to create a cascading dropdown directive .
<my-dropdown label="states" url="http://statelisturl">
<my-dropdown label="cities" url="http://citylisturl">
</my-dropdown>
</my-dropdown>
But states will be list first, when I select a state cities will be get from url.
Is this possible for angularjs technically? Or I should be seperated directive for every dropdown?
You could do something like this:
.directive("myDropdown", function() {
return {
restrict: "E",
require: ["myDropdown", "?^^myDropdown"],
template: "<select ng-options='opt as opt.label for opt in $ctrl.options' ng-model='$ctrl.selectedOption' ng-change='$ctrl.changed($ctrl.selectedOption)'></select><div ng-transclude></div>",
scope: true,
bindToController: {
url: "#",
label: "#"
},
controller: function($scope, $http) {
var _self = this;
_self.init = function() {
$http.get(_self.url).then(function(response) {
_self.options = response.data;
});
}
_self.parentChanged = function(item) {
var id = item.id;
$http.get(_self.url + "?id=" + id).then(function(response) {
_self.options = response.data;
});
}
},
link: function(scope, element, attrs, ctrls) {
var ctrl = ctrls[0];
var parentCtrl = ctrls[1];
if (parentCtrl) {
scope.$watch(function() {
return parentCtrl.selectedOption;
}, function(newval) {
if (newval) {
ctrl.parentChanged(newval);
}
});
} else {
ctrl.init();
}
},
controllerAs: "$ctrl",
transclude: true
}
});
EDIT: I have a working example here

Cannot access DOM inside ngRepeat inside custom directive

In the following example (http://jsfiddle.net/akanieski/t4chbuwq/1/) I have a directive that has an ngRepeat inside it... during the link stage I cannot access html inside the ngRepeat. Only thing inside shade-element is <!-- ngRepeat i in items --> What am I doing wrong?
Controller:
module.controller("MainCtrl", function($scope, $timeout) {
$scope.currentPanel = 1;
$timeout(function(){
$scope.items = [1,2];
}, 1000)
$scope.loadPanelOne = function() {
if (!$scope.$$phase) $scope.$apply(function(){$scope.loadingPanelOne = true;});
$timeout(function(){
$scope.loadingPanelOne = false;
}, 2000);
}
$scope.loadPanelTwo = function() {
if (!$scope.$$phase) $scope.$apply(function(){$scope.loadingPanelTwo = true;});
$timeout(function(){
$scope.loadingPanelTwo = false;
}, 2000);
}
$scope.loadPanel3 = function() {
if (!$scope.$$phase) $scope.$apply(function(){$scope.loadingPanel3 = true;});
$timeout(function(){
$scope.loadingPanel3 = false;
}, 2000);
}
$scope.loadPanel4 = function() {
if (!$scope.$$phase) $scope.$apply(function(){$scope.loadingPanel4 = true;});
$timeout(function(){
$scope.loadingPanel4 = false;
}, 2000);
}
});
Directives:
var module = angular.module("myApp", []);
module.directive('shadeSet', ['$rootScope', '$timeout', function($rootScope, $timeout) {
return {
restrict: "E",
scope: {
"mode": "=",
"currentPanel": "="
},
controller: function() {
},
link: function(scope, element, attributes) {
$rootScope.$on('panelOpening', function(ev, args) {
if (scope.mode === 'exclusive') {
element
.find('shade-panel')
.addClass('hide')
$('[id="' + args.id + '"]',element).removeClass('hide');
} else if (args.state === 'open') {
$('[id="' + args.id + '"]',element).removeClass('hide');
} else if (args.state === 'close') {
$('[id="' + args.id + '"]',element).addClass('hide');
} else {
$('[id="' + args.id + '"]',element).toggleClass('hide');
}
$rootScope.$broadcast('panelOpened', {id: args.id, state: 'open'});
})
if (scope.currentPanel) {
$rootScope.$broadcast('panelOpening', {id: scope.currentPanel, state: 'open'});
}
}
}
}])
module.directive('shadePanel', ['$rootScope', function($rootScope) {
return {
restrict: "E",
require: '^shadeSet',
scope: {
"id": "=",
"onOpen": "&",
"scrollOnOpen": "="
},
link: function(scope, element, attributes) {
$rootScope.$on('panelOpened', function(ev, args){
if (args.id === scope.id && scope.onOpen) scope.onOpen();
if (scope.scrollOnOpen) {
console.log("Scrolling to " + args.id)
$('html, body').animate({
scrollTop: $(element).offset().top
}, 500);
}
})
}
}
}])
module.directive('shadeToggle', ['$rootScope', function($rootScope) {
return {
restrict: "E",
require: '^shadeSet',
scope: {
target: "="
},
compile: function() {
return {
pre: function(scope, element, attributes) {
element.on('click', function() {
$rootScope.$emit('panelOpening', {id: scope.target});
});
scope.$on('$destroy', function() {
element.off('click');
});
}
};
}
}
}])
HTML
<div ng-controller="MainCtrl" ng-app="myApp">
<shade-set current-panel="currentPanel">
<div ng-repeat="i in items">
<shade-toggle target="1"><a href>Open One</a></shade-toggle>
<shade-panel id="1" class="hide" on-open="loadPanelOne()">
<span ng-show="loadingPanelOne">... Loading Panel 1 ...</span>
Hello World
</shade-panel>
</div>
</shade-set>
</div>

Unit Testing a watch method call in Directive link function

I have the following Directive
.directive("myContainer", function (myContainerService,$window) {
return {
restrict: 'A',
templateUrl: "myContainer/MyContainer.tpl.html",
controller: 'myCtrl',
controllerAs: 'myCtrl',
scope: {
items: '='
},
link: function (scope, element) {
var timeout;
scope.$watch('myCtrl.items', function (items) {
var windowWidth = $window.innerWidth - 65;
window.clearTimeout(timeout);
timeout = window.setTimeout(function () {
myContainerService.saveItems(items);
}, 1000);
}, true);
}
};
})
And here is the Unit Test i have.
describe("myCtrl", function(){
var myCtrl;
var dirEle ;
var myScope;
// var to store the Mock Items
var myContainerService = $injector.get('myContainerService');
var items = [..]
beforeEach(inject(function($compile, $httpBackend){
$httpBackend.whenGET(/.*my-app\/restful-services\/items*./).respond({...});
scope.items = myContainerService.getItems();
dirEle = $compile('<div my-container items="items"></div>')(scope);
scope.$digest();
myScope = dirEle.isolateScope();
myCtrl = myScope.myCtrl;
}));
fit("Saving Items", inject(function($timeout){
spyOn(myContainerService, 'saveItems');
//$timeout.flush();
myScope.$digest();
$timeout.flush();
expect(myContainerService.saveItems).toHaveBeenCalledWith(myCtrl.items);
}));
});
And my test is failing as the saveItems is not getting called at all. Not sure what i am doing wrong.
Appreciate any inputs.
Thanks
You need to be using angulars $timeout that way in your test your $timeout.flush() will work:
.directive("myContainer", function (myContainerService,$window, $timeout) {
return {
restrict: 'A',
templateUrl: "myContainer/MyContainer.tpl.html",
controller: 'myCtrl',
controllerAs: 'myCtrl',
scope: {
items: '='
},
link: function (scope, element) {
var timeout;
scope.$watch('myCtrl.items', function (items) {
var windowWidth = $window.innerWidth - 65;
$timeout.cancel(timeout);
timeout = $timeout(function () {
myContainerService.saveItems(items);
}, 1000);
}, true);
}
};
})

Angular directive not rendering scope values in view

My directive uses a service which returns a promise, I need to display the scope attributes geoZip, geoCity and geoState in the template.
The issue is that those scope variables are not being shown in the template, I just see the comma.
What should I do to make it display the scope variables?
This is my directive code:
.directive('cityStateZip', function() {
return {
restrict: 'A',
transclude: true,
scope: {
zip: '=',
},
template: '<p>{{geoCity}}, {{geoState}} {{geoZip}}</p>',
controller: ['$scope', 'GeolocationService', function ($scope, GeolocationService) {
GeolocationService.geocode($scope.zip).then(function(result) {
if (result) {
console.log(result);
$scope.geoZip = result['address_components'][0]['long_name'];
$scope.geoCity = result['address_components'][1]['long_name'];
$scope.geoState = result['address_components'][2]['short_name'];
}
});
}]
};
})
.service('GeolocationService', ['underscore', '$q', function (underscore, $q) {
var gs = {};
gs.geocode = function(address) {
var geocoder = new google.maps.Geocoder();
var deferred = $q.defer();
geocoder.geocode( { "address": address }, function(results, status) {
if (status == google.maps.GeocoderStatus.OK && results.length > 0) {
return deferred.resolve(underscore.first(results));
}
return deferred.reject();
});
return deferred.promise;
}
return gs;
}]);
I found that I have to use the $timeout service to make it work:
.directive('cityStateZip', function() {
return {
restrict: 'A',
transclude: true,
scope: {
zip: '=',
},
template: '<p>{{geoCity}}, {{geoState}} {{geoZip}}</p>',
controller: ['$scope', '$timeout', 'GeolocationService', function ($scope, $timeout, GeolocationService) {
GeolocationService.geocode($scope.zip).then(function(result) {
if (result) {
console.log(result);
$timeout(function() {
$scope.geoZip = result['address_components'][0]['long_name'];
$scope.geoCity = result['address_components'][1]['long_name'];
$scope.geoState = result['address_components'][2]['short_name'];
});
}
});
}]
};
})
Please let me know if there is best alternative (not using $timeout), Thanks!

Error in LInking service and directive in Angular

I have an issue at linking service having http to a directive. This is the code of that...
myapp.factory ( 'autoCompleteDataService', ['$http', function($http) {
return {
getSource: function(callback) {
var url = 'get_data_from_server.php';
$http.get(url).success(function(data) {
callback(data);
});
}
}
} ] );
myapp.directive('autoComplete', function(autoCompleteDataService) {
return {
restrict: 'A',
link: function(scope, elem, attr, ctrl) {
elem.autocomplete({
source: autoCompleteDataService.getSource(),
select: function( event, ui ) {
scope.$apply(function() {
scope.item.unit_id = ui.item.value;
});
},
minLength: 2
});
}
};
});
I replaced callback(data); with return data; with no result...
Any help is appreciated..
Edit: added working code without http
If I keep this code instead the above one its working
myapp.factory('autoCompleteDataService', [function() {
return {
getSource: function() {
return ['apples', 'oranges', 'bananas'];
}
}
}]);
Another way to answer the issue,, with full filtering logic at server side..
myapp.directive('autoComplete', function(autoCompleteDataService) {
return {
restrict: 'A',
link: function(scope, elem, attr, ctrl) {
elem.autocomplete({
source: function( request, response ) {
$.ajax({
url: "./get_data_from_server.php",
dataType: "json",
data: {
maxRows: 10,
startsWith: request.term
},
success: function( data ) {
response(data);
}
});
},
select: function( event, ui ) {
scope.$apply(function() { scope.item.unit_id = ui.item.value; });
},
minLength: 2
});
}
};
});
myapp.factory('autoCompleteDataService', ['$http', function($http) {
return {
getSource: function() {
var url = 'get_data_from_server.php';
return $http.get(url);
}
}
}]);
Directive:
link : function(scope, elem, attr, ctrl) {
autoCompleteDataService.getSource().success(function(data) {
elem.autocomplete({
source: data,
select: function( event, ui ) {
scope.$apply(function() { scope.item.unit_id = ui.item.value; });
},
change: function (event, ui) {
if (ui.item === null) {
scope.$apply(function() { scope.foo = null });
}
},
minLength: 2
});
});
}

Resources