Skip to content

Commit

Permalink
fix(signals): allow withCalls generated method to receive Signal and …
Browse files Browse the repository at this point in the history
…Observables as params

The methods generated by withCalls are rxMethods, this PR enables the generated methods to also
receive Signal or Observable with the params of the method

Fixes #68
  • Loading branch information
Gabriel Guerrero authored and gabrielguerrero committed May 9, 2024
1 parent 212e7b0 commit bf3d6f9
Show file tree
Hide file tree
Showing 11 changed files with 193 additions and 18 deletions.
2 changes: 1 addition & 1 deletion apps/example-app/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
"maximumError": "2mb"
},
{
"type": "anyComponentStyle",
Expand Down
3 changes: 1 addition & 2 deletions apps/example-app/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import { Component } from '@angular/core';

@Component({
selector: 'ngrx-traits-root',
template: `<router-outlet></router-outlet>
<a [routerLink]="'/product-list'"></a> `,
template: `<router-outlet />`,
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
Expand Down
26 changes: 19 additions & 7 deletions apps/example-app/src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import {
MAT_FORM_FIELD_DEFAULT_OPTIONS,
MatFormFieldDefaultOptions,
} from '@angular/material/form-field';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule } from '@angular/router';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ExamplesModule } from './examples/examples.module';
import { StoreModule } from '@ngrx/store';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';

import { environment } from '../environments/environment';
import { HttpClientModule } from '@angular/common/http';
import { AppComponent } from './app.component';
import { ExamplesModule } from './examples/examples.module';
import './examples/services/mock-backend';

@NgModule({
Expand All @@ -29,7 +33,15 @@ import './examples/services/mock-backend';
EffectsModule.forRoot(),
RouterModule,
],
providers: [],
providers: [
{
provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
useValue: {
appearance: 'outline',
subscriptSizing: 'dynamic',
} satisfies MatFormFieldDefaultOptions,
},
],
bootstrap: [AppComponent],
})
export class AppModule {}
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ import { rebuildFormArray } from '../../utils/form-utils';
mat-cell
*matCellDef="let row; let index = index"
[formGroup]="$any(controls.at(index))"
class="!p-2"
>
<mat-form-field>
<input
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import { ProductBasketComponent } from '../../../../components/product-basket/pr
import { ProductDetailComponent } from '../../../../components/product-detail/product-detail.component';
import { Product } from '../../../../models';
import { ProductsShopStore } from '../../products-shop.store';
import { SmartProductDetailComponent } from '../smart-product-detail/smart-product-detail.component';

