Skip to content

Commit

Permalink
feat(create-effect): add createEffect
Browse files Browse the repository at this point in the history
closes #27
  • Loading branch information
nartc committed Sep 14, 2023
1 parent 0dd119b commit 48c4b19
Show file tree
Hide file tree
Showing 23 changed files with 312 additions and 2 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ nx generate ngxtension:init
| `computed-from` | [README](./libs/ngxtension/computed-from/README.md) |
| `inject-destroy` | [README](./libs/ngxtension/inject-destroy/README.md) |
| `connect` | [README](./libs/ngxtension/connect/README.md) |
| `createEffect` | [README](./libs/ngxtension/create-effect/README.md) |

<!-- UTILITIES:END -->

Expand Down
1 change: 1 addition & 0 deletions docs/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export default defineConfig({
},
{ label: 'repeat', link: '/utilities/repeat' },
{ label: 'resize', link: '/utilities/resize' },
{ label: 'createEffect', link: '/utilities/create-effect' },
],
},
],
Expand Down
70 changes: 70 additions & 0 deletions docs/src/content/docs/utilities/create-effect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
title: createEffect
description: ngxtension/create-effect
---

`createEffect` is a standalone version of [NgRx ComponentStore Effect](https://ngrx.io/guide/component-store/effect)

:::tip[From ComponentStore documentation]

- Effects isolate side effects from components, allowing for more pure components that select state and trigger updates and/or effects in ComponentStore(s).
- Effects are Observables listening for the inputs and piping them through the "prescription".
- Those inputs can either be values or Observables of values.
- Effects perform tasks, which are synchronous or asynchronous.

:::

In short, `createEffect` creates a callable function that accepts some data (imperative) or some stream of data (declarative), or none at all.

```ts
import { createEffect } from 'ngxtension/create-effect';
```

## Usage

```ts
@Component({})
export class Some {
log = createEffect<number>(
pipe(
map((value) => value * 2),
tap(console.log.bind(console, 'double is -->'))
)
);

ngOnInit() {
// start the effect
this.log(interval(1000));
}
}
```

### Injection Context

`createEffect` accepts an optional `Injector` so we can call `createEffect` outside of an Injection Context.

```ts
@Component({})
export class Some {
// 1. setup an Input; we know that Input isn't resolved in constructor
@Input() multiplier = 2;

// 2. grab the Injector
private injector = inject(Injector);

ngOnInit() {
// 3. create log effect in ngOnInit; where Input is resolved
const log = createEffect<number>(
pipe(
map((value) => value * this.multiplier),
tap(console.log.bind(console, 'multiply is -->'))
),
// 4. pass in the injector
this.injector
);

// 5. start the effect
log(interval(1000));
}
}
```
14 changes: 12 additions & 2 deletions libs/local-plugin/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,24 @@
"files": ["*.json"],
"parser": "jsonc-eslint-parser",
"rules": {
"@nx/dependency-checks": "error"
"@nx/nx-plugin-checks": [
"error",
{
"ignoredDependencies": ["@nx/angular"]
}
]
}
},
{
"files": ["./package.json", "./generators.json"],
"parser": "jsonc-eslint-parser",
"rules": {
"@nx/nx-plugin-checks": "error"
"@nx/nx-plugin-checks": [
"error",
{
"ignoredDependencies": ["@nx/angular"]
}
]
}
}
]
Expand Down
5 changes: 5 additions & 0 deletions libs/local-plugin/generators.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
"factory": "./src/generators/convert-entry-point-to-project/generator",
"schema": "./src/generators/convert-entry-point-to-project/schema.json",
"description": "convert-entry-point-to-project generator"
},
"entry-point": {
"factory": "./src/generators/entry-point/generator",
"schema": "./src/generators/entry-point/schema.json",
"description": "entry-point generator"
}
}
}
11 changes: 11 additions & 0 deletions libs/local-plugin/src/generators/entry-point/generator.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
describe('entry-point generator', () => {
// let tree: Tree;
//
// beforeEach(() => {
// tree = createTreeWithEmptyWorkspace();
// });

it('should run successfully', async () => {
expect(true).toBeTruthy();
});
});
18 changes: 18 additions & 0 deletions libs/local-plugin/src/generators/entry-point/generator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { librarySecondaryEntryPointGenerator } from '@nx/angular/generators';
import type { GeneratorOptions as SecondaryEntryPointGeneratorOptions } from '@nx/angular/src/generators/library-secondary-entry-point/schema';
import { formatFiles, Tree } from '@nx/devkit';
import convertEntryPointToProjectGenerator from '../convert-entry-point-to-project/generator';

export async function entryPointGenerator(
tree: Tree,
options: SecondaryEntryPointGeneratorOptions
) {
await librarySecondaryEntryPointGenerator(tree, options);
await convertEntryPointToProjectGenerator(tree, {
name: options.name,
project: options.library,
});
await formatFiles(tree);
}

