querySelectorAll doesn't work with ngInclude, uiView - javascript

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.

Related

attribute directive not working for ajax loaded element?

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>

AngularJS ng-repeat to generate TABS

I have a situation in which a dynamic number of TABS can be created (i.e. the use clicks on an entry within a commands menu and a new pill-tab pair is added to those already existing - NOTE: I'm using Bootstrap for the graphical elements).
The list of tabs is managed within a $rootScope array and, whenever a new tab needs to be open, an entry is added to the array (simple push action). Of course, there is an ng-repeat that handles the actual creation of pill-tab pairs.
This works perfectly (tabs and pills are added , except that I cannot set the new tab to be the ACTIVE one since its creation (i.e. addition to the list) takes place within a controller and the element is still unknown to the DOM.
I thought of setting some kind of timer, but this path is tremendously ugly.
One thing that you can use in angular is $timeout service:
https://docs.angularjs.org/api/ng/service/$timeout
For example, if you need to do some steps after the directive is rendered and controller is initialized, then just add to the end of the controller code:
$timeout(function(){ $scope.steps_to_do(); } );
This will trigger the steps after the digest cycle of the component is done.
Also you do have a good control over it in unittests.
So, one possible approach would be:
before adding a new tab, create a promise in the some parent scope or service
after the child controller is created, add a promise resolution on $timeout in that child controller
Don't use timeout or anything like this because you never know how long it will take to load it.
What I suggest is use this directive on your ng-repeat:
.directive('onFinishRender', function ($rootScope) {
return {
restrict: 'A',
link: function (scope, element, attr) {
if (scope.$last) {
$rootScope.$broadcast("ngRepeatFinished");
}
}
};
});
HTML:
<ul>
<li class="pages" data-ng-repeat="page in sidebarPages" on-finish-render></li>
</ul>
What this will do is listen for last item being rendered in ng-repeat and then fire $broadcast event to the controller. What you need to do then is listen for that event in your controller like this:
$rootScope.$on('ngRepeatFinished', function(event, args) {
// do what you want to do
});
You can also try $viewContentLoaded
$scope.$on('$viewContentLoaded', function(event, args) {
// do what you want to do
});

Adding ngInit to all the elements with specified css class

I have a single page application (angularjs + angular-ui-router) and there are many elements with 'someCssClass'.
I need to add a global handler (at window object), that handles all these elements init event. Is there a way to do this, except manually adding ngInit (or some like this) for each element in each view?
I end up with directive and linkoption.
app.directive('someCssClass', function () {
return {
restrict: 'C',
link: function initHandler(scope, element, attrs){}
};
});
Example on JSFiddle.
Angular directive has restrict parameter, which could be class. As you already have classes on elements — you could use them.
And $emit/$on for events, with what data you need.

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