How to prevent scope collision between angularjs directives? - javascript

I have created a simple directive, called match that is used like:
<input match='pattern' />
The declaration line of my directive is:
app.directive('match', function () {
return {
restrict: 'A',
require: 'ngModel',
scope: {
pattern: '=match'
},
link: function (scope, element, attributes, ngModel) {
// doing stuff here
}
};
});
However, after a while I wanted to use BootstrapUI for angularjs, and as soon as I started using typeahead component, they encountered a problem on using the same scope:
Multiple directives [match, uibTypeaheadMatch] asking for new/isolated scope on
I need match, and typeahead together in one page. Typeahead is not under my control, and I don't want to change match's name.
What can I do to prevent their collision?

The problem is that both your directive and Typeahead directive, are asking for isolated scope on the same element and angular does not allow it.
To overcome this problem, define the directive in a different way:
app.directive('match', function () {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attributes, ngModel) {
var match = attributes.match;
//do your stuff
}
};
});

Related

Resolving scope variables in an angular directive

I'm working on creating an angular directive (element) that will apply some transformation to the text within.
Here is the directive:
module.directive('myDir', function() {
return {
restrict: 'E',
link: function(scope, elem, attrs) {
console.log(elem.text());
},
};
});
For now I've just placed a console.log in there to make sure I am seeing the expected text.
If my html is like this it works as expected:
<my-dir>Hello World!</my-dir>
However, if I use a variable from the scope such as this:
<my-dir>{{myMessage}}</my-dir>
Then that is the text I seen in the console output instead of the variables value. I think I understand why the output is this way, however I'm not sure how to get the correct value. The requirement is that the directive should be able to transform text from both examples.
Thoughts?
Use $interpolate service to convert the string to a function, and apply the scope as parameter to get the value (fiddle):
module.directive('myDir', function($interpolate) {
return {
restrict: 'E',
link: function(scope, elem, attrs) {
console.log($interpolate(elem.text())(scope));
},
};
});
If you really interested get the interpolated content then do use $interpolate service to evaluated the interpolated content.
Code
link: function(scope, elem, attrs) {
console.log($interpolate(elem.text())(scope));
},
Don't forget to add $interpolate as dependency on directive function.
Have a look on $compile or http://odetocode.com/blogs/scott/archive/2014/05/07/using-compile-in-angular.aspx
This should work:
module.directive('myDir', function($compile) {
return {
restrict: 'E',
link: function(scope, elem, attrs) {
console.log($compile(elem.text())(scope));
},
};
});
I would suggest you use an isolated scope on directive which will give you access to the value without having to get it from the dom. You will also be able to manipulate it directly in the link function as part of scope
<my-dir my-message="myMessage"></my-dir>
JS
module.directive('myDir', function() {
return {
restrict: 'E',
template: '{{myMessage}}',
scope:{
myMessage: '=',
},
link: function(scope, elem, attrs) {
console.log(scope.myMessage);
},
};
});

How to pass the angular's directive's link and controller to it

