Retain scroll position when a button is toggled - javascript

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.

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;
}

Related

AngularJS Controller Not Showing/Hiding Correctly

I am trying to get content to disappear on button click and then show a new set of content on that button click. I cannot quite get this to work. I commented what each section is doing. The first section doesn't disappear on button click. The second section works as expected and does disappear on button click and the third section doesn't show up on button click. Helps is greatly appreciated and I look forward to learning from this!
I thought by adding a controller it would all function together.
HTML
<!-- THIS DOESN'T DISAPPEAR ON BUTTON CLICK -->
<div ng-controller="EventCtrl" ng-hide="eventComplete">
<h2>Example that doesn't disappear on button click</h2>
</div>
<!-- THIS WILL DISAPPEAR ON BUTTON CLICK -->
<div ng-controller="EventCtrl" ng-hide="eventComplete">
<div>
<h2>Example</h2>
<md-button ng-click="eventFinish();">Finish</md-button>
</div>
<!-- THIS DOESN'T SHOW ON BUTTON CLICK -->
<div ng-controller="EventCtrl" ng-show="eventComplete">
<h2>Complete!</h2>
</div>
</div>
ANGULAR
.controller('EventCtrl', function($rootScope,$state,$scope,$timeout){
var self = this;
$scope.eventComplete = false;
$scope.eventFinish=function(){
console.log('eventFinish'); //This logs
$scope.eventComplete = true;
};
})
You wrapped the div you want to hide around the div you want to show. The following html should solve the issue:
<div ng-controller="EventCtrl">
<div ng-hide="eventComplete">
<h2>Example that doesn't disappear on button click</h2>
</div>
<div ng-hide="eventComplete">
<div>
<h2>Example</h2>
<md-button ng-click="eventFinish();">Finish</md-button>
</div>
</div>
<div ng-show="eventComplete">
<h2>Complete!</h2>
</div>
</div>
EDIT: Found an issue in controller as well. You're missing the closing } for eventFinish :
.controller('EventCtrl', function($rootScope,$state,$scope,$timeout){
var self = this;
$scope.eventComplete = false;
$scope.eventFinish = function() {
console.log('eventFinish');
$scope.eventComplete = true;
};
})
Try to avoid placing same controller inside each other. That will only lead to problems. Instead use Components.
But if you insist on using controllers you could solve it this way. (Code not tested)
HTML
<div ng-controller="EventCtrl">
<div ng-if="showExample(1)">
<h2>Example 1</h2>
<md-button ng-click="onClickExample(2);">Finish</md-button>
</div>
<div ng-if="showExample(2)">>
<h2>Example 2</h2>
<md-button ng-click="onClickExample(1);">Finish</md-button>
</div>
</div>
JS
.controller('EventCtrl', function($rootScope,$state,$scope,$timeout){
$scope.currentExample=1;
$scope.showExample = function(id){
return $scope.currentExample === id;
}
$scope.onClickExample = function(id){
$scope.currentExample = id;
}
});

AngularJS : Adding animation when div tag is updated

