Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pause dispatching NgRx actions while animation is in progress

I have a tiny application that displays a single dot on the screen. application screenshot

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 spacing

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.

like image 353
Taras Hupalo Avatar asked Jul 14 '19 11:07

Taras Hupalo


People also ask

Can I dispatch an action in effect 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.

Is NgRx dispatch async?

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.

Is NgRx asynchronous?

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.

What is props in NgRx actions?

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.


1 Answers

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)
  }
}
like image 167
Valeriy Katkov Avatar answered Sep 20 '22 05:09

Valeriy Katkov