I am trying to implement a countdown timer using angular as implemented in this
The problem is that the animations are not being applied on change of values, what I am I missing?
html
<div>
<div class="flipclock" *ngIf="timer$ | async as timer">
<div id="container" class="flipclock">
<ul class="flip " *ngFor="let time of timer">
<li
*ngFor="let item of time.split(''); let i = index"
[class.d1]="i === 1"
[class.d2]="i === 0"
>
<section class="ready">
<div class="up">
<div class="shadow"></div>
<div class="inn">{{ item }}</div>
</div>
<div class="down">
<div class="shadow"></div>
<div class="inn">{{ item }}</div>
</div>
</section>
<section class="active">
<div class="up">
<div class="shadow"></div>
<div class="inn">{{ item }}</div>
</div>
<div class="down">
<div class="shadow"></div>
<div class="inn">{{ item }}</div>
</div>
</section>
</li>
</ul>
</div>
</div>
</div>
export class AppComponent {
name = 'Angular ' + VERSION.major;
initialMinutes$ = new BehaviorSubject(30);
expired$ = new Subject();
@Input()
set minutes(val) {
this.initialMinutes$.next(val);
}
timer$ = this.initialMinutes$.pipe(
switchMap(minutes => timer(0, 1000).pipe(
map(t => minutes * 60 - t),
tap(seconds => {
if (seconds < 0) {
this.expired$.next();
}
}),
takeUntil(this.expired$),
map(seconds => ({
hr: Math.max(Math.floor(seconds / 3600), 0),
min: Math.max(Math.floor((seconds % 3600) / 60), 0),
s: (seconds % 60)
})),
map(({hr, min, s}) => ([
hr > 9 ? hr.toString() : '0' + hr.toString(),
min > 9 ? min.toString() : '0' + min.toString(),
s > 9 ? s.toString() : '0' + s.toString(),
]))
))
);
}
css
.flipclock {
}
.flipclock hr {
position: absolute;
left: 0;
top: 65px;
width: 100%;
height: 3px;
border: 0;
background: #000;
z-index: 10;
opacity: 0;
}
ul.flip {
position: relative;
float: left;
margin: 10px;
padding: 0;
width: 90px;
height: 60px;
font-size: 60px;
font-weight: 400;
line-height: 60px;
}
ul.flip li {
float: left;
margin: 0;
padding: 0;
width: 49%;
height: 100%;
-webkit-perspective: 200px;
list-style: none;
}
ul.flip li.d1 {
float: right;
}
ul.flip li section {
z-index: 1;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
ul.flip li section:first-child {
z-index: 2;
}
ul.flip li div {
z-index: 1;
position: absolute;
left: 0;
width: 100%;
height: 49%;
overflow: hidden;
}
ul.flip li div .shadow {
display: block;
position: absolute;
width: 100%;
height: 100%;
z-index: 2;
}
ul.flip li div.up {
-webkit-transform-origin: 50% 100%;
top: 0;
}
ul.flip li div.down {
-webkit-transform-origin: 50% 0;
bottom: 0;
}
ul.flip li div div.inn {
position: absolute;
left: 0;
z-index: 1;
width: 100%;
height: 200%;
color: #fff;
text-shadow: 0 0 2px #fff;
text-align: center;
background-color: #000;
border-radius: 6px;
}
ul.flip li div.up div.inn {
top: 0;
}
ul.flip li div.down div.inn {
bottom: 0;
}
/*--------------------------------------
PLAY
--------------------------------------*/
.play ul section.ready {
z-index: 3;
}
.play ul section.active {
-webkit-animation: index .5s .5s linear both;
z-index: 2;
}
@-webkit-keyframes index {
0% {
z-index: 2;
}
5% {
z-index: 4;
}
100% {
z-index: 4;
}
}
.play ul section.active .down {
z-index: 2;
-webkit-animation: flipdown .5s .5s linear both;
}
@-webkit-keyframes flipdown {
0% {
-webkit-transform: rotateX(90deg);
}
80% {
-webkit-transform: rotateX(5deg);
}
90% {
-webkit-transform: rotateX(15deg);
}
100% {
-webkit-transform: rotateX(0deg);
}
}
.play ul section.ready .up {
z-index: 2;
-webkit-animation: flipup .5s linear both;
}
@-webkit-keyframes flipup {
0% {
-webkit-transform: rotateX(0deg);
}
90% {
-webkit-transform: rotateX(0deg);
}
100% {
-webkit-transform: rotateX(-90deg);
}
}
/*--------------------------------------
SHADOW
--------------------------------------*/
.play ul section.ready .up .shadow {
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, rgba(0, 0, 0, .1)), color-stop(100%, rgba(0, 0, 0, 1)));
background: linear-gradient(to bottom, rgba(0, 0, 0, .1) 0%, rgba(0, 0, 0, 1) 100%);
-webkit-animation: show .5s linear both;
}
.play ul section.active .up .shadow {
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, rgba(0, 0, 0, .1)), color-stop(100%, rgba(0, 0, 0, 1)));
background: linear-gradient(to bottom, rgba(0, 0, 0, .1) 0%, rgba(0, 0, 0, 1) 100%);
-webkit-animation: hide .5s .3s linear both;
}
/*DOWN*/
.play ul section.ready .down .shadow {
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, rgba(0, 0, 0, 1)), color-stop(100%, rgba(0, 0, 0, .1)));
background: linear-gradient(to bottom, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, .1) 100%);
-webkit-animation: show .5s linear both;
}
.play ul section.active .down .shadow {
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, rgba(0, 0, 0, 1)), color-stop(100%, rgba(0, 0, 0, .1)));
background: linear-gradient(to bottom, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, .1) 100%);
-webkit-animation: hide .5s .3s linear both;
}
@-webkit-keyframes show {
0% {
opacity: 0;
}
90% {
opacity: .10;
}
100% {
opacity: 1;
}
}
@-webkit-keyframes hide {
0% {
opacity: 1;
}
80% {
opacity: .20;
}
100% {
opacity: 0;
}
}
Edit 1
I have managed to get animations to reflect but it is now reflecting on all the items
We can do it using angular animations. One aproach (it's diferent than your css) is make a "flip-vertical" from border botton. In one face you has a number, and in the other face another number. Well, really we need that ot has the full number else half of the number.
Imagine you has some like
<div class="content">
<div class="flip">
<div class="up">
<div>{{oldvalue}}</div>
</div>
<div class="down">
<div>
<div>{{oldvalue}}</div>
</div>
</div>
</div>
</div>
the .css is like
.content {
font-family: "Droid Sans Mono", monospace;
height: 60px;
display:inline-block;
margin-left:10px;
}
.flip {
position: relative;
height: 60px;
width: 45px;
}
.up,
.down {
text-align: center;
height: 30px;
overflow: hidden;
}
.up > div,
.down > div {
font-size: 50px;
font-weight: 800;
line-height: 60px;
align-self: center;
}
.down > div > div {
margin-top: -30px;
}
With this you has a number in two halfs
We can then use a clasic flip vertical
<div class="content">
<div class="flip-card">
<div class="flip-card-inner" [@flip]="value">
<div class="flip-card-front">
<div class="up">
<div>{{oldvalue}}</div>
</div>
</div>
<div class="flip-card-back">
<div class="down">
<div>
<div>{{newvalue}}</div>
</div>
</div>
</div>
</div>
</div>
</div>
the .css
.flip-card {
perspective: 300px;
position: relative;
height: 30px;
width: 45px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
}
.flip-card-inner {
width: 100%;
height: 100%;
text-align: center;
transform-style: preserve-3d;
-ms-transform-origin: 50% 100%; /* IE 9 */
transform-origin: 50% 100%; /* IE 9 */
}
.flip-card-front,
.flip-card-back {
position: absolute;
width: 100%;
height: 100%;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
overflow: hidden;
}
.flip-card-back {
transform: rotateX(180deg);
}
The animation is really simple
animations:[
trigger("flip",[
transition('*=>*',[
animate(".6s",keyframes([
style({transform:"rotateX(0deg)",offset: 0}),
style({transform:"rotateX(-90deg)",offset: .5}),
style({transform:"rotateX(-180deg)",offset: 1}),
]))
])
])]
Well, the full digit it's now the two before divs
<div class="content">
<div style="position:absolute">
<--here the digit-->
</div>
<div style="position:absolute">
<---here the flip-card--->
</div>
</div>
You can see the first stackblitz, see how when you click the button, it's animate the digit
Well, how use all this. As Ac_mmi say you need has the old and the new value. You can use your code adding a map to return an array of six numbers, a pipe pairwise to get the old and the value and a map to return an array of object with the two properties:
map(val => val.map(i => i.split("")).reduce((a, b) => [...a, ...b], [])),
pairwise(),
map(([old,value])=>{
return value.map((x,index)=>({value:x,old:old[index]}))
})
Then we make a simple loop over [0,1,2,3,4,5] to get the numbers
<ng-container *ngIf="timer$ |async as timer">
<div class="content" *ngFor="let i of [0,1,2,3,4,5]">
....
</ng-container>
Update
Using the class and animation provided by @Ac_mmi, you defined an animation like
animations:[
trigger("flip",[
transition('*=>*',[
animate(".6s",keyframes([
style({transform:"rotateX(130deg)",offset: 0}),
style({transform:"rotateX(0deg)",offset: .45}),
style({transform:"rotateX(7deg)",offset: .50}),
style({transform:"rotateX(0deg)",offset: .53}),
style({transform:"rotateX(5deg)",offset: .56}),
style({transform:"rotateX(0deg)",offset: .60}),
style({transform:"rotateX(0deg)",offset: .95}),
style({transform:"rotateX(0deg)",offset: 1}),
]))
])
])]
and the elements that are animated becomes like, e.g. for the "hours"
<span class="hours" [@flip]="timer[0].value">
{{timer[0].value}}
</span>
You has another stackblitz
Update2 to get hours, minutes and secons, we can use formatDate -the function use Angular for DatePipe- and the timer becomes more easy:
timer$ = this.initialMinutes$.pipe(
map(minutes => minutes * 60000 + new Date().getTime()),
switchMap(minutes =>
timer(0, 500).pipe(
map(t => formatDate((minutes - new Date().getTime()),"HHmmss","en-US","+0000").split('')),
takeUntil(this.expired$),
pairwise(),
map(([old,value])=>{
return value.map((x,index)=>({value:x,old:old[index]}))
})
)
)
);