I tried to write a directive making an arbitrary element work like a link. Something like
<span goto="overview">Overview</span>
to be translated into something like
<span ng-click="$location.path('#/overview')">Overview</span>
or whatever will work. As a bonus I'd like the link to work, even when the current URL is already .../#overview.1
This sound pretty trivial, but I'm stuck with something like
.directive('goto', function($location) {
return {
template: function(element, attrs) {
// Here `attrs` should be used but how?
var ref = "#/" + element.attr('goto');
// This is a mystery to me.
// I guess, transclusion is the way to go, but how exactly?
var elementContent = getEverythingBelowElementButHow();
// I doubt `$location` will be visible on the invocation site.
return "<span ng-click='$location.path(" +
ref + "'>" +
elementContent +
"</span>";
}
};
})
I'm perfectly sure, I'm doing it all wrong. I'm nearly sure it's all unnecessary complicated. However, I'm hoping to be able to learn something from the answers.
1I found myself repeatedly clicking on such a link in order to get to the "pristine overview state" and wondering that nothing happens. Obviously, the browser ignores links to the current location, but there could be a way around this?
You can add click event to directive dynamically.
angular.module('ExApp')
.directive('goto', function($location) {
return {
restrict: 'A',
scope: {
//scope variables
},
link: function(scope, element, attr) {
element.on('click', function() {
$location.path('/overview');
scope.$apply();
});
}
}
});
plunker : http://plnkr.co/edit/n6VbZnAVzM4y0p8dbmvf?p=preview
Take a look at this example:
app.directive('goto', function($location){
function link(scope, element, attr){
scope.gotoLink = function(){
alert(scope.goto);
//$location.path(scope.goto);
};
}
return {
restrict: 'A',
transclude: true,
scope:{ goto: '#' },
template: '<span ng-click="gotoLink()"><span ng-transclude></span></span>',
link: link
};
});
You can play with it here (plnkr)
Related
All:
Suppose I have two directives( dir1 and dir2) , which are both isolated scope. From some posts, I learnt that I need to use "require" to get scope of the other directive, but there is one question confused me so much:
Suppose I use ng-repeat generated a lot of dir1 and dir2, how do I know in certain dir1, which specific dir2's controller scope is required( cos there are many dir2 and in my understanding, all those scopes in dir2 controller are independent to each other)?
For example:
app.directive("dir1", function(){
var counter = 0;
return {
restrict:"AE",
scope:{},
template: "<button class='dir1_btn'>highlight dir2_"+(couter++)+" </button>",
link:function(scope, EL, attrs){
EL.find("button.dir1_btn").on("click", function(){
// How to let according dir2 know this click?
})
}
}
})
app.directive("dir2", function(){
var counter = 0;
return {
restrict:"AE",
scope:{},
template: "<span class='dir2_area'>This is dir2_"+(couter++)+" </span>",
link:function(scope, EL, attrs){
// Maybe put something here to listening the event passed from dir1?
}
}
})
And the html like( for simple purpose, I just put 2 of each there, actually it will generated by ng-repeat) :
<dir1></dir1>
<dir2></dir2>
<dir1></dir1>
<dir2></dir2>
Consider this just like the switch and light, dir1 is the switch to open(by change background-color) according light (dir2).
In actual project, what I want to do is angularJS directive version
sidemenu and scrollContent, each item in sidemenu is a directive,
click it will make according content(another directive) to auto scroll
to top.
I wonder how to do this? I know this is easy in jQuery, just wondering how to hook this into AngularJS data-driven pattern.
Thanks
The most important thing to note here is that I think you want to use ng-class Since you are creating both directives in an ng-repeat, I assume you are iterating over a list of objects (even if they are two separate ng-repeats, if you iterate over the same list of objects in both it will work. JQuery should not be necessary)? Attach an ngClass object to each object you iterate over, put it on an ng-class attribute in your dir2, then give dir1 access to change it. ngClass provides animation hooks if you want to animate the transition. The rest of my answer may help, though I would like to redo it now that I thought of ng-class. I have to get back to work for now though. I'll watch for feedback and try to answer quickly if you have questions.
I think there are probably a few ways to better accomplish what you are trying to do. It is not clear why both of your directives need to have isolate scopes. As I use angular more I find that though isolating a scope is a powerful technique, it is best to avoid over using it.
As for the require directive property, this post explains how to make directives communicate via their controllers very well.
I have two possible suggestions for you.
Make it one directive
Why can't you just put the templates into one?
Or if as I assume there is some reason they need to be separate, you could consider just sharing an object between them.
<div ng-repeat='obj in sharedDirObjs'>
<dir1 shared-dir-obj='obj'></dir1>
<dir2 shared-dir-obj='obj'></dir2>
</div>
app.controller('ctrl', function() {
$scope.sharedDirObjs = [obj1, obj2, obj3]
});
app.directive("dir1", function(){
var counter = 0;
return {
restrict:"AE",
scope:{sharedDirObj : '='},
template: "<button class='dir1_btn' ng-click='clickFn()'>highlight dir2_"+(couter++)+" </button>",
link:function(scope, EL, attrs){
var dir1vars...
scope.clickFn = function(){
// dir1 logic...
scope.sharedDirObj.dir2.clickFn(dir1vars...);
};
}
}})
app.directive("dir2", function(){
var counter = 0;
return {
restrict:"AE",
scope:{sharedDirObj : '='},
template: "<span class='dir2_area'>This is dir2_"+(couter++)+" </span>",
link:function(scope, EL, attrs){
scope.sharedDirObj.dir2 = {};
scope.sharedDirObj.dir2.clickFn(dir1vars...) {
// access to dir2 vars
};
}
}})
Similarly, you could create a service that holds an array of objects that are shared by injecting the service and indexed using the $index from the ng-repeat, or you could use an id system as PSL suggests. Note that the solution I describe above could work with isolate scope, or without it using scope.$eval(attr.sharedObj); on either or both of your directives. The answer to this question provides a solid runthrough of when and why to use isolated scope. In any case it would likely be best not to pipe functions through a shared object as I am showing above, and timing issues would need to be dealt with. Better would be to store properties on the object and set a scope.$watch on them in your dir2.
You may have to use some sort of strategy. Some kind of identifier hook up. Clearly you cannot use require(to require the controller of a directive and you don't have any also it can only look up to ancestors or itself not siblings). For example you could add an id attribute and a for attribute and target the element with a selection based on specific attribute value and fire an event. With this position of related element does not matter.
Your directive could look like:
<dir1 dir-for="id1"></dir1>
<dir2 dir-id="id1"></dir2>
<dir1 dir-for="id2"></dir1>
<dir2 dir-id="id2"></dir2>
and simple implementation:
.directive("dir1", function($document) {
var counter = 0;
return {
restrict: "AE",
scope: {
dirFor: '#'
},
template: "<button class='dir1_btn' ng-click='handleClick()'>highlight dir2_({{dirFor}}) </button>",
link: function(scope, EL, attrs) {
var $target = angular.element(
$document[0].querySelector('[dir-id="' + scope.dirFor + '"]'))
.contents().scope();
var clicked = false;
scope.handleClick = function() {
clicked = !clicked;
targetScope.$broadcast("SWITCH_CLICKED", clicked);
}
scope.$on('$destory',function() {
$target = null;
}
}
}
})
app.directive("dir2", function() {
var counter = 0;
return {
restrict: "AE",
scope: {
dirId: '#'
},
template: "<span class='dir2_area' ng-class=\"{true:'on', false:'off'}[status]\">This is dir2_{{dirId}}</span>",
link: function(scope, EL, attrs) {
console.log(scope.$id);
scope.status = false;
scope.$on('SWITCH_CLICKED', function(e, data) {
scope.status = data;
});
}
}
});
Demo
var app = angular.module('app', []).controller('ctrl', angular.noop);
app.directive("dir1", function($document) {
var counter = 0;
return {
restrict: "AE",
scope: {
dirFor: '#'
},
template: "<button class='dir1_btn' ng-click='handleClick()'>highlight dir2_({{dirFor}}) </button>",
link: function(scope, EL, attrs) {
var $target = angular.element($document[0].querySelector('[dir-id="' + scope.dirFor + '"]')).contents();
var clicked = false;
scope.handleClick = function() {
clicked = !clicked;
$target.scope().$broadcast("SWITCH_CLICKED", clicked);
}
scope.$on('$destroy',function() {
$target = null;
});
}
}
})
app.directive("dir2", function() {
var counter = 0;
return {
restrict: "AE",
scope: {
dirId: '#'
},
template: "<span class='dir2_area' ng-class=\"{true:'on', false:'off'}[status]\">This is dir2_{{dirId}}</span>",
link: function(scope, EL, attrs) {
scope.status = false;
scope.$on('SWITCH_CLICKED', function(e, data) {
scope.status = data;
});
}
}
})
.on{
color:green;
}
.off{
color:blue;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.15/angular.min.js"></script>
<div ng-app="app" ng-controller="ctrl">
<dir1 dir-for="id1"></dir1>
<dir2 dir-id="id1"></dir2>
<dir1 dir-for="id2"></dir1>
<dir2 dir-id="id2"></dir2>
</div>
I have used $document[0].querySelector('[dir-id="' + scope.dirFor + '"]')).contents().scope() to get hold of the scope, similarly you could do .controller to get hold of the controller instance as well. Current example is doing an absolute selection(with document), you could as well make it relative.
As we known, the angular have performance if there are more than 2000 records in the page, because only some of the records need add some behavior , so I would prefer to dynamically add attribute to element according model value in link function, as there will be less watches.
So I use $compile to recompile the element like below:
mainApp.directive("popoverSetting", function ($compile) {
return {
restrict: "A",
link: function (scope, element, attrs) {
if (scope.item.isTrue) {
element.attrs("ns-popover-trigger", "mouseenter");
element.attrs("ns-popover-timeout", "0.01");
$compile(element)(scope);
}
}
}
})
Because there are about 1000 records, so the speed is very slow, is there some other way to add attribute and compile quickly? although there only 5 records need to add these attribute, it still increase about twofold time than before.
The problem with trying to do this is that nested compiles are slow (an understatement), especially when you are talking thousands of them. It just isn't the way to go.
I could see where some may consider this ugly, and depending on your needs, this solution may NOT work for you, but it does get everything compiled pretty quickly, with the attributes applied. I ended up building a compiler directive that would generate markup based on the dataset, adding the necessary attributes if isTrue was true, and then compiling the whole thing once. The ugly part is that it doesn't generate bindings. So if you need to change something or update something, you'll have to recompile the whole thing.
It's running right now at about 200-300ms to compile the whole thing with 2000 items in the dataset.
Plunker
Directives:
app.directive('nsPopoverTrigger', function($timeout) {
return {
restrict: 'A',
link: function(scope, element, attrs) {
element.on(attrs.nsPopoverTrigger, function() {
console.log('popover triggered')
})
}
}
})
app.directive('compiler', function($compile) {
return {
restrict: 'A',
link: function(scope, element, attrs) {
var i = 0, template = '<ul>'
scope.generateMarkup = function() {
for (i; i < scope.data.length; i++) {
if(scope.data[i].isTrue) {
template = template + "<li ns-popover-trigger='mouseenter' ns-popover-timeout='0.01'>" + scope.data[i].key + "</li>"
} else {
template = template + '<li>' + scope.data[i].key + '</li>'
}
}
var $template = angular.element(template)
$compile($template)(scope)
element.append($template)
}
}
}
})
because I don't remove the directive so it makes the angular compile element over and over again. so i need to remove the directive or set a flat to determine if it is necessary to compile again.
function groupPopoverDirective($compile) {
return {
restrict: "A",
scope:true,
link: function ($scope, $element, $attrs) {
$scope.groupPopoverData = $scope.$eval($attrs.groupPopoverData);
if ($scope.groupPopoverData.isTrue) {
if ($element.attr("ns-popover")) {
return;
}
$element.attr("ns-popover", "true");
$element.attr("ns-popover-template", "popover-template.html");
$element.attr("ns-popover-trigger", "mouseenter");
$element.attr("ns-popover-placement", "right|top");
$element.attr("ns-popover-mouse-relative", "x");
$element.attr("ns-popover-theme", "ns-popover-tooltip-theme");
$element.attr("ns-popover-timeout", "0.01");
//element.removeAttr('popover-info');
var linkFn = $compile($element);
var pp = linkFn($scope);
}
}
};
};
I have some code that I have taken over. It looks like this:
<div id="init"
data-admin-template
ng-show="ss.display.init"
data-template-url="init.html">
</div>
There's a directive adminTemplate:
app.directive('adminTemplate', ['stateService', function (stateService) {
return {
restrict: 'A',
templateUrl: function (elem, attrs) {
return "/Content/app/admin/templates/" + attrs.templateUrl;
},
link: function (scope, element, attrs) {
scope.stateService = stateService;
}
};
}]);
I am not very familiar with templates and it seems that this directive does not really do much. Is there a way I can do this without the directive?
As #Oli said in the comments, ng-include is definitely one solution. There are others as well - angular likes to offer you just enough options to leave you doubting yourself - but i'm wondering what you'd actually gain by changing it.
With ng-include you'd need to add a controller to to your html, to supply stateService (if you don't do it here, you'd have to add it to every different admin template). So you'd end up with:
<div id="init"
ng-include="/Content/app/admin/templates/init.html"
ng-show="ss.display.init"
ng-controller="AdminController">
</div>
So you end up with the same amount of attributes, you need the whole template path and it becomes less readable. Looking at what you have now, it's clear to see the intent.
You could also go one step further and give it the flexibility of being an element or attribute
<admin-template
id="init"
ng-show="ss.display.init"
data-template-url="init.html">
</admin-template>
app.directive('adminTemplate', ['stateService', function (stateService) {
return {
restrict: 'EA',
templateUrl: function (elem, attrs) {
return "/Content/app/admin/templates/" + attrs.templateUrl;
},
link: function (scope, element, attrs) {
scope.stateService = stateService;
}
};
}]);
It may appear to be doing very little, but my feeling is that the previous developer has refactored out repetition, to get to this little directive. I'd argue that as it is it removes boiler plate, allows easy re-usability and communicates intent well in your mark up.
I need to update my model after is has loaded its data. So i tried to write a directive which can do that. I didn't thought it would be this hard :(
I first tried a filter, which should be more simple, but got this error.
Error: [ngModel:nonassign] Expression 'editpage.url | addUrl' is non-assignable.
So now i try the directive way. This is my html code in the view:
<input ng-model="editpage.url" add-url type="text" class="light_txtbox" readonly>
And this is my directive:
app.directive('addUrl', [function() {
return {
restrict: 'A',
require: '?ngModel',
replace: true,
link: function(scope, element, attrs, ngModel) {
if (!ngModel) return;
// what to do next?
}
};
}]);
In the "what to do next" part i tried a watch like this:
scope.$watch(attrs.ngModel, function(site) {
if (typeof site !== undefined) {
ngModel.$setViewValue('www.mysite.com/' + site);
ngModel.$render();
}
});
But of course now the model is updated and "hey, i am changed so update again!" and again and again...
I only need the update to take place once. I think i need another approach, but can not figure out what to do.
You can unregister a $watch
var unregister = scope.$watch(attrs.ngModel, function() {
if (shouldStopWatching) {
unregister();
}
});
where shouldStopWatching is whatever condition you need (i.e. stop on second call of callback etc)
I have a directive myDirective with variable type. If I run <my-directive type="X"> I want the directive to use templateUrl: x-template.html.
If I do <my-directive type="Y"> I want the directive to use templateUrl: y-template.html.
This is my current directive.
app.directive('myDirective', function() {
var myDirective = {
templateUrl: 'X-template.html',
restrict: 'E',
scope: {
type: '='
},
};
return myDirective;
});
I read thru stackoverflow and angular documentation but have not found anything that I need.
I am now trying to do something along the lines of:
if ($scope.type === 'X') {
templateUrl: 'X-template.html',
}
else if ($scope.type === 'Y') {
templateUrl: 'Y-template.html',
}
But do not know where to do it.
Do you guys know if this is possible and how?
Angular will accept a function as the template option, so you could do something like so:
.directive('myDirective', function () {
return {
templateUrl: function (tElement, tAttrs) {
if (tAttrs) {
if (tAttrs.type === 'X') {
return 'X-template.html';
}
if (tAttrs.type === 'Y') {
return 'Y-template.html';
}
}
}
}
});
For more info, see the documentation for the $compile service.
You can work around this issue using ng-include inside compile:
app.directive('myDirective', function() {
return {
restrict: 'E',
compile: function(element, attrs) {
element.append('<div ng-include="\'' + attrs.type + '-template.html\'"></div>');
}
}
});
fiddle
If you're willing to live on the bleeding edge with a build on the 1.1.x code path (note the warning attached to every 1.1.x build notes entry so I don't dilute this answer by repeating it again here), you're in luck--this very feature was just added in the 1.1.4 release on April 3rd. You can find the release notes for 1.1.4 here and the feature's task log includes a Jasmine test that demonstrates how to use the new functionality.
If you're more conservative and are using a 1.0.x release, then you won't be able to accomplish this as easily, but it can be done. Mark Rajcok's solution looks like it would fit your requirements as-stated, but I would just add a few additional notes:
Aside from its 1.1.4 release, compile-time directives don't support modification at runtime.
As of 1.1.4, you can safely modify the attributes of compile-time directives, but only from another compile-time directive.
You may want to consider replaceWith() instead of append() since <my-directive> is not a standard-defined HTML element type.
If your X and Y templates contain additional directives, I don't think you'll be able to pass attributes on <my-template> through to the root element of your template so easily.
A directive with replace: true will transfer attributes from the source element to its replacement root, but I do not think that ngInclude will do the same from is host to the root of the included template.
I also seem to recall that ngInclude does not require that its template have exactly one root element.
You could perhaps preserve attributes on a replacement parent by using replaceWith() instead of append() and wrapping the <div ng-include=""> tag within a <div></div>. The outer <div> could hold attributes and would still be accessible after the <div ngInclude> element replaced itself with loaded content.
Be aware that ngInclude creates a new scope. This subjects you to a flashing yellow klaxons warning about the dangers of primitive scope models. For more information, see this fine page from Angular's GitHub depot.
I can propose another alternative for those on 1.0.x, but it involves a fair amount of code. It's a more heavy-weight operation, but it has the upside of not only being able of switching between templates, but full-fledged directives as well. Furthermore, its behavior is more readily dynamic.
app.directive('myDirective', function() {
return {
restrict: 'E',
replace: true,
templateUrl: 'partials/directive/my-directive.html',
link: function(scope, element, attrs, ctrl) {
// You can do this with isolated scope as well of course.
scope.type = attrs.type;
}
}
);
my-directive.js
<div ng-switch on="{{type}}">
<div ng-switch-where="X" ng-include="X-template.html"></div>
<div ng-switch-where="Y" ng-include="Y-template.html"></div>
</div>
my-directive.html
This is my version for optionally overriding a default template
templateUrl: function (elem, attrs) {
if (attrs.customTemplate) {
return '/path/to/components/tmpl/' + attrs.customTemplate + '.html';
} else {
return '/path/to/components/tmpl/directive.html';
}
}
e.g on a directive
<div my-directive custom-template="custom"></div>
I solve this problem so:
app.directive("post", function ($templateCache, $compile) {
function getTemplate(mode) {
switch (mode) {
case "create":
return "createPost.html";
case "view":
return "viewPost.html";
case "delete":
return "deletePost.html";
}
}
var defaultMode = "view";
return {
scope: {},
restrict: "AE",
compile: function (elem, attrs, transclude) {
return function ($scope, $element, $attr) {
function updateTemplate() {
$element.html("");
$compile($templateCache.get(getTemplate($scope.mode)).trim())($scope, function (clonedElement, scope) {
clonedElement.appendTo($element);
});
}
$scope.mode = $attr.mode || defaultMode;
$scope.$watch("mode", updateTemplate);
}
}
}
});
It's probably not the best way to do this, but I have no extra scope.
Ok, this might help someone here :-)
To inject your custom attr into your link or controller function use the following.
I'm at work right now but will post a fiddle later if I get a chance :-)
.directive('yourDirective', function() {
return {
restrict: 'EA',
template: '<div></div>', // or use templateUrl with/without function
scope: {
myAttibute: '#myAttr' // adds myAttribute to the scope
},
link: function(scope) {
console.log(scope.myAttibute);
}
}
}
// HTML ""
// Console will output "foo"