From a directive using the controllerAs declaration, I want to pass a scope variable to a nested directive, that is to be used within. I want any changes from "inside" to be reflected on the outside, and vice versa.
Grabbing the variable through an isolated scope is not an option, as the controllerAs directive is already asking for an isolated scope. See example here
I have come up with this, in the inner directive:
link: function($scope, el, attrs, ngModel) {
$scope.ngModel = {};
$scope.modelValue = ngModel.$modelValue;
$scope.$watch(function () {
return ngModel.$modelValue;
}, function(newValue) {
$scope.ngModel = newValue;
});
$scope.$watch(function () {
return $scope.ngModel;
}, function(newValue) {
ngModel.$setViewValue(newValue);
}, true);
}
- as seen in the following example, but it seems very hacky. How is this done best?
Related
I've got next directive:
(function() {
'use strict';
angular
.module('myApp')
.directive('inner', inner);
function inner () {
return {
restrict: 'A',
scope: false,
link: linkFunc
};
function linkFunc (scope, element, attrs) {
}
}
})();
And HTML:
<span inner>{{vm.number}}</span>
How can I access vm.number's value in linkFunc? I need to take value exactly from content of the span tag.
There are various ways you can do this but here are the 2 most common ways:
ngModel
You could use ng-model like so in your template:
<span inner ng-model="vm.number">{{vm.number}}</span>
In your directive you require the ngModel where you can pull its value:
.directive( 'inner', function(){
return {
require: 'ngModel',
link: function($scope, elem, attrs, ngModel){
var val = ngModel.$modelValue
}
}
})
declare isolate scope properties
<span inner="vm.number">{{vm.number}}</span>
.directive( 'inner', function(){
return {
scope: { inner:'=' } ,
link: function($scope, elem, attrs){
var val = $scope.inner
}
}
})
Some less common ways:
use $parse service to get the value
Using the template again:
<span inner="vm.number">{{vm.number}}</span>
Let's assume you're going to Firstly you'll need to inject the $parse service in your directive's definition. Then inside your link function do the following:
var val = $parse(attrs.inner)
inherited scope for read only
I don't recommend this, because depending on how you defined your directive's scope option, things might get out of sync:
isolate (aka isolated) scopes will not inherit that value and vm.number will probably throw an undefined reference error because vm is undefined in most cases.
inherited scope will inherit the initial value from the parent scope but could diverge during run-time.
no scope will be the only case where it will stay in sync since the directive's $scope reference is the same scope present in the expression {{vm.number}}
Again I stress this is probably not the best option here. I'd only recommend this if you are suffering performance issues from a large number of repeated elements or large number of bindings. More on the directive's scope options - https://spin.atomicobject.com/2015/10/14/angular-directive-scope/
Well, In Angular directive, Link function can do almost everything controller can.
To make it very simple, we use one of them most of the time.
var app = angular.module('app', []);
app.controller('AppCtrl', function ($scope) {
$scope.number = 5;
}).directive('inner', function () {
return {
restrict: 'A',
scope: false,
link: function (scope, element, attrs) {
var number = scope.number;
console.log(number);
}
}
});
Inside html :
<div inner ng-model="number">{{number}}</div>
https://plnkr.co/edit/YbXYpNtu7S3wc0zuBw3u?p=preview
In order to take value from HTML, Angular provides ng-model directive which is works on two way data binding concepts.
There are other ways which is already explain by #jusopi :)
cheers!
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
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.
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
I have a directive that uses an isolate scope to pass in data to a directive that changes over time. It watches for changes on that value and does some computation on each change. When I try to unit test the directive, I can not get the watch to trigger (trimmed for brevity, but the basic concept is shown below):
Directive:
angular.module('directives.file', [])
.directive('file', function() {
return {
restrict: 'E',
scope: {
data: '=',
filename: '#',
},
link: function(scope, element, attrs) {
console.log('in link');
var convertToCSV = function(newItem) { ... };
scope.$watch('data', function(newItem) {
console.log('in watch');
var csv_obj = convertToCSV(newItem);
var blob = new Blob([csv_obj], {type:'text/plain'});
var link = window.webkitURL.createObjectURL(blob);
element.html('<a href=' + link + ' download=' + attrs.filename +'>Export to CSV</a>');
}, true);
}
};
});
Test:
describe('Unit: File export', function() {
var scope;
beforeEach(module('directives.file'));
beforeEach(inject(function ($rootScope, $compile) {
scope = $rootScope.$new();
};
it('should create a CSV', function() {
scope.input = someData;
var e = $compile('<file data="input" filename="filename.csv"></file>')(scope);
//I've also tried below but that does not help
scope.$apply(function() { scope.input = {}; });
});
What can I do to trigger the watch so my "In watch" debugging statement is triggered? My "In link" gets triggered when I compile.
For a $watch to get triggered, a digest cycle must occur on the scope it is defined or on its parent. Since your directive creates an isolate scope, it doesn't inherit from the parent scope and thus its watchers won't get processed until you call $apply on the proper scope.
You can access the directive scope by calling scope() on the element returned by the $compile service:
scope.input = someData;
var e = $compile('<file data="input" filename="filename.csv"></file>')(scope);
e.isolateScope().$apply();
This jsFiddler exemplifies that.