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);
});
}
};
});
Related
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
Currently, I have two directives and parent controller, all on some form.
I would like to implement "related field"-like functionality. That is, data from one directive should be passed to other preferably through controller so that I have full flexibility in "routing" data between fields.
So I have this set-up:
Controller:
$scope.$watch('form.model.object', function (newValue, oldValue) {
$scope.$broadcast("cmsRelatedChanged", {key: 'subcarriers', value: newValue['#identity']});
};
Second directive:
$scope.$on('cmsRelatedChanged', function(event, aditionalData) {
$scope.related[aditionalData.key] = aditionalData.value;
console.log("[CMSCollectionDirective] updated with: ", aditionalData, " event: ", event);
});
It do not work first time, when form is pre-populated with existing object But following changeds made in browser work.
As if second directive $on registered that listener after first $broadcast was made.
Additional info:
Everything in second controller is done in link:, and second directive is also second in DOM.
Questions:
How can I delay that first broadcast enough for $on to register listener?
EDIT:
Added console.log("Adding listener") before $scope.$on, and it in fast is executed after first $broadcast, and that's why its not cough.
Maybe try to not use $broadcast and $on. For me this solution works to share data between directives:
JS:
app
.controller('MainCtrl', function () {
$scope.sharedData = {
data: 1
};
$scope.testAction = function () {
vm.sharedData.data++;
}
})
.directive("dirFirst", function () {
var directive = {
link: link,
restrict: 'AE',
scope: {
sharedData: '='
}
};
return directive;
function link(scope, element) {
scope.$watch(function () {
return scope.sharedData.data;
}, function () {
console.log("dirFirst: " + scope.sharedData.data)
});
element.on('click', function () {
scope.sharedData.data++;
console.log(scope.sharedData.data);
});
}
})
.directive("dirSecond", function () {
var directive = {
link: link,
restrict: 'AE',
scope: {
sharedData: '='
}
};
return directive;
function link(scope, element) {
scope.$watch(function () {
return scope.sharedData.data;
}, function () {
console.log("dirSecond: " + scope.sharedData.data)
});
element.on('click', function () {
scope.sharedData.data++;
console.log(scope.sharedData.data);
});
}
});
HTML usage:
<button dir-first shared-data="sharedData">
Directive1
</button>
<button dir-second shared-data="sharedData">
Directive2
</button>
<button ng-click="testAction()">ControllerAction</button>
Biggest issue in my question ended up being order of initialization of directives and inability to use $scope as transfer medium between controller and 2nd directive.
Solution would be to use service as such medium.
Controller still register watch on 1st directive, and as soon as it gets corresponding event put received data in service.
Data availability is signalled to 2nd directive by emmiting event. However 2nd directive check availability of data in service on its own on start up.
This way 2nd directive can be initialized long after some data was sent, and still act upon it.
I have a function I'm binding on:
angular.module('app').directive('resizable', function($window) {
return function(scope) {
angular.element($window).bind('resize', function() {
scope.$apply(function() {
//console.log($window.innerWidth);
scope.windowWidth = $window.innerWidth;
});
})
}
});
But this doesn't fire on onload. I need the initial screen width upon page load. How do I get this using Angular?
UPDATE:
I've also tried this ...
angular.module('ccsApp').directive('setSize',
['$document', function($document) {
return {
restrict: 'A',
link: function($scope, elements, attrs) {
$document.on("load", function() {
$scope.$apply(function () {
console.log('initial=');
});
});
}
}}
]
);
This code is in a directive. So you probably don't want the function to be executed when the application is loaded, but only when this directive is used. So, simply execute the function directly in the directive function:
angular.module('app').directive('resizable', function($window) {
return function(scope) {
// define the function
var updateWindowWidth = function() {
// console.log($window.innerWidth);
scope.windowWidth = $window.innerWidth
};
// call it immediately to initialize the scope variable as soon as the directive is used
updateWindowWidth();
// and make sure it's called every time the window is resized
angular.element($window).bind('resize', function() {
scope.$apply(updateWindowWidth);
});
};
});
You should probably also make sure that the event handler is unbound when the directive is destroyed. Otherwise, every time the directive is used, an additional handler is added to the window.
You could inject $window into a .run function of your module. Although there would be no scope available.
It is unclear what your are trying to do with the window size so our ability to provide helpful answers is limited
How does one abstract a directive properly?
As a really basic example, let's say I have this:
http://plnkr.co/edit/h5HXEe?p=info
var app = angular.module('TestApp', []);
app.controller('testCtrl', function($scope) {
this.save = function() {
console.log("hi");
}
this.registerListeners = function() {
console.log('do stuff to register listeners');
}
this.otherFunctionsNotToBeChangedWithDifferentInstances() {
console.log('these should not change between different directives')
}
return $scope.testCtrl = this;
});
app.directive("tester", function() {
return {
restrict: 'A',
controller: 'testCtrl',
template: '<button ng-click="testCtrl.save()">save</button>'
};
});
The tester directive has some methods on it, but only two will be changed or used depending on where the directive is placed. I could pass in the function as a directive attribute, but I am wondering if there is a better way to do this. I have been looking at providers, but I am unsure how or if those would even fit into this.
Instead of letting your directive assume that testCtrl.save() exist on the scope, you would pass in that function as an attribute. Something like this: http://jsbin.com/jidizoxi/1/edit
Your directive binds the value of the my-on-click attribute as a callable function. Your template passes in the controllers ctrlOnClick() function, and when the buttons ng-click calls myOnClick() Angular will call ctrlOnClick() since they are bound to each other.
EDIT:
Another common approach is to pass in a config object to the directive. So your controller would look something like:
$scope.directiveConfig = {
method1: function() { ... },
method2: function() { ... },
method3: function() { ... },
...
}
And your template:
<my-directive config="directiveConfig"></my-directive>
The directive then gets a reference to that object by:
scope: {
config: '='
}
The directive can then call methods on the object like this: $scope.config.method1().
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