I have a tiny application that displays a single dot on the screen.
This is a simple div bound to state in NgRx store.
<div class="dot"
[style.width.px]="size$ | async"
[style.height.px]="size$ | async"
[style.backgroundColor]="color$ | async"
[style.left.px]="x$ | async"
[style.top.px]="y$ | async"
(transitionstart)="transitionStart()"
(transitionend)="transitionEnd()"></div>
The dot state changes are animated by CSS transitions.
.dot {
border-radius: 50%;
position: absolute;
$moveTime: 500ms;
$sizeChangeTime: 400ms;
$colorChangeTime: 900ms;
transition:
top $moveTime, left $moveTime,
background-color $colorChangeTime,
width $sizeChangeTime, height $sizeChangeTime;
}
I have a backend which pushes updates for the dot (position, color and size). I map these updates on NgRx actions.
export class AppComponent implements OnInit {
...
constructor(private store: Store<AppState>, private backend: BackendService) {}
ngOnInit(): void {
...
this.backend.update$.subscribe(({ type, value }) => {
// TODO: trigger new NgRx action when all animations ended
if (type === 'position') {
const { x, y } = value;
this.store.dispatch(move({ x, y }));
} else if (type === 'color') {
this.store.dispatch(changeColor({ color: value }));
} else if (type === 'size') {
this.store.dispatch(changeSize({ size: value }));
}
});
}
}
The problem is that new changes from backend sometimes come earlier than animation ends.
My objective is to delay updating the state in store (pause triggering new NgRx actions) until all transitions ended. We can easily handle this moment because chrome already supports the transitionstart
event.
I can also explain this with such a diagram
The spacing depends on the transition duration.
Here is the runnable application https://stackblitz.com/edit/angular-qlpr2g and repo https://github.com/cwayfinder/pausable-ngrx.
Most effects are straightforward: they receive a triggering action, perform a side effect, and return an Observable stream of another action which indicates the result is ready. NgRx effects will then automatically dispatch that action to trigger the reducers and perform a state change.
As you said, dispatch is asynchronous. What you should do is use @ngrx/effects. It's nearly the same as using addAuthorAction except that instead of calling a function, you "catch" the dispatched actions and do something just after they've been applied by the reducers.
It uses NgRx for state management. Dispatching of the UpdateDashboardConfiguration() action will result in the application sending a http request to the server to save the dashboard's configurations, therefore it is asynchronous too.
The props method is used to define any additional metadata needed for the handling of the action. Action creators provide a consistent, type-safe way to construct an action that is being dispatched. Creating actions for liking and disliking a photo could look like this: // src/app/store/photo.
You can use concatMap and delayWhen to do this. Also notice that transitionEnd
event can be fired multiple times if multiple properties were changed, so I use debounceTime to filter such double events. We cannot use distinctUntilChanged
instead, because the first transitionEnd
will trigger next update which immediately changes transitionInProgress$ state to true. I don't use transitionStart
callback, because multiple updates can come before the transitionStart will be triggered. Here is the working example.
export class AppComponent implements OnInit {
...
private readonly transitionInProgress$ = new BehaviorSubject(false);
ngOnInit(): void {
...
this.backend.update$.pipe(
concatMap(update => of(update).pipe(
delayWhen(() => this.transitionInProgress$.pipe(
// debounce the transition state, because transitionEnd event fires multiple
// times for a single transiation, if multiple properties were changed
debounceTime(1),
filter(inProgress => !inProgress)
))
))
).subscribe(update => {
this.transitionInProgress$.next(true)
if (update.type === 'position') {
this.store.dispatch(move(update.value));
} else if (update.type === 'color') {
this.store.dispatch(changeColor({ color: update.value }));
} else if (update.type === 'size') {
this.store.dispatch(changeSize({ size: update.value }));
}
});
}
transitionEnd(event: TransitionEvent) {
this.transitionInProgress$.next(false)
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With