In my AngularJS app, I have an about page with several sub-sections. All these sub-sections are on the same page and part of the same template. However I want to access each section via it's own URL that will scroll down to the correct section if it matches.
I've set up the states as so:
.state('about', {
url: "/about",
templateUrl: "partials/about.html",
})
.state('about.team', {
url: "/about/team",
templateUrl: "partials/about.html"
})
.state('about.office', {
url: "/about/office",
templateUrl: "partials/about.html"
})
.state('about.other', {
url: "/about/other",
templateUrl: "partials/about.html"
});
And on the about page I have a menu like:
<div class="menu">
<a ui-sref-active="selected"
ui-sref="about.team">The Team</a>
<a ui-sref-active="selected"
ui-sref="about.office">The Office</a>
<a ui-sref-active="selected"
ui-sref="about.other">Other</a>
</div>
This menu is fixed at the bottom and when the user clicks the link or when visiting the url direct via the address bar it should scroll to that section (updating the url where need be to match it).
However I'm not sure how to do this (via Angular). To summarize:
- How can I scroll to the different sections on the same page via the URL? As I can't use the standard hash trick as it already uses hashes for the address (we support IE8). And I don't want to use hashes at all when in HTML5 mode! It should also scroll to the section on page load AFTER the app has loaded, so need some way of firing the code that scrolls the user after something else...
- How can I make this work on click of the buttons? Currently when clicking the buttons, they change the URL but don't scroll to the content.
The HTML for the page looks Like:
You can see the app here:
For an example of something similar () as you can see it scrolls down to the correct section based on the url AFTER the page has loaded... and doesn't use a hash but the ACTUAL url.
In my AngularJS app, I have an about page with several sub-sections. All these sub-sections are on the same page and part of the same template. However I want to access each section via it's own URL that will scroll down to the correct section if it matches.
I've set up the states as so:
.state('about', {
url: "/about",
templateUrl: "partials/about.html",
})
.state('about.team', {
url: "/about/team",
templateUrl: "partials/about.html"
})
.state('about.office', {
url: "/about/office",
templateUrl: "partials/about.html"
})
.state('about.other', {
url: "/about/other",
templateUrl: "partials/about.html"
});
And on the about page I have a menu like:
<div class="menu">
<a ui-sref-active="selected"
ui-sref="about.team">The Team</a>
<a ui-sref-active="selected"
ui-sref="about.office">The Office</a>
<a ui-sref-active="selected"
ui-sref="about.other">Other</a>
</div>
This menu is fixed at the bottom and when the user clicks the link or when visiting the url direct via the address bar it should scroll to that section (updating the url where need be to match it).
However I'm not sure how to do this (via Angular). To summarize:
- How can I scroll to the different sections on the same page via the URL? As I can't use the standard hash trick as it already uses hashes for the address (we support IE8). And I don't want to use hashes at all when in HTML5 mode! It should also scroll to the section on page load AFTER the app has loaded, so need some way of firing the code that scrolls the user after something else...
- How can I make this work on click of the buttons? Currently when clicking the buttons, they change the URL but don't scroll to the content.
The HTML for the page looks Like: http://pastebin./Q60FWtL7
You can see the app here: http://dev.driz.co.uk/AngularPage/app/#/about
For an example of something similar (https://devart.withgoogle./#/about/your-opportunity) as you can see it scrolls down to the correct section based on the url AFTER the page has loaded... and doesn't use a hash but the ACTUAL url.
Share Improve this question edited Sep 4, 2014 at 15:43 allenhwkim 27.8k18 gold badges92 silver badges127 bronze badges asked Aug 28, 2014 at 10:43 CameronCameron 28.9k102 gold badges289 silver badges490 bronze badges 6-
You can add a delimiter in url to separate sections. e.g.
#about/home
.#about/office
. Then, you can splithash
by/
and scroll to desired location when you have a section. – Jashwant Commented Aug 30, 2014 at 11:25 - That's not what I want to do! It should be a URL and not rely on the hash! – Cameron Commented Aug 30, 2014 at 11:26
-
/about/your-opportunity
is the hash inhttps://devart.withgoogle./#/about/your-opportunity
, but formatted to look like part of url. – Jashwant Commented Aug 30, 2014 at 14:29 - That's because they have HTML5 mode switched off. In my implementation HTML5 mode will be switched on and therefore cannot rely on hashes. – Cameron Commented Aug 30, 2014 at 15:00
- @Jashwant you able to show an example of what you mean? And how it might work in HTML5 mode? Thanks. – Cameron Commented Aug 31, 2014 at 12:01
8 Answers
Reset to default 2You can try this approach where you create a directive which triggers the scroll on click. You can add this directive to your link as an attribute. where the sref attribute will be the id of the target div.
app.directive('scrollTo', function() {
return {
link: function(scope, element, attrs) {
element.bind('click', function() {
var divPosition = $(attrs.sRef).offset();
$('html, body').animate({
scrollTop: divPosition.top
}, "slow");
});
}
};
});
Plunker
ui-router doesn't place restrictions on the signature of your state object, so you can simply add a property:
.state('about.office', {
url: "/about/office"
,templateUrl: "partials/about.html"
,scrollToId = "divAboutOffice";
})
Then your controller can access this data when you inject the $state service.
$('html, body').animate({
scrollTop: $($state.current.scrollToId).offset().top
}, "slow");
assuming you included the id="divAboutOffice"
in your HTML. (and assuming my jQuery reference was correct; I don't use jquery so I'm not sure.)
Depending on your needs, you would wrap that animate()
in an $on()
for one of the events: $locationChangeSuccess)
, $routeChangeSuccess
, or $stateChangeSuccess
also: You don't have to use the "id", as I have - You could leverage the existing ui-sref
if you really want to search for elements by attribute. In your example that appears to match $state.current.name
I would create a scrollTo directive using window.scrollTo or a jquery scrollTo Plugin.
Example: Angular directive to scroll to a given item using window.scrollTo
Jquery scrollTo Plugins:
https://github./flesler/jquery.localScroll
https://github./flesler/jquery.scrollTo
On Page loading you scroll to the View defined by the hashtag or any other url parameter. Example: www.sample.#aboutUs Or www.sample?view=aboutUs
on scrolling, i should:
1. bind to "scroll" events,
2. pare if I reached the position of each headers, using "element.offset().top"
(after making this headers a directive. (i should make headers a directive, check how table of contents are generated from ngtutorial./learn/scope)
3. update location.url()
on loading (i prefer using hashtags, because I can just use $anchorScroll() or use .when()), anyways examples on updating when loading are:
1. http://ngtutorial./guide.html#/learn-filter.html
2. http://ngtutorial./learn/route.html#/_tblcontents_5
if you don't like hashtags, you can parse location.url()
I use https://github./durated/angular-scroll in my own application to do scrolling control (and scroll spy stuff), instead of using locationHash. I've not tried this solution myself, but I reckon it'll work.
You'll want to declare some kind of item id on important divs (to be the place you scroll to).
Then you can declare a state "about.stately" with a url /about/:locale (in ui-router stateParams don't have to be integers). Then in the controller for about.stately, you can do (from the angular-scroll readme):
var someElement = angular.element(document.getElementById('some-id'));
$document.scrollToElement(someElement, offset, duration);
Pick which element you're scrolling to based on the $stateParams.locale in your controller. Be careful to wait until after all your child directives and stuff load - you may have to $evalAsync to get it to work properly. But that's the gist of it, I think.
This is what I have tried at the moment:
var scrolled = false;
var listenToScroll = false;
$('.subsection').each(function(i) {
var position = $(this).position();
$(this).scrollspy({
min: position.top,
max: position.top + $(this).outerHeight(),
onEnter: function(element, position) {
if(element.id){
if(listenToScroll) {
scrolled = true;
$state.transitionTo('about.'+element.id);
}
} else {
if(listenToScroll) {
scrolled = true;
$state.transitionTo('about');
}
}
}
});
});
$scope.startListenToScroll = function() {
listenToScroll = true;
}
$scope.stopListenToScroll = function(){
listenToScroll = false;
}
$scope.$on('$stateChangeStart', function(){
$scope.stopListenToScroll();
});
$scope.$on('$stateChangeSuccess', function(){
console.log(scrolled);
if( !scrolled ) {
var link = $state.current.name.split('.');
if(link.length>1){
var divPosition = $( '#' + link[1] ).offset();
$('body').stop(true,true).animate({ scrollTop: divPosition.top }, "fast", function(){
$scope.startListenToScroll();
scrolled = false;
});
} else {
$('body').stop(true,true).animate({ scrollTop: 0 }, "fast", function(){
$scope.startListenToScroll();
scrolled = false;
});
}
} else {
$scope.startListenToScroll();
scrolled = false;
}
});
So as you can see I listen to the stateChangeSuccess instead of clicks! That way I hook into any state changes regardless if they were called by links or browser history buttons and perform the scroll as need be.
I turn scrollspy off as soon as a start change starts, so that the scrolling animation doesn't call another state change (as it's possible you will scroll THROUGH other sections creating fake history entries). And then switch it back on again when the animation is plete so I can listen for scroll events again.
stateChangeSuccess is fired on page load so it handles any pasting of URLs.
I have a variable called scrolled
so that I can track if a state change was caused by the user scrolling or not. This is because I don't need to animate the scrollTop as the user has scrolled themseleves.
Thoughts?
what you need to do is create a directive at the top of the page besides the ui-router that watches the state and then scrolls according to the right anchor. There is no need then to use templateUrls since what you are trying to achieve is some kind of smoothing.
// get the current path
var x=$location.path();
// say x='about/team'
//read the last tag and set scrollTo to the top offset of desired div
if(lasttag(x)==team){
scroll to top offset of TEAM div;
}
else if(lasttag(x)==office){
scroll to top offset of OFFICE div;
}