Directive has no access to transcluded elements? - javascript

I might get some concept terribly wrong, but I don't get this to work I expected:
http://plnkr.co/edit/Qe2IzMMMR5BJZJpwkx9e?p=preview
What I'm trying to do is to define a directive that gets attached to a top-level <nav> element, and then modifies the contained DOM elements in its link function (such as adding css classes to <li> etc.).
However, the link function seems to only get the original directive template (<nav><ul><ng-transclude/></ul></nav), and not the transcluded/expanded DOM elements.
If this is "by design", how should I do this?
It find it pretty useless to define a transcluding "root" directive, if it does not have access to the transcluded DOM tree....

Please read some of my answers about transclusion in angular:
What is the main use of transclusion in angularjs
Why I can't access the right scope?
As to your question:
First, it's not useless , even if not fit to your use case.
Yes, it's by design - but it's just the default behavior of ng-transclude.
If it was the opposite then everyone would be yelling about scope leaking.
You can do anything you want with $transclude, just be careful.
There are probably better solutions like creating isolated scope with bindings.
This is what you wanted (plunker):
angular.module('app').directive ('myNav', ['$timeout', function($timeout) {
return {
replace: false,
transclude: true,
template: '<nav><ul></ul></nav>',
link: function (scope, element, attrs,ctrl,$translcude){
$transclude(scope,function(clone){
element.find('ul').html(clone)
});
var items = element.find('li'); //element.find('ng-transclude') === 1 !
window.console.log (items.length);
}
};

(Correct answers see above from Ilan and others)
I finally got my (simple) use case working without transclude at all with the old dirty $timeout hack: http://plnkr.co/edit/FEEDYJLK9qRt0F4DNzRr?p=preview
link: function(scope, element) {
// add to end of event queue
$timeout(function() {
var items = element.children('ul:first').children('li');
window.console.log(items.length);
}, 0);
}
I know this is a bad thing to do, and not totally sure if this will work always, but at least seems to work for my simple case...

I think the issue is that you have a ng-repeat within the directive so the "element" is not able to access the child nodes until the ng-repeats have been resolved. A way around this is to have your directive on each of the list tags. I'd add transclude to the tag, and then you can remove the template from your directive all together.
You'd end up with something like:
<li ng-repeat="item in menuItems" my-nav ng-transclude>
Your directive would look like
angular.module('app').directive ('myNav', ['$timeout', function($timeout) {
return {
replace: false,
transclude: true,
compile: function (element, attrs, transclude){
// this will always return 0 unless you split this into two directives
// and emit or watch for the ng-repeats to complete in the parent
// directive
//var items = $(element).find('li'); //element.find('ng-transclude') === 1 !
//instead showing you how to access css for the given element
element.css( "color", "red" );
}
};
}]);
As I mentioned in the comments above, you could split the directive into two directives: one at the nav level and one on your ng-repeat that simply emits when the repeats are done and you can apply css accordingly as the find will then be able to find the child nodes as they are resolved. I think that approach is redundant however, as you'd be setting css for nodes to which you've already applied your change. I think as noted in one of the comments below, smaller directives work better and your project is less likely to become a transcluded mess of spaghetti like scopes. Happy coding :)

Related

Inject object into scope of transcluded content in Angular 1.3

I would like to create a directive that has transcluded content that the directive can bind to and modify. The directive has an isolate scope. I imagine it working something like this:
<my-directive bound-item-name="childObj">
<input ng-model="childObj.someField">
</my-directive>
At runtime, I want to use childObj as an alias for an object on my-directive's isolate scope called activeObject. Essentially, you might think of this as similar to the way ng-repeat lets you use a statment like obj as alias in objList and in the transcluded content alias refers to the individual instance.
I can't seem to figure out how I can actually do this... if I change the transluded content to refer to $parent.activeItem it does work the way I intended, but I feel like that's expecting the transcluded content to know too much about how the directive works. It seems like modifying in the compile function might work, except I can't see, in the docs, how I can actually do that with the transcluded content. Forcing the transcluded content to share its scope with the directive would be OK, although I see no evidence that there's some way to do that.
This must be possible, but how?
Fiddling around with this some more, I am able to get it to work by modifying scope.$$childHead[scope.boundItemName] instead of using scope.activeObject in the directive. While this works I'd like to not rely on undocumented internal objects, if possible.
The link function of the directive is given the transclude function as the 5th parameter.
link: function(scope, element, attrs, ctrls, transclude){
// ...
}
This transclude function takes a scope variable that you can create and another function - called "clone linking function" - that places the pre-linked transcluded content in the DOM. The transclude function links against that scope variable that you provided.
Here's how it works.
transclude: true,
scope: {}, // you are free to use whatever scope you need
link: function(scope, element, attrs, ctrls, transclude){
var boundObj = {}; // your object
var alias = attrs.boundItemName;
// let's create an isolate scope for the transcluded content
var newScope = scope.$new(true);
newScope[alias] = boundObj;
transclude(newScope, function(preLinkContent){
element.append(preLinkContent);
});
}
Then, if you used your example:
<my-directive bound-item-name="foo">
<input ng-model="foo.text">
</my-directive>
Then, the transcluded ng-model would write into your internal boundObj's .text property.
Demo

Can we use directives dynamically in AngularJS app

I am trying to call (or use) few custom directives in ionic framework, dynamic is like <mydir-{{type}} where {{type}} will come from services and scope variable, having values radio, checkbox, select etc, and created my directives as mydirRadio, MydirCheckbox, mydirSelect, But its not working.
Is their any good approach to get the dynamic html as per {{type}} in scope?
Long story short; no you can't load directives dynamically in that way.
There are a few options for what you can do. You can, as other answers have mentioned, pass your context as an attribute (mydir type="checkbox"). You could make a directive that dynamically loads another directive, as also mentioned by others. Neither of these options are imo every good.
The first option only works if you write the directive yourself, not when using something like ionic. It also requires you to write multiple directives as one, which can get very messy very quickly. This mega directive will become hard to test and easy to mess up when maintaining it in the future. Note that this is the correct way to pass data to a directive from the view, it's just not good for this specific use case.
The second option is problematic because obfuscates things a bit too much. If someone reads your html and sees a directive called dynamic that is given dynamic data... they have no idea what is going to happen. If they see a directive called dropdown that is given a list they have a fair idea of what the result will be. Readability is important, don't skimp on it.
So I would suggest something simpler that requires much less work from you. Just use a switch:
<div ng-switch="type">
<mydir-select ng-switch-when="select"></mydir-select>
<mydir-checkbox ng-switch-when="checkbox"></mydir-checkbox>
</div>
I dont understand why do you need dynamic directives.
Simple use single directive and change the template accordingly.
For example -
angular.module('testApp')
.directive('dynamicDirective', function($compile,$templateCache,$http) {
return {
restrict: 'C',
link: function($scope,el) {
//get template
if(radio){
$http.get('radio.html', {cache: $templateCache}).success(function(html){
//do the things
el.replaceWith($compile(html)($scope));
});
} else if(checkbox){
//load checkbox template
} //vice-versa
}
};
});
You can inject service variable in directive also.
a bit more code would help. I don't know, if its possible to do dynamic directives like the ones in a tag
<{dyntag}></{dyntag}>
but you also can use an expression like
<your-tag dynamic_element="{type}">...</your-tag>
which should have exactly the same functionality. In your case it would be like:
Your JSObject ($scope.dynamics):
{"radio", "checkbox", "select"}
and your HTML:
<div ng-repeat="dyn in dynamics">
<your-tag dynamic_element="{dyn}"></your-tag>
</div>
Yes, that's not a problem. You can interpolate your data using {{}} and in your directive compile a new element using that data:
myApp.directive('dynamic', function($compile, $timeout) {
return {
restrict: "E",
scope: {
data: "#var" // say data is `my-directive`
},
template: '<div></div>',
link: function (scope, element, attr) {
var dynamicDirective = '<' + scope.data + ' var="this works!"><' + scope.data + '>';
var el = $compile(dynamicDirective)(scope);
element.parent().append( el );
}
}
});
HTML:
<div ng-controller="MyCtrl">
<dynamic var="{{test}}"></dynamic>
</div>
Fiddle

Prevent controller from creating new scope object

I am passing a custom scope object to the $compile and creating a custom template. If I apply a directive on the elements inside the template, scope that is changing is the one that is passed to the $compile, and that's really what I wanted.
However, I just thought that it might be good to also have a controller on some elements inside the template,
<div ng-controller="controllerName" >
</div>
but ng-controller doesn't set data on the passed scope but creates its own and uses that one. Is there a way to make ngController to use existing scope and not create a new one ?
We create our controllers and wrap them in factories to make them accessible. We apply or controllers through directives (also going away). This gives you a controller that is scoped to the directive, which has better control for scope, this works for us as the directives where we do this for are usually components.
I don't know if this will be an option given the road you are down now. I would suggest trying to stop using ng-controller. You may want to look at angular 2 now just to keep it in mind as a migration path, it is coming in the fairly near future. They have removed ng-controller, a lot of what they are doing in angular 2 can be done now.
This is a good resource on why these things are a bad idea
https://www.youtube.com/watch?v=gNmWybAyBHI&t=9m10s
If you look at the source code for ng-controller, you will see it is very simple:
var ngControllerDirective = [function() {
return {
restrict: 'A',
scope: true,
controller: '#',
priority: 500
};
}];
You can actually create an almost identical alternate directive that just defines scope: false (or omits the scope key altogether, same thing):
app.directive('controllerNoScope', function () {
return {
restrict: 'A',
scope: false,
controller: '#',
priority: 500 // same as ng-controller
}
});
(You may want to give it a better name).
See this Plunkr for a demo that shows the scope has the same $id as the outer one, meaning it is the same scope.

AngularJs: Binding a property of a scope object to a directive

I'm somewhat new to AngularJs, so forgive me if this is a newb question, but I've looked around a bit and haven't been able to figure this out.
I'm trying to send an attribute of an object into a directive and I'm not quite sure why this isn't working.
I've got a scope variable that's an object, something like:
$scope.player = {name:"", hitpoints:10};
In my HTML, I'm attempting to bind that to a directive:
<span accelerate target="player.hitpoints" increment="-1">Take Damage</span>
In my directive, I'm attempting to modify player.hitpoints like this:
scope[attrs.target] += attrs.increment;
When I trace it out, scope[attrs.target] is undefined, even though attrs.target is "player.hitpoints." When I use target="player", that traces out just fine but I don't want to have to manipulate the .hitpoints property explicitly in the directive.
Edit: I've made a jsfiddle to illustrate what I'm trying to do: http://jsfiddle.net/csafo41x/
There is a way to share scope between your controller and directive. Here is very good post by Dan Wahlin on scope sharing in Directive - http://weblogs.asp.net/dwahlin/creating-custom-angularjs-directives-part-2-isolate-scope
There are 3 ways to do so
# Used to pass a string value into the directive
= Used to create a two-way binding to an object that is passed into the directive
& Allows an external function to be passed into the directive and invoked
Just a very basic example on how the above mentioned scope are to be used
angular.module('directivesModule').directive('myIsolatedScopeWithModel', function () {
return {
scope: {
customer: '=' //Two-way data binding
},
template: '<ul><li ng-repeat="prop in customer">{{ prop }}</li></ul>'
};
});
There are a number of things going on here:
#1 - scope
Once you define your isolated scope (along the lines of #Yasser's answer), then you don't need to deal with attrs - just use scope.target.
#2 - template
Something actually needs to handle the click event. In your fiddle there is just <span class="btn"...>. You need ng-click somewhere. In your case, you probably want the directive to handle the click. So modify the directive's template and define the click handler in the directive's scope:
...
template: "<button class='btn' ng-click='click()'>Button</button>",
link: function(scope, element, attrs)
{
scope.click = function(){
scope.target += parseInt(attrs.increment);
}
}
...
#3 - transclude
Now, you need to get the contents of the directive to be the contents of the button within your directive's template. You can use transclude parameter with ng-transclude - for location, for that. So, the template above is modified to something like the following:
...
template: "<button class='btn' ng-click='click()'><div ng-transclude</div></button>",
transclude: true,
...
Here's your modified fiddle

Angular JS: What is the need of the directive’s link function when we already had directive’s controller with scope?

I need to perform some operations on scope and the template. It seems that I can do that in either the link function or the controller function (since both have access to the scope).
When is it the case when I have to use link function and not the controller?
angular.module('myApp').directive('abc', function($timeout) {
return {
restrict: 'EA',
replace: true,
transclude: true,
scope: true,
link: function(scope, elem, attr) { /* link function */ },
controller: function($scope, $element) { /* controller function */ }
};
}
Also, I understand that link is the non-angular world. So, I can use $watch, $digest and $apply.
What is the significance of the link function, when we already had controller?
After my initial struggle with the link and controller functions and reading quite a lot about them, I think now I have the answer.
First lets understand,
How do angular directives work in a nutshell:
We begin with a template (as a string or loaded to a string)
var templateString = '<div my-directive>{{5 + 10}}</div>';
Now, this templateString is wrapped as an angular element
var el = angular.element(templateString);
With el, now we compile it with $compile to get back the link function.
var l = $compile(el)
Here is what happens,
$compile walks through the whole template and collects all the directives that it recognizes.
All the directives that are discovered are compiled recursively and their link functions are collected.
Then, all the link functions are wrapped in a new link function and returned as l.
Finally, we provide scope function to this l (link) function which further executes the wrapped link functions with this scope and their corresponding elements.
l(scope)
This adds the template as a new node to the DOM and invokes controller which adds its watches to the scope which is shared with the template in DOM.
Comparing compile vs link vs controller :
Every directive is compiled only once and link function is retained for re-use. Therefore, if there's something applicable to all instances of a directive should be performed inside directive's compile function.
Now, after compilation we have link function which is executed while attaching the template to the DOM. So, therefore we perform everything that is specific to every instance of the directive. For eg: attaching events, mutating the template based on scope, etc.
Finally, the controller is meant to be available to be live and reactive while the directive works on the DOM (after getting attached). Therefore:
(1) After setting up the view[V] (i.e. template) with link. $scope is our [M] and $controller is our [C] in M V C
(2) Take advantage the 2-way binding with $scope by setting up watches.
(3) $scope watches are expected to be added in the controller since this is what is watching the template during run-time.
(4) Finally, controller is also used to be able to communicate among related directives. (Like myTabs example in https://docs.angularjs.org/guide/directive)
(5) It's true that we could've done all this in the link function as well but its about separation of concerns.
Therefore, finally we have the following which fits all the pieces perfectly :
Why controllers are needed
The difference between link and controller comes into play when you want to nest directives in your DOM and expose API functions from the parent directive to the nested ones.
From the docs:
Best Practice: use controller when you want to expose an API to other directives. Otherwise use link.
Say you want to have two directives my-form and my-text-input and you want my-text-input directive to appear only inside my-form and nowhere else.
In that case, you will say while defining the directive my-text-input that it requires a controller from the parent DOM element using the require argument, like this: require: '^myForm'. Now the controller from the parent element will be injected into the link function as the fourth argument, following $scope, element, attributes. You can call functions on that controller and communicate with the parent directive.
Moreover, if such a controller is not found, an error will be raised.
Why use link at all
There is no real need to use the link function if one is defining the controller since the $scope is available on the controller. Moreover, while defining both link and controller, one does need to be careful about the order of invocation of the two (controller is executed before).
However, in keeping with the Angular way, most DOM manipulation and 2-way binding using $watchers is usually done in the link function while the API for children and $scope manipulation is done in the controller. This is not a hard and fast rule, but doing so will make the code more modular and help in separation of concerns (controller will maintain the directive state and link function will maintain the DOM + outside bindings).
The controller function/object represents an abstraction model-view-controller (MVC). While there is nothing new to write about MVC, it is still the most significant advanatage of angular: split the concerns into smaller pieces. And that's it, nothing more, so if you need to react on Model changes coming from View the Controller is the right person to do that job.
The story about link function is different, it is coming from different perspective then MVC. And is really essential, once we want to cross the boundaries of a controller/model/view (template).
Let' start with the parameters which are passed into the link function:
function link(scope, element, attrs) {
scope is an Angular scope object.
element is the jqLite-wrapped element that this directive matches.
attrs is an object with the normalized attribute names and their corresponding values.
To put the link into the context, we should mention that all directives are going through this initialization process steps: Compile, Link. An Extract from Brad Green and Shyam Seshadri book Angular JS:
Compile phase (a sister of link, let's mention it here to get a clear picture):
In this phase, Angular walks the DOM to identify all the registered
directives in the template. For each directive, it then transforms the
DOM based on the directive’s rules (template, replace, transclude, and
so on), and calls the compile function if it exists. The result is a
compiled template function,
Link phase:
To make the view dynamic, Angular then runs a link function for each
directive. The link functions typically creates listeners on the DOM
or the model. These listeners keep the view and the model in sync at
all times.
A nice example how to use the link could be found here: Creating Custom Directives. See the example: Creating a Directive that Manipulates the DOM, which inserts a "date-time" into page, refreshed every second.
Just a very short snippet from that rich source above, showing the real manipulation with DOM. There is hooked function to $timeout service, and also it is cleared in its destructor call to avoid memory leaks
.directive('myCurrentTime', function($timeout, dateFilter) {
function link(scope, element, attrs) {
...
// the not MVC job must be done
function updateTime() {
element.text(dateFilter(new Date(), format)); // here we are manipulating the DOM
}
function scheduleUpdate() {
// save the timeoutId for canceling
timeoutId = $timeout(function() {
updateTime(); // update DOM
scheduleUpdate(); // schedule the next update
}, 1000);
}
element.on('$destroy', function() {
$timeout.cancel(timeoutId);
});
...

Categories

Resources