Skip to content

Commit

Permalink
feat(create-repeat): implement the function, docs and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
lgarcia committed May 21, 2024
1 parent 5baf2fc commit bebeee9
Show file tree
Hide file tree
Showing 10 changed files with 10,706 additions and 8,865 deletions.
4 changes: 3 additions & 1 deletion docs/src/content/contributors/lucas-garcia.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
{
"name": "Lucas Garcia"
"name": "Lucas Garcia",
"twitter": "https://x.com/LcsGa_",
"github": "https://github.com/LcsGa"
}
62 changes: 62 additions & 0 deletions docs/src/content/docs/utilities/Operators/create-repeat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
title: createRepeat
description: ngxtension/create-repeat
entryPoint: create-repeat
badge: stable
contributors: ['lucas-garcia']
---

## Import

```ts
import { createRepeat } from 'ngxtension/create-repeat';
```

## Usage

Create an RxJS `repeat` operator with an `emit` method on it, to notify when the source should be repeated.

```ts
@Component({
...,
template: `
...
<button (click)="repeat.emit()">Repeat</button>
`
})
export class SomeComponent {
readonly repeat = createRepeat();

// Will log 'hello' directly, then each time the 'Repeat' button gets clicked
readonly #sayHello = rxEffect(of('hello').pipe(this.repeat()), console.log);
}
```

## API

### `createRepeat` overloads

#### Overload 1

| arguments | type | description |
| ------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `globalCount` | `number` | Optional. Default is `undefined`.<br>The number of times (applied globally) the source Observable items are repeated (a count of 0 will yield an empty Observable) |
| `destroyRef` | `DestroyRef` | Optional. Default is `undefined`.<br>The `DestroyRef` to pass when `createRepeat` is used outside of an injection context. |

#### Overload 2

| arguments | type | description |
| ------------ | ------------ | -------------------------------------------------------------------------------------------------------------------------- |
| `destroyRef` | `DestroyRef` | Optional. Default is `undefined`.<br>The `DestroyRef` to pass when `createRepeat` is used outside of an injection context. |

### Returned `repeat` operator

| arguments | type | description |
| --------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `count` | `number` | Optional. Default is `undefined`.<br>The number of times the source Observable items are repeated (a count of 0 will yield an empty Observable) |

## See also

- [`rxEffect`](https://ngxtension.netlify.app/utilities/operators/rx-effect)
- RxJS [`repeat`](https://rxjs.dev/api/index/function/repeat)
3 changes: 3 additions & 0 deletions libs/ngxtension/create-repeat/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# ngxtension/create-repeat

Secondary entry point of `ngxtension`. It can be used by importing from `ngxtension/create-repeat`.
5 changes: 5 additions & 0 deletions libs/ngxtension/create-repeat/ng-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"lib": {
"entryFile": "src/index.ts"
}
}
27 changes: 27 additions & 0 deletions libs/ngxtension/create-repeat/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "ngxtension/create-repeat",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"projectType": "library",
"sourceRoot": "libs/ngxtension/create-repeat/src",
"targets": {
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/ngxtension/jest.config.ts",
"testPathPattern": ["create-repeat"],
"passWithNoTests": true
},
"configurations": {
"ci": {
"ci": true,
"codeCoverage": true
}
}
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"]
}
}
}
70 changes: 70 additions & 0 deletions libs/ngxtension/create-repeat/src/create-repeat.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { TestBed } from '@angular/core/testing';
import { of } from 'rxjs';
import { createRepeat } from './create-repeat';

type Tests = {
expectation: string;
input: {
globalCount?: number;
emitCount: number;
count?: number;
};
expected: number[];
}[];

describe('createRepeat', () => {
const sourceValue = 123;

const tests: Tests = [
{
expectation: 'should repeat the source stream once after repeat emits',
input: { emitCount: 1 },
expected: [sourceValue, sourceValue],
},
{
expectation:
'should emit the source stream value only once with the global count set to 1',
input: { globalCount: 1, emitCount: 2 },
expected: [sourceValue],
},
{
expectation:
'should emit the source stream value twice with the global count set to 2',
input: { globalCount: 2, emitCount: 2 },
expected: [sourceValue, sourceValue],
},
{
expectation:
'should emit the source stream value twice with the count set to 2',
input: { count: 2, emitCount: 2 },
expected: [sourceValue, sourceValue],
},
{
expectation:
'should emit the source stream value twice with the count set to 2, even if the global count is set to 1',
input: { globalCount: 1, count: 2, emitCount: 2 },
expected: [sourceValue, sourceValue],
},
];

tests.forEach(
({ expectation, input: { globalCount, count, emitCount }, expected }) =>
it(expectation, () =>
TestBed.runInInjectionContext(() => {
const repeat = createRepeat(globalCount);
const notifs: number[] = [];

of(sourceValue)
.pipe(repeat(count))
.subscribe((n) => notifs.push(n));

for (let i = 0; i < emitCount; i++) repeat.emit();

expect(notifs).toEqual(expected);
}),
),
);

it('should throw an error when it is invoked outside an injection context', () =>
expect(createRepeat).toThrow());
});
48 changes: 48 additions & 0 deletions libs/ngxtension/create-repeat/src/create-repeat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { assertInInjectionContext, DestroyRef, inject } from '@angular/core';
import { repeat, Subject, type MonoTypeOperatorFunction } from 'rxjs';

type CreateRepeat = (<T>(count?: number) => MonoTypeOperatorFunction<T>) & {
emit: () => void;
};

export function createRepeat(destroyRef?: DestroyRef): CreateRepeat;
export function createRepeat(
generalCount?: number,
destroyRef?: DestroyRef,
): CreateRepeat;
export function createRepeat(
generalCountOrDestroyRef?: number | DestroyRef,
destroyRef?: DestroyRef,
) {
const [generalCount, _destroyRef] = parseArgs(
generalCountOrDestroyRef,
destroyRef,
);

const repeat$ = new Subject<void>();

_destroyRef.onDestroy(() => repeat$.complete());

const repeatFn = <T>(count?: number) =>
repeat<T>({ count: count ?? generalCount, delay: () => repeat$ });

repeatFn.emit = () => repeat$.next();

return repeatFn;
}

function parseArgs(
generalCountOrDestroyRef?: number | DestroyRef,
destroyRef?: DestroyRef,
) {
const isGeneralCount = typeof generalCountOrDestroyRef === 'number';

const generalCount = isGeneralCount ? generalCountOrDestroyRef : undefined;

destroyRef ??= !isGeneralCount ? generalCountOrDestroyRef : undefined;

if (!destroyRef) assertInInjectionContext(createRepeat);
destroyRef ??= inject(DestroyRef);

return [generalCount, destroyRef] as const;
}
1 change: 1 addition & 0 deletions libs/ngxtension/create-repeat/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './create-repeat';
Loading

0 comments on commit bebeee9

Please sign in to comment.