Skip to content

Commit

Permalink
feat(rx-stateful): provide possibility to configure rxStateful$ globally
Browse files Browse the repository at this point in the history
BREAKING CHANGE: bump peer dependencies to ng v14. as rxStateful$ now internally uses the inject-function
  • Loading branch information
michaelbe812 committed Aug 22, 2023
1 parent 98fe47a commit aba73ac
Show file tree
Hide file tree
Showing 7 changed files with 268 additions and 74 deletions.
46 changes: 45 additions & 1 deletion libs/rx-stateful/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
`rxStateful$` is a powerful RxJs operator that wraps any sync or async Observable and provides a
stateful stream.

Hint: You can use it on both sync and async Observables. However the real benefits you will get for async Observables.

## Installation
```bash

Expand Down Expand Up @@ -59,6 +61,48 @@ const stateful$ = rxStateful$(from(fetch('...')), {keepValueOnRefresh: true, ref
- `hasValue$` - boolean if a value is present
- `context$` - the context of the stream ('suspense', 'next', 'error', 'complete')
- `hasError$` - boolean if an error is present
- `error$` - the error if present
- `error$` - the error, if present

## Configuration
`rxStateful$` provides two configuration possibilities:

### Global configuration

Use `provideRxStatefulConfig` to provide a global configuration for all `rxStateful$` instances.

For a standalone application:
```typescript
import { provideRxStatefulConfig } from '@angular-kit/rx-stateful';

// app.component.ts
@Component({...})
export class AppComponent{}

// main.ts
bootstrapApplication(AppComponent, {
providers: [provideRxStatefulConfig({ keepValueOnRefresh: true })]
});

```
For a ngModule based application:
```typescript
import { provideRxStatefulConfig } from '@angular-kit/rx-stateful';

// main.ts
platformBrowserDynamic()
.bootstrapModule(AppModule, {
providers: [provideRxStatefulConfig({ keepValueOnRefresh: true })]
})
.catch((err) => console.error(err));

```

### Configuration on instance level

You can also provide a configuration on instance level. This will also override the global configuration (if present).

```typescript
import { rxStateful$ } from '@angular-kit/rx-stateful';

