Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 44 additions & 4 deletions apps/rxjs/49-hold-to-save-button/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,65 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import {
ChangeDetectionStrategy,
Component,
ElementRef,
viewChild,
} from '@angular/core';
import {
BTN_SEND_TRIGGER_INTERVAL,
PROGRESS_INITIAL_VALUE,
PROGRESS_MAX_VALUE,
PROGRESS_UPDATE_COUNT,
} from '../constants/app.constants';
import { HoldableDirective } from './holdable.directive';

@Component({
imports: [],
imports: [HoldableDirective],
selector: 'app-root',
template: `
<main class="flex h-screen items-center justify-center">
<div
class="flex w-full max-w-screen-sm flex-col items-center gap-y-8 p-4">
<button
holdable
[updateInterval]="BTN_SEND_TRIGGER_INTERVAL"
(onInterval)="updateProgress()"
(onRelease)="resetProgress()"
(onComplete)="onSend()"
class="rounded bg-indigo-600 px-4 py-2 font-bold text-white transition-colors ease-in-out hover:bg-indigo-700">
Hold me
</button>

<progress [value]="20" [max]="100"></progress>
<progress
#progress
[value]="PROGRESS_INITIAL_VALUE"
[max]="PROGRESS_MAX_VALUE"></progress>
</div>
</main>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent {
onSend() {
progress = viewChild<string, ElementRef<HTMLProgressElement>>('progress', {
read: ElementRef,
});

readonly PROGRESS_INITIAL_VALUE = PROGRESS_INITIAL_VALUE;
readonly PROGRESS_MAX_VALUE = PROGRESS_MAX_VALUE;
readonly BTN_SEND_TRIGGER_INTERVAL = BTN_SEND_TRIGGER_INTERVAL;

onSend(): void {
console.log('Save it!');
}

resetProgress(): void {
this.progress().nativeElement.value = this.PROGRESS_INITIAL_VALUE;
}

updateProgress(): void {
const progressEl = this.progress().nativeElement;
const currentVal = progressEl.value;
const increment = this.PROGRESS_MAX_VALUE / PROGRESS_UPDATE_COUNT;

progressEl.value = currentVal + increment;
}
}
91 changes: 91 additions & 0 deletions apps/rxjs/49-hold-to-save-button/src/app/holdable.directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import {
Directive,
DOCUMENT,
ElementRef,
inject,
input,
OnDestroy,
OnInit,
output,
} from '@angular/core';
import {
fromEvent,
last,
merge,
Observable,
Subject,
switchMap,
take,
takeUntil,
tap,
timer,
} from 'rxjs';
import { PROGRESS_UPDATE_COUNT } from '../constants/app.constants';

@Directive({
selector: '[holdable]',
})
export class HoldableDirective implements OnInit, OnDestroy {
updateInterval = input.required<number>();
numberOfUpdates = input<number>(PROGRESS_UPDATE_COUNT);

/** emitted on each `updateInterval` */
onInterval = output();
/** emitted only when `pointerup`, `pointerleave` events are fired. */
onRelease = output();
/** emitted once the `numberOfUpdates` is reached */
onComplete = output();

private _destroy$ = new Subject<void>();
private _document = inject(DOCUMENT);
private _el: ElementRef<HTMLButtonElement> = inject(ElementRef);

ngOnInit(): void {
merge(
fromEvent(this._el.nativeElement, 'pointerdown'),
fromEvent(this._el.nativeElement, 'touchstart'),
)
.pipe(
switchMap(() => {
return this._startTimer$();
}),
takeUntil(this._destroy$),
)
.subscribe({
next: () => {
this.onComplete.emit();
},
});
}

ngOnDestroy(): void {
this._destroy$.next();
this._destroy$.complete();
}

/**
* Updates Progress bar every second
*
* Emits when the `PROGRESS_UPDATE_COUNT` is reached
*
* 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.
*/
private _startTimer$(): Observable<number> {
const mouseleave$ = merge(
fromEvent(this._document, 'pointerup'),
fromEvent(this._el.nativeElement, 'pointerleave'),
).pipe(tap(() => this.onRelease.emit()));

return timer(this.updateInterval(), this.updateInterval()).pipe(
tap(() => this.onInterval.emit()),
take(this.numberOfUpdates()),
last(),
takeUntil(mouseleave$),
// Due to `take` above, the stream is completed and takeUntil is unsubscribed
// so `onRelease` needs to be triggered on a different `mouseleave$`
tap(() => {
mouseleave$.pipe(take(1)).subscribe({});
}),
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// all intervals, delay, timeouts in milliseconds

export const PROGRESS_INITIAL_VALUE: number = 0;
export const PROGRESS_MAX_VALUE: number = 100;
/** number of progress updates to reach completion */
export const PROGRESS_UPDATE_COUNT: number = 5;
/** progress update interval */
export const BTN_SEND_TRIGGER_INTERVAL: number = 300;
3 changes: 2 additions & 1 deletion apps/rxjs/49-hold-to-save-button/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"strictNullChecks": false,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"module": "preserve",
"moduleResolution": "bundler",
"lib": ["dom", "es2022"]
"lib": ["dom", "es2022"],
},
"files": [],
"include": [],
Expand Down
Loading