AngularJS object field not updating from child directive - javascript

I have a parent controller A, who's scope contains an object representing its state. So I can access state.status from A's scope to update its status.
I have a child directive, B, who has a binding to a field in A's status object. This allows me to update A's state from B.
Here's the simplified code for my controller A:
angular.module("myApp").controller('A', function($scope){
$scope.state = {
"status": "All"
};
function doSomething() {
console.log(state.status);
}
});
And for the directive B:
angular.module('myApp').directive('B', function () {
return {
templateUrl: 'B.html',
scope: {
selectedStatus: '=status',
onChange: '=onChange'
},
link: function (scope, element, attrs) {
scope.setStatus = function(status) {
scope.selectedStatus = status;
scope.onChange();
};
}
}
});
In the DOM:
<B status="state.status" on-change="doSomething" />
When I call setStatus() from within my directive B, I expect the changes I've made to the object to be propagated by the time doSomething() is called. However, the console.log() output is of the old value, not what I've just updated it to. The change is made, just not fast enough.
I've tried to call $scope.$apply() however it complains that it's within a $scope.$apply() call already. :(
What's the best way to deal with this?

Methinks you are a bit confused about how this works.
When you do something like status="state.status" and change status inside a directive, changes will be applied only after executing the digest cycle, but in your case you try to output state.status before finishing the digest, so it doesn't use the synchronized value.
There are a few ways to solve this:
call scope.onChange inside $timeout
pass full object state instead state.status
use scope.$parent for access to parent scope where you define state object
angular.module("myApp", []).directive('bb', function($timeout) {
return {
template: '<div><div ng-click="setStatus1(\'status1\')">1. Selected status: {{selectedStatus}}</div><div ng-click="setStatus2(\'status2\')">2. Selected status: {{selectedStatus}}</div><div ng-click="setStatus3(\'status3\')">3. Selected status: {{selectedStatus}}</div></div>',
scope: {
selectedStatus: '=status',
onChange: '=onChange',
state: '='
},
link: function(scope, element, attrs) {
scope.setStatus1 = function(status) {
console.log('setstatus1');
scope.selectedStatus = status;
$timeout(function() {
scope.onChange();
});
};
scope.setStatus2 = function(status) {
console.log('setstatus2');
scope.selectedStatus = status;
scope.state.status = status;
scope.onChange();
};
scope.setStatus3 = function(status) {
console.log('setstatus3');
scope.selectedStatus = status;
scope.$parent.state.status = status;
scope.onChange();
};
}
}
}).controller('A', function($scope) {
$scope.state = {
"status": "All"
};
$scope.doSomething = doSomething;
function doSomething() {
console.log('doSomething: ', $scope.state.status);
}
});
<script data-require="angular.js#1.4.0-rc.1" data-semver="1.4.0-rc.1" src="https://code.angularjs.org/1.3.8/angular.js"></script>
<div ng-app="myApp" ng-controller="A">
<bb status="state.status" state="state" on-change="doSomething"></bb>
{{state}}
</div>

In parent directive instead of passing callback, you can use $watch.
angular.module("myApp").controller('A', function($scope){
$scope.state = {
"status": "All"
};
$scope.$watch('state', function(newVal, oldVal){
if(oldVal === newVal) return;
// Do something using newVal
}, true);
});

Related

Call function in Directive when Parent Scope Variable Changes

I need to call a function in my directive when the value of variable in the parent controller changes. I tried adding a watch (I'm obviously doing it wrong) because nothing happens when the value changes. Here is the directive:
angular.module('ssq.shared').directive('checkboxPicklist', function() {
return {
restrict: 'E',
templateUrl: '/Scripts/app/Shared/directives/checkboxPicklist.html',
replace: true,
scope: {
itemId: '=',
list: '=',
nameProp: '=',
title: '#',
searchPlaceholder: '#',
callbackFn: '&',
callMore: '&',
clear: '='
},
link: function (scope, element, attrs) {
scope.query = '';
var parent = scope.$parent;
var clear = parent.clear;
scope.$watch(clear, function () {
if (clear == true) {
this.clearAll();
}
})
var child = element.find('.dropdown-menu');
child.on({
'click': function (e) {
e.stopPropagation();
}
});
var selectedItemFn = function (item) {
return item.selected;
};
scope.getSelectedCount = function () {
return _.filter(scope.list, selectedItemFn).length;
};
scope.loadMore = function () {
scope.callMore();
};
scope.allSelected = function(list) {
var newValue = !scope.allNeedsMet(list);
_.each(list, function(item) {
item.selected = newValue;
scope.callbackFn({ object: item });
});
};
scope.allNeedsMet = function(list) {
var needsMet = _.reduce(list, function(memo, item) {
return memo + (item.selected ? 1 : 0);
}, 0);
if (!list) {
return (needsMet === 0);
}
return (needsMet === list.length);
};
function clearAll() {
_.each(list, function (item) {
item.selected = false;
})
}
}
};
});
Here is where I am trying to watch the variable:
var parent = scope.$parent;
var clear = parent.clear;
scope.$watch(clear, function () {
if (clear == true) {
this.clearAll();
}
})
Here is the function in my parent controller that changes the value of "clear"
$scope.clearFilters = function (clear) {
$scope.clear = true;
$scope.form.selected.services = [];
$scope.form.picked.areas = [];
$scope.form.certified.verifications = [];
$scope.form.subscribed.subscriptions = [];
$scope.form.OperatorBusinessUnitID = null;
$scope.form.OperatorBusinessUnitID = null;
};
I tried setting an attribute called "clearFilter" and assigning the variable to it, but the watch still doesn't trigger:
scope.$watch(attrs.clearFilter, function (value) {
if (value == true) {
this.clearAll();
}
});
<checkbox-picklist data-item-id="'servicesPicklist'"
data-search-placeholder="Search Services"
data-list="services"
data-title="Service(s)"
data-name-prop="'vchDescription'"
data-callback-fn="addService(object)"
call-more="loadMoreServices()"
clear-filter="clear">
</checkbox-picklist>
I'm not really sure if I am calling the function correctly. scope.$parent above does get the initial value of the variable from the parent scope, but once it changes, it never updates.
EDIT:What I have discovered is the normal scope.$watch('clear', function...) is not working it seems because the directive is in "ssq.shared" module which is injected in my my Main Module "myModule" (see below), so even though the page the directive is on uses my 'GeneralSearchCtrl', I cannot get the watch to work on the variable located in 'GeneralSearchCtrl'. If I use scope.$parent.clear I can see the value of the variable, but I cannot seem to set a watch on it.
My module injection code:
var app = angular.module('myModule', ['ui.bootstrap', 'checklist-model', 'ssq.shared', 'ngAnimate', 'ngTouch', 'ui.grid', 'ui.grid.pagination', 'ui.grid.selection', 'ui.grid.exporter', 'ui.grid.autoResize', 'ui.router', 'cgBusy', 'ui.mask', 'ngFileUpload', 'ngSanitize']);
The page where the directive lives uses:
<div ng-app="myModule" ng-controller="GeneralSearchCtrl">
I am unable to get a watch on the variable located in GeneralSearchCtrl.
Any assistance is greatly appreciated!!!!
Add a watch for the $scope value and call the function,
scope.$watch('clear', function(newValue, oldValue) {
if (newValue) {
this.clearAll();
}
});
scope.$watch(clear, function () {
if (clear == true) {
this.clearAll();
}
})
This.clearAll() doesn't exist in the scope of your $watch function. Simply calling clearAll() should work better.
The signature of the watch function is not correct.
scope.$watch('clear', function (new, old) {}
As it turns out, the problem was that the directive had scope:{...} in its definition which stopped the "normal" scope.$watch('clear', function...) from working. I had to add clear: '=' to the scope list like so:
replace: true,
scope: {
itemId: '=',
list: '=',
nameProp: '=',
title: '#',
searchPlaceholder: '#',
callbackFn: '&',
callMore: '&',
clear: '='
},
Then clear="clear" to the directive like so:
<checkbox-picklist data-item-id="'servicesPicklist'"
data-search-placeholder="Search Services"
data-list="services"
data-title="Service(s)"
data-name-prop="'vchDescription'"
data-callback-fn="addService(object)"
call-more="loadMoreServices()"
clear="clear">
</checkbox-picklist>
Then in the directive I had to add the watch like this for it work:
scope.$watch('$parent.clear', function (newValue, oldValue) {
if (newValue == true) {
clearAll();
alert('it works!');
}
})
I really hope this helps someone else as this was difficult for me to figure out. Happy coding!

Dynamic controller for directives with ECMA6

I'm trying to set a controller dynamically to my directive using the name property. So far this is my code.
html
<view-edit controller-name="vm.controller" view="home/views/med.search.results.detail.resources.audios.html" edit="home/views/med.media.resources.edit.html"></view-edit>
js
export default class SearchResultsCtrl extends Pageable {
/*#ngInject*/
constructor($injector, $state, api) {
super(
{
injector: $injector,
endpoint: 'mediaMaterialsList',
selectable:{
itemKey: 'cid',
enabled:true,
params: $state.params
},
executeGet: false
}
);
this.controller = SearchResultsResourcesAudiosCtrl;
}
}
Directive
export default class ViewEditDirective {
constructor() {
this.restrict = 'E';
this.replace = true;
this.templateUrl = 'home/views/med.view.edit.html';
this.scope = {};
this.controller = "#";
this.name = "controllerName";
this.bindToController = {
'view': '#?',
'edit': '#?'
};
this.open = false;
this.controllerAs = 'ctrl';
}
}
I get undefined for vm.controller. I guess that it's rendering before the controller can assign the controller to the variable (I debbuged it, and it's setting the controller in the variable).
I'm following this answer to achieve this, but no luck so far.
How to set the dynamic controller for directives?
Thanks.
The problem is not related to ES6 (which is a sugar syntax coating over ES5), this is how Angular scope life cycle works.
This directive may show what's the deal with attribute interpolation
// <div ng-init="a = 1"><div sum="{{ a + 1 }}"></div></div>
app.directive('sum', function () {
return {
scope: {},
controller: function ($attrs) {
console.log($attrs.sum) // {{ a + 1 }}
// ...
},
link: function (scope, element, attrs) {
console.log(attrs.sum) // 2
}
};
});
And $attrs.sum may still not be 2 in link if a value was set after that (i.e. in parent directive link).
It is unsafe (and wrong per se) to assume that the value on one scope can be calculated based on the value from another scopes at some point of time. Because it may be not. That is why watchers and data binding are there.
All that controller: '#' magic value does is getting uninterpolated attribute value and using it as controller name. So no, it won't interpolate controller name from vm.controller and will use 'vm.controller' string as controller name.
An example of a directive that allows to set its controller dynamically may look like
// dynamic-controller="{{ ctrlNameVariable }}"
app.directive('dynamicController', function () {
return {
restrict: 'A',
priority: 2500,
controller: function ($scope, $element, $attrs, $interpolate, $compile) {
var ctrlName = $interpolate($attrs.dynamicController)($scope);
setController(ctrlName);
$attrs.$observe('dynamicController', setController);
function setController (ctrlName) {
if (!ctrlName || $attrs.ngController === ctrlName) {
return;
}
$attrs.$set('ngController', ctrlName);
$compile($element)($scope);
}
}
};
});
with all the side-effects that re-compilation may bring.

angular.js directive two-way-binding scope updating

I wanted to use a directive to have some click-to-edit functionality in my front end.
This is the directive I am using for that: http://icelab.com.au/articles/levelling-up-with-angularjs-building-a-reusable-click-to-edit-directive/
'use strict';
angular.module('jayMapApp')
.directive('clickToEdit', function () {
return {
templateUrl: 'directives/clickToEdit/clickToEdit.html',
restrict: 'A',
replace: true,
scope: {
value: '=clickToEdit',
method: '&onSave'
},
controller: function($scope, $attrs) {
$scope.view = {
editableValue: $scope.value,
editorEnabled: false
};
$scope.enableEditor = function() {
$scope.view.editorEnabled = true;
$scope.view.editableValue = $scope.value;
};
$scope.disableEditor = function() {
$scope.view.editorEnabled = false;
};
$scope.save = function() {
$scope.value = $scope.view.editableValue;
$scope.disableEditor();
$scope.method();
};
}
};
});
I added a second attribute to the directive to call a method after when the user changed the value and then update the database etc. The method (´$onSave´ here) is called fine, but it seems the parent scope is not yet updated when I call the method at the end of the directive.
Is there a way to call the method but have the parent scope updated for sure?
Thanks in advance,
Michael
I believe you are supposed to create the functions to attach inside the linking function:
Take a look at this code:
http://plnkr.co/edit/ZTx0xrOoQF3i93buJ279?p=preview
app.directive('clickToEdit', function () {
return {
templateUrl: 'clickToEdit.html',
restrict: 'A',
replace: true,
scope: {
value: '=clickToEdit',
method: '&onSave'
},
link: function(scope, element, attrs){
scope.save = function(){
console.log('save in link fired');
}
},
controller: function($scope, $attrs) {
$scope.view = {
editableValue: $scope.value,
editorEnabled: false
};
$scope.enableEditor = function() {
$scope.view.editorEnabled = true;
$scope.view.editableValue = $scope.value;
};
$scope.disableEditor = function() {
$scope.view.editorEnabled = false;
};
$scope.save = function() {
console.log('save in controller fired');
$scope.value = $scope.view.editableValue;
$scope.disableEditor();
$scope.method();
};
}
};
});
I haven't declared the functions inside the controller before, but I don't see why it wouldn't work.
Though this question/answer explain it Link vs compile vs controller
From my understanding:
The controller is used to share data between directive instances, not to "link" functions which would be run as callbacks.
The method is being called but angular doesn't realise it needs to run the digest cycle to update the controller scope. Luckily you can still trigger the digest from inside your isolate scope just wrap the call to the method:
$scope.$apply($scope.method());

AngularJS directive scope not updating when value changed outside of AngularJS

I am getting started with AngularJS and have a noob problem that I am not sure how to resolve. I am modifying a value outside of angular (I have put it in the .run section only for demonstration purposes), and then attempting to run $apply so that Angular will notice that the scope needs to be updated.
However, in the following code, the {{currentState}} value gets set to "Initial value" and does not ever update to "Second value".
What is the correct approach to get the value to update?
angular.module("exampleApp", [])
.run(function(userNotificationService) {
userNotificationService.setStatus("Initial value");
setTimeout(function() {
userNotificationService.setStatus("Second value");
}, 1000);
})
.factory('userNotificationService', function($rootScope) {
var currentState = 'Unknown state'; // this should never be displayed
return {
setStatus: function(state) {
$rootScope.$apply(function() {
currentState = state;
});
},
getStatus: function() {
return currentState;
}
};
}).directive('currentState', function(userNotificationService) {
return {
restrict: 'AE',
scope: false, // set to false so that directive scope is used for transcluded expressions
link: function(scope) {
scope.currentState = userNotificationService.getStatus();
}
};
}).controller("defaultCtrl", function ($scope) {
// does nothing
});
And the html is the following:
<body ng-controller="defaultCtrl">
<div current-state>
current state: {{ currentState }}
</div>
</body>
If your use-case involves a timer, then Angular provides its own timer service called $interval which wraps the call in a scope.$apply for you. You should use that instead of setTimeout.
Now in this case, since you need a one way binding between a service and a value in your scope, you can set up a $watch in your directive:
.directive('currentState', function(userNotificationService) {
return {
restrict: 'AE',
scope: false, // set to false so that directive scope is used for transcluded expressions
link: function(scope) {
scope.$watch(function () { return userNotificationService.getStatus(); }, function (newVal) {
scope.currentState = userNotificationService.getStatus();
});
}
};
Ideally how you would do it is by creating this one way (or two way) binding in your controller (which you have left empty). The $scope you define on the controller will be available to the directive (if you set $scope: false or $scope: true), and then you can leave the link function empty.

Angular Directive refresh on parameter change

I have an angular directive which is initialized like so:
<conversation style="height:300px" type="convo" type-id="{{some_prop}}"></conversation>
I'd like it to be smart enough to refresh the directive when $scope.some_prop changes, as that implies it should show completely different content.
I have tested it as it is and nothing happens, the linking function doesn't even get called when $scope.some_prop changes. Is there a way to make this happen ?
Link function only gets called once, so it would not directly do what you are expecting. You need to use angular $watch to watch a model variable.
This watch needs to be setup in the link function.
If you use isolated scope for directive then the scope would be
scope :{typeId:'#' }
In your link function then you add a watch like
link: function(scope, element, attrs) {
scope.$watch("typeId",function(newValue,oldValue) {
//This gets called when data changes.
});
}
If you are not using isolated scope use watch on some_prop
What you're trying to do is to monitor the property of attribute in directive. You can watch the property of attribute changes using $observe() as follows:
angular.module('myApp').directive('conversation', function() {
return {
restrict: 'E',
replace: true,
compile: function(tElement, attr) {
attr.$observe('typeId', function(data) {
console.log("Updated data ", data);
}, true);
}
};
});
Keep in mind that I used the 'compile' function in the directive here because you haven't mentioned if you have any models and whether this is performance sensitive.
If you have models, you need to change the 'compile' function to 'link' or use 'controller' and to monitor the property of a model changes, you should use $watch(), and take of the angular {{}} brackets from the property, example:
<conversation style="height:300px" type="convo" type-id="some_prop"></conversation>
And in the directive:
angular.module('myApp').directive('conversation', function() {
return {
scope: {
typeId: '=',
},
link: function(scope, elm, attr) {
scope.$watch('typeId', function(newValue, oldValue) {
if (newValue !== oldValue) {
// You actions here
console.log("I got the new value! ", newValue);
}
}, true);
}
};
});
I hope this will help reloading/refreshing directive on value from parent scope
<html>
<head>
<!-- version 1.4.5 -->
<script src="angular.js"></script>
</head>
<body ng-app="app" ng-controller="Ctrl">
<my-test reload-on="update"></my-test><br>
<button ng-click="update = update+1;">update {{update}}</button>
</body>
<script>
var app = angular.module('app', [])
app.controller('Ctrl', function($scope) {
$scope.update = 0;
});
app.directive('myTest', function() {
return {
restrict: 'AE',
scope: {
reloadOn: '='
},
controller: function($scope) {
$scope.$watch('reloadOn', function(newVal, oldVal) {
// all directive code here
console.log("Reloaded successfully......" + $scope.reloadOn);
});
},
template: '<span> {{reloadOn}} </span>'
}
});
</script>
</html>
angular.module('app').directive('conversation', function() {
return {
restrict: 'E',
link: function ($scope, $elm, $attr) {
$scope.$watch("some_prop", function (newValue, oldValue) {
var typeId = $attr.type-id;
// Your logic.
});
}
};
}
If You're under AngularJS 1.5.3 or newer, You should consider to move to components instead of directives.
Those works very similar to directives but with some very useful additional feautures, such as $onChanges(changesObj), one of the lifecycle hook, that will be called whenever one-way bindings are updated.
app.component('conversation ', {
bindings: {
type: '#',
typeId: '='
},
controller: function() {
this.$onChanges = function(changes) {
// check if your specific property has changed
// that because $onChanges is fired whenever each property is changed from you parent ctrl
if(!!changes.typeId){
refreshYourComponent();
}
};
},
templateUrl: 'conversation .html'
});
Here's the docs for deepen into components.

Categories

Resources