export default entryPointGenerator;
36 changes: 36 additions & 0 deletions libs/local-plugin/src/generators/entry-point/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"$schema": "http://json-schema.org/schema",
"$id": "LocalPluginEntryPoint",
"title": "Creates a secondary entry point for a library",
"description": "Creates a secondary entry point for an Angular publishable library.",
"type": "object",
"cli": "nx",
"properties": {
"name": {
"type": "string",
"description": "The name of the secondary entry point.",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "What name would you like to use for the secondary entry point?",
"pattern": "^[a-zA-Z].*$",
"x-priority": "important"
},
"library": {
"type": "string",
"description": "The name of the library to create the secondary entry point for.",
"x-prompt": "What library would you like to create the secondary entry point for?",
"pattern": "^[a-zA-Z].*$",
"x-dropdown": "projects",
"x-priority": "important"
},
"skipModule": {
"type": "boolean",
"description": "Skip generating a module for the secondary entry point.",
"default": false
}
},
"additionalProperties": false,
"required": ["name", "library"]
}
2 changes: 2 additions & 0 deletions libs/local-plugin/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as convertEntryPointToProjectGenerator } from './generators/convert-entry-point-to-project/generator';
export { default as entryPointGenerator } from './generators/entry-point/generator';
1 change: 1 addition & 0 deletions libs/ngxtension/assert-injector/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/ngxtension/jest.config.ts",
"testPathPattern": ["assert-injector"],
"passWithNoTests": true
},
"configurations": {
Expand Down
1 change: 1 addition & 0 deletions libs/ngxtension/computed-from/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/ngxtension/jest.config.ts",
"testPathPattern": ["computed-from"],
"passWithNoTests": true
},
"configurations": {
Expand Down
1 change: 1 addition & 0 deletions libs/ngxtension/connect/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/ngxtension/jest.config.ts",
"testPathPattern": ["connect"],
"passWithNoTests": true
},
"configurations": {
Expand Down
3 changes: 3 additions & 0 deletions libs/ngxtension/create-effect/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# ngxtension/create-effect

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

describe(createEffect.name, () => {
@Component({
standalone: true,
template: '',
})
class Foo {
count = 0;
log = createEffect<number>(tap(() => (this.count += 1)));

ngOnInit() {
this.log(interval(1000));
}
}

it('should run until component is destroyed', fakeAsync(() => {
const fixture = TestBed.createComponent(Foo);
const component = fixture.componentInstance;
fixture.detectChanges();
expect(component.count).toEqual(0);

tick(1000);
expect(component.count).toEqual(1);

tick(1000);
expect(component.count).toEqual(2);

fixture.destroy();
tick(1000);
expect(component.count).toEqual(2);
}));
});
68 changes: 68 additions & 0 deletions libs/ngxtension/create-effect/src/create-effect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import {
DestroyRef,
Injector,
inject,
runInInjectionContext,
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { assertInjector } from 'ngxtension/assert-injector';
import {
Observable,
Subject,
Subscription,
isObservable,
of,
retry,
} from 'rxjs';

/**
* This code is a copied `ComponentStore.effect()` method from NgRx and edited to:
* 1) be a standalone function;
* 2) use `takeUntilDestroyed()` with an injected `DestroyRef`;
* 3) resubscribe on errors.
*
* Credits: NgRx Team
* https://ngrx.io/
* Source: https://github.com/ngrx/platform/blob/main/modules/component-store/src/component-store.ts#L382
* Docs:
* https://ngrx.io/guide/component-store/effect#effect-method
*/
export function createEffect<
ProvidedType = void,
OriginType extends
| Observable<ProvidedType>
| unknown = Observable<ProvidedType>,
ObservableType = OriginType extends Observable<infer A> ? A : never,
ReturnType = ProvidedType | ObservableType extends void
? (
observableOrValue?: ObservableType | Observable<ObservableType>
) => Subscription
: (
observableOrValue: ObservableType | Observable<ObservableType>
) => Subscription
>(
generator: (origin$: OriginType) => Observable<unknown>,
injector?: Injector
): ReturnType {
injector = assertInjector(createEffect, injector);
return runInInjectionContext(injector, () => {
const destroyRef = inject(DestroyRef);
const origin$ = new Subject<ObservableType>();
generator(origin$ as OriginType)
.pipe(retry(), takeUntilDestroyed(destroyRef))
.subscribe();

return ((
observableOrValue?: ObservableType | Observable<ObservableType>
): Subscription => {
const observable$ = isObservable(observableOrValue)
? observableOrValue.pipe(retry())
: of(observableOrValue);
return observable$
.pipe(takeUntilDestroyed(destroyRef))
.subscribe((value) => {
origin$.next(value as ObservableType);
});
}) as unknown as ReturnType;
});
}
1 change: 1 addition & 0 deletions libs/ngxtension/create-effect/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './create-effect';
1 change: 1 addition & 0 deletions libs/ngxtension/create-injection-token/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/ngxtension/jest.config.ts",
"testPathPattern": ["create-injection-token"],
"passWithNoTests": true
},
"configurations": {
Expand Down
1 change: 1 addition & 0 deletions libs/ngxtension/inject-destroy/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/ngxtension/jest.config.ts",
"testPathPattern": ["inject-destroy"],
"passWithNoTests": true
},
"configurations": {
Expand Down
1 change: 1 addition & 0 deletions libs/ngxtension/repeat/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/ngxtension/jest.config.ts",
"testPathPattern": ["repeat"],
"passWithNoTests": true
},
"configurations": {
Expand Down
Loading

0 comments on commit 48c4b19

Please sign in to comment.