I am stuck in this problem where I have a div tag which updates and shows a list of images. I want to add animation when the value in the div tag is updated in the transition from one set of images to another.
Here as you can see in the bottom there are a set of images for girl's hair. And when the user goes to other tab, a different set of images comes. I want animation in that transition.
The AngularJS part for the transition is as follows :
<div ng-swipe-left="avDesignController.onSwipeLeftAction()" ng-swipe-right="avDesignController.onSwipeRightAction()">
<!-- CUSTOMIZABLE TAB BAR -->
<div class="tab-bar-container" >
<div class="scrollmenutab">
<!-- CUSTOMIZABLE MENU -->
<div ng-repeat="customizable in customizables"
ng-click="avDesignController.onCustomizableClicked($event)"
style="display: inline;">
<a ng-if="customizable.allowed == 1">
<div ng-class="{selected: $index==currentIndex}">
<div ng-if="customizable.name == 'Hair'">
<img class="scrollmenutab-icon"
id="{{customizable.name}}-{{$index}}"
src="/app/app_resources/icons/{{genderImage}}Hair.png">
</div>
<div ng-if="customizable.name != 'Hair'">
<img class="scrollmenutab-icon"
id="{{customizable.name}}-{{$index}}"
src="/app/app_resources/icons/{{customizable.name}}.png">
</div>
</div>
</a>
</div> <!-- MENU : END -->
</div>
</div>
<!-- CUSTOMIZABLES -->
<div class="avdesign-item-container" id="avdesign-item-container">
<div id="four-columns" class="grid-container" >
<!-- LOAD CUSTOMIZABLES IF NOT LAST ITEM IN TAB -->
<ul ng-if="currentIndex < (customizables.length - 1)"
class="rig columns-4">
<li ng-repeat="customizableItem in currentCustomizable.customizable_item">
<img class="tab-icon"
src="/app/app_resources/resources/av/{{avatarInfo.name}}/as/{{customizableItem.as}}"
id="customizable-{{$index}}"
ng-click="avDesignController.onCustomizedItemClicked($event)"
ng-class="{highlight: customizableItem.id==currentID}">
</li>
</ul>
<!-- LOAD OUTFITS (FROM avatarOutfit) IF LAST ITEM IN TAB -->
<ul ng-if="currentIndex == (customizables.length - 1)"
class="rig columns-outfit">
<div ng-repeat="brand in outfitBrand" ng-style="{'margin-bottom':'1vh'}">
<div class="brand-icon" >
<img src="/app/app_resources/icons/{{brand.bg_image}}">
</div>
<li ng-repeat="outfit in brand.outfitList">
<img class="outfit-icon"
src="/app/app_resources/resources/av/{{avatarInfo.name}}/as/{{outfit.as}}"
id="outfit-{{$index}}"
ng-click="avDesignController.onOutfitItemClicked($event,$parent.$index)"
ng-class="{highlightOutfit: $index==avatar.outfit_index && $parent.$index==indexParent}">
</li>
</div>
</ul>
</div>
</div>
</div>
Where the functions being called in the JS part is updating accordingly the images.
So Question being how to add transition animation for the same element when it is updated because we are never leaving or entering that element tag
Your question is answered here
Html
<div ng-controller="c">
<div id={{my_id}} width={{widthOfOutsideWrapper}} height={{heightOfOutsideWrapper}}>
<img ng-src="{{src}}" imageonload />
</div>
</div>
and in controller
var app = angular.module('app', []);
app.directive('imageonload', function() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
element.bind('load', function() {
alert('image is loaded');
scope.widthOfOutsideWrapper= this.width;
scope.heightOfOutsideWrapper= this.height;
scope.my_id = "testing";
});
}
};
});
app.controller('c', function($scope) {
$scope.src ="https://www.google.com.ua/images/srpr/logo4w.png";
});
and http://jsfiddle.net/2CsfZ/855/ working example is available here
The above answer is very helpful and might be of a great help for many.
But the answer to my question is you simply can't. ngAnimate's ngEnter and ngLeave will not able to recognize a change unless the element is changed and not what is inside the element.
So either some hack needs to be applied like using ngRepeat with different id's for different updation that happens to the element or something else needs to be done.

Create custom follow button for my application

I would like a directive that dynamically knows if I'm following the user in my App.
I have a resource to get the currentUser
this.following = function () {
var defer = $q.defer();
var user = $cookies.getObject('currentUser');
UserResource.get({id: user.id}).$promise.then(
function (data) {
defer.resolve(data.following);
});
return defer.promise;
};
This is in one of my services. It returns all users that I'm following.
When instantiating my controller I fetch the users I follow within my app:
UserService.following().then(
function (data) {
$scope.following = data;
});
I would like to move that into a directive so that I can easily reuse it somewhere else in my app.
This is the HTML I am using right now (and it's not really beautiful) :
<div class="item" ng-repeat="user in users">
<div class="right floated content">
<div ng-show="isFollowing(user)" class="ui animated flip button" tabindex="0"
ng-click='unFollow(user)'>
<div class='visible content'>
Following
</div>
<div class="hidden content">
Unfollow
</div>
</div>
<div ng-hide="isFollowing(user)" ng-click="follow(user)" class="ui button">Follow</div>
</div>
</div>
But instead something like :
<div class="item" ng-repeat="user in users">
<um-follow-button um-user="user"></um-follow-button>
</div>
then depending if I'm following the user or not then render one of the two options.
I don't know if I will have to use a controller in my directive.
I have looked at : https://gist.github.com/jhdavids8/6265398
But it looks like a mess.

How to solve opening & closing div bootstrap issue?

I'm rendering with handlebars a list of items and I'm using bootstrap to create a toggle div for every item. Handlebars works fine and also the toggle but not the closing-div function of bootstrap.
(I've already try to use bootstrap accordion but it was not working, so I would like solve the problem using jQuery)
Every time I open a new div it should close the other one (if there is one already open); This is not working, I can open more than one panel for time.
<section id="list-wrap">
<script id="list-items" type="text/x-handlebars-template">
{{#each cards}}
<div class="panel">
<div class="panel-heading grad">
<h4>
<span class="title-style">{{name}}</span>
<a data-toggle="collapse" href="#{{this.code}}">
<i class="chevron_toggleable indicator glyphicon glyphicon-chevron-right pull-left"></i>
</a>
<p class="apr title-style"> {{apr}} % APR </p>
</h4>
</div>
<div id="{{this.code}}" class="changeClass panel-collapse collapse">
<div class="panel-body">
<div>
<div class="img-div">
<img src="assets/{{code}}.png">
</div>
<div class="info-div"><p class="info-paragraf">{{information}}</p>
</div>
<div class="cashback-div">
<p class="cashback-paragraf-1">Cashback</p>
<p class="cashback-paragraf-2">{{cashback}}</p>
</div>
</div>
</div>
</div>
</div>
{{/each}}
</script>
</section>
this was working until I added a express server:
$('i').click(function () {
$('.changeClass').removeClass('in');
});
Try using jQuery UIs accordion as an option.
You just have to put the items in an accordion tag, wrap each item in a div as a child of the accordion div and then call the accordion method on the accordion div.
http://jqueryui.com/accordion/
I would use a jquery accordion function like this one. Fiddle
$('#accordion').accordion({
collapsible:true,
beforeActivate: function(event, ui) {
// The accordion believes a panel is being opened
if (ui.newHeader[0]) {
var currHeader = ui.newHeader;
var currContent = currHeader.next('.ui-accordion-content');
// The accordion believes a panel is being closed
} else {
var currHeader = ui.oldHeader;
var currContent = currHeader.next('.ui-accordion-content');
}
// Since we've changed the default behavior, this detects the actual status
var isPanelSelected = currHeader.attr('aria-selected') == 'true';
// Toggle the panel's header
currHeader.toggleClass('ui-corner-all',isPanelSelected).toggleClass('accordion-header-active ui-state-active ui-corner-top',!isPanelSelected).attr('aria-selected',((!isPanelSelected).toString()));
// Toggle the panel's icon
currHeader.children('.ui-icon').toggleClass('ui-icon-triangle-1-e',isPanelSelected).toggleClass('ui-icon-triangle-1-s',!isPanelSelected);
// Toggle the panel's content
currContent.toggleClass('accordion-content-active',!isPanelSelected)
if (isPanelSelected) { currContent.slideUp(); } else { currContent.slideDown(); }
return false; // Cancels the default action
}
});

ng-repeat: showing an item on click and hiding the others

I have a ng-repeat which display a list of divs and when I click on one it shows an aditionnal div for the clicked item.
This is working
<div ng-repeat="item in items">
<div ng-click="showfull = !showfull">
<div>
<h1>{{item.title}}</h1>
<p>{{item.content}}</p>
</div>
</div>
<div ng-show="showfull">
<p>{{item.info}}</p>
</div>
<hr/>
</div>
My items are loaded from a json containing a list of item and each item have a default attribut showfull set to false in this json. This is working, but now I want to hide all others item in the list when an item is clicked. I've tryied something like this :
This is not working
<div ng-repeat="item in items">
<div ng-click="expand(item)">
<div>
<h1>{{item.title}}</h1>
<p>{{item.content}}</p>
</div>
</div>
<div ng-show="showfull">
<p>{{item.info}}</p>
</div>
<hr/>
</div>
and in the controller I've added a function :
$scope.expand = function(e) {
e.showfull = !e.showfull;
//TODO: put here a foreach logic to hide all other items
}
But even without the foreach logic this is not working, the item don't show the additionnal div when clicked. I have two question :
I suppose this is due to item being "passed by copy" in the expand function or some kind of scope isolation issue but can you explain me why in detail ?
SOLVED
How can I hide all the additional div of other items when I click an item and show the additionnal content for this item ? Do I need to do a directive ?
NOT SOLVED
Thanks
I think you're on the right track. You need to set the showfull on the item and then use ng-show or ng-if to hide it, or as Gavin mentioned, use a class.
On ng-click you can call your expand function, where you pass the item, toggle it and set all others to hidden.
Template:
<div ng-repeat="item in items>
<div ng-click="expand(item)">
<div>
<h1>{{item.title}}</h1>
<p>{{item.content}}</p>
</div>
</div>
<div ng-show="item.showfull">
<p>{{item.info}}</p>
</div>
<hr/>
</div>
Controller:
$scope.expand = function (item) {
angular.forEach($scope.items, function (currentItem) {
currentItem.showfull = currentItem === item && !currentItem.showfull;
});
};
Your expand method is modifying the item, so your ng-show needs to reference the item:
<div ng-show="item.showfull">
<p>{{item.info}}</p>
</div>
To hide all of your items you need to do something like
$scope.expand = function(item) {
angular.forEach(items, function(i) {
if (i === item) {
i.showfull = !i.showfull;
} else {
i.showfull = false;
}
});
};
Shouldn't the second div you want to show be referencing the item?
<div ng-repeat="item in items>
<div ng-click="expand(item)">
<div>
<h1>{{item.title}}</h1>
<p>{{item.content}}</p>
</div>
</div>
<!-- Added item to ng-show -->
<div ng-show="item.showfull">
<p>{{item.info}}</p>
</div>
<hr/>
</div>

Categories