const rxStateful$ = rxStateful$(someSource$, { keepValueOnRefresh: true });
```
4 changes: 2 additions & 2 deletions libs/rx-stateful/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
"access": "public"
},
"peerDependencies": {
"@angular/common": ">=12.2.0",
"@angular/core": ">=12.2.0",
"@angular/common": ">=14.0.0",
"@angular/core": ">=14.0.0",
"rxjs": ">=7.0.0"
},
"dependencies": {
Expand Down
1 change: 1 addition & 0 deletions libs/rx-stateful/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export {
RxStatefulConfig,
} from './lib/types/types';
export { RxStatefulAccumulationFn } from './lib/types/accumulation-fn';
export { provideRxStatefulConfig } from './lib/config/provide-config';
44 changes: 44 additions & 0 deletions libs/rx-stateful/src/lib/config/provide-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {
assertInInjectionContext,
EnvironmentProviders,
inject,
InjectionToken,
makeEnvironmentProviders
} from "@angular/core";
import {RxStatefulConfig} from "../types/types";

type Config<T, E> = Pick<RxStatefulConfig<T, E>, 'keepValueOnRefresh' | 'keepErrorOnRefresh' | 'accumulationFn' | 'errorMappingFn'>

/**
* @internal
*/
const RX_STATEFUL_CONFIG = <T, E>() => InjectionToken <Config<T, E> >

/**
* @publicApi
*
* Global configuration for {@link rxStateful$}.
*
* Provide this configuration in your environment providers.
*
* @param config
*/
export function provideRxStatefulConfig<T, E>(config: Config<T, E>): EnvironmentProviders {
return makeEnvironmentProviders([
{
provide: RX_STATEFUL_CONFIG(),
useValue: config,
},
]);
};

/**
* @internal
*
*/
export function injectConfig<T, E>() {
assertInInjectionContext(injectConfig);

return inject(RX_STATEFUL_CONFIG<T, E>(), {optional: true})
}

101 changes: 79 additions & 22 deletions libs/rx-stateful/src/lib/rx-stateful$.spec.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
import {mergeAll, Observable, Subject, throwError} from 'rxjs';
import {subscribeSpyTo} from '@hirez_io/observer-spy';
import {rxStateful$} from './rx-stateful$';
import {TestBed} from "@angular/core/testing";
import {provideRxStatefulConfig} from "./config/provide-config";

const test = (description :string, testFn: () => void, testBed?: TestBed) => {
it(description, () => {
(testBed ?? TestBed).runInInjectionContext(() => {
testFn();
})
});
}




describe('rxStateful$', () => {
describe('without refreshTrigger$', () => {
describe('value$', () => {
it('should be lazy', () => {
test('should be lazy', () => {
const source$ = new Subject<number>();
const result = subscribeSpyTo(rxStateful$<number>(source$).value$);

expect(result.getValues().length).toEqual(0);
});
it('value$ should return the current value ', () => {
test('value$ should return the current value ', () => {
const source$ = new Subject<number | null>();

const result = subscribeSpyTo(rxStateful$<number | null>(source$).value$);
Expand All @@ -24,13 +37,13 @@ describe('rxStateful$', () => {
});

describe('hasValue$', () => {
it('should return false initially', () => {
test('should return false initially', () => {
const source$ = new Subject<number>();
const result = subscribeSpyTo(rxStateful$<number>(source$).hasValue$);

expect(result.getValues()).toEqual([false]);
});
it('should return true if there is a value', () => {
test('should return true if there is a value', () => {
const source$ = new Subject<number>();
const result = subscribeSpyTo(rxStateful$<number>(source$).hasValue$);

Expand All @@ -40,7 +53,7 @@ describe('rxStateful$', () => {
});
});
describe('isSuspense$', () => {
it('should return true and false', () => {
test('should return true and false', () => {
const source$ = new Subject<number>();
const result = subscribeSpyTo(rxStateful$<number>(source$).isSuspense$);

Expand All @@ -50,13 +63,13 @@ describe('rxStateful$', () => {
});
});
describe('hasError$', () => {
it('should return false initially', () => {
test('should return false initially', () => {
const source$ = new Subject<number>();
const result = subscribeSpyTo(rxStateful$<number>(source$).hasError$);

expect(result.getValues()).toEqual([false]);
});
it('should return true if there is a error', () => {
test('should return true if there is a error', () => {
const source$ = new Subject<Observable<any>>();
const result = subscribeSpyTo(rxStateful$<number>(source$.pipe(mergeAll())).hasError$);

Expand All @@ -66,13 +79,13 @@ describe('rxStateful$', () => {
});
});
describe('error$', () => {
it('should not emit when there is no error', () => {
test('should not emit when there is no error', () => {
const source$ = new Subject<number>();
const result = subscribeSpyTo(rxStateful$<number>(source$).error$);

expect(result.getLastValue()).toEqual(undefined);
});
it('should return the error if there is a error', () => {
test('should return the error if there is a error', () => {
const source$ = new Subject<Observable<any>>();
const result = subscribeSpyTo(rxStateful$<number>(source$.pipe(mergeAll())).error$);

Expand All @@ -82,7 +95,7 @@ describe('rxStateful$', () => {
});
});
describe('context$', () => {
it('should return the correct context', () => {
test('should return the correct context', () => {
const source$ = new Subject<number>();
const result = subscribeSpyTo(rxStateful$<number>(source$).context$);

Expand All @@ -94,7 +107,7 @@ describe('rxStateful$', () => {
});
describe('with refreshTrigger$', () => {
describe('value$', () => {
it('should be lazy', () => {
test('should be lazy', () => {
const source$ = new Subject<number>();
const refreshTrigger$ = new Subject<void>();
const result = subscribeSpyTo(
Expand All @@ -103,7 +116,7 @@ describe('rxStateful$', () => {

expect(result.getValues().length).toEqual(0);
});
it('keepValueOnRefresh: true - should return the current value when refreshTrigger$ emits', () => {
test('keepValueOnRefresh: true - should return the current value when refreshTrigger$ emits', () => {
const source$ = new Subject<number>();
const refreshTrigger$ = new Subject<void>();

Expand All @@ -117,7 +130,7 @@ describe('rxStateful$', () => {

expect(result.getValues()).toEqual([10, 10, 10]);
});
it('keepValueOnRefresh: false - should return the current value when refreshTrigger$ emits', () => {
test('keepValueOnRefresh: false - should return the current value when refreshTrigger$ emits', () => {
const source$ = new Subject<number>();
const refreshTrigger$ = new Subject<void>();

Expand All @@ -133,7 +146,7 @@ describe('rxStateful$', () => {
});
});
describe('hasValue$', () => {
it('should return false - true - true', () => {
test('should return false - true - true', () => {
const source$ = new Subject<number>();
const refreshTrigger$ = new Subject<void>();
const result = subscribeSpyTo(
Expand All @@ -145,7 +158,7 @@ describe('rxStateful$', () => {

expect(result.getValues()).toEqual([false, true]);
});
it('should return false - true - false - true', () => {
test('should return false - true - false - true', () => {
const source$ = new Subject<number>();
const refreshTrigger$ = new Subject<void>();
const result = subscribeSpyTo(
Expand All @@ -159,7 +172,7 @@ describe('rxStateful$', () => {
});
});
describe('isSuspense$', () => {
it('should return false - true - true', () => {
test('should return false - true - true', () => {
const source$ = new Subject<number>();
const refreshTrigger$ = new Subject<void>();
const result = subscribeSpyTo(
Expand All @@ -171,7 +184,7 @@ describe('rxStateful$', () => {

expect(result.getValues()).toEqual([true, false, true, false]);
});
it('should return false - true - false - true', () => {
test('should return false - true - false - true', () => {
const source$ = new Subject<number>();
const refreshTrigger$ = new Subject<void>();
const result = subscribeSpyTo(
Expand All @@ -185,7 +198,7 @@ describe('rxStateful$', () => {
});
});
describe('context$', () => {
it('should return the correct context', () => {
test('should return the correct context', () => {
const source$ = new Subject<number>();
const refreshTrigger$ = new Subject<void>();
const result = subscribeSpyTo(rxStateful$<number>(source$, { refreshTrigger$ }).context$);
Expand All @@ -197,7 +210,7 @@ describe('rxStateful$', () => {
});
});
describe('state$', () => {
it('should return the correct state', () => {
test('should return the correct state', () => {
const source$ = new Subject<any>();
const refreshTrigger$ = new Subject<void>();
const result = subscribeSpyTo(rxStateful$<number>(source$, { refreshTrigger$ }).state$);
Expand All @@ -218,7 +231,7 @@ describe('rxStateful$', () => {
});
});
describe('hasError$', () => {
it('should return false true false true', () => {
test('should return false true false true', () => {
const source$ = new Subject<Observable<any>>();
const refreshTrigger$ = new Subject<void>();
const result = subscribeSpyTo(rxStateful$<number>(source$.pipe(mergeAll()), { refreshTrigger$ }).hasError$);
Expand All @@ -232,7 +245,7 @@ describe('rxStateful$', () => {
});
describe('Configuration options', () => {
describe('keepErrorOnRefresh', () => {
it('should not keep the error on refresh when option is set to false', function () {
test('should not keep the error on refresh when option is set to false', function () {
const source$ = new Subject<Observable<any>>();
const refreshTrigger$ = new Subject<void>();
const result = subscribeSpyTo(rxStateful$<number>(source$.pipe(mergeAll()),{ refreshTrigger$, keepErrorOnRefresh: false }).state$);
Expand All @@ -249,7 +262,7 @@ describe('rxStateful$', () => {
]);
});

it('should keep the error on refresh when option is set to true', function () {
test('should keep the error on refresh when option is set to true', function () {
const source$ = new Subject<Observable<any>>();
const refreshTrigger$ = new Subject<void>();
const result = subscribeSpyTo(rxStateful$<number>(source$.pipe(mergeAll()),{ refreshTrigger$, keepErrorOnRefresh: true }).state$);
Expand All @@ -268,4 +281,48 @@ describe('rxStateful$', () => {
});
});
});
describe('Configuration', () => {
it('should use config from provider', () => {
TestBed.configureTestingModule({
providers: [provideRxStatefulConfig({ keepValueOnRefresh: true })],
}).runInInjectionContext(() => {
const source$ = new Subject<number>();
const refreshTrigger$ = new Subject<void>();

const result = subscribeSpyTo(
rxStateful$<number>(source$, { refreshTrigger$ }).value$
);
source$.next(10);

refreshTrigger$.next(void 0);
refreshTrigger$.next(void 0);

expect(result.getValues()).toEqual([10, 10, 10]);
})



});

it('should override config from provider', () => {
TestBed.configureTestingModule({
providers: [provideRxStatefulConfig({ keepValueOnRefresh: true })],
})

TestBed.runInInjectionContext(() => {
const source$ = new Subject<number>();
const refreshTrigger$ = new Subject<void>();

const result = subscribeSpyTo(
rxStateful$<number>(source$, { refreshTrigger$, keepValueOnRefresh: false }).value$
);
source$.next(10);

refreshTrigger$.next(void 0);
refreshTrigger$.next(void 0);

expect(result.getValues()).toEqual([10, null, 10, null, 10]);
})
});
})
});
Loading

0 comments on commit aba73ac

Please sign in to comment.