Add/Remove CSS Class on Route Change in Angularjs - angularjs

I am not sure how this can be achieved in Angular. I want to add and remove CSS class on route change. I am trying to Show and Hide vertical menu. Currently I am using ui-route. Any Suggestion or link to example would be appreciated or any other suggestion on different approach to my problem is also welcome

Easiest and most efficient way:
angular.module(...).run(function($rootScope, $state) {
$rootScope.$state = $state;
});
<div ng-if="$state.contains('someState')">...</div>
This will remove the DOM which will improve performance if the menu has lots of bindings.
However, I constantly tell people to consider leveraging named views for navigation:
<body>
<nav ui-view="nav"></nav>
<div class="container" ui-view></div>
</body>
$stateProvider.state('home', {
views: {
'nav#': {
templateUrl: 'nav.html'
}
'': {
// normal templateUrl and controller goes here.
}
}
});
The cool part about this is that children states can override and control what nav file to use, and can even setup resolves and controllers that share data between the nav and the content. No directives/services needed!
Finally, you can do these too:
<nav ng-show="$state.contains('somestate')"></nav>
<nav ng-class="{someClass:$state.contains('somestate')}"></nav>
Alternatively checkout ui-sref-active
All of my suggestions primarily assume you're using UI-Router since it's the best!

Try this:
app.run(function ($rootScope) {
$rootScope.$on("$stateChangeStart", function(event, toState, fromState){
if (toState.url === "/path") {
$('div').addClass('className');
} else {
$('div').removeClass('className');
}
});
});

You can register the route changed and add this css to your DOM:
$rootScope.$on('$routeChangeSuccess', function (event, current, previous) {
// Add your logic, for instance:
$('body').addClass('hide-menu');
});
Obviously there are events raised before the route has been changed: "$locationChangeStart", here.
/Edit/ - Better approach
Also I would rather using the ng-class attribute and simple bind a certain value from your main controller to it.
app.controller('MainController', function ($scope) {
$scope.toggleMenu = function(isToShow) {
$scope.isVisibleMenu = isToShow == true;
};
});
then in your html:
<!-- Menu toggle button-->
<button ng-click="toggleMenu()"></button>
<div class="toggleable-menu" ng-class="{'visible-menu': isVisibleMenu}">
<!-- The menu content-->
</div>
and the simplest CSS possbile (you can obviously add animations or any other thing to toggle this menu.)
.toggelable-menu {
display: none;
}
.toggelable-menu.visible-menu {
display: block;
}

Related

Opening URL with an anchor (#) in a new window/tab (angularjs with angular-ui router) [duplicate]

Do any of you know how to nicely handle anchor hash linking in AngularJS?
I have the following markup for a simple FAQ-page
Question 1
Question 2
Question 3
<h3 id="faq-1">Question 1</h3>
<h3 id="faq-2">Question 2</h3>
<h3 id="fa1-3">Question 3</h3>
When clicking on any of the above links AngularJS intercepts and routes me to a completely different page (in my case, a 404-page as there are no routes matching the links.)
My first thought was to create a route matching "/faq/:chapter" and in the corresponding controller check $routeParams.chapter after a matching element and then use jQuery to scroll down to it.
But then AngularJS shits on me again and just scrolls to the top of the page anyway.
So, anyone here done anything similar in the past and knows a good solution to it?
Edit: Switching to html5Mode should solve my problems but we kinda have to support IE8+ anyway so I fear it's not an accepted solution :/
You're looking for $anchorScroll().
Here's the (crappy) documentation.
And here's the source.
Basically you just inject it and call it in your controller, and it will scroll you to any element with the id found in $location.hash()
app.controller('TestCtrl', function($scope, $location, $anchorScroll) {
$scope.scrollTo = function(id) {
$location.hash(id);
$anchorScroll();
}
});
<a ng-click="scrollTo('foo')">Foo</a>
<div id="foo">Here you are</div>
Here is a plunker to demonstrate
EDIT: to use this with routing
Set up your angular routing as usual, then just add the following code.
app.run(function($rootScope, $location, $anchorScroll, $routeParams) {
//when the route is changed scroll to the proper element.
$rootScope.$on('$routeChangeSuccess', function(newRoute, oldRoute) {
$location.hash($routeParams.scrollTo);
$anchorScroll();
});
});
and your link would look like this:
Test/Foo
Here is a Plunker demonstrating scrolling with routing and $anchorScroll
And even simpler:
app.run(function($rootScope, $location, $anchorScroll) {
//when the route is changed scroll to the proper element.
$rootScope.$on('$routeChangeSuccess', function(newRoute, oldRoute) {
if($location.hash()) $anchorScroll();
});
});
and your link would look like this:
Test/Foo
In my case, I noticed that the routing logic was kicking in if I modified the $location.hash(). The following trick worked..
$scope.scrollTo = function(id) {
var old = $location.hash();
$location.hash(id);
$anchorScroll();
//reset to old to keep any additional routing logic from kicking in
$location.hash(old);
};
There is no need to change any routing or anything else just need to use target="_self" when creating the links
Example:
Question 1
Question 2
Question 3
And use the id attribute in your html elements like this:
<h3 id="faq-1">Question 1</h3>
<h3 id="faq-2">Question 2</h3>
<h3 id="faq-3">Question 3</h3>
There is no need to use ## as pointed/mentioned in comments ;-)
Question 1
Question 2
Question 3
<h3 id="faq-1">Question 1</h3>
<h3 id="faq-2">Question 2</h3>
<h3 id="faq-3">Question 3</h3>
If you always know the route, you can simply append the anchor like this:
href="#/route#anchorID
where route is the current angular route and anchorID matches an <a id="anchorID"> somewhere on the page
$anchorScroll works for this, but there's a much better way to use it in more recent versions of Angular.
Now, $anchorScroll accepts the hash as an optional argument, so you don't have to change $location.hash at all. (documentation)
This is the best solution because it doesn't affect the route at all. I couldn't get any of the other solutions to work because I'm using ngRoute and the route would reload as soon as I set $location.hash(id), before $anchorScroll could do its magic.
Here is how to use it... first, in the directive or controller:
$scope.scrollTo = function (id) {
$anchorScroll(id);
}
and then in the view:
Text
Also, if you need to account for a fixed navbar (or other UI), you can set the offset for $anchorScroll like this (in the main module's run function):
.run(function ($anchorScroll) {
//this will make anchorScroll scroll to the div minus 50px
$anchorScroll.yOffset = 50;
});
This was my solution using a directive which seems more Angular-y because we're dealing with the DOM:
Plnkr over here
github
CODE
angular.module('app', [])
.directive('scrollTo', function ($location, $anchorScroll) {
return function(scope, element, attrs) {
element.bind('click', function(event) {
event.stopPropagation();
var off = scope.$on('$locationChangeStart', function(ev) {
off();
ev.preventDefault();
});
var location = attrs.scrollTo;
$location.hash(location);
$anchorScroll();
});
};
});
HTML
<ul>
<li>Section 1</li>
<li>Section 2</li>
</ul>
<h1 id="section1">Hi, I'm section 1</h1>
<p>
Zombie ipsum reversus ab viral inferno, nam rick grimes malum cerebro. De carne lumbering animata corpora quaeritis.
Summus brains sit​​, morbo vel maleficia? De apocalypsi gorger omero undead survivor dictum mauris.
Hi mindless mortuis soulless creaturas, imo evil stalking monstra adventus resi dentevil vultus comedat cerebella viventium.
Nescio brains an Undead zombies. Sicut malus putrid voodoo horror. Nigh tofth eliv ingdead.
</p>
<h1 id="section2">I'm totally section 2</h1>
<p>
Zombie ipsum reversus ab viral inferno, nam rick grimes malum cerebro. De carne lumbering animata corpora quaeritis.
Summus brains sit​​, morbo vel maleficia? De apocalypsi gorger omero undead survivor dictum mauris.
Hi mindless mortuis soulless creaturas, imo evil stalking monstra adventus resi dentevil vultus comedat cerebella viventium.
Nescio brains an Undead zombies. Sicut malus putrid voodoo horror. Nigh tofth eliv ingdead.
</p>
I used the $anchorScroll service. To counteract the page-refresh that goes along with the hash changing I went ahead and cancelled the locationChangeStart event. This worked for me because I had a help page hooked up to an ng-switch and the refreshes would esentially break the app.
Try to set a hash prefix for angular routes $locationProvider.hashPrefix('!')
Full example:
angular.module('app', [])
.config(['$routeProvider', '$locationProvider',
function($routeProvider, $locationProvider){
$routeProvider.when( ... );
$locationProvider.hashPrefix('!');
}
])
I got around this in the route logic for my app.
function config($routeProvider) {
$routeProvider
.when('/', {
templateUrl: '/partials/search.html',
controller: 'ctrlMain'
})
.otherwise({
// Angular interferes with anchor links, so this function preserves the
// requested hash while still invoking the default route.
redirectTo: function() {
// Strips the leading '#/' from the current hash value.
var hash = '#' + window.location.hash.replace(/^#\//g, '');
window.location.hash = hash;
return '/' + hash;
}
});
}
This is an old post, but I spent a long time researching various solutions so I wanted to share one more simple one. Just adding target="_self" to the <a> tag fixed it for me. The link works and takes me to the proper location on the page.
However, Angular still injects some weirdness with the # in the URL so you may run into trouble using the back button for navigation and such after using this method.
This may be a new attribute for ngView, but I've been able to get it anchor hash links to work with angular-route using the ngView autoscroll attribute and 'double-hashes'.
ngView (see autoscroll)
(The following code was used with angular-strap)
<!-- use the autoscroll attribute to scroll to hash on $viewContentLoaded -->
<div ng-view="" autoscroll></div>
<!-- A.href link for bs-scrollspy from angular-strap -->
<!-- A.ngHref for autoscroll on current route without a location change -->
<ul class="nav bs-sidenav">
<li data-target="#main-html5">HTML5</li>
<li data-target="#main-angular"><a href="#main-angular" ng-href="##main-angular" >Angular</a></li>
<li data-target="#main-karma">Karma</li>
</ul>
I could do this like so:
<li>
About
</li>
Here is kind of dirty workaround by creating custom directive that will scrolls to specified element (with hardcoded "faq")
app.directive('h3', function($routeParams) {
return {
restrict: 'E',
link: function(scope, element, attrs){
if ('faq'+$routeParams.v == attrs.id) {
setTimeout(function() {
window.scrollTo(0, element[0].offsetTop);
},1);
}
}
};
});
http://plnkr.co/edit/Po37JFeP5IsNoz5ZycFs?p=preview
Question 1
Question 2
Question 3
If you don't like to use ng-click here's an alternate solution. It uses a filter to generate the correct url based on the current state. My example uses ui.router.
The benefit is that the user will see where the link goes on hover.
My element
The filter:
.filter('anchor', ['$state', function($state) {
return function(id) {
return '/#' + $state.current.url + '#' + id;
};
}])
My solution with ng-route was this simple directive:
app.directive('scrollto',
function ($anchorScroll,$location) {
return {
link: function (scope, element, attrs) {
element.click(function (e) {
e.preventDefault();
$location.hash(attrs["scrollto"]);
$anchorScroll();
});
}
};
})
The html is looking like:
link
You could try to use anchorScroll.
Example
So the controller would be:
app.controller('MainCtrl', function($scope, $location, $anchorScroll, $routeParams) {
$scope.scrollTo = function(id) {
$location.hash(id);
$anchorScroll();
}
});
And the view:
Scroll to #foo
...and no secret for the anchor id:
<div id="foo">
This is #foo
</div>
I was trying to make my Angular app scroll to an anchor opon loading and ran into the URL rewriting rules of $routeProvider.
After long experimentation I settled on this:
register a document.onload event handler from the .run() section of
the Angular app module.
in the handler find out what the original
has anchor tag was supposed to be by doing some string operations.
override location.hash with the stripped down anchor tag (which
causes $routeProvider to immediately overwrite it again with it's
"#/" rule. But that is fine, because Angular is now in sync with
what is going on in the URL 4) call $anchorScroll().
angular.module("bla",[]).}])
.run(function($location, $anchorScroll){
$(document).ready(function() {
if(location.hash && location.hash.length>=1) {
var path = location.hash;
var potentialAnchor = path.substring(path.lastIndexOf("/")+1);
if ($("#" + potentialAnchor).length > 0) { // make sure this hashtag exists in the doc.
location.hash = potentialAnchor;
$anchorScroll();
}
}
});
I am not 100% sure if this works all the time, but in my application this gives me the expected behavior.
Lets say you are on ABOUT page and you have the following route:
yourApp.config(['$routeProvider',
function($routeProvider) {
$routeProvider.
when('/about', {
templateUrl: 'about.html',
controller: 'AboutCtrl'
}).
otherwise({
redirectTo: '/'
});
}
]);
Now, in you HTML
<ul>
<li>First Part</li>
<li>Second Part</li>
<li>Third Part</li>
</ul>
<div id="tab1">1</div>
<div id="tab2">2</div>
<div id="tab3">3</div>
In conclusion
Including the page name before the anchor did the trick for me.
Let me know about your thoughts.
Downside
This will re-render the page and then scroll to the anchor.
UPDATE
A better way is to add the following:
First Part
Get your scrolling feature easily. It also supports Animated/Smooth scrolling as an additional feature. Details for Angular Scroll library:
Github - https://github.com/oblador/angular-scroll
Bower: bower install --save angular-scroll
npm : npm install --save angular-scroll
Minfied version - only 9kb
Smooth Scrolling (animated scrolling) - yes
Scroll Spy - yes
Documentation - excellent
Demo - http://oblador.github.io/angular-scroll/
Hope this helps.
See https://code.angularjs.org/1.4.10/docs/api/ngRoute/provider/$routeProvider
[reloadOnSearch=true] - {boolean=} - reload route when only $location.search() or $location.hash() changes.
Setting this to false did the trick without all of the above for me.
Based on #Stoyan I came up with the following solution:
app.run(function($location, $anchorScroll){
var uri = window.location.href;
if(uri.length >= 4){
var parts = uri.split('#!#');
if(parts.length > 1){
var anchor = parts[parts.length -1];
$location.hash(anchor);
$anchorScroll();
}
}
});
Try this will resolve the anchor issue.
app.run(function($location, $anchorScroll){
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
document.querySelector(this.getAttribute('href')).scrollIntoView({
behavior: 'smooth'
});
});
});
});
On Route change it will scroll to the top of the page.
$scope.$on('$routeChangeSuccess', function () {
window.scrollTo(0, 0);
});
put this code on your controller.
In my mind #slugslog had it, but I would change one thing. I would use replace instead so you don't have to set it back.
$scope.scrollTo = function(id) {
var old = $location.hash();
$location.hash(id).replace();
$anchorScroll();
};
Docs Search for "Replace method"
None of the solution above works for me, but I just tried this, and it worked,
Question 1
So I realized I need to notify the page to start with the index page and then use the traditional anchor.
Sometime in angularjs application hash navigation not work and bootstrap jquery javascript libraries make extensive use of this type of navigation, to make it work add target="_self" to anchor tag.
e.g. <a data-toggle="tab" href="#id_of_div_to_navigate" target="_self">
I'm using AngularJS 1.3.15 and looks like I don't have to do anything special.
https://code.angularjs.org/1.3.15/docs/api/ng/provider/$anchorScrollProvider
So, the following works for me in my html:
<ul>
<li ng-repeat="page in pages"><a ng-href="#{{'id-'+id}}">{{id}}</a>
</li>
</ul>
<div ng-attr-id="{{'id-'+id}}" </div>
I didn't have to make any changes to my controller or JavaScript at all.

