AngularJS : Directive not able to access isolate scope objects - angularjs

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

Related

Get data back from angular directive

I've created a directive which I called my-tree, and I'm calling this directive from a view exemple-tree-view.html as following:
<my-tree ng-model="sampleTreeView.listNoeuds" ... />
this view's controller called sampleTreeView.
In my directive's link function I have a function that returns some data, which I affect to scope variable declared in the directive's controller, as following :
function linkFn(scope, element, attrs) {
//some code
scope.createNode = function ($event) {
var sel = $(element).jstree(true).create_node($($event.currentTarget)[0].closest('.jstree-node').id);
if (sel) {
$(element).jstree(true).edit(sel, '', function (node, success, cancelled) {
scope.treeActionsResult.createdNode = node;
});
}
};
//some code
}
My question is how can I get the scope.treeActionsResult.createdNode value in the sampleTreeView controller, since it's the controller for the exemple-tree-view.html where I call my directive.
You can use shared scope between the directive and controller by removing the scope property
like in this example:
MyApp.directive('studentDirective', function () {
return {
template: "{{student.name}} is {{student.age}} years old !!",
replace: true,
restrict: 'E',
controller: function ($scope) {
console.log($scope);
}
}
});
Still you have the $scope object, but in this case the scope object is shared with parent controller's scope.
You can read more about it fron the following link
Understanding Scope in AngularJs Custom Directive
If you don't create isolated scope for your directive then you can access directive scope values from your controller. like bellow
your controller and directive:
app.controller('MainCtrl', function($scope) {
$scope.value = 1;
});
app.directive('myTree', function() {
return {
restrict: 'AE',
link: function(scope, element, attrs) {
scope.values = {};
scope.values.price = 1234;
}
};
});
then use in your html like:
<body ng-controller="MainCtrl">
<p>value {{values.price}}</p>
<my-tree att="{{attValue}}"></my-tree>
</body>
here values.price shown from directive in MainCtrl

How to stub methods in a controller that has a shared scope with a directive's controller

I'm testing the controller of a directive that has scope set to false in the directive definition object. A method from the 'parent' controller is called from the directive controller - I am unable to stub this method as I have no access to it from my directive's controller's test file though - what is the best procedure for dealing with this situation?
Many thanks.
You could pre-define the known methods and set them to angular.noop:
function ChildController() {
var _this = this;
_this.parentMethod1 = angular.noop;
_this.parentMethod2 = angular.noop;
_this.childMethod = function() {
// do some stuff
};
}
Then you can create an init method on the child that is called during link that replaces the methods with the parent methods if the parent is there:
// directive definition
.directive('myDirective', function myDirective() {
return {
bindToController: true,
controller: ChildController,
controlelrAs: 'childController',
require: ['childController', '^parentController'],
scope: false,
link: function (scope, element, attributes, controllers) {
// controllers[0] = childController
// controllers[1] = parentController
controllers[0].init(controllers[1]);
}
};
});
// child controller
function ChildController() {
// ...same as above...
_this.init = function(parentController) {
_this.parentMethod1 = parentController.parentMethod1;
_this.parentMethod2 = parentController.parentMethod2;
};
}
You could also define the controller using .controller to make testing simpler (without needing to create the directive with a mock parentController). But this should get what you described done.

Angular JS: Watch changes in a variable made in FOO directive to reflect in BAR directive

