Asmor
Asmor

Reputation: 5181

Persisting scroll position in AngularJS after refresh

I've got a web app I've written with AngularJS: http://asmor.com/anr

This app is for helping to build decks for a particular game.

When you add cards to your deck, they're added to a fixed-positioned div in the top right corner. That div has a dynamically-set max-height so that it won't be occluded by the bottom of the browser's window or by another fixed div in the bottom right corner of the page.

If you hit the max height, the list of cards in the deck scrolls. See below:

enter image description here

Now the problem is that when you add or remove a card from the deck while it's scrolled by clicking the red or green buttons, Angular redraws the deck list and resets the scroll back to the top.

For convenience, here's the code I'm using for the deck list:

<div id="deck" class="shown-{{ deck.notEmpty }} faction{{ deck.identity.faction }}">
    <div class="deckStat cardTotal">
        <strong class="valid-{{ deck.enoughCards }}">Cards:</strong> {{ deck.cardCount }} / {{ deck.minCards }}
    </div>
    <div class="deckStat agendaPointsTotal shown-{{ deck.isCorp }}">
        <strong class="valid-{{ deck.enoughAgendaPoints }}">Agenda points:</strong> {{ deck.totalPoints }} / {{ deck.minPoints }}
    </div>
    <div class="deckStat influencePointsTotal">
        <strong class="valid-{{ deck.withinInfluenceLimit }}">Influence points:</strong> {{ deck.influenceTotal }} / {{ deck.influenceAvailable }}
    </div>

    <div class="deckIdentity" ng-mouseover="setPreviewLink(deck.identity)">
        <strong>Identity:</strong>
        {{ deck.identity.name }}
    </div>

    <div id="deckScrollContainer" style="max-height: {{ getMaxDeckHeight() }}px">
        <ul ng-repeat="(typeName, typeHash) in deck.cardsByType">
            <li class="deckTypeHeader">
                <strong>{{ typeName }}</strong>
                <span class="quantity">
                    ({{ typeHash.qty }})
                </span>
            </li>
            <li ng-repeat="(cardName, qty) in typeHash.cards" class="card faction{{ getCardFaction(cardName) }}" ng-mouseover="setPreviewLink(cardName)">
                <button class="btn btn-mini btn-success add qty{{qty}}" ng-click="addCard(cardName)"><i class="icon-plus"></i></button>
                <button class="btn btn-mini btn-danger remove qty{{qty}}" ng-click="removeCard(cardName)"><i class="icon-minus"></i></button>
                <span class="quantity">
                    {{ qty }}x
                </span>
                {{ cardName }}
                <span class="influence">
                    {{ getInfluenceString(cardName, qty) }}
                </span>
            </li>
        </ul>
    </div>
</div>

One thing I've tried was adding a function when you add or remove cards that will grab the current scroll position, and then later on replace it.

$scope.addCard = function (c) {
    $scope.setDeckScroll();
    $scope.deck.add(c);
};
$scope.setDeckScroll = function () {
    $scope.deckScrollPosition = document.getElementById("deckScrollContainer").scrollTop;
};

And...

$scope.getCardFaction = function (card) {
    $scope._persist();
    card = getCardByName(card);
    return card.faction;
};
$scope._persist = function () {
    // Any weird stuff that needs to be done on redraw/refresh
    if ($scope.deckScrollPosition) {
        document.getElementById("deckScrollContainer").scrollTop = $scope.deckScrollPosition;
        $scope.deckScrollPosition = null;
    }
};

As far as I can tell, Angular is redrawing the page many times when it does redraw it. I don't know when the last re-draw is going to be, and I can't just blindly set the scrollPosition every time because then if someone scrolled to the top on their own I wouldn't be able to tell the difference between that and if it had scrolled to the top because of a redraw.

One option I've considered is using a setTimeout to clear $scope.deckScrollPosition, so that

I was able to get it to work by clearing the scroll position variable with a setTimeout (so that every redraw would have access to the variable)

$scope._persist = function () {
    // Any weird stuff that needs to be done on redraw/refresh
    if ($scope.deckScrollPosition) {
        document.getElementById("deckScrollContainer").scrollTop = $scope.deckScrollPosition;
        setTimeout(function () {
            $scope.deckScrollPosition = null;
        }, 100);
    }
};

...but this seems really hacky. I'm hoping there might be a better way. Any ideas on how I could either...

  1. Make angular only redraw once per change in the data (e.g. maybe I've coded something poorly that's forcing it to redraw multiple times?)

  2. Somehow cope with the redraw without resorting to setTimeout chicanery?

Upvotes: 3

Views: 10829

Answers (2)

Pete
Pete

Reputation: 4622

A couple of things that should help you. If the items in deck.cardsByType have an ID or some unique field, you can tell AngularJS to track the items and only redraw ones with a new ID, the other items get updated, which should stop the whole list being re-drawn:

<li ng-repeat="(cardName, qty) in typeHash.cards track by id">

You also said "I don't know when the last re-draw is going to be" - you can add a directive for when an item is drawn:

app.directive( 'repeatLoad', function(){
    return {
        link: function( scope, element, attrs ) {
            scope.$eval( attrs['repeatLoad'] );
        }
    }
}); 

Then add the directive with a function in your controller to call

<li 
  ng-repeat="(cardName, qty) in typeHash.cards track by id"
  repeat-load="loadedElement()"
>

And you could count them in your controller, and check when the final one has been added

$scope.cnt_cards = 0;
$scope.loaded_cards = 0;

$scope.loadedElement = function() {
  $scope.loaded_cards++;

  if( $scope.cnt_cards == $scope.loaded_cards ) {
    console.log('All cards loaded');
  }
}

Upvotes: 1

Narretz
Narretz

Reputation: 4993

Found an answer here:

http://www.benlesh.com/2013/02/angular-js-scrolling-to-element-by-id.html

And the example plunkr, a little customised is here:http://plnkr.co/edit/CTVgvEoY7CnLX38o70i4?p=preview

Basically, you set the location.hash and the scroll to it. Hope it works for you, as you setup is a little more complex.

e: Just noticed that it actually focusses the viewport on the last item, which is in fact undesireable.

Upvotes: 2

Related Questions