画面に単一のドットを表示する小さなアプリケーションがあります。
これは、NgRxストアの状態にバインドされた単純なdivです。
<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>
ドット状態の変化は、CSSトランジションによってアニメーション化されます。
.dot {
border-radius: 50%;
position: absolute;
$moveTime: 500ms;
$sizeChangeTime: 400ms;
$colorChangeTime: 900ms;
transition:
top $moveTime, left $moveTime,
background-color $colorChangeTime,
width $sizeChangeTime, height $sizeChangeTime;
}
ドット(位置、色、サイズ)の更新をプッシュするバックエンドがあります。これらの更新をNgRxアクションにマッピングします。
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 }));
}
});
}
}
問題は、バックエンドからの新しい変更がアニメーションの終了よりも前に来ることがあるということです。私の目的は、ストア内の状態の更新(新しいNgRxアクションのトリガーを一時停止)をすべての遷移が終了するまで遅らせることです。 chromeはすでにtransitionstart
イベントをサポートしているため、この瞬間を簡単に処理できます。
間隔は遷移時間に依存します。
これが実行可能なアプリケーション https://stackblitz.com/edit/angular-qlpr2g およびrepo https://github.com/cwayfinder/pausable-ngrx です。
これを行うには、 concatMap および delayWhen を使用できます。また、transitionEnd
event 複数回発生する可能性があります 複数のプロパティが変更された場合は、 debounceTime を使用して、このような二重のイベントをフィルタリングします。最初のdistinctUntilChanged
は次の更新をトリガーし、transitionInProgress $状態をすぐにtrueに変更するため、代わりにtransitionEnd
を使用できません。 transitionStartがトリガーされる前に複数の更新が行われる可能性があるため、transitionStart
コールバックは使用しません。 ここにあります 実例。
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)
}
}
StackBlitzのデモを実際の例を示すように変更しました。 here をご覧ください。
説明については、StackBlitzから重要なコードをコピーして、重要な詳細を説明しました。
_const delaySub = new BehaviorSubject<number>(0);
const delay$ = delaySub.asObservable().pipe(
concatMap(time => timer(time + 50)),
share(),
)
const src$ = this.backend.update$
.pipe(
tap(item => item['type'] === 'position' && delaySub.next(3000)),
tap(item => item['type'] === 'size' && delaySub.next(2000)),
tap(item => item['type'] === 'color' && delaySub.next(1000)),
)
Zip(src$, delay$).pipe(
map(([item, delay]) => item)
).subscribe(({ type, value }) => {
// TODO: trigger new NgRx action when all animations ended
if (type === 'position') {
this.store.dispatch(move(value));
} else if (type === 'color') {
this.store.dispatch(changeColor({ color: value }));
} else if (type === 'size') {
this.store.dispatch(changeSize({ size: value }));
}
})
_
_this.backend.update$
_からイベントが到着すると、イベントタイプに従って遅延サブジェクトを更新します。ミリ秒単位で継続時間の数値を出力します。これにより、後で他のイベントを遅延させて、追加の注意のために時間+ 50を延ばすことができます。
Zip(src$, delay$)
は、遅延なしでsrc $から最初のイベントを発行しますが、_src$
_からの発行は、アイテムタイプに基づいて_delay$
_の新しい値を発生させます。たとえば、最初のイベントがposition
の場合、delaySubは値3000を取得し、次のイベントが_src$
_に到達すると、この新しい値と最新の遅延3000をconcatMap(time => timer(time + 50)),
。最後に、意図した動作を取得します。最初のアイテムは遅延なく到着し、後続のイベントは、Zip
、concatMap
の助けを借りて、前のイベントに基づいて特定の時間待機する必要がありますおよびその他の演算子。
私のコードについて質問がある場合は、私の回答を更新しましょう。
私は多かれ少なかれ良い解決策を持っていると思います。チェック https://stackblitz.com/edit/angular-xh7ndi
NgRxクラスActionSubject
をオーバーライドしました
import { Injectable } from '@angular/core';
import { Action, ActionsSubject } from '@ngrx/store';
import { BehaviorSubject, defer, from, merge, Observable, Subject } from 'rxjs';
import { bufferToggle, distinctUntilChanged, filter, map, mergeMap, share, tap, windowToggle } from 'rxjs/operators';
@Injectable()
export class PausableActionsSubject extends ActionsSubject {
queue$ = new Subject<Action>();
active$ = new BehaviorSubject<boolean>(true);
constructor() {
super();
const active$ = this.active$.pipe(distinctUntilChanged());
active$.subscribe(active => {
if (!active) {
console.time('pauseTime');
} else {
console.timeEnd('pauseTime');
}
});
const on$ = active$.pipe(filter(v => v));
const off$ = active$.pipe(filter(v => !v));
this.queue$.pipe(
share(),
pause(on$, off$, v => this.active$.value)
).subscribe(action => {
console.log('action', action);
super.next(action);
});
}
next(action: Action): void {
this.queue$.next(action);
}
pause(): void {
this.active$.next(false);
}
resume(): void {
this.active$.next(true);
}
}
export function pause<T>(on$: Observable<any>, off$: Observable<any>, haltCondition: (value: T) => boolean) {
return (source: Observable<T>) => defer(() => { // defer is used so that each subscription gets its own buffer
let buffer: T[] = [];
return merge(
source.pipe(
bufferToggle(off$, () => on$),
// append values to your custom buffer
tap(values => buffer = buffer.concat(values)),
// find the index of the first element that matches the halt condition
map(() => buffer.findIndex(haltCondition)),
// get all values from your custom buffer until a haltCondition is met
map(haltIndex => buffer.splice(0, haltIndex === -1 ? buffer.length : haltIndex + 1)),
// spread the buffer
mergeMap(toEmit => from(toEmit)),
),
source.pipe(
windowToggle(on$, () => off$),
mergeMap(x => x),
),
);
});
}
AppModule
でプロバイダーを指定しました
providers: [
PausableActionsSubject,
{ provide: ActionsSubject, useExisting: PausableActionsSubject }
]
デバッグの目的で、CSS移行時間を増やしました
.dot {
border-radius: 50%;
position: absolute;
$moveTime: 3000ms;
$sizeChangeTime: 2000ms;
$colorChangeTime: 1000ms;
transition:
top $moveTime, left $moveTime,
background-color $colorChangeTime,
width $sizeChangeTime, height $sizeChangeTime;
}
ブラウザコンソールでこれを確認します
実際には、@ goga-koreliが作成したものと同様のZip
を使用したかなり単純な解決策があると思います。
基本的にZip
は、すべてのソースがn番目のアイテムを放出する場合にのみn番目のアイテムを放出します。そのため、できるだけ多くのバックエンド更新をプッシュし、アニメーションの終了イベントでのみn番目の値を出力する別の監視可能オブジェクト(この場合はサブジェクト)を保持できます。言い換えると、バックエンドが更新を送信するのが速すぎる場合でも、Zip
はアニメーションが完了するのと同じ速さでアクションをディスパッチします。
private animationEnd$ = new Subject();
...
Zip(
this.backend.update$,
this.animationEnd$.pipe(startWith(null)), // `startWith` to trigger the first animation.
)
.pipe(
map(([action]) => action),
)
.subscribe(({ type, value }) => {
...
});
...
transitionEnd() {
this.animationEnd$.next();
}
更新されたデモ: https://stackblitz.com/edit/angular-42alkp?file=src/app/app.component.ts