Different view depending on which state

I have an Angular page which has a login page, a register account page, and the rest of the page. Login/register looks different from the other pages, as they have no sidebar, toolbar, and so on. Totally different layout. I have an index page and an index controller, which determines which view to load currently. Something like this:
<body ng-app="myApp" ng-controller="IndexController as vm">
<div ui-view ng-if="$state.current.name === 'home'"></div>
<div ng-if="$state.current.name !== 'home'">
<!--TOOLBAR-->
<!--SIDEBAR-->
<div ui-view></div>
</div>
</body>
So basically if the current state is home, it should load the first view (with no toolbar, sidenav etc.). If the current state is not set to home, it should show the toolbar, sidebar, and so on. I thought this would work, but is there a better way to do it? I have all my states inside my app.js.
Let me know if there's a better way of separating the different views. Why I'm doing this, is because I want to keep all the includes (models, views, controllers) and other scripts on one page (my index.html which contains the above) and just change the view accordingly. Thanks.
Using the controller as syntax, i'm assuming $state isn't defined. You should add it to the controller scope (not $scope) (this.$state = $state) and call it using vm.$state.current....
Ideally you wouldn't do this though and use ui-routers nested states which solves this problem exactly.
https://github.com/angular-ui/ui-router/wiki/nested-states-%26-nested-views
There are a number of ways to do this. One is to fork the states so home does not inherit parent views.
$stateProvider.state('home', {})
.state('app', {}) // has parent views like menu
.state('app.foo', {}) // inherits views from app
Or used named views
<body>
<div ui-view="menu"></div>
<div ui-view="content"></div>
</body>
$stateProvider.state('app' { views: { menu: { ... } } } )
.state('app.home', { views: { 'menu#app': { /* inject empty tpl */ }, 'content#app' : { ... } } )
.state('app.foo', { views: { 'content#app' : { ... } } } );

Is it possible to autoscroll in AngularUI Router without changing states?

I'm using AngularUI Router with Bootstrap. I have two views within the same state, and want to scroll to the second view when a button is clicked. I don't need to do any scrolling when the initial state and views load. I want the page to scroll to #start when the "Add More" button is clicked. I've attempted to use $anchorScroll to accomplish this, but am not having any luck.
Any suggestions on a good way to accomplish this?
HTML:
<!-- index.html -->
<div ui-view="list"></div>
<div ui-view="selectionlist"></div>
<!-- list.html -->
<div id="scrollArea"><a ng-click="gotoBottom()" class="btn btn-danger btn-lg" role="button">Add More</a>
<!-- selectionlist.html -->
<div class="row" id="start"></div>
Javascript for Controller:
myApp.controller('SelectionListCtrl', function (Selections, $location, $anchorScroll) {
var selectionList = this;
selectionList.selections = Selections;
this.selectedServices = Selections;
selectionList.gotoBottom = function() {
$location.hash('start');
$anchorScroll();
};
});
Javascript for Routes:
myApp.config(function ($stateProvider, $urlRouterProvider, $uiViewScrollProvider) {
$uiViewScrollProvider.useAnchorScroll();
$urlRouterProvider.otherwise('/');
$stateProvider
.state('selection', {
url: '/selection',
views: {
'list': {
templateUrl: 'views/list.html',
controller: 'ProjectListCtrl as projectList'
},
'selectionlist': {
templateUrl: 'views/selectionlist.html',
controller: 'SelectionListCtrl as selectionList'
}
}
})
Yes, it is possible to autoscroll in AngularUI Router without changing states.
As mentionned previously in my comment, you need to call the scrolling function with an ng-click="gotoBottom()" instead of an ng-click="gotoSection()"
Also, the function definition gotoBottom() must be in the ProjectListCtrl, not in the SelectionListCtrl. This is because the call gotoBottom() happens in the list view:
'list': {
templateUrl: 'list.html',
controller: 'ProjectListCtrl as projectList'
}
As gotoBottom() is called from the list.html view, the corresponding controller in $stateProvider must be the one where you define gotoBottom().
Here are two working ways of accomplishing your goal:
1. You inject $scope inside the controller ProjectListCtrl. You then define the $scope.gotoBottom function in the same controller.
The scope is the glue between the controller and the view. If you want to call a controller function from your view, you need to define the controller function with $scope
app.controller('ProjectListCtrl', function ($location, $anchorScroll,$scope) {
var selectionList = this;
//selectionList.selections = Selections;
//this.selectedServices = Selections;
$scope.gotoBottom = function() {
console.log("go to bottom");
$location.hash('start');
$anchorScroll();
};
});
In the list.html view, you can then call the $scope.gotoBottom function just with gotoBottom(): ng-click="gotoBottom()"
2. Or you use the controller as notation, as when you wrote ProjectListCtrl as projectList.
this.gotoBottomWithoutScope = function(){
$location.hash('start');
$anchorScroll();
};
With this notation, you write this.gotoBottomWithoutScope in the ProjectListCtrl. But in the view, you must refer to it as projectList.gotoBottomWithoutScope().
Please find a working plunker
To learn more about the this and $scope notations, please read this:
Angular: Should I use this or $scope
this: AngularJS: "Controller as" or "$scope"?
and this: Digging into Angular’s “Controller as” syntax

Angular $location doesn't work

I created a plunkr for this code and it can be viewed here:
The problem is very simple. I am trying to create a master/details scenario. So there are two templates: listings and details. In the listing controller there is a methods redirects to the detials route. This method works well as i verified it with the debugger (via breaking point).
$scope.goToDetails = function(propItem) {
//$rootScope.currentProperty = propItem;
$location.path('/details/');
}
The 'details' path (see blow) calls the 'detailsController', which is currently (for testing purposes) defined as:
var detailsController = function($scope, $http, $routeParams, $rootScope) {
var dosomething = "do";
};
I verified with the debugger that the execution indeed reaches the "dosomething" command and that the route changes in the browser to "details". However, and HERE is the problem, when I continue with the debugger, angular changes the route back to the default route. I went over the definitions but nothing that i did seems wrong.Any ideas?
Here is how I defined the routes:
app.config(function($routeProvider) {
$routeProvider
.when('/main/', routes.main)
.when('/details/', routes.details)
.otherwise({
redirectTo: '/main'
});
});
var routes = {
main: {
templateUrl: 'PropertiesResults.html',
controller: 'listingController'
},
details: {
templateUrl: 'property-detail.html',
controller: 'detailsController'
},
}
Replace:
<a href="#" ng-click="goToDetails(property)" ...
With:
<a href="" ng-click="goToDetails(property)" ...
Or it will go to your otherwise route.
Change your link to be
Read More
A few things to consider:
Links should work just like in a regular html page
If you want to execute code on a new page, look into putting that code on the route, or in a controller in the new view.
If you want to conditionally enable or disable the link, think about disabling the link with something like <a ng-disabled="expression"... this might not work out of the box but you could add a custom directive.
If you still need to run that code in a controller method, consider using a <button type="button" class="link"... and style it to look like a link, e.g. display: inline; border: 0; background: transparent;"
Happy coding

Loading icon on state change with resolve

I'm want to display a loading icon in my view, but I doesn't seem to work..
My view renders when my $resource in the resolve is loaded.
.run(['$rootScope', '$state', '$stateParams', function () {
$rootScope.$on('$stateChangeStart', function () {
console.log('start');
$rootScope.spinner = true;
});
$rootScope.$on('$stateChangeSuccess', function () {
console.log('end');
$rootScope.spinner = false;
});
}])
Can't set the spinner because my view isn't loaded yet..
How am I supposed to ng-show on my spinner div then..?
You can put the spinner to be outside of any view. That way you don't need to include it in any template but you'll still be able to control it with ng-show and watching state changes like you did.
I've solved it by putting my loading div outside of the ui-view.
Because it is outside the controller of the ui-view, I can just use the $rootScope.loading here.
<div ng-show="loading" class="loader">
<p>Loading, please wait...</p>
</div>
<div ui-view ng-class="{transparent: loading}"></div>

Resources