Using the AngularJS UI Directives for bootstrap, is there any way to collapse the tab content using the tag?
I have several tabs/pills with content, which will start collapsed (hidden). When any of the tabs is activated, the tab content should collapse open, and stay open until a close button is clicked, which will close the collapsable section.
In the controller, I set $scope.isCollapsed to true. Each of the tabs have the ng-click which calls openCollapse(), which sets isCollapsed to false. If I add the collapse="isCollapsed" directive right to the tag, then the tabs disappear too, not just the content.
How can I fix this?
It took some figuring out, but it is possible!
The main problem I had was with scoping issues and transclusion. I hadn't run into transclusion yet (I'm fairly new to Angular), so I'm still wrapping my head around it a bit.
I tried a few different ways, and a couple others might have worked, if I understood transclusion a bit better, but in the end, this was the simplest for me, and I got it working.
So basically I had to do 4 main things to get this working.
Open up ui.bootstrap-tpls-0.11.0.js (or whichever version # you're using). Do a search for angular.module("template/tabs/tabset.html". In the <div class=\"tab-content\">\n" tag, add collapse=\"isCollapsed\".
The tag (and everything in it) gets replaced when compiled, and this is the code that it is replaced with, so this way we can directly put the collapse directive where we need to.
Also in ui.bootstrap-tpls-0.11.0.js, do a search for .directive('tabset'. Inside the link: function(scope, element, attrs) {, add: scope.isCollapsed = scope.$parent.isCollapsed'
Here we're linking the transcluded scope's isCollapsed to the isCollapsed that is being set in your initial controller (you could also just put initialize isCollapsed in the controller in the next step, instead of just linking it, but I'd already initialized it in my controller and I'd linked it trying to do another method)
Still in ui.bootstrap-tpls-0.11.0.js, do a search for .controller('TabsetController'. Inside this controller, add:
$scope.$on('openCol', function(event){
$scope.isCollapsed = false;
});
$scope.$on('closeCol', function(event){
$scope.isCollapsed = true;
});
What we're doing here is adding event listeners inside the tabset's transcluded scope. What we're going to do at the end, is create an event broadcast. I also added a .$watch(), just so I could see if it was changing:
$scope.$watch('isCollapsed', function(){
console.log("isCollapsed has changed, it is now: " + $scope.isCollapsed);
});
Lastly, in the view's controller (or whichever controller contains the .openCollapse() and .closeCollapse()), change your functions from editing this scopes isCollapsed, to:
$scope.openCollapse = function(){
$rootScope.$broadcast("openCol");
}
$scope.closeCollapse = function($event){
$rootScope.$broadcast("closeCol");
}
This will broadcast the events that are being listened for in the TabsetController. So now the proper scope of the isCollapsed is being changed, and is reflected in the code. Now watch that lovely tab-content collapsing.
Please let me know if I haven't got my terminology quite right, or if there's something inherently wrong with the way I'm doing it. Or, simply, if there are other ways. Always open to suggestions :)
Related
Edit: DON'T DO THIS. It's a waste of time unless you want to spend hours debugging, turns out this is a lot more complicated than I first thought. For now the solution to this is:
Move the loading of resources out of the resolvers and into the controller.
Remove all enter animations handled by ui-router.
Add your own animation init and enter classes to the main scope with ng-class.
Use $scope.$emit from the page-specific controllers to tell the main controller when the stuff has finished loading.
In short, if you need this (I have seen a few questions on the ui-router issue tracker) don't use resolvers or ng-animate for the enter animations. You also can't do it on the $stateChangeStart event and the leaving animations as this collides with how ui-router works.
Below is my original question.
I have a specific use case where I need the page transitions and resolves to happen in a certain order currently they happen like this:
resolve > animate out > animate in
I need it more like this:
animate out > resolve > animate in
I decided to check out the ui-router source code and find out why it behaves they way it does. Fortunately it's a very simple mod. In the ui-view directive we have this code.
scope.$on('$stateChangeSuccess', function() {
updateView(false); // cleanupLastView(); is at the end of this function
});
scope.$on('$viewContentLoading', function() {
updateView(false); // cleanupLastView(); is at the end of this function
});
I need to updated it to:
scope.$on('$stateChangeStart', function() {
cleanupLastView();
});
scope.$on('$stateChangeSuccess', function() {
updateView(false); // remove cleanupLastView();
});
scope.$on('$viewContentLoading', function() {
updateView(false); // remove cleanupLastView();
});
The problem is that for obvious reasons, I don't want to go and hack the core. Is there any way to "de-register" the ui-router's ui-view directive and tell it to use one of mine instead?
So I actually wrote this question up and then found an answer. Posting it for anyone else on their travels around the internet.
The answer was to copy BOTH ui-view directives and simply add a new directive called (for example) rich97-view. Then you can use it in your view as if you were using ui-view. The great thing about this methods is that the mod only applies where you need it to, the default behavior is unchanged.
I'm trying to implement a Angular UI Bootstrap carousel, but I'm using it for a Quiz. Therefore, I don't need normal Prev() and Next() buttons.
Rather, I need a custom Next() button that makes sure they've selected an answer before continuing on to next "slide" of question/answers.
How do I hook into the carousel directive functions to run my code and then use the carousel.next() function?
Thanks,
Scott
There is no official possibility to achieve this. but this can be hacked, if you want. But i think it is better grab the bootstrap original one, have a look the at angular bootstrap ui sources (carousel) and write your own wrapper.
Here comes the hack:
The first problem we have to solve is, how to access the CarouselController. There is no API that exposes this and the carousel directive creates an isolated scope. To get access to this scope wie need the element that represents the carousel after the directive has been instantiated by angular. To achieve this we may use a directive like this one, that must be put at the same element as our ng-controller:
app.directive('carouselControllerProvider', function($timeout){
return {
link:function(scope, elem, attr){
$timeout(function(){
var carousel = elem.find('div')[1];
var carouselCtrl = angular.element(carousel).isolateScope();
var origNext = carouselCtrl.next;
carouselCtrl.next = function(){
if(elem.scope().interceptNext()){
origNext();
}
};
});
}
};
});
We must wrap our code in a $timeout call to wait until angular has created the isolated scope (this is our first hack - if we don't want this, we had to place our directive under the carousel. but this is not possible, because the content will be replaced). The next step is to find the element for the carousel after the replacement. By using the function isolateScope we have access to the isolated Scope - e.g. to the CarouselController.
The next hack is, we must replace the original next function of the CarouselController with our implementation. But to call the original function later we have to keep this function for later use. Now we can replace the next function. In this case we call the function interceptNext of our own controller. We may access this function through the scope of the element that represents our controller. If the interceptNext returns true we call the original next function of the carousel. For sure you can expose the complete original next function to our controller - but for demonstration purposes this is sufficient. And we define our interceptNext function like this:
$scope.intercept = false;
$scope.interceptNext = function(){
console.log('intercept next');
return !$scope.intercept;
}
We can now control the next function of the carousel by a checkbox, that is bound to $scope.intercept. A PLUNKR demonstrates this.
I knew this is not exactly what you want, but how you can do this is demonstrated.
That hack is neat michael, I started working on something similar for my needs. But then realized I might as well finally dip my toe into contributing to the open source community.
I just submitted a pull request to update the library so the index of the current slide is exposed to the Carousel scope.
https://github.com/angular-ui/bootstrap/pull/2089
This change allows you to have per-slide behavior in the carousel template.
This change allowed me to override the base carousel template so that for instance on the first slide the "prev" button would not show or the "next" button would not show for the final slide.
You can add more complex logic for your own personal needs, but exposing the current index in this manner to the $scope is part of making this part of the framework more flexible.
EDIT
I made more changes for my personal use, but don't want quite yet to contribute this change which is closer to what you are needing.
I modified the carousel directive, adding the "finish" property to scope.
.directive('carousel', [function () {
return {
restrict: 'EA',
transclude: true,
replace: true,
controller: 'CarouselController',
require: 'carousel',
templateUrl: 'template/carousel/carousel.html',
scope: {
interval: '=',
noTransition: '=',
noPause: '=',
finish: '='
}
};
}])
Then, when I declare the carousel, I can pass in a method to that directive attribute which is a method in the scope of the controller containing the carousel.
<carousel interval="-1" finish="onFinish">
...
</carousel>
This allows me to modify my template to have a button that looks like this:
<button ng-hide="slides().length-1 != currentIndex" ng-click="finish()" class="btn next-btn">finish<span class="glyphicon glyphicon-stats"></span></button>
So it only shows conditionally on the correct slide and with ng-click it is calling the carousel's $scope.finish() which is a pointer to a method in the controller I created for this application.
Make sense?
edit: This only works if you don't use sort functionality with ng-repeat. There is a bug which breaks the indexing of the slides for this kind of functionality.
I am having a sortable list that gets populated by data in my Angular Controller. This list also contains an extra element containing some controls that can also be dragged
What I want do do, is make sure that the extra element stays in place after the $digest cycle is run, and I made a directive just for that case.
App.directive('ngIgnore', ['$timeout',function($timeout){
return {
link: function(scope, element){
scope.$watch(function(){
// Keep track of the original position of the element...
var el = element[0];
var siblings = Array.prototype.slice.call(el.parentNode.children);
var parent = el.parentNode;
var index = siblings.indexOf(el);
$timeout(function(){
// After the digest is complete, place it to it's previous position if it exists
// Otherwise angular places it to it's original position
var item;
if(index in parent.children)
item = parent.children[index];
if(!!item){
parent.insertBefore(el, item);
}
});
});
}
}
}]);
It worked, but not as I wanted it to... As you can see in this example shuffling the list does not move the ignored element, but the problem is that the $watch function gets executed infinitely since the $timeout triggers another $digest cycle... I tried changing the $timeout with setTimeout but no luck...
Is there any Angulary way to run code right after $digest is executed? Or is my entire logic wrong here?
Another (highly recommended) solution is to stop thinking in DOM manipulations.
Common problem with developers who start writing AngularJS code is tend to do DOM manipulations as the result of some event. Its common in jQuery:
$('#someid').click(function() { this.toggleClass('clicked') })
In AngularJS you should design your View to visualize your Model state (that should be in $scope). So
<div ng-click="clicked = !clicked" ng-class="{clicked: clicked}">I was clicked</div>
Same logic should be applied when designing components. In a HTML code you should put all visual logic - hide some elements using ng-show/ng-if, ng-switch. Add/remove classes using ng-class etc. So you define all possible model states.
Then by just changing model state you will get your view automatically updated reflecting current model state.
Same goes for repeated elements: you can repeat on some collection and then, depending on what element is present, you define how it would look. Keep in mind, that in ng-repeat each element will have own child (so mostly independed) Scope, so you can define some per-element manipulations.
See also this great answer: "Thinking in AngularJS" if I have a jQuery background?
You can try to use Scope.evalAsync Scope.evalAsync:
it will execute after the function that scheduled the evaluation
(preferably before DOM rendering).
Update: Fiddle w/ full solution: http://jsfiddle.net/langdonx/VXBHG/
In efforts to compare and contrast KnockoutJS and AngularJS, I ran through the KnockoutJS interactive tutorial, and after each section, I'd rewrite it in AngularJS using what little I already knew + the AngularJS reference.
When I got to step 3 of the Creating custom bindings tutorial, I figured it would be a good time to get spun up on Angular Directives and write a custom tag. Then I failed miserably.
I'm up against two issues that I haven't been able to figure out. I created a new Fiddle to try and wrap my head around what was going on...
1 (fiddle): I figured out my scoping issue, but, is it possible to just passthrough ng-click? The only way I could get it to work is to rename it to jqb-click which is a little annoying.
2 (fiddle): As soon as I applied .button() to my element, things went weird. My guess is because both Angular and jQuery UI are manipulating the HTML. I wouldn't expect this, but Angular seems to be providing its own span for my button (see line 21 of the JavaScript), and of course so is jQuery UI, which I would expect. I hacked up the HTML to get it looking right, but even before that, none of the functionality works. I still have the scope issue, and there's no template binding. What am I missing?
I understand that there's an AngularUI project I should be taking a look at and I can probably pull off what I'm trying to do with just CSS, but at this point it's more about learning how to use Directives rather than thinking this is a good idea.
You can create an isolated scope in a directive by setting the scope parameter, or let it use the parent scope by not setting it.
Since you want the ng-click from parent scope it is likely easiest for this instance to use the parent scope within directive:
One trick is to use $timeout within a directive before maniplulatig the DOM within a templated directive to give the DOM time to repaint before the manipulation, otherwise it seems that the elements don't exist in time.
I used an attribute to pass the text in, rather than worrying about transclusion compiling. In this manner the expression will already have been compiled when the template is added and the link callback provides easy access to the attributes.
<jqbutton ng-click="test(3)" text="{{title}} 3"></jqbutton>
angular.module('Components', [])
.directive('jqbutton', function ($timeout) {
return {
restrict: 'E', // says that this directive is only for html elements
replace: true,
template: '<button></button>',
link: function (scope, element, attrs) {
// turn the button into a jQuery button
$timeout(function () {
/* set text from attribute of custom tag*/
element.text(attrs.text).button();
}, 10);/* very slight delay, even using "0" works*/
}
};
});
Demo: http://jsfiddle.net/gWjXc/8/
Directives are very powerful, but also have a bit of a learning curve. Also in comparison of angular to knockout, angular is more of a meta framework that in the long run has far more flexibilty than knockout
Very helpful reading for understanding scope in directives:
https://github.com/angular/angular.js/wiki/The-Nuances-of-Scope-Prototypal-Inheritance
I've been researching this for a few hours now.
Let's say I have a jQuery selector of $('#bottom .downloads .links a').click.....
How can I do the same type of thing in an Angular directive?
This is what I have so far and it works, but for all tags on the page.
angular.module('directives', []).directive('a', function(mongoDB){ //Don't need ['customServices'], it can just be [] to use mongoDB
return {
restrict : 'E',
link : function(scope, element, attrs){
element.on('click', function(){
//But this gets called for all links on the page
//I just want it to be links within the #bottom .downloads .links div
//I wanted to use a directive instead of ng-click="someMethod()"
});
}
});
Is there a way to target this directive to only a certain div? I guess I could change the restrict to 'C' or 'A' and add an attribute to the links, but I was wondering if I could still layout the front end like I currently am used to with my jQuery selectors.
There is a pretty significant philosophical difference between AngularJS and jQuery. In jQuery, everything is in the DOM - including your data - and you do everything through DOM transformations. AngularJS, on the other hand, has separation of concerns built in: models, views, controllers, services, etc., are all separate. We use controllers to glue code together, but each component knows nothing about the other components.
So whereas in jQuery, one might use a selector to find all links matching a certain pattern and then add a certain functionality to it (say a click handler), in AngularJS, the HTML is the "offical record". Instead of abstracting away the attachment of a click handler into a JavaScript function, it is put right into the markup:
<a ng-click="doWhatever()">Click me!</a>
In this case, doWhatever is a method on the scope for that part of the page, probably set in your controller:
$scope.doWhatever = function () {
console.log("Hello!");
}
So the way you are approaching the problem is not going to work in AngularJS. Instead, you need to look at directives not like jQuery selectors with a function, but as an extension of HTML. You ask yourself, "what does HTML not do out of the box that I need it to?" Your answer is your directive.
But AngularJS already has a built-in directive for click handlers (the ngClick used above).
Angular already has an a directive, so you probably shouldn't create your own.
In an Angular world, to "target only a certain div" (well, <a> within the div) we declaratively target that <a> with a directive, rather than use CSS-like selectors. So yes, restrict to 'A' and add an attribute to the <a> would be best:
<a ... target-this-one>...</a>
I personally think this reads better. Looking at the HTML it is clear which <a>s have special/additional functionality.
As #Josh pointed out, you would only need to do this if ng-click isn't sufficient for your needs.