How can I animate the movement of remaining ng-repeat items when one is removed? - angularjs

I have a dynamic list of items using ng-repeat. When something happens an item may disappear. I have handled smoothly animating the removal of these items using ng-animate, but after they are gone, the remaining items simply snap to their new position. How can I animate this movement smoothly?
I've tried applying an "all" transition to the repeated class and using ng-move with no success.

You can achieve this by animating the max-height property. Check out this sample:
http://jsfiddle.net/k4sR3/8/
You will need to pick a sufficiently high value for max-height (in my sample, I used 90px). When an item is initially being added, you want it to start off with 0 height (I'm also animating left to have the item slide in from the left, as well as opacity, but you can remove these if they don't jibe with what you're doing):
.repeated-item.ng-enter {
-webkit-transition:0.5s linear all;
-moz-transition:0.5s linear all;
-o-transition:0.5s linear all;
transition:0.5s linear all;
max-height: 0;
opacity: 0;
left: -50px;
}
Then, you set the final values for these properties in the ng-enter-active rule:
.repeated-item.ng-enter.ng-enter-active {
max-height: 90px;
opacity: 1;
left: 0;
}
Item removal is a bit trickier, as you will need to use keyframe-based animations. Again, you want to animate max-height, but this time you want to start off at 90px and decrease it down to 0. As the animation runs, the item will shrink, and all the following items will slide up smoothly.
First, define the animation that you will be using:
#keyframes my_animation {
from {
max-height: 90px;
opacity: 1;
left: 0;
}
to {
max-height: 0;
opacity: 0;
left: -50px;
}
}
(For brevity, I'm omitting the vendor-specific definitions here, #-webkit-keyframes, #-moz-keyframes, etc - check out the jsfiddle above for the full sample.)
Then, declare that you will be using this animation for ng-leave as follows:
.repeated-item.ng-leave {
-webkit-animation:0.5s my_animation;
-moz-animation:0.5s my_animation;
-o-animation:0.5s my_animation;
animation:0.5s my_animation;
}
Basics
In case anyone is struggling with figuring out how to get AngularJS animations to work at all, here's an abbreviated guide.
First, to enable animation support, you will need to include an additional file, angular-animate.js, after you load up angular.js. E.g.:
<script type="text/javascript" src="angular-1.2/angular.js"></script>
<script type="text/javascript" src="angular-1.2/angular-animate.js"></script>
Next, you will need to load ngAnimate by adding it to the list of your module's dependencies (in the 2nd parameter):
var myApp = angular.module('myApp', ['ngAnimate']);
Then, assign a class to your ng-repeat item. You will be using this class name to assign the animations. In my sample, I used repeated-item as the name:
<li ng-repeat="item in items" class="repeated-item">
Then, you define your animations in the CSS using the repeated-item class, as well as the special classes ng-enter, ng-leave, and ng-move that Angular adds to the item when it is being added, removed, or moved around.
The official documentation for AngularJS animations is here:
http://docs.angularjs.org/guide/animations

TLDR: Jank is bad, do animations with transform. Check out this fiddle for css and demo.
Explanation
Note that animating height, max-height, top, ... is really bad performance wise because they cause reflows and thus jank (more information on html5rocks|high-performance-animations).
There is however a method getting this type of animation using only transforms by utilizing the sibling selector.
When elements are added there is one reflow because of the new item, all items below are transformed up so they stay at the same position and then the transformation is removed for a smooth slide-in.
In reverse when elements are removed they are transformed to the new position for a smooth slide-out and when the element is finally removed there is again one reflow and the transform is removed instantly so they stay at their position (this is also why it is important to only have transition set on ng-animate).
Alternatively to the example you could also do a transform: scaleY(0) on the deleted item and only transform: translateY() the siblings.
Caveat
Note that this snippet has trouble when multiple elements are removed in quick succession (before the previous animation has completed).
This can be fixed by having an animation time faster than the time a user takes to delete another item or by doing some more work on the animation (out of scope of this answer).
Finally some code
Note: apparently SO breaks the demo with multiple deletes - check out the fiddle to see it in work.
angular.module('app', ['ngAnimate'])
.controller('testCtrl', ['$scope', function($scope) {
var self = this;
self.items = [];
var i = 65;
for(; i < 72; i++)
{
self.items.push({ value: String.fromCharCode(i) });
}
self.addItem = function()
{
self.items.push({ value: String.fromCharCode(i) });
i++;
}
self.removeItemAt = function(index)
{
self.items.splice(index, 1);
}
}])
li
{
height: 48px;
width: 300px;
border: 1px solid lightgrey;
background-color: white;
position: relative;
list-style: none;
}
li.ng-enter,
li.ng-enter ~ li {
transform: translateY(-100%);
}
li.ng-enter.ng-enter-active,
li.ng-enter.ng-enter-active ~ li {
transform: translateY(0);
}
li.ng-animate {
z-index: -1;
}
li.ng-animate,
li.ng-animate ~ li {
transition: transform 0.6s;
}
li.ng-leave,
li.ng-leave ~ li {
transform: translateY(0);
}
li.ng-leave.ng-leave-active,
li.ng-leave.ng-leave-active ~ li {
transform: translateY(-100%);
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.23/angular-animate.js"></script>
<div ng-app="app" ng-controller="testCtrl as ctrl">
<ul>
<li ng-repeat="item in ctrl.items" ng-bind="item.value">
</li>
</ul>
<button ng-click="ctrl.addItem()">
Add
</button>
<button ng-click="ctrl.removeItemAt(5)">
Remove at 5
</button>
</div>

Related

ngAnimate doesn't animate transitions

I'm writing and app and ngAnimate stopped working at some point. I'm trying to debug it and cut down my app to this very minimal example where there's nothing more happening than animation when pushing a button. Still, I can't find out why it's not working. I'm injecting the ngAnimate as it's supposed to and I also have script-tag to include the source.
var searchApp = angular.module('searchApp', ['ngAnimate'])
http://plnkr.co/edit/ZJIF6BGs0ORr3S6YnJwG?p=preview
Anyone can see what I'm doing wrong? It must be some very obvious thing because the app is now so simple... spent hours trying to see it.
The issue is with your CSS not with Angular.
Here's a working version
Problem one is that .testiDiv .ng-hide is not valid in this situation, it implies that you're expecting .ng-hide to be within .testiDiv rather than on it.
Problem two is that the animation was on the wrong class, it should be related to the animation classes Angular automatically applies on elements using ng-show / ng-hide.
.testiDiv {
background-color: black;
width: 100px;
height: 100px;
opacity: 1;
}
.testiDiv.ng-hide {
opacity: 0;
}
.testiDiv.ng-hide-add, .testiDiv.ng-hide-remove {
-webkit-transition:all 1s linear;
-moz-transition:all 1s linear;
-o-transition:all 1s linear;
transition:all 1s linear;
}

How to get height of a div in Ionic

I'm working to develop a mobile app using Ionic.
A bit of background first as to what I am trying to achieve.
I've got a challenging bit of design I am coding in. I am placing an image of fixed height, in the bottom right hand corner of a div which has a flexible height. The text within the div then needs to wrap around the image.
Like this:
What the end result should be like
The HTML and CSS side of things
I've got the CSS and HTML sussed (at least I think!). The HTML is:
//this line is in the head
<style ng-bind-html="myStyles"></style>
//the rest is in the body of the HTML
<div class="score_block">
<div class="description">
<div class="image_container">
<img src="img/emotional_man.png">
</div>
<p>{{area.levelling.description}}</p>
<div class="habits_button">
<button ng-click="$state.go('app.planner')" class="button button-stable button-icon button-block">Plan habits</button>
</div>
</div>
</div>
And the CSS (written using SASS) is like this:
.score_block {
text-align: center;
position: relative;
.description {
text-align: left;
margin: 10px 0;
}
.image_container {
clear: both;
float: right;
width: 100px;
height: 100px;
img {
height: 100px;
width: 100px;
}
}
}
.score_block:before {
content: "";
float: right;
height: 200px;
width: 0;
}
If I change the height of the 'score_block:before' class I can reposition the image just I need.
The Javascript so far
So with the Javascript side of things I'm hoping that if I can figure out the height of the .description div, I can subtract the height of the image from it and tell the CSS how to position the image. I need some AngularJS to do this - I think that's what I need as JQuery doesn't work in Ionic as far as I know.
So far I have JS code that does this:
.controller('emotionalCtrl', function ($scope, $state, AreasService, _) {
//these commented out lines are to show things I have tried but don't work
//var blockH = $(".description").height();
//var descriptionHeight = angular.element('description');
//var number = descriptionHeight('offsetHeight');
var number = 0;
$scope.myStyles = "#habit_area_homepage .score_block:before { height:" + number + "px; }";
})
I'm looking to do a calculation on the variable number and pass that back in. I can manually change the value of number of it works fine so I know everything else is good. I've read some stuff about doing directives etc but all the examples I've seen confuse me. Maybe I need to put a directive in here or something to help me get the height of the .description element but I just can't figure out to do this. I've spent nearly two days getting this far!
I'm pretty new to AngularJS and Ionic so any help would be really appreciated. Thanks!
There are multiple ways to accomplish dynamic styles.
According to your provided code. I recommend you add styles to head.
Run below codes in your controller or "run":
angular.module("app",[]).run(function(){
var stylesTpl="<style>#habit_area_homepage .score_block:before { height:" + number + "px; } </style>";
angular.element(document).find("head").append(stylesTpl);
})
Check this post for built-in directives of angular to achieve dynamic styles:
How do I conditionally apply CSS styles in AngularJS?
If you want to get the height of a specific div, you have two ways:
Assign an id to the div, and use
var element = document.getElementById("id");
console.log(element.offsetHeight);
Use querySelectors, this returns the first and only one element:
var element = document.querySelector(".description");
console.log(element.offsetHeight);
Using directive is also a good way, check:
Get HTML Element Height without JQuery in AngularJS

How to adjust the height for fadeInUp in ng-animate or Animate.css?

This may be a very simple question for most of you, but I'm having a very difficult time figuring this out.
I have a data table that's big enough to run the entire screen. It's very specific so pagination is out of question. The issue that I'm having is with animation. I have the table inside div with ng-view. I want fadeInUp animation such that the fadeInUp seems to be from around 60-100px from the top. However, the current fadeInUp comes sliding up from the bottom of the screen which I do not want. How can I make it so that it slides up from the desired height?
<div ng-view class="content-wrapper ng-fadeInUp">
[ng-view].ng-enter{
animation:fadeInUp 1s;
}
This is what's making it slide up from the bottom of the screen. I tried using "Animate.css" which did the same thing. I want it so that it seems it's barely sliding up. Just a small hint of animation instead of the full slide up.
Take the original CSS from example animate.css and modify it:
#keyframes fadeInUp {
from {
opacity: 0;
transform: translate3d(0, 100%, 0);
}
to {
opacity: 1;
transform: none;
}
}
.fadeInUp {
animation-name: fadeInUp;
}
Change the second parameter in translate3d to what you want, for example:
transform: translate3d(0, 20px, 0);
Demo: http://plnkr.co/edit/W5zsmhYkwXxYJbuft5pu?p=preview

How to properly translate shadow DOM CSS selectors to non-shadow-DOM selectors

I want to test Polymer applications with non-Shadow-DOM capable browsers like Firefox, PhantomJS, and maybe others using WebDriver.
WebDriver commands for Firefox and PhantomJS fail when I use something like
driver.findElement(const By.cssSelector('* /deep/ #some-div'));
Are there some rules how to best translated/approximate these selectors when the polyfills can not be applied:
/deep/
::shadow
:host()
:host-context()
:content
I would like to create a function that translates such selectors automatically to non-shadow-DOM selectors for browsers that don't support them before sending the request and for that I need to know how to translate them.
Question is a bit old, but in case you haven't figured it out yourselves yet.
/deep/ (deprecated): As you said in your answer, just removing it should work in most of the cases.
::shadow (deprecated): Can also just be removed. Replacing it with > might not work if node which you are targeting is not an immediate child of host element's shadow root.
:host() pseudo classes is used to select custom element from inside shadow-dom, in non-supported browsers it will be equal to selecting parent from child element. Since we don't have parent selectors in css and you are writing js for conversion, you can identify tagName of host element and use it instead of :host selector. Something like below:
:host {
opacity: 0.4;
transition: opacity 420ms ease-in-out;
}
:host(:hover) {
opacity: 1;
}
:host(:active) {
position: relative;
top: 3px;
left: 3px;
}
/*Convert it to*/
x-element {
opacity: 0.4;
transition: opacity 420ms ease-in-out;
}
x-element:hover {
opacity: 1;
}
x-element:active {
position: relative;
top: 3px;
left: 3px;
}
:host-context(<selector>) pseudo class matches the host element if it or any of its ancestors matches <selector>. for example:
Below rule will apply on custom element only when it's a descendant of an element with the class .different.
:host-context(.different) {
color: red;
}
<body class="different">
<x-foo></x-foo>
</body>
It won't be very easy to replace this one with anything simple. Even webcomponents polyfill doesn't attempt it. I can't think of any css only way to achieve this.
::content targets distributed child nodes of host element, i.e. all elements which are picked to display using content selectors. Replacing ::content selectors with tagName of host elements should work here. i.e.
::content > h3 {
color: green;
}
/*replace it with*/
x-element h3 {
color: green;
}
Note that I have removed child selector > also from above, because in non-supported browsers after distribution h3 won't be a direct descendant of x-element anymore. Given the way content selector is used, I'd suggest removing child selector also wherever available.
/deep/ can just be removed
::shadow can be replaced by >
don't know about the others yet

Can I use the !important css keyword with Angularjs ng-style directive?

I'm having an issue with boostrap3 and it's default background: transparent !important; print setting.
I have the need to show a heatmap in my webapp and have these printable.
I'm using an ng-style directive for that to dynamically calculate the needed background-color.
In short, this is the html part
<div class="heatmap" ng-style="scalecolor(10)">lorem ipsum</div>
and this is the controller part
$scope.scalecolor = function(perc) {
return { backgroundColor: plnkUtils.scaleColorInt('#EE0000', '#88FF00', perc) }
};
However, because bootstrap3 sets all backgrounds to transparent with the !important keyword, I can't print these.
This is the part of bootstrap 3.1.0 css causing the issue with missing background-color:
#media print {
* {
color: #000 !important;
text-shadow: none !important;
background: transparent !important;
box-shadow: none !important;
}
}
Since the inverse of background: transparent !important; is background: color hex code !important;
(see here )
I'm trying to set that using the ng-style, but then the exclamantion mark causes Angularjs to flip when I try this:
$scope.scalecolor = function(perc) {
return { backgroundColor: plnkUtils.scaleColorInt('#EE0000', '#88FF00', perc) !important }
};
or alternatively when I try this in the html template
ng-style="scalecolor(10) !important"
Anyone out there that knows how I can use the !important css keyword with the Angularjs ng-style directive to override the bootstrap3 wildcard?
For those who want to see this with their own eyes, here's a plunker showing the issue
Apparently you can't and it's a known issue but there is some workaround that can be found here:
https://github.com/angular/angular.js/issues/5379
The solution below has been copied from the site just in case the link breaks, or get changed.
You're not able to use the !important directive in the DOM style property in either Chrome nor FF (probably others too). The style attribute gets parsed as CSS, but the HTMLElement.style property doesn't. Unfortunately you're not able to set a property's priority here (in Gecko/Blink/Webkit, at least).
So, there are some workarounds here:
Workaround #1
<ANY data-ng-attr-style="{{ blue && 'background-color: blue!important' || '' }}">
</ANY>
This would be your best way to go in terms of browser-support, because the CSS property priority will get parsed correctly here.
Workaround #2
Alternatively, you can also do the following in javascript:
$scope.$watch(conditionalStyle);
function conditionalStyle() {
if ($scope.blue) {
$element[0].style.setProprety('background-color', 'blue', 'important');
} else {
$element[0].style.backgroundColor = '';
}
}
Where $element is a jQuery/jqLite object referencing an HTMLElement.
Note: caveats are not supported in IE < 9 so workaround #1 is probably your best bet for this behavior where you depend on property priority...
Using the tips for #Wawy I updated my code to get it to work.
For anyone landing on this page, for clarification I made an updated Plunker that can be found here
In short, this is now the html part
<div class="heatmap" ng-attr-style="{{ngAttrScaleColor(10)}}">YES, ng-attr-style works !</div>
and this is the controller part
$scope.ngAttrScaleColor = function(perc) {
return 'background-color: ' + plnkUtils.scaleColorInt('#EE0000', '#88FF00', perc) + '!important';
};

Resources