Say that I have a straight forward directive:
angular
.module('myApp')
.directive('myDiv', ['MyService1', 'MyService2',
function(MyService1, MyService2) {
return {
restrict: 'E',
link: function(scope, element, attrs) {
scope.myVars = MyService1.generateSomeList();
MyService2.runSomeCommands();
// Do a lot more codes here
// Lines and Lines
// Now run some embedded function here
someEmbeddedFunction();
function someEmbeddedFunction()
{
// More embedding
// This code is really getting nasty
}
}
}
}
]);
The code above has so much indentation and crowded that at least to me, it is very hard to read and unenjoyable to work with.
Instead, I want to move the link and someEmbeddedFunction out and just call them. So something along the lines of this:
function link(scope, element, attrs, MyService1, MyService2)
{
scope.myVars = MyService1.generateSomeList();
MyService2.runSomeCommands();
// Do a lot more codes here
// Lines and Lines
// Now run some embedded function here
someEmbeddedFunction();
}
function someEmbeddedFunction()
{
// This is much nicer and less tabbing involved
}
angular
.module('myApp')
.directive('myDiv', ['MyService1', 'MyService2',
function(MyService1, MyService2) {
return {
restrict: 'E',
link: link // This is line that I need to get it to work
}
]);
The problem is that MyService1 and MyService2 are not passed to the link function (i.e. if I only had a link function with scope, element, attrs then the code above would work just fine). How can I pass those variables as well?
I tried to call the function as link: link(scope, element, attrs, MyService1, MyService2) but then it says scope, element, attrs are undefined.
Note I realize that the someEmbeddedFunction can right now be moved out without a problem. This was just for demonstrating purposes.
Edit
The only way I can think to get this to work is call the link function from the directive this way:
link: function(scope, element, attrs) {
link(scope, element, attrs, MyService1, MyService2);
}
As you observed, the only way to call your non-standard link function is to do so manually within a "standard" link function.
i.e.
link: function(scope, element, attrs) {
link(scope, element, attrs, MyService1, MyService2);
}
This is because the link function doesn't get injected like other functions in Angular. Instead, it always gets the same series of arguments (regardless of what you call the function parameters):
The scope
The element (as an angular.element() instance)
The attrs object
An array or single controller instance that you required
A transclude function (if your directive uses transclusion)
Nothing else.
I use this scheme to keep it simple & readable:
var awesomeDir = function (MyService, MyAnotherService) {
var someEmbeddedFunction = function () {
MyService.doStuff();
};
var link = function ($scope, $elem, $attrs) {
someEmbeddedFunction();
};
return {
template: '<div>...</div>',
replace: true,
restrict: 'E',
link: link
};
};
awesomeDir.$inject = ['MyService', 'MyAnotherService'];
app.directive('awesomeDir', awesomeDir);

Tell child directive to act after a parent directive has done DOM actions?

Let's say we have some nested directives:
<big-poppa>
<baby-bird></baby-bird>
</big-poppa>
And let's say that big-poppa wants to create a component that all of his children directives can share. It would be nice to put it in the controller, but this component needs the DOM, so it needs to be build in the link function.
Then let's say the baby-bird component wants to read from component. Maybe it wants to listen to events from it, maybe send it a command or two. The challenge is that controllers fire down the dom (first parent, then child), and post-link methods fire the other direction, so the execution order looks like this:
bigPoppa controller
babyBird controller
babyBird link
bigPoppa link
The fact that the parent's link method fires after the child's is the cause of an intra-directive communication challenge for me. I want the parent to build the shared DOM component, but DOM construction should happen in a link function. The parent therefore builds the component after any children
I can solve this with a timeout (gross), or a promise (complex/non-idiomatic?). Here is the fiddle:
http://jsfiddle.net/8xF3Z/4/
var app = angular.module('app',[]);
app.directive('bigPoppa', function($q){
return {
restrict: 'E',
controller: function($scope){
console.log('bigPoppa controller');
var d = $q.defer()
$scope.bigPoppaLinkDeferred = d
$scope.bigPoppaLink = d.promise
},
link: function(scope, el, attrs){
console.log('bigPoppa link');
scope.componentThatNeedsDom = { el: el, title: 'Something' };
scope.bigPoppaLinkDeferred.resolve()
}
}
});
app.directive('babyBird', function(){
return {
restrict: 'E',
controller: function(){ console.log('babyBird controller'); },
link: function(scope, el, attrs, bigPoppaController){
console.log('babyBird link');
// console.log('poppa DOM component', scope.componentThatNeedsDom); // Not yet defined, because the parent's link function runs after the child's
// setTimeout(function(){ console.log('poppa DOM component', scope.componentThatNeedsDom); }, 1); // Works, but gross
scope.bigPoppaLink.then(function(){
console.log('poppa DOM component', scope.componentThatNeedsDom);
}); // works, but so complex!
}
}
});
console.log(''); // blank line
Lots of background here, but my question is this simple:
Is there a clean way to do behavior in a child directive after a parent's directive has run its post-link function?
Maybe a way of using priority, or the pre and post link methods?
Another way of achieving this is to use plain Angular scope events to communicate from the parent linking function to the child.
var app = angular.module('app',[]);
app.directive('bigPoppa', function($q){
return {
restrict: 'E',
link: function(scope, el, attrs){
scope.$broadcast('bigPoppa::initialised', {el: el, title: 'Something'});
}
}
});
app.directive('babyBird', function(){
return {
restrict: 'E',
link: function(scope, el, attrs) {
scope.$on('bigPoppa::initialised', function(e, componentThatNeedsDom) {
console.log('poppa DOM component in bird linking function', componentThatNeedsDom);
});
}
}
});
This can be seen working at http://jsfiddle.net/michalcharemza/kerptcrw/3/
This way has the benefits:
Has no scope watchers
Instead of depending on knowledge of the order of controller/pre-link/post-link phases, it uses a clear message send/receive paradigm, and so I would argue is easier to understand and maintain.
Doesn't depend on behaviour being in the pre-link function, which isn't that typical, and you have to be mindful to not put in behaviour that modifies the DOM in it.
Doesn't add variables to the scope hierarchy (but it does add events)
Based on experiments, and asking correction if I am wrong, I have found Angular runs its compile phase in the following order:
1. compile methods of all directives both parent and child, run in flat order
2. parent controller
3. parent pre-link
4. (all actions of children directives)
5. parent post-link (AKA regular `link` function)
The public gist of this experiment is here: https://gist.github.com/SimpleAsCouldBe/4197b03424bd7766cc62
With this knowledge, it seems like the pre-link callback on the parent directive is a perfect fit. The solution looks like this:
var app = angular.module('app',[]);
app.directive('bigPoppa', function($q){
return {
restrict: 'E',
compile: function(scope, el) {
return {
pre: function(scope, el) {
console.log('bigPoppa pre');
scope.componentThatNeedsDom = { el: el, title: 'Something' };
}
};
}
}
});
app.directive('babyBird', function(){
return {
restrict: 'E',
link: function(scope, el, attrs, bigPoppaController){
console.log('babyBird post-link');
console.log('bigPoppa DOM-dependent component', scope.componentThatNeedsDom);
}
}
});
http://jsfiddle.net/a5G72/1/
Thanks to #runTarm and this question for pointing me in the pre-link direction.
There are 2 patterns that you can use to achieve what you want
You can have code in a child linking function that reacts to changes in a parent directive's controller, by requireing the parent directive's controller, and creating a $watcher on some value in it.
If you need run something in the parent linking function, and only then change a value in its controller, it is possible for a directive to require itself, and access the controller from the linking function.
Putting these together in your example becomes:
var app = angular.module('app',[]);
app.directive('bigPoppa', function($q){
return {
restrict: 'E',
require: 'bigPoppa',
controller: function($scope) {
this.componentThatNeedsDom = null;
},
link: function(scope, el, attrs, controller){
controller.componentThatNeedsDom = { el: el, title: 'Something' };
}
}
});
app.directive('babyBird', function(){
return {
restrict: 'E',
require: '^bigPoppa',
link: function(scope, el, attrs, bigPoppaController){
scope.$watch(function() {
return bigPoppaController.componentThatNeedsDom
}, function(componentThatNeedsDom) {
console.log('poppa DOM component in bird linking function', componentThatNeedsDom);
});
}
}
});
Which can be seen at http://jsfiddle.net/4L5bj/1/ . This has the benefits over your answer that it doesn't depend on scope inheritance, and doesn't pollute the scope with values that are only used by these directives.

Angularjs binding issues when input has a directive with scope

I've created a custom directive in AngularJS. The directive uses isolated scope, and it somehow prevents the binding for standard ngModel on the same element.
I want to create a confirm password field (text for readability in the example).
<input type="text" name="one" ng-model="fields.field_one">
<input type="text" validate-match="fields.field_one" name="two" ng-model="field_two">
My directive invalidates the field, when there is no match.
app.directive('validateMatch', function() {
return {
require: 'ngModel',
scope: { matchValue: '=validateMatch' },
link: function(scope, elm, attr, ctrl) {
scope.$watch('matchValue', function(value) {
ctrl.$setValidity('match',
ctrl.$viewValue === value
|| !ctrl.$viewValue && !value);
});
function validate(value) {
ctrl.$setValidity('match', value === scope.matchValue);
return value;
}
ctrl.$parsers.push(validate);
ctrl.$formatters.push(validate);
}
}
});
The thing is, why can't I change the value of that field by changing the model? First field works just fine.
Look at the plunker for details and commented code.
As mentioned in a comment, isolate scopes and ng-model don't mix well. Further, we shouldn't be using an isolate scope here, since we're trying to create a directive/component that needs to interact with another directive (ng-model in this case).
Since the validateMatch directive does not create any new properties, the directive does not need to create any new scope. $parse can be used to get the value of the property that attribute validate-match refers to:
app.directive('validateMatch', function($parse) {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, elm, attr, ctrl) {
var model = $parse(attr.validateMatch);
// watch for linked field change (field_one)
scope.$watch(model, function(value) {
console.log('linked change:', value, ctrl.$viewValue);
// set valid if equal or both falsy (empty/undefined/null)
ctrl.$setValidity('match',
ctrl.$viewValue === value
|| !ctrl.$viewValue && !value);
});
// validate on parse/format (field_two)
function validate(value) {
var otherFieldValue = model(scope);
console.log('validate:', value, otherFieldValue);
// set valid if equal
ctrl.$setValidity('match', value === otherFieldValue);
return value;
}
ctrl.$parsers.push(validate);
ctrl.$formatters.push(validate);
}
};
});
plunker
Following Mark's suggestion, I managed to produce a work-around.
When isolated scope exists on the element, ngModel refers to it. The trick is to look the parent scope from within. You can either change the ngModel by hand (prepending it with $parent.), or automatize this process inside the directive by proper compile function.
This is how i did this:
compile: function(element, attrs, transclude) {
// reference parent scope, because isolated
// scopes are not looking up by default
attrs.$set('ngModel', '$parent.'+attrs.ngModel, false);
return function(scope, elm, attr, ctrl) {
// link function body there
}
}
For the full example, look at this plunk.
From what I understand of AngularJS Directives you can use the transclude parameter to access the parent scope of the controller.

Modify template in directive (dynamically adding another directive)

Problem
Dynamically add the ng-bind attribute through a custom directive to be able to use ng-bind, ng-bind-html or ng-bind-html-unsafe in a custom directive with out manually adding to the template definition everywhere.
Example
http://jsfiddle.net/nstuart/hUxp7/2/
Broken Directive
angular.module('app').directive('bindTest', [
'$compile',
function ($compile) {
return {
restrict: 'A',
scope: true,
compile: function (tElem, tAttrs) {
if (!tElem.attr('ng-bind')) {
tElem.attr('ng-bind', 'content');
$compile(tElem)
}
return function (scope, elem, attrs) {
console.log('Linking...');
scope.content = "Content!";
};
}
};
}]);
Solution
No idea. Really I can not figure out why something like the above fiddle doesn't work. Tried it with and with out the extra $compile in there.
Workaround
I can work around it might adding a template value in the directive, but that wraps the content in an extra div, and I would like to be able to that if possible. (See fiddle)
Second Workaround
See the fiddle here: http://jsfiddle.net/nstuart/hUxp7/4/ (as suggested by Dr. Ikarus below). I'm considering this a workaround for right now, because it still feels like you should be able to modify the template before you get to the linking function and the changes should be found/applied.
You could do the compiling part inside the linking function, like this:
angular.module('app').directive('bindTest', ['$compile', function ($compile) {
return {
restrict: 'A',
scope: true,
link: {
post: function(scope, element, attrs){
if (!element.attr('ng-bind')) {
element.attr('ng-bind', 'content');
var compiledElement = $compile(element)(scope);
}
console.log('Linking...');
scope.content = "Content!";
}
}
};
}]);
Let me know how well this worked for you http://jsfiddle.net/bPCFj/
This way seems more elegant (no dependency with $compile) and appropriate to your case :
angular.module('app').directive('myCustomDirective', function () {
return {
restrict: 'A',
scope: {},
template: function(tElem, tAttrs) {
return tAttrs['ng-bind'];
},
link: function (scope, elem) {
scope.content = "Happy!";
}
};
});
jsFiddle : http://jsfiddle.net/hUxp7/8/
From Angular directive documentation :
You can specify template as a string representing the template or as a function which takes two arguments tElement and tAttrs (described in the compile function api below) and returns a string value representing the template.
The source code tells all! Check out the compileNodes() function and its use of collectDirectives().
First, collectDirectives finds all the directives on a single node. After we've collected all the directives on that node, then the directives are applied to the node.
So when your compile function on the bindTest directive executes, the running $compile() is past the point of collecting the directives to compile.
The extra call to $compile in your bindTest directive won't work because you are not linking the directive to the $scope. You don't have access to the $scope in the compile function, but you can use the same strategy in a link function where you do have access to the $scope
You guys were so close.
function MyDirective($compile) {
function compileMyDirective(tElement) {
tElement.attr('ng-bind', 'someScopeProp');
return postLinkMyDirective;
}
function postLinkMyDirective(iScope, iElement, iAttrs) {
if (!('ngBind' in iAttrs)) {
// Before $compile is run below, `ng-bind` is just a DOM attribute
// and thus is not in iAttrs yet.
$compile(iElement)(iScope);
}
}
var defObj = {
compile: compileMyDirective,
scope: {
someScopeProp: '=myDirective'
}
};
return defObj;
}
The result will be:
<ANY my-directive="'hello'" ng-bind="someScopeProp">hello</ANY>

Categories