I have multiple AngularJS directives that are nearly identical - there are only two differences: the template URL and one single element in the linking function. Both are constant for each directive. So, for simplicity's sake, this is how it looks like:
app.directive("myDirective", [function() {
return {
templateUrl: "this/path/changes.html",
scope: true,
link: function(scope, element, attrs) {
var veryImportantString = "this_string_changes";
// a few dozen lines of code identical to all directives
}
};
}]);
Now, moving the linking function to a commonly available place is obvious. What is not so obvious to me is how to set that "very important string" on the scope (or otherwise pass it to the directive) without declaring it in the HTML.
Here's what I've tried.
app.directive("myDirective", [function() {
return {
templateUrl: "this/path/changes.html",
scope: {
veryImportantString: "this_string_changes"
},
link: someCommonFunction
};
}]);
Nope, apparently the scope config doesn't take values from nobody. I can bind a value coming from the HTML attribute, but this is precisely what I don't want to do.
Also tried this:
app.directive("myDirective", [function() {
return {
templateUrl: "this/path/changes.html",
veryImportantString: "this_string_changes",
scope: true,
link: function(scope, element, attrs) {
var veryImportantString = this.veryImportantString;
}
};
}]);
But alas, the linking function is then called with this set to something else.
I assume this might work:
app.directive("myDirective", [function() {
return {
templateUrl: "this/path/changes.html",
scope: true,
compile: function(element, attrs) {
// no access to the scope...
attrs.veryImportantString = "this_string_changes";
return someCommonFunction;
}
};
}]);
However, I am not 100% sure this is what I want either, as it reeks of being a dirty workaround.
What are my other options?
I have devised a completely different approach: using a factory-like function to spawn directives.
var factory = function(name, template, importantString) {
app.directive(name, [function() {
return {
scope: true,
templateUrl: template,
link: function(scope, element, attrs) {
var veryImportantString = importantString;
// directive logic...
}
};
}]);
};
Then, in order to create individual directives, I simply call:
factory("myDirective", "/path/to/template.html", "important");
factory("myDirective2", "/path/to/template2.html", "important2");
What about the following:
Before wherever you define someCommonFunction, add the line
var veryImportantString = "someOptionalDefault"
This then puts veryImportantString in scope of both your someCommonFunction and .directive()
Then you can change your directive code to:
app.directive("myDirective", [function() {
return {
templateUrl: "this/path/changes.html",
scope: true,
link: function(args){
veryImportantString = "thatUberImportantValue";
someCommonFunction(args);
}
};
}]);
Proof of concept fiddle
Related
I was trying to declare a directive, it is apparently right but when I load it in html nothing occurs.
This is the code:
(function() {
'use strict';
var app = angular
.module('App');
app.directive('directiveFunction', directiveFunction);
function directiveFunction(){
return {
restrict: 'EA',
replace: true,
template: '<div>Example</div>',
controller: directiveController,
controllerAs: 'example',
bindToController: true,
link: linkFunction
}
}
linkFunction.$inject = ['$scope', 'elem', 'attrs', 'ctrl'];
function linkFunction(scope, element, attrs, ctrl) {}
function directiveController() {
var example = this;
}
})();
I call this in html as <directive-function></directive-function> but it does nothing.
I created a fiddle for you.. you are doing all well, I think that you are using it like
https://jsbin.com/koporel/edit?html,js,output
<directiveFunction></directiveFunction>
No, use - where case changes, like
<directive-function></directive-function>
I have AngularJs directive accordionPanel that requires controller of parent directive accordion. I need to test accordionPanel directive to see if model changes when I call foldUnfold function. How would I write unit test to see if the model changes on foldUnfold call. Thats simplified version of my directives and test I got so far is below that:
.directive("accordion", [
function() {
return {
templateUrl: "otherurl",
transclude: true,
replace: true,
scope: {
},
controller: ["$scope",function($scope) {
this.isOneOpenOnly = function() {
return $scope.oneOpenOnly;
}
}],
link: function(scope, elem, attrs, ctrl, linker) {
// some code
}
}
}
])
.directive("accordionPanel", [
function() {
return {
templateUrl: "urlblah",
transclude: true,
replace: true,
require: "^accordion",
scope: {},
link: function(scope, elem, attrs, ctrl, linker) {
scope.foldUnfold = function() {
// some logic here then
scope.changeThisModel=ctrl.isOneOpenOnly();
}
}
}
}
])
Thats my test so far:
it('Should return unfolded as true', function() {
var scope=$rootScope.$new(),
element=$compile("<div accordion><div accordion-panel></div></div>")(scope);
scope.$digest();
scope.foldUnfold(); // this is fails as scope refers to accordion but I need to access accordionPanel
expect(scope.changeThisModel).toBe(true);
});
The problem is I cannot get access to accordionPanel scope where foldUnfold sits. I think it might be possible to access it via $$childHead and such, but even if possible it doesn't seem like the right way to do. How would I test it then?
I need to watch a model from within a directive.
angular.module('app', [])
.directive('myDirective', [function() {
return {
restrict: 'A',
scope: {
modelToWatch: '#'
},
link: function(scope, element, attrs) {
scope.$watch(scope.modelToWatch, function(val) {
// do something...
});
}
};
]})
.controller('MyController', ['$scope', function($scope) {
$scope.obj = {
foo: 'val'
};
}]);
<div ng-controller="MyController">
<div my-directive model-to-watch="obj.foo"></div>
</div>
The above works fine.
However, I encounter a problem when there is an intermediary scope between the actual owner of the model and the directive.
I used another controller to demonstrate the scenario below:
.controller('AnotherController', ['$scope', function($scope) {}])
<div ng-controller="MyController">
<div ng-controller="AnotherController">
<div my-directive model-to-watch="obj.foo"></div>
</div>
</div>
In the case for above, I could look up the $parent tree to find the scope which owns the property I want to watch using the code below:
...
link: function(scope, element, attrs) {
var contextScope = scope;
// find for the scope which owns the property that we want to watch
while (contextScope != null && contextScope.hasOwnProperty(attrs.modelToWatch)) {
contextScope = contextScope.$parent;
}
// use the scope found to watch the model
if (contextScope != null) {
contextScope.$watch(scope.modelToWatch, function(val) {
// do something...
});
}
}
Additional problem, however is if the modelToWatch is a complex expression (e.g: "tableParams.filter().shop_id" then the hasOwnProperty cannot be relied upon.
Is there an easy way to watch a model in the context of its owner scope? Or is it's possible to watch a model even from a prototypal child?
Or can I pass scope as a parameter, so at least I don't have to look for it...
restrict: 'A',
scope: {
modelToWatch: '#',
sourceScope: '=', // don't know how to do this..
}
Note: I need to use isolate scope
As suggested by #pixelbit, I tried using the $eval to find the correct scope
link: function(scope, element, attrs) {
var contextScope = scope;
// find for the scope which owns the property that we want to watch
while (contextScope != null && contextScope.$eval(attrs.modelToWatch) != undefined) {
contextScope = contextScope.$parent;
}
...
}
Works for most cases except when the modelToWatch expression actually evaluates to undefined.. There is an ambiguity whether the modelToWatch doesn't exist in the current scope (meaning it's not the owner) or the modelToWatch expression just happens to evaluate to undefined.
You can declare a controller directly inside your directive :
angular.module('app', [])
.directive('myDirective', [function() {
return {
restrict: 'A',
scope: {
modelToWatch: '='
},
link: function(scope, element, attrs) {
scope.$watch(scope.modelToWatch, function(val) {
// do something...
});
},
controller: 'MyController'
};
]})
.controller('MyController', ['$scope', function($scope) {
$scope.obj = {
foo: 'val'
};
}]);
<div my-directive model-to-watch="obj.foo"></div>
That way, when you will call your directive, your controller will be instanciated first, then the link will be executed, sharing the same scope.
You can watch a function instead:
scope.$watch(function() {
return scope.modelToWatch;
}, function(val) {
// do something
});
There is no need for an isolated scope - you can inherit scope instead. Also to address complex expressions, you can use scope.$eval to evaluate the model and find the appropriate scope. Once you've evaluated the model, return it from a watched function:
angular.module('app', [])
.directive('myDirective', [function() {
return {
restrict: 'A',
scope: false,
link: function(scope, element, attrs) {
scope.$watch(function() {
return scope.$eval(attrs.modelToWatch);
}, function(val) {
// do something...
});
}
};
]})
If you must to use an isolated scope, then watch a function and return the model:
angular.module('app', [])
.directive('myDirective', [function() {
return {
restrict: 'A',
scope: {
modelToWatch: '='
},
link: function(scope, element, attrs) {
scope.$watch(function() {
return scope.modelToWatch;
}, function(val) {
// do something...
});
}
};
]})
I want to dynamically specify a controller based on a config that we load. Something like this:
<div ng-controller="{{config.controllerNameString}}>
...
</div>
How do I do this in angular? I thought this would be very easy, but I can seem to find a way of doing this.
What you want to do is have another directive run before anything else is called, get the controller name from some model remove the new directive and add the ng-controller directive, then re-compile the element.
That looks like this:
global.directive('dynamicCtrl', ['$compile', '$parse',function($compile, $parse) {
return {
restrict: 'A',
terminal: true,
priority: 100000,
link: function(scope, elem) {
var name = $parse(elem.attr('dynamic-ctrl'))(scope);
elem.removeAttr('dynamic-ctrl');
elem.attr('ng-controller', name);
$compile(elem)(scope);
}
};
}]);
Then you could use it in your template, like so:
<div dynamic-ctrl="'blankCtrl'">{{tyler}}</div>
with a controller like this:
global.controller('blankCtrl',['$scope',function(tyler){
tyler.tyler = 'tyler';
tyler.tyler = 'chameleon';
}]);
There's probably a way of interpolating the value ($interpolate) of the dynamic-ctrl instead of parsing it ($parse), but I couldn't get it to work for some reason.
I'm using it in ng-repeat, so this is improved code for loops and sub objects:
Template:
<div class="col-xs6 col-sm-5 col-md-4 col-lg-3" ng-repeat="box in boxes">
<div ng-include src="'/assets/js/view/box_campaign.html'" ng-dynamic-controller="box.type"></div>
</div>
Directive:
mainApp.directive('ngDynamicController', ['$compile', '$parse',function($compile, $parse) {
return {
scope: {
name: '=ngDynamicController'
},
restrict: 'A',
terminal: true,
priority: 100000,
link: function(scope, elem, attrs) {
elem.attr('ng-controller', scope.name);
elem.removeAttr('ng-dynamic-controller');
$compile(elem)(scope);
}
};
}]);
Personally the 2 current solutions here didn't work for me, as the name of the controller would not be known when first compiling the element but later on during another digest cycle. Therefore I ended up using:
myapp.directive('dynamicController', ['$controller', function($controller) {
return {
restrict: 'A',
scope: true,
link: function(scope, elem, attrs) {
attrs.$observe('dynamicController', function(name) {
if (name) {
elem.data('$Controller', $controller(name, {
$scope: scope,
$element: elem,
$attrs: attrs
}));
}
});
}
};
}]);
I have 2 directives, one for searching and one for pagination. The pagination directive needs to access the search directive to find out what property we're currently searching by. When I load the page though, it throws an error saying Error: [$compile:ctreq] Controller 'search', required by directive 'pagination', can't be found!. However I have a controller setup in my search directive.
Here is my search directive:
angular.module('webappApp')
.directive('search', function ($route) {
return {
templateUrl: 'views/search.html',
restrict: 'E',
scope: {
searchOptions: '=',
action: '=',
currentProperty: '=',
currentValue: '='
},
controller: function($scope) {
$scope.searchBy = $scope.searchOptions[0].text;
$scope.searchByProperty = $scope.searchOptions[0].property;
$scope.setSearchBy = function(event, property, text) {
event.preventDefault();
$scope.searchBy = text;
$scope.searchByProperty = property;
};
$scope.search = function() {
$scope.searching = true;
$scope.currentProperty = $scope.searchByProperty;
$scope.currentValue = angular.element('#searchCriteria').val();
$scope.action($scope.searchByProperty, $scope.currentValue, function() {
$scope.searching = false;
});
};
$scope.reload = function() {
$route.reload();
};
}
};
});
Here is my pagination directive:
angular.module('webappApp')
.directive('pagination', function () {
return {
templateUrl: 'views/pagination.html',
restrict: 'E',
require: '^search',
scope: {
basePath: '#',
page: '=',
sort: '='
},
link: function(scope, element, attrs, searchCtrl) {
console.debug(searchCtrl);
scope.searchByProperty = searchCtrl.searchByProperty;
}
};
});
In order for one directive to use another's controller by use of require, it needs to either share the same element as the controller containing directive, or it has to be a child of it.
You can't use require in the way you have, where the elements are siblings.
Angular docs about directives, including require
If it doesn't make sense to rearrange the DOM in the way I've described, you should inject a service into both directives which contains the data/methods you wish to share between the two.
Note: you could also experiment with the $$nextSibling / $$prevSibling properties of the directives' scopes, but this would present only a very fragile solution
You cannot use require in directive like that, however , since the only thing you need to pass between directives is a string , just bind them to the same property in parent controller (it can be parent directive controller):
...
<div ng-app='app' ng-controller='MyCtrl as ctrl'>
<my-dir-one s1='ctrl.message'></my-dir-one>
<my-dir-two s2='ctrl.message'></my-dir-two>
and first directives:
app.directive('myDirOne', function ($route) {
return {
templateUrl: 'views/my-dir-one.html',
restrict: 'E',
scope: {
s1: '=',
second directive
app.directive('myDirTwo', function ($route) {
return {
templateUrl: 'views/my-dir-one.html',
restrict: 'E',
scope: {
s2: '=',