Skip to content

Commit

Permalink
feat: add effectOnceIf helper function (#419)
Browse files Browse the repository at this point in the history
* feat: add effectOnceIf helper function

* docs: effectOnceIf

---------

Co-authored-by: Chau Tran <[email protected]>
  • Loading branch information
lorenzodianni and nartc authored Jul 15, 2024
1 parent 70611b8 commit bd56a88
Show file tree
Hide file tree
Showing 9 changed files with 180 additions and 0 deletions.
6 changes: 6 additions & 0 deletions docs/src/content/contributors/lorenzo-dianni.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "Lorenzo D'Ianni",
"twitter": "https://x.com/lorenzodianni",
"linkedin": "https://www.linkedin.com/in/lorenzo-d-ianni-3990bba6/",
"github": "https://github.com/lorenzodianni"
}
38 changes: 38 additions & 0 deletions docs/src/content/docs/utilities/Signals/effect-once-if.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
title: effectOnceIf
description: ngxtension/effect-once-if
entryPoint: effect-once-if
badge: stable
contributors: ['lorenzo-dianni']
---

`effectOnceIf` is a helper function that allows you to create an effect that will be executed only once if a certain condition occurs.

## Usage

```ts
@Component({})
class Example {
count = signal(0);

effectOnceIfRef = effectOnceIf(
// condition function: if it returns a truly value, the execution function will run
() => this.count() > 3,
// execution function: will run only once
(valueReturnedFromCondition, onCleanup) => {
console.log(
`triggered with value returned: ${valueReturnedFromCondition}`,
);
onCleanup(() => console.log('cleanup'));
},
);
}

// example.count.set(1);
// -> nothing happens
// example.count.set(4);
// -> log: triggered with value returned: true
// -> log: cleanup
// example.count.set(6);
// -> nothing happens
```
3 changes: 3 additions & 0 deletions libs/ngxtension/effect-once-if/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# ngxtension/effect-once-if

Secondary entry point of `ngxtension`. It can be used by importing from `ngxtension/effect-once-if`.
5 changes: 5 additions & 0 deletions libs/ngxtension/effect-once-if/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/effect-once-if/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "ngxtension/effect-once-if",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"projectType": "library",
"sourceRoot": "libs/ngxtension/effect-once-if/src",
"targets": {
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/ngxtension/jest.config.ts",
"testPathPattern": ["effect-once-if"],
"passWithNoTests": true
},
"configurations": {
"ci": {
"ci": true,
"codeCoverage": true
}
}
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"]
}
}
}
63 changes: 63 additions & 0 deletions libs/ngxtension/effect-once-if/src/effect-once-if.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Component, signal } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { effectOnceIf } from './effect-once-if';

function createTestComponent(triggerValue: number) {
const log: string[] = [];
const logCleanup: string[] = [];

@Component({ standalone: true, template: '' })
class Example {
count = signal(0);

ref = effectOnceIf(
() => this.count() === triggerValue,
(value, onCleanup) => {
log.push(`received ${triggerValue}: ${value}`);
onCleanup(() => {
logCleanup.push(`cleaning effect with condition ${triggerValue}`);
});
},
);
}

return { component: Example, log, logCleanup };
}

describe(effectOnceIf.name, () => {
it('should run effect once and cleanup', () => {
const test = createTestComponent(2);
const fixture = TestBed.createComponent(test.component);
fixture.detectChanges();
expect(test.log).toEqual([]);
expect(test.logCleanup).toEqual([]);

fixture.componentInstance.count.set(1);
fixture.detectChanges();
expect(test.log).toEqual([]);
expect(test.logCleanup).toEqual([]);

fixture.componentInstance.count.set(2);
fixture.detectChanges();
expect(test.log).toEqual(['received 2: true']);
expect(test.logCleanup).toEqual(['cleaning effect with condition 2']);

fixture.componentInstance.count.set(3);
fixture.detectChanges();
expect(test.log).toEqual(['received 2: true']);
expect(test.logCleanup).toEqual(['cleaning effect with condition 2']);
});

it('should run effect once and cleanup on init', () => {
const test = createTestComponent(0);
const fixture = TestBed.createComponent(test.component);
fixture.detectChanges();
expect(test.log).toEqual(['received 0: true']);
expect(test.logCleanup).toEqual(['cleaning effect with condition 0']);

fixture.componentInstance.count.set(1);
fixture.detectChanges();
expect(test.log).toEqual(['received 0: true']);
expect(test.logCleanup).toEqual(['cleaning effect with condition 0']);
});
});
34 changes: 34 additions & 0 deletions libs/ngxtension/effect-once-if/src/effect-once-if.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {
CreateEffectOptions,
effect,
EffectCleanupRegisterFn,
EffectRef,
runInInjectionContext,
untracked,
} from '@angular/core';
import { assertInjector } from 'ngxtension/assert-injector';

export function effectOnceIf<T = any>(
condition: () => T,
execution: (
valueFromCondition: NonNullable<T>,
onCleanup: EffectCleanupRegisterFn,
) => void,
options?: Omit<CreateEffectOptions, 'allowSignalWrites' | 'manualCleanup'>,
): EffectRef {
const assertedInjector = assertInjector(effectOnceIf, options?.injector);
return runInInjectionContext(assertedInjector, () => {
const effectRef = effect((onCleanup) => {
const hasCondition = condition();
if (hasCondition) {
untracked(() => execution(hasCondition, onCleanup));
effectRef.destroy();
}
}, options);
return effectRef;
});
}

export type EffectOnceIfConditionFn<T> = Parameters<typeof effectOnceIf<T>>[0];
export type EffectOnceIfExecutionFn<T> = Parameters<typeof effectOnceIf<T>>[1];
export type EffectOnceIfOptions<T> = Parameters<typeof effectOnceIf<T>>[2];
1 change: 1 addition & 0 deletions libs/ngxtension/effect-once-if/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './effect-once-if';
3 changes: 3 additions & 0 deletions tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@
"libs/ngxtension/derived-async/src/index.ts"
],
"ngxtension/derived-from": ["libs/ngxtension/derived-from/src/index.ts"],
"ngxtension/effect-once-if": [
"libs/ngxtension/effect-once-if/src/index.ts"
],
"ngxtension/filter-array": ["libs/ngxtension/filter-array/src/index.ts"],
"ngxtension/filter-nil": ["libs/ngxtension/filter-nil/src/index.ts"],
"ngxtension/gestures": ["libs/ngxtension/gestures/src/index.ts"],
Expand Down

0 comments on commit bd56a88

Please sign in to comment.