I am currently working on extending the UX of Angular Typeahead. I made it possible for the results list not to get cleared on input .blur and remain in the DOM until the query changes.
But this way the results do not disappear. And I want to bind the input.blur() event to the variable, that is later on passed to the scope that has control over the typeahead-popup.html module.
Here's the adapted relevant-code of what I currently have:
angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position'])
.directive('typeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$window', '$rootScope', '$position', 'typeaheadParser',
function($compile, $parse, $q, $timeout, $document, $window, $rootScope, $position, typeaheadParser) {
return {
link: function(originalScope, element, attrs, ctrls) {
var isBlurred;
//some irrelevant library code
//on this event I change isBlurred;
element.bind('blur', function() {
//irrelevant functionality
isBlurred = true;
});
element.bind('focus', function() {
isBlurred = false;
});
}
}
}
}])
.directive('typeaheadPopup', function() {
return {
scope {
//<irrelevant other variables>
isBlurred: '='
}
},
replace: true,
//<irrelevant code>
link: function(scope, element, attrs) {
//<irrelevant other functions that have access to the scope and apply params at the DOM element>
scope.$watch('isBlurred', function() {
console.log(isBlurred)
});
});
})
});
Directive typeaheadPopup has control over the relevant DOM elements. Therefore the variable change from the .blur and .focus events on the input. But this doesn't happen. I just get wasBlurred is not defined.
Question:
How do I adjust the code in a way so that variable change that happens in typeahead directive is reflected properly within typeaheadPopup directive?
Here is a working Plunker with ui-bootstrap.
Here is an integration with ui-bootstrap and a close button control. Plunker
Here is a working Plunker with the code you gave and the question about how to communicate between directives.
Your isBlurred variable needs to be assigned to the scope :
originalScope.isBlurred;
Blur and focus are not part of the ngModelOptions that will trigger the scope to automaticaly change. You need to trigger it manualy.
element.on('blur', function() {
originalScope.$apply(function(){
originalScope.isBlurred = true;
});
});
Now your isBlurred is linked to the scope and that it will update with your custom events, you can forward it to your typeaheadPopup directive this way :
<input type="text" ng-model="name" typeahead typeahead-popup="isBlurred">
Define a isolated scope in your typeaheadPopup :
scope : {
isBlurred: '=typeaheadPopup'
}
There it is : your isBlurred variable is accessible and will automatically be updated in your directive. No need for a $watch. But you'll need one if you want to trigger an event when it changes :
scope.$watch('isBlurred', function() {
alert("inside typeaheadPopup directive" + scope.isBlurred)
});
The general answer is for all non-Angular event-handlers (in your case it's element.bind) you should call scope.$apply (or scope.$digest) to run the digest cycle and synchronise the values.
So for your case you either do manual call:
element.on('blur', function() {
originalScope.isBlurred = true;
originalScope.$apply();
});
or to run Angular method that will do it for you, for example $timeout:
element.on('blur', function() {
$timeout(function(){
originalScope.isBlurred = true;
});
});
$timeout sometimes is needed like a hack to integrate 3rd party non-Angular libraries.
If you can nest your directives, or use one as an attribute on the other, then they are able to communicate more cleanly. If they exist on the same level, then I believe some injected share state is you best bet.
If possible, nest the directives and make them communicate as described under "Creating Directives that Communicate" (at the bottom) of the documentation.
Example:
angular.module('foo', [])
.directive('parent', function() {
return {
restrict: 'E',
transclude: true,
controller: function() {
this.someFunction = function(pane) {
};
}
};
})
.directive('child', function() {
return {
require: '^^parent',
restrict: 'E',
transclude: true,
link: function(scope, element, attrs, parentCtrl) {
parentCtrl.someFunction();
}
};
});
I guess You need something like this demo right? So you need to assign a two-way bounded scope in each directive like this
scope: {
model: "="
},
then set both of them with the same model
<body ng-controller='MainController as vm'>
<foo model='vm.foobar'></foo>
<bar model='vm.foobar'></bar>
</body>
so MainController's foobar is set two-way bounded to foo and bar's scope, so technically, they share the same model and have every changes the model have.
please see the demo for the full code

Angularjs, watching scope variable in directive

From controller, I need to watch variable which is 2-way bound to directive scope.
As I tested in JsFiddle,
angular 1.1 works well, but angular1.2 does not.
How can I fix it?
http://jsfiddle.net/4091qg9r/3/
var simulationAppModule = angular.module('simulationApp', [])
simulationAppModule.controller('tst', function ($scope) {
$scope.acts = [];
$scope.$watch('acts', function (neww, old) {
console.log('controller', neww)
}, true)
})
simulationAppModule.directive('bn', function () {
return {
restrict: "A",
scope: {
acts: '='
},
link: function ($scope, iElement, iAttrs) {
$scope.addaction = function () {
$scope.acts.push('aaa')
}
}
}
})
The problem is that you are declaring isolated scope in your directive, so $scope.addaction is never reached when you use ng-click="addaction()" on the directive element. ngClick and the addaction are in to different scopes.
One solution is not to use isolated scope, so you can remove
scope: {
acts: '='
},
If you however do need the scope to be isolated child scope, then you can try (not recommended)
$scope.$parent.addaction = function () {
$scope.acts.push('aaa')
}
But the best approach is to revise your set up, because addaction function should be declared in controller, not in directive.

Multiple directive instances and events

I have a directive that I use multiple times on a page. It fires an event when the state changes, and the controller then handles the event.
The problem is that the event is fired twice. I get why this happens, but I am stuck either trying to find a workaround or better design. Any tips?
Plunker example: http://plnkr.co/edit/xObOvi253qejphU5arFr
You need to define isolated scope to make the directive reusable. A simple fix is to just add scope: {} to create an isolated scope so when you click on each button, it only fire once.
app.directive('myDirective', function () {
return {
restrict: 'E',
scope: {}, // Add this line to create an isolated scope
template: '<div>Foo: {{foo}}</div><button ng-click="incrementFoo()">Increment Foo</button>',
controller: function ($scope) {
$scope.foo = 0;
$scope.incrementFoo = function () {
$scope.foo += 1;
};
$scope.$watch('foo', function () {
$scope.$emit('fooChanged', {foo: $scope.foo});
console.log($scope);
});
}
};
});

Resources