Custom directive hidden but code still runs - angularjs

I have a custom directive which is sometimes hidden using ng-hide:
<my-custom-directive ng-show="vm.showBox"
value="vm.objects"
></my-custom-directive>
A snippet from my custom directive code:
function myCustomDirective() {
var directive = {
controller: controller,
controllerAs: 'vm',
///...
scope: {
value: '='
}
};
return directive;
function controller($scope) {
var vm = this;
///...
$scope.value.dates = $scope.value.dates || [];
}
}
The problem: even when the directive isn't supposed to be loaded/displayed (because vm.showBox is false), the custom directive's controller code runs and in such case it fails because $scope.value isn't passed (it's undefined there).
Why does the directive's controller code runs anyway, if the directive is hidden? I want to assume that if the directive is used, it's given valid parameters without having to check whether $scope.value is defined.

ng-show controls visibility of element (change of css property display) but element still exists in DOM. to remove/create element you can use ng-if instead of ng-show

ng-show only controls display property of element. It does not prevent element to render in DOM. To prevent it you can use ng-if.
But the problem with ng-if is that according to condition every it destroy scope and creates new one.
To prevent this problem you should use ng-show.
In your case you can use watch expressin in your directive.
<my-custom-directive ng-show="vm.showBox"
value="vm.objects"
></my-custom-directive>
function myCustomDirective() {
var directive = {
controller: controller,
controllerAs: 'vm',
///...
scope: {
value: '='
}
};
return directive;
function controller($scope) {
var vm = this;
///...
var watchExpression = $scope.$watch('value', function(newValue, oldValue){
if(newValue)
{
$scope.value.dates = $scope.value.dates || [];
watchExpression(); //de-register watch expression
}
});
}
}

Related

Two way data binding with directive doesn't work

I have a controller used to add tasks. On that page a user needs to select a group to act upon. I have written a directive that is used to allow a user to pick groups (folders)
My page controller
function AddTaskController($scope) {
var vm = this;
vm.group = { whatsit: true };
$scope.$watch("vm.group", function () {
console.log("controller watch", vm.group);
},true);
}
The page html where the directive is used
<em-group-selection group="vm.group"></em-group-selection>
The directive configuration
function GroupSelectionDirective() {
return {
scope: {
group: '='
},
controller: GroupSelectionDirectiveController,
controllerAs: 'vm',
templateUrl: '/views/templates/common/folderselection.html'
};
}
The directive controller:
function GroupSelectionDirectiveController($scope) {
var vm = this;
$scope.$watch("group", function () { console.log("yo1", vm.group); }, true)
$scope.$watch("vm.group", function () { console.log("yo2", vm.group); }, true)
}
Now when this fires, both console.log() calls in the directive fire once, with undefined. They never fire again. If in the controller I set vm.group to something else the $watch in the AddTaskController never gets fired.
Why isnt the data binding working?
Update:
I notice that if, in the directive, I change the init() function in my directive to use $scope it works! Can I not, as Fedaykin suggests, use controllerAs with two way data binding?
function init() {
$timeout(function () {
$scope.group.shizzy = 'timeout hit';
}, 200);
}
Turns out that if you use isolate scopes and controlelrAs syntax you need to also use bindToController : true. Without this you will not be able to only use vm and will have to use $scope for the isolate scope variables
More information can be found in the John Papa style guide and this SO answer
The final directive setup is as so:
function GroupSelectionDirective() {
return {
scope: {
group: '='
},
controller: GroupSelectionDirectiveController,
controllerAs: 'vm',
bindToController: true,
templateUrl: '/views/templates/common/folderselection.html'
};
}

AngularJS directive, ControllerAs, scope and vm property

