-
Notifications
You must be signed in to change notification settings - Fork 88
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' into feat/migrate-to-self-closing-tag
- Loading branch information
Showing
8 changed files
with
191 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# ngxtension/derive-loading | ||
|
||
Secondary entry point of `ngxtension`. It can be used by importing from `ngxtension/derive-loading`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"lib": { | ||
"entryFile": "src/index.ts" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
{ | ||
"name": "ngxtension/derive-loading", | ||
"$schema": "../../../node_modules/nx/schemas/project-schema.json", | ||
"projectType": "library", | ||
"sourceRoot": "libs/ngxtension/derive-loading/src", | ||
"targets": { | ||
"test": { | ||
"executor": "@nx/jest:jest", | ||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"], | ||
"options": { | ||
"jestConfig": "libs/ngxtension/jest.config.ts", | ||
"testPathPattern": ["derive-loading"], | ||
"passWithNoTests": true | ||
}, | ||
"configurations": { | ||
"ci": { | ||
"ci": true, | ||
"codeCoverage": true | ||
} | ||
} | ||
}, | ||
"lint": { | ||
"executor": "@nx/eslint:lint", | ||
"outputs": ["{options.outputFile}"] | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import { of, timer } from 'rxjs'; | ||
import { TestScheduler } from 'rxjs/testing'; | ||
import { deriveLoading } from './derive-loading'; | ||
|
||
describe(deriveLoading.name, () => { | ||
describe('For sync observable', () => { | ||
it('should return false', (done) => { | ||
const source$ = of(1).pipe(deriveLoading()); | ||
|
||
source$.subscribe((result) => { | ||
expect(result).toBe(false); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('For async observable', () => { | ||
let testScheduler: TestScheduler; | ||
|
||
beforeEach(() => { | ||
testScheduler = new TestScheduler((actual, expected) => { | ||
expect(actual).toEqual(expected); | ||
}); | ||
}); | ||
|
||
it('should emit false when source emits before threshold', () => { | ||
testScheduler.run(({ cold, expectObservable }) => { | ||
const source$ = timer(1).pipe( | ||
deriveLoading({ threshold: 1, loadingTime: 2 }), | ||
); | ||
const expected = 'f--|'; | ||
expectObservable(source$).toBe(expected, { f: false }); | ||
}); | ||
}); | ||
|
||
// Operation takes longer than threshold | ||
it('should emit false-true-false when source emits after threshold', () => { | ||
testScheduler.run(({ cold, expectObservable }) => { | ||
const source$ = timer(2).pipe( | ||
deriveLoading({ threshold: 1, loadingTime: 2 }), | ||
); | ||
const expected = 'ft-(f|)'; | ||
expectObservable(source$).toBe(expected, { f: false, t: true }); | ||
}); | ||
}); | ||
|
||
// Operation takes longer than threshold + loadingTime | ||
it('should emit false-true-false when source emits after threshold + loadingTime', () => { | ||
testScheduler.run(({ cold, expectObservable }) => { | ||
const source$ = timer(4).pipe( | ||
deriveLoading({ threshold: 1, loadingTime: 2 }), | ||
); | ||
const expected = 'ft--(f|)'; | ||
expectObservable(source$).toBe(expected, { f: false, t: true }); | ||
}); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import { | ||
combineLatest, | ||
debounce, | ||
distinctUntilChanged, | ||
map, | ||
merge, | ||
MonoTypeOperatorFunction, | ||
Observable, | ||
OperatorFunction, | ||
ReplaySubject, | ||
share, | ||
startWith, | ||
takeUntil, | ||
timer, | ||
} from 'rxjs'; | ||
|
||
export type DeriveLoadingOptions = { | ||
/** | ||
* The time in milliseconds to wait before emiting the loading flag = true. | ||
*/ | ||
threshold?: number; | ||
/** | ||
* The time in milliseconds to wait before emiting the loading flag = false. | ||
*/ | ||
loadingTime?: number; | ||
}; | ||
|
||
/** | ||
* Derive a loading state from the source observable. | ||
* | ||
* It will emit a loading flag in a "non-flickering" way. This means | ||
* if the async operation finishes before the threshold time, the loading flag will not change | ||
* to "true", it will stay false. | ||
* | ||
* It will only emit "true" when the async operation takes longer than the threshold time. | ||
* It will change back to "false" after at least the defined threshold + loadingTime has passed. | ||
* If the async operation takes longer than threshold + loadingtime, "false" will be emitted after the operation | ||
* has finished. | ||
* | ||
* @param options - The options to configure the loading state derivation. | ||
* @returns A observable that emits the loading flag. | ||
* | ||
* @param options | ||
*/ | ||
export function deriveLoading<T>( | ||
options?: DeriveLoadingOptions, | ||
): OperatorFunction<T, boolean> { | ||
const threshold = options?.threshold ?? 500; | ||
const loadingTime = options?.loadingTime ?? 1000; | ||
|
||
return function <T>(source: Observable<T>): Observable<boolean> { | ||
const result$ = source.pipe( | ||
share({ | ||
connector: () => new ReplaySubject(1), | ||
resetOnComplete: false, | ||
resetOnRefCountZero: true, | ||
resetOnError: true, | ||
}), | ||
); | ||
|
||
return merge( | ||
timer(threshold).pipe( | ||
map(() => true), | ||
takeUntil(result$), | ||
), | ||
combineLatest([result$, timer(threshold + loadingTime)]).pipe( | ||
map(() => false), | ||
), | ||
).pipe(startWith(false), distinctUntilChanged(), handleSyncValue()); | ||
}; | ||
} | ||
|
||
function handleSyncValue<T>(): MonoTypeOperatorFunction<any> { | ||
return (source$: Observable<T>): Observable<T> => { | ||
return new Observable<T>((observer) => { | ||
const isReadySubject = new ReplaySubject<unknown>(1); | ||
|
||
const subscription = source$ | ||
.pipe( | ||
/* Wait for all synchronous processing to be done. */ | ||
debounce(() => isReadySubject), | ||
) | ||
.subscribe(observer); | ||
|
||
/* Sync emitted values have been processed now. | ||
* Mark source as ready and emit last computed state. */ | ||
isReadySubject.next(undefined); | ||
|
||
return () => subscription.unsubscribe(); | ||
}); | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { DeriveLoadingOptions, deriveLoading } from './derive-loading'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters