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": [],