attribute directive not working for ajax loaded element? - javascript

I'm dealing with legacy code. My page is composed of three partial views, one for header, one for footer, one for the content. I have this element with my-directive in my footer:
<a my-directive>
<img>
</a>
My footer is rendered at the same time with the others on page load. However, in my header, I have #products_menu whose content is loaded via ajax:
// calls a route to do some processing before returning the view
// to be rendered inside #products_menu
$('#products_menu').load(...);
#products_menu contains the same element with the same directive:
<a my-directive>
<img>
</a>
This is my directive:
angular
.module('module1')
.directive('myDirective', ['$rootScope', 'ModalService', '$compile',
function($rootScope, ModalService, $compile) {
return {
restrict: 'A',
link: function(scope, element, attrs) {
console.log(element);
element.on('click', function(e) {...}
} ...
The problem is when I click on the element in the footer, it fires. But when I click on the element in the header, it doesn't. I'm assuming it's because the element was loaded dynamically, and the attribute was not bound during the time angular was compiling.
AM I right in my assumption and if so, is there a workaround?

So just dumping the html via using jquery or DOM manipulation would not intimate angular to do its work. You have to tell angular about this or better compile the DOM using $compile service. Angular things will automatically triggers in for that template
$('#products_menu').load(..., function() {
$compile($('#products_menu'))($scope);
})
I'd recommend to mix jQuery with angular, best way to handle this in angular way is using ng-include directive
<div id="products_menu" ng-include="'template.html'">
</div>

Related

Angular attribute directive - add an ng-repeat below current directive

If I had an attribute directive, for example something like this:
<select multiple ... ng-model="ctrl.model" custom-directive="ctrl.customModel" />
where let's say that ngModel and customModel are arrays. Is there a way I can, within the directive's code, add a piece of html below the directives element which could have access to the scope of the directive and be able to reference the customModel so that in the end it looks something like this:
<select multiple ... ng-model="ctrl.model" custom-directive="ctrl.customModel" />
<div><!-- this code gets added by the custom-directive directive and uses it's scope -->
<span ng-repeat="item in customDirectiveCtrl.customModel" ng-bind="item.property"></span>
</div>
I know I can add html manually using jqLite, however this html doesn't have access to directive scope. The reason I don't want to convert the custom-directive directive from attribute directive to element directive is because it makes it way more difficult to add attributes such as id, name, required, disabled,... to underlying template elements (in the case of this example, a select element)
EDIT: as requested here's an example of how to add an element after the directives element:
{
restrict: 'A',
require: 'ngModel',
scope: { customModel: '=customDirective' },
link: function(scope, element, attrs, ngModel) {
//element.after('<div></div>'); //this adds a div after the directives element
element.after('<div><span ng-repeat="item in customModel" ng-bind="item.property"></span></div>'); //this will add the html in the string, but will not interpret the angular directives within since (i assume) that it is not bound to any scope.
}
}
Any angular component/directive added like this will not work properly or at all.
If you are injecting new HTML into the page in your directive, and you need that HTML to use angular directives (ng-repeat, ng-bind, etc) then you will need to use the $compile service to make angular aware of your new DOM elements. In your case, you would inject the $compile service into your directive and then use it like this:
link: function(scope, element, attrs, ngModel) {
//create the new html
var newElement = angular.element('<div><span ng-repeat="item in customModel" ng-bind="item.property"></span></div>');
//compile it with the scope so angular will execute the directives used
$compile(newElement)(scope); //<-this is the scope in your link function so the "customModel" will be accessible.
//insert the HTML wherever you want it
element.after(newElement);
}

querySelectorAll doesn't work with ngInclude, uiView

I want to fetch all <img> elements inside an element. For this I created a directive 'find-images':
app.directive('findImages', function() {
return {
restrict: 'A',
scope: {},
link: function(scope, element, attrs) {
var img_elements = element[0].querySelectorAll('img');
console.log(img_elements) //doesn't have img elements nested inside ngInclude, uiView
}
}
})
I used this directive in the body tag like this:
<body find-images>
However, I am using ui-router and in many places ng-include(to load nested partials). And the problem I am facing is that querySelectorAll isn't returning img elements which are nested inside ng-include and ui-view. How can I get elements which are nested in them? Also, all nested elements(ui-view, ng-include) are within the body tag.
Implement the logic in the statechangesuccess event fired in ui-router which you could consider the same as page load.
angular.module('app', [])
.run(['$document', function ($document) {
$rootScope.$on("$stateChangeSuccess", function (event, toState, toParams, fromState, fromParams) {
var body = $document.getElementsByTagName("body")[0];
var img_elements = element[body].querySelectorAll('img');
}
});
UPDATE
Another option is to use the onload event raised by ng-include when it is finished rendering.
Also don't forget nginclude is itself a directive, you can use ngswitch in many instances rather than nginclude and render custom directives instead.
That way you have less logic in your controller, and more obvious in the view what is being rendered when.
The problem is, ng-include will include the element after the directive's link function. It will depend on the structure of your app and what you're trying to achieve, but essentially (via timeout, or promises, or callbacks), you need to have a mechanism to determine when your DOM is in a stable state (ng-includes may have more ng-includes etc and it can get complex), and then run your directive logic after that.

Load event of an element with a directive

Suppose you have made a new directive in angularJS with attribute restriction, say my-directive.
Suppose to have the following HTML code:
<img src="..." my-directive />
Now, the load event of the <img> is triggered only when all the code of my-directive is executed and the DOM is completly loaded, or when all the information of the natively <img> tag are loaded (for example the src data, the style, ...)?
I want to know this information because I want understand what is the effect to intercept the load event inside the code of a directive.
As my comment mentions, you'll likely want to use a pre-compile link funciton, but you will also be aided by using ng-src instead of the native src attribute, which allows angular to insert itself into the process. Then you can do your interception prior to compilation like so:
module.directive('interceptImg',function(){
//other functions on your directive like template and controller
compile:{
return: {
pre: function preLink(scope, iElement, iAttrs, controller){
//you can listen/bind your events here by accessing iElement
}
}
}
})
Checkout the docs for $compile

Where is the best place to have a method to update the dom called from a $scope function? [closed]

Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 9 years ago.
Improve this question
I have a form with sections that are scrolled and lined up automatically when the user interacts with it. I would like to have all the logic defined in a directive but at the moment can't figure out how to get some of the DOM manipulation logic out of my controller. Most of the functionally can be attached to on scrolls, clicks or focus events but how do I get a function attached to my scope to trigger some DOM manipulation without having the DOM logic in my controller?
What I currently have is
$scope.scrollToNextSection = function(section){
//DOM manipulation logic to scroll to next section.
}
Would it be valid for me to have
directiveDOMObject.scrollToNextSection = function(section){
//DOM manipulation logic to scroll to next section.
}
and call it from my controller with
$scope.scrollToNextSection = function(section){
directiveDOMObject.scrollToNextSection(section);
}
Is attaching a function to a DOM object like this ok so all my DOM manipulation can be contained in the directive? Is there a standard pattern for triggering DOM manipulation logic defined in a directive from a controller?
HTML handles scrolling within the page using name anchors. <a name="sectionX"> and <a href="#sectionX"> These are getting heavily (mis)used in an SPA if you use a router.
The scope/controller does not know about the dom and cannot/shouldnot change it. The FAQ says:
DOM Manipulation
Stop trying to use jQuery to modify the DOM in controllers. Really.
That includes adding elements, removing elements, retrieving their
contents, showing and hiding them. Use built-in directives, or write
your own where necessary, to do your DOM manipulation. See below about
duplicating functionality.
Someone has written an ngScrollTo directive which keeps the logic in the view + directive. I haven't tried it out but it looks like the way to go.
See also See Anchor links in Angularjs? for alternative solutions.
Is attaching a function to a DOM object like this ok so all my DOM manipulation can be contained in the directive
The short answer here is no, not really. If the controller has business logic, then it shouldn't be concerned with what's going on in the DOM.
Is there a standard pattern for triggering DOM manipulation logic defined in a directive from a controller?
Not sure if they're standard, but they are a few ways. Their common theme is that the controller, that handles business logic either directly or via services, doesn't actually call the directive, or really know what's going on in the DOM / view. It just provides "hooks" in one form or another, so the directive can react appropriately.
The ways I know of are:
React to changes of variable on the scope. So you can have a variable, like state
<div scroll-listen-to="state"> .... </div>
And a directive, scrollListenTo, with a scope + link function as follows:
scope: {
scrollListenTo: '='
},
link: function postLink(scope, iElement, iAttrs) {
scope.$watch('scrollListenTo', function(newValue, oldValue) {
// Do something, maybe with scrolling?
});
}
React to events $broadcast from the controller. This sends the event to child scopes (and so scopes in directives within the sending scope). The name of this event can also be configurable. So, for example
<div ng-controller="MyController">
<input scroller-event="MyController::stateChanged" />
</div>
Then in MyController, at the appropriate point:
$scope.$broadcast('MyController::stateChanged', 'someData');
And in the directive:
scope: {
'eventName': '#scrollerEvent'
},
link: function postLink(scope, iElement, iAttrs) {
scope.$on(scope.eventName, function(e, data) {
// Do something the data
});
}
React to events $emited from the controller. This is very similar to $broadcast, but the event is emitted upwards through the hierarchy. You can "wrap" several controllers and then they can send events to a directive that wraps them.
<div scroller-event="MyController::stateChanged">
<div ng-controller="MyController">
</div>
<div ng-controller="MyController">
</div>
</div>
Then in MyController
$scope.$emit('MyController::stateChanged', 'someData');
In this case, you probably shouldn't use the scope parameter in the directive, as this would create an isolated scope, which in this case probably isn't desired. The directive could have something like
link: function postLink(scope, iElement, iAttrs) {
var eventName = iAttrs.scrollerEvent;
scope.$on(eventName, function(e, data) {
// Do something with the data, like scrolling.
});
}
You say you're using a form. You could create a set of custom directives that interact, much like ngModel and ngForm interact. So, for example, you could have:
<div scroller-container>
<input scroll-on-focus />
<input scroll-on-focus />
</div>
Then in the scrollOnFocus directive
require: '^scrollerContainer',
link: function(scope, iElement, iAttrs, scrollerContainerController) {
iElement.on('focus', function() {
scrollerContainerController.scrollTo(iElement);
});
}
And in the scollerContainer directive, you must define scrollTo on its controller
controller: function() {
this.scrollTo = function(element) {
// Some code that scrolls the container so the element is visible
};
}
I realise the above ways are not especially specific to your issue of scrolling: they are more generic, and to be honest, I'm not yet sure which to recommend in any given case.

Transclude in AngularJS without adding new element

Is there any way to transclude some content into a directive without adding extra elements.
For example
directive:
{
scope: {
someParam: "="
},
link: function(scope, element, attrs){
//do something
},
transclude: true,
template:'<div ng-transclude></div>'
}
source html:
<div my-directive some-param="somethingFromController">
my transcluded content: {{somethingElseFromController}}
</div>
With this example an extra div gets added to the markup. Normally this would be fine but I'm trying to use this directive inside a table so adding a div tag screws things up.
I also tried not specifying transclude or template which gets rid of the extra div tag but now {{somethingElseFromController}} cannot be found as the "transcluded" content is in an isolated scope. I know I could just get the parameters for my directive from the attrs object in the linking function instead of creating an isolated scope but I'd rather avoid needing to evaluate strings with scope.$apply().
Anyone know how to accomplish this?
Thanks!
What #Vakey answered is what I was searching for.
But as today, the Angular documentation says:
The transclude function that is passed to the compile function is deprecated, as it e.g. does not know about the right outer scope. Please use the transclude function that is passed to the link function instead.
So I used instead the controller (for the moment) and its $transclude function, as part of the example shown on the $compile documentation:
controller: function($scope, $element, $transclude) {
var transcludedContent, transclusionScope;
$transclude(function(clone, scope) {
$element.append(clone);
transcludedContent = clone;
transclusionScope = scope;
});
},
This actually is possible with Angular. Directives such as ng-repeat do this. Here is how you do it:
{
restrict: 'A',
transclude: true,
compile: function (tElement, attrs, transclude) {
return function ($scope) {
transclude($scope, function (clone) {
tElement.append(clone);
});
};
}
};
So what's going here? During linking, we are just appending the clone, which is the element we are trying to transclude, into the directive's element. Angular will apply $scope onto the clone element so you can do all the angular goodness inside that element.
To elaborate on #rob's post...
Transclusion requires that Angular creates an element that is a clone of the content of whatever tag the directive is/lives on... If the content is text, it will wrap it in a span.
This is so it has a DOM element to apply the scope to when $compile is called.
So, basically transclude adds an element for the same reason you can't $compile('plain text here {{wee}}').
Now, you can do something sort of like what you're trying to do with $interpolate, which allows you to apply a scope to bindings in a string like "blah {{foo}}".... but since I'm really not sure what you're trying to do, I can't really give you a specific example.

Categories

Resources