Skip to content

Commit 1490cb3

Browse files
committed
feat(Answer:49): rxjs hold to save;
1 parent f6bc32c commit 1490cb3

4 files changed

Lines changed: 145 additions & 5 deletions

File tree

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,65 @@
1-
import { ChangeDetectionStrategy, Component } from '@angular/core';
1+
import {
2+
ChangeDetectionStrategy,
3+
Component,
4+
ElementRef,
5+
viewChild,
6+
} from '@angular/core';
7+
import {
8+
BTN_SEND_TRIGGER_INTERVAL,
9+
PROGRESS_INITIAL_VALUE,
10+
PROGRESS_MAX_VALUE,
11+
PROGRESS_UPDATE_COUNT,
12+
} from '../constants/app.constants';
13+
import { HoldableDirective } from './holdable.directive';
214

315
@Component({
4-
imports: [],
16+
imports: [HoldableDirective],
517
selector: 'app-root',
618
template: `
719
<main class="flex h-screen items-center justify-center">
820
<div
921
class="flex w-full max-w-screen-sm flex-col items-center gap-y-8 p-4">
1022
<button
23+
holdable
24+
[updateInterval]="BTN_SEND_TRIGGER_INTERVAL"
25+
(onInterval)="updateProgress()"
26+
(onRelease)="resetProgress()"
27+
(onComplete)="onSend()"
1128
class="rounded bg-indigo-600 px-4 py-2 font-bold text-white transition-colors ease-in-out hover:bg-indigo-700">
1229
Hold me
1330
</button>
1431
15-
<progress [value]="20" [max]="100"></progress>
32+
<progress
33+
#progress
34+
[value]="PROGRESS_INITIAL_VALUE"
35+
[max]="PROGRESS_MAX_VALUE"></progress>
1636
</div>
1737
</main>
1838
`,
1939
changeDetection: ChangeDetectionStrategy.OnPush,
2040
})
2141
export class AppComponent {
22-
onSend() {
42+
progress = viewChild<string, ElementRef<HTMLProgressElement>>('progress', {
43+
read: ElementRef,
44+
});
45+
46+
readonly PROGRESS_INITIAL_VALUE = PROGRESS_INITIAL_VALUE;
47+
readonly PROGRESS_MAX_VALUE = PROGRESS_MAX_VALUE;
48+
readonly BTN_SEND_TRIGGER_INTERVAL = BTN_SEND_TRIGGER_INTERVAL;
49+
50+
onSend(): void {
2351
console.log('Save it!');
2452
}
53+
54+
resetProgress(): void {
55+
this.progress().nativeElement.value = this.PROGRESS_INITIAL_VALUE;
56+
}
57+
58+
updateProgress(): void {
59+
const progressEl = this.progress().nativeElement;
60+
const currentVal = progressEl.value;
61+
const increment = this.PROGRESS_MAX_VALUE / PROGRESS_UPDATE_COUNT;
62+
63+
progressEl.value = currentVal + increment;
64+
}
2565
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import {
2+
Directive,
3+
DOCUMENT,
4+
ElementRef,
5+
inject,
6+
input,
7+
OnDestroy,
8+
OnInit,
9+
output,
10+
} from '@angular/core';
11+
import {
12+
fromEvent,
13+
last,
14+
merge,
15+
Observable,
16+
Subject,
17+
switchMap,
18+
take,
19+
takeUntil,
20+
tap,
21+
timer,
22+
} from 'rxjs';
23+
import { PROGRESS_UPDATE_COUNT } from '../constants/app.constants';
24+
25+
@Directive({
26+
selector: '[holdable]',
27+
})
28+
export class HoldableDirective implements OnInit, OnDestroy {
29+
updateInterval = input.required<number>();
30+
numberOfUpdates = input<number>(PROGRESS_UPDATE_COUNT);
31+
32+
/** emitted on each `updateInterval` */
33+
onInterval = output();
34+
/** emitted only when `pointerup`, `pointerleave` events are fired. */
35+
onRelease = output();
36+
/** emitted once the `numberOfUpdates` is reached */
37+
onComplete = output();
38+
39+
private _destroy$ = new Subject<void>();
40+
private _document = inject(DOCUMENT);
41+
private _el: ElementRef<HTMLButtonElement> = inject(ElementRef);
42+
43+
ngOnInit(): void {
44+
merge(
45+
fromEvent(this._el.nativeElement, 'pointerdown'),
46+
fromEvent(this._el.nativeElement, 'touchstart'),
47+
)
48+
.pipe(
49+
switchMap(() => {
50+
return this._startTimer$();
51+
}),
52+
takeUntil(this._destroy$),
53+
)
54+
.subscribe({
55+
next: () => {
56+
this.onComplete.emit();
57+
},
58+
});
59+
}
60+
61+
ngOnDestroy(): void {
62+
this._destroy$.next();
63+
this._destroy$.complete();
64+
}
65+
66+
/**
67+
* Updates Progress bar every second
68+
*
69+
* Emits when the `PROGRESS_UPDATE_COUNT` is reached
70+
*
71+
* Completes when the `PROGRESS_UPDATE_COUNT` is reached or either of the `mouseup` or `mouseleave` event is fired. If event is fired before reaching the count, no emission occurs and the stream simply completes.
72+
*/
73+
private _startTimer$(): Observable<number> {
74+
const mouseleave$ = merge(
75+
fromEvent(this._document, 'pointerup'),
76+
fromEvent(this._el.nativeElement, 'pointerleave'),
77+
).pipe(tap(() => this.onRelease.emit()));
78+
79+
return timer(this.updateInterval(), this.updateInterval()).pipe(
80+
tap(() => this.onInterval.emit()),
81+
take(this.numberOfUpdates()),
82+
last(),
83+
takeUntil(mouseleave$),
84+
// Due to `take` above, the stream is completed and takeUntil is unsubscribed
85+
// so `onRelease` needs to be triggered on a different `mouseleave$`
86+
tap(() => {
87+
mouseleave$.pipe(take(1)).subscribe({});
88+
}),
89+
);
90+
}
91+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// all intervals, delay, timeouts in milliseconds
2+
3+
export const PROGRESS_INITIAL_VALUE: number = 0;
4+
export const PROGRESS_MAX_VALUE: number = 100;
5+
/** number of progress updates to reach completion */
6+
export const PROGRESS_UPDATE_COUNT: number = 5;
7+
/** progress update interval */
8+
export const BTN_SEND_TRIGGER_INTERVAL: number = 300;

apps/rxjs/49-hold-to-save-button/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@
55
"esModuleInterop": true,
66
"forceConsistentCasingInFileNames": true,
77
"strict": true,
8+
"strictNullChecks": false,
89
"noImplicitOverride": true,
910
"noPropertyAccessFromIndexSignature": true,
1011
"noImplicitReturns": true,
1112
"noFallthroughCasesInSwitch": true,
1213
"module": "preserve",
1314
"moduleResolution": "bundler",
14-
"lib": ["dom", "es2022"]
15+
"lib": ["dom", "es2022"],
1516
},
1617
"files": [],
1718
"include": [],

0 commit comments

Comments
 (0)