@Component({
selector: 'product-basket-tab',
template: `
<div gdColumns="700px 500px" style="gap: 10px">
<div class="grid gap-4 m-8">
<mat-card>
<mat-card-header>
<mat-card-title>Product Basket</mat-card-title>
Expand All @@ -30,6 +31,8 @@ import { ProductsShopStore } from '../../products-shop.store';
(toggleAllSelectForRemove)="
store.toggleSelectAllOrderItemsEntities()
"
[selectedProduct]="store.productsEntitySelected()"
(selectProduct)="store.selectProductsEntity($event)"
(sort)="sortBasket($event)"
></product-basket>
</mat-card-content>
Expand Down Expand Up @@ -59,6 +62,11 @@ import { ProductsShopStore } from '../../products-shop.store';
</button>
</mat-card-actions>
</mat-card>
@if (store.productsEntitySelected()) {
<smart-product-detail
[productId]="store.productsEntitySelected()!.id"
/>
}
</div>
`,
styles: [
Expand All @@ -80,6 +88,7 @@ import { ProductsShopStore } from '../../products-shop.store';
MatButtonModule,
ProductDetailComponent,
AsyncPipe,
SmartProductDetailComponent,
],
})
export class ProductBasketTabComponent {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@ import { ProductsShopStore } from '../../products-shop.store';
@Component({
selector: 'product-shop-tab',
template: `
<div class="m-8 grid sm:grid-cols-2" style="gap: 10px">
<div class="m-8 grid sm:grid-cols-2 gap-4">
<mat-card>
<mat-card-header>
<mat-card-title>Product List</mat-card-title>
</mat-card-header>
<mat-card-content>
<product-search-form
class="m-2"
[searchProduct]="store.productsFilter()"
(searchProductChange)="filter($event)"
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { CurrencyPipe } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
inject,
input,
OnInit,
} from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { withCalls } from '@ngrx-traits/signals';
import { signalStore } from '@ngrx/signals';

import { ProductService } from '../../../../services/product.service';

const ProductDetailStore = signalStore(
withCalls(() => ({
loadProductDetail: (id: string) =>
inject(ProductService).getProductDetail(id),
})),
);
@Component({
selector: 'smart-product-detail',
styles: [
`
mat-spinner {
margin: 10px auto;
}
`,
],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [MatCardModule, MatProgressSpinnerModule, CurrencyPipe],
providers: [ProductDetailStore],
template: `
@if (store.isLoadProductDetailLoaded()) {
<mat-card>
<mat-card-header>
<mat-card-title>{{
store.loadProductDetailResult()?.name
}}</mat-card-title>
<mat-card-subtitle
>Price: £{{ store.loadProductDetailResult()?.price | currency }}
Released:
{{
store.loadProductDetailResult()?.releaseDate
}}</mat-card-subtitle
>
</mat-card-header>
<img
mat-card-image
src="/{{ store.loadProductDetailResult()?.image }}"
/>
<mat-card-content>
<p>{{ store.loadProductDetailResult()?.description }}</p>
</mat-card-content>
</mat-card>
} @else if (store.isLoadProductDetailLoading()) {
<mat-spinner />
}
`,
})
export class SmartProductDetailComponent implements OnInit {
store = inject(ProductDetailStore);
productId = input.required<string>();
ngOnInit() {
// 👇 loadProductDetail is an rxMethod it can receive a signal reference
this.store.loadProductDetail(this.productId);
}
// other ways

// loadProductDetail = effect(
// () => {
// this.store.loadProductDetail(this.productId());
// },
// { allowSignalWrites: true },
// );

// constructor() {
// effect(
// () => {
// this.store.loadProductDetail(this.productId());
// },
// { allowSignalWrites: true },
// );
// }
}
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ export const ProductsShopStore = signalStore(
withCalls(({ orderItemsEntities }, snackBar = inject(MatSnackBar)) => ({
loadProductDetail: ({ id }: { id: string }) =>
inject(ProductService).getProductDetail(id),

checkout: typedCallConfig({
call: () =>
inject(OrderService).checkout(
Expand Down
2 changes: 1 addition & 1 deletion apps/example-app/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body class="mat-app-background">
<ngrx-traits-root></ngrx-traits-root>
<ngrx-traits-root />
</body>
</html>
44 changes: 43 additions & 1 deletion libs/ngrx-traits/signals/src/lib/with-calls/with-calls.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { signal } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { patchState, signalStore, withState } from '@ngrx/signals';
import { Subject, tap, throwError } from 'rxjs';
import { BehaviorSubject, Subject, tap, throwError } from 'rxjs';

import { typedCallConfig, withCalls } from '../index';

Expand Down Expand Up @@ -45,6 +46,47 @@ describe('withCalls', () => {
});
});

it('passing a signal should call when signal value changes ', async () => {
TestBed.runInInjectionContext(() => {
const store = new Store();
expect(store.isTestCallLoading()).toBeFalsy();
const param = signal({ ok: true });
store.testCall(param);
TestBed.flushEffects();
expect(store.isTestCallLoading()).toBeTruthy();
apiResponse.next('test');
expect(store.isTestCallLoaded()).toBeTruthy();
expect(store.testCallResult()).toBe('test');

param.set({ ok: true });
TestBed.flushEffects();
expect(store.isTestCallLoading()).toBeTruthy();
apiResponse.next('test2');
expect(store.isTestCallLoaded()).toBeTruthy();
expect(store.testCallResult()).toBe('test2');
});
});

it('passing a observable should call when value changes ', async () => {
TestBed.runInInjectionContext(() => {
const store = new Store();
expect(store.isTestCallLoading()).toBeFalsy();
const param = new BehaviorSubject({ ok: true });

store.testCall(param);
expect(store.isTestCallLoading()).toBeTruthy();
apiResponse.next('test');
expect(store.isTestCallLoaded()).toBeTruthy();
expect(store.testCallResult()).toBe('test');

param.next({ ok: true });
expect(store.isTestCallLoading()).toBeTruthy();
apiResponse.next('test2');
expect(store.isTestCallLoaded()).toBeTruthy();
expect(store.testCallResult()).toBe('test2');
});
});

describe('when using a CallConfig', () => {
it('Successful call should set status to loading and loaded ', async () => {
TestBed.runInInjectionContext(() => {
Expand Down
31 changes: 27 additions & 4 deletions libs/ngrx-traits/signals/src/lib/with-calls/with-calls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
first,
from,
map,
Observable,
of,
pipe,
switchMap,
Expand All @@ -47,8 +48,12 @@ import {
import { getWithCallKeys } from './with-calls.util';

/**
* Generates necessary state, computed and methods to track the progress of the call
* and store the result of the call
* Generates necessary state, computed and methods to track the progress of the
* call and store the result of the call. The generated methods are rxMethods with
* the same name as the original call, which accepts either the original parameters
* or a Signal or Observable of the same type as the original parameters. The Signal
* or Observable type will be the type of the first param if it only has one parameter
* or an array with the same type as the parameters.
* @param {callsFactory} callsFactory - a factory function that receives the store and returns an object of type {Record<string, Call | CallConfig>} with the calls to be made
*
* @example
Expand Down Expand Up @@ -86,7 +91,7 @@ import { getWithCallKeys } from './with-calls.util';
* store.isCheckoutLoaded // boolean
* store.checkoutError // string | null
* // generates the following methods
* store.loadProductDetail // ({id: string}) => void
* store.loadProductDetail // ({id: string} | Signal<{id: string}> | Observable<{id: string}>) => void
* store.checkout // () => void
*
*/
Expand Down Expand Up @@ -116,7 +121,25 @@ export function withCalls<
};
signals: NamedCallStatusComputed<keyof Calls & string>;
methods: {
[K in keyof Calls]: (...arg: ExtractCallParams<Calls[K]>) => void;
[K in keyof Calls]: ExtractCallParams<Calls[K]> extends []
? { (): void }
: ExtractCallParams<Calls[K]> extends [any]
? {
(
param:
| ExtractCallParams<Calls[K]>[0]
| Observable<ExtractCallParams<Calls[K]>[0]>
| Signal<ExtractCallParams<Calls[K]>[0]>,
): void;
}
: {
(...params: ExtractCallParams<Calls[K]>): void;
(
param:
| Observable<readonly [...ExtractCallParams<Calls[K]>]>
| Signal<readonly [...ExtractCallParams<Calls[K]>]>,
): void;
};
};
}
> {
Expand Down

0 comments on commit bf3d6f9

Please sign in to comment.