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
});
Related
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.
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.
I have been developing an advanced web-based GUI in AngularJS. Recently, I decided to use the call document.getElementsByClassName() (I hate using element collecting methods, but here I had to use one) and my boss flipped his lid for accessing the document element. He says that I "need to use only Angular calls for everything", even for element collection! Is there an "Angular way" to collect elements by class name? If so, which way is better to use within the Angular framework? Please provide reasons why. Thanks!
UPDATE: Why I need to use an element collector...
So, I really wish I didn't have to do this, but I do...
I am using a third-party directive that I found online called the Bootstrap DateTimePicker. Its pretty cool and very nice looking, yet it might have a bug...
First, I make a directive bound to an attribute, stating that the element I pass in is meant to be a "DateTimePicker". I then pass that element to the DateTimePicker function.
When invoked, this function creates a new div with absolute positioning and appends it to the body of the page.
Now, I open a dialog in my GUI that has a table in it. On each row of the table, I have two DateTimePickers: one for end-date and one for start-date.
My problem is that, once I leave my screen and the elements which the DateTimePickers were bound to are destroyed, the DateTimePickers still remain! If I open the dialog box again, it creates a ton more of these divs as well!
Until I could determine a true solution to this issue, I decided to use the element collector as a temporary quick-fix. I grab all of the elements with the datetimepicker class and perform a:
elem[i].parentNode.removeChild(elem[i]);
Not having your exact use case but knowing that you are attempting to aggregate elements by class name in your controller makes me agree with you boss. Think of the controller as an object which exposes data and and services to your declarative html page. The data is bound into the markup for presentation and possible modification. THe services are usually wrapped in functions on your controller which are then tied to event handling directives like ng-click or ng-change. These services should operate exclusively on your data and never touch the DOM. If you need to modify a DOM element in your declarative markup then that should be done through directives like ng-class etc.
In any case, It would be useful to know what you are trying to accomplish so as to give you a better idea of the "angular way" to approach the problem.
Well, I have my answer. This does not solve the question "Grab all elements with a certain class name without touching the document element" yet it does solve my problem and eliminates my need to use document.getElementsByClassName.
First of all, it turns out that every element using the DateTimePicker directive have an element.datetimepicker("remove") function.
I use a directive for each DateTimePicker:
components.directive('DateTimePicker', function() {
// Requires bootstrap-datetimestamp.js
return {
restrict: 'E',
replace: true,
scope: {
dateTimeField: '='
},
template:
'<div>' +
'<input type="text" readonly data-date-format="yyyy-mm-ddThh:ii:ssZ" data-date-time required/>'+
'</div>',
link: function(scope, element, attrs, ngModel)
{
var input = element.find('input');
input.
datetimepicker(
{
//stuff
})
.on('changeDate', function(ev)
{
//more stuff
});
...
Directive drastically shortened for the sake of your eyeballs...
I then need to remove the DateTimePicker and the input it is bound to from the DOM on destruction of the dialog box that the input is a child of. To do so, I added this to my directive:
scope.$on("$destroy",function handleDestroyEvent()
{
input.datetimepicker("remove");
input = null;
});
And it works! The DateTimePicker gets removed, the DateTimePicker's handles to the input are cleaned up, and I've marked my input for the GC! WooHoo! Thanks everybody!
If you include jQuery in your project before AngularJS, Angular will use jQuery instead of jqLite for the angular.element function. This means you should be able to use jQuery's selectors for finding / referencing DOM elements.
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.
I'm trying to find out how I can stop a DOM element from binding data from the scope in angular.
I know that you could do this with if statements and all, but is there a genuine & permanent way to stop binding a element in angular but keep the content that was added?
So say i have this
<div ng-bind="content" class="ng-binding">Welcome</div>
And i change the model so that the div changes to this.
<div ng-bind="content" class="ng-binding">Welcome World</div>
Then I click the button that will unbind it, so if I change the model to 'Welcome Universe', I wan't the <div> to be the same as before. This
<div ng-bind="content" class="ng-binding">Welcome World</div>
I know there are many other ways to do this, but i don't know any way to genuinely unbind the element, without cloning it and replacing the old one looping through the attributes and text..ect
Demo thing: http://jsfiddle.net/a9tZY/
So, by doing this, it shouldn't affect the model or other elements that are binding to that model.
Long story short, Tell Angular to leave the element alone forever.
UPDATE
The way to do this is to create a new scope on the element with a directive like so.
yourModule.directive('unbindable', function(){
return { scope: true };
});
And apply it to your element like so
<div unbindable id="yourId"></div>
Then to unbind this element from any updates you do this.
angular.element( document.getElementById('yourId') ).scope().$destroy();
Done, here's a demo.
Demo: http://jsfiddle.net/KQD6H/
So this creates a new scope on the element and only works because all scopes inherit all data from their parent scopes. so the scope is basically the same as the parent scope, but allows you to destroy the scope without affecting the parent scope. Because this element was given it's own scope, when you destroy it it doesn't get the parent scope back like all of the other elements, if that makes sense 0.o
Everything below this line was my original answer,I'll leave it here incase someone prefers this way
I have managed to achieve this genuinely with a unbindable directive.
When you have the unbinable directive set up on the element all that is required to unbind the element is this.
yourElement.attr('unbind', 'true'); // Ref 1
$scope.$broadcast('unbind'); // Ref 2
Here is the directive.
app.directive('unbindable', function(){
return {
scope: true, // This is what lets us do the magic.
controller: function( $scope, $element){
$scope.$on('unbind', function(){ // Ref 3
if($element.attr('unbind') === 'true'){ // Ref 4
window.setTimeout(function(){ $scope.$destroy() }, 0);//Ref 5
}
});
}
}
});
and you set your element up like this.
<h1 unbindable></h1>
So whenever you add the unbind="true" attribute to the h1 and broadcast unbind the element will be unbind-ed
REF-1: Add the unbind true attribute to the element so that the directive knows what element you are unbinding.
REF-2: Broadcast the unbind event across the scopes so that the directive knows that you want to unbind a element - Make sure you add the attribute first. --- Depending on your app layout, you might need to use $rootScope.$broadcast
REF-3: When the unbind event is broadcasted
REF-4: If the element associated with the directive has a true unbind attribute
REF-5: Then destroy the scope made by the directive. We have to use setTimeout because I think angular tries to do something after the $on event and we get a error, so using setTimeout will prevent that error. Although it fires instantly.
This works on multiple elements, here is a nice demo.
Demo: http://jsfiddle.net/wzAXu/2/
This one got me curious, so I did some poking around. At first I tried the "unbind()" method suggested in the other answer, but that only worked with removing event handlers from the element when what you're actually trying to do is remove the angular scope from the element. There may be some neater hidden function in Angular to do this, but this works just fine too:
angular.element(document.getElementById('txtElem')).scope().$destroy();
This retains the model (and updates anything else still bound to it), but removes the binding from the element. Also, in your example above, there is no binding to remove because you aren't binding to any element, just displaying the model expression inline. My example shows this in action: http://jsfiddle.net/3jQMx/1/
You can call the unbind method that stops listening to the element where the ng-model attribute is present. See fiddle: http://jsfiddle.net/jexgF/
angular.element(document.getElementById('txtElem')).unbind()
unbind removes all event listeners, so whenever any changes are made, it wont listen for those and hence not go through the angular loop. I have also assumed that you are not using jQuery, but if you are, you can use a better selector than document.getElementById