RITZ XAVI RITZ XAVI - 1 month ago 9
HTML Question

Retain scroll position when a button is toggled

I am working on an angularJS application which has a page where I display around 30 items using ng-repeat. In front of each item, there is a toggle button (enabled/disabled). With the current code that I have, I can toggle these items. But the problem is if I scroll down and toggle lets say item 25, then automatically it scrolls to the top of the page. If I now scroll down, I can see that the toggle actually took place.

So the requirement now is to make sure that the scroll position is retained after the toggle button is clicked.

Please see below the code that I have.

HTML

<div id="eventTypes" class="panel-body">
<div ng-if="!spinner" ng-repeat="item in items" class="panel-body">
<div class="row">
<div class="col-md-9">{{item.itemName)}}</div>
<div class="col-md-3">
<input id="toggleEnabled"
type="button"
ng-class="{'btn-primary': item.enabled}"
value="{{item.enabled ? 'enabled' : 'disabled'}}"
ng-click="toggleEnabled(item)">
</div>
</div>
</div>
<div ng-if="spinner" class="spinner">
<div class="spinner-container container1">
<div class="circle1"></div>
<div class="circle2"></div>
<div class="circle3"></div>
<div class="circle4"></div>
</div>
<div class="spinner-container container2">
<div class="circle1"></div>
<div class="circle2"></div>
<div class="circle3"></div>
<div class="circle4"></div>
</div>
<div class="spinner-container container3">
<div class="circle1"></div>
<div class="circle2"></div>
<div class="circle3"></div>
<div class="circle4"></div>
</div>
</div>
</div>


JS

(function () {
'use strict';
angular.module('myApp').controller('itemsController', function ($scope, itemsService) {
var serviceError = function (errorMsg) {
console.log(errorMsg);
$scope.turnOffSpinner();
};

$scope.items = [];
$scope.item = {};
$scope.spinner = true;

$scope.toggleEnabled = function (item) {
$scope.turnOnSpinner();
itemsService.toggleEnabled(item)
.then(function () {
$scope.loaditems();
});
};

$scope.loaditems = function () {
itemsService.getitems().then(function (response) {
$scope.items = response.data;
}, serviceError);
$scope.turnOffSpinner();
};

$scope.turnOnSpinner = function () {
$scope.spinner = true;
};

$scope.turnOffSpinner = function () {
$scope.spinner = false;
};

$scope.loaditems();
});
}());


How this works right now is, once I click the toggle button, a spinner is enabled. Meanwhile the controller will call the
itemService.toggleEnabled()
method which does an ajax call to the server to just change the status of the item(enabled to disabled or vice-versa) in the backend. On successful change of the status and when the ajax call returns, the
$scope.loadItems()
method is called in the controller. This method will then do another ajax call to fetch the items (now with the updated status of the item that was toggled). The spinner is disabled and the data is then displayed on the UI.
When all of this is done, the page is scrolled to the top. This is annoying when I want to toggle an item which is way down in the list.
I want the page to be present at the same position when I clicked the toggle button of the corresponding item and not scrolling up to the top.

I am new to AngularJS and any help in this regard would be really helpful.

Answer

It looks like your spinner scheme is what's causing you problems:

...
<div ng-if="!spinner" ng-repeat="item in items" class="panel-body">
...
<div ng-if="spinner" class="spinner">
...

Whenever you click your button, you are removing every single element in your ng-repeat from the DOM when you $scope.turnOnSpinner(). That's why it appears to jump to the top. It's not really jumping, there just aren't enough DOM elements to fill up the page, making the page so short that the scrollbar disappears (even if it's only for a second). Then when the spinner is done, your ng-repeat fills up the page with DOM elements again, resulting in your scroll position being lost.

So basically what you are trying to fix is a symptom of a less than ideal loading spinner implementation.

ng-if is a "brutal" way of hiding things in Angular. It's mostly meant to hide things for a longer period of time than "softer" directives like ng-show/ng-hide. One solution to your problem is to use ng-disabled on each one of your buttons to prevent the user from interacting with it while the spinner is active, rather than doing a hard removal of each element:

Before:

<div ng-if="!spinner" ng-repeat="item in items" class="panel-body">
    <div class="row">
        <div class="col-md-9">{{item.itemName)}}</div>
        <div class="col-md-3">
            <input id="toggleEnabled" 
                   type="button" 
                   ng-class="{'btn-primary': item.enabled}" 
                   value="{{item.enabled ? 'enabled' : 'disabled'}}" 
                   ng-click="toggleEnabled(item)">
        </div>
    </div>
</div>

After:

<div ng-repeat="item in items" class="panel-body">
    <div class="row">
        <div class="col-md-9">{{item.itemName)}}</div>
        <div class="col-md-3">
            <input id="toggleEnabled" 
                   ng-disabled="spinner"
                   type="button" 
                   ng-class="{'btn-primary': item.enabled}" 
                   value="{{item.enabled ? 'enabled' : 'disabled'}}" 
                   ng-click="toggleEnabled(item)">
        </div>
    </div>
</div>

Another solution, which I really like and use myself is this Angular module: https://github.com/darthwade/angular-loading

You can attach it to any element in the page and it will put a loading spinner over it and prevent you from interacting with it until your ajax or whatever is done.

If you don't like either of those, try putting your ng-repeat into a container that you can use to prevent interaction with your elements when the spinner is up:

<div class="container" ng-class="{'you-cant-touch-this': spinner}">
  <div ng-repeat="item in items" class="panel-body">
    <div class="row">
        <div class="col-md-9">{{item.itemName)}}</div>
        <div class="col-md-3">
            <input id="toggleEnabled" 
                   type="button" 
                   ng-class="{'btn-primary': item.enabled}" 
                   value="{{item.enabled ? 'enabled' : 'disabled'}}" 
                   ng-click="toggleEnabled(item)">
        </div>
    </div>
  </div>
</div>

Now you can style it in some way to prevent interaction without having to remove all those items from the DOM:

.you-cant-touch-this {
  pointer-events: none;
}
Comments