Using Angular I created a directive like this:
angular
.module('my-module', [])
.directive('myDirective', function () {
return {
restrict: 'E',
templateUrl: currentScriptPath.replace('.js', '.html'),
scope: {
scenarios: '='
},
controller: MyDirectiveController,
controllerAs: 'vm',
bindToController: true,
replace: true
}
});
MyDirectiveController:
MyDirectiveController.$inject = ['$scope'];
function MyDirectiveController($scope) {
var vm = this;
vm.scenarios = $scope.scenarios;
}
My directive HTML template is this:
<div>{{vm.scenarios[0].name}}</div>
In my parent view HTML I'm using the directive this way:
<my-directive scenarios="vm.scenarios"></my-directive>
The parent controller has a property:
vm.scenarios = [] // could be [{ name : "test"}]
As the vm.scenarios of the parent controller gets set after an $http call it is not available when the vm.scenarios of the directive controller is bound to the $scope.scenarios and it doesn't get updated when the parents controller vm.scenarios gets populated eventually.
When adding this to my directives controller, it works but the solution seems wrong to me:
$scope.$watch('scenarios', function(newValue) {
if (newValue !== undefined) {
vm.scenarios = $scope.scenarios;
}
});
This is how you should define your directive controller:
MyDirectiveController.$inject = [];
function MyDirectiveController() {
// nothing here
}
You don't need to use $scope because you already bind to controller instance this. It means that scope config
scope: {
scenarios: '='
},
populates controller instance this object, not $scope object, and hence $scope.scenarios is undefined. With vm.scenarios = $scope.scenarios; in controller you just overwrite correct binding with undefined value.
Demo: http://plnkr.co/edit/lYg15Xpb3CsbQGIb37ya?p=preview

How is this attribute value bound without interpolation markup?

MyController has clickedvalue as false in its scope.
the click changes it to true
the dir directive watches for change on the clicked attribute.
How is the $watch seeing change - or how is the clickvalue changing without being interpolation markup {{clickvalue}}
right now click="clickvalue" triggers the $watch which would mean it is 'bound' to the MyContoller's clickvalue
<div ng-controller='MyController'>
<button ng-click='click()'>Call function inside directive</button>
<dir clicked="clickedvalue"></dir>
</div>
app.js
app.controller('MyController', ['$scope', function($scope) {
$scope.clickedvalue = false;
$scope.click = function() {
$scope.clickedvalue = !$scope.clickedvalue;
};
}
]);
appDirectives = angular.module('app.directives', []);
appDirectives.directive('dir', [function() {
return {
restrict: 'E',
scope: {
clicked: '='
},
link:function(scope, element, attrs ) {
scope.$watch('clicked', function() {
console.log('innerFunc called');
})
}
};
}]);
If I understood you well, the '=' does not require you to use {{interpolation}}, '#' would.
Moreover, take note that you set up the $watch on attribute, not on the controller's clickedvalue.
In angular js {{}} means one way binding & when you are using directive and declaring isolated scope with using = that means two way binding of the scope variables. If one of variable changes then the relative scope updates.
scope: {
clicked: '='
},
In your case clicked has been mapped with clickedvalue.As value of clicked gets update, it will also update the value of clickedvalue which is isolated scope directive

AngularJS directive with isolated scope doesn't set local scope

This has caught me with a surprise. AngularJS directive is not setting a local scope if it's being used with isolated scope.
I've a amount field which is being validated. The validation message is within a directive which watches for the amount, if greater than 5 it should display an error message. However the error message is not displayed, but the template responses to ng-if which has property declared on local scope.
Below is the code and PLUNKR
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.vm = {};
});
app.directive('errorMessage', function() {
return {
restrict: "E",
replace: true,
templateUrl: 'my-template.tpl.html',
scope: {
amount: '='
},
link: function(scope, element, attrs) {
scope.isAmountError = true;
scope.$watch('amount', function() {
if (scope.amount > 5) {
scope.isAmountError = true;
scope.errorText = 'Amount lesser than 5';
} else {
scope.isAmountError = false;
}
});
}
};
});
Your problem is ng-if. From the API page at https://docs.angularjs.org/api/ng/directive/ngIf
Note that when an element is removed using ngIf its scope is destroyed and a new scope is created when the element is restored.
So each time ng-if is evaluating to true, a new scope is created which does not reference your errorText. On the other hand, if you use ng-show, it hides/shows but keeps the scope, and all works fine.
<div class="row" ng-show="isAmountError">
Here is the updated plunkr http://plnkr.co/edit/BsSueLNpWA8XveyxblmE?p=preview

AngularJS : Directive not able to access isolate scope objects

I am trying to put some default values in my directive with Isolate scope. Basically, I need to do some DOM manipulations using the scope object when my directive is bound. Below is my code:
Controller:
angular.module('ctrl').controller('TempCtrl', function($scope, $location, $window, $timeout, RestService, CommonSerivce) {
$scope.showAppEditWindow = function() {
//Binding the directive isolate scope objects with parent scope objects
$scope.asAppObj = $scope.appObj;
$scope.asAppSubs = $scope.appSubscriptions;
//Making Initial Settings
CommonSerivce.broadcastFunction('doDirectiveBroadcast', "");
};
Service:
angular.module('Services').factory('CommonSerivce', function ($rootScope) {
return {
broadcastFunction: function(listener, args) {
$rootScope.$broadcast(listener, args);
}
};
Directive:
angular.module('directives').directive('tempDirective', function() {
return {
restrict : 'E',
scope:{
appObj:'=asAppObj',
appSubs: '=asAppSubs'
},
link : function(scope, element, attrs) {},
controller : function ($scope,Services,CommonSerivce) {
//Broadcast Listener
$scope.$on('doDirectiveBroadcast', function (event, args) {
$scope.setDefaults();
});
$scope.setDefaults = function() {
//Setting Default Value
alert(JSON.stringify($scope.appSubs)); //Coming as undefined
};
},
templateUrl:"../template.html"
};
});
Custom Directive element:
<temp-directive as-app-obj="asAppObj" as-app-subs="asAppSubs" />
Now, the issue is that while trying to access the isolate scope in the default method inside directive, I aam getting an undefined value whereas the data is coming and is getting bound to the DOM. How can I access the isolate scope in the broadcast listener and modify the directive template HTML? Is there another wasy for handling this?
The problem is: at that time angular does not update its bindings yet.
You should not access your variables like this, try to use angular js binding mechanism to bind it to view (by using $watch for example). Binding to parent scope variables means you're passive, just listen for changes and update other variables or your view. That's how we should work with angular.
If you still need to access it. You could try a workaround using $timeout
$scope.setDefaults = function() {
$timeout(function () {
alert(JSON.stringify($scope.appSubs)); //Coming as undefined
},0);
};
DEMO
It's better to use $watch
angular.module('ctrl', []).controller('TempCtrl', function ($scope, $location, $rootScope) {
$scope.appSubscriptions = "Subscriptions";
$scope.appObj = "Objs";
$scope.showAppEditWindow = function () {
//Binding the directive isolate scope objects with parent scope objects
$scope.asAppObj = $scope.appObj;
$scope.asAppSubs = $scope.appSubscriptions;
};
});
angular.module('ctrl').directive('tempDirective', function () {
return {
restrict: 'E',
replace: true,
scope: {
appObj: '=asAppObj',
appSubs: '=asAppSubs'
},
link: function (scope, element, attrs) {
},
controller: function ($scope, $timeout) {
$scope.$watch("appSubs",function(newValue,OldValue,scope){
if (newValue){
alert(JSON.stringify(newValue));
}
});
},
template: "<div>{{appSubs}}</div>"
};
});
DEMO
By using $watch, you don't need to broadcast your event in this case.
Most likely the isolated scope variable is not available when the directive's controller first instantiates but probably its available when you need it for a following event such as: within a function bound to an ng-click
its just a race condition and the object doesn't arrive exactly when directive's controller loads

Resources