From 1490cb3467eb0d40b24795deae7f4a80fefaf0ee Mon Sep 17 00:00:00 2001 From: SUSHANT KAKROO <37952551+sushant-47@users.noreply.github.com> Date: Wed, 24 Dec 2025 12:11:51 +0000 Subject: [PATCH] feat(Answer:49): rxjs hold to save; --- .../src/app/app.component.ts | 48 +++++++++- .../src/app/holdable.directive.ts | 91 +++++++++++++++++++ .../src/constants/app.constants.ts | 8 ++ .../rxjs/49-hold-to-save-button/tsconfig.json | 3 +- 4 files changed, 145 insertions(+), 5 deletions(-) create mode 100644 apps/rxjs/49-hold-to-save-button/src/app/holdable.directive.ts create mode 100644 apps/rxjs/49-hold-to-save-button/src/constants/app.constants.ts diff --git a/apps/rxjs/49-hold-to-save-button/src/app/app.component.ts b/apps/rxjs/49-hold-to-save-button/src/app/app.component.ts index 8f0dbbc70..f7c11e6f9 100644 --- a/apps/rxjs/49-hold-to-save-button/src/app/app.component.ts +++ b/apps/rxjs/49-hold-to-save-button/src/app/app.component.ts @@ -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: `
- +
`, changeDetection: ChangeDetectionStrategy.OnPush, }) export class AppComponent { - onSend() { + progress = viewChild>('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; + } } diff --git a/apps/rxjs/49-hold-to-save-button/src/app/holdable.directive.ts b/apps/rxjs/49-hold-to-save-button/src/app/holdable.directive.ts new file mode 100644 index 000000000..3cb673f3d --- /dev/null +++ b/apps/rxjs/49-hold-to-save-button/src/app/holdable.directive.ts @@ -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(); + numberOfUpdates = input(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(); + private _document = inject(DOCUMENT); + private _el: ElementRef = 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 { + 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({}); + }), + ); + } +} diff --git a/apps/rxjs/49-hold-to-save-button/src/constants/app.constants.ts b/apps/rxjs/49-hold-to-save-button/src/constants/app.constants.ts new file mode 100644 index 000000000..b38b105ef --- /dev/null +++ b/apps/rxjs/49-hold-to-save-button/src/constants/app.constants.ts @@ -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; diff --git a/apps/rxjs/49-hold-to-save-button/tsconfig.json b/apps/rxjs/49-hold-to-save-button/tsconfig.json index ad0529440..100f12697 100644 --- a/apps/rxjs/49-hold-to-save-button/tsconfig.json +++ b/apps/rxjs/49-hold-to-save-button/tsconfig.json @@ -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": [],