Skip to content

Commit

Permalink
feat: add toLazySignal() (#166)
Browse files Browse the repository at this point in the history
* feat: add toLazySignal()

Function `toLazySignal()` is a proxy function that will call the original
`toSignal()` function when the returned signal is read for the first time.

* feat: add toLazySignal()

Function `toLazySignal()` is a proxy function that will call the original
`toSignal()` function when the returned signal is read for the first time.

* feat: add toLazySignal()

Function `toLazySignal()` is a proxy function that will call the original
`toSignal()` function when the returned signal is read for the first time.
  • Loading branch information
e-oz committed Nov 21, 2023
1 parent 9cae021 commit 3659fbe
Show file tree
Hide file tree
Showing 8 changed files with 157 additions and 4 deletions.
8 changes: 4 additions & 4 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"singleQuote": true,
"useTabs": true,
"htmlWhitespaceSensitivity": "ignore",
"plugins": ["prettier-plugin-organize-imports"]
"singleQuote": true,
"useTabs": true,
"htmlWhitespaceSensitivity": "ignore",
"plugins": ["prettier-plugin-organize-imports"]
}
3 changes: 3 additions & 0 deletions libs/ngxtension/to-lazy-signal/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# ngxtension/to-lazy-signal

Secondary entry point of `ngxtension`. It can be used by importing from `ngxtension/to-lazy-signal`.
5 changes: 5 additions & 0 deletions libs/ngxtension/to-lazy-signal/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/to-lazy-signal/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "ngxtension/to-lazy-signal",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"projectType": "library",
"sourceRoot": "libs/ngxtension/to-lazy-signal/src",
"targets": {
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/ngxtension/jest.config.ts",
"testPathPattern": ["to-lazy-signal"],
"passWithNoTests": true
},
"configurations": {
"ci": {
"ci": true,
"codeCoverage": true
}
}
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": [
"libs/ngxtension/to-lazy-signal/**/*.ts",
"libs/ngxtension/to-lazy-signal/**/*.html"
]
}
}
}
}
1 change: 1 addition & 0 deletions libs/ngxtension/to-lazy-signal/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './to-lazy-signal';
59 changes: 59 additions & 0 deletions libs/ngxtension/to-lazy-signal/src/to-lazy-signal.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Component, signal, type Signal } from '@angular/core';
import { fakeAsync, TestBed } from '@angular/core/testing';
import { createEffect } from 'ngxtension/create-effect';
import { Observable } from 'rxjs';
import { toLazySignal } from './to-lazy-signal';

/**
* https://stackblitz.com/edit/stackblitz-starters-wxbnsh?devToolsHeight=33&file=src%2Fmain.ts
*/
describe(createEffect.name, () => {
let subscribed = 0;

@Component({
standalone: true,
template: `
@if ($display1()) {
<div>{{ $s() }}</div>
} @if ($display2()) {
<div>{{ $s() }}</div>
}
`,
})
class Foo {
public readonly $display1 = signal<boolean>(false);
public readonly $display2 = signal<boolean>(false);
protected readonly $s: Signal<string | undefined>;

constructor() {
const obs = new Observable<string | undefined>((subscriber) => {
subscribed++;
subscriber.next('test');
});

this.$s = toLazySignal<string | undefined>(obs);
}
}

it('should subscribe lazily and only once', fakeAsync(() => {
const fixture = TestBed.createComponent(Foo);
const component = fixture.componentInstance;
fixture.detectChanges();
expect(subscribed).toEqual(0);

component.$display1.set(true);
fixture.detectChanges();

expect(subscribed).toEqual(1);

component.$display1.set(false);
fixture.detectChanges();

expect(subscribed).toEqual(1);

component.$display2.set(true);
fixture.detectChanges();

expect(subscribed).toEqual(1);
}));
});
49 changes: 49 additions & 0 deletions libs/ngxtension/to-lazy-signal/src/to-lazy-signal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { computed, untracked, type Signal } from '@angular/core';
import { toSignal, type ToSignalOptions } from '@angular/core/rxjs-interop';
import { assertInjector } from 'ngxtension/assert-injector';
import type { Observable, Subscribable } from 'rxjs';

type ReturnType<T, U> = (T | U) | (T | undefined) | (T | null) | T;

export function toLazySignal<T>(
source: Observable<T> | Subscribable<T>
): Signal<T | undefined>;

export function toLazySignal<T>(
source: Observable<T> | Subscribable<T>,
options: ToSignalOptions & { initialValue?: undefined; requireSync?: false }
): Signal<T | undefined>;

export function toLazySignal<T>(
source: Observable<T> | Subscribable<T>,
options: ToSignalOptions & { initialValue?: null; requireSync?: false }
): Signal<T | null>;

export function toLazySignal<T>(
source: Observable<T> | Subscribable<T>,
options: ToSignalOptions & { initialValue?: undefined; requireSync: true }
): Signal<T>;

export function toLazySignal<T, const U extends T>(
source: Observable<T> | Subscribable<T>,
options: ToSignalOptions & { initialValue: U; requireSync?: false }
): Signal<T | U>;

/**
* Function `toLazySignal()` is a proxy function that will call the original
* `toSignal()` function when the returned signal is read for the first time.
*/
export function toLazySignal<T, U = undefined>(
source: Observable<T> | Subscribable<T>,
options?: ToSignalOptions & { initialValue?: U }
): Signal<ReturnType<T, U>> {
const injector = assertInjector(toLazySignal, options?.injector);
let s: Signal<ReturnType<T, U>>;

return computed<ReturnType<T, U>>(() => {
if (!s) {
s = untracked(() => toSignal(source, { ...options, injector } as any));
}
return s();
});
}
3 changes: 3 additions & 0 deletions tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@
"ngxtension/singleton-proxy": [
"libs/ngxtension/singleton-proxy/src/index.ts"
],
"ngxtension/to-lazy-signal": [
"libs/ngxtension/to-lazy-signal/src/index.ts"
],
"ngxtension/trackby-id-prop": [
"libs/ngxtension/trackby-id-prop/src/index.ts"
],
Expand Down

0 comments on commit 3659fbe

Please sign in to comment.