简体   繁体   中英

Multiple views on one page with history in AngularJS using Angular UI Router

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:

  1. 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...
  2. 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.com/Q60FWtL7

You can see the app here: http://dev.driz.co.uk/AngularPage/app/#/about

For an example of something similar ( https://devart.withgoogle.com/#/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.

You 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.com/flesler/jquery.localScroll

https://github.com/flesler/jquery.scrollTo

On Page loading you scroll to the View defined by the hashtag or any other url parameter. Example: www.sample.com#aboutUs Or www.samplecom?view=aboutUs

on scrolling, i should:

1. bind to "scroll" events, 
2. compare 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.com/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.com/guide.html#/learn-filter.html
2. http://ngtutorial.com/learn/route.html#/_tblcontents_5

if you don't like hashtags, you can parse location.url()

I use https://github.com/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 complete 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;
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM